fix(downloads): harden power management and aria2 recovery

This commit is contained in:
NimBold
2026-07-26 03:04:50 +03:30
parent 931e1f2f1e
commit cf09bfa2e4
17 changed files with 1197 additions and 130 deletions
+32 -11
View File
@@ -101,6 +101,7 @@ const PageLoadingFallback = () => {
let automaticUpdateCheckStarted = false;
const processingScheduleKeys = new Set<string>();
let powerPreferencesSync: Promise<void> = Promise.resolve();
const waitForSettingsHydration = (): Promise<void> => {
if (useSettingsStore.persist.hasHydrated()) return Promise.resolve();
@@ -245,6 +246,9 @@ function App() {
>>([]);
const maxConcurrentDownloads = useSettingsStore(state => state.maxConcurrentDownloads);
const preventsSleepWhileDownloading = useSettingsStore(state => state.preventsSleepWhileDownloading);
const preventsDisplaySleepWhileDownloading = useSettingsStore(
state => state.preventsDisplaySleepWhileDownloading
);
const activeTransferCount = downloads.filter(download => isTransferActiveStatus(download.status)).length;
const { addToast, removeToast } = useToast();
const isMacUserAgent = navigator.userAgent.includes('Mac');
@@ -716,17 +720,34 @@ function App() {
}, [platform.os, showDockBadge, dockBadgeSyncVersion, activeDownloadCount]);
useEffect(() => {
invoke('set_prevent_sleep', {
prevent: preventsSleepWhileDownloading && activeTransferCount > 0
}).catch(error => {
console.error('Failed to update sleep prevention:', error);
addToast({
message: t($ => $.app.sleepPreventionFailed, { detail: String(error) }),
variant: 'error',
isActionable: true
});
});
}, [addToast, preventsSleepWhileDownloading, activeTransferCount]);
const sync = () => {
powerPreferencesSync = powerPreferencesSync
.catch(() => undefined)
.then(() => invoke('set_power_preferences', {
preventSystemSleep: preventsSleepWhileDownloading,
preventDisplaySleep: preventsDisplaySleepWhileDownloading
}))
.catch(error => {
console.error('Failed to update power prevention:', error);
addToast({
message: t($ => $.app.sleepPreventionFailed, { detail: String(error) }),
variant: 'error',
isActionable: true
});
});
};
if (useSettingsStore.persist.hasHydrated()) {
sync();
return;
}
return useSettingsStore.persist.onFinishHydration(sync);
}, [
addToast,
preventsDisplaySleepWhileDownloading,
preventsSleepWhileDownloading,
t
]);
useEffect(() => {
invoke('toggle_tray_icon', { show: showMenuBarIcon }).catch(console.error);
+1 -1
View File
@@ -11,4 +11,4 @@ import type { SiteLogin } from "./SiteLogin";
import type { Theme } from "./Theme";
import type { WindowControlStyle } from "./WindowControlStyle";
export type PersistedSettings = { theme: Theme, fontFamily: FontFamily, windowControlStyle: WindowControlStyle, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
export type PersistedSettings = { theme: Theme, fontFamily: FontFamily, windowControlStyle: WindowControlStyle, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, preventsDisplaySleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
+13
View File
@@ -1475,6 +1475,19 @@ runEngineChecks(false);
<p className="text-text-muted text-xs mt-0.5">{t($ => $.settings.power.preventSleepDescription)}</p>
</div>
</label>
<label className="flex items-start gap-3 cursor-default select-none text-[13px] text-text-secondary">
<input
type="checkbox"
checked={settings.preventsDisplaySleepWhileDownloading}
onChange={(e) => settings.setPreventsDisplaySleepWhileDownloading(e.target.checked)}
className="mt-0.5 rounded accent-accent"
/>
<div>
<p className="font-semibold text-text-primary">{t($ => $.settings.power.preventDisplaySleep)}</p>
<p className="text-text-muted text-xs mt-0.5">{t($ => $.settings.power.preventDisplaySleepDescription)}</p>
</div>
</label>
</div>
)}
+2
View File
@@ -738,6 +738,8 @@ const common = {
title: 'Power Management',
preventSleep: 'Prevent system sleep while downloads are active',
preventSleepDescription: 'The display may still turn off. Firelink only keeps the device awake enough to complete active transfers.',
preventDisplaySleep: 'Keep the display awake while downloads are active',
preventDisplaySleepDescription: 'Optional and separate from system-sleep prevention. The display will remain on until active transfers finish.',
},
engine: {
title: 'Media Downloader & Engines',
+2
View File
@@ -738,6 +738,8 @@ const fa = {
title: 'مدیریت انرژی',
preventSleep: 'جلوگیری از خواب سیستم در حالی که دانلودها فعال هستند',
preventSleepDescription: 'نمایشگر همچنان می‌تواند خاموش شود. Firelink تنها به اندازه‌ای دستگاه را بیدار نگه می‌دارد تا انتقال‌های فعال را تکمیل کند.',
preventDisplaySleep: 'روشن نگه داشتن نمایشگر هنگام فعال بودن دانلودها',
preventDisplaySleepDescription: 'این گزینه جدا از جلوگیری از خواب سیستم است و اختیاری است. نمایشگر تا پایان انتقال‌های فعال روشن می‌ماند.',
},
engine: {
title: 'دانلودکننده رسانه و موتورها',
+2
View File
@@ -738,6 +738,8 @@ const he = {
title: 'ניהול צריכת חשמל',
preventSleep: 'מנע שינה של המערכת בזמן שהורדות פעילות',
preventSleepDescription: 'התצוגה עדיין עשויה להכבות. Firelink משאיר את המכשיר ער רק במידה המספקת להשלמת העברות פעילות.',
preventDisplaySleep: 'השאר את התצוגה פעילה בזמן שהורדות פעילות',
preventDisplaySleepDescription: 'אפשרות זו נפרדת ממניעת שינת המערכת. התצוגה תישאר פעילה עד לסיום ההעברות הפעילות.',
},
engine: {
title: 'מוריד מדיה ומנועים',
+2
View File
@@ -738,6 +738,8 @@ const ru = {
title: 'Управление питанием',
preventSleep: 'Запрещать спящий режим системы во время активных загрузок',
preventSleepDescription: 'Экран всё ещё может выключаться. Firelink предотвращает засыпание устройства только настолько, чтобы завершить активные передачи.',
preventDisplaySleep: 'Не выключать экран во время активных загрузок',
preventDisplaySleepDescription: 'Это отдельная необязательная настройка, не связанная с запретом сна системы. Экран останется включённым до завершения передач.',
},
engine: {
title: 'Загрузчик медиа и движки',
+2
View File
@@ -738,6 +738,8 @@ const uk = {
title: 'Керування живленням',
preventSleep: 'Запобігати переходу системи в сплячий режим під час активних завантажень',
preventSleepDescription: 'Дисплей може все одно вимкнутися. Firelink лише не дає пристрою заснути, щоб завершити активні передачі.',
preventDisplaySleep: 'Не вимикати дисплей під час активних завантажень',
preventDisplaySleepDescription: 'Це окреме необов’язкове налаштування, незалежне від запобігання сну системи. Дисплей залишатиметься ввімкненим до завершення передач.',
},
engine: {
title: 'Завантажувач медіа та рушії',
+2
View File
@@ -738,6 +738,8 @@ const zhCN = {
title: '电源管理',
preventSleep: '下载活动时阻止系统睡眠',
preventSleepDescription: '显示器可能仍会关闭。Firelink 仅保持设备唤醒足以完成活动的传输。',
preventDisplaySleep: '下载活动时保持显示器唤醒',
preventDisplaySleepDescription: '这是与阻止系统睡眠分开的可选设置。活动传输完成前,显示器将保持开启。',
},
engine: {
title: '媒体下载器和引擎',
+4
View File
@@ -46,6 +46,10 @@ type CommandMap = {
get_platform_info: { args: undefined; result: PlatformInfo };
approve_download_root: { args: { path: string }; result: string };
set_prevent_sleep: { args: { prevent: boolean }; result: void };
set_power_preferences: {
args: { preventSystemSleep: boolean; preventDisplaySleep: boolean };
result: void;
};
perform_system_action: { args: { action: PostQueueAction }; result: void };
ack_schedule_trigger: { args: { action: 'start' | 'stop'; key: string }; result: void };
set_concurrent_limit: { args: { limit: number }; result: void };
+12
View File
@@ -237,6 +237,7 @@ export interface SettingsState {
customUserAgent: string;
askWhereToSaveEachFile: boolean;
preventsSleepWhileDownloading: boolean;
preventsDisplaySleepWhileDownloading: boolean;
mediaCookieSource: MediaCookieSource;
siteLogins: SiteLogin[];
extensionPairingToken: string;
@@ -286,6 +287,7 @@ export interface SettingsState {
setCustomUserAgent: (userAgent: string) => void;
setAskWhereToSaveEachFile: (ask: boolean) => void;
setPreventsSleepWhileDownloading: (prevent: boolean) => void;
setPreventsDisplaySleepWhileDownloading: (prevent: boolean) => void;
setMediaCookieSource: (source: MediaCookieSource) => void;
setRememberLastUsedDownloadDirectory: (enabled: boolean) => void;
setLastUsedDownloadDirectory: (path: string) => void;
@@ -361,6 +363,7 @@ export const useSettingsStore = create<SettingsState>()(
customUserAgent: '',
askWhereToSaveEachFile: false,
preventsSleepWhileDownloading: true,
preventsDisplaySleepWhileDownloading: false,
mediaCookieSource: 'none',
siteLogins: [],
extensionPairingToken: '',
@@ -457,6 +460,10 @@ export const useSettingsStore = create<SettingsState>()(
info('Settings updated: preventsSleepWhileDownloading');
set({ preventsSleepWhileDownloading });
},
setPreventsDisplaySleepWhileDownloading: (preventsDisplaySleepWhileDownloading) => {
info('Settings updated: preventsDisplaySleepWhileDownloading');
set({ preventsDisplaySleepWhileDownloading });
},
setMediaCookieSource: (mediaCookieSource) => { info('Settings updated: mediaCookieSource'); set({ mediaCookieSource }); },
setRememberLastUsedDownloadDirectory: (rememberLastUsedDownloadDirectory) => {
info('Settings updated: rememberLastUsedDownloadDirectory');
@@ -631,6 +638,7 @@ export const useSettingsStore = create<SettingsState>()(
customUserAgent: state.customUserAgent,
askWhereToSaveEachFile: state.askWhereToSaveEachFile,
preventsSleepWhileDownloading: state.preventsSleepWhileDownloading,
preventsDisplaySleepWhileDownloading: state.preventsDisplaySleepWhileDownloading,
mediaCookieSource: state.mediaCookieSource,
siteLogins: state.siteLogins,
keychainAccessGranted: state.keychainAccessGranted,
@@ -704,6 +712,10 @@ export const useSettingsStore = create<SettingsState>()(
persisted.preventsSleepWhileDownloading,
currentState.preventsSleepWhileDownloading
),
preventsDisplaySleepWhileDownloading: persistedBoolean(
persisted.preventsDisplaySleepWhileDownloading,
currentState.preventsDisplaySleepWhileDownloading
),
keychainAccessGranted: persistedBoolean(
persisted.keychainAccessGranted,
currentState.keychainAccessGranted