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

332 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.