mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-08 20:59:47 +10:00
UI updates
This commit is contained in:
+33
-22
@@ -18,22 +18,30 @@ function App() {
|
||||
const [currentMusic, setCurrentMusic] = useState<Music | null>(null)
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [playlist, setPlaylist] = useState<Music[]>([])
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('dark')
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
|
||||
// Initialize from localStorage or system preference
|
||||
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null
|
||||
if (savedTheme) {
|
||||
return savedTheme
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
})
|
||||
const [playMode, setPlayMode] = useState<'loop' | 'shuffle' | 'repeat-one'>('loop')
|
||||
const audioRef = useRef<HTMLAudioElement>(null)
|
||||
const location = useLocation()
|
||||
|
||||
// Load theme from localStorage on mount
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null
|
||||
if (savedTheme) {
|
||||
setTheme(savedTheme)
|
||||
} else {
|
||||
// Default to system preference
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
setTheme(prefersDark ? 'dark' : 'light')
|
||||
}
|
||||
}, [])
|
||||
// Load theme from localStorage on mount - NO LONGER NEEDED, handled in initial state
|
||||
// useEffect(() => {
|
||||
// const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null
|
||||
// console.log('Loading theme from localStorage:', savedTheme)
|
||||
// if (savedTheme) {
|
||||
// setTheme(savedTheme)
|
||||
// } else {
|
||||
// // Default to system preference
|
||||
// const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
// setTheme(prefersDark ? 'dark' : 'light')
|
||||
// }
|
||||
// }, [])
|
||||
|
||||
// Load play mode from localStorage
|
||||
useEffect(() => {
|
||||
@@ -45,12 +53,14 @@ function App() {
|
||||
|
||||
// Apply theme to document
|
||||
useEffect(() => {
|
||||
console.log('Applying theme:', theme)
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
localStorage.setItem('theme', theme)
|
||||
console.log('Theme saved to localStorage:', theme)
|
||||
}, [theme])
|
||||
|
||||
// Save play mode to localStorage
|
||||
@@ -58,8 +68,9 @@ function App() {
|
||||
localStorage.setItem('playMode', playMode)
|
||||
}, [playMode])
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prev => prev === 'dark' ? 'light' : 'dark')
|
||||
const handleThemeChange = (isDark: boolean) => {
|
||||
console.log('handleThemeChange called with:', isDark)
|
||||
setTheme(isDark ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
const togglePlayMode = () => {
|
||||
@@ -93,7 +104,7 @@ function App() {
|
||||
if (!currentMusic || playlist.length === 0) return
|
||||
|
||||
const currentIndex = playlist.findIndex(m => m.id === currentMusic.id)
|
||||
|
||||
|
||||
if (playMode === 'repeat-one') {
|
||||
// Repeat current song
|
||||
if (audioRef.current) {
|
||||
@@ -102,20 +113,20 @@ function App() {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (playMode === 'shuffle') {
|
||||
// Pick a random song (not the current one)
|
||||
const availableIndexes = playlist
|
||||
.map((_, idx) => idx)
|
||||
.filter(idx => idx !== currentIndex)
|
||||
|
||||
|
||||
if (availableIndexes.length > 0) {
|
||||
const randomIndex = availableIndexes[Math.floor(Math.random() * availableIndexes.length)]
|
||||
playMusic(playlist[randomIndex], playlist)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Loop mode (default)
|
||||
if (currentIndex >= 0) {
|
||||
const nextIndex = currentIndex < playlist.length - 1 ? currentIndex + 1 : 0
|
||||
@@ -158,12 +169,12 @@ function App() {
|
||||
<Routes>
|
||||
{/* Public share route - no navigation or player */}
|
||||
<Route path="/share/:token" element={<SharePlayer />} />
|
||||
|
||||
|
||||
{/* Main app routes */}
|
||||
<Route path="/*" element={
|
||||
<div className="flex flex-col h-screen overflow-hidden bg-background">
|
||||
<Navigation onToggleTheme={toggleTheme} theme={theme} />
|
||||
|
||||
<Navigation />
|
||||
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden pb-24 md:pb-28">
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage onPlayMusic={playMusic} />} />
|
||||
@@ -173,7 +184,7 @@ function App() {
|
||||
<Route path="/playlists" element={<PlaylistsPage onPlayMusic={playMusic} />} />
|
||||
<Route path="/playlists/:playlistId" element={<PlaylistDetailPage onPlayMusic={playMusic} />} />
|
||||
<Route path="/downloads" element={<DownloadCenter onPlayMusic={playMusic} />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/settings" element={<SettingsPage theme={theme} onThemeChange={handleThemeChange} />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ export default api
|
||||
|
||||
// Music API
|
||||
export const musicApi = {
|
||||
getAll: (sortBy?: string, sortOrder?: string) =>
|
||||
api.get('/music/', { params: { sort_by: sortBy, sort_order: sortOrder } }),
|
||||
getAll: (sortBy?: string, sortOrder?: string, skip?: number, limit?: number) =>
|
||||
api.get('/music/', { params: { sort_by: sortBy, sort_order: sortOrder, skip, limit } }),
|
||||
getStats: () => api.get('/music/stats'),
|
||||
search: (query: string) => api.get('/music/search', { params: { q: query } }),
|
||||
getById: (id: number) => api.get(`/music/${id}`),
|
||||
|
||||
@@ -7,7 +7,7 @@ interface HomePageProps {
|
||||
|
||||
export default function HomePage({ onPlayMusic }: HomePageProps) {
|
||||
return (
|
||||
<div className="max-w-screen-xl mx-auto p-4">
|
||||
<div className="max-w-screen-xl mx-auto p-0 md:p-4">
|
||||
<MusicLibrary onPlayMusic={onPlayMusic} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useState, useMemo, useRef, useEffect } from 'react'
|
||||
import { musicApi } from '@/api/client'
|
||||
import { Music } from '@/types'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Play, AlertCircle, Info, Search, LayoutList, LayoutGrid, ArrowUpDown } from 'lucide-react'
|
||||
import { Play, AlertCircle, Info, Search, LayoutList, LayoutGrid, ArrowUpDown, Loader2 } from 'lucide-react'
|
||||
import MusicDetailModal from './music/MusicDetailModal'
|
||||
import {
|
||||
Select,
|
||||
@@ -28,15 +28,30 @@ export default function MusicLibrary({ onPlayMusic }: MusicLibraryProps) {
|
||||
const [layoutMode, setLayoutMode] = useState<LayoutMode>('list')
|
||||
const [sortBy, setSortBy] = useState('created_at')
|
||||
const [sortOrder, setSortOrder] = useState('desc')
|
||||
|
||||
const { data: musicList = [], isLoading } = useQuery({
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const {
|
||||
data,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isLoading
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ['music', sortBy, sortOrder],
|
||||
queryFn: async () => {
|
||||
const response = await musicApi.getAll(sortBy, sortOrder)
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
const response = await musicApi.getAll(sortBy, sortOrder, pageParam, 50)
|
||||
return response.data
|
||||
},
|
||||
getNextPageParam: (lastPage, allPages) => {
|
||||
return lastPage.length === 50 ? allPages.length * 50 : undefined
|
||||
},
|
||||
initialPageParam: 0,
|
||||
})
|
||||
|
||||
const musicList = useMemo(() => {
|
||||
return data?.pages.flatMap(page => page) ?? []
|
||||
}, [data])
|
||||
|
||||
const { data: stats } = useQuery({
|
||||
queryKey: ['music-stats'],
|
||||
queryFn: async () => {
|
||||
@@ -45,11 +60,34 @@ export default function MusicLibrary({ onPlayMusic }: MusicLibraryProps) {
|
||||
},
|
||||
})
|
||||
|
||||
// Infinite scroll observer
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
)
|
||||
|
||||
const currentRef = loadMoreRef.current
|
||||
if (currentRef) {
|
||||
observer.observe(currentRef)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (currentRef) {
|
||||
observer.unobserve(currentRef)
|
||||
}
|
||||
}
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
|
||||
|
||||
const filteredMusic = useMemo(() => {
|
||||
if (!filterText.trim()) return musicList
|
||||
|
||||
|
||||
const searchTerm = filterText.toLowerCase()
|
||||
return musicList.filter((music: Music) =>
|
||||
return musicList.filter((music: Music) =>
|
||||
music.title.toLowerCase().includes(searchTerm) ||
|
||||
music.artist?.toLowerCase().includes(searchTerm) ||
|
||||
music.album?.toLowerCase().includes(searchTerm)
|
||||
@@ -61,14 +99,13 @@ export default function MusicLibrary({ onPlayMusic }: MusicLibraryProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-screen-xl mx-auto p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="max-w-screen-xl mx-auto p-2 md:p-4">
|
||||
<div className="flex items-center justify-between mb-2 md:mb-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">Your Library</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{stats?.total_songs !== undefined ? (
|
||||
<>
|
||||
{stats.total_songs} {stats.total_songs === 1 ? 'song' : 'songs'} total
|
||||
{stats.total_songs} {stats.total_songs === 1 ? 'song' : 'songs'}
|
||||
{filterText && filteredMusic.length !== stats.total_songs && (
|
||||
<span> • {filteredMusic.length} shown</span>
|
||||
)}
|
||||
@@ -97,7 +134,7 @@ export default function MusicLibrary({ onPlayMusic }: MusicLibraryProps) {
|
||||
<SelectItem value="duration">Duration</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
@@ -105,7 +142,7 @@ export default function MusicLibrary({ onPlayMusic }: MusicLibraryProps) {
|
||||
>
|
||||
<ArrowUpDown className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="icon"
|
||||
@@ -124,8 +161,8 @@ export default function MusicLibrary({ onPlayMusic }: MusicLibraryProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mb-4">
|
||||
|
||||
<div className="relative mb-2 md:mb-4">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
@@ -141,11 +178,12 @@ export default function MusicLibrary({ onPlayMusic }: MusicLibraryProps) {
|
||||
No music found matching "{filterText}"
|
||||
</div>
|
||||
) : layoutMode === 'list' ? (
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<div className="grid grid-cols-1 gap-1 md:gap-2">
|
||||
{filteredMusic.map((music: Music) => (
|
||||
<div
|
||||
key={music.id}
|
||||
className="flex items-center gap-4 p-3 rounded-lg hover:bg-accent transition-colors"
|
||||
className="flex items-center gap-2 md:gap-4 p-2 md:p-3 rounded-lg hover:bg-accent transition-colors cursor-pointer"
|
||||
onClick={() => music.file_exists && onPlayMusic(music, filteredMusic)}
|
||||
>
|
||||
{music.thumbnail ? (
|
||||
<img
|
||||
@@ -158,18 +196,7 @@ export default function MusicLibrary({ onPlayMusic }: MusicLibraryProps) {
|
||||
<Play className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => onPlayMusic(music, filteredMusic)}
|
||||
disabled={!music.file_exists}
|
||||
>
|
||||
<Play className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium truncate">{music.title}</h3>
|
||||
@@ -190,7 +217,7 @@ export default function MusicLibrary({ onPlayMusic }: MusicLibraryProps) {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{music.duration && `${Math.floor(music.duration / 60)}:${String(Math.floor(music.duration % 60)).padStart(2, '0')}`}
|
||||
@@ -248,7 +275,7 @@ export default function MusicLibrary({ onPlayMusic }: MusicLibraryProps) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-medium truncate text-sm">{music.title}</h3>
|
||||
{music.artist && music.artist !== 'Unknown' ? (
|
||||
@@ -273,8 +300,17 @@ export default function MusicLibrary({ onPlayMusic }: MusicLibraryProps) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MusicDetailModal
|
||||
|
||||
{/* Infinite scroll loader */}
|
||||
{!filterText && (
|
||||
<div ref={loadMoreRef} className="flex justify-center py-8">
|
||||
{isFetchingNextPage && (
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MusicDetailModal
|
||||
musicId={selectedMusicId}
|
||||
open={selectedMusicId !== null}
|
||||
onClose={() => setSelectedMusicId(null)}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Home, Search, ListMusic, Download, Music2, Sun, Moon, Settings } from 'lucide-react'
|
||||
import { Button } from './ui/button'
|
||||
import { Home, Search, ListMusic, Download, Music2, Settings } from 'lucide-react'
|
||||
|
||||
interface NavigationProps {
|
||||
onToggleTheme: () => void
|
||||
theme: 'light' | 'dark'
|
||||
}
|
||||
|
||||
export default function Navigation({ onToggleTheme, theme }: NavigationProps) {
|
||||
export default function Navigation() {
|
||||
const location = useLocation()
|
||||
|
||||
const navItems = [
|
||||
@@ -40,21 +34,6 @@ export default function Navigation({ onToggleTheme, theme }: NavigationProps) {
|
||||
<span className="hidden md:inline">{label}</span>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onToggleTheme}
|
||||
className="ml-2"
|
||||
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<Sun className="h-5 w-5" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -266,19 +266,19 @@ export default function ArtistDetailPage({ onPlayMusic }: ArtistDetailProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleAutoDownload(song)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="Download this song"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{formatDuration(song.duration || 0)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAutoDownload(song)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Loader2, FileAudio, HardDrive, Clock, CheckCircle, XCircle } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Loader2, FileAudio, HardDrive, Clock, CheckCircle, XCircle, Download } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface MusicDetailModalProps {
|
||||
musicId: number | null
|
||||
@@ -41,14 +43,55 @@ export default function MusicDetailModal({ musicId, open, onClose }: MusicDetail
|
||||
return `${mins}:${String(secs).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!musicInfo) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/music/file/${musicInfo.id}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Download failed')
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${musicInfo.artist ? musicInfo.artist + ' - ' : ''}${musicInfo.title}.${musicInfo.file_format || 'mp3'}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
|
||||
toast.success('Download started!')
|
||||
} catch (error) {
|
||||
toast.error('Failed to download file')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Music Details</DialogTitle>
|
||||
<DialogDescription>
|
||||
Detailed information about this music file
|
||||
</DialogDescription>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<DialogTitle>Music Details</DialogTitle>
|
||||
<DialogDescription>
|
||||
Detailed information about this music file
|
||||
</DialogDescription>
|
||||
</div>
|
||||
{musicInfo?.file_exists && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDownload}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{isLoading ? (
|
||||
|
||||
@@ -10,11 +10,16 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { toast } from 'sonner'
|
||||
import { Loader2, FolderOpen, RefreshCw, CheckCircle, Clock, History } from 'lucide-react'
|
||||
import { Loader2, FolderOpen, RefreshCw, CheckCircle, Clock, History, Sun, Moon, Palette } from 'lucide-react'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import APIKeysManagement from './APIKeysManagement'
|
||||
|
||||
export default function SettingsPage() {
|
||||
interface SettingsPageProps {
|
||||
theme: 'light' | 'dark'
|
||||
onThemeChange: (isDark: boolean) => void
|
||||
}
|
||||
|
||||
export default function SettingsPage({ theme, onThemeChange }: SettingsPageProps) {
|
||||
const queryClient = useQueryClient()
|
||||
const [localSettings, setLocalSettings] = useState<Partial<Settings>>({})
|
||||
const [scanIntervalHours, setScanIntervalHours] = useState(1)
|
||||
@@ -130,8 +135,9 @@ export default function SettingsPage() {
|
||||
<h1 className="text-3xl font-bold mb-6">Settings</h1>
|
||||
|
||||
<Tabs defaultValue="general" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||
<TabsList className="grid w-full grid-cols-3 mb-6">
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="appearance">Appearance</TabsTrigger>
|
||||
<TabsTrigger value="api-keys">API Keys</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -429,6 +435,39 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="appearance" className="space-y-6">
|
||||
{/* Theme Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Palette className="h-5 w-5" />
|
||||
Theme
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Customize the appearance of the application
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label htmlFor="dark-mode" className="flex items-center gap-2">
|
||||
{theme === 'dark' ? <Moon className="h-4 w-4" /> : <Sun className="h-4 w-4" />}
|
||||
Dark Mode
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="dark-mode"
|
||||
checked={theme === 'dark'}
|
||||
onCheckedChange={onThemeChange}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="api-keys">
|
||||
<APIKeysManagement />
|
||||
</TabsContent>
|
||||
|
||||
Reference in New Issue
Block a user