client server connection UI

This commit is contained in:
2026-08-06 19:17:13 +08:00
parent eae86c73f7
commit 81e03ba152
5 changed files with 167 additions and 4 deletions
+2
View File
@@ -61,11 +61,13 @@ fn specta_builder() -> tauri_specta::Builder<tauri::Wry> {
profile::get_profile,
profile::update_profile,
keypair::get_public_key,
network::list_statuses,
])
.events(tauri_specta::collect_events![
friends::FriendsChanged,
remotes::RemotesChanged,
profile::ProfileChanged,
network::NetworkStatusChanged,
])
}
+106 -4
View File
@@ -1,9 +1,12 @@
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use futures_util::{SinkExt, StreamExt};
use tauri::{AppHandle, Manager};
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::{AppHandle, Manager, State};
use tauri_specta::Event;
use tokio::sync::mpsc;
use tokio_tungstenite::tungstenite::Message;
use wyd_common::{ClientMessage, Profile, ServerMessage, message_bytes, register_bytes};
@@ -12,8 +15,34 @@ use crate::db::AppDatabase;
use crate::keypair::AppKeypair;
use crate::remotes::{self, Remote};
type Statuses = Arc<Mutex<HashMap<String, ConnectionStatus>>>;
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub enum ConnectionState {
Connecting,
Connected,
Disconnected,
}
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct ConnectionStatus {
pub remote_id: String,
pub address: String,
pub name: Option<String>,
pub state: ConnectionState,
}
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
#[serde(rename_all = "camelCase")]
pub struct NetworkStatusChanged {
pub statuses: Vec<ConnectionStatus>,
}
pub struct Network {
senders: Mutex<HashMap<String, mpsc::Sender<String>>>,
statuses: Statuses,
}
impl Network {
@@ -35,34 +64,59 @@ pub async fn init(handle: &AppHandle) -> Result<(), Box<dyn std::error::Error>>
let profile = crate::profile::get(&database, keypair.public_key()).await?;
let remotes = remotes::all(&database).await?;
let mut senders = HashMap::new();
let statuses = Statuses::default();
for remote in remotes {
let (sender, receiver) = mpsc::channel(32);
senders.insert(remote.id.clone(), sender);
tauri::async_runtime::spawn(run(remote, profile.clone(), keypair.clone(), receiver));
set(&statuses, &remote, ConnectionState::Connecting);
tauri::async_runtime::spawn(run(
handle.clone(),
statuses.clone(),
remote,
profile.clone(),
keypair.clone(),
receiver,
));
}
handle.manage(Network {
senders: Mutex::new(senders),
statuses,
});
Ok(())
}
async fn run(
handle: AppHandle,
statuses: Statuses,
remote: Remote,
profile: crate::user::User,
keypair: AppKeypair,
mut outgoing: mpsc::Receiver<String>,
) {
loop {
if let Err(error) = connect(&remote, &profile, &keypair, &mut outgoing).await {
changed(&handle, &statuses, &remote, ConnectionState::Connecting);
if let Err(error) = connect(
&handle,
&statuses,
&remote,
&profile,
&keypair,
&mut outgoing,
)
.await
{
eprintln!("remote {} disconnected: {error}", remote.id);
}
changed(&handle, &statuses, &remote, ConnectionState::Disconnected);
tokio::time::sleep(Duration::from_secs(5)).await;
}
}
async fn connect(
handle: &AppHandle,
statuses: &Statuses,
remote: &Remote,
profile: &crate::user::User,
keypair: &AppKeypair,
@@ -93,6 +147,7 @@ async fn connect(
if !matches!(recv(&mut reader).await?, ServerMessage::Registered) {
return Err("server rejected registration".into());
}
changed(handle, statuses, remote, ConnectionState::Connected);
loop {
tokio::select! {
@@ -112,6 +167,53 @@ async fn connect(
}
}
#[tauri::command]
#[specta::specta]
pub fn list_statuses(
handle: AppHandle,
network: State<'_, Network>,
) -> Result<Vec<ConnectionStatus>, String> {
let statuses = snapshot(&network.statuses)?;
NetworkStatusChanged {
statuses: statuses.clone(),
}
.emit(&handle)
.map_err(|error| error.to_string())?;
Ok(statuses)
}
fn changed(handle: &AppHandle, statuses: &Statuses, remote: &Remote, state: ConnectionState) {
set(statuses, remote, state);
if let Ok(statuses) = snapshot(statuses) {
let _ = NetworkStatusChanged { statuses }.emit(handle);
}
}
fn set(statuses: &Statuses, remote: &Remote, state: ConnectionState) {
if let Ok(mut statuses) = statuses.lock() {
statuses.insert(
remote.id.clone(),
ConnectionStatus {
remote_id: remote.id.clone(),
address: remote.address.clone(),
name: remote.name.clone(),
state,
},
);
}
}
fn snapshot(statuses: &Statuses) -> Result<Vec<ConnectionStatus>, String> {
let mut statuses: Vec<_> = statuses
.lock()
.map_err(|error| error.to_string())?
.values()
.cloned()
.collect();
statuses.sort_by(|a, b| a.remote_id.cmp(&b.remote_id));
Ok(statuses)
}
async fn send<S>(
writer: &mut S,
message: &ClientMessage,
+8
View File
@@ -40,6 +40,9 @@ async updateProfile(displayName: string) : Promise<User> {
},
async getPublicKey() : Promise<string> {
return await TAURI_INVOKE("get_public_key");
},
async listStatuses() : Promise<ConnectionStatus[]> {
return await TAURI_INVOKE("list_statuses");
}
}
@@ -48,10 +51,12 @@ async getPublicKey() : Promise<string> {
export const events = __makeEvents__<{
friendsChanged: FriendsChanged,
networkStatusChanged: NetworkStatusChanged,
profileChanged: ProfileChanged,
remotesChanged: RemotesChanged
}>({
friendsChanged: "friends-changed",
networkStatusChanged: "network-status-changed",
profileChanged: "profile-changed",
remotesChanged: "remotes-changed"
})
@@ -62,7 +67,10 @@ remotesChanged: "remotes-changed"
/** user-defined types **/
export type ConnectionState = "connecting" | "connected" | "disconnected"
export type ConnectionStatus = { remoteId: string; address: string; name: string | null; state: ConnectionState }
export type FriendsChanged = { friends: User[] }
export type NetworkStatusChanged = { statuses: ConnectionStatus[] }
export type ProfileChanged = { profile: User }
export type Remote = { id: string; address: string; name: string | null; port: number | null }
export type RemoteInput = { address: string; name: string | null; port: number | null }
@@ -0,0 +1,47 @@
<script lang="ts">
import { onMount } from "svelte";
import { commands, events, type ConnectionStatus } from "$lib/bindings";
let statuses: ConnectionStatus[] = [];
let error = "";
onMount(() => {
const unlisten = events.networkStatusChanged.listen((event) => {
statuses = event.payload.statuses;
});
commands
.listStatuses()
.then((current) => {
statuses = current;
})
.catch((err) => {
error = String(err);
});
return () => {
unlisten.then((stop) => stop());
};
});
</script>
<section>
<h1>Connections</h1>
{#if error}
<p aria-live="polite">{error}</p>
{:else if statuses.length === 0}
<p>No remote connections configured.</p>
{:else}
<ul aria-live="polite">
{#each statuses as status (status.remoteId)}
<li>
<strong>{status.name ?? status.address}</strong>: {status.state}
{#if status.name}
<span> — {status.address}</span>
{/if}
</li>
{/each}
</ul>
{/if}
</section>
+4
View File
@@ -1,5 +1,6 @@
<script>
import FriendsList from "$lib/components/friends-list.svelte";
import ConnectionStatus from "$lib/components/connection-status.svelte";
import Profile from "$lib/components/profile.svelte";
import RemotesList from "$lib/components/remotes-list.svelte";
</script>
@@ -14,4 +15,7 @@
<div>
<RemotesList />
</div>
<div>
<ConnectionStatus />
</div>
</main>