diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx index 9a7f341..82bdf25 100644 --- a/frontend/src/components/PlexBrowse.tsx +++ b/frontend/src/components/PlexBrowse.tsx @@ -26,6 +26,7 @@ import { type PlexSeasonDetail, type PlexUnifiedResult, } from "../lib/api"; +import { formatRuntime } from "../lib/format"; import { useDebounced } from "../lib/useDebounced"; import { useHistorySubview } from "../lib/history"; import { DetailCustomizeMenu, Filterable, useArtBackdrop, useDetailPrefs } from "../lib/plexDetailUi"; @@ -67,13 +68,6 @@ type Props = { const PAGE = 40; -function dur(n?: number | null): string { - if (!n) return ""; - const h = Math.floor(n / 3600); - const m = Math.floor((n % 3600) / 60); - return h ? `${h}h ${m}m` : `${m}m`; -} - export default function PlexBrowse({ q, onClearSearch, @@ -483,7 +477,7 @@ function PlexPosterCard({ style={{ background: "color-mix(in srgb, var(--card) 60%, transparent)" }} >
{card.title}
-
{[card.year, dur(card.duration_seconds)].filter(Boolean).join(" · ")}
+
{[card.year, formatRuntime(card.duration_seconds)].filter(Boolean).join(" · ")}
); @@ -526,7 +520,8 @@ function PlexInfoView({ onPlay={onPlay} onPlayItem={onPlayItem} onFilter={onFilter} - onStateChange={() => q.refetch()} + // No onStateChange: PlexInfo.setState already invalidates ["plex-item", id] (this very + // query), so q refetches automatically — a manual refetch here just double-fetched. /> )} @@ -845,7 +840,7 @@ function EpisodeCard({ {!withShowTitle && {ep.episode_number}.} {ep.title} -
{dur(ep.duration_seconds)}
+
{formatRuntime(ep.duration_seconds)}
{onAdd && ( diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts index d8ac660..7461c35 100644 --- a/frontend/src/lib/format.ts +++ b/frontend/src/lib/format.ts @@ -70,6 +70,15 @@ export function formatDuration(sec: number | null): string { return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`; } +/** Compact human runtime ("1h 30m", "45m") from seconds — used for Plex movie/episode lengths. + * Empty string for a missing/zero duration (nothing to show). */ +export function formatRuntime(sec?: number | null): string { + if (!sec) return ""; + const h = Math.floor(sec / 3600); + const m = Math.floor((sec % 3600) / 60); + return h ? `${h}h ${m}m` : `${m}m`; +} + // Human label for a canonical quota-action key (see backend app.quota.QuotaAction). Resolved // from i18n (quotaActions.) so it's translated in all UI languages; falls back to the raw // key for any unmapped/legacy value. diff --git a/frontend/src/lib/storage.ts b/frontend/src/lib/storage.ts index fc576e3..1102d3b 100644 --- a/frontend/src/lib/storage.ts +++ b/frontend/src/lib/storage.ts @@ -32,6 +32,7 @@ export const LS = { plexFilters: "siftlode.plexFilters", plexQ: "siftlode.plexQ", plexPlaylistLayout: "siftlode.plexPlaylistLayout", + plexPlayerPrefs: "siftlode.plexPlayerPrefs", notifHistory: "siftlode.notifications", notifSettings: "siftlode.notifSettings", onboardingDismissed: "siftlode.onboarding.dismissed",