- {Array.from({ length: 4 }).map((_, i) =>
+ {/* Top half: 2 montage posters, or the colour hash when a group has no art. */}
+
+ {posters.length > 0 &&
+ [0, 1].map((i) =>
posters[i] ? (
![]()
),
)}
-
- )}
-
- {/* After the scrim in DOM order → paints above it without a z-index token. */}
- {topRight != null &&
{topRight}}
-
-
{title}
- {subtitle != null &&
{subtitle}
}
+
+ {/* Bottom half: solid colour band — the label lives here so it stays readable over any art. */}
+
+ {topRight != null &&
{topRight}}
+
{title}
+ {subtitle != null &&
{subtitle}
}
);
diff --git a/frontend/src/components/plexBrowseLists.tsx b/frontend/src/components/plexBrowseLists.tsx
index 12040dc1..efe7ba82 100644
--- a/frontend/src/components/plexBrowseLists.tsx
+++ b/frontend/src/components/plexBrowseLists.tsx
@@ -1,13 +1,25 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
+import type { TFunction } from "i18next";
import { useQuery } from "@tanstack/react-query";
-import { Layers, ListMusic } from "lucide-react";
+import { ListMusic } from "lucide-react";
import { qk } from "../lib/queryKeys";
-import { api } from "../lib/api";
+import { api, type PlexCollection } from "../lib/api";
+import { formatRuntime } from "../lib/format";
import { useDebounced } from "../lib/useDebounced";
import { LoadingState, StateMessage } from "./QueryState";
import PosterTile from "./PosterTile";
+// A collection tile's sub-line: item count · total runtime (each dropped when unknown, e.g. show-only
+// collections have no movie runtime).
+function collectionMeta(c: PlexCollection, t: TFunction): string {
+ const parts: string[] = [];
+ if (c.child_count != null) parts.push(t("plex.browse.items", { count: c.child_count }));
+ const rt = formatRuntime(c.total_runtime_s);
+ if (rt) parts.push(rt);
+ return parts.join(" · ");
+}
+
// The Playlist / Collection browse surfaces reached from the header pill (Piece 2): a grid of tiles,
// each a way INTO that area — a playlist tile opens the playlist view, a collection tile filters the
// title grid by that collection. Both reuse the existing list endpoints (plexPlaylists / plexCollections
@@ -87,10 +99,7 @@ export function PlexCollectionsBrowse({ onPick }: { onPick: (id: string, title:
key={c.id}
title={c.title}
art={c.art}
- subtitle={
- c.child_count != null ? t("plex.browse.items", { count: c.child_count }) : undefined
- }
- topRight={
}
+ subtitle={collectionMeta(c, t)}
onClick={() => onPick(c.id, c.title)}
className="h-28"
/>
diff --git a/frontend/src/lib/api/plex.ts b/frontend/src/lib/api/plex.ts
index 9b6171c8..bad246c0 100644
--- a/frontend/src/lib/api/plex.ts
+++ b/frontend/src/lib/api/plex.ts
@@ -184,7 +184,8 @@ export interface PlexCollection {
source?: string;
editable: boolean;
can_edit?: boolean; // effective: admin-marked editable AND a plain (non-smart/non-auto) collection
- art?: string[]; // up to 4 poster rating_keys for the browse-tile collage (browse union only)
+ art?: string[]; // up to 2 poster rating_keys for the browse-tile montage (browse union only)
+ total_runtime_s?: number; // total runtime of all items, for the tile sub-line (browse union only)
}
export interface PlexPlaylist {
id: number;