mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-27 19:17:13 +00:00
fix(ui): harden lazy page preloading
This commit is contained in:
+62
-5
@@ -27,10 +27,21 @@ import { isTrustedFirelinkReleaseUrl } from './utils/releaseUrls';
|
|||||||
import { changeAppLocale, localeDirection, resolveAppLocale, syncDocumentLocale } from './i18n';
|
import { changeAppLocale, localeDirection, resolveAppLocale, syncDocumentLocale } from './i18n';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const SettingsView = lazy(() => import('./components/SettingsView'));
|
const loadSettingsView = () => import('./components/SettingsView');
|
||||||
const SchedulerView = lazy(() => import('./components/SchedulerView'));
|
const loadSchedulerView = () => import('./components/SchedulerView');
|
||||||
const SpeedLimiterView = lazy(() => import('./components/SpeedLimiterView'));
|
const loadSpeedLimiterView = () => import('./components/SpeedLimiterView');
|
||||||
const LogsView = lazy(() => import('./components/LogsView'));
|
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 => ({
|
const AddDownloadsModal = lazy(() => import('./components/AddDownloadsModal').then(module => ({
|
||||||
default: module.AddDownloadsModal,
|
default: module.AddDownloadsModal,
|
||||||
})));
|
})));
|
||||||
@@ -44,6 +55,50 @@ const KeychainPermissionModal = lazy(() => import('./components/KeychainPermissi
|
|||||||
default: module.KeychainPermissionModal,
|
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 (
|
||||||
|
<div className="flex flex-1 items-center justify-center bg-main-bg" role="status" aria-live="polite">
|
||||||
|
<span className="sr-only">{t($ => $.app.loading)}</span>
|
||||||
|
<div className="h-1.5 w-24 overflow-hidden rounded-full bg-item-hover" aria-hidden="true">
|
||||||
|
<div className="h-full w-1/3 animate-pulse rounded-full bg-accent" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
let automaticUpdateCheckStarted = false;
|
let automaticUpdateCheckStarted = false;
|
||||||
const processingScheduleKeys = new Set<string>();
|
const processingScheduleKeys = new Set<string>();
|
||||||
|
|
||||||
@@ -187,6 +242,8 @@ function App() {
|
|||||||
// briefly rendering underneath native or custom window controls.
|
// briefly rendering underneath native or custom window controls.
|
||||||
const hasWindowChrome = isMacUserAgent || ['macos', 'windows', 'linux', 'unknown'].includes(platform.os);
|
const hasWindowChrome = isMacUserAgent || ['macos', 'windows', 'linux', 'unknown'].includes(platform.os);
|
||||||
|
|
||||||
|
useEffect(() => scheduleAfterFirstPaint(preloadPageChunks), []);
|
||||||
|
|
||||||
useEffect(() => subscribeToSettingsPersistenceErrors(() => {
|
useEffect(() => subscribeToSettingsPersistenceErrors(() => {
|
||||||
addToast({
|
addToast({
|
||||||
message: t($ => $.app.settingsSaveFailed),
|
message: t($ => $.app.settingsSaveFailed),
|
||||||
@@ -1002,7 +1059,7 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 flex flex-col overflow-hidden relative">
|
<div className="flex-1 flex flex-col overflow-hidden relative">
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={<PageLoadingFallback />}>
|
||||||
{activeView === 'downloads' && <DownloadTable filter={filter} />}
|
{activeView === 'downloads' && <DownloadTable filter={filter} />}
|
||||||
{activeView === 'settings' && <SettingsView />}
|
{activeView === 'settings' && <SettingsView />}
|
||||||
{activeView === 'scheduler' && <SchedulerView />}
|
{activeView === 'scheduler' && <SchedulerView />}
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ const common = {
|
|||||||
storeUnavailable: 'Credential store access is unavailable.',
|
storeUnavailable: 'Credential store access is unavailable.',
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
loading: 'Loading…',
|
||||||
settingsSaveFailed: 'Could not save settings. Check storage permissions and try again.',
|
settingsSaveFailed: 'Could not save settings. Check storage permissions and try again.',
|
||||||
systemActionCountdown: '{{action}} in 10 seconds.',
|
systemActionCountdown: '{{action}} in 10 seconds.',
|
||||||
systemActionCancelled: 'System action cancelled because another download is active or queued.',
|
systemActionCancelled: 'System action cancelled because another download is active or queued.',
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ const fa = {
|
|||||||
storeUnavailable: 'دسترسی به مخزن اعتبارنامه در دسترس نیست.',
|
storeUnavailable: 'دسترسی به مخزن اعتبارنامه در دسترس نیست.',
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
loading: 'در حال بارگذاری…',
|
||||||
settingsSaveFailed: 'تنظیمات ذخیره نشدند. مجوزهای ذخیرهسازی را بررسی کنید و دوباره امتحان کنید.',
|
settingsSaveFailed: 'تنظیمات ذخیره نشدند. مجوزهای ذخیرهسازی را بررسی کنید و دوباره امتحان کنید.',
|
||||||
systemActionCountdown: '{{action}} در ۱۰ ثانیه.',
|
systemActionCountdown: '{{action}} در ۱۰ ثانیه.',
|
||||||
systemActionCancelled: 'اقدام سیستم لغو شد زیرا دانلود دیگری فعال یا در صف است.',
|
systemActionCancelled: 'اقدام سیستم لغو شد زیرا دانلود دیگری فعال یا در صف است.',
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ const he = {
|
|||||||
storeUnavailable: 'הגישה לאחסון האישורים אינה זמינה.',
|
storeUnavailable: 'הגישה לאחסון האישורים אינה זמינה.',
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
loading: 'טוען…',
|
||||||
settingsSaveFailed: 'לא ניתן לשמור הגדרות. בדוק הרשאות אחסון ונסה שוב.',
|
settingsSaveFailed: 'לא ניתן לשמור הגדרות. בדוק הרשאות אחסון ונסה שוב.',
|
||||||
systemActionCountdown: '{{action}} בעוד 10 שניות.',
|
systemActionCountdown: '{{action}} בעוד 10 שניות.',
|
||||||
systemActionCancelled: 'פעולת המערכת בוטלה מכיוון שהורדה אחרת פעילה או בתור.',
|
systemActionCancelled: 'פעולת המערכת בוטלה מכיוון שהורדה אחרת פעילה או בתור.',
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ const ru = {
|
|||||||
storeUnavailable: 'Доступ к хранилищу учетных данных недоступен.',
|
storeUnavailable: 'Доступ к хранилищу учетных данных недоступен.',
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
loading: 'Загрузка…',
|
||||||
settingsSaveFailed: 'Не удалось сохранить настройки. Проверьте права доступа к хранилищу и повторите попытку.',
|
settingsSaveFailed: 'Не удалось сохранить настройки. Проверьте права доступа к хранилищу и повторите попытку.',
|
||||||
systemActionCountdown: '{{action}} через 10 секунд.',
|
systemActionCountdown: '{{action}} через 10 секунд.',
|
||||||
systemActionCancelled: 'Системное действие отменено, так как другая загрузка активна или находится в очереди.',
|
systemActionCancelled: 'Системное действие отменено, так как другая загрузка активна или находится в очереди.',
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ const uk = {
|
|||||||
storeUnavailable: 'Доступ до сховища облікових даних недоступний.',
|
storeUnavailable: 'Доступ до сховища облікових даних недоступний.',
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
loading: 'Завантаження…',
|
||||||
settingsSaveFailed: 'Не вдалося зберегти налаштування. Перевірте дозволи сховища та спробуйте ще раз.',
|
settingsSaveFailed: 'Не вдалося зберегти налаштування. Перевірте дозволи сховища та спробуйте ще раз.',
|
||||||
systemActionCountdown: '{{action}} через 10 секунд.',
|
systemActionCountdown: '{{action}} через 10 секунд.',
|
||||||
systemActionCancelled: 'Системна дія скасована, оскільки активне або в черзі інше завантаження.',
|
systemActionCancelled: 'Системна дія скасована, оскільки активне або в черзі інше завантаження.',
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ const zhCN = {
|
|||||||
storeUnavailable: '凭据存储访问不可用。',
|
storeUnavailable: '凭据存储访问不可用。',
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
loading: '加载中…',
|
||||||
settingsSaveFailed: '无法保存设置。请检查存储权限并重试。',
|
settingsSaveFailed: '无法保存设置。请检查存储权限并重试。',
|
||||||
systemActionCountdown: '10 秒后{{action}}。',
|
systemActionCountdown: '10 秒后{{action}}。',
|
||||||
systemActionCancelled: '系统操作已取消,因为有其他下载正在进行或已排队。',
|
systemActionCancelled: '系统操作已取消,因为有其他下载正在进行或已排队。',
|
||||||
|
|||||||
Reference in New Issue
Block a user