p-stream/src/index.tsx
2026-01-16 19:59:40 -07:00

263 lines
8 KiB
TypeScript

import "@/setup/pwa";
import "core-js/stable";
import "./stores/__old/imports";
import "@/setup/ga";
import "@/assets/css/index.css";
import { StrictMode, Suspense, useCallback, useState } from "react";
import type { ReactNode } from "react";
import { createRoot } from "react-dom/client";
import { HelmetProvider } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { BrowserRouter, HashRouter } from "react-router-dom";
import { useAsync, useAsyncFn } from "react-use";
import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
import { Loading } from "@/components/layout/Loading";
import { useAuth } from "@/hooks/auth/useAuth";
import { useAuthRestore } from "@/hooks/auth/useAuthRestore";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { ErrorBoundary } from "@/pages/errors/ErrorBoundary";
import { MigrationPart } from "@/pages/parts/migrations/MigrationPart";
import { LargeTextPart } from "@/pages/parts/util/LargeTextPart";
import App from "@/setup/App";
import { conf } from "@/setup/config";
import { useAuthStore } from "@/stores/auth";
import { BookmarkSyncer } from "@/stores/bookmarks/BookmarkSyncer";
import { GroupSyncer } from "@/stores/groupOrder/GroupSyncer";
import { changeAppLanguage, useLanguageStore } from "@/stores/language";
import { ProgressSyncer } from "@/stores/progress/ProgressSyncer";
import { SettingsSyncer } from "@/stores/subtitles/SettingsSyncer";
import { ThemeProvider } from "@/stores/theme";
import { WatchHistorySyncer } from "@/stores/watchHistory/WatchHistorySyncer";
import { detectRegion, useRegionStore } from "@/utils/detectRegion";
import {
extensionInfo,
isExtensionActiveCached,
} from "./backend/extension/messaging";
import { initializeChromecast } from "./setup/chromecast";
import { initializeImageFadeIn } from "./setup/imageFadeIn";
import { initializeOldStores } from "./stores/__old/migrations";
// initialize
initializeChromecast();
initializeImageFadeIn();
function LoadingScreen(props: { type: "user" | "lazy" }) {
const mapping = {
user: "screens.loadingUser",
lazy: "screens.loadingApp",
};
const { t } = useTranslation();
return (
<LargeTextPart iconSlot={<Loading />}>
{t(mapping[props.type] ?? "unknown.translation")}
</LargeTextPart>
);
}
function ErrorScreen(props: {
children: ReactNode;
showResetButton?: boolean;
showLogoutButton?: boolean;
showReloadButton?: boolean;
showDisconnectButton?: boolean;
}) {
const { t } = useTranslation();
const { logout, disconnectFromBackend } = useAuth();
const setBackendUrl = useAuthStore((s) => s.setBackendUrl);
const [showDisconnectConfirm, setShowDisconnectConfirm] = useState(false);
const resetBackend = useCallback(() => {
setBackendUrl(null);
// eslint-disable-next-line no-restricted-globals
location.reload();
}, [setBackendUrl]);
const logoutFromBackend = useCallback(() => {
logout().then(() => {
// eslint-disable-next-line no-restricted-globals
location.reload();
});
}, [logout]);
const handleDisconnectConfirm = useCallback(() => {
disconnectFromBackend().then(() => {
// eslint-disable-next-line no-restricted-globals
location.reload();
});
}, [disconnectFromBackend]);
return (
<LargeTextPart
iconSlot={
<Icon className="text-type-danger text-2xl" icon={Icons.WARNING} />
}
>
{props.children}
{props.showResetButton ? (
<div className="mt-6">
<Button theme="secondary" onClick={resetBackend}>
{t("screens.loadingUserError.reset")}
</Button>
</div>
) : null}
{props.showLogoutButton ? (
<div className="mt-6">
<Button theme="secondary" onClick={logoutFromBackend}>
{t("screens.loadingUserError.logout")}
</Button>
</div>
) : null}
{props.showDisconnectButton ? (
<div className="mt-6">
<Button
theme="secondary"
onClick={() => setShowDisconnectConfirm(true)}
>
{t("screens.loadingUserError.disconnect")}
</Button>
</div>
) : null}
{props.showReloadButton ? (
<div className="mt-6">
<Button theme="secondary" onClick={() => window.location.reload()}>
{t("screens.loadingUserError.reload")}
</Button>
</div>
) : null}
{/* Disconnect Confirmation Modal */}
{showDisconnectConfirm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-modal-background rounded-xl p-8 max-w-md mx-4">
<h2 className="text-white text-xl font-semibold mb-4">
{t("screens.loadingUserError.disconnectTitle")}
</h2>
<p className="text-type-secondary mb-6">
{t("screens.loadingUserError.disconnectMessage")}
</p>
<div className="flex gap-3 justify-end">
<Button
theme="secondary"
onClick={() => setShowDisconnectConfirm(false)}
>
{t("screens.loadingUserError.disconectCancel")}
</Button>
<Button theme="danger" onClick={handleDisconnectConfirm}>
{t("screens.loadingUserError.disconnectConfirm")}
</Button>
</div>
</div>
</div>
)}
</LargeTextPart>
);
}
function AuthWrapper() {
const status = useAuthRestore();
const backendUrl = conf().BACKEND_URL;
const userBackendUrl = useBackendUrl();
const { t } = useTranslation();
const isCustomUrl = backendUrl !== userBackendUrl;
if (status.loading) return <LoadingScreen type="user" />;
if (status.error)
return (
<ErrorScreen
showResetButton={isCustomUrl}
showLogoutButton={!isCustomUrl}
showDisconnectButton={!isCustomUrl}
showReloadButton={!isCustomUrl}
>
{t(
isCustomUrl
? "screens.loadingUserError.textWithReset"
: "screens.loadingUserError.text",
)}
</ErrorScreen>
);
return <App />;
}
function MigrationRunner() {
const status = useAsync(async () => {
changeAppLanguage(useLanguageStore.getState().language);
await initializeOldStores();
const region = await detectRegion();
useRegionStore.getState().setRegion(region);
}, []);
const { t } = useTranslation();
if (status.loading) return <MigrationPart />;
if (status.error)
return <ErrorScreen>{t("screens.migration.failed")}</ErrorScreen>;
return <AuthWrapper />;
}
function TheRouter(props: { children: ReactNode }) {
const normalRouter = conf().NORMAL_ROUTER;
if (normalRouter)
return (
<BrowserRouter
future={{ v7_startTransition: true, v7_relativeSplatPath: true }}
>
{props.children}
</BrowserRouter>
);
return (
<HashRouter
future={{ v7_startTransition: true, v7_relativeSplatPath: true }}
>
{props.children}
</HashRouter>
);
}
// Checks if the extension is installed
function ExtensionStatus() {
const { t } = useTranslation();
const [state] = useAsyncFn(async () => {
if (!isExtensionActiveCached) {
return extensionInfo();
}
});
if (state.loading) {
return <LoadingScreen type="lazy" />;
}
if (state.error) {
return <ErrorScreen>{t("screens.loadingUserError.reload")}</ErrorScreen>;
}
return null;
}
const container = document.getElementById("root");
const root = createRoot(container!);
root.render(
<StrictMode>
<ErrorBoundary>
<HelmetProvider>
<Suspense fallback={<LoadingScreen type="lazy" />}>
<ExtensionStatus />
<ThemeProvider applyGlobal>
<ProgressSyncer />
<BookmarkSyncer />
<WatchHistorySyncer />
<GroupSyncer />
<SettingsSyncer />
<TheRouter>
<MigrationRunner />
</TheRouter>
</ThemeProvider>
</Suspense>
</HelmetProvider>
</ErrorBoundary>
</StrictMode>,
);