15 KiB
v0.2.0 — Player Mode Design Spec
Date: 2026-09-22
Version target: 0.2.0
Scope: Add a dedicated "Player" mode alongside the existing "Clipper" mode, transforming the app into a clean, minimal video player when clipping isn't needed.
1. Overview
The app currently operates exclusively as a video clipper. This spec adds a second mode — Player — that strips away clipping UI and presents a focused video playback experience (think QuickTime / VLC). Users toggle between modes with a toolbar button or keyboard shortcut; the switch preserves all session state (playhead position, loaded video, clips, waveform data).
Architecture approach: Single App.svelte with conditional rendering. The <video> element inside VideoPlayer.svelte is never destroyed during mode switches — only the surrounding chrome changes. New Player-specific components (PlayerControls, PlayerTimeline, SpeedSelector) stay focused and small.
2. State & Preferences
New preference field
Add appMode: 'clipper' | 'player' to the Preferences interface in preferences.svelte.ts. Default: 'clipper'. Persisted via the Tauri store so it survives relaunches.
New store helper
setAppMode(mode: 'clipper' | 'player')— updatespreferences.appModeand saves.
Mode switching invariant
Toggling between modes preserves all session state: playhead position, loaded video, playback state (playing/paused), clips, waveform data, captions, volume, speed — everything. The <video> element itself never unmounts, so playback state is inherently preserved (same principle as the existing clip list layout toggle).
3. Keyboard Shortcuts
New shortcuts
| Key | Action | Available in |
|---|---|---|
p |
Switch to Player mode | Both modes |
c |
Switch to Clipper mode | Both modes |
f |
Toggle fullscreen | Player mode only |
Existing shortcuts by mode
| Key | Action | Clipper | Player |
|---|---|---|---|
Space / k |
Play/Pause | ✓ | ✓ |
← / → |
Seek ±5s (±1s with Shift) | ✓ | ✓ |
, / . |
Frame step back/forward (works when paused) | ✓ | ✓ |
Shift+, / Shift+. |
Keyframe jump | ✓ | ✓ |
j / l |
Shuttle speed down/up | ✓ | ✓ |
i / o |
Mark in/out point | ✓ | No-op |
Delete / Backspace |
Delete selected clip | ✓ | No-op |
Cmd+E |
Export dialog | ✓ | No-op |
4. App Layout
Clipper mode (unchanged)
Renders exactly as v0.1.x: toolbar → video player → transport controls → resizable timeline → clip list (bottom or sidebar).
Player mode
The layout collapses to a single full-bleed video area with auto-hiding overlays:
┌──────────────────────────────────┐
│ Toolbar (auto-hides) │ ← slides down from top on cursor proximity
├──────────────────────────────────┤
│ │
│ VideoPlayer │ ← fills all available space
│ (floating controls overlay) │
│ │
├──────────────────────────────────┤
│ PlayerTimeline (auto-hides) │ ← slides up from bottom on cursor proximity
└──────────────────────────────────┘
Hidden in Player mode:
TransportControlsbar (replaced by floatingPlayerControls)ClipList(both bottom and sidebar variants)- Timeline resize handle
- Minimap
- Timeline clip rendering
- I/O mark buttons
Visible in Player mode:
VideoPlayer.svelte(unchanged, always mounted)StatusBarat the bottom (stays visible in both modes)PlayerControls(floating overlay)PlayerTimeline(auto-hiding simplified timeline)
CSS: Player mode sets a different grid-template on .content that gives the video area 1fr and collapses the lower/clip-list rows to 0. The auto-hiding panels use position: absolute within the app shell.
5. Mode Toggle Button
- Position: In the toolbar, between the URL input and the existing layout toggle button.
- Appearance: Text label showing current mode —
ClipperorPlayer— styled as a small pill/badge button. - Click: Toggles between modes.
- Tooltip: "Switch to Player (P)" or "Switch to Clipper (C)".
6. Floating Player Controls (PlayerControls.svelte)
An absolutely-positioned overlay rendered over the video area. Designed as a semi-transparent frosted-glass panel.
Show/hide logic
- Show when: mouse moves anywhere over the video area, OR video is paused.
- Hide when: mouse is still for 2.5 seconds AND video is playing.
- Transition: fade in/out over ~200ms using CSS opacity + pointer-events.
- Fullscreen: Same show/hide behavior applies when video is in fullscreen mode.
Panel layout
┌─────────────────────────────────────────────────────────────┐
│ ▁▂▃▅▃▂▁▂▃▅▇▅▃▂▁▂▃▅▃▂▁ ●─────────────────── │ ← seek bar
├─────────────────────────────────────────────────────────────┤
│ ▶❚❚ ◀◀ ▶▶ 🔊━━━━ 0:32 / 4:15 1.0× CC ⧉ ⛶ │ ← controls row
└─────────────────────────────────────────────────────────────┘
Top row — Seek bar
- Full-width thin progress bar spanning the entire panel width.
- Shows playback progress as a filled track (
var(--accent)) against a dark unfilled track. - Hovering shows a time tooltip above the cursor position.
- Click or drag to seek (reuses
seekTo()fromplayback.ts). - Track height: 4px, expanding to 6px on hover (CSS transition).
- Thumb: 12px circle, visible only on hover.
Bottom row — Controls
Three groups arranged with flexbox space-between:
Left group (playback):
- Play/Pause — primary action button,
▶/❚❚ - Skip back 10s —
⏪button, callsseekBy(-10) - Skip forward 10s —
⏩button, callsseekBy(10)
Center group (time):
- Time display:
0:32 / 4:15in monospace font (var(--font-mono)), slightly dimmed
Right group (settings):
- Speed — shows current rate (e.g.
1×,1.5×). Click opensSpeedSelectorpopup. SetsvideoElement.playbackRatedirectly. - CC —
CCtext button, highlighted when captions active. Click toggles captions. Right-click opensCaptionSettingsPanel. - PiP —
⧉icon. Click enters/exits Picture-in-Picture. Active state: accent color. - Fullscreen —
⛶icon. Click enters/exits fullscreen. Swaps to exit icon when active.
Visual styling
- Background:
rgba(0, 0, 0, 0.75)withbackdrop-filter: blur(8px)(frosted glass) - Border radius:
12pxon top corners only (bottom flush with video edge) - Padding:
12px 16px,8pxgap between rows - Buttons: Borderless, icon-only,
color: var(--text-primary), hover:color: var(--accent). ~32px tap targets. - Pointer events: Panel has
pointer-events: auto; surrounding overlay haspointer-events: noneso clicks pass through to the video.
Video click/double-click behavior
- Single click on video (outside controls) → toggles play/pause
- Double-click on video → toggles fullscreen
7. Speed Selector (SpeedSelector.svelte)
A small vertical popup menu triggered by clicking the speed button in PlayerControls.
- Options: 0.5×, 0.75×, 1×, 1.25×, 1.5×, 2×
- Current rate has a checkmark indicator
- Click an option to set rate, popup closes
- Click outside popup to dismiss
- Popup appears above the speed button, anchored to its position
- Sets
videoElement.playbackRatedirectly - JKL shuttle keys continue to work independently (they adjust
playbackRatetoo)
8. Simplified Player Timeline (PlayerTimeline.svelte)
A lightweight timeline component for Player mode. Shows only waveform and timestamps — no clips, thumbnails, clip handles, or minimap.
Auto-hide behavior
- Show when cursor is within ~80px of the bottom edge of the video area.
- Hide when cursor moves away from bottom edge AND video is playing.
- Stays visible when video is paused.
- Transition: slides up from below + fades in over ~200ms.
- Relationship to floating controls: The floating controls and the player timeline have overlapping but distinct triggers. The floating controls show on any mouse movement over the video area; the player timeline shows only on bottom-edge proximity (~80px). When the cursor is near the bottom, both are visible and they appear together as a cohesive unit. When the cursor moves elsewhere over the video, only the floating controls appear (not the timeline).
Layout
- Fixed height: ~50px (not user-resizable)
- Position: Bottom of the player area, below the floating controls panel
- Background: Semi-transparent dark, matching floating controls (
rgba(0, 0, 0, 0.6),backdrop-filter: blur(4px))
Rendering
- Waveform: Reuses
drawWaveform()fromwaveformRenderer.ts, rendered at full duration (always zoom level 1, no pan). - Timestamps: Reuses timestamp tick rendering from
renderer.ts, simplified to major ticks only (no clip lanes). - Playhead: Vertical white line showing current position.
- Click-to-seek: Clicking anywhere seeks to that position.
- No zoom/pan: Always shows the full duration.
9. Toolbar Auto-Hide (Player Mode)
Clipper mode
Toolbar always visible (no change).
Player mode
- Show when cursor is within ~50px of the top edge of the window.
- Hide when cursor moves away from top edge AND video is playing.
- Stays visible when video is paused.
- Transition: slides down from above + fades in (~200ms), matching the bottom HUD timing.
- Content: Same as Clipper mode (URL input, mode toggle, about, preferences).
10. Picture-in-Picture
- API:
HTMLVideoElement.requestPictureInPicture()— uses the native macOS PiP window via WKWebView. - Scope: Player mode only. No PiP button in Clipper mode.
- Button:
⧉icon inPlayerControls. Active state shown with accent color. - Events: Listens for
enterpictureinpictureandleavepictureinpictureon the video element to track state. - Exiting PiP: Returns focus to the app window.
New playback helpers
Add to playback.ts:
requestPiP()— callsvideoElement.requestPictureInPicture()exitPiP()— callsdocument.exitPictureInPicture()isPiPActive(): boolean— checksdocument.pictureInPictureElement
11. Fullscreen
- API:
videoElement.requestFullscreen()with fallback tovideoElement.webkitEnterFullscreen()for WKWebView compatibility. WKWebView's webkit-prefixed API fullscreens the video natively (similar to Safari's behavior). - Scope: Player mode only. Keyboard shortcut
f. - Button:
⛶icon inPlayerControls. Swaps to exit-fullscreen icon when active. - Events: Listens for
fullscreenchangeto track state. - Controls in fullscreen: The floating
PlayerControlsoverlay remains functional in fullscreen mode with the same auto-hide behavior.
New playback helpers
Add to playback.ts:
toggleFullscreen()— enters or exits fullscreen on the video elementisFullscreenActive(): boolean— checksdocument.fullscreenElement
12. Transitions (FLIP Animations)
Clipper ↔ Player mode switch
-
Video player area — FLIP animation: the video container smoothly expands to fill the full area (Clipper→Player) or shrinks back to its grid cell (Player→Clipper). Manual FLIP: snapshot bounding rect before mode change, apply mode, snapshot new rect, animate from old→new using CSS
transform. -
Transport controls / Clip list — fade + slide out downward when switching to Player, fade + slide back in when returning to Clipper. Duration ~250ms, eased.
-
Floating controls — fade in after video expansion completes (staggered ~100ms delay).
-
Toolbar — slides up and fades out (Clipper→Player), slides back down (Player→Clipper).
Implementation approach
- CSS custom properties +
transitionfor grid layout changes (grid rows/columns animate in modern browsers). - Svelte
transition:directives (fly,fade,slide) for elements entering/leaving the DOM. - Manual FLIP on the video container for the expansion/contraction.
- Total transition budget: ~300ms.
Edge cases
- Mid-transition mode switch: new transition takes over from current interpolated position (CSS transitions handle this naturally).
- No video loaded: transition still runs (placeholder area expands/contracts).
13. New & Modified Files
New files
| File | Purpose |
|---|---|
src/lib/components/PlayerControls.svelte |
Floating controls overlay (seek bar, play/pause, volume, speed, CC, PiP, fullscreen, time display) |
src/lib/components/PlayerTimeline.svelte |
Simplified auto-hiding timeline (waveform + timestamps only) |
src/lib/components/SpeedSelector.svelte |
Speed popup menu used by PlayerControls |
Modified files
| File | Changes |
|---|---|
src/lib/stores/preferences.svelte.ts |
Add appMode field, setAppMode(), persist/load |
src/App.svelte |
Mode toggle button, conditional layout, auto-hide toolbar, p/c/f shortcuts, FLIP transitions |
src/lib/components/VideoPlayer.svelte |
PiP/fullscreen event listeners, expose PiP/fullscreen state, conditional CC toggle rendering (Clipper only; Player delegates to PlayerControls) |
src/lib/transport/playback.ts |
Add requestPiP(), exitPiP(), isPiPActive(), toggleFullscreen(), isFullscreenActive(), setPlaybackRate() |
src/app.css |
CSS variables/classes for player mode transitions, auto-hide panels |
Unchanged files
Timeline.svelte— used only in Clipper modeClipList.svelte— hidden in Player modeTransportControls.svelte— hidden in Player mode- All Rust backend code — no backend changes
StatusBar.svelte— visible in both modes
Testing
- Existing tests unaffected (timeline interactions, clips store, time utilities).
- New tests: PlayerControls show/hide logic, speed selector state, mode switching preserves session state.
14. Version Bump
Bump version from 0.1.3 to 0.2.0 in:
package.jsonsrc-tauri/tauri.conf.jsonsrc-tauri/Cargo.tomlVERSIONfile
Use the existing scripts/bump-version.sh if it handles all locations, otherwise update manually.