feat: add Tooltip component and related hooks for improved UI interactions
- Implemented Tooltip component using Radix UI for better accessibility and customization. - Created TooltipProvider, TooltipTrigger, and TooltipContent for modular usage. - Added useIsMobile hook to detect mobile devices based on screen width. - Updated themes with new color variables for better design consistency across the application.
This commit is contained in:
@@ -21,14 +21,7 @@ import {
|
||||
FaListUl
|
||||
} from "react-icons/fa6";
|
||||
import { Heart } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu";
|
||||
|
||||
interface LyricLine {
|
||||
time: number;
|
||||
@@ -294,10 +287,9 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
||||
|
||||
{/* Overlay for better contrast */}
|
||||
<div className="absolute inset-0 bg-black/50" />
|
||||
<div className="relative h-full w-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 lg:p-6 shrink-0">
|
||||
<h2 className="text-lg lg:text-xl font-semibold text-white"></h2>
|
||||
<div className="relative h-full w-full">
|
||||
{/* Floating Header */}
|
||||
<div className="absolute top-0 right-0 z-50 p-4 lg:p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
{onOpenQueue && (
|
||||
<button
|
||||
@@ -319,7 +311,7 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col lg:flex-row gap-4 lg:gap-8 p-4 lg:p-6 pt-0 overflow-hidden min-h-0">
|
||||
<div className="h-full flex flex-col lg:flex-row gap-4 lg:gap-8 p-4 lg:p-6 overflow-hidden">
|
||||
{/* Left Side - Album Art and Controls */}
|
||||
<div className="flex flex-col items-center justify-center min-h-0 flex-1 min-w-0">
|
||||
{/* Album Art */}
|
||||
@@ -453,7 +445,7 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
||||
<div className="flex-1 min-w-0 min-h-0 flex flex-col" ref={lyricsRef}>
|
||||
<div className="h-full flex flex-col">
|
||||
<ScrollArea className="flex-1 min-h-0">
|
||||
<div className="space-y-3 sm:space-y-4 pl-12 pr-4 py-4">
|
||||
<div className="space-y-2 sm:space-y-3 pl-4 pr-4 py-4">
|
||||
{lyrics.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
@@ -461,7 +453,7 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
||||
onClick={() => handleLyricClick(line.time)}
|
||||
className={`text-sm sm:text-base lg:text-base leading-relaxed transition-all duration-300 break-words cursor-pointer hover:text-foreground ${
|
||||
index === currentLyricIndex
|
||||
? 'text-foreground font-bold text-lg sm:text-xl lg:text-2xl'
|
||||
? 'text-foreground font-bold text-2xl'
|
||||
: index < currentLyricIndex
|
||||
? 'text-foreground/60'
|
||||
: 'text-foreground/40'
|
||||
@@ -470,8 +462,8 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
hyphens: 'auto',
|
||||
paddingBottom: '6px',
|
||||
paddingLeft: '16px'
|
||||
paddingBottom: '4px',
|
||||
paddingLeft: '8px'
|
||||
}}
|
||||
title={`Click to jump to ${formatTime(line.time)}`}
|
||||
>
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
|
||||
type Theme = 'blue' | 'violet' | 'red' | 'rose' | 'orange' | 'green' | 'yellow';
|
||||
type Theme = 'default' | 'blue' | 'violet' | 'red' | 'rose' | 'orange' | 'green' | 'yellow';
|
||||
type Mode = 'light' | 'dark' | 'system';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
mode: Mode;
|
||||
setTheme: (theme: Theme) => void;
|
||||
setMode: (mode: Mode) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
@@ -25,18 +27,25 @@ interface ThemeProviderProps {
|
||||
}
|
||||
|
||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
const [theme, setTheme] = useState<Theme>('blue');
|
||||
const [theme, setTheme] = useState<Theme>('default');
|
||||
const [mode, setMode] = useState<Mode>('system');
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// Load theme settings from localStorage on component mount
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const validThemes: Theme[] = ['blue', 'violet', 'red', 'rose', 'orange', 'green', 'yellow'];
|
||||
const savedMode = localStorage.getItem('theme-mode');
|
||||
const validThemes: Theme[] = ['default', 'blue', 'violet', 'red', 'rose', 'orange', 'green', 'yellow'];
|
||||
const validModes: Mode[] = ['light', 'dark', 'system'];
|
||||
|
||||
if (savedTheme && validThemes.includes(savedTheme as Theme)) {
|
||||
setTheme(savedTheme as Theme);
|
||||
}
|
||||
|
||||
if (savedMode && validModes.includes(savedMode as Mode)) {
|
||||
setMode(savedMode as Mode);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Apply theme changes
|
||||
@@ -46,35 +55,54 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Remove existing theme classes
|
||||
root.classList.remove('theme-blue', 'theme-violet', 'theme-red', 'theme-rose', 'theme-orange', 'theme-green', 'theme-yellow', 'dark');
|
||||
root.classList.remove('theme-default', 'theme-blue', 'theme-violet', 'theme-red', 'theme-rose', 'theme-orange', 'theme-green', 'theme-yellow', 'dark');
|
||||
|
||||
// Add new theme class
|
||||
root.classList.add(`theme-${theme}`);
|
||||
|
||||
// Always follow system preference for dark mode
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const applySystemTheme = () => {
|
||||
root.classList.toggle('dark', mediaQuery.matches);
|
||||
// Apply dark/light mode
|
||||
const applyMode = () => {
|
||||
if (mode === 'dark') {
|
||||
root.classList.add('dark');
|
||||
} else if (mode === 'light') {
|
||||
root.classList.remove('dark');
|
||||
} else { // system
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
root.classList.toggle('dark', mediaQuery.matches);
|
||||
}
|
||||
};
|
||||
|
||||
applySystemTheme();
|
||||
mediaQuery.addEventListener('change', applySystemTheme);
|
||||
applyMode();
|
||||
|
||||
// Save theme to localStorage
|
||||
// Listen for system preference changes only if mode is 'system'
|
||||
let mediaQuery: MediaQueryList | null = null;
|
||||
if (mode === 'system') {
|
||||
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mediaQuery.addEventListener('change', applyMode);
|
||||
}
|
||||
|
||||
// Save settings to localStorage
|
||||
localStorage.setItem('theme', theme);
|
||||
localStorage.setItem('theme-mode', mode);
|
||||
|
||||
// Cleanup listener
|
||||
return () => mediaQuery.removeEventListener('change', applySystemTheme);
|
||||
}, [theme, mounted]);
|
||||
return () => {
|
||||
if (mediaQuery) {
|
||||
mediaQuery.removeEventListener('change', applyMode);
|
||||
}
|
||||
};
|
||||
}, [theme, mode, mounted]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
theme,
|
||||
mode,
|
||||
setTheme,
|
||||
setMode,
|
||||
}}
|
||||
>
|
||||
<div className={`theme-${theme}`}>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
</ThemeContext.Provider>
|
||||
|
||||
@@ -112,7 +112,7 @@ export function AlbumArtwork({
|
||||
<div className={cn("space-y-3", className)} {...props}>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<Card key={album.id} className="overflow-hidden cursor-pointer" onClick={() => handleClick()}>
|
||||
<Card key={album.id} className="overflow-hidden cursor-pointer px-0 py-0 gap-0" onClick={() => handleClick()}>
|
||||
<div className="aspect-square relative group">
|
||||
{album.coverArt && api ? (
|
||||
<Image
|
||||
|
||||
@@ -25,12 +25,14 @@ interface ArtistIconProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
artist: Artist
|
||||
size?: number
|
||||
imageOnly?: boolean
|
||||
responsive?: boolean
|
||||
}
|
||||
|
||||
export function ArtistIcon({
|
||||
artist,
|
||||
size = 150,
|
||||
imageOnly = false,
|
||||
responsive = false,
|
||||
className,
|
||||
...props
|
||||
}: ArtistIconProps) {
|
||||
@@ -54,9 +56,9 @@ export function ArtistIcon({
|
||||
starItem(artist.id, 'artist');
|
||||
}
|
||||
};
|
||||
// Get cover art URL with proper fallback
|
||||
// Get cover art URL with proper fallback - use higher resolution for better quality
|
||||
const artistImageUrl = artist.coverArt && api
|
||||
? api.getCoverArtUrl(artist.coverArt, 200)
|
||||
? api.getCoverArtUrl(artist.coverArt, 320)
|
||||
: '/default-user.jpg';
|
||||
|
||||
// If imageOnly is true, return just the image without context menu or text
|
||||
@@ -79,22 +81,33 @@ export function ArtistIcon({
|
||||
);
|
||||
}
|
||||
|
||||
// Determine if we should use responsive layout
|
||||
const isResponsive = responsive;
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-3", className)} {...props}>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<Card key={artist.id} className="overflow-hidden cursor-pointer" onClick={() => handleClick()}>
|
||||
<Card key={artist.id} className="overflow-hidden cursor-pointer px-0 py-0 gap-0" onClick={() => handleClick()}>
|
||||
<div
|
||||
className="aspect-square relative group"
|
||||
style={{ width: size, height: size }}
|
||||
style={!isResponsive ? { width: size, height: size } : undefined}
|
||||
>
|
||||
<div className="w-full h-full">
|
||||
<Image
|
||||
src={artist.coverArt && api ? api.getCoverArtUrl(artist.coverArt, 200) : '/placeholder-artist.png'}
|
||||
src={artist.coverArt && api ? api.getCoverArtUrl(artist.coverArt, 600) : '/placeholder-artist.png'}
|
||||
alt={artist.name}
|
||||
width={size}
|
||||
height={size}
|
||||
className="object-cover w-full h-full"
|
||||
{...(isResponsive
|
||||
? {
|
||||
fill: true,
|
||||
sizes: "(max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
|
||||
}
|
||||
: {
|
||||
width: size,
|
||||
height: size
|
||||
}
|
||||
)}
|
||||
className={isResponsive ? "object-cover" : "object-cover w-full h-full"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,19 +118,6 @@ export function ArtistIcon({
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* <div
|
||||
className="overflow-hidden rounded-full cursor-pointer shrink-0"
|
||||
onClick={handleClick}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<Image
|
||||
src={artistImageUrl}
|
||||
alt={artist.name}
|
||||
width={size}
|
||||
height={size}
|
||||
className="w-full h-full object-cover transition-all hover:scale-105"
|
||||
/>
|
||||
</div> */}
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className="w-40">
|
||||
<ContextMenuItem onClick={handleStar}>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useNavidrome } from "@/app/components/NavidromeContext";
|
||||
import { AlbumArtwork } from "@/app/components/album-artwork";
|
||||
import { ArtistIcon } from "@/app/components/artist-icon";
|
||||
@@ -167,33 +166,14 @@ const FavoritesPage = () => {
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
{favoriteAlbums.map((album) => (
|
||||
<Card key={album.id} className="overflow-hidden">
|
||||
<div className="aspect-square relative group">
|
||||
{album.coverArt && api ? (
|
||||
<Image
|
||||
src={api.getCoverArtUrl(album.coverArt)}
|
||||
alt={album.name}
|
||||
fill
|
||||
className="w-full h-full object-cover rounded"
|
||||
sizes="(max-width: 768px) 100vw, 300px"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-muted rounded flex items-center justify-center">
|
||||
<Disc className="w-12 h-12 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
|
||||
<Play className="w-12 h-12 mx-auto hidden group-hover:block" onClick={() => handlePlayAlbum(album)}/>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent className="p-4">
|
||||
<h3 className="font-semibold truncate">{album.name}</h3>
|
||||
<p className="text-sm text-muted-foreground truncate">{album.artist}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{album.songCount} songs • {Math.floor(album.duration / 60)} min
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AlbumArtwork
|
||||
key={album.id}
|
||||
album={album}
|
||||
className="w-full"
|
||||
aspectRatio="square"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -271,23 +251,7 @@ const FavoritesPage = () => {
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7">
|
||||
{favoriteArtists.map((artist) => (
|
||||
<Card key={artist.id} className="overflow-hidden">
|
||||
<CardContent className="p-3 text-center">
|
||||
<div className="w-24 h-24 mx-auto mb-4">
|
||||
<Image
|
||||
src={artist.coverArt && api ? api.getCoverArtUrl(artist.coverArt, 200) : '/placeholder-artist.png'}
|
||||
alt={artist.name}
|
||||
width={250}
|
||||
height={250}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="font-semibold truncate">{artist.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{artist.albumCount} albums
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<ArtistIcon key={artist.id} artist={artist} responsive />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
994
app/globals.css
994
app/globals.css
File diff suppressed because it is too large
Load Diff
@@ -7,13 +7,11 @@ 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, Heart } from 'lucide-react';
|
||||
import { Search } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
|
||||
@@ -106,27 +104,7 @@ export default function ArtistPage() {
|
||||
<ScrollArea>
|
||||
<div className="grid gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 cursor-pointer">
|
||||
{filteredArtists.map((artist) => (
|
||||
<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">
|
||||
</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>
|
||||
<ArtistIcon key={artist.id} artist={artist} responsive />
|
||||
))}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function SearchPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full px-4 py-6 lg:px-8">
|
||||
<div className="h-full px-4 py-6 lg:px-8 pb-32">
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="space-y-1">
|
||||
@@ -136,19 +136,25 @@ export default function SearchPage() {
|
||||
)}
|
||||
|
||||
{/* Artists */}
|
||||
{searchResults.artists.length > 0 && (
|
||||
{/* {searchResults.artists.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Artists</h2>
|
||||
<ScrollArea className="w-full">
|
||||
<div className="flex space-x-4 pb-4">
|
||||
{searchResults.artists.map((artist) => (
|
||||
<ArtistIcon key={artist.id} artist={artist} className="shrink-0" />
|
||||
<ArtistIcon
|
||||
key={artist.id}
|
||||
artist={artist}
|
||||
className="shrink-0 overflow-hidden"
|
||||
size={190}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
{/* broken for now */}
|
||||
|
||||
{/* Albums */}
|
||||
{searchResults.albums.length > 0 && (
|
||||
|
||||
@@ -14,34 +14,24 @@ import { FaServer, FaUser, FaLock, FaCheck, FaTimes, FaLastfm, FaCog } from 'rea
|
||||
import { Settings, ExternalLink } from 'lucide-react';
|
||||
|
||||
const SettingsPage = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { theme, setTheme, mode, setMode } = useTheme();
|
||||
const { config, updateConfig, isConnected, testConnection, clearConfig } = useNavidromeConfig();
|
||||
const { toast } = useToast();
|
||||
const { isEnabled: isStandaloneLastFmEnabled, getCredentials, getAuthUrl, getSessionKey } = useStandaloneLastFm();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
serverUrl: config.serverUrl,
|
||||
username: config.username,
|
||||
password: config.password
|
||||
serverUrl: '',
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
|
||||
// Last.fm scrobbling settings (Navidrome integration)
|
||||
const [scrobblingEnabled, setScrobblingEnabled] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('lastfm-scrobbling-enabled') === 'true';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const [scrobblingEnabled, setScrobblingEnabled] = useState(true);
|
||||
|
||||
// Standalone Last.fm settings
|
||||
const [standaloneLastFmEnabled, setStandaloneLastFmEnabled] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('standalone-lastfm-enabled') === 'true';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const [standaloneLastFmEnabled, setStandaloneLastFmEnabled] = useState(false);
|
||||
|
||||
const [lastFmCredentials, setLastFmCredentials] = useState({
|
||||
apiKey: '',
|
||||
@@ -50,6 +40,9 @@ const SettingsPage = () => {
|
||||
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 &&
|
||||
@@ -58,25 +51,51 @@ const SettingsPage = () => {
|
||||
}, []);
|
||||
|
||||
// Sidebar settings
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('sidebar-collapsed') === 'true';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
|
||||
// Load Last.fm credentials on mount
|
||||
// Initialize client-side state after hydration
|
||||
useEffect(() => {
|
||||
const credentials = getCredentials();
|
||||
if (credentials) {
|
||||
setLastFmCredentials({
|
||||
apiKey: credentials.apiKey,
|
||||
apiSecret: credentials.apiSecret,
|
||||
sessionKey: credentials.sessionKey || '',
|
||||
username: credentials.username || ''
|
||||
});
|
||||
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');
|
||||
}
|
||||
}, [getCredentials]);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// 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 }));
|
||||
@@ -171,7 +190,9 @@ const SettingsPage = () => {
|
||||
|
||||
const handleScrobblingToggle = (enabled: boolean) => {
|
||||
setScrobblingEnabled(enabled);
|
||||
localStorage.setItem('lastfm-scrobbling-enabled', enabled.toString());
|
||||
if (isClient) {
|
||||
localStorage.setItem('lastfm-scrobbling-enabled', enabled.toString());
|
||||
}
|
||||
toast({
|
||||
title: enabled ? "Scrobbling Enabled" : "Scrobbling Disabled",
|
||||
description: enabled
|
||||
@@ -182,7 +203,9 @@ const SettingsPage = () => {
|
||||
|
||||
const handleStandaloneLastFmToggle = (enabled: boolean) => {
|
||||
setStandaloneLastFmEnabled(enabled);
|
||||
localStorage.setItem('standalone-lastfm-enabled', enabled.toString());
|
||||
if (isClient) {
|
||||
localStorage.setItem('standalone-lastfm-enabled', enabled.toString());
|
||||
}
|
||||
toast({
|
||||
title: enabled ? "Standalone Last.fm Enabled" : "Standalone Last.fm Disabled",
|
||||
description: enabled
|
||||
@@ -193,7 +216,9 @@ const SettingsPage = () => {
|
||||
|
||||
const handleSidebarToggle = (collapsed: boolean) => {
|
||||
setSidebarCollapsed(collapsed);
|
||||
localStorage.setItem('sidebar-collapsed', collapsed.toString());
|
||||
if (isClient) {
|
||||
localStorage.setItem('sidebar-collapsed', collapsed.toString());
|
||||
}
|
||||
toast({
|
||||
title: collapsed ? "Sidebar Collapsed" : "Sidebar Expanded",
|
||||
description: collapsed
|
||||
@@ -202,7 +227,9 @@ const SettingsPage = () => {
|
||||
});
|
||||
|
||||
// Trigger a custom event to notify the sidebar component
|
||||
window.dispatchEvent(new CustomEvent('sidebar-toggle', { detail: { collapsed } }));
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('sidebar-toggle', { detail: { collapsed } }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleLastFmAuth = () => {
|
||||
@@ -234,7 +261,9 @@ const SettingsPage = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('lastfm-credentials', JSON.stringify(lastFmCredentials));
|
||||
if (isClient) {
|
||||
localStorage.setItem('lastfm-credentials', JSON.stringify(lastFmCredentials));
|
||||
}
|
||||
toast({
|
||||
title: "Credentials Saved",
|
||||
description: "Last.fm credentials have been saved locally.",
|
||||
@@ -256,7 +285,9 @@ const SettingsPage = () => {
|
||||
};
|
||||
|
||||
setLastFmCredentials(updatedCredentials);
|
||||
localStorage.setItem('lastfm-credentials', JSON.stringify(updatedCredentials));
|
||||
if (isClient) {
|
||||
localStorage.setItem('lastfm-credentials', JSON.stringify(updatedCredentials));
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Last.fm Authentication Complete",
|
||||
@@ -273,11 +304,19 @@ const SettingsPage = () => {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 pb-24 max-w-2xl">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Settings</h1>
|
||||
<p className="text-muted-foreground">Customize your music experience</p>
|
||||
{!isClient ? (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Settings</h1>
|
||||
<p className="text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Settings</h1>
|
||||
<p className="text-muted-foreground">Customize your music experience</p>
|
||||
</div>
|
||||
|
||||
{!hasEnvConfig && (
|
||||
<Card>
|
||||
@@ -619,6 +658,7 @@ const SettingsPage = () => {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">Default</SelectItem>
|
||||
<SelectItem value="blue">Blue</SelectItem>
|
||||
<SelectItem value="violet">Violet</SelectItem>
|
||||
<SelectItem value="red">Red</SelectItem>
|
||||
@@ -630,9 +670,23 @@ const SettingsPage = () => {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mode-select">Display Mode</Label>
|
||||
<Select value={mode} onValueChange={setMode}>
|
||||
<SelectTrigger id="mode-select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p><strong>Theme:</strong> Choose between blue and violet color schemes</p>
|
||||
<p><strong>Dark Mode:</strong> Automatically follows your system preferences</p>
|
||||
<p><strong>Theme:</strong> Choose from multiple color schemes including default (white)</p>
|
||||
<p><strong>Display Mode:</strong> Choose light, dark, or system (follows your device preferences)</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -666,7 +720,8 @@ const SettingsPage = () => {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user