feat: fix menubar, add lazy loading, improve image quality, limit search results, filter browse artists
This commit is contained in:
@@ -124,13 +124,13 @@ export default function AlbumPage() {
|
|||||||
// Dynamic cover art URLs based on image size
|
// Dynamic cover art URLs based on image size
|
||||||
const getMobileCoverArtUrl = () => {
|
const getMobileCoverArtUrl = () => {
|
||||||
return album.coverArt && api
|
return album.coverArt && api
|
||||||
? api.getCoverArtUrl(album.coverArt, 280)
|
? api.getCoverArtUrl(album.coverArt, 600)
|
||||||
: '/default-user.jpg';
|
: '/default-user.jpg';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDesktopCoverArtUrl = () => {
|
const getDesktopCoverArtUrl = () => {
|
||||||
return album.coverArt && api
|
return album.coverArt && api
|
||||||
? api.getCoverArtUrl(album.coverArt, 300)
|
? api.getCoverArtUrl(album.coverArt, 600)
|
||||||
: '/default-user.jpg';
|
: '/default-user.jpg';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,8 +146,8 @@ export default function AlbumPage() {
|
|||||||
<Image
|
<Image
|
||||||
src={getMobileCoverArtUrl()}
|
src={getMobileCoverArtUrl()}
|
||||||
alt={album.name}
|
alt={album.name}
|
||||||
width={280}
|
width={600}
|
||||||
height={280}
|
height={600}
|
||||||
className="rounded-md shadow-lg"
|
className="rounded-md shadow-lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,8 +182,8 @@ export default function AlbumPage() {
|
|||||||
<Image
|
<Image
|
||||||
src={getDesktopCoverArtUrl()}
|
src={getDesktopCoverArtUrl()}
|
||||||
alt={album.name}
|
alt={album.name}
|
||||||
width={300}
|
width={600}
|
||||||
height={300}
|
height={600}
|
||||||
className="rounded-md"
|
className="rounded-md"
|
||||||
/>
|
/>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ import Loading from '@/app/components/loading';
|
|||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
|
||||||
export default function BrowsePage() {
|
export default function BrowsePage() {
|
||||||
const { artists, isLoading: contextLoading } = useNavidrome();
|
const { artists: allArtists, isLoading: contextLoading } = useNavidrome();
|
||||||
|
// Filter to only show album artists (artists with at least one album)
|
||||||
|
const artists = allArtists.filter(artist => artist.albumCount && artist.albumCount > 0);
|
||||||
const { shuffleAllAlbums } = useAudioPlayer();
|
const { shuffleAllAlbums } = useAudioPlayer();
|
||||||
|
|
||||||
// Use our progressive loading hook
|
// Use our progressive loading hook
|
||||||
@@ -78,12 +80,13 @@ export default function BrowsePage() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<div className="flex space-x-4 pb-4">
|
<div className="flex space-x-4 pb-4">
|
||||||
{artists.map((artist) => (
|
{artists.map((artist, index) => (
|
||||||
<ArtistIcon
|
<ArtistIcon
|
||||||
key={artist.id}
|
key={artist.id}
|
||||||
artist={artist}
|
artist={artist}
|
||||||
className="shrink-0 overflow-hidden"
|
className="shrink-0 overflow-hidden"
|
||||||
size={190}
|
size={190}
|
||||||
|
loading={index < 10 ? 'eager' : 'lazy'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -110,7 +113,7 @@ export default function BrowsePage() {
|
|||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
<div className="h-full overflow-y-auto">
|
<div className="h-full overflow-y-auto">
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4 p-4 pb-8">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4 p-4 pb-8">
|
||||||
{albums.map((album) => (
|
{albums.map((album, index) => (
|
||||||
<AlbumArtwork
|
<AlbumArtwork
|
||||||
key={album.id}
|
key={album.id}
|
||||||
album={album}
|
album={album}
|
||||||
@@ -118,6 +121,7 @@ export default function BrowsePage() {
|
|||||||
aspectRatio="square"
|
aspectRatio="square"
|
||||||
width={200}
|
width={200}
|
||||||
height={200}
|
height={200}
|
||||||
|
loading={index < 20 ? 'eager' : 'lazy'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ interface AlbumArtworkProps extends Omit<
|
|||||||
aspectRatio?: "portrait" | "square"
|
aspectRatio?: "portrait" | "square"
|
||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
|
loading?: 'eager' | 'lazy'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AlbumArtwork({
|
export function AlbumArtwork({
|
||||||
@@ -43,6 +44,7 @@ export function AlbumArtwork({
|
|||||||
aspectRatio = "portrait",
|
aspectRatio = "portrait",
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
loading = 'lazy',
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: AlbumArtworkProps) {
|
}: AlbumArtworkProps) {
|
||||||
@@ -160,7 +162,7 @@ export function AlbumArtwork({
|
|||||||
onLoad={handleImageLoad}
|
onLoad={handleImageLoad}
|
||||||
onError={handleImageError}
|
onError={handleImageError}
|
||||||
priority={false}
|
priority={false}
|
||||||
loading="lazy"
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full bg-muted rounded flex items-center justify-center">
|
<div className="w-full h-full bg-muted rounded flex items-center justify-center">
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface ArtistIconProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
size?: number
|
size?: number
|
||||||
imageOnly?: boolean
|
imageOnly?: boolean
|
||||||
responsive?: boolean
|
responsive?: boolean
|
||||||
|
loading?: 'eager' | 'lazy'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ArtistIcon({
|
export function ArtistIcon({
|
||||||
@@ -34,6 +35,7 @@ export function ArtistIcon({
|
|||||||
size = 150,
|
size = 150,
|
||||||
imageOnly = false,
|
imageOnly = false,
|
||||||
responsive = false,
|
responsive = false,
|
||||||
|
loading = 'lazy',
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ArtistIconProps) {
|
}: ArtistIconProps) {
|
||||||
@@ -77,6 +79,7 @@ export function ArtistIcon({
|
|||||||
width={size}
|
width={size}
|
||||||
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"
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -116,6 +119,7 @@ export function ArtistIcon({
|
|||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
className={isResponsive ? "object-cover" : "object-cover w-full h-full"}
|
className={isResponsive ? "object-cover" : "object-cover w-full h-full"}
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -191,11 +191,8 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
|
|||||||
<MenubarMenu>
|
<MenubarMenu>
|
||||||
<MenubarTrigger className="relative">File</MenubarTrigger>
|
<MenubarTrigger className="relative">File</MenubarTrigger>
|
||||||
<MenubarContent>
|
<MenubarContent>
|
||||||
<MenubarSub>
|
<MenubarItem onClick={() => router.push('/library/playlists')}>
|
||||||
<MenubarSubTrigger>New</MenubarSubTrigger>
|
View Playlists
|
||||||
<MenubarSubContent className="w-[230px]">
|
|
||||||
<MenubarItem>
|
|
||||||
Playlist <MenubarShortcut>⌘N</MenubarShortcut>
|
|
||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
<MenubarItem disabled>
|
<MenubarItem disabled>
|
||||||
Playlist from Selection <MenubarShortcut>⇧⌘N</MenubarShortcut>
|
Playlist from Selection <MenubarShortcut>⇧⌘N</MenubarShortcut>
|
||||||
@@ -205,8 +202,6 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
|
|||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
<MenubarItem>Playlist Folder</MenubarItem>
|
<MenubarItem>Playlist Folder</MenubarItem>
|
||||||
<MenubarItem disabled>Genius Playlist</MenubarItem>
|
<MenubarItem disabled>Genius Playlist</MenubarItem>
|
||||||
</MenubarSubContent>
|
|
||||||
</MenubarSub>
|
|
||||||
<MenubarItem>
|
<MenubarItem>
|
||||||
Open Stream URL <MenubarShortcut>⌘U</MenubarShortcut>
|
Open Stream URL <MenubarShortcut>⌘U</MenubarShortcut>
|
||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
@@ -386,7 +381,7 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu
|
|||||||
) : navidromeUrl ? (
|
) : navidromeUrl ? (
|
||||||
navidromeUrl
|
navidromeUrl
|
||||||
) : (
|
) : (
|
||||||
<span className="italic text-gray-400">Not set</span>
|
<span className="italic text-gray-400">Auto-configured</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ export default function SearchPage() {
|
|||||||
try {
|
try {
|
||||||
setIsSearching(true);
|
setIsSearching(true);
|
||||||
const results = await search2(query);
|
const results = await search2(query);
|
||||||
setSearchResults(results);
|
// Limit results to 5 of each type
|
||||||
|
setSearchResults({
|
||||||
|
artists: results.artists.slice(0, 5),
|
||||||
|
albums: results.albums.slice(0, 5),
|
||||||
|
songs: results.songs.slice(0, 5)
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search failed:', error);
|
console.error('Search failed:', error);
|
||||||
setSearchResults({ artists: [], albums: [], songs: [] });
|
setSearchResults({ artists: [], albums: [], songs: [] });
|
||||||
|
|||||||
Reference in New Issue
Block a user