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
+17
View File
@@ -681,6 +681,23 @@ describe('useDownloadStore', () => {
.toEqual(['active']);
});
it('asks the backend to preserve resumable assets during replacement removal', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'paused', url: 'https://example.com/file', fileName: 'file', status: 'paused', category: 'Other', dateAdded: '' }
] as any[]
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
await useDownloadStore.getState().removeDownload('paused', true, true);
expect(ipc.invokeCommand).toHaveBeenCalledWith('remove_download', {
id: 'paused',
deleteAssets: true,
preserveResumable: true
});
});
it('starts staged queue items in their persisted queue order', async () => {
useDownloadStore.setState({
downloads: [
+7 -3
View File
@@ -356,7 +356,7 @@ interface DownloadState {
setSelectedPropertiesDownloadId: (id: string | null) => void;
addDownload: (item: DownloadDraft, action: AddDownloadAction) => Promise<boolean>;
updateDownload: (id: string, updates: Partial<DownloadItem>) => void;
removeDownload: (id: string, deleteFile?: boolean) => Promise<void>;
removeDownload: (id: string, deleteFile?: boolean, preserveResumable?: boolean) => Promise<void>;
pauseDownload: (id: string) => Promise<void>;
redownload: (id: string) => Promise<void>;
resumeDownload: (id: string) => Promise<boolean>;
@@ -670,12 +670,16 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
syncSystemIntegrations();
}
},
removeDownload: async (id, deleteFile = false) => {
removeDownload: async (id, deleteFile = false, preserveResumable = false) => {
await invalidateDispatch(id);
const item = get().downloads.find(d => d.id === id);
if (item) {
await invoke('remove_download', { id, deleteAssets: deleteFile });
await invoke('remove_download', {
id,
deleteAssets: deleteFile,
preserveResumable
});
}
set((state) => ({