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
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { Library, BookOpen, Mic, ListMusic } from "lucide-react";
|
|
import { cn } from "@/utils/cn";
|
|
import { useIsMobile, useIsTablet } from "@/hooks/useMediaQuery";
|
|
|
|
const navigationItems = [
|
|
{
|
|
name: "Library",
|
|
href: "/library",
|
|
icon: Library,
|
|
matchPattern: "/library"
|
|
},
|
|
{
|
|
name: "Audiobooks",
|
|
href: "/audiobooks",
|
|
icon: BookOpen,
|
|
matchPattern: "/audiobooks"
|
|
},
|
|
{
|
|
name: "Podcasts",
|
|
href: "/podcasts",
|
|
icon: Mic,
|
|
matchPattern: "/podcasts"
|
|
},
|
|
{
|
|
name: "Playlists",
|
|
href: "/playlists",
|
|
icon: ListMusic,
|
|
matchPattern: "/playlist" // Matches both /playlists and /playlist/[id]
|
|
},
|
|
];
|
|
|
|
export function BottomNavigation() {
|
|
const pathname = usePathname();
|
|
const isMobile = useIsMobile();
|
|
const isTablet = useIsTablet();
|
|
const isMobileOrTablet = isMobile || isTablet;
|
|
|
|
// Only render on mobile/tablet
|
|
if (!isMobileOrTablet) return null;
|
|
|
|
return (
|
|
<nav
|
|
className="fixed bottom-0 left-0 right-0 z-40 bg-black border-t border-white/10"
|
|
role="navigation"
|
|
aria-label="Main navigation"
|
|
style={{
|
|
paddingBottom: 'env(safe-area-inset-bottom, 0px)'
|
|
}}
|
|
>
|
|
<div className="flex items-center justify-around h-14">
|
|
{navigationItems.map((item) => {
|
|
const isActive = pathname.startsWith(item.matchPattern);
|
|
const Icon = item.icon;
|
|
|
|
return (
|
|
<Link
|
|
key={item.name}
|
|
href={item.href}
|
|
className={cn(
|
|
"flex flex-col items-center justify-center flex-1 h-full py-2 transition-colors",
|
|
isActive
|
|
? "text-white"
|
|
: "text-gray-500 active:text-gray-300"
|
|
)}
|
|
aria-label={item.name}
|
|
aria-current={isActive ? "page" : undefined}
|
|
>
|
|
<Icon
|
|
className={cn(
|
|
"w-5 h-5 mb-1",
|
|
isActive && "text-white"
|
|
)}
|
|
strokeWidth={isActive ? 2.5 : 2}
|
|
/>
|
|
<span
|
|
className={cn(
|
|
"text-[10px] tracking-wide",
|
|
isActive ? "font-semibold" : "font-medium"
|
|
)}
|
|
>
|
|
{item.name}
|
|
</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|