From 620dd8178f31807dca005c5937f8b590995fd3c5 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 25 Oct 2026 02:43:01 +0100 Subject: [PATCH] feat(player): the side panel's list controls and watch toggle are glyphs on one line The queue row (previous, count, next, auto-advance, loop) wrapped onto three lines in the narrow side panel; glyph-only, it fits one. Mark watched joins the action icons as a toggle (check / double check). Every control keeps its full name and shortcut as its tooltip and accessible name; the docked bar keeps its labels. The related switch moves up to the toolbar rung (12px), and its second side reads just "Trending". --- docs/DESIGN.md | 6 +- .../features/player/PlayerModal.dom.test.tsx | 22 +++++ frontend/src/features/player/PlayerModal.tsx | 21 ++-- .../player/RelatedVideos.dom.test.tsx | 8 ++ .../src/features/player/RelatedVideos.tsx | 1 - .../features/player/playerModalControls.tsx | 96 +++++++++++++------ frontend/src/i18n/locales/en/player.json | 2 +- frontend/src/i18n/locales/hu/player.json | 2 +- 8 files changed, 114 insertions(+), 44 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 3cf78d66..e430a58c 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -559,7 +559,8 @@ of a full-width panel and nothing on the right. row is not a card and keeps its one play button: started, that button continues, its "…" has Continue + "Play from the beginning", and its picture carries the cards' progress bar. - **The YouTube modal:** its chrome uses the `Button` rungs (44 on touch); Previous / Next live in the panel - (its transport row, or the shut panel's strip) and on Shift+← / →, with no strips beside the card. Close is + (its transport row: glyphs only — Previous, the count, Next, auto-advance, loop on one line, each named in + its tooltip — or the shut panel's strip) and on Shift+← / →, with no strips beside the card. Close is an icon at the top right of the panel (wide: its head, or its strip) and at the end of the title row under the video (stacked). Wide, the panel shows the description outright, clamped to three lines with a More (`ClampedText`; a Tab onto a link below the cut opens it), and collapses to a 2.75rem @@ -576,7 +577,7 @@ of a full-width panel and nothing on the right. now is (owner's pick D1 a): its title, its own watched / playlist / download controls, its progress kept, marked watched at its end — and its end does not step the queue. What the user watched or hid is never in it; a list read so far with nothing unseen says so above Show more. Over it, a two-way - switch: on topic or Hungarian trending, kept on the account. Turned off on the instance (a daily + switch: on topic or trending (the Hungarian chart, labelled just "Trending"), kept on the account. Turned off on the instance (a daily limit of 0, owner's pick D2 a), there is no tab and no button; a refusal a retry cannot answer (the day's cap, turned off, the shared budget) offers no Retry. - **Clip mode (both players, owner's picks A + P1 + a):** a scissors in the control row (Plex: the C5 row, @@ -812,3 +813,4 @@ of a full-width panel and nothing on the right. | 2026-10-18 | Activity page (owner D1, D2, G1–G3 ★) | One page, "Napló" / "Activity": four tabs (Runs, Admin actions, Server log, Quota) under one filter bar (text, a period of 1 h · 24 h · 7 d · 30 d · 90 d defaulting to 7 days, an account), the view in the URL; runs and admin actions as dense rows that open in place as a log line does, filtered and paged on the server; a run's or a request's log lines one pivot away, as a history entry | One merged timeline; a page per source; a default period per tab; card rows; the audit log's client-side table | Each source keeps its own table and retention, read in one place; one bar keeps what you narrowed across tabs; about 1.8k runs a day need a log's density and a server-side filter | | 2026-10-24 | Related videos (owner U1–U3 ★, D1 A, D3) | On demand: a tab beside the description (wide), a drawer (stacked); the narrow thumbnailed row, its menu alone, a followed check; plays in place and the list stays the opened video's; an on-topic / Hungarian-trending switch kept on the account | Always shown; the end-of-video grid alone; a dialog; a big card or a text row; a list that follows the playing video | The modal is full on a small screen; a picture is what a choice among strangers rests on; a list that stays put is tried one by one, and spends no more of the day's cap | | 2026-10-25 | Related: one played; off (owner D1 a, D2 a) | Played from the list, it is the player's item: its title, controls and progress, marked watched at its end, the queue left where it was; a daily limit of 0 takes the tab and the button away | A linked video's in-place play (no controls, nothing kept); a tab that only shows a refusal | What is watched should count as watched, wherever it was started from; a control that can only be refused is not offered | +| 2026-10-25 | Player panel glyphs (owner 1A, 2A, 3) | In the side panel the queue row is glyph-only on one line, and Mark watched is a glyph toggle among the actions (✓ / ✓✓); the docked bar keeps its labels; the related switch at the default size, "Trending" | Labelled buttons wrapping onto three lines; glyphs on the docked bar too; Mark watched on its own line | The panel is narrow and the list controls are the same few every time; each glyph keeps its full name and shortcut in its tooltip; the docked bar has the width for words | diff --git a/frontend/src/features/player/PlayerModal.dom.test.tsx b/frontend/src/features/player/PlayerModal.dom.test.tsx index daf4d2bc..9ce63c93 100644 --- a/frontend/src/features/player/PlayerModal.dom.test.tsx +++ b/frontend/src/features/player/PlayerModal.dom.test.tsx @@ -509,6 +509,28 @@ describe("PlayerModal — no side strips", () => { expect(row.className).not.toMatch(/px-14/); }); + // The panel is narrow: worded, the queue row wrapped onto three lines. Each glyph keeps its name. + it("keeps the panel's queue row and the watch toggle to glyphs, each still named", () => { + vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {})); + const A = vid("A", 0); + const B = vid("B", 0); + renderModal( + {}} onState={() => {}} />, + ); + const named = [ + "player.previous", + "player.next", + /^player\.autoAdvance\.label: /, + /^player\.loop\.label: /, + "player.markWatched", + ]; + for (const name of named) { + const [button] = screen.getAllByRole("button", { name }); + expect(button!.textContent!.trim(), String(name)).toBe(""); + expect(button!.querySelector("svg"), String(name)).not.toBeNull(); + } + }); + it("closes from one icon-only button", () => { vi.spyOn(api, "videoDetail").mockReturnValue(new Promise(() => {})); const onClose = vi.fn(); diff --git a/frontend/src/features/player/PlayerModal.tsx b/frontend/src/features/player/PlayerModal.tsx index 46866de7..5549e69f 100644 --- a/frontend/src/features/player/PlayerModal.tsx +++ b/frontend/src/features/player/PlayerModal.tsx @@ -1298,7 +1298,7 @@ export default function PlayerModal({ )} {hasQueue && ( -
+
)} + setWatched(!watched)} + label={watched ? t("player.watchedUnmark") : t("player.markWatched")} + > + {watched ? : } + {onRemoveFromList && !related && ( )} -
)}
diff --git a/frontend/src/features/player/RelatedVideos.dom.test.tsx b/frontend/src/features/player/RelatedVideos.dom.test.tsx index 3b0374ed..f034257e 100644 --- a/frontend/src/features/player/RelatedVideos.dom.test.tsx +++ b/frontend/src/features/player/RelatedVideos.dom.test.tsx @@ -118,6 +118,14 @@ describe("RelatedVideos", () => { expect(saved).toHaveBeenCalledWith({ relatedRegion: "hu" }); }); + it("sets the region switch at the toolbar rung, not the 10px caption one", async () => { + vi.spyOn(api, "related").mockResolvedValue(page([row("A")])); + setup(); + await title("Video A"); + const track = screen.getByRole("group", { name: "player.related.region" }); + expect(track.className).toMatch(/\btext-xs\b/); + }); + // A plain pick resumes (null: the player knows the freshest position); the row's own position // travels on the video it hands over. it("plays a row from where the user left it", async () => { diff --git a/frontend/src/features/player/RelatedVideos.tsx b/frontend/src/features/player/RelatedVideos.tsx index 031032b9..1ce57d63 100644 --- a/frontend/src/features/player/RelatedVideos.tsx +++ b/frontend/src/features/player/RelatedVideos.tsx @@ -164,7 +164,6 @@ export function RelatedVideos({ value={region} onChange={setRegion} ariaLabel={t("player.related.region")} - size="sm" fullWidth className="mb-3" /> diff --git a/frontend/src/features/player/playerModalControls.tsx b/frontend/src/features/player/playerModalControls.tsx index 3dba6a2c..bf183621 100644 --- a/frontend/src/features/player/playerModalControls.tsx +++ b/frontend/src/features/player/playerModalControls.tsx @@ -4,6 +4,7 @@ import { Repeat, Repeat1, Shuffle, SkipBack, SkipForward } from "lucide-react"; import type { VideoDetail } from "../../lib/api"; import type { AutoMode, LoopMode } from "../../lib/playerModes"; import { Button } from "../../components/ui/Button"; +import { IconButton } from "../../components/ui/IconButton"; import { StateMessage } from "../../components/ui/QueryState"; import { ClampedText } from "../../components/ui/ClampedText"; import { renderDescription } from "./descriptionLinks"; @@ -14,9 +15,11 @@ import { renderDescription } from "./descriptionLinks"; /** The queue transport cluster — prev / counter / next, then the auto-advance and loop mode toggles. * Rendered in two places with identical logic: the modal's queue bar (`variant:"modal"`, normal glass - * theme, labels always shown) and the big-view docked control bar (`variant:"docked"`, white-on-dark, - * labels hidden until a breakpoint). Only the theme + label-visibility differ; the behaviour is one, - * so a mode/icon change lands once. The wrapper (fragment vs. bordered row) stays at the call site. */ + * theme, glyphs only so the whole cluster fits one line of the side panel) and the big-view docked + * control bar (`variant:"docked"`, white-on-dark, labels shown from a breakpoint up). Only the theme + + * label visibility differ; the behaviour is one, so a mode/icon change lands once. Every control keeps + * its full name (with the shortcut) as its tooltip and accessible name, so a glyph-only one still says + * what it does and which mode it is in. The wrapper (fragment vs. bordered row) stays at the call site. */ export function PlayerTransportControls({ variant, index, @@ -40,16 +43,61 @@ export function PlayerTransportControls({ }) { const { t } = useTranslation(); const docked = variant === "docked"; - // The docked bar sits on the video (always dark): the overlay variant; the modal's is a page surface. - const chrome = docked ? "overlay" : "ghost"; - const navLabel = docked ? "hidden sm:inline" : ""; const counter = docked ? "tabular-nums text-white/70" : "text-muted tabular-nums"; const divider = docked ? "h-4 w-px bg-white/20" : "w-px h-4 bg-border"; - const toggleLabel = docked ? "hidden md:inline" : ""; + const autoName = `${t("player.autoAdvance.label")}: ${t(`player.autoAdvance.${autoMode}`)}`; + const loopName = `${t("player.loop.label")}: ${t(`player.loop.${loopMode}`)}`; + const autoIcon = + autoMode === "prev" ? : autoMode === "random" ? : ; + const loopIcon = loopMode === "one" ? : ; + const count = ( + + {index + 1} / {queueLength} + + ); + if (!docked) + return ( + <> + + + + {count} + + + + + + {autoIcon} + + + {loopIcon} + + + ); return ( <> - - {index + 1} / {queueLength} - + {count} {/* Persistent playback settings — cycle on click, saved to your account. */} ); diff --git a/frontend/src/i18n/locales/en/player.json b/frontend/src/i18n/locales/en/player.json index 039ed69b..a64a1d25 100644 --- a/frontend/src/i18n/locales/en/player.json +++ b/frontend/src/i18n/locales/en/player.json @@ -31,7 +31,7 @@ "title": "Related", "region": "Which related videos", "topical": "On topic", - "hu": "Hungarian trending", + "hu": "Trending", "followed": "You follow this channel", "empty": "No related videos you haven't seen yet.", "emptySoFar": "Nothing you haven't seen among these yet — there are more.", diff --git a/frontend/src/i18n/locales/hu/player.json b/frontend/src/i18n/locales/hu/player.json index 5a8fc576..6ddbd0fd 100644 --- a/frontend/src/i18n/locales/hu/player.json +++ b/frontend/src/i18n/locales/hu/player.json @@ -31,7 +31,7 @@ "title": "Kapcsolódók", "region": "Milyen kapcsolódó videók", "topical": "Témába vágó", - "hu": "Magyar trend", + "hu": "Trend", "followed": "Követed ezt a csatornát", "empty": "Nincs olyan kapcsolódó videó, amit még nem láttál.", "emptySoFar": "Ezek közt még nincs olyan, amit nem láttál — van több is.",