feat: v0.2.1 — Player controls polish, screenshot capture, quit shortcut

v0.2.0 introduced Player mode with floating controls, auto-hide UI,
PiP, fullscreen, JKL shuttle, speed selector, captions, and cut-clip.

v0.2.1 builds on that with extensive UI polish and new features:

Player Controls Polish:
- Enlarged center playback icons (Play 48, Rewind/FF 36) with filled SVGs
- Moved Cut Clip scissors into center group with hidden cut-before button
- Narrowed floating panel from 70% to 60% width
- Custom-styled volume slider matching seek bar (linear-gradient fill)
- Pure opacity fade animation (0.35s) replacing slide-in
- Tinted panel background (rgba(53,54,80,0.75)) visible on black canvas
- Drop shadow on center transport controls
- Scissors buttons dimmed via color (not opacity) with single-property transitions
- ScissorsLineDashed icon for cut-before variant
- Timestamps in single seek row (time | bar | duration) with sans-serif font
- Milliseconds shown at smaller size, revealed on hover

Screenshot Capture (S / Shift+S):
- Canvas compositing at native video resolution with optional caption burn-in
- Caption rendering matches CSS preview (font, background, outline, word highlight)
- New Rust save_screenshot command using existing filename pattern
- Output: {title} - {ms}ms-screenshot.png
- crossorigin=anonymous on video element for untainted canvas

Other:
- Q keyboard shortcut to quit via Tauri process plugin
- formatTimeParts() utility for split timestamp rendering
- Updated About dialog shortcuts list
- Version bump to 0.2.1

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 05:30:45 -04:00
parent 619fe59dbe
commit c1de18715e
18 changed files with 629 additions and 66 deletions

View File

@@ -0,0 +1,55 @@
# v0.2.1 — Polish, Icons, Cut Clip & Shortcuts Reference
## Task Description
Design, plan, and implement v0.2.1 of the GUI Video Clipper app. Six items: raise floating controls position, replace emoji icons with Lucide SVGs, fix blur pop-in animation, make C/P shortcuts bidirectional toggles, add "cut clip" feature from Player mode, and add keyboard shortcuts reference in a tabbed About dialog.
## Changes Made
### Commits (d8a6e0e → d7a2071)
1. **`646f2e5`** — `fix: raise controls to 64px, smooth blur via ::before pseudo-element`
- `src/lib/components/PlayerControls.svelte`: Changed `bottom: 48px` → `64px`, moved `background` + `backdrop-filter` from `.player-controls-panel` to a `::before` pseudo-element with its own opacity transition
2. **`be27b1b`** — `feat: replace emoji icons with Lucide SVG components`
- Installed `@lucide/svelte` dependency
- `src/lib/components/PlayerControls.svelte`: Replaced 🔇/🔉/🔊, ⏪, ❚❚/▶, ⏩, CC, ⧉, ⛶/⤓ with Lucide components (VolumeX/Volume1/Volume2, Rewind, Pause/Play, FastForward, Captions, PictureInPicture2, Maximize/Minimize)
- `src/lib/components/TransportControls.svelte`: Replaced ◄K/K►, ◄|/|►, ❚❚/▶, 🔇/🔉/🔊 with ChevronFirst/ChevronLast, SkipBack/SkipForward, Pause/Play, VolumeX/Volume1/Volume2
- `src/App.svelte`: Replaced ⬒/⬓, ℹ, ⚙ with PanelLeft/PanelBottom, Info, Settings
3. **`d964c97`** — `feat: C and P both toggle between Clipper/Player modes`
- `src/App.svelte`: Combined `'p'/'P'/'c'/'C'` cases to all call `toggleMode()`
4. **`ac7cb70`** — `feat: cut clip from Player mode (X/Z keys, scissors button)`
- Created `src/lib/utils/clipRange.ts` with `computeQuickClipRange()` utility
- Created `tests/lib/utils/clipRange.test.ts` (8 tests)
- `src/lib/components/PlayerControls.svelte`: Added scissors button with `onCutClip` prop
- `src/App.svelte`: Added `cutClip()` function, `X`/`Z` keyboard shortcuts (Player mode only)
5. **`507e3b7`** — `feat: tabbed About dialog with keyboard shortcuts reference`
- Rewrote `src/lib/components/AboutDialog.svelte` with "About" / "Shortcuts" tabs
- Created `tests/lib/components/AboutDialog.test.ts` (5 tests)
- Dialog dynamically sizes: ~360px for About, ~520px for Shortcuts
6. **`d7a2071`** — `fix: guard against zero-length clip at EOF`
- `src/lib/utils/clipRange.ts`: Added `start >= end` guard returning `null`
- Added test for `computeQuickClipRange(120, 120, 'at')` → `null`
## Test Results
- **69/69 tests passing** across 10 test files
- **svelte-check**: 0 errors, 0 warnings
## Follow-Up Items
- Remaining non-Lucide glyphs in ClipList, VideoPlayer, TimelineContextMenu (visual consistency for v0.2.2)
- Icon-only buttons could benefit from `aria-label` for screen reader accessibility
- Shortcuts reference in AboutDialog is static — could extract to shared source of truth with keyboard handler
- No `role="tabpanel"` / `aria-controls` wiring on About dialog tabs (full WAI-ARIA tabs pattern)
## Lessons Learned
- **WebKit `backdrop-filter` + `opacity` compositing**: WebKit doesn't smoothly composite `backdrop-filter` through an ancestor `opacity` transition. The fix is to isolate the blur into a `::before` pseudo-element with its own opacity transition.
- **`@lucide/svelte` is the official package name** (not `lucide-svelte`). Uses tree-shakable named imports: `import { Play } from '@lucide/svelte'`.
- **Svelte component testing text collisions**: When testing components with duplicate visible text (e.g., "Play / Pause" appears twice in a shortcuts table), use `getAllByText` or `getByRole` with name filters instead of `getByText`.
- **Edge case: zero-length clips**: When clamping clip ranges to video bounds, always guard against `start >= end` after clamping to prevent zero-duration clips.

