friends list UI
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
use crate::db::{self, AppDatabase};
|
use crate::db::{self, AppDatabase};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
use tauri::State;
|
use tauri::{AppHandle, State};
|
||||||
|
use tauri_specta::Event;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, sqlx::FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, sqlx::FromRow)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -10,9 +11,32 @@ pub struct Friend {
|
|||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FriendsChanged {
|
||||||
|
pub friends: Vec<Friend>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn all(database: &AppDatabase) -> Result<Vec<Friend>, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Friend>(
|
||||||
|
"SELECT id, display_name FROM friends ORDER BY display_name COLLATE NOCASE, id",
|
||||||
|
)
|
||||||
|
.fetch_all(database.pool())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn emit_changed(handle: &AppHandle, database: &AppDatabase) -> Result<(), String> {
|
||||||
|
FriendsChanged {
|
||||||
|
friends: all(database).await.map_err(db::command_error)?,
|
||||||
|
}
|
||||||
|
.emit(handle)
|
||||||
|
.map_err(db::command_error)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn create_friend(
|
pub async fn create_friend(
|
||||||
|
handle: AppHandle,
|
||||||
database: State<'_, AppDatabase>,
|
database: State<'_, AppDatabase>,
|
||||||
friend: Friend,
|
friend: Friend,
|
||||||
) -> Result<Friend, String> {
|
) -> Result<Friend, String> {
|
||||||
@@ -23,18 +47,26 @@ pub async fn create_friend(
|
|||||||
.await
|
.await
|
||||||
.map_err(db::command_error)?;
|
.map_err(db::command_error)?;
|
||||||
|
|
||||||
|
emit_changed(&handle, &database).await?;
|
||||||
|
|
||||||
Ok(friend)
|
Ok(friend)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn list_friends(database: State<'_, AppDatabase>) -> Result<Vec<Friend>, String> {
|
pub async fn list_friends(
|
||||||
sqlx::query_as::<_, Friend>(
|
handle: AppHandle,
|
||||||
"SELECT id, display_name FROM friends ORDER BY display_name COLLATE NOCASE, id",
|
database: State<'_, AppDatabase>,
|
||||||
)
|
) -> Result<Vec<Friend>, String> {
|
||||||
.fetch_all(database.pool())
|
let friends = all(&database).await.map_err(db::command_error)?;
|
||||||
.await
|
|
||||||
.map_err(db::command_error)
|
FriendsChanged {
|
||||||
|
friends: friends.clone(),
|
||||||
|
}
|
||||||
|
.emit(&handle)
|
||||||
|
.map_err(db::command_error)?;
|
||||||
|
|
||||||
|
Ok(friends)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -52,12 +84,22 @@ pub async fn get_friend(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn delete_friend(database: State<'_, AppDatabase>, id: String) -> Result<bool, String> {
|
pub async fn delete_friend(
|
||||||
|
handle: AppHandle,
|
||||||
|
database: State<'_, AppDatabase>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<bool, String> {
|
||||||
let result = sqlx::query("DELETE FROM friends WHERE id = ?1")
|
let result = sqlx::query("DELETE FROM friends WHERE id = ?1")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(database.pool())
|
.execute(database.pool())
|
||||||
.await
|
.await
|
||||||
.map_err(db::command_error)?;
|
.map_err(db::command_error)?;
|
||||||
|
|
||||||
Ok(result.rows_affected() > 0)
|
let changed = result.rows_affected() > 0;
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
emit_changed(&handle, &database).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(changed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ pub fn run() {
|
|||||||
crate::friends::get_friend,
|
crate::friends::get_friend,
|
||||||
crate::friends::delete_friend
|
crate::friends::delete_friend
|
||||||
])
|
])
|
||||||
.events(tauri_specta::collect_events![]);
|
.events(tauri_specta::collect_events![
|
||||||
|
crate::friends::FriendsChanged
|
||||||
|
]);
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
specta_builder
|
specta_builder
|
||||||
@@ -36,7 +38,8 @@ pub fn run() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.invoke_handler(specta_builder.invoke_handler())
|
.invoke_handler(specta_builder.invoke_handler())
|
||||||
.setup(|app| {
|
.setup(move |app| {
|
||||||
|
specta_builder.mount_events(app);
|
||||||
tauri::async_runtime::block_on(launch_app(app))?;
|
tauri::async_runtime::block_on(launch_app(app))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use tauri::{AppHandle, Manager, WebviewUrl};
|
||||||
|
|
||||||
|
const WINDOW_LABEL: &str = "main";
|
||||||
|
|
||||||
|
pub fn init(handle: &AppHandle) {
|
||||||
|
if let Some(window) = handle.get_webview_window(WINDOW_LABEL) {
|
||||||
|
if !window.is_visible().unwrap() {
|
||||||
|
window.center().unwrap();
|
||||||
|
window.show().unwrap();
|
||||||
|
};
|
||||||
|
window.set_focus().unwrap();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let builder =
|
||||||
|
tauri::WebviewWindowBuilder::new(handle, WINDOW_LABEL, WebviewUrl::App("/".into()))
|
||||||
|
.title("Wyd")
|
||||||
|
.inner_size(800.0, 600.0)
|
||||||
|
.min_inner_size(500.0, 400.0)
|
||||||
|
.visible(true);
|
||||||
|
|
||||||
|
let window = builder.build().unwrap();
|
||||||
|
let window_c = window.clone();
|
||||||
|
window.on_window_event(move |event: &tauri::WindowEvent| {
|
||||||
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
api.prevent_close();
|
||||||
|
window_c.hide().unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -22,6 +22,11 @@ async deleteFriend(id: string) : Promise<boolean> {
|
|||||||
/** user-defined events **/
|
/** user-defined events **/
|
||||||
|
|
||||||
|
|
||||||
|
export const events = __makeEvents__<{
|
||||||
|
friendsChanged: FriendsChanged
|
||||||
|
}>({
|
||||||
|
friendsChanged: "friends-changed"
|
||||||
|
})
|
||||||
|
|
||||||
/** user-defined constants **/
|
/** user-defined constants **/
|
||||||
|
|
||||||
@@ -30,6 +35,7 @@ async deleteFriend(id: string) : Promise<boolean> {
|
|||||||
/** user-defined types **/
|
/** user-defined types **/
|
||||||
|
|
||||||
export type Friend = { id: string; displayName: string }
|
export type Friend = { id: string; displayName: string }
|
||||||
|
export type FriendsChanged = { friends: Friend[] }
|
||||||
|
|
||||||
/** tauri-specta globals **/
|
/** tauri-specta globals **/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { commands, events, type Friend } from "$lib/bindings";
|
||||||
|
|
||||||
|
let friends: Friend[] = [];
|
||||||
|
let error = "";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const unlisten = events.friendsChanged.listen((event) => {
|
||||||
|
friends = event.payload.friends;
|
||||||
|
});
|
||||||
|
|
||||||
|
commands.listFriends().catch((err) => {
|
||||||
|
error = String(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then((stop) => stop());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createFriend(event: SubmitEvent) {
|
||||||
|
const form = event.currentTarget as HTMLFormElement;
|
||||||
|
const data = new FormData(form);
|
||||||
|
|
||||||
|
error = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await commands.createFriend({
|
||||||
|
id: String(data.get("id") ?? ""),
|
||||||
|
displayName: String(data.get("displayName") ?? ""),
|
||||||
|
});
|
||||||
|
form.reset();
|
||||||
|
} catch (err) {
|
||||||
|
error = String(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFriend(id: string) {
|
||||||
|
error = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await commands.deleteFriend(id);
|
||||||
|
} catch (err) {
|
||||||
|
error = String(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h1>Friends</h1>
|
||||||
|
|
||||||
|
<form onsubmit={createFriend}>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
ID
|
||||||
|
<input name="id" required />
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
Display name
|
||||||
|
<input name="displayName" required />
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button type="submit">Create friend</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<p>{error}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if friends.length === 0}
|
||||||
|
<p>No friends saved.</p>
|
||||||
|
{:else}
|
||||||
|
<ul>
|
||||||
|
{#each friends as friend (friend.id)}
|
||||||
|
<li>
|
||||||
|
<span>{friend.displayName} ({friend.id})</span>
|
||||||
|
<button type="button" onclick={() => deleteFriend(friend.id)}>Delete</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
import FriendsList from "$lib/components/friends-list.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div>Hello, wyd?</div>
|
<div>
|
||||||
|
<FriendsList />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user