mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
fix: app nap ipc drop and implement delete modal
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -367,6 +367,7 @@ pub struct DownloadProgressEvent {
|
||||
fraction: f64,
|
||||
speed: String,
|
||||
eta: String,
|
||||
size: Option<String>,
|
||||
}
|
||||
|
||||
fn collect_download_uris(url: &str, mirrors: Option<&str>) -> Vec<String> {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+11
-5
@@ -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() {
|
||||
|
||||
<AddDownloadsModal />
|
||||
<PropertiesModal />
|
||||
<DeleteConfirmationModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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, };
|
||||
|
||||
@@ -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 (
|
||||
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 animate-fade-in">
|
||||
<div
|
||||
className="bg-bg-modal rounded-xl w-full max-w-md overflow-hidden flex flex-col shadow-2xl border border-border-modal scale-in"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-5 py-4 border-b border-border-modal flex items-center gap-3">
|
||||
<div className="p-2 bg-red-500/10 rounded-full flex items-center justify-center">
|
||||
<AlertTriangle size={20} className="text-red-400" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-text-primary m-0">
|
||||
Remove Download
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-5 py-6 flex-1 text-sm text-text-secondary leading-relaxed">
|
||||
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.
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-5 py-4 border-t border-border-modal flex justify-end gap-3 bg-bg-modal-accent">
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors text-text-secondary hover:bg-item-hover hover:text-text-primary"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRemoveFromList}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors bg-border-modal hover:bg-border-modal/80 text-text-primary"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteFile}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors bg-red-500/20 text-red-400 hover:bg-red-500/30"
|
||||
>
|
||||
Delete file
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -11,7 +11,7 @@ interface DownloadTableProps {
|
||||
}
|
||||
|
||||
export const DownloadTable: React.FC<DownloadTableProps> = ({ 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<DownloadTableProps> = ({ 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<DownloadTableProps> = ({ filter }) => {
|
||||
>
|
||||
<Pause size={15} fill="currentColor" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="main-control-button hover:!text-red-400"
|
||||
disabled={filteredDownloads.length === 0}
|
||||
onClick={clearFinished}
|
||||
title="Clear Finished"
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -428,7 +415,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 text-red-400 hover:bg-red-500/10 transition-colors"
|
||||
>
|
||||
Remove from List
|
||||
Remove
|
||||
</button>
|
||||
|
||||
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
|
||||
|
||||
@@ -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<DownloadItem>) => void;
|
||||
removeDownload: (id: string) => Promise<void>;
|
||||
clearFinished: () => void;
|
||||
removeDownload: (id: string, deleteFile?: boolean) => Promise<void>;
|
||||
redownload: (id: string) => void;
|
||||
processQueue: () => Promise<void>;
|
||||
startQueue: (queueId: string) => Promise<number>;
|
||||
@@ -93,6 +100,9 @@ export const useDownloadStore = create<DownloadState>((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<DownloadState>((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<DownloadState>((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<DownloadState>((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<DownloadState>((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) => ({
|
||||
|
||||
Reference in New Issue
Block a user