fix(downloads): harden queue controls and resumable replacement

Prevent queue action clicks from triggering row double-clicks, preserve aria2/yt-dlp resumable sidecars during duplicate replacement, and make aria2 require resume instead of silently restarting.

Add a persisted, accessible Folders collapse control with reduced-motion animation and regression coverage for replacement sidecar handling.

Fixes #11

Fixes #12

Closes #13

Refs #14
This commit is contained in:
NimBold
2026-07-11 20:07:33 +03:30
parent 1922db8ea0
commit 33375df2ff
9 changed files with 182 additions and 19 deletions
+30 -2
View File
@@ -3475,8 +3475,10 @@ async fn remove_download(
state: tauri::State<'_, AppState>,
id: String,
delete_assets: bool,
preserve_resumable: Option<bool>,
) -> Result<(), String> {
log::info!("remove_download called for id: {}", id);
let preserve_resumable = preserve_resumable.unwrap_or(false);
let primary_path = crate::download_ownership::primary_path_for_id(&app_handle, &id)?;
let active_kind = state.queue_manager.active_kind(&id).await;
@@ -3535,8 +3537,13 @@ async fn remove_download(
crate::ipc::DownloadStateEvent::new(id.clone(), crate::ipc::DownloadStatus::Paused),
);
let preserve_assets = preserve_resumable
&& primary_path
.as_deref()
.is_some_and(has_resumable_download_assets);
let cleanup_result = async {
if delete_assets {
if delete_assets && !preserve_assets {
if let Some(path) = primary_path.as_deref() {
remove_download_assets(path, &app_handle).await?;
}
@@ -3550,6 +3557,14 @@ async fn remove_download(
cleanup_result
}
fn has_resumable_download_assets(primary: &std::path::Path) -> bool {
[".aria2", ".part", ".ytdl"].iter().any(|suffix| {
let mut candidate = primary.as_os_str().to_os_string();
candidate.push(suffix);
std::path::PathBuf::from(candidate).exists()
})
}
pub(crate) async fn remove_download_assets<R: tauri::Runtime>(
primary: &std::path::Path,
app_handle: &tauri::AppHandle<R>,
@@ -4802,7 +4817,8 @@ mod tests {
normalize_speed_limit_for_aria2,
parse_firelink_deep_link, parse_ffmpeg_version, parse_media_progress_line,
redact_log_line, redact_log_line_for_output, sanitize_ytdlp_config_value,
should_cleanup_media_artifacts_after_failure, FirelinkDeepLink, MediaProgress,
has_resumable_download_assets, should_cleanup_media_artifacts_after_failure,
FirelinkDeepLink, MediaProgress,
MediaSpeedSampler, MEDIA_PROGRESS_PREFIX,
};
use serde_json::json;
@@ -4825,6 +4841,18 @@ mod tests {
assert_eq!(template, destination.join("clip.mp4"));
}
#[test]
fn recognizes_resume_sidecars_without_treating_the_primary_file_as_partial() {
let directory = tempfile::tempdir().unwrap();
let primary = directory.path().join("download.bin");
std::fs::write(&primary, b"partial").unwrap();
assert!(!has_resumable_download_assets(&primary));
std::fs::write(directory.path().join("download.bin.aria2"), b"control").unwrap();
assert!(has_resumable_download_assets(&primary));
}
#[test]
fn ytdlp_progress_args_force_progress_in_quiet_print_mode() {
let args = media_progress_args();
+2
View File
@@ -1248,6 +1248,8 @@ impl SidecarSpawner for ProductionSpawner {
options.insert("max-tries".to_string(), serde_json::json!(mt.to_string()));
options.insert("retry-wait".to_string(), serde_json::json!("2"));
options.insert("continue".to_string(), serde_json::json!("true"));
options.insert("always-resume".to_string(), serde_json::json!("true"));
options.insert("auto-file-renaming".to_string(), serde_json::json!("false"));
if let Some(speed) = payload
.speed_limit
.as_deref()