Twelve requirements were marked Done in docs/requirements.md with zero TRACES anywhere in the tree. The features work — the tags were simply never written — so the matrix over-reported on exactly the requirements a reviewer would most want to verify. Each is now tagged at the code that actually implements it: - JA-006 / JA-009 / JA-013 / JA-014 / JA-015 / JA-018 and IR-022 / IR-024 at their Jellyfin call sites in repository/online.rs (search, get_item's MediaStreams/People fields, Items/Resume, Shows/NextUp, FavoriteItems DELETE, get_person/get_items_by_person), plus the commands that expose them. - UR-006 / IR-006 across the lockscreen spine: JellyTauPlaybackService (the MediaSessionCompat owner), the nativeOnMediaCommand JNI intake, and LockscreenMetadata / update_lockscreen_metadata. - IR-008 at both audio-focus mechanisms — ExoPlayer-managed for audio, the manual AudioFocusRequest listener for video — and at the media-type string that chooses between them. - UR-037 (with DR-042, also untraced) on the video-library poster grid: LibraryGrid, MediaCard, and the tv/movies routes. Resolve contradictory statuses across layers, evidence first: - IR-018/IR-019 were Planned under Done URs because they were scoped to libmpv. MpvBackend is the audio-only backend and overrides neither set_subtitle_track nor set_audio_track — the trait's not_implemented() default still stands — so UR-020/UR-021 are met by ExoPlayer and by the HTML5 <video> path instead. Both IRs are re-scoped to those backends and marked Done; IT-008/IT-009 and the stale @req-planned markers in backend.rs follow. - IR-005 (MPRIS) stays Planned: there is no MPRIS/D-Bus code or dependency in the project and update_lockscreen_metadata is a no-op off Android. UR-006 is corrected to Done (Android) rather than the IR being marked Done. - A note under the IR table records where a UR is met by a different mechanism than its IR anticipated. Define the two dangling IDs the source already referenced: DR-189 (the control bar never auto-hid on a touchscreen, because its timer was armed only from onmousemove) and UT-188 (its rule test). The live-denominator assertion in extract-traces.test.ts moves 187/330 to 188/331 accordingly. Traced requirements 444 to 459; IR coverage 19/32 to 25/32.
96 lines
3.7 KiB
Svelte
96 lines
3.7 KiB
Svelte
<!-- TRACES: UR-029, UR-037, UR-051 | DR-042, DR-069, DR-070 -->
|
|
<script lang="ts">
|
|
import type { MediaItem, Library } from "$lib/api/types";
|
|
import MediaCard from "./MediaCard.svelte";
|
|
import LibraryListView from "./LibraryListView.svelte";
|
|
import { library, viewMode } from "$lib/stores/library";
|
|
|
|
interface Props {
|
|
items: (MediaItem | Library)[];
|
|
title?: string;
|
|
loading?: boolean;
|
|
showViewToggle?: boolean;
|
|
musicContent?: boolean;
|
|
onItemClick?: (item: MediaItem | Library) => void;
|
|
/**
|
|
* Optional per-item secondary label (e.g. on-disk size for the Downloaded
|
|
* surface), forwarded to each card. TRACES: UR-056 | DR-085
|
|
*/
|
|
sizeLabelFor?: (item: MediaItem | Library) => string | undefined;
|
|
/** Optional per-item container download badge for the Downloaded surface. */
|
|
downloadedBadgeFor?: (item: MediaItem | Library) => "full" | "partial" | undefined;
|
|
/** Optional per-item remove-from-device handler for the Downloaded surface. */
|
|
onItemRemove?: (item: MediaItem | Library) => void;
|
|
}
|
|
|
|
let { items, title, loading = false, showViewToggle = true, musicContent = false, onItemClick, sizeLabelFor, downloadedBadgeFor, onItemRemove }: Props = $props();
|
|
</script>
|
|
|
|
<div class="space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
{#if title}
|
|
<h2 class="text-xl font-semibold text-white">{title}</h2>
|
|
{:else}
|
|
<div></div>
|
|
{/if}
|
|
|
|
{#if showViewToggle && items.length > 0}
|
|
<div class="flex gap-1">
|
|
<button
|
|
onclick={() => library.setViewMode("grid")}
|
|
class="p-2 rounded transition-colors {$viewMode === 'grid' ? 'bg-[var(--color-jellyfin)] text-white' : 'text-gray-400 hover:bg-white/10 hover:text-white'}"
|
|
aria-label="Grid view"
|
|
title="Grid view"
|
|
>
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M3 3h8v8H3V3zm0 10h8v8H3v-8zm10-10h8v8h-8V3zm0 10h8v8h-8v-8z"/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onclick={() => library.setViewMode("list")}
|
|
class="p-2 rounded transition-colors {$viewMode === 'list' ? 'bg-[var(--color-jellyfin)] text-white' : 'text-gray-400 hover:bg-white/10 hover:text-white'}"
|
|
aria-label="List view"
|
|
title="List view"
|
|
>
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if loading}
|
|
<div class="flex gap-4 overflow-hidden">
|
|
{#each Array(6) as _}
|
|
<div class="w-36 flex-shrink-0 animate-pulse">
|
|
<div class="{musicContent ? 'aspect-square' : 'aspect-[2/3]'} bg-[var(--color-surface)] rounded-lg"></div>
|
|
<div class="mt-2 h-4 bg-[var(--color-surface)] rounded w-3/4"></div>
|
|
<div class="mt-1 h-3 bg-[var(--color-surface)] rounded w-1/2"></div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else if items.length === 0}
|
|
<div class="text-center py-12 text-gray-400">
|
|
<p>No items found</p>
|
|
</div>
|
|
{:else if $viewMode === "list"}
|
|
<LibraryListView {items} showProgress={true} onItemClick={onItemClick} />
|
|
{:else}
|
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
|
{#each items as item, index (item.id)}
|
|
<div data-grid-index={index}>
|
|
<MediaCard
|
|
{item}
|
|
showProgress={true}
|
|
sizeLabel={sizeLabelFor?.(item)}
|
|
downloadedBadge={downloadedBadgeFor?.(item)}
|
|
onRemove={onItemRemove ? () => onItemRemove(item) : undefined}
|
|
onclick={() => onItemClick?.(item)}
|
|
/>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|