mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 04:26:16 -08:00
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
This commit is contained in:
1 parent
32779f99a4
commit
e2a9af37db
2 files changed
+86
-3
No files matched your search
@@ -10,6 +10,7 @@
|
||||
(pointerenter)="onPlayerInteraction()"
|
||||
(pointermove)="onPlayerInteraction()"
|
||||
(click)="onPlayerInteraction()"
|
||||
(dblclick)="onPlayerDblClick($event)"
|
||||
(focusin)="onPlayerInteraction()"
|
||||
>
|
||||
<div #viewport class="embedded-mpv-player__viewport">
|
||||
|
||||
@@ -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<void> {
|
||||
const session = this.session();
|
||||
if (!session?.id || !window.electron?.setEmbeddedMpvAudioTrack) {
|
||||
|
||||
Reference in new issue
Block a user