import { useEffect, useRef, useState } from "react"; import { BrowserRouter, Routes, Route, Navigate, useLocation, useNavigate, useParams, useSearchParams, } from "react-router"; import { QueryClientProvider, useQueryClient } from "@tanstack/react-query"; import { queryClient } from "@/lib/query-client"; import { AuthProvider, useAuth } from "@/hooks/useAuth"; import { ThemeProvider } from "@/hooks/useTheme"; import { CustomThemeProvider } from "@/contexts/CustomThemeProvider"; import { ErrorBoundary } from "@/components/ErrorBoundary"; import ImpersonationBanner from "@/components/ImpersonationBanner"; import { loadStoredImpersonationAdminSession } from "@/lib/impersonationSession"; import { Toaster } from "@/components/ui/sonner"; import Layout from "@/components/Layout"; import AdminLayout from "@/components/AdminLayout"; import Home from "@/pages/Home"; import Login from "@/pages/Login"; import OAuthComplete from "@/pages/OAuthComplete"; import ActivateDevice from "@/pages/ActivateDevice"; import SetupWizard from "@/pages/SetupWizard"; import Profiles from "@/pages/Profiles"; import Catalog from "@/pages/Catalog"; import LibraryPage from "@/pages/LibraryPage"; import ItemDetail from "@/pages/ItemDetail/index"; import PersonDetail from "@/pages/PersonDetail"; import Collections from "@/pages/Collections"; import CollectionEditor from "@/pages/CollectionEditor"; import AdminDashboard from "@/pages/AdminDashboard"; import AdminActivity from "@/pages/AdminActivity"; import AdminLogs from "@/pages/AdminLogs"; import AdminUsers from "@/pages/AdminUsers"; import AdminDevices from "@/pages/AdminDevices"; import AdminLibraries from "@/pages/AdminLibraries"; import AdminSettingsLayout from "@/pages/admin-settings/AdminSettingsLayout"; import AdminNodes from "@/pages/AdminNodes"; import AdminSections from "@/pages/AdminSections"; import AdminCollections from "@/pages/AdminCollections"; import AdminCollectionEditor from "@/pages/AdminCollectionEditor"; import AdminPlaybackHistory from "@/pages/AdminPlaybackHistory"; import AdminMaintenance from "@/pages/AdminMaintenance"; import AdminApiKeys from "@/pages/AdminApiKeys"; import AdminUserDetail from "@/pages/AdminUserDetail"; import AdminTasks from "@/pages/AdminTasks"; import AdminTaskDetail from "@/pages/AdminTaskDetail"; import AdminPlugins from "@/pages/AdminPlugins"; import AdminHistoryImport from "@/pages/AdminHistoryImport"; import AdminRecommendations from "@/pages/AdminRecommendations"; import Recommendations from "@/pages/Recommendations"; import RecommendationsSection from "@/pages/RecommendationsSection"; import Calendar from "@/pages/Calendar"; import Signup from "@/pages/Signup"; import TasteSeed from "@/pages/TasteSeed"; import { useFavorites } from "@/hooks/queries/favorites"; import { isTasteSeedDismissed } from "@/lib/tasteSeed"; import SettingsLayout from "@/pages/SettingsLayout"; import AppearanceSettings from "@/pages/settings/AppearanceSettings"; import AccessibilitySettings from "@/pages/settings/AccessibilitySettings"; import PlaybackSettings from "@/pages/settings/PlaybackSettings"; import ProfilesSettings from "@/pages/settings/ProfilesSettings"; import LibrarySettings from "@/pages/settings/LibrarySettings"; import HistoryImportSettings from "@/pages/settings/HistoryImportSettings"; import WebhookSyncSettings from "@/pages/settings/WebhookSyncSettings"; import WatchProvidersSettings from "@/pages/settings/WatchProvidersSettings"; import SubtitleAppearanceSettings from "@/pages/settings/SubtitleAppearanceSettings"; import HomeScreenSettings from "@/pages/settings/HomeScreenSettings"; import ThemeEditorSettings from "@/pages/settings/ThemeEditorSettings"; import CardOverlaySettings from "@/pages/settings/CardOverlaySettings"; import PersonalizeSettings from "@/pages/settings/PersonalizeSettings"; import WatchTogetherJoin from "@/pages/WatchTogetherJoin"; import WatchTogetherRoomPage from "@/pages/WatchTogetherRoomPage"; import WatchRoute from "@/pages/WatchRoute"; import ProfileCustomizeHome from "@/pages/ProfileCustomizeHome"; import { WatchPlaybackBar, WatchPlaybackHost, WatchPlaybackProvider, } from "@/playback/WatchPlaybackChrome"; import type { ReactNode } from "react"; import { buildLegacyBrowseCatalogHref, buildPersonalCatalogHref, buildQueryCatalogHref, buildUserCollectionCatalogHref, } from "@/pages/catalogSearchParams"; import { buildLegacyWebhookSyncRedirectTarget } from "@/lib/webhookSync"; import { toast } from "sonner"; /** Scrolls to top on pathname change (custom replacement for ScrollRestoration which requires data router). */ function useScrollRestoration() { const { pathname } = useLocation(); useEffect(() => { window.scrollTo(0, 0); }, [pathname]); } /** Announces route changes to screen readers via an aria-live region. */ function RouteAnnouncer() { const location = useLocation(); const [announcement, setAnnouncement] = useState(""); useEffect(() => { // Small delay so document.title has time to update via useDocumentTitle hooks const id = setTimeout(() => { setAnnouncement(document.title || "Page loaded"); }, 100); return () => clearTimeout(id); }, [location.pathname]); return (
{announcement}
); } function ScrollRestorationManager() { useScrollRestoration(); return null; } function RequireAuth({ children }: { children: ReactNode }) { const { user, loading, setupLoading } = useAuth(); if (loading || setupLoading) { return (
Loading application Loading...
); } if (!user) return ; return <>{children}; } function SetupGate({ children }: { children: ReactNode }) { const { user, setupLoading, setupRequired } = useAuth(); if (setupLoading) { return (
Loading application Loading...
); } if (setupRequired && !user) return ; return <>{children}; } function RequireProfile({ children }: { children: ReactNode }) { const { profile } = useAuth(); if (!profile) return ; return <>{children}; } function RequireAdmin({ children }: { children: ReactNode }) { const { user } = useAuth(); if (user?.role !== "admin") return ; return <>{children}; } function RequirePrimaryOrAdmin({ children }: { children: ReactNode }) { const { user, profile } = useAuth(); if (user?.role !== "admin" && profile?.is_primary !== true) { return ; } return <>{children}; } /** * Redirects new profiles (no favorites yet, no skip flag) to the taste-seed * onboarding screen the first time they land on Home. Only checks on Home so * deep-links to other pages aren't blocked. Once the user picks any items * (or favorites anything by normal use), or explicitly skips, the gate stops * redirecting. */ function TasteSeedGate({ children }: { children: ReactNode }) { const { profile } = useAuth(); const { data: favorites, isPending, isError } = useFavorites(); if (isPending || isError || !profile) return <>{children}; const hasFavorites = (favorites?.length ?? 0) > 0; const dismissed = isTasteSeedDismissed(profile.id); if (!hasFavorites && !dismissed) { return ; } return <>{children}; } /** Clears user-scoped query caches on profile switch or logout. */ function QueryCacheManager() { const { user, profile } = useAuth(); const qc = useQueryClient(); const prevProfileId = useRef(profile?.id); useEffect(() => { if (!user) { qc.clear(); prevProfileId.current = undefined; return; } if (prevProfileId.current && prevProfileId.current !== profile?.id) { qc.removeQueries({ queryKey: ["favorites"] }); qc.removeQueries({ queryKey: ["watchlist"] }); qc.removeQueries({ queryKey: ["history"] }); qc.removeQueries({ queryKey: ["collections"] }); qc.removeQueries({ queryKey: ["libraryPlaybackPreferences"] }); qc.removeQueries({ queryKey: ["progress"] }); qc.removeQueries({ queryKey: ["sections"] }); qc.removeQueries({ queryKey: ["calendar"] }); // Recommendation rows include per-profile user_state (is_favorite, etc.); // the taste-seed picker depends on this for pre-selection. qc.removeQueries({ queryKey: ["recommendations"] }); } prevProfileId.current = profile?.id; }, [user, profile?.id, qc]); return null; } function AppChrome() { const { user, isImpersonating, endImpersonation } = useAuth(); const navigate = useNavigate(); if (!user?.impersonation || !isImpersonating) { return null; } async function handleEndImpersonation() { const returnPath = loadStoredImpersonationAdminSession()?.returnPath ?? "/admin/users"; try { await endImpersonation(); navigate(returnPath, { replace: true }); } catch (error) { toast.error(error instanceof Error ? error.message : "Failed to end impersonation"); } } return ( ); } function LegacySearchRedirect() { const [searchParams] = useSearchParams(); return ; } function LegacyBrowseRedirect() { const [searchParams] = useSearchParams(); const href = buildLegacyBrowseCatalogHref(searchParams); if (!href) { return ; } return ; } function LegacyWebhookSyncRedirect() { const { search } = useLocation(); return ; } function LegacyPersonalCatalogRedirect({ source, }: { source: "favorites" | "watchlist" | "history"; }) { return ; } function LegacyUserCollectionRedirect() { const { id } = useParams<{ id: string }>(); const location = useLocation(); if (!id) { return ; } return ( ); } function AppRoutes() { return ( } /> } /> } /> } /> } /> } /> } /> } /> {/* Admin area — own layout, no profile required */} } > } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Settings area — own layout, requires profile */} } > } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); } export default function App() { return ( ); }