//! The libraries on this device: listing, starting, opening, renaming and //! deleting them. //! //! Each is a separate working copy (see [`AppState`]), and one is open at a //! time. Linking them up with the account's libraries on a sync server is //! `sync.rs`'s job; the commands here only touch the server for what the user //! asks for directly — a rename, or deleting a library everywhere — and //! otherwise leave it to the next sync. use mtgc_core::sync::remote::{HttpRemote, NOT_FOUND}; use serde::Serialize; use tauri::AppHandle; use crate::state::{AppState, LocalLibrary}; use crate::sync::{blocking, lock, sync_now}; /// A library, as the UI lists it. #[derive(Serialize)] pub struct LibraryInfo { pub id: String, pub name: String, /// The one that's open. pub active: bool, /// The library the device started with, which can't be deleted. pub is_default: bool, /// Whether it should be on the sync server. pub sync: bool, /// Whether it's linked to a library on the server yet. pub linked: bool, /// Another device deleted it from the server; this copy is all that's left. pub server_deleted: bool, } fn info(s: &AppState, lib: &LocalLibrary) -> LibraryInfo { LibraryInfo { id: lib.id.clone(), name: lib.name.clone(), active: s.active_library().id == lib.id, is_default: lib.is_default(), sync: lib.sync, linked: lib.remote_id.is_some(), server_deleted: lib.server_deleted, } } fn list(s: &AppState) -> Vec { s.libraries().iter().map(|l| info(s, l)).collect() } /// The signed-in account's server, if any. fn account_remote(s: &AppState) -> Option { let a = s.sync_account()?; Some(HttpRemote::new(&a.server_url, a.token.as_deref()?)) } /// Let the next sync put a change on the server now rather than at the next /// trigger, without making the user wait for it. fn sync_soon(app: &AppHandle) { let app = app.clone(); tauri::async_runtime::spawn(async move { let _ = sync_now(app).await; }); } #[tauri::command] pub async fn list_libraries(app: AppHandle) -> Result, String> { blocking(move || Ok(list(&lock(&app)))).await } /// Start a new, empty library and open it. With `sync`, the next sync puts it /// on the server. #[tauri::command] pub async fn create_library(app: AppHandle, name: String, sync: bool) -> Result, String> { let app2 = app.clone(); let libs = blocking(move || { let mut s = lock(&app2); let lib = s.create_library(&name, sync, None)?; s.switch_library(&lib.id)?; crate::mlog!("create_library: {} (sync={sync})", lib.name); Ok(list(&s)) }) .await; let libs = crate::logged("create_library", libs)?; if sync { sync_soon(&app); } Ok(libs) } /// Open another library. #[tauri::command] pub async fn switch_library(app: AppHandle, id: String) -> Result, String> { let libs = blocking(move || { let mut s = lock(&app); s.switch_library(&id)?; crate::mlog!("switch_library: {}", s.active_library().name); Ok(list(&s)) }) .await; crate::logged("switch_library", libs) } /// Rename a library — on the server too when it's there, so every device /// picks the new name up. #[tauri::command] pub async fn rename_library(app: AppHandle, id: String, name: String) -> Result, String> { let libs = blocking(move || { let (name, remote_id, remote) = { let s = lock(&app); let lib = s.library(&id).ok_or("There's no such library on this device.")?; (s.check_name(&name, Some(&id))?, lib.remote_id.clone(), account_remote(&s)) }; if let Some(rid) = &remote_id { let remote = remote.ok_or("Sign in to the sync server to rename a library that's on it.")?; remote.rename_library(rid, &name).map_err(|e| format!("Couldn't rename it on the server: {e}"))?; } let mut s = lock(&app); s.update_library(&id, |l| l.name = name); Ok(list(&s)) }) .await; crate::logged("rename_library", libs) } /// Delete a library from this device. With `everywhere`, from the sync /// server too — and so, at their next sync, from every other device's list /// of what's on the server (they keep their own copies until removed). #[tauri::command] pub async fn delete_library(app: AppHandle, id: String, everywhere: bool) -> Result, String> { let libs = blocking(move || { let (remote_id, remote) = { let s = lock(&app); let lib = s.library(&id).ok_or("There's no such library on this device.")?; (lib.remote_id.clone(), account_remote(&s)) }; if let (true, Some(rid)) = (everywhere, &remote_id) { let remote = remote.ok_or("Sign in to the sync server to delete a library from it.")?; match remote.delete_library(rid) { Ok(()) => {} // Someone got there first. Err(e) if e.to_string() == NOT_FOUND => {} Err(e) => return Err(format!("Couldn't delete it from the server: {e}")), } } let mut s = lock(&app); let lib = s.delete_library(&id)?; if let (false, Some(rid)) = (everywhere, &remote_id) { // Still on the server: don't download it straight back. s.dismiss_remote(rid); } crate::mlog!("delete_library: {} (everywhere={everywhere})", lib.name); Ok(list(&s)) }) .await; crate::logged("delete_library", libs) } /// Start or stop syncing a library. Stopping leaves the server's copy where /// it is, for the other devices; starting again links back up with it if it's /// still there (by name), or puts this one on the server. #[tauri::command] pub async fn set_library_sync(app: AppHandle, id: String, sync: bool) -> Result, String> { let app2 = app.clone(); let libs = blocking(move || { let mut s = lock(&app2); let lib = s.library(&id).cloned().ok_or("There's no such library on this device.")?; if s.sync_running { return Err("Wait for the sync to finish, then try again.".into()); } if !sync { if let Some(rid) = &lib.remote_id { s.dismiss_remote(rid); } } s.update_library(&id, |l| { l.sync = sync; l.server_deleted = false; if !sync { l.remote_id = None; } }); Ok(list(&s)) }) .await; let libs = crate::logged("set_library_sync", libs)?; if sync { sync_soon(&app); } Ok(libs) }