Named libraries: switch between several, each syncing on its own

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.
This commit is contained in:
2026-10-16 21:44:55 -05:00
parent 6072d44ebe
commit 81b1df8c9f
21 changed files with 2451 additions and 202 deletions

View File

@@ -1,5 +1,5 @@
//! The account side of the sync server: checking an address, finishing a
//! sign-in, and managing the account's devices.
//! sign-in, and managing the account's devices and libraries.
//!
//! The sign-in itself happens in the browser, on the server's own pages (see
//! the server's `auth` module); the app only starts it and redeems the
@@ -25,6 +25,10 @@ pub struct ServerInfo {
/// The identity provider's display name, when single sign-on is on.
#[serde(default)]
pub oidc: Option<String>,
/// Whether an account can keep several named libraries. Servers from
/// before then keep one, at `/api/library`.
#[serde(default)]
pub libraries: bool,
}
/// The newest server API this build speaks.
@@ -190,6 +194,18 @@ pub struct DeviceInfo {
pub current: bool,
}
/// One of an account's libraries on the server.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RemoteLibrary {
pub id: String,
pub name: String,
/// The library `/api/library` serves — the one the account started with.
#[serde(default)]
pub default: bool,
#[serde(default)]
pub version: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Me {
pub user: AccountUser,
@@ -209,6 +225,36 @@ impl HttpRemote {
Ok(())
}
/// The account's libraries, the default first.
pub fn list_libraries(&self) -> Result<Vec<RemoteLibrary>> {
Ok(self.request("GET", "/api/libraries").call().map_err(super::remote::http_error)?.into_json()?)
}
/// Start a new, empty library on the server.
pub fn create_library(&self, name: &str) -> Result<RemoteLibrary> {
Ok(self
.request("POST", "/api/libraries")
.send_json(serde_json::json!({ "name": name }))
.map_err(super::remote::http_error)?
.into_json()?)
}
pub fn rename_library(&self, id: &str, name: &str) -> Result<RemoteLibrary> {
Ok(self
.request("PATCH", &format!("/api/libraries/{}", super::remote::url_path_segment(id)))
.send_json(serde_json::json!({ "name": name }))
.map_err(super::remote::http_error)?
.into_json()?)
}
/// Delete a library from the server. Devices keep their own copies.
pub fn delete_library(&self, id: &str) -> Result<()> {
self.request("DELETE", &format!("/api/libraries/{}", super::remote::url_path_segment(id)))
.call()
.map_err(super::remote::http_error)?;
Ok(())
}
/// Sign this device out on the server, so its token stops working even if
/// a copy of it survives somewhere.
pub fn logout(&self) -> Result<()> {