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
This commit is contained in:
NimBold
2026-06-28 00:03:12 +03:30
parent 1cba5ee556
commit 77b940df60
10 changed files with 63 additions and 7 deletions
+2
View File
@@ -79,6 +79,8 @@ pub struct DownloadItem {
pub category: DownloadCategory,
pub date_added: String,
#[ts(optional)]
pub resumable: Option<bool>,
#[ts(optional)]
pub connections: Option<i32>,
#[ts(optional)]
pub speed_limit: Option<String>,
+11
View File
@@ -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,
})
}
+1 -1
View File
@@ -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, };
+1 -1
View File
@@ -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, };
+10 -1
View File
@@ -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<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, 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<SiteLogin>, 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<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, 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<SiteLogin>,
/**
* 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, };
+3 -1
View File
@@ -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);
+1 -1
View File
@@ -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;
+23 -2
View File
@@ -255,7 +255,15 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ 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<DownloadTableProps> = ({ 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"
>
+10
View File
@@ -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;
+1
View File
@@ -27,6 +27,7 @@ export interface AddDownloadDraftRow {
status: MetadataStatus;
generation: number;
isMedia: boolean;
resumable?: boolean;
formats?: AddMediaFormat[];
selectedFormat?: number;
}