Files
suwayomi-material-you-webui/src/App.tsx

315 lines
15 KiB
TypeScript
Raw Normal View History

/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
2021-01-26 23:32:12 +03:30
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
2021-01-26 23:32:12 +03:30
2021-09-09 17:51:22 +04:30
import CssBaseline from '@mui/material/CssBaseline';
import React, { useEffect, useLayoutEffect } from 'react';
import { Navigate, Outlet, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
2023-08-27 21:39:47 +02:00
import { loadErrorMessages, loadDevMessages } from '@apollo/client/dev';
2024-04-29 16:35:08 +02:00
import { loadable } from 'react-lazily/loadable';
2024-07-11 17:22:52 +02:00
import Box from '@mui/material/Box';
2025-11-02 17:18:49 +01:00
import { AwaitableComponent } from 'awaitable-component';
import { AppContext } from '@/base/contexts/AppContext.tsx';
2025-08-15 22:02:58 +02:00
import { DefaultNavBar } from '@/features/navigation-bar/components/DefaultNavBar.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
2025-08-15 22:02:58 +02:00
import { WebUIUpdateChecker } from '@/features/app-updates/components/WebUIUpdateChecker.tsx';
import { ServerUpdateChecker } from '@/features/app-updates/components/ServerUpdateChecker.tsx';
import { lazyLoadFallback } from '@/base/utils/LazyLoad.tsx';
import { ErrorBoundary } from '@/base/components/feedback/ErrorBoundary.tsx';
import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
2025-08-15 22:02:58 +02:00
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
2025-08-15 22:02:58 +02:00
import { BrowseTab } from '@/features/browse/Browse.types.ts';
2025-07-27 00:03:51 +02:00
import { LoginPage } from '@/features/authentication/screens/LoginPage.tsx';
import { AuthGuard } from '@/features/authentication/components/AuthGuard.tsx';
import { SearchParam } from '@/base/Base.types.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { ReactRouter } from '@/lib/react-router/ReactRouter.ts';
import { AuthManager } from '@/features/authentication/AuthManager.ts';
2024-04-29 16:35:08 +02:00
2025-08-15 22:02:58 +02:00
const { Browse } = loadable(() => import('@/features/browse/screens/Browse.tsx'), lazyLoadFallback);
const { DownloadQueue } = loadable(() => import('@/features/downloads/screens/DownloadQueue.tsx'), lazyLoadFallback);
const { Library } = loadable(() => import('@/features/library/screens/Library.tsx'), lazyLoadFallback);
const { Manga } = loadable(() => import('@/features/manga/screens/Manga.tsx'), lazyLoadFallback);
const { SearchAll } = loadable(() => import('@/features/global-search/screens/SearchAll.tsx'), lazyLoadFallback);
const { Settings } = loadable(() => import('@/features/settings/screens/Settings.tsx'), lazyLoadFallback);
const { About } = loadable(() => import('@/features/settings/screens/About.tsx'), lazyLoadFallback);
const { Backup } = loadable(() => import('@/features/backup/screens/Backup.tsx'), lazyLoadFallback);
2024-10-05 21:39:58 +02:00
const { CategorySettings } = loadable(
2025-08-15 22:02:58 +02:00
() => import('@/features/category/screens/CategorySettings.tsx'),
2024-10-05 21:39:58 +02:00
lazyLoadFallback,
);
2025-08-16 15:48:07 +02:00
const { SourceConfigure } = loadable(
() => import('@/features/source/configuration/screens/SourceConfigure.tsx'),
lazyLoadFallback,
);
const { SourceMangas } = loadable(() => import('@/features/source/browse/screens/SourceMangas.tsx'), lazyLoadFallback);
2025-08-16 16:03:32 +02:00
const { ExtensionInfo } = loadable(
() => import('@/features/extension/info/screens/ExtensionInfo.tsx'),
lazyLoadFallback,
);
2025-08-15 22:02:58 +02:00
const { Updates } = loadable(() => import('@/features/updates/screens/Updates.tsx'), lazyLoadFallback);
const { History } = loadable(() => import('@/features/history/screens/History.tsx'), lazyLoadFallback);
const { LibrarySettings } = loadable(() => import('@/features/library/screens/LibrarySettings.tsx'), lazyLoadFallback);
2024-10-05 22:31:21 +02:00
const { DownloadSettings } = loadable(
2025-08-15 22:02:58 +02:00
() => import('@/features/downloads/screens/DownloadSettings.tsx'),
lazyLoadFallback,
);
const { ServerSettings } = loadable(() => import('@/features/settings/screens/ServerSettings.tsx'), lazyLoadFallback);
const { BrowseSettings } = loadable(() => import('@/features/browse/screens/BrowseSettings.tsx'), lazyLoadFallback);
const { WebUISettings } = loadable(() => import('@/features/settings/screens/WebUISettings.tsx'), lazyLoadFallback);
const { Migrate } = loadable(() => import('@/features/migration/screens/Migrate.tsx'), lazyLoadFallback);
const { DeviceSetting } = loadable(() => import('@/features/device/screens/DeviceSetting.tsx'), lazyLoadFallback);
const { TrackingSettings } = loadable(
() => import('@/features/tracker/screens/TrackingSettings.tsx'),
2024-10-05 22:31:21 +02:00
lazyLoadFallback,
);
2024-10-05 21:32:31 +02:00
const { TrackerOAuthLogin } = loadable(
2025-08-15 22:02:58 +02:00
() => import('@/features/tracker/screens/TrackerOAuthLogin.tsx'),
2024-10-05 21:32:31 +02:00
lazyLoadFallback,
);
2024-10-05 19:33:09 +02:00
const { LibraryDuplicates } = loadable(
2025-08-15 22:02:58 +02:00
() => import('@/features/library/screens/LibraryDuplicates.tsx'),
2024-10-05 19:33:09 +02:00
lazyLoadFallback,
);
2025-08-15 22:02:58 +02:00
const { Appearance } = loadable(() => import('@/features/settings/screens/Appearance.tsx'), lazyLoadFallback);
const { GlobalReaderSettings } = loadable(
2025-08-16 02:02:13 +02:00
() => import('@/features/reader/settings/screens/GlobalReaderSettings.tsx'),
2024-11-04 18:00:35 +01:00
lazyLoadFallback,
);
2025-08-15 22:02:58 +02:00
const { More } = loadable(() => import('@/features/settings/screens/More.tsx'), lazyLoadFallback);
const { Reader } = loadable(() => import('@/features/reader/screens/Reader.tsx'), lazyLoadFallback);
const { HistorySettings } = loadable(() => import('@/features/history/screens/HistorySettings.tsx'), lazyLoadFallback);
2021-09-09 17:51:22 +04:30
2025-07-27 00:03:51 +02:00
if (import.meta.env.DEV) {
2023-08-27 21:39:47 +02:00
// Adds messages only in a dev environment
loadDevMessages();
loadErrorMessages();
}
const ScrollToTop = () => {
const { pathname } = useLocation();
useLayoutEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
};
const InitialBackgroundRequests = () => {
2025-10-16 21:56:54 +02:00
// Load the full download status once on startup to fill the cache
requestManager.useGetDownloadStatus({ nextFetchPolicy: 'standby' });
const [fetchExtensionList] = requestManager.useExtensionListFetch();
useEffect(() => {
// Fetch extension list on startup to show up-to-date number of available extension updates in the navigation bar
// without having to open the extensions page.
fetchExtensionList().catch(defaultPromiseErrorHandler('App::InitialBackgroundRequests: extension list'));
}, []);
return null;
};
/**
* Creates permanent subscriptions to always have the latest data.
*
* E.g. in case a view is open, which does not subscribe to the download updates, finished downloads are never received
* and thus, data of existing chapters/mangas in the cache get outdated
*/
const BackgroundSubscriptions = () => {
const { isAuthRequired, accessToken } = AuthManager.useSession();
const skipConnection = isAuthRequired === null || (isAuthRequired && !accessToken);
2025-07-27 00:03:51 +02:00
requestManager.useDownloadSubscription({ skip: skipConnection });
requestManager.useUpdaterSubscription({ skip: skipConnection });
requestManager.useWebUIUpdateSubscription({ skip: skipConnection });
return null;
};
const ReactRouterSetter = () => {
const navigate = useNavigate();
useEffect(() => {
ReactRouter.setNavigateFn(navigate);
}, []);
return null;
};
2025-07-27 00:03:51 +02:00
const PrivateRoutes = () => {
const isAuthenticated = AuthManager.useIsAuthenticated();
2025-07-27 00:03:51 +02:00
if (!isAuthenticated) {
return (
<Navigate
to={{
pathname: AppRoutes.authentication.childRoutes.login.path,
search: `${SearchParam.REDIRECT}=${window.location.pathname}`,
}}
replace
/>
);
}
return <Outlet />;
};
2024-07-11 17:22:52 +02:00
const MainApp = () => {
2024-08-30 04:34:28 +02:00
const { navBarWidth, appBarHeight, bottomBarHeight } = useNavBarContext();
2025-04-05 15:23:39 +02:00
const isMobileWidth = MediaQuery.useIsMobileWidth();
2024-07-11 17:22:52 +02:00
2025-03-22 01:16:38 +01:00
const {
settings: { hideHistory },
} = useMetadataServerSettings();
2024-07-11 17:22:52 +02:00
return (
<Box
id="appMainContainer"
2024-07-11 17:22:52 +02:00
component="main"
sx={{
2024-08-30 04:34:28 +02:00
minHeight: `calc(100vh - ${appBarHeight + bottomBarHeight}px)`,
width: `calc(100vw - (100vw - 100%) - ${navBarWidth}px)`,
minWidth: `calc(100vw - (100vw - 100%) - ${navBarWidth}px)`,
maxWidth: `calc(100vw - (100vw - 100%) - ${navBarWidth}px)`,
2024-07-11 17:22:52 +02:00
position: 'relative',
2024-08-30 04:34:28 +02:00
mt: `${appBarHeight}px`,
pb: `calc(${bottomBarHeight}px + ${!bottomBarHeight ? 'env(safe-area-inset-bottom)' : '0px'})`,
pr: 'env(safe-area-inset-right)',
}}
>
2024-04-29 16:35:08 +02:00
<ErrorBoundary>
<Routes>
2025-07-27 00:03:51 +02:00
<Route path={AppRoutes.authentication.match}>
<Route path={AppRoutes.authentication.childRoutes.login.match} element={<LoginPage />} />
</Route>
<Route element={<PrivateRoutes />}>
{/* General Routes */}
<Route
path={AppRoutes.root.match}
element={<Navigate to={AppRoutes.library.path()} replace />}
/>
<Route
path={AppRoutes.matchAll.match}
element={<Navigate to={AppRoutes.root.path} replace />}
/>
{isMobileWidth && <Route path={AppRoutes.more.match} element={<More />} />}
<Route path={AppRoutes.about.match} element={<About />} />
<Route path={AppRoutes.settings.match}>
<Route index element={<Settings />} />
<Route
path={AppRoutes.settings.childRoutes.categories.match}
element={<CategorySettings />}
/>
<Route
path={AppRoutes.settings.childRoutes.reader.match}
element={<GlobalReaderSettings />}
/>
<Route path={AppRoutes.settings.childRoutes.library.match}>
<Route index element={<LibrarySettings />} />
<Route
path={AppRoutes.settings.childRoutes.library.childRoutes.duplicates.match}
element={<LibraryDuplicates />}
/>
</Route>
<Route
path={AppRoutes.settings.childRoutes.download.match}
element={<DownloadSettings />}
/>
<Route path={AppRoutes.settings.childRoutes.backup.match} element={<Backup />} />
<Route path={AppRoutes.settings.childRoutes.server.match} element={<ServerSettings />} />
<Route path={AppRoutes.settings.childRoutes.webui.match} element={<WebUISettings />} />
<Route path={AppRoutes.settings.childRoutes.browse.match} element={<BrowseSettings />} />
<Route path={AppRoutes.settings.childRoutes.history.match} element={<HistorySettings />} />
<Route path={AppRoutes.settings.childRoutes.device.match} element={<DeviceSetting />} />
2024-12-11 20:10:59 +01:00
<Route
2025-07-27 00:03:51 +02:00
path={AppRoutes.settings.childRoutes.tracking.match}
element={<TrackingSettings />}
2024-12-11 20:10:59 +01:00
/>
2025-07-27 00:03:51 +02:00
<Route path={AppRoutes.settings.childRoutes.appearance.match} element={<Appearance />} />
</Route>
2021-03-18 21:46:24 +03:30
2025-07-27 00:03:51 +02:00
{/* Manga Routes */}
2021-01-23 01:20:16 +03:30
2025-07-27 00:03:51 +02:00
<Route path={AppRoutes.sources.match}>
{/* TODO: deprecated - "source" and "extension" page got merged into "browse" */}
<Route index element={<Navigate to={AppRoutes.browse.path(BrowseTab.SOURCES)} replace />} />
<Route path={AppRoutes.sources.childRoutes.browse.match} element={<SourceMangas />} />
<Route path={AppRoutes.sources.childRoutes.configure.match} element={<SourceConfigure />} />
<Route path={AppRoutes.sources.childRoutes.searchAll.match} element={<SearchAll />} />
</Route>
<Route path={AppRoutes.extension.match}>
<Route
index
element={<Navigate to={AppRoutes.browse.path(BrowseTab.EXTENSIONS)} replace />}
/>
<Route path={AppRoutes.extension.childRoutes.info.match} element={<ExtensionInfo />} />
</Route>
<Route path={AppRoutes.downloads.match} element={<DownloadQueue />} />
<Route path={AppRoutes.manga.match}>
<Route path={AppRoutes.manga.childRoutes.reader.match} element={null} />
<Route index element={<Manga />} />
</Route>
<Route path={AppRoutes.library.match} element={<Library />} />
<Route path={AppRoutes.updates.match} element={<Updates />} />
{!hideHistory && <Route path={AppRoutes.history.match} element={<History />} />}
<Route path={AppRoutes.browse.match} element={<Browse />} />
<Route path={AppRoutes.migrate.match}>
<Route index element={<Migrate />} />
<Route path={AppRoutes.migrate.childRoutes.search.match} element={<SearchAll />} />
</Route>
<Route path={AppRoutes.tracker.match} element={<TrackerOAuthLogin />} />
2024-04-29 16:35:08 +02:00
</Route>
</Routes>
</ErrorBoundary>
2024-07-11 17:22:52 +02:00
</Box>
);
};
const ReaderApp = () => (
<ErrorBoundary>
<Routes>
2025-09-03 00:13:41 +02:00
<Route element={<PrivateRoutes />}>
<Route path={AppRoutes.matchAll.match} element={<Reader />} />
</Route>
2024-07-11 17:22:52 +02:00
</Routes>
</ErrorBoundary>
);
export const App: React.FC = () => (
<AppContext>
<ScrollToTop />
2025-11-02 17:18:49 +01:00
<AwaitableComponent.Root />
<AuthGuard>
<ServerUpdateChecker />
<WebUIUpdateChecker />
<InitialBackgroundRequests />
<BackgroundSubscriptions />
<ReactRouterSetter />
<CssBaseline enableColorScheme />
2025-07-27 00:03:51 +02:00
<Box sx={{ display: 'flex' }}>
<Box sx={{ flexShrink: 0, position: 'relative', height: '100vh' }}>
<DefaultNavBar />
</Box>
<Routes>
<Route path={AppRoutes.matchAll.match} element={<MainApp />} />
<Route path={AppRoutes.reader.match} element={<ReaderApp />} />
</Routes>
2024-07-11 17:22:52 +02:00
</Box>
2025-07-27 00:03:51 +02:00
</AuthGuard>
</AppContext>
);