feat: implement recently played albums and sidebar shortcut preferences
This commit is contained in:
@@ -1 +1 @@
|
|||||||
NEXT_PUBLIC_COMMIT_SHA=c1541e6
|
NEXT_PUBLIC_COMMIT_SHA=31aec81
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import { Button } from "../../components/ui/button";
|
|||||||
import { ScrollArea } from "../../components/ui/scroll-area";
|
import { ScrollArea } from "../../components/ui/scroll-area";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Playlist } from "@/lib/navidrome";
|
import { Playlist, Album } from "@/lib/navidrome";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { useNavidrome } from "./NavidromeContext";
|
import { useNavidrome } from "./NavidromeContext";
|
||||||
|
import { useRecentlyPlayedAlbums } from "@/hooks/use-recently-played-albums";
|
||||||
|
import { useSidebarShortcuts } from "@/hooks/use-sidebar-shortcuts";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
@@ -29,6 +31,8 @@ interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
export function Sidebar({ className, playlists, collapsed = false, onToggle, visible = true, favoriteAlbums = [], onRemoveFavoriteAlbum }: SidebarProps) {
|
export function Sidebar({ className, playlists, collapsed = false, onToggle, visible = true, favoriteAlbums = [], onRemoveFavoriteAlbum }: SidebarProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { api } = useNavidrome();
|
const { api } = useNavidrome();
|
||||||
|
const { recentAlbums } = useRecentlyPlayedAlbums();
|
||||||
|
const { showPlaylists, showAlbums } = useSidebarShortcuts();
|
||||||
|
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
return null;
|
return null;
|
||||||
@@ -337,7 +341,7 @@ export function Sidebar({ className, playlists, collapsed = false, onToggle, vis
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Favorite Albums Section */}
|
{/* Favorite Albums Section */}
|
||||||
{favoriteAlbums.length > 0 && (
|
{showAlbums && favoriteAlbums.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="border-t my-2"></div>
|
<div className="border-t my-2"></div>
|
||||||
{favoriteAlbums.slice(0, 5).map((album) => (
|
{favoriteAlbums.slice(0, 5).map((album) => (
|
||||||
@@ -393,9 +397,41 @@ export function Sidebar({ className, playlists, collapsed = false, onToggle, vis
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Recently Played Albums Section */}
|
||||||
|
{showAlbums && recentAlbums.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="border-t my-2"></div>
|
||||||
|
{recentAlbums.slice(0, 5).map((album) => {
|
||||||
|
const albumImageUrl = album.coverArt && api
|
||||||
|
? api.getCoverArtUrl(album.coverArt, 32)
|
||||||
|
: '/play.png';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link key={album.id} href={`/album/${album.id}`}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-center px-2 h-10"
|
||||||
|
title={`${album.name} - ${album.artist} (Recently Played)`}
|
||||||
|
>
|
||||||
|
<div className="w-6 h-6 rounded-sm overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={albumImageUrl}
|
||||||
|
alt={album.name}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Playlists Section */}
|
{/* Playlists Section */}
|
||||||
{playlists.length > 0 && (
|
{showPlaylists && playlists.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="border-t my-2"></div>
|
<div className="border-t my-2"></div>
|
||||||
{playlists.slice(0, 5).map((playlist) => {
|
{playlists.slice(0, 5).map((playlist) => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useTheme } from '@/app/components/ThemeProvider';
|
|||||||
import { useNavidromeConfig } from '@/app/components/NavidromeConfigContext';
|
import { useNavidromeConfig } from '@/app/components/NavidromeConfigContext';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useStandaloneLastFm } from '@/hooks/use-standalone-lastfm';
|
import { useStandaloneLastFm } from '@/hooks/use-standalone-lastfm';
|
||||||
|
import { useSidebarShortcuts, SidebarShortcutType } from '@/hooks/use-sidebar-shortcuts';
|
||||||
import { FaServer, FaUser, FaLock, FaCheck, FaTimes, FaLastfm, FaCog } from 'react-icons/fa';
|
import { FaServer, FaUser, FaLock, FaCheck, FaTimes, FaLastfm, FaCog } from 'react-icons/fa';
|
||||||
import { Settings, ExternalLink } from 'lucide-react';
|
import { Settings, ExternalLink } from 'lucide-react';
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ const SettingsPage = () => {
|
|||||||
const { config, updateConfig, isConnected, testConnection, clearConfig } = useNavidromeConfig();
|
const { config, updateConfig, isConnected, testConnection, clearConfig } = useNavidromeConfig();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { isEnabled: isStandaloneLastFmEnabled, getCredentials, getAuthUrl, getSessionKey } = useStandaloneLastFm();
|
const { isEnabled: isStandaloneLastFmEnabled, getCredentials, getAuthUrl, getSessionKey } = useStandaloneLastFm();
|
||||||
|
const { shortcutType, updateShortcutType } = useSidebarShortcuts();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
@@ -568,9 +570,29 @@ const SettingsPage = () => {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="sidebar-shortcuts">Sidebar Shortcuts</Label>
|
||||||
|
<Select
|
||||||
|
value={shortcutType}
|
||||||
|
onValueChange={(value: SidebarShortcutType) => updateShortcutType(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="sidebar-shortcuts">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="both">Albums & Playlists</SelectItem>
|
||||||
|
<SelectItem value="albums">Albums Only</SelectItem>
|
||||||
|
<SelectItem value="playlists">Playlists Only</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="text-sm text-muted-foreground space-y-2">
|
<div className="text-sm text-muted-foreground space-y-2">
|
||||||
<p><strong>Visible:</strong> Sidebar is always shown with icon navigation</p>
|
<p><strong>Visible:</strong> Sidebar is always shown with icon navigation</p>
|
||||||
<p><strong>Hidden:</strong> Sidebar is completely hidden for maximum space</p>
|
<p><strong>Hidden:</strong> Sidebar is completely hidden for maximum space</p>
|
||||||
|
<p><strong>Albums & Playlists:</strong> Show both favorite albums, recently played albums, and playlists as shortcuts</p>
|
||||||
|
<p><strong>Albums Only:</strong> Show only favorite and recently played albums as shortcuts</p>
|
||||||
|
<p><strong>Playlists Only:</strong> Show only playlists as shortcuts</p>
|
||||||
<p className="mt-3"><strong>Note:</strong> The sidebar now shows only icons with tooltips on hover for a cleaner interface.</p>
|
<p className="mt-3"><strong>Note:</strong> The sidebar now shows only icons with tooltips on hover for a cleaner interface.</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
36
hooks/use-recently-played-albums.ts
Normal file
36
hooks/use-recently-played-albums.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Album } from '@/lib/navidrome';
|
||||||
|
import { useNavidrome } from '@/app/components/NavidromeContext';
|
||||||
|
|
||||||
|
export function useRecentlyPlayedAlbums() {
|
||||||
|
const [recentAlbums, setRecentAlbums] = useState<Album[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const { api } = useNavidrome();
|
||||||
|
|
||||||
|
const fetchRecentAlbums = async () => {
|
||||||
|
if (!api) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const albums = await api.getAlbums('recent', 5, 0);
|
||||||
|
setRecentAlbums(albums);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch recent albums:', error);
|
||||||
|
setRecentAlbums([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRecentAlbums();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
recentAlbums,
|
||||||
|
loading,
|
||||||
|
refetch: fetchRecentAlbums
|
||||||
|
};
|
||||||
|
}
|
||||||
29
hooks/use-sidebar-shortcuts.ts
Normal file
29
hooks/use-sidebar-shortcuts.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export type SidebarShortcutType = 'playlists' | 'albums' | 'both';
|
||||||
|
|
||||||
|
export function useSidebarShortcuts() {
|
||||||
|
const [shortcutType, setShortcutType] = useState<SidebarShortcutType>('both');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Load preference from localStorage
|
||||||
|
const savedType = localStorage.getItem('sidebar-shortcut-type');
|
||||||
|
if (savedType && ['playlists', 'albums', 'both'].includes(savedType)) {
|
||||||
|
setShortcutType(savedType as SidebarShortcutType);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateShortcutType = (type: SidebarShortcutType) => {
|
||||||
|
setShortcutType(type);
|
||||||
|
localStorage.setItem('sidebar-shortcut-type', type);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
shortcutType,
|
||||||
|
updateShortcutType,
|
||||||
|
showPlaylists: shortcutType === 'playlists' || shortcutType === 'both',
|
||||||
|
showAlbums: shortcutType === 'albums' || shortcutType === 'both'
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user