diff --git a/src/App.svelte b/src/App.svelte index 9540442..91393bd 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -10,9 +10,11 @@ import ExportDialog from '$lib/components/ExportDialog.svelte'; import AboutDialog from '$lib/components/AboutDialog.svelte'; import ProcessingModal from '$lib/components/ProcessingModal.svelte'; - import { loadPreferences, preferences, setClipListPosition, setClipListWidth } from '$lib/stores/preferences.svelte'; + import PlayerControls from '$lib/components/PlayerControls.svelte'; + import PlayerTimeline from '$lib/components/PlayerTimeline.svelte'; + import { loadPreferences, preferences, setClipListPosition, setClipListWidth, setAppMode } from '$lib/stores/preferences.svelte'; import { getSelectedClipId, removeClip } from '$lib/stores/clips.svelte'; - import { adjustShuttle, resetShuttleRate } from '$lib/transport/playback'; + import { adjustShuttle, resetShuttleRate, toggleFullscreen } from '$lib/transport/playback'; import { session } from '$lib/stores/videoSession.svelte'; import { listen } from '@tauri-apps/api/event'; import type { TransportKeyAction } from '$lib/transport/playback'; @@ -36,6 +38,22 @@ let sidebarResizeStartWidth = $state(0); let windowWidth = $state(window.innerWidth); + let isPlayerMode = $derived(preferences.appMode === 'player'); + + // Auto-hide state for Player mode + let showPlayerControls = $state(true); + let showToolbar = $state(true); + let showPlayerTimeline = $state(false); + let controlsHideTimer = $state | null>(null); + let toolbarHideTimer = $state | null>(null); + + // Caption state for PlayerControls + let captionsEnabled = $state(true); + + const CONTROLS_HIDE_DELAY = 2500; + const TOOLBAR_PROXIMITY = 50; + const TIMELINE_PROXIMITY = 80; + let isLeft = $derived(preferences.clipListPosition === 'left'); $effect(() => { @@ -57,6 +75,20 @@ } }); + $effect(() => { + if (!isPlayerMode) return; + if (!session.isPlaying) { + // Video paused — show everything + showPlayerControls = true; + showToolbar = true; + if (controlsHideTimer) clearTimeout(controlsHideTimer); + if (toolbarHideTimer) clearTimeout(toolbarHideTimer); + } else { + // Video playing — start hide timers + resetControlsTimer(); + } + }); + function dispatchTransport(action: TransportKeyAction) { transportControls?.handleKeyAction(action); } @@ -107,6 +139,57 @@ } } + function resetControlsTimer() { + if (controlsHideTimer) clearTimeout(controlsHideTimer); + showPlayerControls = true; + if (session.isPlaying) { + controlsHideTimer = setTimeout(() => { + showPlayerControls = false; + }, CONTROLS_HIDE_DELAY); + } + } + + function resetToolbarTimer() { + if (toolbarHideTimer) clearTimeout(toolbarHideTimer); + showToolbar = true; + if (session.isPlaying) { + toolbarHideTimer = setTimeout(() => { + showToolbar = false; + }, CONTROLS_HIDE_DELAY); + } + } + + function handlePlayerMouseMove(e: MouseEvent) { + if (!isPlayerMode) return; + + // Controls: show on any mouse movement over the video area + resetControlsTimer(); + + // Toolbar: show when near top edge + if (e.clientY < TOOLBAR_PROXIMITY) { + resetToolbarTimer(); + } + + // Timeline: show when near bottom edge + const windowH = window.innerHeight; + showPlayerTimeline = e.clientY > windowH - TIMELINE_PROXIMITY; + } + + function toggleMode() { + const next = preferences.appMode === 'clipper' ? 'player' : 'clipper'; + setAppMode(next); + // When entering Player mode, show controls initially + if (next === 'player') { + showPlayerControls = true; + showToolbar = true; + resetControlsTimer(); + } + } + + function handleToggleCaptions() { + captionsEnabled = !captionsEnabled; + } + function handleGlobalKeydown(e: KeyboardEvent) { const target = e.target as HTMLElement; if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return; @@ -132,15 +215,41 @@ e.preventDefault(); dispatchTransport(e.shiftKey ? 'seek-forward-1' : 'seek-forward-5'); break; + case 'p': + case 'P': + e.preventDefault(); + setAppMode('player'); + if (preferences.appMode !== 'player') { + showPlayerControls = true; + showToolbar = true; + resetControlsTimer(); + } + break; + case 'c': + case 'C': + e.preventDefault(); + setAppMode('clipper'); + break; + case 'f': + case 'F': + if (isPlayerMode) { + e.preventDefault(); + toggleFullscreen(); + } + break; case 'i': case 'I': - e.preventDefault(); - dispatchTransport('mark-in'); + if (!isPlayerMode) { + e.preventDefault(); + dispatchTransport('mark-in'); + } break; case 'o': case 'O': - e.preventDefault(); - dispatchTransport('mark-out'); + if (!isPlayerMode) { + e.preventDefault(); + dispatchTransport('mark-out'); + } break; case 'j': case 'J': @@ -159,9 +268,11 @@ break; case 'Delete': case 'Backspace': { - e.preventDefault(); - const selectedId = getSelectedClipId(); - if (selectedId) removeClip(selectedId); + if (!isPlayerMode) { + e.preventDefault(); + const selectedId = getSelectedClipId(); + if (selectedId) removeClip(selectedId); + } break; } case 'a': @@ -170,7 +281,7 @@ } break; case 'e': - if (e.metaKey) { + if (e.metaKey && !isPlayerMode) { e.preventDefault(); showExportDialog = true; } @@ -192,6 +303,7 @@ onmousemove={(e) => { handleResizeMove(e); handleSidebarResizeMove(e); + handlePlayerMouseMove(e); }} onmouseup={() => { handleResizeEnd(); @@ -222,94 +334,118 @@ {/if} -
-
+
+
+ {#if !isPlayerMode} + + {/if}
-
- - - - - -
- - -
- - -
+
+ {#if !isPlayerMode} + +
{ - if (e.key === 'ArrowDown') { + const maxWidth = Math.round(windowWidth * 0.4); + const minWidth = Math.min(150, maxWidth); + if (e.key === 'ArrowRight') { e.preventDefault(); - timelineHeight = Math.min(500, timelineHeight + 10); - } else if (e.key === 'ArrowUp') { + setClipListWidth( + Math.max(minWidth, Math.min(maxWidth, preferences.clipListWidth + 10)), + ); + } else if (e.key === 'ArrowLeft') { e.preventDefault(); - timelineHeight = Math.max(40, timelineHeight - 10); + setClipListWidth( + Math.max(minWidth, Math.min(maxWidth, preferences.clipListWidth - 10)), + ); } }} >
-
- -
+ {/if} + + +
+ + {#if isPlayerMode} + {}} + /> + + {:else} + + {/if}
- -
- (showExportDialog = true)} /> -
+ {#if !isPlayerMode} + +
+
{ + if (e.key === 'ArrowDown') { + e.preventDefault(); + timelineHeight = Math.min(500, timelineHeight + 10); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + timelineHeight = Math.max(40, timelineHeight - 10); + } + }} + >
+
+ +
+
+ + +
+ (showExportDialog = true)} /> +
+ {/if}
(showAboutDialog = true)} /> @@ -422,6 +558,49 @@ background: var(--accent, #89b4fa); } + /* --- Mode toggle button --- */ + .mode-btn { + font-size: 12px; + padding: 4px 10px; + border-radius: 10px; + font-weight: 600; + letter-spacing: 0.3px; + } + + /* --- Player mode layout --- */ + .content.player-mode { + grid-template-areas: "video"; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + } + + .player-video { + position: relative; + overflow: hidden; + } + + /* --- Toolbar auto-hide --- */ + .toolbar.auto-hide { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 30; + transition: opacity 0.2s ease, transform 0.2s ease; + } + + .toolbar.toolbar-hidden { + opacity: 0; + transform: translateY(-100%); + pointer-events: none; + } + + .toolbar.toolbar-visible { + opacity: 1; + transform: translateY(0); + pointer-events: auto; + } + .layout-btn { font-size: 16px; padding: 4px 8px; diff --git a/src/app.css b/src/app.css index a39859f..af7351c 100644 --- a/src/app.css +++ b/src/app.css @@ -37,6 +37,11 @@ body { height: 100vh; } +/* Player mode: app shell becomes relative for overlay positioning */ +.app-shell.player-mode { + position: relative; +} + button { cursor: pointer; border: none;