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:
@@ -1 +1 @@
|
||||
NEXT_PUBLIC_COMMIT_SHA=3ca162e
|
||||
NEXT_PUBLIC_COMMIT_SHA=bc159ac
|
||||
|
||||
@@ -23,7 +23,7 @@ const CHANGELOG = [
|
||||
'Improved search and browsing experience',
|
||||
'Added history tracking for played songs',
|
||||
'New Library Artist Page',
|
||||
'Artist page with top songs and albums',
|
||||
'Enhanced audio player with better controls',
|
||||
'Added settings page for customization options',
|
||||
'Introduced Whats New popup for version updates',
|
||||
'Improved UI consistency with new Badge component',
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import Image from "next/image"
|
||||
import { PlusCircledIcon } from "@radix-ui/react-icons"
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
ContextMenu,
|
||||
@@ -15,20 +14,23 @@ import {
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger,
|
||||
} from "../../components/ui/context-menu"
|
||||
|
||||
import { Artist } from "@/lib/navidrome"
|
||||
import { useNavidrome } from "./NavidromeContext"
|
||||
import { useAudioPlayer } from "@/app/components/AudioPlayerContext";
|
||||
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> {
|
||||
artist: Artist
|
||||
size?: number
|
||||
imageOnly?: boolean
|
||||
}
|
||||
|
||||
export function ArtistIcon({
|
||||
artist,
|
||||
size = 150,
|
||||
imageOnly = false,
|
||||
className,
|
||||
...props
|
||||
}: ArtistIconProps) {
|
||||
@@ -57,11 +59,59 @@ export function ArtistIcon({
|
||||
? api.getCoverArtUrl(artist.coverArt, 200)
|
||||
: '/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 (
|
||||
<div className={cn("space-y-3", className)} {...props}>
|
||||
<ContextMenu>
|
||||
<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"
|
||||
onClick={handleClick}
|
||||
style={{ width: size, height: size }}
|
||||
@@ -73,7 +123,7 @@ export function ArtistIcon({
|
||||
height={size}
|
||||
className="w-full h-full object-cover transition-all hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className="w-40">
|
||||
<ContextMenuItem onClick={handleStar}>
|
||||
@@ -117,10 +167,6 @@ export function ArtistIcon({
|
||||
<ContextMenuItem>Share</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />}
|
||||
</Button>
|
||||
|
||||
<div className={`space-y-4 py-4 ${collapsed ? "pt-6" : "" }`}>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="px-3 py-2">
|
||||
<p className={cn("mb-2 px-4 text-lg font-semibold tracking-tight", collapsed && "sr-only")}>
|
||||
Discover
|
||||
|
||||
@@ -7,17 +7,34 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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 { useNavidrome } from '@/app/components/NavidromeContext';
|
||||
import { Artist } from '@/lib/navidrome';
|
||||
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() {
|
||||
const { artists, isLoading } = useNavidrome();
|
||||
const { artists, isLoading, api, starItem, unstarItem } = useNavidrome();
|
||||
const [filteredArtists, setFilteredArtists] = useState<Artist[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
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(() => {
|
||||
if (artists.length > 0) {
|
||||
@@ -87,14 +104,32 @@ export default function ArtistPage() {
|
||||
<Separator className="my-4" />
|
||||
<div className="relative">
|
||||
<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) => (
|
||||
<ArtistIcon
|
||||
key={artist.id}
|
||||
artist={artist}
|
||||
className="flex justify-center"
|
||||
size={150}
|
||||
/>
|
||||
<Card key={artist.id} className="overflow-hidden">
|
||||
<div className="aspect-square relative group cursor-pointer" onClick={() => handleViewArtist(artist)}>
|
||||
<div className="w-full h-full">
|
||||
<Image
|
||||
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>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
|
||||
@@ -443,6 +443,37 @@ const SettingsPage = () => {
|
||||
</CardContent>
|
||||
</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>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user