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:
85
src/App.tsx
85
src/App.tsx
@@ -1,7 +1,16 @@
|
||||
import { useEffect, useRef, useState, useCallback, ReactNode } from "react";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { api, IngestProgress, IngestStageInfo, LibraryStats, Status, SyncStatus } from "./api";
|
||||
import {
|
||||
api,
|
||||
IngestProgress,
|
||||
IngestStageInfo,
|
||||
LibraryInfo,
|
||||
LibraryStats,
|
||||
setOpenLibrary,
|
||||
Status,
|
||||
SyncStatus,
|
||||
} from "./api";
|
||||
import {
|
||||
ColorDistributionBar,
|
||||
formatRelativeTime,
|
||||
@@ -13,6 +22,7 @@ import {
|
||||
usePersistedState,
|
||||
} from "./components";
|
||||
import { CommandPalette } from "./CommandPalette";
|
||||
import { LibrariesPanel, LibrarySwitcher } from "./Libraries";
|
||||
import { SyncIndicator, SyncPanel } from "./SyncPanel";
|
||||
import { CardEntry } from "./views/CardEntry";
|
||||
import { LibraryBrowser } from "./views/LibraryBrowser";
|
||||
@@ -236,6 +246,7 @@ export default function App() {
|
||||
// Library sync state: signed in or not, and how the last sync went. Fetched
|
||||
// at startup, after every sync, and whenever the Info tab opens.
|
||||
const [sync, setSync] = useState<SyncStatus | null>(null);
|
||||
const [libraries, setLibraries] = useState<LibraryInfo[]>([]);
|
||||
// A sync is in flight, for the top-bar indicator.
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const lastProgress = useRef<IngestProgress | null>(null);
|
||||
@@ -462,16 +473,37 @@ export default function App() {
|
||||
|
||||
/**
|
||||
* Bring every view into line after the library may have changed wholesale:
|
||||
* signing in or out, adopting the server's copy, importing a file. The top
|
||||
* bar's label comes from get_status, which nothing else re-fetches; the
|
||||
* sidebar reads `libStats`; and each tab holds rows it fetched earlier.
|
||||
* signing in or out, opening another library, adopting the server's copy,
|
||||
* importing a file. The top bar's label comes from get_status, which nothing
|
||||
* else re-fetches; the sidebar reads `libStats`; each tab holds rows it
|
||||
* fetched earlier; and signing in can bring libraries from other devices.
|
||||
*/
|
||||
const refreshEverything = useCallback(() => {
|
||||
api.getStatus().then(setStatus).catch(() => {});
|
||||
api.listLibraries().then(setLibraries).catch(() => {});
|
||||
if (readyRef.current) api.getLibraryStats().then(setLibStats).catch(() => {});
|
||||
setDeckRefreshSignal((v) => v + 1);
|
||||
}, []);
|
||||
|
||||
const refreshLibraries = useCallback(() => {
|
||||
api.listLibraries().then(setLibraries).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (statusLoaded) refreshLibraries();
|
||||
}, [statusLoaded, refreshLibraries]);
|
||||
|
||||
/** Another library was opened, or the list changed: the open library may
|
||||
* be a different one now, so everything refetches. */
|
||||
const librariesChanged = useCallback(
|
||||
(libs: LibraryInfo[]) => {
|
||||
setLibraries(libs);
|
||||
refreshEverything();
|
||||
api.syncStatus().then(setSync).catch(() => {});
|
||||
},
|
||||
[refreshEverything],
|
||||
);
|
||||
|
||||
/** Merge with the server and publish local edits.
|
||||
*
|
||||
* Shared by the Info tab's button and the automatic triggers below. Resolves
|
||||
@@ -490,12 +522,18 @@ export default function App() {
|
||||
}
|
||||
setDeckRefreshSignal((v) => v + 1);
|
||||
}
|
||||
if (report?.libraries_changed) {
|
||||
// Downloaded, renamed or deleted elsewhere — the open one's name in
|
||||
// the top bar included.
|
||||
refreshLibraries();
|
||||
api.getStatus().then(setStatus).catch(() => {});
|
||||
}
|
||||
return report;
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
api.syncStatus().then(setSync).catch(() => {});
|
||||
}
|
||||
}, []);
|
||||
}, [refreshLibraries]);
|
||||
|
||||
// Automatic sync, whenever signed in (and not waiting on the first-sync
|
||||
// choice): once now, on every trip to or from the background — leaving
|
||||
@@ -555,6 +593,10 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
// Before any view renders: they read per-library settings (`libraryKey`)
|
||||
// as they mount.
|
||||
if (status) setOpenLibrary(status.library.id);
|
||||
|
||||
return (
|
||||
<HoverPreviewProvider>
|
||||
<div className="app">
|
||||
@@ -589,9 +631,17 @@ export default function App() {
|
||||
|
||||
<div className="main">
|
||||
<header className="topbar">
|
||||
<span className="path" title={status?.library_label ?? ""}>
|
||||
{status?.library_label ?? ""}
|
||||
</span>
|
||||
{status ? (
|
||||
<LibrarySwitcher
|
||||
label={status.library_label}
|
||||
libraries={libraries}
|
||||
signedIn={sync?.signed_in ?? false}
|
||||
onChanged={librariesChanged}
|
||||
onManage={() => setTab("info")}
|
||||
/>
|
||||
) : (
|
||||
<span className="path" />
|
||||
)}
|
||||
<SyncIndicator sync={sync} syncing={syncing} onClick={() => setTab("info")} />
|
||||
{/* On mobile this lives in the bottom nav's Info tab instead —
|
||||
see below — rather than crowding the topbar. */}
|
||||
@@ -701,12 +751,13 @@ export default function App() {
|
||||
</button>
|
||||
</div>
|
||||
) : tab === "add" ? (
|
||||
<CardEntry />
|
||||
) : /* Keyed on the sync account: switching to a different one can
|
||||
replace every card, deck and id these views are holding, so
|
||||
they're remounted rather than left to reconcile state that
|
||||
belongs to a library that's no longer open. Stable across
|
||||
ordinary status refreshes, so it costs nothing otherwise. */
|
||||
<CardEntry key={status?.library_label} />
|
||||
) : /* Keyed on the open library and its sync account: switching
|
||||
either can replace every card, deck and id these views are
|
||||
holding, so they're remounted rather than left to reconcile
|
||||
state that belongs to a library that's no longer open.
|
||||
Stable across ordinary status refreshes, so it costs
|
||||
nothing otherwise. */
|
||||
tab === "library" ? (
|
||||
<LibraryBrowser key={status?.library_label} onOfferUndo={offerUndo} />
|
||||
) : tab === "search" ? (
|
||||
@@ -741,6 +792,12 @@ export default function App() {
|
||||
</button>
|
||||
</div>
|
||||
<hr className="deck-section-divider" style={{ margin: 0 }} />
|
||||
<LibrariesPanel
|
||||
libraries={libraries}
|
||||
signedIn={sync?.signed_in ?? false}
|
||||
onChanged={librariesChanged}
|
||||
/>
|
||||
<hr className="deck-section-divider" style={{ margin: 0 }} />
|
||||
{sync === null ? (
|
||||
<>
|
||||
<h2 style={{ margin: 0 }}>Your library</h2>
|
||||
|
||||
Reference in New Issue
Block a user