//! The Community area's own small backend: the local contact metadata //! (petnames, groups, notes, the two local marks) and the file dialogs the //! You section needs. Everything Soulseek lives in commands.rs, everything //! Freenet in freenet.rs / persona.rs / profile.rs — this is the glue that //! is neither. use serde::Deserialize; use crate::config::{self, ContactMeta}; #[tauri::command] pub fn contacts_meta_list(app: tauri::AppHandle) -> Vec { config::load(&app).contacts } /// Upsert one contact's local metadata. An entry with nothing set is /// dropped rather than stored empty, so the list only ever holds people the /// user actually said something about. #[tauri::command] pub fn contact_meta_set(app: tauri::AppHandle, meta: ContactMeta) -> Vec { let blank = |s: &Option| s.as_deref().map_or(true, |v| v.trim().is_empty()); let mut cfg = config::load(&app); cfg.contacts.retain(|c| c.username != meta.username); let empty = blank(&meta.petname) && blank(&meta.group) && blank(&meta.note) && !meta.trusted && !meta.favorite; if !empty { let trim = |s: Option| s.map(|v| v.trim().to_string()).filter(|v| !v.is_empty()); cfg.contacts.push(ContactMeta { username: meta.username, petname: trim(meta.petname), group: trim(meta.group), note: trim(meta.note), trusted: meta.trusted, favorite: meta.favorite, }); } config::save(&app, &cfg); cfg.contacts } #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum PickKind { /// A source image for the avatar pipeline. Image, /// An existing recovery artifact to import. RecoveryOpen, /// Where to write a new recovery artifact. RecoverySave, } const RECOVERY_EXT: &str = "ssrecovery"; /// Native file dialogs for the You section. Paths only — the bytes behind /// them are read and written by Rust (persona.rs, profile.rs), never by the /// webview. #[tauri::command] pub async fn community_pick_file( app: tauri::AppHandle, kind: PickKind, ) -> Result, String> { use tauri_plugin_dialog::DialogExt; let (tx, rx) = tokio::sync::oneshot::channel(); let done = move |p: Option| { let _ = tx.send(p.map(|p| p.to_string())); }; match kind { PickKind::Image => app .dialog() .file() .add_filter("Image", &["jpg", "jpeg", "png", "webp"]) .pick_file(done), PickKind::RecoveryOpen => app .dialog() .file() .add_filter("SoulStream recovery file", &[RECOVERY_EXT]) .pick_file(done), PickKind::RecoverySave => app .dialog() .file() .add_filter("SoulStream recovery file", &[RECOVERY_EXT]) .set_file_name(&format!("soulstream-identity.{RECOVERY_EXT}")) .save_file(done), } Ok(rx.await.unwrap_or(None)) }