import { useState, useCallback, useEffect, useMemo, useRef } from "react";
import type { ReactNode } from "react";
import { Link, useLocation, useParams } from "react-router";
import ViewTransitionLink from "@/components/ViewTransitionLink";
import { getProfileMenuSide, isSidebarExpanded } from "@/components/AppSidebar.logic";
import { SiloBrand } from "@/components/SiloBrand";
import { useAuth } from "@/hooks/useAuth";
import { useCurrentProfile } from "@/hooks/useCurrentProfile";
import { navigateToPluginRoute } from "@/lib/buildPluginHref";
import { useUserLibraries } from "@/hooks/queries/libraries";
import { usePluginSettingsList } from "@/hooks/queries/pluginSettings";
import { useRequestFeatureStatus } from "@/hooks/queries/useRequests";
import { useSidebarPins, useToggleSidebarPin } from "@/hooks/queries/sidebarPins";
import { useViewTransitionNavigate } from "@/hooks/useViewTransition";
import { pluginRouteHref } from "@/lib/pluginRouteHref";
import {
buildLibraryCollectionCatalogHref,
buildPersonalCatalogHref,
buildQueryCatalogHref,
buildSectionCatalogHref,
parseCatalogSearchParams,
} from "@/pages/catalogSearchParams";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Film,
Tv,
Library,
Search,
Heart,
List,
Clock,
FolderOpen,
LogOut,
UserCircle,
Shield,
Settings,
Sparkles,
CalendarDays,
Home,
UsersRound,
ChevronDown,
ChevronRight,
PinOff,
LayoutGrid,
Puzzle,
BookHeadphones,
Send,
} from "lucide-react";
import { useTheme } from "@/hooks/useTheme";
import { CURATED_THEME_IDS, THEMES } from "@/lib/themes";
import { cn } from "@/lib/utils";
function getLibraryIcon(type: string) {
switch (type) {
case "movies":
return ;
case "series":
return ;
case "audiobook":
case "audiobooks":
return ;
default:
return ;
}
}
function getLibraryIdFromPathname(pathname: string): number | null {
const match = pathname.match(/^\/library\/(\d+)(?:\/|$)/);
if (!match) return null;
const id = Number(match[1]);
return Number.isInteger(id) && id > 0 ? id : null;
}
function SidebarLabel({ children, show }: { children: ReactNode; show: boolean }) {
return (
{children}
);
}
function SidebarSectionHeader({
children,
show,
collapsible = false,
expanded = false,
onToggle,
}: {
children: ReactNode;
show: boolean;
collapsible?: boolean;
expanded?: boolean;
onToggle?: () => void;
}) {
const textClass = "text-muted-foreground text-[10px] font-semibold tracking-[0.22em] uppercase";
const collapsedDivider = (
);
if (collapsible) {
return (
{collapsedDivider}
);
}
return (
{collapsedDivider}
{children}
);
}
interface AppSidebarProps {
onNavigate?: () => void;
collapsed?: boolean;
}
export default function AppSidebar({ onNavigate, collapsed = false }: AppSidebarProps) {
const location = useLocation();
const navigate = useViewTransitionNavigate();
const params = useParams<{ libraryId: string }>();
const { user, logout, clearProfile } = useAuth();
const { profile } = useCurrentProfile();
const { theme, setTheme, previewTheme, resetPreviewTheme } = useTheme();
const isAdmin = user?.role === "admin";
const { data: libraries } = useUserLibraries();
const { pins } = useSidebarPins();
const { togglePin } = useToggleSidebarPin();
const { data: pluginSettings } = usePluginSettingsList();
const requestStatus = useRequestFeatureStatus();
const showRequestsNav = requestStatus.data?.requests_enabled === true;
const pluginNavLinks = useMemo(() => {
const installations = pluginSettings?.installations ?? [];
const links: { id: string; basePath: string; label: string; pluginId: string }[] = [];
for (const inst of installations) {
for (const route of inst.routes) {
if (!route.navigable || route.navigation_kind !== "user") continue;
links.push({
id: `${inst.id}:${route.id || route.path}`,
basePath: pluginRouteHref(inst.id, route.path),
label: route.navigation_label || inst.plugin_id,
pluginId: inst.plugin_id,
});
}
}
return links;
}, [pluginSettings]);
const catalogState = useMemo(
() =>
location.pathname === "/catalog"
? parseCatalogSearchParams(new URLSearchParams(location.search))
: null,
[location.pathname, location.search],
);
const activeLibraryId =
params.libraryId && Number.isInteger(Number(params.libraryId))
? Number(params.libraryId)
: (getLibraryIdFromPathname(location.pathname) ??
(catalogState?.source === "section" && catalogState.scope === "library"
? (catalogState.library_id ?? null)
: catalogState?.source === "library_collection"
? (catalogState.library_id ?? null)
: null));
// Hover-to-expand when collapsed (150ms enter delay prevents accidental expansion)
const [hovered, setHovered] = useState(false);
const [profileMenuOpen, setProfileMenuOpen] = useState(false);
const hoverTimerRef = useRef | null>(null);
const handleMouseEnter = useCallback(() => {
if (!collapsed) return;
hoverTimerRef.current = setTimeout(() => setHovered(true), 150);
}, [collapsed]);
const handleMouseLeave = useCallback(() => {
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
hoverTimerRef.current = null;
if (profileMenuOpen) return;
setHovered(false);
}, [profileMenuOpen]);
useEffect(() => {
return () => {
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
};
}, []);
const sidebarExpanded = isSidebarExpanded(collapsed, hovered, profileMenuOpen);
const showLabels = sidebarExpanded;
const [librariesExpanded, setLibrariesExpanded] = useState(true);
const [expandedLibraries, setExpandedLibraries] = useState>(
() =>
new Set(
Object.keys(pins)
.map(Number)
.filter((id) => Number.isInteger(id) && id > 0),
),
);
const toggleLibraryExpand = useCallback((libId: number) => {
setExpandedLibraries((prev) => {
const next = new Set(prev);
if (next.has(libId)) {
next.delete(libId);
} else {
next.add(libId);
}
return next;
});
}, []);
function isActive(href: string, exact?: boolean) {
if (exact) return location.pathname === href;
return location.pathname === href || location.pathname.startsWith(`${href}/`);
}
function isCatalogSourceActive(source: "query" | "favorites" | "watchlist" | "history") {
return location.pathname === "/catalog" && catalogState?.source === source;
}
function isPinnedCatalogActive(
libId: number,
pin: { type: "collection" | "section"; id: string },
) {
if (location.pathname !== "/catalog" || !catalogState) {
return false;
}
if (pin.type === "collection") {
return catalogState.source === "library_collection" && catalogState.collection_id === pin.id;
}
return (
catalogState.source === "section" &&
catalogState.scope === "library" &&
catalogState.library_id === libId &&
catalogState.section_id === pin.id
);
}
const navLinkClass = (href: string, exact?: boolean) =>
navLinkClassForState(isActive(href, exact));
const navLinkClassForState = (active: boolean) =>
`relative flex items-center gap-2.5 rounded-xl px-3 py-3 text-[13px] font-medium transition-all duration-200 ${
active
? "text-sidebar-accent-foreground bg-sidebar-accent/90 shadow-[0_16px_30px_-24px_rgba(0,0,0,0.7)]"
: "text-muted-foreground hover:text-sidebar-foreground hover:bg-sidebar-accent/70"
}`;
return (
);
}