Commit Graph
216 Commits
Author SHA1 Message Date
4gray a1b0d9186c chore(test): clean lint and warning baseline 2026-05-26 19:30:21 +02:00
4gray 3d4d2ca9bf fix(angular): remove template diagnostics warnings (#1011)
* fix(angular): remove template diagnostics warnings

* fix(angular): preserve template fallback behavior
2026-05-26 18:33:44 +02:00
4gray 697eab6e73 refactor(epg): route renderer calls through runtime bridge
Add typed EPG runtime bridge, split EPG runtime capabilities, migrate renderer EPG callers away from direct window.electron access, and address Greptile review feedback.
2026-05-24 13:45:51 +03:00
4gray 003a8774a1 fix(electron): tighten navigation review feedback
Restrict packaged file navigation to the app renderer, guard redirects, and handle fire-and-forget header override IPC failures.
2026-05-23 19:05:38 +03:00
4gray fc449ca73e feat(electron): harden renderer security
Scope playback request header overrides to active stream origins and document the Electron runtime security contract.
2026-05-22 23:31:41 +03:00
4gray ed8680116c fix(runtime): address consolidated review feedback 2026-05-22 14:02:56 +03:00
4gray 03586d934a refactor(channels): gate list epg by runtime capability 2026-05-22 13:33:31 +03:00
4gray 54d29f2239 refactor(epg): gate multi epg queries by runtime capability 2026-05-22 13:33:31 +03:00
4gray 030c6ef89d refactor(epg): gate source freshness by runtime capability 2026-05-22 13:33:31 +03:00
4gray 5827ef05f0 refactor(playback): use runtime capability for external fallback 2026-05-22 12:45:09 +03:00
4gray 36bce47764 merge: resolve master conflicts for pwa hardening
- merge origin/master into PR #964 and keep embedded MPV test on the isolated playback sub-entrypoint

- centralize EPG capability through DataService.supportsEpg and update PWA web-e2e expectations

- split BrowserAccessError copy between Electron and PWA diagnostics
2026-05-22 10:20:03 +03:00
4gray 4ab8915483 chore(web): enable strict TypeScript mode 2026-05-22 02:58:12 +03:00
4gray 55efc24608 fix(pwa): harden self-hosted runtime boundaries 2026-05-22 02:09:57 +03:00
4gray 2ecb5c28f4 fix(dashboard): address PR review feedback 2026-05-22 00:59:17 +03:00
4grayandClaude Opus 4.7 6891a1ca30 i18n(epg): translate multi-EPG toolbar tooltips
The Zoom/Filter/Search Programs buttons in the Multi-EPG header
hard-coded English matTooltip strings, so they never localized — even
when the rest of the toolbar (Close, Previous/Next day) used keys. Adds
six new EPG.* keys (ZOOM_IN, ZOOM_OUT, FILTER_CHANNELS, CLOSE_FILTER,
SEARCH_PROGRAMS, CLOSE_PROGRAM_SEARCH), wires both [matTooltip] and
[attr.aria-label] through `| translate`, and fans the strings out to
all 17 non-English locales via the i18n-fill pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:15:35 +02:00
4grayandClaude Opus 4.7 55215a5059 feat(radio): add tooltips + aria-labels on every player button
Radio audio-player buttons (skip-prev, play/pause FAB, skip-next, mute,
volume slider) had zero matTooltip or aria-label. The UX audit flagged
this: "Skip-prev / Skip-next — what do they skip? In radio, is the
next 'track' the next station? Next in favourites? Random? Without a
tooltip the buttons trade silently." Same critique applied to mute
and play/pause for screen-reader users.

Add hover tooltips and screen-reader labels on all 5 controls. Skip
prev/next say "Previous station" / "Next station" so the behaviour is
explicit (they walk the active filtered list). Play/Pause flips its
label with state. Mute/unmute flips when isMuted() || volume === 0.
Volume slider gets its own label so the slider thumb is announced
correctly.

i18n: 7 new AUDIO_PLAYER.* keys translated across all 17 non-English
locales by per-locale agents; all 18 locales pass coverage and
placeholder integrity checks.

52/52 ui-playback tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:03:14 +02:00
4grayandClaude Opus 4.7 7399fbfa9e refactor(multi-epg): match app theme, widen channels, label the now-line
Four UX-audit fixes for the Multi-channel EPG view:

1. Theme harmonization
   - $bg-deep / $bg-surface / $bg-elevated / $bg-hover now resolve through
     --app-content-bg / --app-widget-bg / --app-widget-header-bg /
     --app-card-hover-bg. Multi-EPG was the only screen that switched to
     near-black (#0a0a0f) instead of sitting on the app's dark surface, so
     the audit described it as "visually detaching from the rest of the
     app". It now inherits the same dark graphite chain.
   - Cleaned up 29 remaining hard-coded rgba(139,92,246,…) purples and
     rgba(0,212,170,…) teal accents that survived the earlier color
     unification commit. Purple shadows/glows are gone; the cyan glow
     used by the now-line is now driven by the unified $5cd6ff token.

2. Channel column widened 140px → 180px
   - Long names like "13th Street Universal HD" no longer truncate to 5
     characters. Logo size dropped to 24px so the name actually has room.
     The label allows up to two lines and wraps cleanly on long titles.
   - Responsive breakpoint mirrors the change: 100px → 140px at <768px.

3. Now-line time badge
   - New `currentTimeLabel` computed signal renders an "HH:MM" pill
     pinned to the top of the now-line. Recomputes on the same 60s tick
     as the line position so they always stay in lockstep.

4. "Airing now" program highlight
   - New `isProgramAiringNow(program)` returns true when the now-line's
     x-coordinate falls inside [startPosition, startPosition + width].
     Programs that match get a .is-now class → 1.5px cyan border. Users
     can now see what's on every channel at a glance without tracing the
     line down each row.

Toolbar harmonization (audit item 3 for this screen) intentionally
deferred — that's part of the app-wide toolbar unification work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:21:10 +02:00
4grayandClaude Opus 4.7 a27a29fcf2 refactor(ui): unify primary color to blue across all surfaces
Swap M3 primary palette from violet to azure so checkboxes, radio
buttons, raised CTAs, and active states read as the same blue used by
the rail selection token. Cascading template + SCSS updates align the
remaining hand-rolled surfaces (Add Playlist dialog, VOD play button,
radio player, multi-EPG, empty-state CTAs) with the unified system.

LIVE stays red (broadcast role), cyan stays on EPG "now" indicator,
green stays on completed-download — semantic colors keep their meaning;
only the indiscriminate accent uses get folded into the blue primary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 00:12:34 +02:00
4gray 20e7322353 refactor(portal): split collection data access (#953) 2026-05-19 00:01:34 +02:00
4gray 118d4fce32 fix(playback): improve inline stream diagnostics (#952)
* fix(playback): improve inline stream diagnostics

* fix(playback): preserve headers in diagnostics wrapper

* fix(portal): align unified live player imports
2026-05-18 23:59:20 +02:00
4gray 5e5a89a300 refactor(ui): remove dead portal code and share detail styles (#946) 2026-05-16 00:09:29 +02:00
4gray 2d5c4fa4f9 chore: tighten validation and runtime logging
* chore: tighten validation and runtime logging

* fix(i18n): localize new settings labels
2026-05-15 17:43:42 +02:00
4gray d24c77a143 chore(nx): enforce scoped workspace boundaries (#942) 2026-05-15 09:59:06 +02:00
4gray 8f79b25f52 fix(playback): treat portal mpeg-ts vod as non-live 2026-05-15 01:07:23 +02:00
4gray 4a4334cb64 feat(playback): improve browser-player diagnostics (#939)
* feat(playback): diagnose browser stream access errors
Entire-Checkpoint: f957cd9849e0

* feat(playback): expose browser-player diagnostic details
Entire-Checkpoint: f957cd9849e0

* fix(playback): keep media path extensions authoritative
Entire-Checkpoint: f957cd9849e0

* fix(playback): tighten browser access diagnostics
Entire-Checkpoint: f957cd9849e0

* fix(playback): align diagnostic followups
Entire-Checkpoint: f957cd9849e0

* fix(playback): narrow declared stream metadata
Entire-Checkpoint: f957cd9849e0

* fix(playback): preserve explicit extension overrides
Entire-Checkpoint: f957cd9849e0

* fix(playback): keep mpeg-ts out of unsupported containers
Entire-Checkpoint: f957cd9849e0

* refactor(playback): split diagnostics utilities
Entire-Checkpoint: f957cd9849e0

* fix(playback): treat mpegts early eof as media fallback
Entire-Checkpoint: f957cd9849e0
2026-05-15 00:55:23 +02:00
4gray db08ec2958 fix(playback): keep diagnostics within player viewport
Entire-Checkpoint: f957cd9849e0
2026-05-14 20:49:40 +02:00
Toni Orioland4gray 2f9aef1578 fix(playback): handle IPTV proxy URLs without file extensions (#934)
* fix(playback): handle IPTV proxy URLs without file extensions

IPTV proxy servers (Acexy, Xtream Codes, etc.) use query-string URLs
like `https://proxy.example.com/ace/getstream?infohash=abc` that return
raw MPEG-TS streams without a file extension in the path.

`getExtensionFromUrl()` was returning garbage for these URLs (e.g.
`com/ace/getstream` from splitting on dots across path segments). All
four player backends then misrouted the stream — typically to HLS.js
which failed because the response is raw MPEG-TS, not an HLS manifest.

Root fix: rewrite `getExtensionFromUrl()` to extract the extension from
the last path segment only, returning `undefined` when there is no
dot-separated extension.

Player adjustments: treat `undefined` extension as MPEG-TS (the most
common format for live IPTV proxy streams) across all four backends
(HTML5, ArtPlayer, VideoJs, web-player-view).

* test(m3u-utils): cover URL extension parsing

* fix(playback): preserve query-declared stream extensions

---------

Co-authored-by: 4gray <serega05@gmail.com>
2026-05-14 00:13:08 +02:00
4gray 9ae984ad7f feat(portal): add series quick start CTA (#925)
* feat(portal): add series quick start CTA
Entire-Checkpoint: f957cd9849e0

* fix(portal): address series quick start review
Entire-Checkpoint: f957cd9849e0

* docs: use repo-relative quick start path
Entire-Checkpoint: f957cd9849e0
2026-05-12 23:45:02 +02:00
4gray 6ae1251ad1 fix(playback): use inline player for collection VOD (#928)
* feat(portal): add series quick start CTA
Entire-Checkpoint: f957cd9849e0

* fix(playback): use inline player for collection vod

* fix(playback): address inline collection review feedback

* fix(playback): ignore stale vod position loads
2026-05-12 19:26:40 +02:00
4gray f922dd2d00 style: adjust size and remove unnecessary styles for action buttons
Entire-Checkpoint: f957cd9849e0
2026-05-10 23:37:15 +02:00
4gray 6305dcf00e Add browser playback diagnostics and MPV/VLC fallback (#921)
* feat(playback): add codec diagnostics fallback
Entire-Checkpoint: f957cd9849e0

* fix(playback): address diagnostics review feedback
Entire-Checkpoint: f957cd9849e0

* fix(playback): ignore recoverable hls diagnostics
Entire-Checkpoint: f957cd9849e0

* fix(playback): polish diagnostic fallback banner
Entire-Checkpoint: f957cd9849e0

* fix(xtream): preserve live category search scope
Entire-Checkpoint: f957cd9849e0
2026-05-10 17:27:54 +02:00
4gray 5f4cf01467 fix(playback): avoid redundant VideoJS source resets (#920)
* fix(playback): avoid redundant VideoJS source resets

* fix(playback): clean up cleared VideoJS sources

* refactor(playback): use signal APIs in VideoJS player

closes #608
2026-05-10 15:32:00 +02:00
4gray 724e4b1ab3 feat(playback): add embedded mpv (macos) stream recording (#916)
* feat(playback): add embedded mpv stream recording
Entire-Checkpoint: f957cd9849e0

* fix(playback): address embedded mpv recording review
Entire-Checkpoint: f957cd9849e0

* fix(playback): track mpv recording auto-stop replies
Entire-Checkpoint: f957cd9849e0
2026-05-10 12:27:09 +02:00
4gray 0d93301520 fix(stalker): address radio review feedback
Entire-Checkpoint: f957cd9849e0
2026-05-09 13:08:41 +02:00
4gray ab905af285 feat(stalker): add radio support
Entire-Checkpoint: f957cd9849e0
2026-05-09 01:28:39 +02:00
Tedd Johnson 626948f2f5 Add double-click stream open setting 2026-05-07 20:03:33 -07:00
4gray ed6302c059 feat(m3u): add channel list sorting controls
Entire-Checkpoint: f957cd9849e0
2026-05-06 00:14:52 +02:00
4grayandClaude Opus 4.7 42f718568e fix(embedded-mpv): tighten effect signal-tracking to prevent latent re-run bugs
Audit followup to the support-loop and volume-restart fixes. Two more
effects pulled in transitive signal deps that would have caused the same
class of regression the next time the surrounding helpers grew.

1. Component session-fan-out effect: the body called
   scheduleControlsHide(), which reads isPlaying/menus.anyOpen/statusLabel/
   controlsVisible. Those became tracked deps of the effect, so opening a
   popover, pausing, or hovering re-ran the whole body — re-emitting
   timeUpdate. If a parent ever wires timeUpdate back into
   playback.startTime as a "resume where I left off" feature, this would
   have been the next stream-restart bug. Wrap the side-effect block in
   untracked() so the effect only listens to session changes.

2. Controller stalled-tracker effect: tracked the full session signal
   even though only status was needed. The session payload updates ~2 Hz
   during playback (positionSeconds advances), making the effect re-run
   constantly to call a no-op. Add a sessionStatus computed and track
   that instead — fires only on real status transitions.

No behavior change for current users; both fixes are preventative.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 16:31:25 +02:00
4grayandClaude Opus 4.7 c12b660594 fix(embedded-mpv): do not restart the stream on every volume change
Same root cause as the loading-loop fix: the session-creation effect
read this.volume() while building startSession's initialVolume argument,
which made volume a tracked dependency of the effect. Each volume tick
re-ran the effect, the cleanup disposed the active session, and a fresh
one was created — which for VOD/series meant restarting playback from
the beginning.

Read volume via untracked() inside the effect. The value is only needed
once at session creation; subsequent volume changes flow through
controller.applyVolume() and never go near the effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 10:55:09 +02:00
4grayandClaude Opus 4.7 6ab62cfd8d fix(embedded-mpv): stop re-setting support inside startSession (infinite reload loop)
Regression introduced in the bundle 3 refactor. After calling
prepareEmbeddedMpv, the controller wrote the response back into
this.support — but the component's session-creation effect tracks
this.support(), so updating it cleaned up the just-created session and
ran startSession again, which prepared again, set support again, and so
on. Net effect: endless "Loading stream…" spinner because every session
was disposed before MPV could finish loading the file.

The initial loadSupport() in the constructor already populates support
(including capabilities). Drop the redundant set in startSession; leave
a comment explaining why so it does not get re-added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 10:41:59 +02:00
4grayandClaude Opus 4.7 b5dbc33e62 fix(embedded-mpv): make slider track and thumb visible
The timeline and volume sliders were rendering invisibly because:

- --embedded-mpv-accent resolved to var(--mat-sys-primary) with no
  fallback. If the Material 3 theme tokens did not cascade into the
  component scope, the variable was empty and both the thumb and the
  Firefox progress fill rendered with no color.
- The track contrast was 22% of var(--mat-sys-on-surface) which has the
  same scoping fragility on top of being subtle even when it does
  resolve.
- The thumb had no border, so even when accent was present it could
  blend into similarly-toned popover backgrounds.
- The played portion of the timeline had no fill in Chromium because
  ::-webkit-slider-runnable-track does not support a native progress
  state.

Fixes:

- Add hard fallbacks on every theme-token reference (accent falls back to
  #7dd3fc, surface to #0f1620, outline-variant to a translucent white).
- Add --embedded-mpv-track and --embedded-mpv-thumb-ring as theme-
  independent CSS variables so the slider stays legible regardless of
  the parent theme provider.
- Give the thumb a 2 px white ring border + a stronger drop shadow so it
  reads on any background (panel glass, dark backdrop, accent-coloured
  area).
- Implement played-progress fill via a linear-gradient on
  ::-webkit-slider-runnable-track keyed off a CSS variable
  --slider-progress, set per-slider from the template based on the
  current playback position / volume. Firefox uses native
  ::-moz-range-progress.
- Tidy disabled state styling so live-stream timelines (no duration)
  are dim but still visible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 10:33:18 +02:00
4grayandClaude Opus 4.7 9424dedafc fix(embedded-mpv): stop crashing on IPC for not-yet-created sessions
The renderer was reading session.id and forwarding it to the addon, but
during the loading window that id is the placeholder
"embedded-mpv-starting" set by createLoadingSession. If the user adjusted
volume, seeked, or toggled audio/subtitle/speed/aspect before the addon's
createSession returned the real id, that placeholder id reached the
addon — and the addon's getSessionOrThrow threw a raw std::runtime_error
which libc++abi terminated the process on.

Two fixes, defense in depth:

1. Renderer (session controller): use the canonical sessionId() signal,
   which is null until the addon hands back a real id, as the gate for all
   IPC calls. Wrap every IPC call in a guardIpc helper that swallows
   addon-side throws so a torn-down session or race won't surface as an
   uncaught promise rejection.

2. Native (embedded_mpv.mm): change getSessionOrThrow to take a
   Napi::Env and throw Napi::Error::New(env, ...) instead of
   std::runtime_error. node-addon-api converts Napi::Error to a JS
   exception cleanly; the previous std::runtime_error escaped the C++
   frame and aborted the process when the addon was built without
   NAPI_CPP_EXCEPTIONS translation. Refactor splits findSession (returns
   nullptr) from getSessionOrThrow (env-aware) so call paths that just
   probe a session's existence don't pay the throw cost.

The native fix needs an addon rebuild to take effect; the renderer fix
prevents the crash trigger immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 10:29:23 +02:00
4grayandClaude Opus 4.7 126025aa41 fix(embedded-mpv): expose popover area by shrinking MPV view, not just hiding
Same root cause as the modal-dialog occlusion: control popovers (volume,
audio, subtitle, speed, aspect) extend upward from the controls strip into
the area covered by the MPV NSView. They render in DOM but the native view
paints over them.

Replace the simple boolean overlayActiveProvider with a richer
boundsProvider closure on the session controller. The component drives it
from both the modal overlay state and the popover menu state:

- Modal dialog open (command palette, MatDialog) -> HIDDEN_BOUNDS, MPV
  fully off-screen so the dialog has the whole window.
- Popover menu open -> shrink MPV from the bottom by 300 px so the popover
  region lives in DOM-receiving space; video keeps playing in the upper
  region instead of disappearing entirely.
- Otherwise -> full host bounds.

Bounds-resync effect now tracks menus.anyOpen() in addition to
overlayActive() so opening or closing a popover triggers an immediate
re-sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 10:16:33 +02:00
4grayandClaude Opus 4.7 0249a3a9f7 refactor(embedded-mpv): split player component into focused units
The component had grown to ~1000 lines with session lifecycle, IPC plumbing,
keyboard shortcuts, popover state, formatters, and view orchestration all
living in one file. Per CLAUDE.md's 350–400 line hard cap, split into:

- embedded-mpv-format.utils.ts (~100): pure helpers (formatTime, track
  labels, volume icon/label, persisted-volume access, measureBounds) and
  preset constants (SPEED_PRESETS, ASPECT_PRESETS, HIDDEN_BOUNDS).
- embedded-mpv-shortcuts.ts (~90): EmbeddedMpvShortcuts class that owns the
  document keydown listener and routes through a handler interface; the
  component just provides callbacks.
- embedded-mpv-ui-state.ts (~110): EmbeddedMpvMenuState (single-open
  popover state machine with toggle/open/close/closeAll + anyOpen signal)
  and EmbeddedMpvFeedback (transient overlay with auto-clearing flash).
- embedded-mpv-session-controller.ts (~395): component-scoped Injectable
  that owns support/session/sessionId/stalled/retryToken signals, the
  session-update IPC subscription, bounds-sync (resize, scroll, overlay
  state), the stalled timer, and all per-session IPC operations
  (togglePaused, seekBy, seekTo, applyVolume, setAudioTrack,
  setSubtitleTrack, setSpeed, setAspect, retry).
- embedded-mpv-player.component.ts (now ~540, was ~1000): view-only
  orchestration — view children, derived computed signals, DOM event
  listeners (pointerdown/pointermove/fullscreenchange/dblclick), and three
  effects (session start/teardown, overlay-active bounds sync, session
  payload → volume/timeUpdate fan-out).

No behavior changes. Build clean, electron-backend tests still pass.
Component is still over the 400-line cap but the bulk of its size is now
the necessarily-coupled-to-view computed signals and constructor effects;
the remaining over-cap delta is structural to a player root.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 10:09:31 +02:00
4grayandClaude Opus 4.7 8e3f559033 feat(embedded-mpv): subtitles, playback speed, aspect override, retry on error
Native addon (apps/electron-backend/native/src/embedded_mpv.mm)
- Extend SessionSnapshot with subtitleTracks, selectedSubtitleTrackId,
  playbackSpeed, aspectOverride.
- Refactor track parsing into a shared updateTracksFromNode helper that
  filters by mpv "type" so audio and subtitle tracks share the code path.
- Observe sid, speed, video-aspect-override; clear sub state on
  MPV_EVENT_START_FILE.
- Export setSubtitleTrack (handles trackId === -1 as "no" to disable),
  setSpeed (clamped to 0.25–4.0), setAspect (passthrough string for
  video-aspect-override).
- Snapshot output now includes the new fields.

Service (embedded-mpv-native.service.ts) + IPC + preload
- Mirror methods on EmbeddedMpvNativeService with capability detection: each
  method throws a descriptive error if the loaded addon doesn't expose the
  underlying native function (i.e. user is running an older build).
- New IPC channels EMBEDDED_MPV_SET_SUBTITLE_TRACK, _SET_SPEED, _SET_ASPECT
  registered in events file and exposed via preload.
- Extend EmbeddedMpvSupport with a capabilities probe so the renderer can
  hide controls for features the current addon build doesn't ship.

Renderer (embedded-mpv-player.component.{ts,html})
- Three new popovers anchored above their buttons (subtitle / speed /
  aspect), gated by capabilities() and (for subtitles) by track count.
- Subtitle popover includes an Off entry; speed/aspect use fixed presets.
- Error state now surfaces the same overlay as the stalled state, with a
  Retry button that bumps the existing retryNonce signal — covers #8 from
  the audit.
- All session-payload defaults (loading stub, error stub, refresh fallback,
  dispose payload, native createSession default) updated for the new
  required fields.

NOTE: Existing addon binaries do not expose the new methods. Until the
addon is rebuilt (pnpm run serve:backend:embedded-mpv or the release
build script), capabilities will report subtitles/playbackSpeed/
aspectOverride as false and the new buttons will simply not appear.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 09:53:32 +02:00
4grayandClaude Opus 4.7 dea1d40c3d feat(embedded-mpv): polish bundle — popovers, mute, retry, feedback
- Volume button now toggles mute on click; the slider opens via hover/focus
  in an absolute popover anchored above the button (with hover-bridge so the
  cursor can reach it without crossing a dead zone). Wheel over the volume
  area adjusts volume.
- Audio track menu likewise becomes a click-toggle popover anchored to its
  button instead of a panel takeover; the back-arrow / mode-panel
  scaffolding is gone.
- Slider gets explicit thumb/track styling for both WebKit and Firefox so
  it reads as part of the design system instead of a raw native control;
  aria-valuetext on the timeline announces formatted time.
- Stalled overlay: if status remains "loading" for 30 s, surface a centered
  warning with a Retry button that disposes the session and recreates it
  via a retryNonce signal the playback effect tracks.
- Keypress feedback overlay: ←/→ seek, ↑/↓ volume, M mute now flash a
  centered pill with the icon + delta so the action is visible (especially
  important in fullscreen where the controls are hidden).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 09:39:19 +02:00
4grayandClaude Opus 4.7 e2a9af37db feat(embedded-mpv): keyboard shortcuts and double-click fullscreen
Add the standard set of player shortcuts to match the audio/HTML video
players already in the app:

- Space, K   — play/pause
- F          — toggle fullscreen
- Left/Right — seek -5 s / +5 s
- Up/Down    — volume +/- 5 %
- M          — toggle mute (restores prior volume)

Shortcuts are ignored while focus is in an input/select/textarea or while
any MatDialog (e.g. command palette) is open, so the palette's filter and
dialog inputs keep their keys. Volume changes route through a shared
applyVolume helper so keyboard/wheel/slider all persist to localStorage
and the active session.

Also bind dblclick on the player root to toggle fullscreen, with a guard
so double-clicks on buttons/sliders don't trigger it. Note: the native
MPV NSView intercepts mouse events over the video itself, so dblclick
only fires on DOM-receiving regions (currently the controls strip).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 09:26:26 +02:00
4grayandClaude Opus 4.7 32779f99a4 fix(embedded-mpv): detect MatDialog overlays, not just CDK backdrops
The command palette is a MatDialog with a transparent backdrop, and the
prior MutationObserver-only detector did not reliably catch it across
overlay-container instantiation timing — the player kept rendering at full
bounds while the palette was open and continued occluding it. Subscribe
directly to MatDialog.afterOpened / afterAllClosed for the authoritative
"any modal dialog open" signal, and keep the OverlayContainer mutation
observer as a fallback for non-dialog CDK overlays. Either signal flips
overlayActive, hiding the embedded MPV view as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 09:23:32 +02:00
4grayandClaude Opus 4.7 2b0287c2f1 fix(embedded-mpv): reserve controls strip so hover reveal works
The native MPV NSView sits above the WebContents and is sized to the
viewport rect, so any DOM region it covers never receives pointer events.
Bundle 1 made the viewport fill the whole player, leaving no DOM strip to
catch pointermove — once the controls faded out there was no way to bring
them back via hover. Reapply the bottom inset on the viewport, scoped to
the slimmer 64 px panel, so the bottom strip is DOM-only and hover wakes
the controls. Swap the gradient for the glass surface treatment now that
the panel no longer needs to fade into video.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 09:14:35 +02:00
4grayandClaude Opus 4.7 2b57f3a35d style(embedded-mpv): align chrome with app theme and fix viewport crop
- Switch palette and surfaces to Material 3 tokens (--mat-sys-*) so the
  player respects the app theme instead of a hardcoded cyan/sky scheme.
- Stop reserving 96 px below the viewport for the control panel; the panel
  now floats over the bottom with a gradient fade so the video fills the
  frame (especially in fullscreen).
- Drop control panel height from 96 to 64 px (88 px on narrow widths).
- Replace the corner "Loading stream in MPV..." pill with a centered
  spinner + concise label during load; move the transient status toast
  to the top-right and add role="status" aria-live.
- Add a red badge with pulsing dot for live streams in place of the plain
  "LIVE" text label.
- Apply font-variant-numeric: tabular-nums to time/volume readouts so
  digits stop jittering as playback advances.
- Slightly loosen transport/actions gaps for a less cramped rhythm and
  use ease-out easing on control fade.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 09:10:58 +02:00