Files
lidify/frontend/components/layout/BottomNavigation.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

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>
);
}