From 76850f24337bccb8a2631195ecec6612e58b876c Mon Sep 17 00:00:00 2001 From: NimBold Date: Wed, 15 Jul 2026 01:25:59 +0330 Subject: [PATCH] fix(ui): harden main window interactions --- src-tauri/tauri.linux.conf.json | 2 +- src/components/AddDownloadsModal.tsx | 28 +++- src/components/DeleteConfirmationModal.tsx | 18 ++- src/components/DownloadItem.tsx | 2 +- src/components/DuplicateResolutionModal.tsx | 19 ++- src/components/KeychainPermissionModal.tsx | 20 ++- src/components/PropertiesModal.tsx | 20 ++- src/components/Sidebar.tsx | 13 +- src/index.css | 7 +- src/store/downloadProgressStore.ts | 26 ++++ src/store/downloadStore.test.ts | 30 ++++ src/store/downloadStore.ts | 145 +++++++++----------- src/store/useDownloadStore.test.ts | 23 ++++ src/store/useDownloadStore.ts | 2 + 14 files changed, 258 insertions(+), 97 deletions(-) create mode 100644 src/store/downloadProgressStore.ts diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index 7b4a26b..f56a439 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -9,7 +9,7 @@ "minWidth": 960, "minHeight": 640, "transparent": false, - "decorations": true + "decorations": false } ] }, diff --git a/src/components/AddDownloadsModal.tsx b/src/components/AddDownloadsModal.tsx index 558fa28..95bdd7e 100644 --- a/src/components/AddDownloadsModal.tsx +++ b/src/components/AddDownloadsModal.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useCallback, useState, useEffect, useRef } from 'react'; import { useDownloadStore, getSiteLogin, @@ -154,6 +154,15 @@ export const AddDownloadsModal = () => { return hasExtensionRequestContext ? '' : pendingAddFilename; }; + const closeModalFromDismissAction = useCallback(() => { + if (isSubmitting) return; + const hasPendingInput = Boolean( + urls.trim() || pendingAddUrls.trim() || parsedItems.length || headers.trim() || cookies.trim() + ); + if (hasPendingInput && !window.confirm('Discard this download setup?')) return; + toggleAddModal(false); + }, [cookies, headers, isSubmitting, parsedItems.length, pendingAddUrls, toggleAddModal, urls]); + useEffect(() => { if (!isAddModalOpen) { modalSessionRef.current = false; @@ -233,18 +242,20 @@ export const AddDownloadsModal = () => { }, [isQueueMenuOpen]); useEffect(() => { - if (!isQueueMenuOpen && !showingDuplicates) return; + if (!isAddModalOpen) return; const closeOnEscape = (event: KeyboardEvent) => { if (event.key !== 'Escape') return; if (showingDuplicates) { setShowingDuplicates(false); - } else { + } else if (isQueueMenuOpen) { setIsQueueMenuOpen(false); + } else { + closeModalFromDismissAction(); } }; window.addEventListener('keydown', closeOnEscape); return () => window.removeEventListener('keydown', closeOnEscape); - }, [isQueueMenuOpen, showingDuplicates]); + }, [closeModalFromDismissAction, isAddModalOpen, isQueueMenuOpen, showingDuplicates]); useEffect(() => { const requestId = ++freeSpaceRequestRef.current; @@ -859,7 +870,14 @@ export const AddDownloadsModal = () => { onCancel={() => setShowingDuplicates(false)} /> )} -
+
{ + if (event.target === event.currentTarget) closeModalFromDismissAction(); + }} + role="dialog" + aria-modal="true" + >
{/* Main Content Split */} diff --git a/src/components/DeleteConfirmationModal.tsx b/src/components/DeleteConfirmationModal.tsx index 1647ea4..06d7333 100644 --- a/src/components/DeleteConfirmationModal.tsx +++ b/src/components/DeleteConfirmationModal.tsx @@ -14,6 +14,15 @@ export const DeleteConfirmationModal: React.FC = () => { } }, [deleteModalState.isOpen]); + useEffect(() => { + if (!deleteModalState.isOpen || isRemoving) return; + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') closeDeleteModal(); + }; + window.addEventListener('keydown', handleEscape); + return () => window.removeEventListener('keydown', handleEscape); + }, [closeDeleteModal, deleteModalState.isOpen, isRemoving]); + if (!deleteModalState.isOpen) return null; const handleCancel = () => { @@ -53,7 +62,14 @@ export const DeleteConfirmationModal: React.FC = () => { const handleDeleteFile = () => removeMany(true); return ( -
+
{ + if (event.target === event.currentTarget && !isRemoving) handleCancel(); + }} + role="dialog" + aria-modal="true" + >
e.stopPropagation()} diff --git a/src/components/DownloadItem.tsx b/src/components/DownloadItem.tsx index 65e208e..77c2d6f 100644 --- a/src/components/DownloadItem.tsx +++ b/src/components/DownloadItem.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useShallow } from 'zustand/react/shallow'; import { useDownloadStore } from '../store/useDownloadStore'; -import { useDownloadProgressStore } from '../store/downloadStore'; +import { useDownloadProgressStore } from '../store/downloadProgressStore'; import { Play, Pause, MoreVertical, Clock, ArrowUp, ArrowDown } from 'lucide-react'; import type { DownloadItem as DownloadItemType } from '../bindings/DownloadItem'; import { canPauseDownload, canStartDownload, startActionLabel } from '../utils/downloadActions'; diff --git a/src/components/DuplicateResolutionModal.tsx b/src/components/DuplicateResolutionModal.tsx index aeb7d26..11f7f68 100644 --- a/src/components/DuplicateResolutionModal.tsx +++ b/src/components/DuplicateResolutionModal.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; export type DuplicateReason = { type: 'url', msg: string } | { type: 'file', msg: string }; type DuplicateResolution = 'rename' | 'replace' | 'skip'; @@ -20,12 +20,27 @@ interface Props { export const DuplicateResolutionModal = ({ conflicts: initialConflicts, onConfirm, onCancel }: Props) => { const [conflicts, setConflicts] = useState(initialConflicts); + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') onCancel(); + }; + window.addEventListener('keydown', handleEscape); + return () => window.removeEventListener('keydown', handleEscape); + }, [onCancel]); + const updateResolution = (id: string, resolution: DuplicateResolution) => { setConflicts(conflicts.map(c => c.id === id ? { ...c, resolution } : c)); }; return ( -
+
{ + if (event.target === event.currentTarget) onCancel(); + }} + role="dialog" + aria-modal="true" + >

Duplicate Downloads Detected

diff --git a/src/components/KeychainPermissionModal.tsx b/src/components/KeychainPermissionModal.tsx index 44cd688..2aac463 100644 --- a/src/components/KeychainPermissionModal.tsx +++ b/src/components/KeychainPermissionModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useSettingsStore } from '../store/useSettingsStore'; import { invokeCommand as invoke } from '../ipc'; import { KeyRound, ShieldAlert } from 'lucide-react'; @@ -11,6 +11,15 @@ export const KeychainPermissionModal: React.FC = () => { const [isGranting, setIsGranting] = useState(false); const [error, setError] = useState(null); + useEffect(() => { + if (!showKeychainModal || isGranting) return; + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') setShowKeychainModal(false); + }; + window.addEventListener('keydown', handleEscape); + return () => window.removeEventListener('keydown', handleEscape); + }, [isGranting, setShowKeychainModal, showKeychainModal]); + if (!showKeychainModal) { return null; } @@ -62,7 +71,14 @@ export const KeychainPermissionModal: React.FC = () => { }; return ( -
+
{ + if (event.target === event.currentTarget && !isGranting) handleLater(); + }} + role="dialog" + aria-modal="true" + >
e.stopPropagation()} diff --git a/src/components/PropertiesModal.tsx b/src/components/PropertiesModal.tsx index 6da6bf2..ab8a49f 100644 --- a/src/components/PropertiesModal.tsx +++ b/src/components/PropertiesModal.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useDownloadStore, DownloadItem } from '../store/useDownloadStore'; -import { useDownloadProgressStore } from '../store/downloadStore'; +import { useDownloadProgressStore } from '../store/downloadProgressStore'; import { useShallow } from 'zustand/react/shallow'; import { useSettingsStore } from '../store/useSettingsStore'; import { ChevronDown, ChevronRight, FolderPlus, Info, CheckCircle, AlertCircle, Play, Pause } from 'lucide-react'; @@ -114,6 +114,15 @@ export const PropertiesModal = () => { } }, [selectedPropertiesDownloadId, baseDownloadFolder, setSelectedPropertiesDownloadId]); + useEffect(() => { + if (!selectedPropertiesDownloadId) return; + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') setSelectedPropertiesDownloadId(null); + }; + window.addEventListener('keydown', handleEscape); + return () => window.removeEventListener('keydown', handleEscape); + }, [selectedPropertiesDownloadId, setSelectedPropertiesDownloadId]); + if (!selectedPropertiesDownloadId || !item) return null; const handleBrowse = async () => { @@ -186,7 +195,14 @@ export const PropertiesModal = () => { else if (item.status === 'failed') { statusColor = 'text-red-500'; StatusIcon = AlertCircle; } return ( -
+
{ + if (event.target === event.currentTarget) setSelectedPropertiesDownloadId(null); + }} + role="dialog" + aria-modal="true" + >
{/* Header Summary */} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 9b21c36..3e314ff 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -82,6 +82,9 @@ export const Sidebar: React.FC = (props) => { const queueId = selectedFilter.replace('queue:', ''); const q = queues.find(q => q.id === queueId); if (q && !q.isMain) { + if (!window.confirm(`Delete queue "${q.name}"? Its unfinished downloads will move to Main Queue.`)) { + return; + } void removeQueue(queueId).catch(error => { addToast({ message: `Could not delete queue: ${String(error)}`, variant: 'error', isActionable: true }); }); @@ -92,7 +95,7 @@ export const Sidebar: React.FC = (props) => { }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [selectedFilter, activeView, queues]); + }, [addToast, activeView, queues, removeQueue, selectedFilter]); const getCount = (filter: SidebarFilter) => { if (filter.startsWith('queue:')) { @@ -383,6 +386,14 @@ export const Sidebar: React.FC = (props) => { className="w-full text-left px-3 py-1.5 flex items-center hover:bg-red-500/20 text-red-400" onClick={() => { const queueId = contextMenu.id; + const queue = queues.find(q => q.id === queueId); + if (!queue || queue.isMain) { + setContextMenu(null); + return; + } + if (!window.confirm(`Delete queue "${queue.name}"? Its unfinished downloads will move to Main Queue.`)) { + return; + } setContextMenu(null); void removeQueue(queueId).catch(error => { addToast({ diff --git a/src/index.css b/src/index.css index 47ff054..dea3f64 100644 --- a/src/index.css +++ b/src/index.css @@ -1017,6 +1017,7 @@ html[data-list-density="relaxed"] { z-index: 60; width: 8px; cursor: col-resize; + -webkit-app-region: no-drag; } .sidebar-resize-handle::after { @@ -1254,6 +1255,7 @@ html[data-list-density="relaxed"] { border-radius: 6px; color: hsl(var(--text-secondary)); background: transparent; + -webkit-app-region: no-drag; } .sidebar-toggle-button:hover { @@ -1653,7 +1655,7 @@ html[data-list-density="relaxed"] { letter-spacing: -0.01em; } - .main-control-group { +.main-control-group { margin-left: auto; height: 28px; display: inline-flex; @@ -1662,6 +1664,7 @@ html[data-list-density="relaxed"] { border-radius: 15px; border: 1px solid hsl(var(--border-modal)); background: hsl(var(--bg-input)); + -webkit-app-region: no-drag; } .main-control-button { @@ -1672,6 +1675,7 @@ html[data-list-density="relaxed"] { justify-content: center; color: hsl(var(--text-secondary)); border-right: 1px solid hsl(var(--border-color)); + -webkit-app-region: no-drag; } .main-control-button svg { @@ -1842,6 +1846,7 @@ html[data-list-density="relaxed"] { z-index: 5; width: 8px; cursor: col-resize; + -webkit-app-region: no-drag; } .column-resize-handle::after { diff --git a/src/store/downloadProgressStore.ts b/src/store/downloadProgressStore.ts new file mode 100644 index 0000000..e8b538d --- /dev/null +++ b/src/store/downloadProgressStore.ts @@ -0,0 +1,26 @@ +import { create } from 'zustand'; +import type { DownloadProgressEvent } from '../bindings/DownloadProgressEvent'; + +interface DownloadProgressState { + progressMap: Record; + updateDownloadProgress: (id: string, payload: DownloadProgressEvent) => void; + clearDownloadProgress: (id: string) => void; +} + +export const useDownloadProgressStore = create((set) => ({ + progressMap: {}, + updateDownloadProgress: (id, payload) => + set((state) => ({ + progressMap: { + ...state.progressMap, + [id]: payload, + }, + })), + clearDownloadProgress: (id) => + set((state) => { + if (!(id in state.progressMap)) return state; + const next = { ...state.progressMap }; + delete next[id]; + return { progressMap: next }; + }), +})); diff --git a/src/store/downloadStore.test.ts b/src/store/downloadStore.test.ts index ad65b6e..e65fb20 100644 --- a/src/store/downloadStore.test.ts +++ b/src/store/downloadStore.test.ts @@ -83,4 +83,34 @@ describe('useDownloadProgressStore', () => { expect(useDownloadStore.getState().downloads[0].status).toBe('completed'); release(); }); + + it('clears progress when events arrive after a download row was removed', async () => { + const handlers: Record void> = {}; + vi.mocked(ipc.listenEvent).mockImplementation((event, handler) => { + handlers[event] = handler as (event: any) => void; + return Promise.resolve(vi.fn()); + }); + useDownloadProgressStore.getState().updateDownloadProgress('removed', { + id: 'removed', + fraction: 0.8, + speed: '1 MB/s', + eta: '2s', + size: '8 MB', + size_is_final: false + }); + useDownloadStore.setState({ downloads: [] }); + + const release = await initDownloadListener(); + handlers['download-progress']({ payload: { + id: 'removed', + fraction: 0.9, + speed: '2 MB/s', + eta: '1s', + size: '9 MB', + size_is_final: false + } }); + + expect(useDownloadProgressStore.getState().progressMap).toEqual({}); + release(); + }); }); diff --git a/src/store/downloadStore.ts b/src/store/downloadStore.ts index fd409e7..5dcca1d 100644 --- a/src/store/downloadStore.ts +++ b/src/store/downloadStore.ts @@ -1,36 +1,13 @@ -import { create } from 'zustand'; import type { UnlistenFn } from '@tauri-apps/api/event'; -import type { DownloadProgressEvent } from '../bindings/DownloadProgressEvent'; import type { DownloadStatus } from '../bindings/DownloadStatus'; import { listenEvent as listen } from '../ipc'; import type { DownloadItem } from '../bindings/DownloadItem'; import { categoryForFileName } from '../utils/downloads'; - -interface DownloadProgressState { - progressMap: Record; - updateDownloadProgress: (id: string, payload: DownloadProgressEvent) => void; - clearDownloadProgress: (id: string) => void; -} +import { useDownloadProgressStore } from './downloadProgressStore'; import { useDownloadStore } from './useDownloadStore'; -export const useDownloadProgressStore = create((set) => ({ - progressMap: {}, - updateDownloadProgress: (id, payload) => - set((state) => ({ - progressMap: { - ...state.progressMap, - [id]: payload, - }, - })), - clearDownloadProgress: (id) => - set((state) => { - if (!(id in state.progressMap)) return state; - const next = { ...state.progressMap }; - delete next[id]; - return { progressMap: next }; - }), -})); +export { useDownloadProgressStore } from './downloadProgressStore'; let unlistenProgress: UnlistenFn | null = null; let unlistenState: UnlistenFn | null = null; @@ -54,6 +31,12 @@ const startDownloadListeners = async () => { const payload = event.payload; const mainStore = useDownloadStore.getState(); const current = mainStore.downloads.find(d => d.id === payload.id); + if (!current) { + // A removed row can still have one queued sidecar event in flight. + // Do not let that event recreate an orphaned progress entry. + useDownloadProgressStore.getState().clearDownloadProgress(payload.id); + return; + } // A sidecar can flush one last progress chunk after a pause, failure, // or completion event. Do not let that stale chunk repopulate the live // progress map or overwrite a later lifecycle's first frame. @@ -61,73 +44,73 @@ const startDownloadListeners = async () => { return; } useDownloadProgressStore.getState().updateDownloadProgress(payload.id, payload); - if (current) { - const shouldUpdateSize = Boolean(payload.size && (!current.isMedia || payload.size_is_final)); - const updates: Partial = {}; - if (current.status === 'downloading' || current.status === 'processing') { - updates.fraction = payload.fraction; - updates.speed = payload.speed; - updates.eta = payload.eta; - } - if (shouldUpdateSize && current.size !== payload.size) { - updates.size = payload.size!; - } - if (Object.keys(updates).length > 0) { - mainStore.updateDownload(payload.id, updates); - } + const shouldUpdateSize = Boolean(payload.size && (!current.isMedia || payload.size_is_final)); + const updates: Partial = {}; + if (current.status === 'downloading' || current.status === 'processing') { + updates.fraction = payload.fraction; + updates.speed = payload.speed; + updates.eta = payload.eta; + } + if (shouldUpdateSize && current.size !== payload.size) { + updates.size = payload.size!; + } + if (Object.keys(updates).length > 0) { + mainStore.updateDownload(payload.id, updates); } }), listen('download-state', (event) => { const payload = event.payload; const mainStore = useDownloadStore.getState(); const current = mainStore.downloads.find(d => d.id === payload.id); - if (current) { - const status = payload.status as DownloadStatus; + if (!current) { + useDownloadProgressStore.getState().clearDownloadProgress(payload.id); + return; + } + const status = payload.status as DownloadStatus; - // Prevent race condition: don't transition backwards from terminal state - if ((current.status === 'completed' || current.status === 'failed') && - status !== current.status) { - return; - } + // Prevent race condition: don't transition backwards from terminal state + if ((current.status === 'completed' || current.status === 'failed') && + status !== current.status) { + return; + } - const progress = useDownloadProgressStore.getState().progressMap[payload.id]; - const updates: Partial = { - status, - ...(progress ? { fraction: progress.fraction } : {}), - ...(payload.error ? { lastError: payload.error } : {}), - ...((status === 'downloading' || status === 'retrying') - ? { lastTry: new Date().toISOString() } - : {}) - }; - if (!payload.error && status !== 'failed' && status !== 'retrying') { - updates.lastError = undefined; - } - if (payload.fileName && payload.fileName !== current.fileName) { - updates.fileName = payload.fileName; - updates.category = categoryForFileName(payload.fileName); - } - if (status !== 'downloading') { - updates.speed = '-'; - updates.eta = '-'; - } - mainStore.updateDownload(payload.id, updates); + const progress = useDownloadProgressStore.getState().progressMap[payload.id]; + const updates: Partial = { + status, + ...(progress ? { fraction: progress.fraction } : {}), + ...(payload.error ? { lastError: payload.error } : {}), + ...((status === 'downloading' || status === 'retrying') + ? { lastTry: new Date().toISOString() } + : {}) + }; + if (!payload.error && status !== 'failed' && status !== 'retrying') { + updates.lastError = undefined; + } + if (payload.fileName && payload.fileName !== current.fileName) { + updates.fileName = payload.fileName; + updates.category = categoryForFileName(payload.fileName); + } + if (status !== 'downloading') { + updates.speed = '-'; + updates.eta = '-'; + } + mainStore.updateDownload(payload.id, updates); - if (status === 'completed' || status === 'failed' || status === 'paused') { - mainStore.setPendingOrder(mainStore.pendingOrder.filter(id => id !== payload.id)); - } else if (status === 'queued') { - if (!mainStore.pendingOrder.includes(payload.id)) { - mainStore.setPendingOrder([...mainStore.pendingOrder, payload.id]); - } + if (status === 'completed' || status === 'failed' || status === 'paused') { + mainStore.setPendingOrder(mainStore.pendingOrder.filter(id => id !== payload.id)); + } else if (status === 'queued') { + if (!mainStore.pendingOrder.includes(payload.id)) { + mainStore.setPendingOrder([...mainStore.pendingOrder, payload.id]); } + } - if (status === 'queued' || status === 'downloading' || status === 'processing' || status === 'retrying') { - mainStore.registerBackendIds([payload.id]); - } else if (status === 'completed' || status === 'failed') { - mainStore.unregisterBackendIds([payload.id]); - } - if (status === 'completed' || status === 'failed' || status === 'paused') { - useDownloadProgressStore.getState().clearDownloadProgress(payload.id); - } + if (status === 'queued' || status === 'downloading' || status === 'processing' || status === 'retrying') { + mainStore.registerBackendIds([payload.id]); + } else if (status === 'completed' || status === 'failed') { + mainStore.unregisterBackendIds([payload.id]); + } + if (status === 'completed' || status === 'failed' || status === 'paused') { + useDownloadProgressStore.getState().clearDownloadProgress(payload.id); } }), listen('tray-action', (event) => { diff --git a/src/store/useDownloadStore.test.ts b/src/store/useDownloadStore.test.ts index acccd47..b92c47e 100644 --- a/src/store/useDownloadStore.test.ts +++ b/src/store/useDownloadStore.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { getProxyArgs, getSiteLogin, normalizeCustomProxy, useDownloadStore } from './useDownloadStore'; +import { useDownloadProgressStore } from './downloadProgressStore'; import { useSettingsStore } from './useSettingsStore'; import * as ipc from '../ipc'; @@ -91,6 +92,7 @@ describe('useDownloadStore', () => { pendingAddRequestContexts: {}, pendingAddRequestVersion: 0, }); + useDownloadProgressStore.setState({ progressMap: {} }); }); it('invalidates in-flight Add-modal handoffs when the modal is toggled', () => { @@ -869,6 +871,27 @@ describe('useDownloadStore', () => { .toEqual(['active']); }); + it('clears live progress when a download is removed', async () => { + useDownloadStore.setState({ + downloads: [ + { id: 'removed-progress', url: 'https://example.com/file', fileName: 'file', status: 'paused', category: 'Other', dateAdded: '' } + ] as any[] + }); + useDownloadProgressStore.getState().updateDownloadProgress('removed-progress', { + id: 'removed-progress', + fraction: 0.5, + speed: '1 MB/s', + eta: '10s', + size: '2 MB', + size_is_final: false + }); + vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never); + + await useDownloadStore.getState().removeDownload('removed-progress'); + + expect(useDownloadProgressStore.getState().progressMap).toEqual({}); + }); + it('asks the backend to preserve resumable assets during replacement removal', async () => { useDownloadStore.setState({ downloads: [ diff --git a/src/store/useDownloadStore.ts b/src/store/useDownloadStore.ts index aba1209..5bfbc94 100644 --- a/src/store/useDownloadStore.ts +++ b/src/store/useDownloadStore.ts @@ -7,6 +7,7 @@ import type { DownloadStatus } from '../bindings/DownloadStatus'; import type { ExtensionDownload } from '../bindings/ExtensionDownload'; import type { Queue } from '../bindings/Queue'; import { useSettingsStore } from './useSettingsStore'; +import { useDownloadProgressStore } from './downloadProgressStore'; import { categoryForFileName, isActiveDownloadStatus, isTransferActiveStatus, normalizeSpeedLimitForBackend, redactDownloadForPersistence } from '../utils/downloads'; import { resolveCategoryDestination @@ -778,6 +779,7 @@ export const useDownloadStore = create((set, get) => ({ Array.from(state.backendRegisteredIds).filter(registeredId => registeredId !== id) ) })); + useDownloadProgressStore.getState().clearDownloadProgress(id); info(`Download ${id} removed`); syncSystemIntegrations(); },