View File

@@ -0,0 +1,28 @@
# v0.2.1 Fixes: Blur, Icon Sizing, Timeline Zoom
## Task Description
Three fixes for the v0.2.1 release: (1) fix the backdrop blur pop-in animation in Player mode floating controls, raise controls another 20px; (2) enlarge center playback icons by 50%; (3) zoom timeline to clip region when using cut-clip from Player mode.
## Changes Made
### Commits
1. **`f6ad41f`** — `fix: blur via visibility (no opacity), raise to 84px, enlarge center icons 50%`
- `src/lib/components/PlayerControls.svelte`:
- **Blur fix**: Restructured CSS so the overlay no longer transitions `opacity` at all. The `::before` blur pseudo-element uses `visibility: hidden/visible` (instant toggle, always `opacity: 1` when visible) so WebKit never has to composite `backdrop-filter` during an opacity transition. A new `.panel-content` wrapper handles the fade+slide animation (`opacity 0->1`, `translateY(6px)->0`) independently.
- **Position**: `bottom: 64px` -> `bottom: 84px`
- **Icon sizes**: Play/Pause `size={18}` -> `size={27}`, Rewind/FastForward `size={16}` -> `size={24}`, play button `min-width: 40px` -> `48px`
2. **`619fe59`** — `feat: timeline zooms to clip region on cut-clip from Player mode`
- `src/lib/components/Timeline.svelte`: Added `zoomRegion` and `onZoomApplied` optional props. Added a second `$effect` (after the duration-reset effect) that zooms to the given region with 20% padding (min 2s) on each side.
- `src/App.svelte`: Added `pendingTimelineZoom` state. `cutClip()` sets it to the created clip's range before switching to Clipper mode. Passed `zoomRegion={pendingTimelineZoom}` and `onZoomApplied` callback to `<Timeline>`.
## Test Results
- 69/69 tests passing, svelte-check 0 errors/0 warnings
## Lessons Learned
- **WebKit `backdrop-filter` + `opacity` compositing**: The `::before` pseudo-element approach from the previous attempt still failed because ANY opacity transition (even on the pseudo-element itself, not just ancestors) prevents WebKit from compositing `backdrop-filter` during the animation. The only reliable fix is to NEVER transition opacity on the element that has `backdrop-filter`. Use `visibility: hidden/visible` for instant show/hide of the blur layer, and animate content separately.
- **Svelte 5 `$effect` ordering**: When two effects run on mount, the one declared later wins for overlapping state mutations. This is leveraged for the timeline zoom — the duration-reset effect runs first (sets full view), then the zoom effect runs second and overrides if `zoomRegion` is set.

View File

