commit 4d995108f093d48c96d05d75c4208da62fb9d6a3 Author: Friendlyduck <78284198+Andgopink@users.noreply.github.com> Date: Fri Nov 21 16:06:20 2025 +0300 Add files via upload diff --git a/attached_assets/generated_images/abstract_dark_digital_sound_wave_album_art.png b/attached_assets/generated_images/abstract_dark_digital_sound_wave_album_art.png new file mode 100644 index 0000000..5d398e4 Binary files /dev/null and b/attached_assets/generated_images/abstract_dark_digital_sound_wave_album_art.png differ diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..6bc886e --- /dev/null +++ b/client/index.html @@ -0,0 +1,27 @@ + + + + + + + Navidrome AI Stream + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/client/public/favicon.png b/client/public/favicon.png new file mode 100644 index 0000000..625caff Binary files /dev/null and b/client/public/favicon.png differ diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..95d61e5 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,49 @@ +import { Switch, Route } from "wouter"; +import { queryClient } from "./lib/queryClient"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import NotFound from "@/pages/not-found"; +import Home from "@/pages/Home"; +import Library from "@/pages/Library"; +import Radio from "@/pages/Radio"; +import Admin from "@/pages/Admin"; +import Login from "@/pages/Login"; +import { isAuthenticated } from "@/lib/auth"; + +function ProtectedRoute({ component: Component }: { component: React.ComponentType }) { + if (!isAuthenticated()) { + window.location.href = "/login"; + return null; + } + return ; +} + +function Router() { + return ( + + + + + + + {isAuthenticated() ? : } + + {/* Fallback to 404 */} + + + ); +} + +function App() { + return ( + + + + + + + ); +} + +export default App; diff --git a/client/src/components/layout/AppLayout.tsx b/client/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..cdccdd9 --- /dev/null +++ b/client/src/components/layout/AppLayout.tsx @@ -0,0 +1,23 @@ +import { ReactNode } from "react"; +import { Sidebar } from "./Sidebar"; +import { Player } from "./Player"; +import { ChatSidebar } from "./ChatSidebar"; + +interface AppLayoutProps { + children: ReactNode; +} + +export function AppLayout({ children }: AppLayoutProps) { + return ( +
+ +
+
+ {children} +
+
+ + +
+ ); +} diff --git a/client/src/components/layout/ChatSidebar.tsx b/client/src/components/layout/ChatSidebar.tsx new file mode 100644 index 0000000..a16bfda --- /dev/null +++ b/client/src/components/layout/ChatSidebar.tsx @@ -0,0 +1,137 @@ +import { useState, useRef, useEffect } from "react"; +import { Send, Bot, Sparkles, Music, MoreHorizontal } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { INITIAL_CHAT, Message } from "@/lib/mockData"; +import { cn } from "@/lib/utils"; + +export function ChatSidebar() { + const [messages, setMessages] = useState(INITIAL_CHAT); + const [input, setInput] = useState(""); + const [isTyping, setIsTyping] = useState(false); + const scrollRef = useRef(null); + + const handleSend = () => { + if (!input.trim()) return; + + const newMsg: Message = { + id: Date.now().toString(), + role: "user", + content: input, + timestamp: new Date() + }; + + setMessages(prev => [...prev, newMsg]); + setInput(""); + setIsTyping(true); + + // Mock AI response + setTimeout(() => { + const aiMsg: Message = { + id: (Date.now() + 1).toString(), + role: "assistant", + content: "I've queued up some similar tracks based on that request. The acoustic properties match your current mood profile.", + timestamp: new Date() + }; + setMessages(prev => [...prev, aiMsg]); + setIsTyping(false); + }, 1500); + }; + + useEffect(() => { + if (scrollRef.current) { + // Basic scroll to bottom + const scrollContainer = scrollRef.current.querySelector('[data-radix-scroll-area-viewport]'); + if(scrollContainer) { + scrollContainer.scrollTop = scrollContainer.scrollHeight; + } + } + }, [messages]); + + return ( + + ); +} diff --git a/client/src/components/layout/Player.tsx b/client/src/components/layout/Player.tsx new file mode 100644 index 0000000..e5935ca --- /dev/null +++ b/client/src/components/layout/Player.tsx @@ -0,0 +1,92 @@ +import { useState, useEffect } from "react"; +import { Play, Pause, SkipBack, SkipForward, Volume2, Repeat, Shuffle, Heart, Mic2 } from "lucide-react"; +import { Slider } from "@/components/ui/slider"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { MOCK_SONGS } from "@/lib/mockData"; + +export function Player() { + const [isPlaying, setIsPlaying] = useState(false); + const [progress, setProgress] = useState(33); + const [currentSong] = useState(MOCK_SONGS[0]); + + // Fake progress animation + useEffect(() => { + if (!isPlaying) return; + const interval = setInterval(() => { + setProgress((p) => (p >= 100 ? 0 : p + 0.5)); + }, 1000); + return () => clearInterval(interval); + }, [isPlaying]); + + return ( +
+ {/* Track Info */} +
+
+ {currentSong.title} +
+ +
+
+
+

{currentSong.title}

+ {currentSong.artist} +
+
+ + {/* Controls */} +
+
+ + + + + +
+ +
+ 1:24 + setProgress(v[0])} + /> + {currentSong.duration} +
+
+ + {/* Volume & Extras */} +
+ +
+ + +
+
+
+ ); +} diff --git a/client/src/components/layout/Sidebar.tsx b/client/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2c7c20c --- /dev/null +++ b/client/src/components/layout/Sidebar.tsx @@ -0,0 +1,67 @@ +import { cn } from "@/lib/utils"; +import { NAV_ITEMS } from "@/lib/mockData"; +import { Link, useLocation } from "wouter"; +import { Settings, LogOut, Command } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { logout } from "@/lib/auth"; + +export function Sidebar() { + const [location] = useLocation(); + + return ( + + ); +} diff --git a/client/src/components/ui/accordion.tsx b/client/src/components/ui/accordion.tsx new file mode 100644 index 0000000..e1797c9 --- /dev/null +++ b/client/src/components/ui/accordion.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..fa2b442 --- /dev/null +++ b/client/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/client/src/components/ui/alert.tsx b/client/src/components/ui/alert.tsx new file mode 100644 index 0000000..5afd41d --- /dev/null +++ b/client/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/client/src/components/ui/aspect-ratio.tsx b/client/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c4abbf3 --- /dev/null +++ b/client/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/client/src/components/ui/avatar.tsx b/client/src/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/client/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx new file mode 100644 index 0000000..3f03665 --- /dev/null +++ b/client/src/components/ui/badge.tsx @@ -0,0 +1,43 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + // @replit + // Whitespace-nowrap: Badges should never wrap. + "whitespace-nowrap inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" + + " hover-elevate ", + { + variants: { + variant: { + default: + // @replit shadow-xs instead of shadow, no hover because we use hover-elevate + "border-transparent bg-primary text-primary-foreground shadow-xs", + secondary: + // @replit no hover because we use hover-elevate + "border-transparent bg-secondary text-secondary-foreground", + destructive: + // @replit shadow-xs instead of shadow, no hover because we use hover-elevate + "border-transparent bg-destructive text-destructive-foreground shadow-xs", + // @replit shadow-xs" - use badge outline variable + outline: "text-foreground border [border-color:var(--badge-outline)]", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/client/src/components/ui/breadcrumb.tsx b/client/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/client/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>