Files
lidify/frontend/components/layout/AuthenticatedLayout.tsx
Your Name cc8d0f6969 Release v1.3.0: Multi-source downloads, audio analyzer resilience, mobile improvements
Major Features:
- Multi-source download system (Soulseek/Lidarr with fallback)
- Configurable enrichment speed control (1-5x)
- Mobile touch drag support for seek sliders
- iOS PWA media controls (Control Center, Lock Screen)
- Artist name alias resolution via Last.fm
- Circuit breaker pattern for audio analysis

Critical Fixes:
- Audio analyzer stability (non-ASCII, BrokenProcessPool, OOM)
- Discovery system race conditions and import failures
- Radio decade categorization using originalYear
- LastFM API response normalization
- Mood bucket infinite loop prevention

Security:
- Bull Board admin authentication
- Lidarr webhook signature verification
- JWT token expiration and refresh
- Encryption key validation on startup

Closes #2, #6, #9, #13, #21, #26, #31, #34, #35, #37, #40, #43
2026-01-06 20:07:33 -06:00

203 lines
8.6 KiB
TypeScript

"use client";
import { useAuth } from "@/lib/auth-context";
import { usePathname } from "next/navigation";
import { useEffect } from "react";
import { Sidebar } from "./Sidebar";
import { TopBar } from "./TopBar";
import { TVLayout } from "./TVLayout";
import { BottomNavigation } from "./BottomNavigation";
import { UniversalPlayer } from "../player/UniversalPlayer";
import { MediaControlsHandler } from "../player/MediaControlsHandler";
import { PlayerModeWrapper } from "../player/PlayerModeWrapper";
import { ActivityPanel } from "./ActivityPanel";
import { GalaxyBackground } from "../ui/GalaxyBackground";
import { GradientSpinner } from "../ui/GradientSpinner";
import { PWAInstallPrompt } from "../PWAInstallPrompt";
import { PullToRefresh } from "../ui/PullToRefresh";
import { ReactNode } from "react";
import { useIsMobile, useIsTablet } from "@/hooks/useMediaQuery";
import { useIsTV } from "@/lib/tv-utils";
import { useActivityPanel } from "@/hooks/useActivityPanel";
const publicPaths = ["/login", "/register", "/onboarding", "/sync"];
export function AuthenticatedLayout({ children }: { children: ReactNode }) {
const { isAuthenticated, isLoading } = useAuth();
const pathname = usePathname();
const isMobile = useIsMobile();
const isTablet = useIsTablet();
const isTV = useIsTV();
const isMobileOrTablet = isMobile || isTablet;
const activityPanel = useActivityPanel();
// Listen for activity panel events (toggle/open/close/tab)
useEffect(() => {
const handleToggle = () => activityPanel.toggle();
const handleOpen = () => activityPanel.open();
const handleClose = () => activityPanel.close();
const handleSetTab = (
e: CustomEvent<{ tab: "notifications" | "active" | "history" }>
) => {
activityPanel.setActiveTab(e.detail.tab);
};
window.addEventListener("toggle-activity-panel", handleToggle);
window.addEventListener("open-activity-panel", handleOpen);
window.addEventListener("close-activity-panel", handleClose);
window.addEventListener(
"set-activity-panel-tab",
handleSetTab as EventListener
);
return () => {
window.removeEventListener("toggle-activity-panel", handleToggle);
window.removeEventListener("open-activity-panel", handleOpen);
window.removeEventListener("close-activity-panel", handleClose);
window.removeEventListener(
"set-activity-panel-tab",
handleSetTab as EventListener
);
};
}, [activityPanel]);
const isPublicPage = publicPaths.includes(pathname);
// Show loading state only on protected pages
if (!isPublicPage && isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-black">
<div className="flex flex-col items-center gap-4">
<GradientSpinner size="lg" />
<p className="text-white/60 text-sm">Loading...</p>
</div>
</div>
);
}
// On public pages (login/register), don't show sidebar/player/topbar
if (isPublicPage) {
return <>{children}</>;
}
// On protected pages, show appropriate layout based on device
if (isAuthenticated) {
// Android TV Layout - Optimized for 10-foot UI
if (isTV) {
return (
<PlayerModeWrapper>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:bg-white focus:text-black focus:rounded-lg focus:font-medium focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Skip to main content
</a>
<MediaControlsHandler />
<TVLayout>{children}</TVLayout>
</PlayerModeWrapper>
);
}
// Mobile/Tablet Layout
if (isMobileOrTablet) {
return (
<PlayerModeWrapper>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:bg-white focus:text-black focus:rounded-lg focus:font-medium focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Skip to main content
</a>
<div className="h-screen bg-black overflow-hidden flex flex-col">
<MediaControlsHandler />
<TopBar />
{/* Sidebar - renders MobileSidebar for hamburger menu */}
<Sidebar />
{/* Activity Panel - for mobile notifications (rendered as overlay) */}
<ActivityPanel
isOpen={activityPanel.isOpen}
onToggle={activityPanel.toggle}
activeTab={activityPanel.activeTab}
onTabChange={activityPanel.setActiveTab}
/>
{/* Main content area with rounded corners */}
<PullToRefresh>
<main
id="main-content"
tabIndex={-1}
className="flex-1 bg-gradient-to-b from-[#1a1a1a] via-black to-black mx-2 mb-2 rounded-lg overflow-y-auto relative focus:outline-none"
style={{
marginTop: "58px",
marginBottom:
"calc(56px + env(safe-area-inset-bottom, 0px) + 8px)",
}}
>
<GalaxyBackground />
{/* Padding at bottom for mini player floating above */}
<div className="pb-24">{children}</div>
</main>
</PullToRefresh>
{/* Mini Player - fixed, positioned above bottom nav */}
<UniversalPlayer />
{/* Bottom Navigation - fixed at bottom */}
<BottomNavigation />
<PWAInstallPrompt />
</div>
</PlayerModeWrapper>
);
}
// Desktop Layout
return (
<PlayerModeWrapper>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:bg-white focus:text-black focus:rounded-lg focus:font-medium focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Skip to main content
</a>
<div
className="h-screen bg-black overflow-hidden flex flex-col"
style={{ paddingTop: "64px" }}
>
<MediaControlsHandler />
<TopBar />
<div className="flex-1 flex gap-2 p-2 pt-0 overflow-hidden">
<Sidebar />
<main
id="main-content"
tabIndex={-1}
className="flex-1 bg-gradient-to-b from-[#1a1a1a] via-black to-black rounded-lg overflow-y-auto relative focus:outline-none"
>
<GalaxyBackground />
{children}
</main>
<ActivityPanel
isOpen={activityPanel.isOpen}
onToggle={activityPanel.toggle}
activeTab={activityPanel.activeTab}
onTabChange={activityPanel.setActiveTab}
/>
</div>
<UniversalPlayer />
<PWAInstallPrompt />
</div>
</PlayerModeWrapper>
);
}
// If not authenticated on a protected page, auth context will redirect
return (
<div className="min-h-screen flex items-center justify-center bg-black">
<div className="flex flex-col items-center gap-4">
<GradientSpinner size="lg" />
<p className="text-white/60 text-sm">Redirecting...</p>
</div>
</div>
);
}