Files
siftlode/frontend/src/lib/feedView.ts
T
peter 944dedb218 fix(feed): keep the duration, live state and resume bar in the bare row
Dropping Thumb dropped everything it draws ON the image — duration, the
live/stream badges, the saved marker and the resume bar. Those are
metadata, not decoration, so they move into the meta line and the bar
onto the row. Splits the spec's overloaded compact flag into thumb/dense,
which meant one thing per family and nothing on its own.
2026-07-16 03:25:05 +02:00

63 lines
3.4 KiB
TypeScript

// The feed's view vocabulary. Lifted out of Feed (like feedSort.ts) because it has three
// consumers that can't share module-private state: App owns the pref, the toolbar's
// ViewSwitcher offers it, and VirtualFeed/VideoCard render it.
export type FeedView = "cards" | "cardsSmall" | "rows" | "rowsCompact" | "tiles";
export const FEED_VIEWS: readonly FeedView[] = [
"cards",
"cardsSmall",
"rows",
"rowsCompact",
"tiles",
] as const;
const FEED_VIEW_DEFAULT: FeedView = "cards";
export interface FeedViewSpec {
/** Which shape VideoCard renders: a portrait card (thumbnail on top) or a horizontal block. */
family: "card" | "row";
/** Narrowest grid column in px, or null for ONE full-width column. VirtualFeed derives the
* column count from this and the container width, so every multi-column view reflows for free. */
minCol: number | null;
/** Show the video thumbnail. False only for the bare row — whose badges (duration, live state,
* saved, resume bar) then move into the meta line, since Thumb is what draws them. */
thumb: boolean;
/** card family: tighter padding and no channel avatar, for the small card's ~180px column. */
dense: boolean;
/** row family: actions sit under the meta rather than docked at the right edge. Frees ~168px of
* width (measured), which is what lets a row block survive in a narrow tile column. */
actionsBelow: boolean;
}
// The whole matrix in one place. Adding a view here makes TS point at every switch that needs it.
export const FEED_VIEW_SPEC: Record<FeedView, FeedViewSpec> = {
cards: { family: "card", minCol: 260, thumb: true, dense: false, actionsBelow: false },
cardsSmall: { family: "card", minCol: 180, thumb: true, dense: true, actionsBelow: false },
// Single column on purpose: the point of the list is that your eye runs down one edge with the
// titles stacked. Density must not cost that — `tiles` is the multi-column option instead.
rows: { family: "row", minCol: null, thumb: true, dense: false, actionsBelow: false },
rowsCompact: { family: "row", minCol: null, thumb: false, dense: false, actionsBelow: false },
// 380 = thumbnail (160) + gap (12) + ~192 for the text + the block's own padding (16). The text
// floor is the action row's measured 156px plus enough for a title to be worth reading.
// Tuned by measuring the real layouts, not guessed: 3 columns need 3n+32 px, so 380 turns the
// channel page (1208px) and the unpinned feed into 3 columns while the filter-pinned feed
// (~955px) still gets 2. 470 was tried first and needed 956px for two — the common layout fell
// back to ONE column and the mode looked broken; 400 then left 596px tiles whose titles ended
// halfway, which is the very waste this mode exists to kill.
tiles: { family: "row", minCol: 380, thumb: true, dense: false, actionsBelow: true },
};
function isFeedView(v: unknown): v is FeedView {
return typeof v === "string" && (FEED_VIEWS as readonly string[]).includes(v);
}
/** Coerce a stored pref to a view. Up to 0.44 it was "grid" | "list"; those map forward here so
* nobody's saved choice is lost. Anything else (a corrupt value, a future view rolled back) falls
* back to the default rather than rendering nothing. */
export function parseFeedView(raw: unknown): FeedView {
if (isFeedView(raw)) return raw;
if (raw === "grid") return "cards";
if (raw === "list") return "rows";
return FEED_VIEW_DEFAULT;
}