Refresh the whole UI when the library document changes

Picking a library left the path bar showing the previous one until the next
launch: it reads status.library_path from get_status, but sync_set_document
returns a SyncStatus, and nothing re-fetched the former.

The less visible half was that every view kept rendering the old library's
rows. Switching documents replaces every card, deck and internal id beneath
them, so they're now keyed on the document path and remounted rather than left
reconciling state belonging to a library that is no longer open — which matters
most for Decks, whose persisted "last selected deck" id means nothing in a
different library.

Both Open and New Library now go through one adoptDocument(), which refreshes
status, library stats and the views together, and reports what was opened
("Opened …/decks-v1.json — 366 cards, 7 decks") so a successful switch isn't
silent.

The backend was already correct: it had loaded the picked document all along.
This commit is contained in:
2026-08-07 22:28:45 -05:00
parent 196f1b9b21
commit 8a868ca18a

View File

@@ -452,6 +452,32 @@ export default function App() {
}, [tab, refreshSync]);
/**
* Point the app at `path` and bring the whole UI into line with it.
*
* Switching documents changes the entire library underneath every view, so
* refreshing the sync panel alone isn't enough: the path bar reads
* `status.library_path`, the sidebar reads `libStats`, and each tab is
* holding rows fetched from the *previous* library. Returns a short " — N
* cards, M decks" suffix so the caller can confirm what actually opened.
*/
async function adoptDocument(path: string): Promise<string> {
setSync(await api.syncSetDocument(path));
// The path bar comes from get_status, which nothing else re-fetches.
setStatus(await api.getStatus());
setDeckRefreshSignal((v) => v + 1);
if (!readyRef.current) return "";
try {
const stats = await api.getLibraryStats();
setLibStats(stats);
return ` — ${stats.unique_cards} card${stats.unique_cards === 1 ? "" : "s"}, ${
stats.deck_count
} deck${stats.deck_count === 1 ? "" : "s"}`;
} catch {
return "";
}
}
/** Choose where the library lives. Android grants a folder through the system
* picker (SAF has no path to hand back); desktop picks the JSON file itself. */
async function pickLibrary() {
@@ -474,7 +500,8 @@ export default function App() {
});
if (typeof path !== "string") return;
setSyncBusy(true);
setSync(await api.syncSetDocument(path));
const opened = await adoptDocument(path);
setSyncTest({ ok: true, message: `Opened ${path}${opened}.` });
} catch (err) {
setSyncTest({ ok: false, message: String(err) });
} finally {
@@ -495,8 +522,8 @@ export default function App() {
setSyncBusy(true);
// Pointing at a file that doesn't exist yet moves the current library
// there rather than starting empty — see sync_set_document.
setSync(await api.syncSetDocument(path));
setSyncTest({ ok: true, message: `Your library is now kept at ${path}.` });
const moved = await adoptDocument(path);
setSyncTest({ ok: true, message: `Your library is now kept at ${path}${moved}.` });
} catch (err) {
setSyncTest({ ok: false, message: String(err) });
} finally {
@@ -795,12 +822,17 @@ export default function App() {
</div>
) : tab === "add" ? (
<CardEntry />
) : tab === "library" ? (
<LibraryBrowser onOfferUndo={offerUndo} />
) : /* Keyed on the library document: switching to a different one
replaces 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_path} onOfferUndo={offerUndo} />
) : tab === "search" ? (
<CardSearch onOfferUndo={offerUndo} />
<CardSearch key={status?.library_path} onOfferUndo={offerUndo} />
) : tab === "decks" ? (
<DeckView refreshSignal={deckRefreshSignal} />
<DeckView key={status?.library_path} refreshSignal={deckRefreshSignal} />
) : (
// The per-install status page: card data, library sync, and the
// collection summary. On mobile it's the only home for the