From 77b940df605a78c305300c41022eb91f9da43f88 Mon Sep 17 00:00:00 2001 From: NimBold Date: Sun, 28 Jun 2026 00:03:12 +0330 Subject: [PATCH] feat: add resumability detection and pause warnings - Update 'fetch_metadata' to detect 'Accept-Ranges' HTTP header - Persist 'resumable' flag in DownloadItem across backend and frontend - Add confirmation dialog when pausing non-resumable downloads (single and bulk actions) - Improve UI for start/pause/options buttons by adopting the application accent color on hover --- src-tauri/src/ipc.rs | 2 ++ src-tauri/src/lib.rs | 11 +++++++++++ src/bindings/DownloadItem.ts | 2 +- src/bindings/MetadataResponse.ts | 2 +- src/bindings/PersistedSettings.ts | 11 ++++++++++- src/components/AddDownloadsModal.tsx | 4 +++- src/components/DownloadItem.tsx | 2 +- src/components/DownloadTable.tsx | 25 +++++++++++++++++++++++-- src/index.css | 10 ++++++++++ src/utils/addDownloadMetadata.ts | 1 + 10 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index 5dbb265..355d815 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -79,6 +79,8 @@ pub struct DownloadItem { pub category: DownloadCategory, pub date_added: String, #[ts(optional)] + pub resumable: Option, + #[ts(optional)] pub connections: Option, #[ts(optional)] pub speed_limit: Option, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 26cde00..4378ff7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -21,6 +21,7 @@ pub struct MetadataResponse { size: String, #[ts(type = "number")] size_bytes: u64, + resumable: bool, } #[derive(Debug, Serialize, serde::Deserialize, Clone, TS)] @@ -954,11 +955,21 @@ async fn fetch_metadata( } } + let mut resumable = false; + if let Some(accept_ranges) = res.headers().get(reqwest::header::ACCEPT_RANGES) { + if let Ok(accept_ranges_str) = accept_ranges.to_str() { + if accept_ranges_str.to_lowercase().contains("bytes") { + resumable = true; + } + } + } + Ok(MetadataResponse { url: current_url, filename, size: size_str, size_bytes, + resumable, }) } diff --git a/src/bindings/DownloadItem.ts b/src/bindings/DownloadItem.ts index b19ebab..50250ab 100644 --- a/src/bindings/DownloadItem.ts +++ b/src/bindings/DownloadItem.ts @@ -2,4 +2,4 @@ import type { DownloadCategory } from "./DownloadCategory"; import type { DownloadStatus } from "./DownloadStatus"; -export type DownloadItem = { id: string, url: string, fileName: string, status: DownloadStatus, fraction?: number, speed?: string, eta?: string, size?: string, category: DownloadCategory, dateAdded: string, connections?: number, speedLimit?: string, username?: string, password?: string, headers?: string, checksum?: string, cookies?: string, mirrors?: string, destination?: string, isMedia?: boolean, mediaFormatSelector?: string, queueId?: string, queuePosition?: number, hasBeenDispatched?: boolean, }; +export type DownloadItem = { id: string, url: string, fileName: string, status: DownloadStatus, fraction?: number, speed?: string, eta?: string, size?: string, category: DownloadCategory, dateAdded: string, resumable?: boolean, connections?: number, speedLimit?: string, username?: string, password?: string, headers?: string, checksum?: string, cookies?: string, mirrors?: string, destination?: string, isMedia?: boolean, mediaFormatSelector?: string, queueId?: string, queuePosition?: number, hasBeenDispatched?: boolean, }; diff --git a/src/bindings/MetadataResponse.ts b/src/bindings/MetadataResponse.ts index 5514d6d..29d0ea7 100644 --- a/src/bindings/MetadataResponse.ts +++ b/src/bindings/MetadataResponse.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type MetadataResponse = { url: string, filename: string, size: string, size_bytes: number, }; +export type MetadataResponse = { url: string, filename: string, size: string, size_bytes: number, resumable: boolean, }; diff --git a/src/bindings/PersistedSettings.ts b/src/bindings/PersistedSettings.ts index ea75a91..c05dad7 100644 --- a/src/bindings/PersistedSettings.ts +++ b/src/bindings/PersistedSettings.ts @@ -8,4 +8,13 @@ import type { SettingsTab } from "./SettingsTab"; import type { SiteLogin } from "./SiteLogin"; import type { Theme } from "./Theme"; -export type PersistedSettings = { theme: Theme, baseDownloadFolder: string, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array, maxConcurrentDownloads: number, globalSpeedLimit: string, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array, extensionPairingToken: string, autoCheckUpdates: boolean, keychainAccessGranted: boolean, }; +export type PersistedSettings = { theme: Theme, baseDownloadFolder: string, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array, maxConcurrentDownloads: number, globalSpeedLimit: string, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array, +/** + * The HMAC shared secret for the browser extension. It is persisted in the + * settings database so that startup never needs to touch the OS keychain. + * The keychain is still used as defence-in-depth — grant_keychain_access + * writes the token there — but the DB copy is the primary read path, + * eliminating the OS credential prompt that macOS shows when the binary + * signature changes after an update. + */ +extensionPairingToken: string, autoCheckUpdates: boolean, keychainAccessGranted: boolean, }; diff --git a/src/components/AddDownloadsModal.tsx b/src/components/AddDownloadsModal.tsx index 8066b72..2ff1751 100644 --- a/src/components/AddDownloadsModal.tsx +++ b/src/components/AddDownloadsModal.tsx @@ -264,7 +264,8 @@ export const AddDownloadsModal = () => { ), size: meta.size_bytes ? meta.size : undefined, sizeBytes: meta.size_bytes || undefined, - status: 'ready' + status: 'ready', + resumable: meta.resumable }) )); } @@ -617,6 +618,7 @@ export const AddDownloadsModal = () => { ? finalLocation : destinationOverrides[itemIndex], isMedia: item.isMedia, + resumable: item.resumable, mediaFormatSelector: formatSelector, size: item.size || (item.sizeBytes ? formatBytes(item.sizeBytes) : undefined) }, action); diff --git a/src/components/DownloadItem.tsx b/src/components/DownloadItem.tsx index 7028ba4..c878034 100644 --- a/src/components/DownloadItem.tsx +++ b/src/components/DownloadItem.tsx @@ -12,7 +12,7 @@ interface DownloadItemProps { index: number; tableGridTemplate: string; setContextMenu: (menu: { x: number; y: number; id: string }) => void; - handlePause: (id: string) => void; + handlePause: (id: string, skipConfirm?: boolean) => void; handleResume: (item: DownloadItemType) => void; getCategoryIcon: (category: string) => React.ReactNode; isSelected: boolean; diff --git a/src/components/DownloadTable.tsx b/src/components/DownloadTable.tsx index 865e98c..b078483 100644 --- a/src/components/DownloadTable.tsx +++ b/src/components/DownloadTable.tsx @@ -255,7 +255,15 @@ export const DownloadTable: React.FC = ({ filter }) => { } }; - const handlePause = async (id: string) => { + const handlePause = async (id: string, skipConfirm = false) => { + const download = useDownloadStore.getState().downloads.find(d => d.id === id); + if (!skipConfirm && download && download.resumable === false) { + const confirmPause = window.confirm("This download does not support resuming. If you pause it, you will have to start over again later. Are you sure you want to pause?"); + if (!confirmPause) { + return; + } + } + try { await invoke('pause_download', { id }); } catch (e) { @@ -324,7 +332,20 @@ export const DownloadTable: React.FC = ({ filter }) => { className="main-control-button" disabled={sortedDownloads.length === 0} onClick={() => { - sortedDownloads.filter(d => canPauseDownload(d.status)).forEach(d => handlePause(d.id)); + const toPause = sortedDownloads.filter(d => canPauseDownload(d.status)); + const nonResumableCount = toPause.filter(d => d.resumable === false).length; + if (nonResumableCount > 0) { + const confirmPause = window.confirm( + nonResumableCount === 1 + ? "1 download does not support resuming. If you pause it, you will have to start over again later. Are you sure you want to pause?" + : `${nonResumableCount} downloads do not support resuming. If you pause them, you will have to start over again later. Are you sure you want to pause?` + ); + if (!confirmPause) { + return; + } + } + // Skip the individual check by passing a flag to handlePause, or just invoking directly. + toPause.forEach(d => handlePause(d.id, true)); }} title="Pause All" > diff --git a/src/index.css b/src/index.css index dfc0653..438a3ac 100644 --- a/src/index.css +++ b/src/index.css @@ -394,6 +394,16 @@ transform: scale(0.94); } + .download-cell-right .app-icon-button:hover:not(:disabled) { + background: hsl(var(--accent-color) / 0.15); + color: hsl(var(--accent-color)); + } + + .download-cell-right .app-icon-button:active:not(:disabled) { + background: hsl(var(--accent-color) / 0.25); + transform: scale(0.94); + } + .app-modal-backdrop { background: hsl(0 0% 0% / 0.2); animation: fade-in 150ms ease-out; diff --git a/src/utils/addDownloadMetadata.ts b/src/utils/addDownloadMetadata.ts index 8756bfa..eb63fc4 100644 --- a/src/utils/addDownloadMetadata.ts +++ b/src/utils/addDownloadMetadata.ts @@ -27,6 +27,7 @@ export interface AddDownloadDraftRow { status: MetadataStatus; generation: number; isMedia: boolean; + resumable?: boolean; formats?: AddMediaFormat[]; selectedFormat?: number; }