friends list UI

This commit is contained in:
2026-08-01 02:43:14 +08:00
parent 7db736e418
commit 5066e453e2
6 changed files with 187 additions and 13 deletions
+52 -10
View File
@@ -1,7 +1,8 @@
use crate::db::{self, AppDatabase};
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::State;
use tauri::{AppHandle, State};
use tauri_specta::Event;
#[derive(Debug, Clone, Serialize, Deserialize, Type, sqlx::FromRow)]
#[serde(rename_all = "camelCase")]
@@ -10,9 +11,32 @@ pub struct Friend {
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]
#[specta::specta]
pub async fn create_friend(
handle: AppHandle,
database: State<'_, AppDatabase>,
friend: Friend,
) -> Result<Friend, String> {
@@ -23,18 +47,26 @@ pub async fn create_friend(
.await
.map_err(db::command_error)?;
emit_changed(&handle, &database).await?;
Ok(friend)
}
#[tauri::command]
#[specta::specta]
pub async fn list_friends(database: State<'_, AppDatabase>) -> Result<Vec<Friend>, String> {
sqlx::query_as::<_, Friend>(
"SELECT id, display_name FROM friends ORDER BY display_name COLLATE NOCASE, id",
)
.fetch_all(database.pool())
.await
.map_err(db::command_error)
pub async fn list_friends(
handle: AppHandle,
database: State<'_, AppDatabase>,
) -> Result<Vec<Friend>, String> {
let friends = all(&database).await.map_err(db::command_error)?;
FriendsChanged {
friends: friends.clone(),
}
.emit(&handle)
.map_err(db::command_error)?;
Ok(friends)
}
#[tauri::command]
@@ -52,12 +84,22 @@ pub async fn get_friend(
#[tauri::command]
#[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")
.bind(id)
.execute(database.pool())
.await
.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)
}
+5 -2
View File
@@ -23,7 +23,9 @@ pub fn run() {
crate::friends::get_friend,
crate::friends::delete_friend
])
.events(tauri_specta::collect_events![]);
.events(tauri_specta::collect_events![
crate::friends::FriendsChanged
]);
#[cfg(debug_assertions)]
specta_builder
@@ -36,7 +38,8 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(specta_builder.invoke_handler())
.setup(|app| {
.setup(move |app| {
specta_builder.mount_events(app);
tauri::async_runtime::block_on(launch_app(app))?;
Ok(())
})
+30
View File
@@ -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();
}
});
}
+6
View File
@@ -22,6 +22,11 @@ async deleteFriend(id: string) : Promise<boolean> {
/** user-defined events **/
export const events = __makeEvents__<{
friendsChanged: FriendsChanged
}>({
friendsChanged: "friends-changed"
})
/** user-defined constants **/
@@ -30,6 +35,7 @@ async deleteFriend(id: string) : Promise<boolean> {
/** user-defined types **/
export type Friend = { id: string; displayName: string }
export type FriendsChanged = { friends: Friend[] }
/** tauri-specta globals **/
+87
View File
@@ -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>
+7 -1
View File
@@ -1,3 +1,9 @@
<script>
import FriendsList from "$lib/components/friends-list.svelte";
</script>
<main>
<div>Hello, wyd?</div>
<div>
<FriendsList />
</div>
</main>