Includes: - Extended caption styling (font, shadow, dimmed color, bg toggle) - Media server, subtitle downloader, VTT parser, processing modal - Waveform tiers, thumbnail/timeline improvements, transport controls - Hybrid download model, dependency management, clip export enhancements - 21 chat summaries, 2 implementation plans, 2 design specs Co-authored-by: Cursor <cursoragent@cursor.com>
70 lines
4.6 KiB
Markdown
70 lines
4.6 KiB
Markdown
# Performance, Captions, Clip UX, and Resizable Panels
|
|
|
|
**Date:** 2026-09-21 14:39
|
|
**Task:** Fix timeline performance, caption positioning, clip creation workflow, resizable panels, and deselect UX
|
|
|
|
## Changes Made
|
|
|
|
### 1. Performance Optimization (3 files)
|
|
|
|
**`src/lib/components/VideoPlayer.svelte`**:
|
|
- Throttled `handleTimeUpdate` to ~15fps (66ms interval) using `performance.now()` check. Previously every `ontimeupdate` event triggered a reactive cascade through `session.currentTime`.
|
|
|
|
**`src/lib/components/Timeline.svelte`**:
|
|
- **In-flight guard**: Added `detailFetchInFlight` flag to prevent overlapping waveform range extraction subprocess calls. Only one ffmpeg call runs at a time.
|
|
- **Increased debounce**: Waveform detail fetch debounce increased from 300ms to 500ms.
|
|
- **Playhead-only redraws**: Split the monolithic redraw `$effect` into two:
|
|
- Structural changes (clips, zoom, waveform data, thumbnails) trigger a full `drawMainCanvas()` which saves an `ImageData` snapshot.
|
|
- `session.currentTime` changes trigger a lightweight `drawPlayheadOnly()` that restores the snapshot and draws only the playhead — avoiding the expensive thumbnail/waveform/clip rendering on every time update.
|
|
- **Thumbnail load callback**: Registered a callback via `setThumbnailLoadCallback` so thumbnail image loads trigger a targeted redraw instead of relying on the next reactive cycle.
|
|
|
|
**`src/lib/timeline/thumbnailRenderer.ts`**:
|
|
- Added `pendingLoads` Set to prevent creating duplicate `Image` objects for the same path across rapid redraws.
|
|
- Added `setThumbnailLoadCallback` API so the Timeline can request a redraw when thumbnails finish loading asynchronously.
|
|
- Image `onerror` handler cleans up the pending state.
|
|
|
|
### 2. Caption Positioning (1 file)
|
|
|
|
**`src/lib/components/VideoPlayer.svelte`**:
|
|
- Added `:global(video::cue)` CSS to center captions at the bottom of the video with a semi-transparent black background, white text, and 16px font size. Used `:global()` to bypass Svelte scoping since `::cue` is a browser-level pseudo-element.
|
|
- Added `position: relative` to the `<video>` element to scope cue rendering.
|
|
|
|
### 3. Pending In-Point Preservation (1 file)
|
|
|
|
**`src/lib/stores/clips.svelte.ts`**:
|
|
- Modified `selectClip()` to only clear `pendingInPoint` when `id !== null` (selecting a specific clip). When `id === null` (deselecting via empty timeline click), the pending in-point is preserved. This fixes the workflow: press I → click timeline to seek → press O to complete the clip.
|
|
|
|
### 4. Resizable Panels (3 files)
|
|
|
|
**`src/App.svelte`**:
|
|
- Wrapped `<Timeline />` and `<ClipList />` in a new `.timeline-clip-area` container with flex column layout.
|
|
- Added a 5px `.resize-handle` divider between them with `cursor: ns-resize` and accent color on hover/active.
|
|
- Implemented `handleResizeStart/Move/End` with `mousedown`/`mousemove`/`mouseup` on `<svelte:window>` to drag-resize. Timeline height is clamped between 80px and (total - 60px).
|
|
- `timelineHeight` stored in local `$state` (defaults to 180px).
|
|
|
|
**`src/lib/components/Timeline.svelte`**:
|
|
- Changed `.timeline-container` from `height: 160px` to `flex: 1; min-height: 80px` so it fills the space given by the parent.
|
|
- `.timeline-wrapper` set to `height: 100%`.
|
|
|
|
**`src/lib/components/ClipList.svelte`**:
|
|
- Changed from `max-height: 200px` to `height: 100%; min-height: 40px` with `box-sizing: border-box`.
|
|
|
|
### 5. Deselect UX (1 file)
|
|
|
|
**`src/lib/components/ClipList.svelte`**:
|
|
- Added `handleContainerClick` on the `.clip-list` div that calls `selectClip(null)` when clicking empty space (not a child clip row).
|
|
- Replaced the single "Export All" button with an `.action-buttons` row containing a conditional "Deselect" button (shown only when a clip is selected) alongside "Export All".
|
|
- Changed `role="button"` to `role="option"` on clip rows and `role="listbox"` on the container for better semantics.
|
|
|
|
## Build Status
|
|
|
|
- Frontend: builds cleanly (0 errors)
|
|
- Rust: compiles with only pre-existing unused variant warnings
|
|
|
|
## Lessons Learned
|
|
|
|
- Svelte's scoped CSS doesn't apply to `::cue` pseudo-elements since they're browser-level shadow constructs — `:global()` wrapper is needed.
|
|
- Canvas `getImageData`/`putImageData` is an effective technique for "snapshot and overlay" rendering patterns, avoiding expensive full redraws when only one layer (like a playhead) changes.
|
|
- `ontimeupdate` fires at varying rates across browsers/engines; throttling to a target frame rate prevents reactive cascades from dominating the UI thread.
|
|
- The `pendingInPoint` clearing bug was a subtle consequence of `selectClip(null)` being used for two different intents (deselecting a clip vs. resetting all clip state).
|