From e17fb5bbf5f4f3d16caf2d5b7ef779b068dc9fbc Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 1 Sep 2026 03:09:53 +0200 Subject: [PATCH] feat(playlists): total playtime in header + sticky-header fade fix (E9 S1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface the playlist's total playtime beside the item count in the detail header (12D) — sums every item's duration (formatRuntime "h m", matching the approved mockup), hidden when nothing sums. Data is already client-side; no backend change. Pass fadeTop:false for the playlists page (12B) so the shell's top edge-fade no longer sits over the already-sticky column header (tableHeader.tsx:257), matching the channels/audit pages. --- frontend/src/components/Playlists.tsx | 9 +++++++++ frontend/src/components/moduleRegistry.tsx | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Playlists.tsx b/frontend/src/components/Playlists.tsx index 977e2926..e3d4e1e1 100644 --- a/frontend/src/components/Playlists.tsx +++ b/frontend/src/components/Playlists.tsx @@ -39,6 +39,7 @@ import { notifyYouTubeActionError } from "../lib/youtubeErrors"; import { playlistName } from "../lib/playlistName"; import { useUndoable } from "../lib/useUndoable"; import { cycleSort, type SortState } from "../lib/columnSort"; +import { formatRuntime } from "../lib/format"; import { applyView, sameOrder } from "../lib/playlistView"; const PlayerModal = lazy(() => import("./PlayerModal")); import UndoToolbar from "./UndoToolbar"; @@ -259,6 +260,13 @@ export default function Playlists() { return (v: Video) => m.get(v.id) ?? 0; }, [items]); + // Total playtime of the whole playlist (sums every item, matching the itemCount beside it — not + // the filtered `shown` set). Items with an unknown duration count as 0; hidden when nothing sums. + const totalSeconds = useMemo( + () => items.reduce((acc, v) => acc + (v.duration_seconds ?? 0), 0), + [items], + ); + const columns = useMemo( () => playlistColumns({ t, indexOf: storedIndex, onPlay, onRemove, canEdit }), [t, storedIndex, onPlay, onRemove, canEdit], @@ -648,6 +656,7 @@ export default function Playlists() { )}
{t("playlists.itemCount", { count: items.length })} + {totalSeconds > 0 && ` · ${formatRuntime(totalSeconds)}`} {filterActive && ` · ${t("playlists.filteredCount", { count: shown.length })}`}
diff --git a/frontend/src/components/moduleRegistry.tsx b/frontend/src/components/moduleRegistry.tsx index c67e1ce5..19eeb6e5 100644 --- a/frontend/src/components/moduleRegistry.tsx +++ b/frontend/src/components/moduleRegistry.tsx @@ -94,7 +94,7 @@ export type PageDef = { export const PAGE_CONTENT: Record = { feed: { Component: FeedPage }, channels: { Component: Channels, fadeTop: false, maxWidth: "96rem" }, - playlists: { Component: Playlists, maxWidth: "64rem" }, + playlists: { Component: Playlists, fadeTop: false, maxWidth: "64rem" }, plex: { Component: PlexBrowse, maxWidth: "1600px" }, notifications: { Component: NotificationsPanel, maxWidth: "48rem" }, messages: { Component: MessagesPage },