The app can keep any number of named libraries and switch between them from the top bar or Info → Libraries. Each can sync to the account's other devices or stay on this one (a scratch library to try things in). The sync server now keeps several libraries per user under /api/libraries; a migration makes each existing library the user's default "Main", which /api/library keeps serving for older app builds. Every sync lines the device's libraries up with the account's: new ones elsewhere are downloaded, new ones here created on the server, ones deleted elsewhere flagged rather than thrown away. The existing library keeps its files and merge base, so upgrading asks nothing.
191 lines
6.7 KiB
Rust
191 lines
6.7 KiB
Rust
//! 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<LibraryInfo> {
|
|
s.libraries().iter().map(|l| info(s, l)).collect()
|
|
}
|
|
|
|
/// The signed-in account's server, if any.
|
|
fn account_remote(s: &AppState) -> Option<HttpRemote> {
|
|
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<Vec<LibraryInfo>, 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<Vec<LibraryInfo>, 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<Vec<LibraryInfo>, 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<Vec<LibraryInfo>, 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<Vec<LibraryInfo>, 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<Vec<LibraryInfo>, 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)
|
|
}
|