From 149ce3da213f330ad35d458a4b6cab3ba46564f4 Mon Sep 17 00:00:00 2001 From: cottongin Date: Mon, 21 Sep 2026 11:25:28 -0400 Subject: [PATCH] fix: address final review findings (clip reset, codecs, paths, timeline) Co-authored-by: Cursor --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/commands/export.rs | 5 ++-- src-tauri/src/services/clip_exporter.rs | 39 +++++++++++++++++++++++-- src/App.svelte | 10 ++++++- src/lib/components/Timeline.svelte | 2 ++ src/lib/stores/videoSession.svelte.ts | 3 ++ src/lib/timeline/interactions.ts | 7 +++++ src/lib/transport/playback.ts | 7 +++++ 9 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c54be24..d6291df 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3297,6 +3297,7 @@ dependencies = [ name = "tauri-app" version = "0.1.0" dependencies = [ + "dirs", "serde", "serde_json", "tauri", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 268240e..b58f53d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,6 +29,7 @@ tokio = { version = "1.53.1", features = ["full"] } thiserror = "2.0.20" uuid = { version = "1.26.1", features = ["v4"] } tempfile = "3.27.0" +dirs = "6.0.0" # Read the optimization guideline for more details: https://tauri.app/concept/size/#cargo-configuration diff --git a/src-tauri/src/commands/export.rs b/src-tauri/src/commands/export.rs index 547aa6e..9c1ac9d 100644 --- a/src-tauri/src/commands/export.rs +++ b/src-tauri/src/commands/export.rs @@ -25,6 +25,7 @@ pub async fn export_clips( on_event: Channel, ) -> Result, String> { tokio::task::spawn_blocking(move || { + let output_directory = clip_exporter::expand_tilde_path(&config.output_directory); let ext = clip_exporter::get_extension(&config.source_file_path, &config.cut_mode); match config.export_scope { @@ -40,7 +41,7 @@ pub async fn export_clips( }); let output = clip_exporter::generate_output_path( - &config.output_directory, + &output_directory, &config.video_title, &clip.label, &ext, @@ -69,7 +70,7 @@ pub async fn export_clips( }); let output = clip_exporter::generate_output_path( - &config.output_directory, + &output_directory, &config.video_title, "Merged", &ext, diff --git a/src-tauri/src/services/clip_exporter.rs b/src-tauri/src/services/clip_exporter.rs index 2aa3166..d49e0b6 100644 --- a/src-tauri/src/services/clip_exporter.rs +++ b/src-tauri/src/services/clip_exporter.rs @@ -23,11 +23,20 @@ pub fn build_ffmpeg_args( args.extend(["-c".to_string(), "copy".to_string()]); } CutMode::Precise { format: _ } => { + let ext = Path::new(output) + .extension() + .map(|e| e.to_string_lossy().to_ascii_lowercase()) + .unwrap_or_default(); + let (video_codec, audio_codec) = match ext.as_str() { + "webm" => ("libvpx-vp9", "libopus"), + "mp4" | "mkv" => ("libx264", "aac"), + _ => ("libx264", "aac"), + }; args.extend([ "-c:v".to_string(), - "libx264".to_string(), + video_codec.to_string(), "-c:a".to_string(), - "aac".to_string(), + audio_codec.to_string(), ]); } } @@ -36,6 +45,20 @@ pub fn build_ffmpeg_args( args } +pub fn expand_tilde_path(path: &str) -> String { + if path == "~" { + return dirs::home_dir() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|| path.to_string()); + } + if let Some(rest) = path.strip_prefix("~/") { + if let Some(home) = dirs::home_dir() { + return home.join(rest).to_string_lossy().to_string(); + } + } + path.to_string() +} + pub fn generate_output_path( output_dir: &str, video_title: &str, @@ -206,6 +229,18 @@ mod tests { let args = build_ffmpeg_args(&clip, "input.mp4", "output.mp4", &mode); assert!(args.contains(&"-c:v".to_string())); assert!(args.contains(&"libx264".to_string())); + assert!(args.contains(&"aac".to_string())); + + let webm_args = build_ffmpeg_args(&clip, "input.mp4", "output.webm", &mode); + assert!(webm_args.contains(&"libvpx-vp9".to_string())); + assert!(webm_args.contains(&"libopus".to_string())); + } + + #[test] + fn test_expand_tilde_path() { + let expanded = expand_tilde_path("~/Videos"); + assert!(!expanded.starts_with("~/")); + assert!(expanded.ends_with("Videos") || expanded.contains("Videos")); } #[test] diff --git a/src/App.svelte b/src/App.svelte index cefb63c..c4a7708 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -10,7 +10,8 @@ import ExportDialog from '$lib/components/ExportDialog.svelte'; import { loadPreferences } from '$lib/stores/preferences.svelte'; import { getSelectedClipId, removeClip } from '$lib/stores/clips.svelte'; - import { adjustShuttle } from '$lib/transport/playback'; + import { adjustShuttle, resetShuttleRate } from '$lib/transport/playback'; + import { session } from '$lib/stores/videoSession.svelte'; import type { TransportKeyAction } from '$lib/transport/playback'; let showSetupWizard = $state(true); @@ -24,6 +25,13 @@ loadPreferences(); }); + $effect(() => { + if (session.status === 'resolving') { + shuttleRate = 1; + resetShuttleRate(); + } + }); + function dispatchTransport(action: TransportKeyAction) { transportControls?.handleKeyAction(action); } diff --git a/src/lib/components/Timeline.svelte b/src/lib/components/Timeline.svelte index 79d6647..fe7bf4e 100644 --- a/src/lib/components/Timeline.svelte +++ b/src/lib/components/Timeline.svelte @@ -31,7 +31,9 @@ $effect(() => { if (session.duration > 0) { + timelineState.visibleStart = 0; timelineState.visibleEnd = session.duration; + timelineState.zoom = 1; } }); diff --git a/src/lib/stores/videoSession.svelte.ts b/src/lib/stores/videoSession.svelte.ts index e1194e3..c01561e 100644 --- a/src/lib/stores/videoSession.svelte.ts +++ b/src/lib/stores/videoSession.svelte.ts @@ -8,6 +8,7 @@ import { extractWaveform, } from '$lib/bindings/mediaAnalysis'; import { clearThumbnailCache } from '$lib/timeline/thumbnailRenderer'; +import { clearAll as clearAllClips } from '$lib/stores/clips.svelte'; export type SessionStatus = 'idle' | 'resolving' | 'ready' | 'error'; export type DownloadStatus = 'idle' | 'downloading' | 'complete' | 'failed'; @@ -32,6 +33,7 @@ export const session = $state({ }); export function setMetadata(meta: VideoMetadata) { + clearAllClips(); session.url = meta.url; session.title = meta.title; session.duration = meta.duration; @@ -73,6 +75,7 @@ export function setError(msg: string) { } export function setResolving() { + clearAllClips(); clearMediaFields(); session.status = 'resolving'; session.error = null; diff --git a/src/lib/timeline/interactions.ts b/src/lib/timeline/interactions.ts index 75b7439..c2472d6 100644 --- a/src/lib/timeline/interactions.ts +++ b/src/lib/timeline/interactions.ts @@ -17,6 +17,13 @@ export function computeZoom( duration: number ): { visibleStart: number; visibleEnd: number; zoom: number } { const currentRange = state.visibleEnd - state.visibleStart; + if (currentRange <= 0) { + return { + visibleStart: state.visibleStart, + visibleEnd: state.visibleEnd, + zoom: state.zoom, + }; + } const newRange = currentRange / factor; const clampedRange = Math.max(MIN_VISIBLE_RANGE, Math.min(newRange, duration)); diff --git a/src/lib/transport/playback.ts b/src/lib/transport/playback.ts index e65e8d1..1775b04 100644 --- a/src/lib/transport/playback.ts +++ b/src/lib/transport/playback.ts @@ -44,6 +44,13 @@ export function jumpKeyframe(direction: 1 | -1) { } } +export function resetShuttleRate() { + const videoEl = document.querySelector('video'); + if (videoEl) { + videoEl.playbackRate = 1; + } +} + export function adjustShuttle(dir: 1 | -1, shuttleRate: number): number { const videoEl = document.querySelector('video'); if (!videoEl) return shuttleRate;