'use client'; import React, { useState, useEffect } from 'react'; import { Label } from '@/components/ui/label'; import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { useAudioPlayer } from '@/app/components/AudioPlayerContext'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { useTheme } from '@/app/components/ThemeProvider'; import { useNavidromeConfig } from '@/app/components/NavidromeConfigContext'; import { useToast } from '@/hooks/use-toast'; import { useStandaloneLastFm } from '@/hooks/use-standalone-lastfm'; import { useSidebarShortcuts, SidebarShortcutType } from '@/hooks/use-sidebar-shortcuts'; import { SidebarCustomization } from '@/app/components/SidebarCustomization'; import { SettingsManagement } from '@/app/components/SettingsManagement'; import EnhancedOfflineManager from '@/app/components/EnhancedOfflineManager'; import { AutoTaggingSettings } from '@/app/components/AutoTaggingSettings'; import { FaServer, FaUser, FaLock, FaCheck, FaTimes, FaLastfm, FaCog, FaTags } from 'react-icons/fa'; import { Settings, ExternalLink, Tag } from 'lucide-react'; import { Switch } from '@/components/ui/switch'; const SettingsPage = () => { const { theme, setTheme, mode, setMode } = useTheme(); const { config, updateConfig, isConnected, testConnection, clearConfig } = useNavidromeConfig(); const { toast } = useToast(); const { isEnabled: isStandaloneLastFmEnabled, getCredentials, getAuthUrl, getSessionKey } = useStandaloneLastFm(); const { shortcutType, updateShortcutType } = useSidebarShortcuts(); const audioPlayer = useAudioPlayer(); const [formData, setFormData] = useState({ serverUrl: '', username: '', password: '' }); const [isTesting, setIsTesting] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // Last.fm scrobbling settings (Navidrome integration) const [scrobblingEnabled, setScrobblingEnabled] = useState(true); // Standalone Last.fm settings const [standaloneLastFmEnabled, setStandaloneLastFmEnabled] = useState(false); const [lastFmCredentials, setLastFmCredentials] = useState({ apiKey: '', apiSecret: '', sessionKey: '', username: '' }); // Client-side hydration state const [isClient, setIsClient] = useState(false); // Check if Navidrome is configured via environment variables const hasEnvConfig = React.useMemo(() => { return !!(process.env.NEXT_PUBLIC_NAVIDROME_URL && process.env.NEXT_PUBLIC_NAVIDROME_USERNAME && process.env.NEXT_PUBLIC_NAVIDROME_PASSWORD); }, []); // Sidebar settings const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [sidebarVisible, setSidebarVisible] = useState(true); const [notifyNowPlaying, setNotifyNowPlaying] = useState(false); // Initialize client-side state after hydration useEffect(() => { setIsClient(true); // Initialize form data with config values setFormData({ serverUrl: config.serverUrl || '', username: config.username || '', password: config.password || '' }); // Load saved preferences from localStorage const savedScrobbling = localStorage.getItem('lastfm-scrobbling-enabled'); if (savedScrobbling !== null) { setScrobblingEnabled(savedScrobbling === 'true'); } const savedStandaloneLastFm = localStorage.getItem('standalone-lastfm-enabled'); if (savedStandaloneLastFm !== null) { setStandaloneLastFmEnabled(savedStandaloneLastFm === 'true'); } const savedSidebarCollapsed = localStorage.getItem('sidebar-collapsed'); if (savedSidebarCollapsed !== null) { setSidebarCollapsed(savedSidebarCollapsed === 'true'); } const savedSidebarVisible = localStorage.getItem('sidebar-visible'); if (savedSidebarVisible !== null) { setSidebarVisible(savedSidebarVisible === 'true'); } else { setSidebarVisible(true); // Default to visible } // Notifications preference const savedNotify = localStorage.getItem('playback-notifications-enabled'); if (savedNotify !== null) { setNotifyNowPlaying(savedNotify === 'true'); } // Load Last.fm credentials const storedCredentials = localStorage.getItem('lastfm-credentials'); if (storedCredentials) { try { const credentials = JSON.parse(storedCredentials); setLastFmCredentials({ apiKey: credentials.apiKey || '', apiSecret: credentials.apiSecret || '', sessionKey: credentials.sessionKey || '', username: credentials.username || '' }); } catch (error) { console.error('Failed to parse stored Last.fm credentials:', error); } } }, [config.serverUrl, config.username, config.password]); const handleInputChange = (field: string, value: string) => { setFormData(prev => ({ ...prev, [field]: value })); setHasUnsavedChanges(true); }; const handleTestConnection = async () => { if (!formData.serverUrl || !formData.username || !formData.password) { toast({ title: "Missing Information", description: "Please fill in all fields before testing the connection.", variant: "destructive" }); return; } setIsTesting(true); try { // Strip trailing slash from server URL before testing const cleanServerUrl = formData.serverUrl.replace(/\/+$/, ''); const success = await testConnection({ serverUrl: cleanServerUrl, username: formData.username, password: formData.password }); if (success) { toast({ title: "Connection Successful", description: "Successfully connected to Navidrome server.", }); } else { toast({ title: "Connection Failed", description: "Could not connect to the server. Please check your settings.", variant: "destructive" }); } } catch (error) { toast({ title: "Connection Error", description: "An error occurred while testing the connection.", variant: "destructive" }); } finally { setIsTesting(false); } }; const handleSaveConfig = async () => { if (!formData.serverUrl || !formData.username || !formData.password) { toast({ title: "Missing Information", description: "Please fill in all fields.", variant: "destructive" }); return; } // Strip trailing slash from server URL to ensure consistency const cleanServerUrl = formData.serverUrl.replace(/\/+$/, ''); updateConfig({ serverUrl: cleanServerUrl, username: formData.username, password: formData.password }); // Update form data to reflect the cleaned URL setFormData(prev => ({ ...prev, serverUrl: cleanServerUrl })); setHasUnsavedChanges(false); toast({ title: "Settings Saved", description: "Navidrome configuration has been saved.", }); }; const handleClearConfig = () => { clearConfig(); setFormData({ serverUrl: '', username: '', password: '' }); setHasUnsavedChanges(false); toast({ title: "Configuration Cleared", description: "Navidrome configuration has been cleared.", }); }; const handleScrobblingToggle = (enabled: boolean) => { setScrobblingEnabled(enabled); if (isClient) { localStorage.setItem('lastfm-scrobbling-enabled', enabled.toString()); } toast({ title: enabled ? "Scrobbling Enabled" : "Scrobbling Disabled", description: enabled ? "Tracks will now be scrobbled to Last.fm via Navidrome" : "Last.fm scrobbling has been disabled", }); }; const handleStandaloneLastFmToggle = (enabled: boolean) => { setStandaloneLastFmEnabled(enabled); if (isClient) { localStorage.setItem('standalone-lastfm-enabled', enabled.toString()); } toast({ title: enabled ? "Standalone Last.fm Enabled" : "Standalone Last.fm Disabled", description: enabled ? "Direct Last.fm integration enabled" : "Standalone Last.fm integration disabled", }); }; const handleSidebarToggle = (collapsed: boolean) => { setSidebarCollapsed(collapsed); if (isClient) { localStorage.setItem('sidebar-collapsed', collapsed.toString()); } toast({ title: collapsed ? "Sidebar Collapsed" : "Sidebar Expanded", description: collapsed ? "Sidebar will show only icons" : "Sidebar will show full labels", }); // Trigger a custom event to notify the sidebar component if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('sidebar-toggle', { detail: { collapsed } })); } }; const handleSidebarVisibilityToggle = (visible: boolean) => { setSidebarVisible(visible); if (isClient) { localStorage.setItem('sidebar-visible', visible.toString()); } toast({ title: visible ? "Sidebar Shown" : "Sidebar Hidden", description: visible ? "Sidebar is now visible" : "Sidebar is now hidden", }); // Trigger a custom event to notify the sidebar component if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('sidebar-visibility-toggle', { detail: { visible } })); } }; const handleNotifyToggle = async (enabled: boolean) => { setNotifyNowPlaying(enabled); if (isClient) { localStorage.setItem('playback-notifications-enabled', enabled.toString()); } if (enabled && typeof window !== 'undefined' && 'Notification' in window) { try { if (Notification.permission === 'default') { await Notification.requestPermission(); } } catch {} } toast({ title: enabled ? 'Notifications Enabled' : 'Notifications Disabled', description: enabled ? 'You will be notified when a new song starts.' : 'Now playing notifications are off.', }); }; const handleTestNotification = () => { if (typeof window === 'undefined') return; if (!('Notification' in window)) { toast({ title: 'Not supported', description: 'Browser does not support notifications.', variant: 'destructive' }); return; } if (Notification.permission === 'denied') { toast({ title: 'Permission denied', description: 'Enable notifications in your browser settings.', variant: 'destructive' }); return; } const title = 'mice – Test Notification'; const body = 'This is how a now playing notification will look.'; try { new Notification(title, { body, icon: '/icon-192.png', badge: '/icon-192.png' }); } catch { toast({ title: 'Test Notification', description: body }); } }; const handleLastFmAuth = () => { if (!lastFmCredentials.apiKey) { toast({ title: "API Key Required", description: "Please enter your Last.fm API key first.", variant: "destructive" }); return; } const authUrl = getAuthUrl(lastFmCredentials.apiKey); window.open(authUrl, '_blank'); toast({ title: "Last.fm Authorization", description: "Please authorize the application in the opened window and return here.", }); }; const handleLastFmCredentialsSave = () => { if (!lastFmCredentials.apiKey || !lastFmCredentials.apiSecret) { toast({ title: "Missing Credentials", description: "Please enter both API key and secret.", variant: "destructive" }); return; } if (isClient) { localStorage.setItem('lastfm-credentials', JSON.stringify(lastFmCredentials)); } toast({ title: "Credentials Saved", description: "Last.fm credentials have been saved locally.", }); }; const handleLastFmSessionComplete = async (token: string) => { try { const { sessionKey, username } = await getSessionKey( token, lastFmCredentials.apiKey, lastFmCredentials.apiSecret ); const updatedCredentials = { ...lastFmCredentials, sessionKey, username }; setLastFmCredentials(updatedCredentials); if (isClient) { localStorage.setItem('lastfm-credentials', JSON.stringify(updatedCredentials)); } toast({ title: "Last.fm Authentication Complete", description: `Successfully authenticated as ${username}`, }); } catch (error) { toast({ title: "Authentication Failed", description: error instanceof Error ? error.message : "Failed to complete Last.fm authentication", variant: "destructive" }); } }; return (
Loading...
Customize your music experience
Note: Your credentials are stored locally in your browser
Security: Always use HTTPS for your Navidrome server
Configured via Environment Variables
Server: {process.env.NEXT_PUBLIC_NAVIDROME_URL}
Username: {process.env.NEXT_PUBLIC_NAVIDROME_USERNAME}
Your Navidrome connection is configured through environment variables. Contact your administrator to change these settings.
Now playing notifications
Show a notification when a new song starts
How it works:
Note: Last.fm credentials must be configured in Navidrome, not here.
Re-run the initial setup wizard to configure your preferences from scratch
Visible: Sidebar is always shown with icon navigation
Hidden: Sidebar is completely hidden for maximum space
Albums & Playlists: Show both favorite albums, recently played albums, and playlists as shortcuts
Albums Only: Show only favorite and recently played albums as shortcuts
Playlists Only: Show only playlists as shortcuts
Note: The sidebar now shows only icons with tooltips on hover for a cleaner interface.
Setup Instructions:
Features:
Theme: Choose from multiple color schemes including default (white)
Display Mode: Choose light, dark, or system (follows your device preferences)
Normalize volume across tracks
Seamless transitions between tracks
Crossfade: Smooth fade between tracks (2-5 seconds)
Equalizer: Preset frequency adjustments for different music styles
ReplayGain: Consistent volume across all tracks in your library
Gapless: Perfect for live albums and continuous DJ mixes
Sample Song Title
Sample Artist
This will clear all localStorage data except your Navidrome server configuration, then reload the page.