From e2a9af37db3352a15690ec088358251bb61a2939 Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 5 May 2026 09:26:26 +0200 Subject: [PATCH] feat(embedded-mpv): keyboard shortcuts and double-click fullscreen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Entire-Checkpoint: f957cd9849e0 --- .../embedded-mpv-player.component.html | 1 + .../embedded-mpv-player.component.ts | 88 ++++++++++++++++++- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.html b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.html index ac4f28119..6322e5177 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.html +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.html @@ -10,6 +10,7 @@ (pointerenter)="onPlayerInteraction()" (pointermove)="onPlayerInteraction()" (click)="onPlayerInteraction()" + (dblclick)="onPlayerDblClick($event)" (focusin)="onPlayerInteraction()" >
diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts index 9733694da..47f9b35ba 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts @@ -194,12 +194,51 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy { } }; private readonly onDocumentKeydown = (event: KeyboardEvent) => { - if (event.key !== 'Escape') { + if (event.key === 'Escape') { + this.closePopovers(); return; } - this.closePopovers(); + if (this.shouldIgnoreShortcut(event)) { + return; + } + + switch (event.key) { + case ' ': + case 'k': + case 'K': + event.preventDefault(); + void this.togglePaused(); + return; + case 'f': + case 'F': + event.preventDefault(); + void this.toggleFullscreen(); + return; + case 'ArrowLeft': + event.preventDefault(); + void this.seekBy(-5); + return; + case 'ArrowRight': + event.preventDefault(); + void this.seekBy(5); + return; + case 'ArrowUp': + event.preventDefault(); + this.adjustVolume(0.05); + return; + case 'ArrowDown': + event.preventDefault(); + this.adjustVolume(-0.05); + return; + case 'm': + case 'M': + event.preventDefault(); + this.toggleMute(); + return; + } }; + private mutedVolume = 0; private readonly onFullscreenChange = () => { const playerRoot = this.playerRoot()?.nativeElement; this.isFullscreen.set( @@ -520,9 +559,37 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy { onVolumeInput(event: Event): void { const nextVolume = Number((event.target as HTMLInputElement).value); + this.applyVolume(nextVolume); + this.revealControls(false); + } + + onPlayerDblClick(event: MouseEvent): void { + const target = event.target as HTMLElement | null; + if (target?.closest('button, input, [role="slider"]')) { + return; + } + void this.toggleFullscreen(); + } + + private adjustVolume(delta: number): void { + const next = Math.max(0, Math.min(1, this.volume() + delta)); + this.applyVolume(next); + this.revealControls(); + } + + private toggleMute(): void { + if (this.volume() > 0) { + this.mutedVolume = this.volume(); + this.applyVolume(0); + } else { + this.applyVolume(this.mutedVolume || 0.5); + } + this.revealControls(); + } + + private applyVolume(nextVolume: number): void { this.volume.set(nextVolume); localStorage.setItem('volume', String(nextVolume)); - this.revealControls(false); const session = this.session(); if (!session?.id || !window.electron?.setEmbeddedMpvVolume) { @@ -539,6 +606,21 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy { .catch(() => undefined); } + private shouldIgnoreShortcut(event: KeyboardEvent): boolean { + if (this.overlayVisibility.overlayActive()) { + return true; + } + const target = event.target as HTMLElement | null; + if (!target) { + return false; + } + const tag = target.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') { + return true; + } + return target.isContentEditable; + } + async selectAudioTrack(trackId: number): Promise { const session = this.session(); if (!session?.id || !window.electron?.setEmbeddedMpvAudioTrack) {