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',
|
'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',
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user