From 9e26be5b2c4e56c80ea8eb3699341d0d194ab9bb Mon Sep 17 00:00:00 2001 From: NimBold Date: Mon, 15 Jun 2026 13:29:20 +0330 Subject: [PATCH] fix: app nap ipc drop and implement delete modal --- src-tauri/src/download.rs | 15 ++++- src-tauri/src/extension_server.rs | 13 +++- src-tauri/src/lib.rs | 4 +- src/App.tsx | 16 +++-- src/bindings/DownloadProgressEvent.ts | 2 +- src/components/DeleteConfirmationModal.tsx | 73 +++++++++++++++++++++ src/components/DownloadTable.tsx | 21 ++---- src/store/useDownloadStore.ts | 75 +++++++--------------- 8 files changed, 141 insertions(+), 78 deletions(-) create mode 100644 src/components/DeleteConfirmationModal.tsx diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs index 331c468..6f1537a 100644 --- a/src-tauri/src/download.rs +++ b/src-tauri/src/download.rs @@ -18,7 +18,7 @@ use tokio::{ }; use uuid::Uuid; -const PROGRESS_INTERVAL: Duration = Duration::from_millis(150); +const PROGRESS_INTERVAL: Duration = Duration::from_millis(1000); const WRITE_BUFFER_CAPACITY: usize = 256 * 1024; #[derive(Debug)] @@ -153,6 +153,7 @@ impl CoordinatorEventSink { fraction, speed: format_speed(speed_bytes), eta, + size: total.map(|t| format_size(t as f64)), }, ); } @@ -586,6 +587,18 @@ fn format_speed(bytes_per_second: f64) -> String { } } +fn format_size(bytes: f64) -> String { + if bytes >= 1024.0 * 1024.0 * 1024.0 { + format!("{:.2} GB", bytes / (1024.0 * 1024.0 * 1024.0)) + } else if bytes >= 1024.0 * 1024.0 { + format!("{:.1} MB", bytes / (1024.0 * 1024.0)) + } else if bytes >= 1024.0 { + format!("{:.1} KB", bytes / 1024.0) + } else { + format!("{bytes:.0} B") + } +} + fn format_duration(seconds: f64) -> String { if seconds >= 3600.0 { format!("{:.0}h {:.0}m", seconds / 3600.0, (seconds % 3600.0) / 60.0) diff --git a/src-tauri/src/extension_server.rs b/src-tauri/src/extension_server.rs index 4fe44e4..cae826c 100644 --- a/src-tauri/src/extension_server.rs +++ b/src-tauri/src/extension_server.rs @@ -164,8 +164,17 @@ async fn download_handler( }; if let Some(window) = state.app_handle.get_webview_window("main") { - let _ = window.show(); - let _ = window.set_focus(); + let is_visible = window.is_visible().unwrap_or(true); + if !is_visible { + let _ = window.show(); + let _ = window.set_focus(); + // Sleep briefly to let the webview wake up from macOS App Nap + // otherwise the IPC event emitted immediately after is dropped. + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + } else { + let _ = window.show(); + let _ = window.set_focus(); + } } if state.app_handle.emit("extension-add-download", download).is_err() { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 96848be..6628999 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -367,6 +367,7 @@ pub struct DownloadProgressEvent { fraction: f64, speed: String, eta: String, + size: Option, } fn collect_download_uris(url: &str, mirrors: Option<&str>) -> Vec { @@ -790,12 +791,13 @@ pub(crate) async fn start_media_download_internal( .unwrap_or_else(|| "-".to_string()); let now = std::time::Instant::now(); - if now.duration_since(last_progress_at) >= std::time::Duration::from_millis(150) { + if now.duration_since(last_progress_at) >= std::time::Duration::from_millis(1000) { let _ = app_handle.emit("download-progress", DownloadProgressEvent { id: id.to_string(), fraction: overall_fraction, speed, eta, + size: None, }); last_progress_at = now; } diff --git a/src/App.tsx b/src/App.tsx index 91cf5b2..6defbf0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { DownloadTable } from "./components/DownloadTable"; import { AddDownloadsModal } from "./components/AddDownloadsModal"; import SettingsView from "./components/SettingsView"; import { PropertiesModal } from "./components/PropertiesModal"; +import { DeleteConfirmationModal } from "./components/DeleteConfirmationModal"; import { listenEvent as listen, invokeCommand as invoke } from "./ipc"; import { useDownloadStore, MAIN_QUEUE_ID } from './store/useDownloadStore'; import { useSettingsStore } from "./store/useSettingsStore"; @@ -182,15 +183,19 @@ function App() { }, [theme]); useEffect(() => { - const unlistenProgress = listen('download-progress', (event) => { - const { id, fraction, speed, eta } = event.payload; + const unlistenProgress = listen('download-progress', (event: any) => { + const { id, fraction, speed, eta, size } = event.payload; const state = useDownloadStore.getState(); const current = state.downloads.find(d => d.id === id); + + const updates: any = { fraction, speed, eta }; + if (size) updates.size = size; + if (current && current.status === 'queued') { - updateDownload(id, { status: 'downloading', fraction, speed, eta }); - } else { - updateDownload(id, { fraction, speed, eta }); + updates.status = 'downloading'; } + + updateDownload(id, updates); }); const unlistenComplete = listen('download-complete', (event) => { @@ -285,6 +290,7 @@ function App() { + ); } diff --git a/src/bindings/DownloadProgressEvent.ts b/src/bindings/DownloadProgressEvent.ts index 1a135ac..3cb1505 100644 --- a/src/bindings/DownloadProgressEvent.ts +++ b/src/bindings/DownloadProgressEvent.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 DownloadProgressEvent = { id: string, fraction: number, speed: string, eta: string, }; +export type DownloadProgressEvent = { id: string, fraction: number, speed: string, eta: string, size: string | null, }; diff --git a/src/components/DeleteConfirmationModal.tsx b/src/components/DeleteConfirmationModal.tsx new file mode 100644 index 0000000..8ec9b59 --- /dev/null +++ b/src/components/DeleteConfirmationModal.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { useDownloadStore } from '../store/useDownloadStore'; +import { AlertTriangle } from 'lucide-react'; + +export const DeleteConfirmationModal: React.FC = () => { + const { deleteModalState, closeDeleteModal, removeDownload } = useDownloadStore(); + + if (!deleteModalState.isOpen) return null; + + const handleCancel = () => { + closeDeleteModal(); + }; + + const handleRemoveFromList = () => { + if (deleteModalState.downloadId) { + removeDownload(deleteModalState.downloadId, false); + } + closeDeleteModal(); + }; + + const handleDeleteFile = () => { + if (deleteModalState.downloadId) { + removeDownload(deleteModalState.downloadId, true); + } + closeDeleteModal(); + }; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+ +
+

