Reconcile the ~50 frontend files that consumed the old hand-written types against
the stricter generated bindings (141 -> 0 svelte-check errors):
- Nullable strictness: null-coalesce/guard at consumer sites; widen shared helpers
(CachedImage.tag, ticksToSeconds, formatDuration, getSessionIcon, session store
send*/transferToRemote) to accept null.
- Field-name fixes exposed by codegen: userData.played -> isPlayed; remote
NowPlayingItem uses album (not albumName); ArtistItem id/name.
- player.ts: normalize remote NowPlayingItem -> MediaItem in mergedMedia.
- ArtistItem now serializes camelCase (PascalCase alias retained for Jellyfin
deserialize); update its serialize test.
- Update downloads.test.ts to the bundled { request } shape; complete the
sessions.test.ts NowPlayingItem mock.
Frontend: svelte-check 0 errors, vitest 448 passing. Backend: 387 passing.
100 lines
3.2 KiB
Svelte
100 lines
3.2 KiB
Svelte
<!-- TRACES: UR-010 | JA-021 | DR-037 -->
|
|
<script lang="ts">
|
|
import { sessions, controllableSessions } from "$lib/stores";
|
|
import SessionCard from "./SessionCard.svelte";
|
|
|
|
interface Props {
|
|
onSelectSession?: (sessionId: string) => void;
|
|
}
|
|
|
|
let { onSelectSession }: Props = $props();
|
|
|
|
function handleSessionClick(sessionId: string) {
|
|
if (onSelectSession) {
|
|
onSelectSession(sessionId);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="flex flex-col gap-3">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-white">
|
|
Active Sessions
|
|
{#if $controllableSessions.length > 0}
|
|
<span class="text-sm text-gray-400 font-normal">
|
|
({$controllableSessions.length})
|
|
</span>
|
|
{/if}
|
|
</h2>
|
|
|
|
<button
|
|
onclick={() => sessions.refresh()}
|
|
class="p-2 rounded-lg text-gray-400 hover:text-white hover:bg-white/10 transition-colors"
|
|
title="Refresh sessions"
|
|
>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Loading state -->
|
|
{#if $sessions.isLoading && $sessions.sessions.length === 0}
|
|
<div class="flex items-center justify-center py-12">
|
|
<div class="flex flex-col items-center gap-3">
|
|
<div class="w-8 h-8 border-4 border-[var(--color-jellyfin)] border-t-transparent rounded-full animate-spin"></div>
|
|
<p class="text-sm text-gray-400">Loading sessions...</p>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Error state -->
|
|
{#if $sessions.error}
|
|
<div class="p-4 rounded-lg bg-red-500/10 border border-red-500/50">
|
|
<p class="text-sm text-red-400">{$sessions.error}</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Sessions list -->
|
|
{#if $controllableSessions.length > 0}
|
|
<div class="flex flex-col gap-2">
|
|
{#each $controllableSessions as session (session.id)}
|
|
<SessionCard
|
|
{session}
|
|
selected={$sessions.selectedSessionId === session.id}
|
|
onclick={() => handleSessionClick(session.id ?? "")}
|
|
/>
|
|
{/each}
|
|
</div>
|
|
{:else if !$sessions.isLoading}
|
|
<!-- Empty state -->
|
|
<div class="flex flex-col items-center justify-center py-12 text-center">
|
|
<svg class="w-16 h-16 text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
/>
|
|
</svg>
|
|
<h3 class="text-lg font-medium text-gray-400 mb-2">No Active Sessions</h3>
|
|
<p class="text-sm text-gray-500 max-w-sm">
|
|
No controllable Jellyfin sessions found. Start playing media on another device to control it from here.
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Last updated -->
|
|
{#if $sessions.lastUpdated && $controllableSessions.length > 0}
|
|
<p class="text-xs text-gray-500 text-center">
|
|
Last updated: {$sessions.lastUpdated.toLocaleTimeString()}
|
|
</p>
|
|
{/if}
|
|
</div>
|