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:
@@ -1,11 +1,13 @@
|
||||
<!-- TRACES: UR-037, UR-051, UR-052, UR-068 | DR-042, DR-068, DR-078, DR-119 -->
|
||||
<!-- TRACES: UR-037, UR-051, UR-052, UR-068 | DR-042, DR-068, DR-078, DR-119, DR-292 -->
|
||||
<script lang="ts">
|
||||
import type { MediaItem, Library } from "$lib/api/types";
|
||||
import { truncateMiddle } from "$lib/utils/truncateMiddle";
|
||||
import { downloads } from "$lib/stores/downloads";
|
||||
import { isConnected } from "$lib/stores/connectivity";
|
||||
import { showServerCatalog } from "$lib/services/offlineCatalog";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { deviceContentIds } from "$lib/services/downloadedCatalog";
|
||||
import { isServerOnly as computeIsServerOnly } from "$lib/utils/serverOnly";
|
||||
import { queueOfflineDownload } from "$lib/services/queueOfflineDownload";
|
||||
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
||||
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
|
||||
import { favoriteOverrides, resolveIsFavorite } from "$lib/stores/favorites";
|
||||
@@ -143,11 +145,21 @@
|
||||
const isQueued = $derived(downloadInfo?.status === "pending");
|
||||
// Actively transferring (as opposed to merely queued/pending for reconnect).
|
||||
const isActivelyDownloading = $derived(downloadInfo?.status === "downloading");
|
||||
// "Server only" = offline, reveal on, and not already downloaded or actively
|
||||
// transferring. A `pending` (queued-for-reconnect) item stays server-only so
|
||||
// it can show the Queued badge in place of the queue button.
|
||||
// "Server only" = offline, reveal on, and nothing on the device behind this
|
||||
// card — neither its own download nor, for a container, its children's (the
|
||||
// album case: only tracks carry download rows). A `pending`
|
||||
// (queued-for-reconnect) item stays server-only so it can show the Queued
|
||||
// badge in place of the queue button. Shared with the list view; see
|
||||
// $lib/utils/serverOnly.
|
||||
const isServerOnly = $derived(
|
||||
isMediaItem && !$isConnected && $showServerCatalog && !isDownloaded && !isActivelyDownloading,
|
||||
computeIsServerOnly({
|
||||
isMediaItem,
|
||||
isConnected: $isConnected,
|
||||
revealServerCatalog: $showServerCatalog,
|
||||
isDownloaded,
|
||||
isActivelyDownloading,
|
||||
hasDeviceContent: $deviceContentIds.has(item.id),
|
||||
}),
|
||||
);
|
||||
|
||||
// The heart is about an item, so libraries never get one, and a greyed
|
||||
@@ -165,29 +177,13 @@
|
||||
async function queueForDownload(e: Event) {
|
||||
e.stopPropagation();
|
||||
if (!isMediaItem) return;
|
||||
const media = item as MediaItem;
|
||||
const userId = auth.getUserId();
|
||||
if (!userId) {
|
||||
queueError = "Not signed in";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
queueError = null;
|
||||
// Derive a sensible on-disk path; the backend heals stream_url on reconnect.
|
||||
const filePath = `downloads/${media.id}`;
|
||||
await downloads.downloadItem(
|
||||
media.id,
|
||||
userId,
|
||||
filePath,
|
||||
undefined,
|
||||
undefined,
|
||||
media.name,
|
||||
media.artists?.join(", ") ?? undefined,
|
||||
media.albumName ?? undefined,
|
||||
);
|
||||
await queueOfflineDownload(item as MediaItem);
|
||||
} catch (err) {
|
||||
log.error("Failed to queue download:", err);
|
||||
queueError = "Failed to queue";
|
||||
queueError =
|
||||
err instanceof Error && err.message === "Not signed in" ? err.message : "Failed to queue";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user