Files
iptvnator/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-format.utils.ts
T
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

110 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { EmbeddedMpvAudioTrack, EmbeddedMpvBounds } from 'shared-interfaces';
export const HIDDEN_BOUNDS: EmbeddedMpvBounds = Object.freeze({
x: -100000,
y: -100000,
width: 1,
height: 1,
}) as EmbeddedMpvBounds;
/**
* Vertical pixels to subtract from the MPV view's height when a control
* popover (volume, audio, subtitle, speed, aspect) is open above the
* controls strip. The native NSView paints over the WebContents, so we
* shrink it from the bottom to expose the popover region in DOM. Sized to
* cover the tallest popover (audio/subtitle list capped at ~240 px plus
* title + padding); video keeps playing in the upper region.
*/
export const MENU_OPEN_BOTTOM_CUTOUT_PX = 300;
export const SPEED_PRESETS: ReadonlyArray<{ value: number; label: string }> = [
{ value: 0.5, label: '0.5×' },
{ value: 0.75, label: '0.75×' },
{ value: 1, label: '1×' },
{ value: 1.25, label: '1.25×' },
{ value: 1.5, label: '1.5×' },
{ value: 2, label: '2×' },
];
export const ASPECT_PRESETS: ReadonlyArray<{ value: string; label: string }> = [
{ value: 'no', label: 'Default' },
{ value: '16:9', label: '16:9' },
{ value: '4:3', label: '4:3' },
{ value: '21:9', label: '21:9' },
{ value: '2.35:1', label: '2.35:1' },
];
export function formatTime(value: number | null | undefined): string {
const safeValue = Math.max(0, Math.floor(value ?? 0));
const hours = Math.floor(safeValue / 3600);
const minutes = Math.floor((safeValue % 3600) / 60);
const seconds = safeValue % 60;
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, '0')}:${String(
seconds
).padStart(2, '0')}`;
}
return `${minutes}:${String(seconds).padStart(2, '0')}`;
}
export function audioTrackLabel(
track: EmbeddedMpvAudioTrack,
index: number
): string {
const label = track.title || track.language || `Audio ${index + 1}`;
return track.defaultTrack ? `${label} · Default` : label;
}
export function subtitleTrackLabel(
track: EmbeddedMpvAudioTrack,
index: number
): string {
const label = track.title || track.language || `Subtitle ${index + 1}`;
return track.defaultTrack ? `${label} · Default` : label;
}
export function speedLabel(speed: number): string {
const value = Math.round(speed * 100) / 100;
return `${value}×`;
}
export function aspectLabel(aspect: string): string {
const preset = ASPECT_PRESETS.find((p) => p.value === aspect);
return preset?.label ?? aspect;
}
export function volumeIcon(value: number): string {
if (value <= 0) {
return 'volume_off';
}
return value < 0.5 ? 'volume_down' : 'volume_up';
}
export function volumeLabel(value: number): string {
return `Volume ${Math.round(value * 100)}%`;
}
export function readStoredVolume(): number {
const rawValue = Number(localStorage.getItem('volume') ?? '1');
if (Number.isNaN(rawValue)) {
return 1;
}
return Math.max(0, Math.min(1, rawValue));
}
export function persistVolume(value: number): void {
localStorage.setItem('volume', String(value));
}
export function measureBounds(host: HTMLElement): EmbeddedMpvBounds {
const rect = host.getBoundingClientRect();
return {
x: Math.round(rect.left),
y: Math.round(rect.top),
width: Math.max(1, Math.round(rect.width)),
height: Math.max(1, Math.round(rect.height)),
};
}