+ Remove Download +

+
+ + {/* Body */} +
+ Are you sure you want to remove this item from the list? You can also choose to delete the underlying file from your hard drive. +
+ + {/* Footer */} +
+ + + +
+
+
+ ); +}; diff --git a/src/components/DownloadTable.tsx b/src/components/DownloadTable.tsx index b85f0bb..d676792 100644 --- a/src/components/DownloadTable.tsx +++ b/src/components/DownloadTable.tsx @@ -11,7 +11,7 @@ interface DownloadTableProps { } export const DownloadTable: React.FC = ({ filter }) => { - const { downloads, toggleAddModal, updateDownload, removeDownload, clearFinished, redownload } = useDownloadStore(); + const { downloads, toggleAddModal, updateDownload, openDeleteModal, redownload } = useDownloadStore(); const { isSidebarVisible, toggleSidebar } = useSettingsStore(); const isMac = navigator.userAgent.includes('Mac'); @@ -104,12 +104,8 @@ export const DownloadTable: React.FC = ({ filter }) => { useDownloadStore.getState().processQueue(); }; - const handleDelete = async (id: string) => { - try { - await removeDownload(id); - } catch (e) { - console.error("Failed to delete download:", e); - } + const handleDelete = (id: string) => { + openDeleteModal(id); }; const contextItem = contextMenu ? downloads.find(d => d.id === contextMenu.id) : null; @@ -168,15 +164,6 @@ export const DownloadTable: React.FC = ({ filter }) => { > - - @@ -428,7 +415,7 @@ export const DownloadTable: React.FC = ({ filter }) => { }} className="w-full text-left px-3 py-2 text-red-400 hover:bg-red-500/10 transition-colors" > - Remove from List + Remove
diff --git a/src/store/useDownloadStore.ts b/src/store/useDownloadStore.ts index f93626a..83b1798 100644 --- a/src/store/useDownloadStore.ts +++ b/src/store/useDownloadStore.ts @@ -59,6 +59,11 @@ export const MAIN_QUEUE_ID = '00000000-0000-0000-0000-000000000001'; export type { DownloadItem, Queue }; export type ExtensionDownloadRequest = ExtensionDownload; +export type DeleteModalState = { + isOpen: boolean; + downloadId?: string; +}; + interface DownloadState { downloads: DownloadItem[]; queues: Queue[]; @@ -70,11 +75,13 @@ interface DownloadState { toggleAddModal: (isOpen: boolean) => void; openAddModalWithUrls: (urls: string, referer?: string | null, filename?: string | null) => void; handleExtensionDownload: (request: ExtensionDownloadRequest) => void; + deleteModalState: DeleteModalState; + openDeleteModal: (downloadId?: string) => void; + closeDeleteModal: () => void; setSelectedPropertiesDownloadId: (id: string | null) => void; addDownload: (item: DownloadItem) => void; updateDownload: (id: string, updates: Partial) => void; - removeDownload: (id: string) => Promise; - clearFinished: () => void; + removeDownload: (id: string, deleteFile?: boolean) => Promise; redownload: (id: string) => void; processQueue: () => Promise; startQueue: (queueId: string) => Promise; @@ -93,6 +100,9 @@ export const useDownloadStore = create((set, get) => ({ pendingAddReferer: '', pendingAddFilename: '', selectedPropertiesDownloadId: null, + deleteModalState: { isOpen: false }, + openDeleteModal: (downloadId) => set({ deleteModalState: { isOpen: true, downloadId } }), + closeDeleteModal: () => set({ deleteModalState: { isOpen: false } }), toggleAddModal: (isOpen) => set({ isAddModalOpen: isOpen, pendingAddUrls: '', @@ -113,44 +123,11 @@ export const useDownloadStore = create((set, get) => ({ const urls = [...new Set(request.urls.map(url => url.trim()).filter(Boolean))]; if (urls.length === 0) return; - const settings = useSettingsStore.getState(); - if (!request.silent || settings.askWhereToSaveEachFile) { - get().openAddModalWithUrls( - urls.join('\n'), - request.referer, - urls.length === 1 ? request.filename : null - ); - return; - } - - const referer = request.referer?.trim(); - const headers = referer ? `Referer: ${referer}` : undefined; - const dateAdded = new Date().toISOString(); - const downloads = urls.map((url, index): DownloadItem => { - const fileName = index === 0 && urls.length === 1 && request.filename?.trim() - ? request.filename.trim() - : fileNameFromUrl(url); - return { - id: crypto.randomUUID(), - url, - fileName, - status: 'queued', - category: categoryForFileName(fileName), - dateAdded, - connections: settings.perServerConnections, - headers, - isMedia: isMediaUrl(url), - queueId: MAIN_QUEUE_ID - }; - }); - - set(state => ({ downloads: [...state.downloads, ...downloads] })); - downloads.forEach(item => { - const toSave = { ...item }; - delete toSave.fraction; delete toSave.speed; delete toSave.eta; - invoke('db_save_download', { id: item.id, status: item.status, queueId: item.queueId, data: JSON.stringify(toSave) }).catch(console.error); - }); - void get().processQueue(); + get().openAddModalWithUrls( + urls.join('\n'), + request.referer, + urls.length === 1 ? request.filename : null + ); }, setSelectedPropertiesDownloadId: (id) => set({ selectedPropertiesDownloadId: id }), addDownload: (item) => { @@ -191,7 +168,7 @@ export const useDownloadStore = create((set, get) => ({ syncSystemIntegrations(); } }, - removeDownload: async (id) => { + removeDownload: async (id, deleteFile = false) => { const item = get().downloads.find(d => d.id === id); if (item && item.status === 'downloading') { try { @@ -199,6 +176,12 @@ export const useDownloadStore = create((set, get) => ({ } catch (e) { console.error("Failed to terminate download on deletion:", e); } + } else if (item && deleteFile) { + try { + await invoke('remove_download', { id, filepath: item.destination || null }); + } catch (e) { + console.error("Failed to delete file from disk:", e); + } } set((state) => ({ downloads: state.downloads.filter(d => d.id !== id) @@ -207,16 +190,6 @@ export const useDownloadStore = create((set, get) => ({ get().processQueue(); syncSystemIntegrations(); }, - clearFinished: () => { - const downloads = get().downloads; - const toRemove = downloads.filter(d => ['completed', 'failed'].includes(d.status)); - set((state) => ({ - downloads: state.downloads.filter(d => !['completed', 'failed'].includes(d.status)) - })); - toRemove.forEach(d => { - invoke('db_delete_download', { id: d.id }).catch(console.error); - }); - }, redownload: (id) => { let updatedItem: DownloadItem | null = null; set((state) => ({