fix: QA round 2 — playhead guard, controls position, caption lift, Tauri fullscreen, timeline sync

- Guard drawPlayhead on session.duration > 0 in PlayerTimeline
- Move floating controls pill from bottom: 24px to 48px
- Add playerControlsVisible prop to VideoPlayer; captions slide up 160px when controls visible
- Replace web Fullscreen API with Tauri getCurrentWindow().setFullscreen() (WKWebView compat)
- Add core:window:allow-set-fullscreen and core:window:allow-is-fullscreen permissions
- Tie PlayerTimeline visibility to showPlayerControls; remove unused proximity logic

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 20:16:10 -04:00
parent 0f2a523cc2
commit 2e36d16bfe
10 changed files with 225 additions and 58 deletions

View File

@@ -49,7 +49,6 @@
// Auto-hide state for Player mode
let showPlayerControls = $state(true);
let showToolbar = $state(true);
let showPlayerTimeline = $state(false);
let controlsHideTimer: ReturnType<typeof setTimeout> | null = null;
let toolbarHideTimer: ReturnType<typeof setTimeout> | null = null;
@@ -58,7 +57,6 @@
const CONTROLS_HIDE_DELAY = 2500;
const TOOLBAR_PROXIMITY = 50;
const TIMELINE_PROXIMITY = 80;
let isLeft = $derived(preferences.clipListPosition === 'left');
@@ -169,10 +167,6 @@
if (e.clientY < TOOLBAR_PROXIMITY) {
resetToolbarTimer();
}
// Timeline: show when near bottom edge
const windowH = window.innerHeight;
showPlayerTimeline = e.clientY > windowH - TIMELINE_PROXIMITY;
}
function toggleMode() {
@@ -405,7 +399,7 @@
<!-- Video area: always mounted, never destroyed -->
<div class="video-area" class:player-video={isPlayerMode}>
<VideoPlayer bind:captionsEnabled />
<VideoPlayer bind:captionsEnabled playerControlsVisible={isPlayerMode && showPlayerControls} />
{#if isPlayerMode}
<PlayerControls
visible={showPlayerControls}
@@ -414,7 +408,7 @@
onToggleCaptions={handleToggleCaptions}
onOpenCaptionSettings={() => {}}
/>
<PlayerTimeline visible={showPlayerTimeline || !session.isPlaying} />
<PlayerTimeline visible={showPlayerControls} />
{:else}
<div transition:fade={{ duration: 200 }}>
<TransportControls bind:this={transportControls} />

View File

@@ -115,11 +115,9 @@
}, 100);
}
function handleFullscreen() {
toggleFullscreen();
setTimeout(() => {
fullscreenActive = isFullscreenActive();
}, 100);
async function handleFullscreen() {
await toggleFullscreen();
fullscreenActive = await isFullscreenActive();
}
function handleCCContextMenu(e: MouseEvent) {
@@ -129,9 +127,9 @@
// Sync PiP/fullscreen/rate state periodically
$effect(() => {
const interval = setInterval(() => {
const interval = setInterval(async () => {
pipActive = isPiPActive();
fullscreenActive = isFullscreenActive();
fullscreenActive = await isFullscreenActive();
currentRate = getPlaybackRate();
}, 500);
return () => clearInterval(interval);
@@ -279,7 +277,7 @@
<style>
.player-controls-overlay {
position: absolute;
bottom: 24px;
bottom: 48px;
left: 15%;
right: 15%;
pointer-events: none;

View File

@@ -56,7 +56,9 @@
}
// Draw playhead
drawPlayhead(ctx, timelineState, session.currentTime);
if (session.duration > 0) {
drawPlayhead(ctx, timelineState, session.currentTime);
}
ctx.restore();
}

View File

@@ -20,8 +20,10 @@
let {
captionsEnabled = $bindable(true),
playerControlsVisible = false,
}: {
captionsEnabled?: boolean;
playerControlsVisible?: boolean;
} = $props();
let videoElement = $state<HTMLVideoElement | null>(null);
@@ -232,7 +234,7 @@
<track kind="captions" />
</video>
{#if activeCues.length > 0}
<div class="caption-overlay" style={captionPosition}>
<div class="caption-overlay" class:lifted={isPlayerMode && playerControlsVisible} style={captionPosition}>
{#each activeCues as cue}
<span
class="caption-text"
@@ -325,6 +327,11 @@
pointer-events: none;
z-index: 5;
padding: 0 10%;
transition: transform 0.2s ease;
}
.caption-overlay.lifted {
transform: translateY(-160px);
}
.caption-text {

View File

@@ -1,4 +1,5 @@
import { session } from '$lib/stores/videoSession.svelte';
import { getCurrentWindow } from '@tauri-apps/api/window';
let _videoEl: HTMLVideoElement | null = null;
@@ -116,25 +117,22 @@ export function isPiPActive(): boolean {
return !!document.pictureInPictureElement;
}
export function toggleFullscreen(): void {
if (document.fullscreenElement) {
document.exitFullscreen().catch((e) => {
console.error('Exit fullscreen failed:', e);
});
} else {
const el = document.documentElement;
if (el.requestFullscreen) {
el.requestFullscreen().catch((e) => {
console.error('Fullscreen request failed:', e);
});
} else if ((el as HTMLElement & { webkitRequestFullscreen?: () => void }).webkitRequestFullscreen) {
(el as HTMLElement & { webkitRequestFullscreen: () => void }).webkitRequestFullscreen();
}
export async function toggleFullscreen(): Promise<void> {
try {
const win = getCurrentWindow();
const isFs = await win.isFullscreen();
await win.setFullscreen(!isFs);
} catch (e) {
console.error('Fullscreen toggle failed:', e);
}
}
export function isFullscreenActive(): boolean {
return !!document.fullscreenElement;
export async function isFullscreenActive(): Promise<boolean> {
try {
return await getCurrentWindow().isFullscreen();
} catch {
return false;
}
}
export function setPlaybackRate(rate: number): void {