Files
gui-video-clipper/docs/superpowers/specs/2026-09-22-v020-player-mode-design.md
2026-09-22 18:51:01 -04:00

15 KiB
Raw Permalink Blame History

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') — updates preferences.appMode and 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:

  • TransportControls bar (replaced by floating PlayerControls)
  • 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)
  • StatusBar at 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 — Clipper or Player — 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() from playback.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, calls seekBy(-10)
  • Skip forward 10s — ⏩ button, calls seekBy(10)

Center group (time):

  • Time display: 0:32 / 4:15 in monospace font (var(--font-mono)), slightly dimmed

Right group (settings):

  • Speed — shows current rate (e.g. 1×, 1.5×). Click opens SpeedSelector popup. Sets videoElement.playbackRate directly.
  • CC — CC text button, highlighted when captions active. Click toggles captions. Right-click opens CaptionSettingsPanel.
  • 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) with backdrop-filter: blur(8px) (frosted glass)
  • Border radius: 12px on top corners only (bottom flush with video edge)
  • Padding: 12px 16px, 8px gap 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 has pointer-events: none so 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.playbackRate directly
  • JKL shuttle keys continue to work independently (they adjust playbackRate too)

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() from waveformRenderer.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 in PlayerControls. Active state shown with accent color.
  • Events: Listens for enterpictureinpicture and leavepictureinpicture on the video element to track state.
  • Exiting PiP: Returns focus to the app window.

New playback helpers

Add to playback.ts:

  • requestPiP() — calls videoElement.requestPictureInPicture()
  • exitPiP() — calls document.exitPictureInPicture()
  • isPiPActive(): boolean — checks document.pictureInPictureElement

11. Fullscreen

  • API: videoElement.requestFullscreen() with fallback to videoElement.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 in PlayerControls. Swaps to exit-fullscreen icon when active.
  • Events: Listens for fullscreenchange to track state.
  • Controls in fullscreen: The floating PlayerControls overlay 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 element
  • isFullscreenActive(): boolean — checks document.fullscreenElement

12. Transitions (FLIP Animations)

Clipper ↔ Player mode switch

  1. 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.

  2. Transport controls / Clip list — fade + slide out downward when switching to Player, fade + slide back in when returning to Clipper. Duration ~250ms, eased.

  3. Floating controls — fade in after video expansion completes (staggered ~100ms delay).

  4. Toolbar — slides up and fades out (Clipper→Player), slides back down (Player→Clipper).

Implementation approach

  • CSS custom properties + transition for 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 mode
  • ClipList.svelte — hidden in Player mode
  • TransportControls.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.json
  • src-tauri/tauri.conf.json
  • src-tauri/Cargo.toml
  • VERSION file

Use the existing scripts/bump-version.sh if it handles all locations, otherwise update manually.