diff --git a/src/App.tsx b/src/App.tsx
index 3063e5b..b641066 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -27,10 +27,21 @@ import { isTrustedFirelinkReleaseUrl } from './utils/releaseUrls';
import { changeAppLocale, localeDirection, resolveAppLocale, syncDocumentLocale } from './i18n';
import { useTranslation } from 'react-i18next';
-const SettingsView = lazy(() => import('./components/SettingsView'));
-const SchedulerView = lazy(() => import('./components/SchedulerView'));
-const SpeedLimiterView = lazy(() => import('./components/SpeedLimiterView'));
-const LogsView = lazy(() => import('./components/LogsView'));
+const loadSettingsView = () => import('./components/SettingsView');
+const loadSchedulerView = () => import('./components/SchedulerView');
+const loadSpeedLimiterView = () => import('./components/SpeedLimiterView');
+const loadLogsView = () => import('./components/LogsView');
+const pageChunkLoaders = [
+ loadSettingsView,
+ loadSchedulerView,
+ loadSpeedLimiterView,
+ loadLogsView,
+] as const;
+
+const SettingsView = lazy(loadSettingsView);
+const SchedulerView = lazy(loadSchedulerView);
+const SpeedLimiterView = lazy(loadSpeedLimiterView);
+const LogsView = lazy(loadLogsView);
const AddDownloadsModal = lazy(() => import('./components/AddDownloadsModal').then(module => ({
default: module.AddDownloadsModal,
})));
@@ -44,6 +55,50 @@ const KeychainPermissionModal = lazy(() => import('./components/KeychainPermissi
default: module.KeychainPermissionModal,
})));
+const preloadPageChunks = async () => {
+ for (const load of pageChunkLoaders) {
+ try {
+ await load();
+ } catch (error) {
+ console.warn('Failed to preload page chunk:', error);
+ }
+ }
+};
+
+const scheduleAfterFirstPaint = (task: () => void): (() => void) => {
+ const idleWindow = window as typeof window & {
+ requestIdleCallback?: (callback: () => void, options?: { timeout?: number }) => number;
+ cancelIdleCallback?: (handle: number) => void;
+ };
+
+ if (idleWindow.requestIdleCallback) {
+ const handle = idleWindow.requestIdleCallback(task, { timeout: 1000 });
+ return () => idleWindow.cancelIdleCallback?.(handle);
+ }
+
+ let timeoutHandle: number | null = null;
+ const frameHandle = window.requestAnimationFrame(() => {
+ timeoutHandle = window.setTimeout(task, 0);
+ });
+ return () => {
+ window.cancelAnimationFrame(frameHandle);
+ if (timeoutHandle !== null) window.clearTimeout(timeoutHandle);
+ };
+};
+
+const PageLoadingFallback = () => {
+ const { t } = useTranslation();
+
+ return (
+
();
@@ -187,6 +242,8 @@ function App() {
// briefly rendering underneath native or custom window controls.
const hasWindowChrome = isMacUserAgent || ['macos', 'windows', 'linux', 'unknown'].includes(platform.os);
+ useEffect(() => scheduleAfterFirstPaint(preloadPageChunks), []);
+
useEffect(() => subscribeToSettingsPersistenceErrors(() => {
addToast({
message: t($ => $.app.settingsSaveFailed),
@@ -1002,7 +1059,7 @@ function App() {
)}
-
+ }>
{activeView === 'downloads' && }
{activeView === 'settings' && }
{activeView === 'scheduler' && }
diff --git a/src/i18n/catalogs/en.ts b/src/i18n/catalogs/en.ts
index a1caaf1..e0ab363 100644
--- a/src/i18n/catalogs/en.ts
+++ b/src/i18n/catalogs/en.ts
@@ -288,6 +288,7 @@ const common = {
storeUnavailable: 'Credential store access is unavailable.',
},
app: {
+ loading: 'Loading…',
settingsSaveFailed: 'Could not save settings. Check storage permissions and try again.',
systemActionCountdown: '{{action}} in 10 seconds.',
systemActionCancelled: 'System action cancelled because another download is active or queued.',
diff --git a/src/i18n/catalogs/fa.ts b/src/i18n/catalogs/fa.ts
index e46a998..417db18 100644
--- a/src/i18n/catalogs/fa.ts
+++ b/src/i18n/catalogs/fa.ts
@@ -288,6 +288,7 @@ const fa = {
storeUnavailable: 'دسترسی به مخزن اعتبارنامه در دسترس نیست.',
},
app: {
+ loading: 'در حال بارگذاری…',
settingsSaveFailed: 'تنظیمات ذخیره نشدند. مجوزهای ذخیرهسازی را بررسی کنید و دوباره امتحان کنید.',
systemActionCountdown: '{{action}} در ۱۰ ثانیه.',
systemActionCancelled: 'اقدام سیستم لغو شد زیرا دانلود دیگری فعال یا در صف است.',
diff --git a/src/i18n/catalogs/he.ts b/src/i18n/catalogs/he.ts
index 5cc562d..001fa89 100644
--- a/src/i18n/catalogs/he.ts
+++ b/src/i18n/catalogs/he.ts
@@ -288,6 +288,7 @@ const he = {
storeUnavailable: 'הגישה לאחסון האישורים אינה זמינה.',
},
app: {
+ loading: 'טוען…',
settingsSaveFailed: 'לא ניתן לשמור הגדרות. בדוק הרשאות אחסון ונסה שוב.',
systemActionCountdown: '{{action}} בעוד 10 שניות.',
systemActionCancelled: 'פעולת המערכת בוטלה מכיוון שהורדה אחרת פעילה או בתור.',
diff --git a/src/i18n/catalogs/ru.ts b/src/i18n/catalogs/ru.ts
index 72220a8..6ccf454 100644
--- a/src/i18n/catalogs/ru.ts
+++ b/src/i18n/catalogs/ru.ts
@@ -288,6 +288,7 @@ const ru = {
storeUnavailable: 'Доступ к хранилищу учетных данных недоступен.',
},
app: {
+ loading: 'Загрузка…',
settingsSaveFailed: 'Не удалось сохранить настройки. Проверьте права доступа к хранилищу и повторите попытку.',
systemActionCountdown: '{{action}} через 10 секунд.',
systemActionCancelled: 'Системное действие отменено, так как другая загрузка активна или находится в очереди.',
diff --git a/src/i18n/catalogs/uk.ts b/src/i18n/catalogs/uk.ts
index 67850e5..91b3489 100644
--- a/src/i18n/catalogs/uk.ts
+++ b/src/i18n/catalogs/uk.ts
@@ -288,6 +288,7 @@ const uk = {
storeUnavailable: 'Доступ до сховища облікових даних недоступний.',
},
app: {
+ loading: 'Завантаження…',
settingsSaveFailed: 'Не вдалося зберегти налаштування. Перевірте дозволи сховища та спробуйте ще раз.',
systemActionCountdown: '{{action}} через 10 секунд.',
systemActionCancelled: 'Системна дія скасована, оскільки активне або в черзі інше завантаження.',
diff --git a/src/i18n/catalogs/zh-CN.ts b/src/i18n/catalogs/zh-CN.ts
index ad37807..ba0c692 100644
--- a/src/i18n/catalogs/zh-CN.ts
+++ b/src/i18n/catalogs/zh-CN.ts
@@ -288,6 +288,7 @@ const zhCN = {
storeUnavailable: '凭据存储访问不可用。',
},
app: {
+ loading: '加载中…',
settingsSaveFailed: '无法保存设置。请检查存储权限并重试。',
systemActionCountdown: '10 秒后{{action}}。',
systemActionCancelled: '系统操作已取消,因为有其他下载正在进行或已排队。',