feat: update commit SHA, enhance artist page with new layout and favorite functionality, improve settings page with first-time setup option

This commit is contained in:
2025-07-01 21:52:36 +00:00
committed by GitHub
parent bc159ac20a
commit 87a2f06053
6 changed files with 134 additions and 22 deletions

View File

@@ -1 +1 @@
NEXT_PUBLIC_COMMIT_SHA=3ca162e NEXT_PUBLIC_COMMIT_SHA=bc159ac

View File

@@ -23,7 +23,7 @@ const CHANGELOG = [
'Improved search and browsing experience', 'Improved search and browsing experience',
'Added history tracking for played songs', 'Added history tracking for played songs',
'New Library Artist Page', 'New Library Artist Page',
'Artist page with top songs and albums', 'Enhanced audio player with better controls',
'Added settings page for customization options', 'Added settings page for customization options',
'Introduced Whats New popup for version updates', 'Introduced Whats New popup for version updates',
'Improved UI consistency with new Badge component', 'Improved UI consistency with new Badge component',

View File

@@ -3,7 +3,6 @@
import Image from "next/image" import Image from "next/image"
import { PlusCircledIcon } from "@radix-ui/react-icons" import { PlusCircledIcon } from "@radix-ui/react-icons"
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { import {
ContextMenu, ContextMenu,
@@ -15,20 +14,23 @@ import {
ContextMenuSubTrigger, ContextMenuSubTrigger,
ContextMenuTrigger, ContextMenuTrigger,
} from "../../components/ui/context-menu" } from "../../components/ui/context-menu"
import { Artist } from "@/lib/navidrome"
import { useNavidrome } from "./NavidromeContext"
import { useAudioPlayer } from "@/app/components/AudioPlayerContext"; import { useAudioPlayer } from "@/app/components/AudioPlayerContext";
import { getNavidromeAPI } from "@/lib/navidrome"; import { getNavidromeAPI } from "@/lib/navidrome";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useNavidrome } from '@/app/components/NavidromeContext';
import { Artist } from '@/lib/navidrome';
interface ArtistIconProps extends React.HTMLAttributes<HTMLDivElement> { interface ArtistIconProps extends React.HTMLAttributes<HTMLDivElement> {
artist: Artist artist: Artist
size?: number size?: number
imageOnly?: boolean
} }
export function ArtistIcon({ export function ArtistIcon({
artist, artist,
size = 150, size = 150,
imageOnly = false,
className, className,
...props ...props
}: ArtistIconProps) { }: ArtistIconProps) {
@@ -57,11 +59,59 @@ export function ArtistIcon({
? api.getCoverArtUrl(artist.coverArt, 200) ? api.getCoverArtUrl(artist.coverArt, 200)
: '/default-user.jpg'; : '/default-user.jpg';
// If imageOnly is true, return just the image without context menu or text
if (imageOnly) {
return (
<div
className={cn("overflow-hidden rounded-full cursor-pointer flex-shrink-0", className)}
onClick={handleClick}
style={{ width: size, height: size }}
{...props}
>
<Image
src={artistImageUrl}
alt={artist.name}
width={size}
height={size}
className="w-full h-full object-cover transition-all hover:scale-105"
/>
</div>
);
}
return ( return (
<div className={cn("space-y-3", className)} {...props}> <div className={cn("space-y-3", className)} {...props}>
<ContextMenu> <ContextMenu>
<ContextMenuTrigger> <ContextMenuTrigger>
<div <Card key={artist.id} className="overflow-hidden">
<div
className="aspect-square relative group cursor-pointer"
style={{ width: size, height: size }}
onClick={() => handleClick()}
>
<div className="w-full h-full">
<Image
src={artist.coverArt && api ? api.getCoverArtUrl(artist.coverArt, 200) : '/placeholder-artist.png'}
alt={artist.name}
width={size}
height={size}
className="object-cover w-full h-full"
/>
</div>
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Button size="sm">
View Artist
</Button>
</div>
</div>
<CardContent className="p-4">
<h3 className="font-semibold truncate">{artist.name}</h3>
<p className="text-sm text-muted-foreground">
{artist.albumCount} albums
</p>
</CardContent>
</Card>
{/* <div
className="overflow-hidden rounded-full cursor-pointer flex-shrink-0" className="overflow-hidden rounded-full cursor-pointer flex-shrink-0"
onClick={handleClick} onClick={handleClick}
style={{ width: size, height: size }} style={{ width: size, height: size }}
@@ -73,7 +123,7 @@ export function ArtistIcon({
height={size} height={size}
className="w-full h-full object-cover transition-all hover:scale-105" className="w-full h-full object-cover transition-all hover:scale-105"
/> />
</div> </div> */}
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent className="w-40"> <ContextMenuContent className="w-40">
<ContextMenuItem onClick={handleStar}> <ContextMenuItem onClick={handleStar}>
@@ -117,10 +167,6 @@ export function ArtistIcon({
<ContextMenuItem>Share</ContextMenuItem> <ContextMenuItem>Share</ContextMenuItem>
</ContextMenuContent> </ContextMenuContent>
</ContextMenu> </ContextMenu>
<div className="space-y-1 text-sm" onClick={handleClick}>
<p className="font-medium leading-none text-center">{artist.name}</p>
<p className="text-xs text-muted-foreground text-center">{artist.albumCount} albums</p>
</div>
</div> </div>
); );
} }

View File

@@ -55,7 +55,7 @@ export function Sidebar({ className, playlists, collapsed = false, onToggle }: S
{collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />} {collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
</Button> </Button>
<div className={`space-y-4 py-4 ${collapsed ? "pt-6" : "" }`}> <div className="space-y-4 py-4">
<div className="px-3 py-2"> <div className="px-3 py-2">
<p className={cn("mb-2 px-4 text-lg font-semibold tracking-tight", collapsed && "sr-only")}> <p className={cn("mb-2 px-4 text-lg font-semibold tracking-tight", collapsed && "sr-only")}>
Discover Discover

View File

@@ -7,17 +7,34 @@ import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent } from "@/components/ui/tabs"; import { Tabs, TabsContent } from "@/components/ui/tabs";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ArtistIcon } from '@/app/components/artist-icon'; import { ArtistIcon } from '@/app/components/artist-icon';
import { useNavidrome } from '@/app/components/NavidromeContext'; import { useNavidrome } from '@/app/components/NavidromeContext';
import { Artist } from '@/lib/navidrome'; import { Artist } from '@/lib/navidrome';
import Loading from '@/app/components/loading'; import Loading from '@/app/components/loading';
import { Search } from 'lucide-react'; import { Search, Heart } from 'lucide-react';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
export default function ArtistPage() { export default function ArtistPage() {
const { artists, isLoading } = useNavidrome(); const { artists, isLoading, api, starItem, unstarItem } = useNavidrome();
const [filteredArtists, setFilteredArtists] = useState<Artist[]>([]); const [filteredArtists, setFilteredArtists] = useState<Artist[]>([]);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState<'name' | 'albumCount'>('name'); const [sortBy, setSortBy] = useState<'name' | 'albumCount'>('name');
const router = useRouter();
const toggleFavorite = async (artistId: string, isStarred: boolean) => {
if (isStarred) {
await unstarItem(artistId, 'artist');
} else {
await starItem(artistId, 'artist');
}
};
const handleViewArtist = (artist: Artist) => {
router.push(`/artist/${artist.id}`);
};
useEffect(() => { useEffect(() => {
if (artists.length > 0) { if (artists.length > 0) {
@@ -87,14 +104,32 @@ export default function ArtistPage() {
<Separator className="my-4" /> <Separator className="my-4" />
<div className="relative"> <div className="relative">
<ScrollArea> <ScrollArea>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4 pb-4"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{filteredArtists.map((artist) => ( {filteredArtists.map((artist) => (
<ArtistIcon <Card key={artist.id} className="overflow-hidden">
key={artist.id} <div className="aspect-square relative group cursor-pointer" onClick={() => handleViewArtist(artist)}>
artist={artist} <div className="w-full h-full">
className="flex justify-center" <Image
size={150} src={artist.coverArt && api ? api.getCoverArtUrl(artist.coverArt, 200) : '/placeholder-artist.png'}
/> alt={artist.name}
width={290}
height={290}
className="object-cover w-full h-full"
/>
</div>
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Button size="sm">
View Artist
</Button>
</div>
</div>
<CardContent className="p-4">
<h3 className="font-semibold truncate">{artist.name}</h3>
<p className="text-sm text-muted-foreground">
{artist.albumCount} albums
</p>
</CardContent>
</Card>
))} ))}
</div> </div>
<ScrollBar orientation="horizontal" /> <ScrollBar orientation="horizontal" />

View File

@@ -443,6 +443,37 @@ const SettingsPage = () => {
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FaCog className="w-5 h-5" />
Application Settings
</CardTitle>
<CardDescription>
General application preferences and setup
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-3">
<Label>First-Time Setup</Label>
<Button
variant="outline"
onClick={() => {
localStorage.removeItem('onboarding-completed');
window.location.reload();
}}
className="w-full sm:w-auto"
>
<Settings className="w-4 h-4 mr-2" />
Run Setup Wizard Again
</Button>
<p className="text-sm text-muted-foreground">
Re-run the initial setup wizard to configure your preferences from scratch
</p>
</div>
</CardContent>
</Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">