fix: resolve main window UI and state sync bugs

- Fix "Date added" showing "Invalid date" on redownload by standardizing to ISO string format
- Update handleExtensionDownload to fallback to AddDownloadsModal on silent download dispatch failure
- Fix macOS sleep prevention by utilizing IOKit assertions (PreventSystemSleep and NetworkClientActive) to maintain network connection when display sleeps
- Improve resumeDownload to eagerly update UI state to "Queued" to fix progress bar freezing on resume
- Ensure robust error handling for removeDownload to maintain consistent frontend store state
This commit is contained in:
NimBold
2026-06-26 13:14:48 +03:30
parent d406cd327f
commit e1507a6c2c
2 changed files with 148 additions and 36 deletions
+95 -7
View File
@@ -1468,7 +1468,7 @@ pub struct AppState {
pub aria2_port: std::sync::Arc<std::sync::atomic::AtomicU16>,
pub aria2_secret: String,
pub media_semaphore: Arc<tokio::sync::Semaphore>,
pub sleep_preventer: Arc<Mutex<Option<keepawake::KeepAwake>>>,
pub sleep_preventer: Arc<Mutex<Option<SleepPreventer>>>,
pub scheduler_settings: Arc<RwLock<Option<crate::ipc::PersistedSettings>>>,
pub queue_manager: Arc<queue::QueueManager>,
}
@@ -3101,6 +3101,99 @@ fn get_platform_info() -> crate::ipc::PlatformInfo {
}
}
#[cfg(target_os = "macos")]
mod macos_sleep {
use std::ffi::c_void;
#[link(name = "IOKit", kind = "framework")]
#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
pub fn IOPMAssertionCreateWithDescription(
AssertionType: *const c_void,
Name: *const c_void,
Details: *const c_void,
HumanReadableReason: *const c_void,
LocalizationBundlePath: *const c_void,
Timeout: f64,
TimeoutAction: *const c_void,
AssertionID: *mut u32,
) -> i32;
pub fn IOPMAssertionRelease(AssertionID: u32) -> i32;
pub fn CFStringCreateWithCString(
alloc: *const c_void,
cStr: *const i8,
encoding: u32,
) -> *const c_void;
pub fn CFRelease(arg: *const c_void);
}
}
pub enum SleepPreventer {
#[cfg(target_os = "macos")]
Mac { system_sleep_id: u32, network_client_id: u32 },
#[cfg(not(target_os = "macos"))]
Other(keepawake::KeepAwake),
}
impl Drop for SleepPreventer {
fn drop(&mut self) {
#[cfg(target_os = "macos")]
if let SleepPreventer::Mac { system_sleep_id, network_client_id } = self {
unsafe {
macos_sleep::IOPMAssertionRelease(*system_sleep_id);
macos_sleep::IOPMAssertionRelease(*network_client_id);
}
}
}
}
#[cfg(target_os = "macos")]
fn create_sleep_preventer() -> Result<SleepPreventer, String> {
use std::ffi::CString;
use std::ptr::null;
unsafe {
let create_cf_string = |s: &str| -> *const std::ffi::c_void {
let cstr = CString::new(s).unwrap();
macos_sleep::CFStringCreateWithCString(null(), cstr.as_ptr(), 0x08000100)
};
let type_sys = create_cf_string("PreventSystemSleep");
let type_net = create_cf_string("NetworkClientActive");
let name = create_cf_string("Firelink active download");
let mut sys_id: u32 = 0;
let mut net_id: u32 = 0;
let res1 = macos_sleep::IOPMAssertionCreateWithDescription(
type_sys, name, null(), null(), null(), 0.0, null(), &mut sys_id
);
let res2 = macos_sleep::IOPMAssertionCreateWithDescription(
type_net, name, null(), null(), null(), 0.0, null(), &mut net_id
);
macos_sleep::CFRelease(type_sys);
macos_sleep::CFRelease(type_net);
macos_sleep::CFRelease(name);
if res1 == 0 && res2 == 0 {
Ok(SleepPreventer::Mac { system_sleep_id: sys_id, network_client_id: net_id })
} else {
if res1 == 0 { macos_sleep::IOPMAssertionRelease(sys_id); }
if res2 == 0 { macos_sleep::IOPMAssertionRelease(net_id); }
Err("Failed to create macOS sleep assertions".to_string())
}
}
}
#[cfg(not(target_os = "macos"))]
fn create_sleep_preventer() -> Result<SleepPreventer, String> {
keepawake::Builder::default()
.idle(true)
.reason("Firelink active download")
.create()
.map(SleepPreventer::Other)
.map_err(|error| format!("failed to prevent system sleep: {error}"))
}
#[tauri::command]
fn set_prevent_sleep(state: tauri::State<'_, AppState>, prevent: bool) -> Result<(), String> {
let mut current_preventer = state
@@ -3109,12 +3202,7 @@ fn set_prevent_sleep(state: tauri::State<'_, AppState>, prevent: bool) -> Result
.unwrap_or_else(|e| e.into_inner());
if prevent {
if current_preventer.is_none() {
let keepawake = keepawake::Builder::default()
.idle(true)
.reason("Firelink active download")
.create()
.map_err(|error| format!("failed to prevent system sleep: {error}"))?;
*current_preventer = Some(keepawake);
*current_preventer = Some(create_sleep_preventer()?);
}
} else {
*current_preventer = None;
+53 -29
View File
@@ -322,19 +322,40 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
if (urls.length === 0) return;
if (request.silent) {
let failedUrls: string[] = [];
for (const url of urls) {
const id = crypto.randomUUID();
const filename = canonicalizeDownloadFileName(
urls.length === 1 && request.filename ? request.filename : suggestedFileNameFromUrl(url)
);
await get().addDownload({
id: crypto.randomUUID(),
url,
fileName: filename,
category: categoryForFileName(filename),
dateAdded: new Date().toISOString(),
headers: request.headers?.trim() || undefined,
cookies: urls.length === 1 ? request.cookies?.trim() || undefined : undefined,
}, { type: 'start-now' });
try {
const added = await get().addDownload({
id,
url,
fileName: filename,
category: categoryForFileName(filename),
dateAdded: new Date().toISOString(),
headers: request.headers?.trim() || undefined,
cookies: urls.length === 1 ? request.cookies?.trim() || undefined : undefined,
}, { type: 'start-now' });
if (!added) {
await get().removeDownload(id);
failedUrls.push(url);
}
} catch (error) {
await get().removeDownload(id);
failedUrls.push(url);
}
}
if (failedUrls.length > 0) {
get().openAddModalWithUrls(
failedUrls.join('\n'),
request.referer,
failedUrls.length === 1 ? request.filename : null,
request.headers,
request.cookies
);
}
return;
}
@@ -446,7 +467,11 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
const item = get().downloads.find(d => d.id === id);
if (item) {
await invoke('remove_download', { id, deleteAssets: deleteFile });
try {
await invoke('remove_download', { id, deleteAssets: deleteFile });
} catch (e) {
console.warn(`Failed to remove download ${id} from backend`, e);
}
}
set((state) => ({
@@ -486,7 +511,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
speed: '-',
eta: '-',
hasBeenDispatched: false,
dateAdded: Date.now().toString()
dateAdded: new Date().toISOString()
});
if (!await dispatchItem(id)) {
@@ -503,36 +528,35 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
try {
if (targetItem.status === 'ready' || targetItem.status === 'staged') {
get().updateDownload(id, { status: 'queued', hasBeenDispatched: true });
if (await dispatchItem(id)) {
get().updateDownload(id, { hasBeenDispatched: true });
return true;
}
get().updateDownload(id, { status: targetItem.status });
return false;
}
const resumedExisting = await invoke('resume_download', { id });
if (resumedExisting) {
const prevStatus = targetItem.status;
get().updateDownload(id, { status: 'queued', speed: '-', eta: '-' });
const resumedExisting = await invoke('resume_download', { id }).catch(() => false);
let dispatchSucceeded = resumedExisting;
if (!dispatchSucceeded) {
get().unregisterBackendIds([id]);
dispatchSucceeded = await dispatchItem(id);
}
if (dispatchSucceeded) {
return true;
}
get().unregisterBackendIds([id]);
set((state) => ({
downloads: state.downloads.map(d => {
if (d.id === id) {
return { ...d, status: 'queued', speed: '-', eta: '-' };
}
return d;
})
}));
if (!await dispatchItem(id)) {
} else {
console.error("Failed to re-enqueue for resume");
get().updateDownload(id, { status: prevStatus });
return false;
}
return true;
} catch (e) {
console.error("Failed to resume download:", e);
get().updateDownload(id, { status: targetItem.status });
return false;
}
},