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 ( +
+ {t($ => $.app.loading)} + + ); +}; + let automaticUpdateCheckStarted = false; const processingScheduleKeys = new Set(); @@ -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: '系统操作已取消,因为有其他下载正在进行或已排队。',