mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
fix(player): keyboard-reachable, bounded labels for timeline segments (#1854)
Seek-bar segments (catch-up programmes, file chapters) now reach keyboard, touch and screen-reader users through the shared timelineSegments path: - aria-valuetext reads the translated "<title> · <time>" inside a titled segment, the plain time otherwise. - ControlsTimelineLabel anchors the label on keyboard focus and drag previews as well as pointer hover. - Two-part label (ellipsized title, whole time), clamped by its measured width and re-placed on resize to stay 8px inside the player. - Opaque --pc-timeline-track with white separators clamped to the track, so boundaries hold 3:1 over any frame. - Translated LIVE badge; LIVE and --:-- are named role="img" elements in both docks. - epglong Xtream mock scenario and an Electron E2E in both themes at 1280/800px and in de/ru. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1058f8d985
commit
015ea203b7
49 files changed
+1675
-278
No files matched your search
@@ -288,9 +288,11 @@ It owns only transient presentation behavior:
|
||||
- `ControlsShortcuts` — document keyboard routing;
|
||||
- `ControlsSurface` — pointer/click/double-click surface interactions;
|
||||
- `ControlsTimeline` — scrub state and timeline projections;
|
||||
- `ControlsTimelineHover` — the time under the pointer over the timeline;
|
||||
- `ControlsTimelineLabel` — where the timeline label points: the time under
|
||||
a hovering pointer, else the slider value while the keyboard or a drag
|
||||
moves it, plus the pure `clampTimelineLabelLeft` placement;
|
||||
- `app-player-timeline` — presentation of the timeline row (current time,
|
||||
segment track, knob, hover label, remaining time / LIVE, recording
|
||||
segment track, knob, label, remaining time / LIVE, recording
|
||||
status); scrub `input`/`change` events go back to the controls component,
|
||||
which owns reveal and seeking;
|
||||
- `ControlsLayout` — the compact/wide dock mode from the host's width;
|
||||
@@ -314,7 +316,10 @@ primary action and progress, cyan `#5cd6ff` for "something is on", violet
|
||||
`#6b7384` text ramp, and two reds: `--pc-live` `#d32f2f` fills the LIVE
|
||||
badge (white label 5.0:1), and `--pc-danger` `#ff5252` colours the active
|
||||
record glyph and the recording status (6.2:1 on the glass over a black
|
||||
frame). They are literal on purpose: the overlay is
|
||||
frame). The seek bar's unplayed track is the opaque `--pc-timeline-track`
|
||||
`#313437` and its segment separators are `--pc-timeline-separator` white,
|
||||
so their contrast does not depend on the frame (see Timeline segments).
|
||||
They are literal on purpose: the overlay is
|
||||
theme-independent (see the UI guidelines' player theme boundary), and the
|
||||
app's `--app-selection-color` is a different blue that would fight the video.
|
||||
Watch progress reads `--pc-progress`, the accent blue declared by the
|
||||
@@ -327,13 +332,20 @@ one colour in every player surface (UI guidelines, "Watch progress colour").
|
||||
a white knob ringed in translucent blue) · remaining time as `−7:03`
|
||||
(`formatRemainingTime`; the LIVE badge replaces it on live streams and
|
||||
`--:--` stands in while no duration is known) · the recording status.
|
||||
The badge text is the translated `LIVE_BADGE`, and both it and the
|
||||
`--:--` placeholder (`UNKNOWN_TIME_TEXT`) are `role="img"` elements named
|
||||
`LIVE_STREAM` / `DURATION_UNKNOWN`: a name on a role-less span is never
|
||||
announced, and the dashes are not words.
|
||||
The `<input type="range">` stays as the interaction and accessibility
|
||||
layer, invisible and full-size over the drawn track: dragging, arrow
|
||||
keys, `aria-valuetext` and the focus ring (drawn on the track through
|
||||
`:has(:focus-visible)`) all belong to it, so scrubbing semantics are
|
||||
unchanged. Hovering the bar with a mouse shows a white marker and a
|
||||
`1:40` label above the pointer (`ControlsTimelineHover`); touch never
|
||||
hovers and a non-seekable timeline never labels.
|
||||
`1:40` label above the pointer. Keyboard focus on the slider
|
||||
(`:focus-visible`, or any seek key) and a drag preview — the only way
|
||||
touch, which never hovers, gets it — show the same label at the slider
|
||||
value instead, without the marker (`ControlsTimelineLabel`; a hovering
|
||||
pointer wins). A non-seekable timeline never labels.
|
||||
- **Control row**: `minmax(0,1fr) auto minmax(0,1fr)`. Left: the volume
|
||||
button, with the slider **inline** (72px) in the wide mode and behind
|
||||
the hover/tap popover in the compact mode — inline, the button is a
|
||||
@@ -359,13 +371,12 @@ one colour in every player surface (UI guidelines, "Watch progress colour").
|
||||
|
||||
### Timeline segments
|
||||
|
||||
The track is drawn as a row of segments, one flex item per segment with
|
||||
`flex-grow` equal to its share of the duration and its own accent fill, so
|
||||
The track is drawn as a row of segments, each with its own accent fill, so
|
||||
a film's chapters or a catch-up recording's programmes read directly off
|
||||
the bar. Each segment is placed absolutely at its time position (`left` =
|
||||
start percent, `width` = share minus the 3px gap every segment but the
|
||||
start percent, `width` = share minus the 2px gap every segment but the
|
||||
last keeps), so a drawn boundary sits exactly where the linear seek input
|
||||
and the hover label change segment; a segment shorter than the gap
|
||||
and the label change segment; a segment shorter than the gap
|
||||
collapses instead of pushing its neighbours. The optional `timelineSegments` input
|
||||
(`PlayerTimelineSegment { startSeconds, endSeconds, title }`) supplies
|
||||
them; `normalizeTimelineSegments` (`controls-timeline-segments.ts`) clamps
|
||||
@@ -374,9 +385,36 @@ the previous end and fills every gap with an untitled segment so the row
|
||||
always covers `[0, duration]`. Without segments — live playback, VOD and
|
||||
series — the row is one untitled segment, which is the plain bar.
|
||||
`ControlsTimeline` owns the normalized list and the per-segment fill for the
|
||||
current scrub or playback value; the hover label becomes `Chapter 2 · 12:40`
|
||||
current scrub or playback value; the label becomes `Chapter 2 · 12:40`
|
||||
over a titled segment. mpv's chapter list is not a producer yet.
|
||||
|
||||
Boundaries hold 3:1 over any frame. Segments only round the bar's two
|
||||
ends, and every gap (2px, `TIMELINE_SEGMENT_GAP_PX`) is filled by a
|
||||
separator (`separatorLeft`, never left of the track's start, where a
|
||||
boundary in the bar's first pixels would put it) in
|
||||
`--pc-timeline-separator` white: 12.5:1 on
|
||||
the opaque `--pc-timeline-track`, 3.2:1 on the `--pc-progress` fill, which
|
||||
keeps 4.0:1 on the track. The former translucent track with see-through
|
||||
gaps measured 1.3–1.6:1 at its boundaries, and its fill fell to 1.1:1 over
|
||||
a bright frame (`player-controls.palette.spec.ts` pins the ratios).
|
||||
|
||||
The names reach every user, whichever source drew the segments. The
|
||||
slider's `aria-valuetext` is the translated `TIMELINE_SEGMENT_POSITION`
|
||||
(`{{title}} · {{time}}`) whenever the scrub or playback value — the
|
||||
keyboard's target included — lies in a titled segment
|
||||
(`ControlsTimeline.segmentTitle`), and the plain time otherwise. The visible
|
||||
label (`aria-hidden`; it mirrors the valuetext) is two parts: the title
|
||||
ellipsizes at `min(320px, 60cqw)` and the time never truncates. The
|
||||
stylesheet centres it on its anchor; once laid out,
|
||||
`PlayerTimelineComponent` moves it by its measured width
|
||||
(`clampTimelineLabelLeft`, an `afterRenderEffect`) so it stays 8px inside
|
||||
the player (`.player-controls-host`) — past the bar's ends when needed, as
|
||||
the bar is narrower than the player. A `ResizeObserver` on the bar and the
|
||||
player re-places it when only the size changes (a paused player whose
|
||||
slider the keyboard holds, in a narrowed window). `xtream-catchup-timeline-label.e2e.ts`
|
||||
tabs to the bar with a 120-character programme title at 1280 and 800px, in
|
||||
both themes and in de and ru.
|
||||
|
||||
**Catch-up producer.** Archive playback of a live channel passes the EPG
|
||||
programmes overlapping its archive window.
|
||||
`buildCatchupTimelineSegments(programmes, activeProgramme, windowEnd?)`
|
||||
@@ -1661,7 +1699,7 @@ libs/ui/playback/src/lib/player-controls/
|
||||
├── controls-feedback.ts
|
||||
├── controls-format.utils.ts
|
||||
├── controls-layout.ts
|
||||
├── controls-timeline-hover.ts
|
||||
├── controls-timeline-label.ts
|
||||
├── controls-timeline-segments.ts
|
||||
├── controls-settings.ts
|
||||
├── controls-settings-groups.ts
|
||||
|
||||
@@ -401,6 +401,7 @@ sometimes only respond to that misspelled action.
|
||||
| `series:series` | 2002 | live:3, vod:4, series:15 | 30 | active |
|
||||
| `minimal:minimal` | 3003 | 2 each | 5 | active |
|
||||
| `epg:epg` | 6006 | live:2, vod:1, series:1 | 3 | active |
|
||||
| `epglong:epglong` | 6006 | live:2, vod:1, series:1 | 3 | active |
|
||||
| `tzoffset:tzoffset` | 6006 | live:2, vod:1, series:1 | 3 | active |
|
||||
| `emptyvod:emptyvod` | 7007 | 2 each | 5 | active |
|
||||
| `marketing:marketing` | 8020 | live:4, vod:4, series:4 | curated | active |
|
||||
@@ -421,6 +422,12 @@ That deliberate mismatch lets Electron tests verify the renderer uses timestamp
|
||||
fields for sorting, current-program selection, progress bars, and local clock
|
||||
labels instead of trusting provider-local strings.
|
||||
|
||||
Timeshift URLs of `epg` and `epglong` play the local MPEG-TS clip
|
||||
(`src/fixtures/live.mpegts`). `epglong:epglong` is the same fixture with
|
||||
`LONG_PROGRAMME_TITLE`, a 120-character title, on the past programme, for
|
||||
the catch-up seek bar's label bounds; `epg:epg` keeps its short titles,
|
||||
which other tests assert verbatim.
|
||||
|
||||
### `marketing:marketing` fixture details
|
||||
|
||||
This scenario is reserved for release screenshots and marketing materials:
|
||||
|
||||
Reference in new issue
Block a user