mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
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:
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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, };
|
||||
|
||||
@@ -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, };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface AddDownloadDraftRow {
|
||||
status: MetadataStatus;
|
||||
generation: number;
|
||||
isMedia: boolean;
|
||||
resumable?: boolean;
|
||||
formats?: AddMediaFormat[];
|
||||
selectedFormat?: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user