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)
|
||
|
|
}
|