From c1de18715e1c606ef9c1e51c58ec403427ff429b Mon Sep 17 00:00:00 2001 From: cottongin Date: Wed, 23 Sep 2026 05:30:45 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20v0.2.1=20=E2=80=94=20Player=20controls?= =?UTF-8?q?=20polish,=20screenshot=20capture,=20quit=20shortcut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- VERSION | 2 +- ...6-32-v021-polish-icons-cut-clip-summary.md | 55 +++++ ...-23_07-18-blur-icons-zoom-fixes-summary.md | 28 +++ ...6-player-controls-polish-round2-summary.md | 44 ++++ ...09-08-quit-screenshot-shortcuts-summary.md | 48 ++++ package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/export.rs | 25 +++ src-tauri/src/lib.rs | 1 + src-tauri/tauri.conf.json | 2 +- src/App.svelte | 38 +++- src/lib/bindings/export.ts | 14 ++ src/lib/components/AboutDialog.svelte | 3 + src/lib/components/PlayerControls.svelte | 198 +++++++++++----- src/lib/components/VideoPlayer.svelte | 5 + src/lib/utils/screenshot.ts | 212 ++++++++++++++++++ src/lib/utils/time.ts | 14 ++ 18 files changed, 629 insertions(+), 66 deletions(-) create mode 100644 chat-summaries/2026-09-23_06-32-v021-polish-icons-cut-clip-summary.md create mode 100644 chat-summaries/2026-09-23_07-18-blur-icons-zoom-fixes-summary.md create mode 100644 chat-summaries/2026-09-23_07-56-player-controls-polish-round2-summary.md create mode 100644 chat-summaries/2026-09-23_09-08-quit-screenshot-shortcuts-summary.md create mode 100644 src/lib/utils/screenshot.ts diff --git a/VERSION b/VERSION index 0ea3a94..0c62199 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.0 +0.2.1 diff --git a/chat-summaries/2026-09-23_06-32-v021-polish-icons-cut-clip-summary.md b/chat-summaries/2026-09-23_06-32-v021-polish-icons-cut-clip-summary.md new file mode 100644 index 0000000..16bd117 --- /dev/null +++ b/chat-summaries/2026-09-23_06-32-v021-polish-icons-cut-clip-summary.md @@ -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. diff --git a/chat-summaries/2026-09-23_07-18-blur-icons-zoom-fixes-summary.md b/chat-summaries/2026-09-23_07-18-blur-icons-zoom-fixes-summary.md new file mode 100644 index 0000000..5a4d801 --- /dev/null +++ b/chat-summaries/2026-09-23_07-18-blur-icons-zoom-fixes-summary.md @@ -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 ``. + +## 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. diff --git a/chat-summaries/2026-09-23_07-56-player-controls-polish-round2-summary.md b/chat-summaries/2026-09-23_07-56-player-controls-polish-round2-summary.md new file mode 100644 index 0000000..c25bbb8 --- /dev/null +++ b/chat-summaries/2026-09-23_07-56-player-controls-polish-round2-summary.md @@ -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. diff --git a/chat-summaries/2026-09-23_09-08-quit-screenshot-shortcuts-summary.md b/chat-summaries/2026-09-23_09-08-quit-screenshot-shortcuts-summary.md new file mode 100644 index 0000000..41305f7 --- /dev/null +++ b/chat-summaries/2026-09-23_09-08-quit-screenshot-shortcuts-summary.md @@ -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` 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). diff --git a/package.json b/package.json index 31aef9c..017b9ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gui-video-clipper", - "version": "0.2.0", + "version": "0.2.1", "description": "macOS GUI video clipper (Tauri + Svelte)", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5d9b685..297ef67 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1281,7 +1281,7 @@ dependencies = [ [[package]] name = "gui-video-clipper" -version = "0.2.0" +version = "0.2.1" dependencies = [ "axum", "dirs", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5cb1d2b..9a594f5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gui-video-clipper" -version = "0.2.0" +version = "0.2.1" description = "A macOS GUI app for clipping online videos" authors = ["cottongin"] edition = "2021" diff --git a/src-tauri/src/commands/export.rs b/src-tauri/src/commands/export.rs index cfe3ab8..5254b02 100644 --- a/src-tauri/src/commands/export.rs +++ b/src-tauri/src/commands/export.rs @@ -1,6 +1,7 @@ use crate::models::{ExportConfig, ExportScope}; use crate::services::clip_exporter; use serde::Serialize; +use std::path::Path; use tauri::ipc::Channel; #[derive(Clone, Serialize)] @@ -26,6 +27,30 @@ pub enum ExportEvent { }, } +#[tauri::command] +pub fn save_screenshot( + output_dir: String, + video_title: String, + timestamp_label: String, + png_data: Vec, +) -> Result { + let expanded_dir = clip_exporter::expand_tilde_path(&output_dir); + let dir_path = Path::new(&expanded_dir); + if !dir_path.exists() { + std::fs::create_dir_all(dir_path) + .map_err(|e| format!("Failed to create output directory: {e}"))?; + } + let path = clip_exporter::generate_output_path( + &expanded_dir, + &video_title, + &format!("{}-screenshot", timestamp_label), + "png", + ); + std::fs::write(&path, &png_data) + .map_err(|e| format!("Failed to write screenshot: {e}"))?; + Ok(path) +} + #[tauri::command] pub async fn export_clips( config: ExportConfig, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dac0806..019860a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -168,6 +168,7 @@ pub fn run() { media_analysis::check_embedded_subtitles, media_analysis::list_system_fonts, export::export_clips, + export::save_screenshot, cache::load_cached_analysis, cache::save_analysis_to_cache, cache::clear_video_cache, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 96d8a48..463ab22 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "GUI Video Clipper", - "version": "0.2.0", + "version": "0.2.1", "identifier": "xyz.cottongin.gui-video-clipper", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.svelte b/src/App.svelte index f400c3b..f850542 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -15,6 +15,8 @@ import { loadPreferences, preferences, setClipListPosition, setClipListWidth, setAppMode } from '$lib/stores/preferences.svelte'; import { getSelectedClipId, removeClip, markInPoint, markOutPoint, addClip } from '$lib/stores/clips.svelte'; import { computeQuickClipRange } from '$lib/utils/clipRange'; + import { captureFrame } from '$lib/utils/screenshot'; + import { exit } from '@tauri-apps/plugin-process'; import { adjustShuttle, resetShuttleRate, @@ -39,6 +41,7 @@ let shuttleRate = $state(1); let transportControls = $state(null); + let videoPlayerRef = $state(null); // Resizable split: timeline height in px (clip list gets the rest) let timelineHeight = $state(180); @@ -198,6 +201,29 @@ setAppMode('clipper'); } + async function captureScreenshot(withCaptions: boolean) { + if (session.status !== 'ready') { + console.warn('[screenshot] No video ready, skipping'); + return; + } + try { + console.log(`[screenshot] Capturing frame (captions: ${withCaptions})...`); + const cues = videoPlayerRef?.getParsedCues() ?? []; + const path = await captureFrame({ + currentTime: session.currentTime, + videoTitle: session.title, + outputDirectory: preferences.outputDirectory, + withCaptions, + captionsEnabled, + captionSettings: preferences.captionSettings, + parsedCues: cues, + }); + console.log(`[screenshot] Saved: ${path}`); + } catch (err) { + console.error('[screenshot] Failed:', err); + } + } + function handleToggleCaptions() { captionsEnabled = !captionsEnabled; } @@ -312,6 +338,16 @@ showExportDialog = true; } break; + case 'q': + case 'Q': + e.preventDefault(); + exit(0); + break; + case 's': + case 'S': + e.preventDefault(); + captureScreenshot(!e.shiftKey); + break; case '/': if (e.metaKey) { e.preventDefault(); @@ -423,7 +459,7 @@
- + {#if isPlayerMode} { + return invoke('save_screenshot', { + outputDir, + videoTitle, + timestampLabel, + pngData, + }); +} diff --git a/src/lib/components/AboutDialog.svelte b/src/lib/components/AboutDialog.svelte index e23044b..2ae9c16 100644 --- a/src/lib/components/AboutDialog.svelte +++ b/src/lib/components/AboutDialog.svelte @@ -26,6 +26,9 @@ { key: '< (Shift+,)', action: 'Previous keyframe' }, { key: '> (Shift+.)', action: 'Next keyframe' }, { key: 'C / P', action: 'Toggle mode' }, + { key: 'S', action: 'Screenshot (with captions)' }, + { key: 'Shift+S', action: 'Screenshot (without captions)' }, + { key: 'Q', action: 'Quit' }, { key: '⌘/', action: 'About' }, ]; diff --git a/src/lib/components/PlayerControls.svelte b/src/lib/components/PlayerControls.svelte index e84bb49..c57b477 100644 --- a/src/lib/components/PlayerControls.svelte +++ b/src/lib/components/PlayerControls.svelte @@ -16,7 +16,7 @@ setPlaybackRate, getPlaybackRate, } from '$lib/transport/playback'; - import { formatTime } from '$lib/utils/time'; + import { formatTime, formatTimeParts } from '$lib/utils/time'; import { VolumeX, Volume1, @@ -26,6 +26,7 @@ Pause, Play, Scissors, + ScissorsLineDashed, Captions, PictureInPicture2, Maximize, @@ -65,6 +66,13 @@ session.duration > 0 ? session.currentTime / session.duration : 0 ); + let currentParts = $derived(formatTimeParts(session.currentTime)); + let durationParts = $derived(formatTimeParts(session.duration)); + + let volumePercent = $derived( + ((isMuted ? 0 : volume) * 100).toFixed(1) + ); + function handleSeekBarMouseDown(e: MouseEvent) { isSeeking = true; seekFromMouseEvent(e); @@ -185,29 +193,42 @@ step="0.05" value={isMuted ? 0 : volume} oninput={handleVolumeChange} + style="background: linear-gradient(to right, var(--accent, #89b4fa) {volumePercent}%, rgba(255,255,255,0.2) {volumePercent}%)" />
+ + > + >{#if session.isPlaying}{:else}{/if} + > +
@@ -228,14 +249,6 @@ {/if} - - {#if hasCaptions}