@@ -0,0 +1,44 @@
# Player Controls Polish — Round 2
**Date:** 2026-09-23 07:56 UTC
**Task:** Five UI polish changes to the floating PlayerControls panel
## Changes Made
All changes in `src/lib/components/PlayerControls.svelte`.
### 1. Enlarged center playback icons by 50%
- Play/Pause: 27 -> 40
- Rewind/FastForward: 24 -> 36
- `.ctrl-btn.play-btn` min-width: 48px -> 64px
### 2. Reorganized Cut Clip buttons into center group
- Moved the "cut at playhead" Scissors button from the right settings group into the center playback group, placed after FastForward. Icon enlarged to 36 to match.
- Added a new "cut before" Scissors button to the left of Rewind. It is hidden by default (`width: 0; opacity: 0; pointer-events: none`) and reveals on `.controls-group.center:hover` with a smooth transition. Appears at 0.7 opacity, full opacity on direct hover.
- Removed the old Scissors button and its right-click handler from the right group.
- New center group order: [Cut-before (hidden)] [Rewind] [Play/Pause] [FastForward] [Cut-at]
### 3. Narrowed floating panel by ~15%
- Overlay insets changed from `left: 15%; right: 15%` (70% width) to `left: 20%; right: 20%` (60% width).
### 4. Restyled volume slider to match seek bar
- Replaced native `accent-color` styling with `appearance: none` custom CSS.
- Track uses transparent background with JS-driven `linear-gradient` inline style showing accent color for the filled portion and `rgba(255,255,255,0.2)` for the dormant portion.
- Thumb is a 10px circle matching seek bar thumb, hidden by default and shown on hover.
- Added `volumePercent` derived state to compute the gradient stop value.
### 5. Replaced slide animation with pure fade
- Removed `transform: translateY(6px)` from `.panel-content` and its `translateY(0)` visible state.
- Changed transition to `opacity 0.35s ease` only (was `opacity 0.2s ease, transform 0.2s ease`).
- Blur `::before` pseudo-element now fades with `opacity 0.35s ease` instead of instant `visibility` toggle. Uses `visibility 0s 0.35s` delay on hide and `visibility 0s 0s` on show to keep pointer-events correct while avoiding the flash.
## Verification
- `svelte-check`: 0 errors, 0 warnings
- `vitest run`: 69 tests passed across 10 test files
## Lessons Learned
- WebKit's `backdrop-filter` still renders correctly with `opacity` transitions as long as the blur layer itself isn't being destroyed/recreated. The `visibility` + delayed `transition` pattern keeps the composited blur layer alive during fade.
- Using `linear-gradient` as an inline style on range inputs is the cleanest way to show a filled/unfilled track without shadow DOM hacks, since WebKit's `::-webkit-slider-runnable-track` doesn't support `background` in scoped Svelte styles as reliably.
## Follow-up Items
- None identified. Visual QA recommended to confirm feel of 0.35s fade timing and hidden cut-before button hover UX.

View File

@@ -0,0 +1,48 @@
# Quit + Screenshot Shortcuts
**Date:** 2026-09-23 09:08 UTC
**Task:** Add Q to quit, S/Shift+S to save screenshot with/without captions, update About dialog shortcuts
## Changes Made
### Rust backend
- **`src-tauri/src/commands/export.rs`** — Added `save_screenshot` Tauri command. Accepts output directory, video title, timestamp label, and PNG byte data. Uses the existing `generate_output_path` and `expand_tilde_path` from `clip_exporter` for consistent filename generation. Creates output directory if it doesn't exist.
- **`src-tauri/src/lib.rs`** — Registered `export::save_screenshot` in the invoke handler.
### Frontend bindings
- **`src/lib/bindings/export.ts`** — Added `saveScreenshot()` function that invokes the new Rust command.
### Screenshot utility
- **`src/lib/utils/screenshot.ts`** (new file) — Canvas-based screenshot capture:
- Draws the current video frame onto an offscreen canvas at native resolution (`videoWidth` x `videoHeight`)
- Optionally composites captions using Canvas 2D text APIs, matching the CSS caption style (font, color, background, outline/shadow, word highlighting, position)
- Scales caption font size proportionally from preview to native resolution
- Exports as PNG blob, converts to byte array, sends to Rust backend
- Filename format: `{videoTitle} - {totalMs}ms-screenshot.png`
### VideoPlayer
- **`src/lib/components/VideoPlayer.svelte`** — Added `export function getParsedCues()` to expose parsed VTT cues to the parent component.
### App keybindings
- **`src/App.svelte`** — Added:
- `Q` — Quit app via `exit(0)` from `@tauri-apps/plugin-process`
- `S` — Screenshot with captions
- `Shift+S` — Screenshot without captions
- `bind:this={videoPlayerRef}` on VideoPlayer for accessing `getParsedCues()`
- `captureScreenshot(withCaptions)` async function that orchestrates the capture
### About dialog
- **`src/lib/components/AboutDialog.svelte`** — Added Q, S, and Shift+S to the global shortcuts list.
## Verification
- `svelte-check`: 0 errors, 0 warnings
- `vitest run`: 69 tests passed across 10 test files
- `cargo check`: compiles clean
## Lessons Learned
- Canvas 2D text rendering requires explicit scaling when compositing at native resolution vs. CSS preview resolution. Using `videoHeight / clientHeight` as the scale factor ensures caption size matches visually.
- The `toBlob` → `arrayBuffer` → `Array.from(Uint8Array)` pipeline is necessary because Tauri's IPC expects `Vec<u8>` on the Rust side, which maps to `number[]` in TypeScript invoke calls.
## Follow-up Items
- Visual QA recommended to verify caption rendering fidelity on screenshots matches the preview exactly (especially text wrapping and word-level highlighting).
- Could add a brief toast notification on successful screenshot save (currently only logs to console).