community: one area for the people on both networks, and the hub grows search tabs, a folder column and a queue column

This commit is contained in:
KeelrLeek committed 2026-09-02 13:43:16 -05:00
1 parent 6dce4e2ac7
commit 8c25f93afe
14 files changed
+2368 -68

No files matched your search

+3
View File
@@ -857,6 +857,9 @@ pub async fn connect_and_login(
"id": u.id,
"username": u.username,
"name": u.filename.rsplit('\\').next().unwrap_or(&u.filename),
// Full shared path too — Transfers shows the
// parent folder ahead of the name, Qt-style.
"path": u.filename,
"state": state,
"sent": u.sent,
"total": u.total,
+89
View File
@@ -0,0 +1,89 @@
//! 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<ContactMeta> {
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<ContactMeta> {
let blank = |s: &Option<String>| 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<String>| 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<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let (tx, rx) = tokio::sync::oneshot::channel();
let done = move |p: Option<tauri_plugin_dialog::FilePath>| {
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))
}
+28
View File
@@ -1,7 +1,10 @@
mod commands;
mod community;
mod config;
mod cover_logo;
mod diagnostics;
mod freenet;
mod persona;
mod hero_crop;
mod import;
mod hero_detector;
@@ -11,6 +14,7 @@ mod lyric_prep;
mod lyrics;
mod pipeline;
mod playback;
mod profile;
mod playlist_formats;
mod playlists;
mod provider_playlist;
@@ -57,6 +61,10 @@ pub fn run() {
preview::init(app.handle());
loudness::init(app.handle());
diagnostics::init(app.handle());
// The experimental Freenet layer: applies the saved opt-in (off
// by default) and forwards adapter status to the webview. Fully
// isolated — its absence or failure touches nothing else.
freenet::init(app.handle());
if let Err(e) = pipeline::init(app.handle()) {
log::error!("library init failed: {e}");
}
@@ -215,6 +223,26 @@ pub fn run() {
commands::preload_cached_stream,
commands::stream_waveform,
commands::stream_cache_ready,
freenet::freenet_status,
freenet::freenet_set_enabled,
freenet::freenet_detect,
freenet::freenet_set_load_remote_images,
freenet::freenet_node_info,
freenet::freenet_node_start,
freenet::freenet_node_stop,
community::contacts_meta_list,
community::contact_meta_set,
community::community_pick_file,
persona::persona_status,
persona::persona_create,
persona::persona_export,
persona::persona_import,
persona::persona_rotate,
persona::persona_remove,
profile::profile_get,
profile::profile_publish,
profile::profile_set_avatar,
profile::profile_clear_avatar,
preview::preview_resolve,
preview::preview_prewarm,
preview::preview_load,