// Presentation logic for the pending-sync queue. // // The queue's *meaning* lives in Rust (which operations exist, how they push, // when one is abandoned — DR-131). What lives here is purely how a row reads on // screen: its label, its subtitle, and whether it is currently erroring. Kept // out of the component so it can be unit-tested, the same pattern as // episodeStrip.ts. // // TRACES: UR-025 | DR-132 | UT-123 import type { SyncQueueItem } from "$lib/api/bindings"; /** How each queued operation reads in the list. */ const OPERATION_LABELS: Record = { report_playback_start: "Playback started", report_playback_stopped: "Watch position", update_progress: "Watch position", mark_played: "Marked as watched", mark_unplayed: "Marked as unwatched", mark_favorite: "Added to favourites", unmark_favorite: "Removed from favourites", playlist_create: "Playlist created", playlist_delete: "Playlist deleted", playlist_rename: "Playlist renamed", playlist_add_items: "Added to playlist", playlist_remove_items: "Removed from playlist", playlist_reorder_item: "Playlist reordered", }; /** * A human label for a queued operation. An operation this build has no label * for still reads as something — an unknown row is the case most worth showing, * since it is the one that will end up abandoned. */ export function describeOperation(operation: string): string { return OPERATION_LABELS[operation] ?? operation.replace(/_/g, " "); } /** What the row is about: the item's title if the catalog knows it, else its id. */ export function describeSubject(item: SyncQueueItem): string { return item.itemName ?? item.itemId ?? "Unknown item"; } /** * A row is "stuck" once it has failed at least once — that is what justifies * showing its error, and what a Retry button acts on. */ export function isStuck(item: SyncQueueItem): boolean { return item.status === "failed" || item.retryCount > 0; } export interface PendingSyncSummary { total: number; stuck: number; /** True when every queued row has already failed — retrying needs the server. */ allStuck: boolean; } export function summarize(items: SyncQueueItem[]): PendingSyncSummary { const stuck = items.filter(isStuck).length; return { total: items.length, stuck, allStuck: items.length > 0 && stuck === items.length, }; } /** * Oldest first — the order they will be replayed in, so the list reads as the * queue it is. Rows without a timestamp sort last rather than being dropped. */ export function sortForDisplay(items: SyncQueueItem[]): SyncQueueItem[] { return [...items].sort((a, b) => { if (!a.createdAt && !b.createdAt) return a.id - b.id; if (!a.createdAt) return 1; if (!b.createdAt) return -1; const diff = Date.parse(a.createdAt) - Date.parse(b.createdAt); return diff !== 0 ? diff : a.id - b.id; }); }