domain: catalog frontend off Jellyfin ticks -> milliseconds (phase 3a)

The catalog surface now speaks milliseconds, the app's neutral time unit.
Ticks no longer reach library/home components.

Rust:
- UserData gains playback_position_ms (dual-carry), populated from ticks
  at the offline mapping seam via domain::ticks_to_ms.

Frontend:
- formatDuration(duration.ts) and the two local copies now take ms, not
  ticks; all callers pass item.durationMs.
- Progress bars (EpisodeRow, EpisodeFocusView, MediaCard, LibraryListView)
  compute playbackPositionMs / durationMs — unit-consistent, no tick math.
- PlaylistDetailView totalDuration sums durationMs.
- duration.test.ts + TrackList.test.ts fixtures updated to ms.

Deferred: player/session/reporting tick math (Queue, SessionCard,
RemoteControls, playbackReporting, playerEvents) — those cross the
storage/Jellyfin command boundary in ticks and need command-signature
changes (phase 3b). Display {item.type} badge -> kind label (phase 4).

Rust 456, frontend 644, check + check:boundary clean.
This commit is contained in:
2026-07-23 21:30:04 +02:00
parent 3e962a202c
commit 7660a33dfc
14 changed files with 80 additions and 58 deletions
+7 -7
View File
@@ -84,7 +84,7 @@
try {
item = await library.loadItem(itemId);
console.log(`[LibraryDetail] ✓ Loaded item: ${item?.name} (${item?.type})`);
console.log(`[LibraryDetail] ✓ Loaded item: ${item?.name} (${item?.kind})`);
console.log(`[LibraryDetail] - Has people? ${item?.people ? `YES (${item.people.length})` : 'NO'}`);
if (item?.people) {
item.people.forEach((p, i) => {
@@ -170,9 +170,9 @@
// Images now handled by CachedImage component
function formatDuration(ticks?: number): string {
if (!ticks) return "";
const seconds = Math.floor(ticks / 10000000);
function formatDuration(ms?: number | null): string {
if (!ms) return "";
const seconds = Math.floor(ms / 1000);
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
@@ -183,7 +183,7 @@
}
function handleItemClick(clickedItem: MediaItem | Library) {
if (!("type" in clickedItem)) {
if (!("kind" in clickedItem)) {
// Library item - navigate to library
goto(`/library/${clickedItem.id}`);
return;
@@ -405,8 +405,8 @@
{#if item.type}
<span class="px-2 py-1 bg-[var(--color-surface)] rounded">{item.type}</span>
{/if}
{#if item.runTimeTicks}
<span>{formatDuration(item.runTimeTicks)}</span>
{#if item.durationMs}
<span>{formatDuration(item.durationMs)}</span>
{/if}
{#if item.communityRating}
<span class="flex items-center gap-1">