docs: add v0.2.0 player mode design spec

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 18:51:01 -04:00
parent c90b6765b7
commit 4f7458d8f8

View File

@@ -0,0 +1,331 @@
# 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.