fix(offline): one server-only rule for both library views

Two defects with one cause: "server only" was a private $derived inside
MediaCard.

The list view (what LibraryGrid renders when the stored view preference
is list) had no notion of it at all, so a library browsed as a list
offline showed every revealed item as an ordinary tappable row that plays
nothing, with no way to queue it.

And the rule asked the downloads store whether *this item id* was
downloaded — but only a playable leaf (Audio, Movie, Episode) ever has a
download row. An album's tracks carry them, the album does not, so a
fully downloaded album greyed itself out and offered to queue what was
already on the device.

The rule moves to the pure $lib/utils/serverOnly and both views call it.
The container half is answered by the backend rather than guessed at:
get_download_disk_usage().sizes already carries container subtotals
beside leaf sizes (DR-085), so deviceContentIds is membership in a
Rust-computed map, not a frontend list of which item types are
containers. That map was loaded only by the Downloads page, so the shell
primes it at startup and re-reads it whenever the offline gate settles.
Queueing is shared too, since the list view had no copy to diverge from.

TRACES: UR-052, UR-055 | DR-292 | UT-257, UT-258
This commit is contained in:
2026-09-22 21:29:38 -04:00
parent a90de67c54
commit bb7d5dc01a
10 changed files with 468 additions and 36 deletions
+15
View File
@@ -19,6 +19,8 @@
showServerCatalog,
lastCatalogSync,
} from "$lib/services/offlineCatalog";
import { downloadedCatalog } from "$lib/services/downloadedCatalog";
import { useOfflineFilterReload } from "$lib/composables/useOfflineFilterReload";
import { playbackMode } from "$lib/stores/playbackMode";
import { sessions } from "$lib/stores/sessions";
import ReauthModal from "$lib/components/auth/ReauthModal.svelte";
@@ -109,6 +111,11 @@
shellReservesBottomInset({ pathname, isAuthenticated: $isAuthenticated }),
);
// The device-content map decides which revealed cards are greyed, so it has
// to be current whenever the offline gate settles — going offline, or
// flipping "Show all server media" — not just at startup. (DR-292)
useOfflineFilterReload(() => downloadedCatalog.refresh());
// The offline banner is shell chrome like the rest, so it stays off the
// full-screen player — where on the native video path it would paint a
// stripe straight through the film (DR-291).
@@ -176,6 +183,14 @@
const userId = get(auth).user?.id;
if (userId) {
downloads.refresh(userId).catch((err) => log.error("Initial downloads refresh failed:", err));
// Which containers hold downloaded children is a backend question
// (`get_download_disk_usage`), and the offline browse needs the answer to
// avoid greying out a fully downloaded album — its *tracks* carry the
// download rows, never the album. Primed here because the map used to be
// loaded only by the Downloads page (DR-292).
downloadedCatalog
.refresh()
.catch((err) => log.error("Initial downloaded-catalog refresh failed:", err));
}
// Start sync service for offline mutation queue