diff --git a/.dockerignore b/.dockerignore index e41e0cc..99d801a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,7 +2,6 @@ node_modules .next .git .gitignore -README.md .env.local .env.example *.log diff --git a/.env.local b/.env.local index 72c8d37..44eabfd 100644 --- a/.env.local +++ b/.env.local @@ -1 +1 @@ -NEXT_PUBLIC_COMMIT_SHA=a00bf3e +NEXT_PUBLIC_COMMIT_SHA=35febc5 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..766fd81 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + groups: + runtime: + patterns: + - "!@types/*" + dev: + patterns: + - "@types/*" + - "eslint*" + - "typescript" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 706d2aa..3228ed5 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -67,7 +67,7 @@ jobs: - name: Login to Docker Hub uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} + username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Node.js @@ -101,24 +101,32 @@ jobs: - name: Setup Docker buildx uses: docker/setup-buildx-action@v3 + with: + driver-opts: | + network=host - name: Build and push id: build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + labels: | + ${{ steps.meta.outputs.labels }} + org.opencontainers.image.description=$(cat README.md | head -20 | tr '\n' ' ') + org.opencontainers.image.documentation=https://github.com/sillyangel/stillnavidrome/blob/main/README.md platforms: | linux/amd64 linux/arm64/v8 - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: | + type=gha,scope=deps-only + cache-to: | + type=gha,mode=max,scope=deps-only - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v1 - with: - subject-name: ${{ env.IMAGE_NAME }} - subject-digest: ${{ steps.build.outputs.digest }} - push-to-registry: true + # - name: Docker Hub Description + # uses: peter-evans/dockerhub-description@v4 + # with: + # username: ${{ vars.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_TOKEN }} + # repository: sillyangel/mice diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a94a9b..d9147d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,20 +58,28 @@ jobs: - name: Setup Docker buildx uses: docker/setup-buildx-action@v3 - + with: + driver-opts: | + network=host + - name: Build and push id: build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + labels: | + ${{ steps.meta.outputs.labels }} + org.opencontainers.image.description=$(cat README.md | head -20 | tr '\n' ' ') + org.opencontainers.image.documentation=https://github.com/sillyangel/stillnavidrome/blob/main/README.md platforms: | linux/amd64 linux/arm64/v8 - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: | + type=gha,scope=deps-only + cache-to: | + type=gha,mode=max,scope=deps-only - name: Generate artifact attestation uses: actions/attest-build-provenance@v1 @@ -79,3 +87,10 @@ jobs: subject-name: ${{ env.IMAGE_NAME }} subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true + + # - name: Docker Hub Description + # uses: peter-evans/dockerhub-description@v4 + # with: + # username: ${{ vars.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_TOKEN }} + # repository: sillyangel/mice diff --git a/Dockerfile b/Dockerfile index f82ba18..a3d167a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# Use Node.js 22 Alpine for smaller image size -FROM node:22-alpine +# Use Node.js 20 Alpine for smaller image size +FROM node:20-alpine # Install pnpm globally RUN npm install -g pnpm@10.12.4 @@ -16,6 +16,9 @@ RUN pnpm install # Copy source code COPY . . +# Copy README.md to the app directory for documentation +COPY README.md /app/ + # Set environment variable placeholders during build # These will be replaced at runtime with actual values ENV NEXT_PUBLIC_NAVIDROME_URL=NEXT_PUBLIC_NAVIDROME_URL diff --git a/app/album/[id]/page.tsx b/app/album/[id]/page.tsx index 498579d..166ff69 100644 --- a/app/album/[id]/page.tsx +++ b/app/album/[id]/page.tsx @@ -12,6 +12,7 @@ import Loading from "@/app/components/loading"; import { Separator } from '@/components/ui/separator'; import { ScrollArea } from '@/components/ui/scroll-area'; import { getNavidromeAPI } from '@/lib/navidrome'; +import { useFavoriteAlbums } from '@/hooks/use-favorite-albums'; export default function AlbumPage() { const { id } = useParams(); @@ -22,6 +23,7 @@ export default function AlbumPage() { const [starredSongs, setStarredSongs] = useState>(new Set()); const { getAlbum, starItem, unstarItem } = useNavidrome(); const { playTrack, addAlbumToQueue, playAlbum, playAlbumFromTrack, currentTrack } = useAudioPlayer(); + const { isFavoriteAlbum, toggleFavoriteAlbum } = useFavoriteAlbums(); const api = getNavidromeAPI(); useEffect(() => { @@ -137,7 +139,7 @@ export default function AlbumPage() {

{album.name}

-
@@ -145,13 +147,13 @@ export default function AlbumPage() {

{album.artist}

-

{album.songCount} songs • {album.year} • {album.genre}

-

Duration: {formatDuration(album.duration)}

-
+

{album.genre} • {album.year}

+

{album.songCount} songs, {formatDuration(album.duration)}

+ +
diff --git a/app/browse/page.tsx b/app/browse/page.tsx index 38d937b..940ac72 100644 --- a/app/browse/page.tsx +++ b/app/browse/page.tsx @@ -91,15 +91,15 @@ export default function BrowsePage() { } return ( -
- <> - - - -
-
-

- Artists +

+
+
+
+ +
+
+

+ Browse Artists

the people who make the music @@ -111,7 +111,7 @@ export default function BrowsePage() {

-
+
@@ -119,7 +119,7 @@ export default function BrowsePage() { ))} @@ -130,7 +130,7 @@ export default function BrowsePage() {
-

+

Browse Albums

@@ -139,7 +139,7 @@ export default function BrowsePage() {

-
+
@@ -176,9 +176,9 @@ export default function BrowsePage() {
- - - +
+
+
); } \ No newline at end of file diff --git a/app/components/AudioPlayer.tsx b/app/components/AudioPlayer.tsx index be2ab56..74357fb 100644 --- a/app/components/AudioPlayer.tsx +++ b/app/components/AudioPlayer.tsx @@ -116,7 +116,7 @@ export const AudioPlayer: React.FC = () => { useEffect(() => { const audioCurrent = audioRef.current; return () => { - if (audioCurrent && currentTrack && audioCurrent.currentTime > 10) { + if (audioCurrent && currentTrack && audioCurrent.currentTime > 5) { localStorage.setItem('navidrome-current-track-time', audioCurrent.currentTime.toString()); } }; @@ -134,12 +134,12 @@ export const AudioPlayer: React.FC = () => { // Notify scrobbler about new track onTrackStart(currentTrack); - // Check for saved timestamp (only restore if more than 10 seconds in) + // Check for saved timestamp (only restore if more than 5 seconds in) const savedTime = localStorage.getItem('navidrome-current-track-time'); if (savedTime) { const time = parseFloat(savedTime); - // Only restore if we were at least 10 seconds in and not near the end - if (time > 10 && time < (currentTrack.duration - 30)) { + // Only restore if we were at least 5 seconds in and not near the end + if (time > 5 && time < (currentTrack.duration - 15)) { const restorePosition = () => { if (audioCurrent.readyState >= 2) { // HAVE_CURRENT_DATA audioCurrent.currentTime = time; @@ -181,9 +181,9 @@ export const AudioPlayer: React.FC = () => { if (audioCurrent && currentTrack) { setProgress((audioCurrent.currentTime / audioCurrent.duration) * 100); - // Save current time every 30 seconds, but only if we've moved forward significantly + // Save current time every 10 seconds, but only if we've moved forward significantly const currentTime = audioCurrent.currentTime; - if (Math.abs(currentTime - lastSavedTime) >= 30 && currentTime > 10) { + if (Math.abs(currentTime - lastSavedTime) >= 10 && currentTime > 5) { localStorage.setItem('navidrome-current-track-time', currentTime.toString()); lastSavedTime = currentTime; } @@ -359,7 +359,7 @@ export const AudioPlayer: React.FC = () => { return (
setIsMinimized(false)} >
@@ -368,7 +368,7 @@ export const AudioPlayer: React.FC = () => { alt={currentTrack.name} width={40} height={40} - className="w-10 h-10 rounded-md flex-shrink-0" + className="w-10 h-10 rounded-md shrink-0" />
@@ -413,16 +413,21 @@ export const AudioPlayer: React.FC = () => { // Compact floating player (default state) return (
-
+
{/* Track info */}
{currentTrack.name}

{currentTrack.name}

diff --git a/app/components/CacheManagement.tsx b/app/components/CacheManagement.tsx new file mode 100644 index 0000000..decca08 --- /dev/null +++ b/app/components/CacheManagement.tsx @@ -0,0 +1,223 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { + Database, + Trash2, + RefreshCw, + HardDrive +} from 'lucide-react'; +import { CacheManager } from '@/lib/cache'; + +export function CacheManagement() { + const [cacheStats, setCacheStats] = useState({ + total: 0, + expired: 0, + size: '0 B' + }); + const [isClearing, setIsClearing] = useState(false); + const [lastCleared, setLastCleared] = useState(null); + + const loadCacheStats = () => { + if (typeof window === 'undefined') return; + + let total = 0; + let expired = 0; + let totalSize = 0; + const now = Date.now(); + + // Check localStorage for cache entries + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && (key.startsWith('cache-') || key.startsWith('navidrome-cache-') || key.startsWith('library-cache-'))) { + total++; + const value = localStorage.getItem(key); + if (value) { + totalSize += key.length + value.length; + try { + const parsed = JSON.parse(value); + if (parsed.expiresAt && now > parsed.expiresAt) { + expired++; + } + } catch (error) { + expired++; + } + } + } + } + + // Convert bytes to human readable format + const formatSize = (bytes: number): string => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + setCacheStats({ + total, + expired, + size: formatSize(totalSize * 2) // *2 for UTF-16 encoding + }); + }; + + useEffect(() => { + loadCacheStats(); + + // Check if there's a last cleared timestamp + const lastClearedTime = localStorage.getItem('cache-last-cleared'); + if (lastClearedTime) { + setLastCleared(new Date(parseInt(lastClearedTime)).toLocaleString()); + } + }, []); + + const handleClearCache = async () => { + setIsClearing(true); + try { + // Clear all cache using the CacheManager + CacheManager.clearAll(); + + // Also clear any other cache-related localStorage items + if (typeof window !== 'undefined') { + const keys = Object.keys(localStorage); + keys.forEach(key => { + if (key.startsWith('cache-') || + key.startsWith('navidrome-cache-') || + key.startsWith('library-cache-') || + key.includes('album') || + key.includes('artist') || + key.includes('song')) { + localStorage.removeItem(key); + } + }); + + // Set last cleared timestamp + localStorage.setItem('cache-last-cleared', Date.now().toString()); + } + + // Update stats + loadCacheStats(); + setLastCleared(new Date().toLocaleString()); + + // Show success feedback + setTimeout(() => { + setIsClearing(false); + }, 1000); + + } catch (error) { + console.error('Failed to clear cache:', error); + setIsClearing(false); + } + }; + + const handleCleanExpired = () => { + if (typeof window === 'undefined') return; + + const now = Date.now(); + const keysToRemove: string[] = []; + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && (key.startsWith('cache-') || key.startsWith('navidrome-cache-') || key.startsWith('library-cache-'))) { + try { + const value = localStorage.getItem(key); + if (value) { + const parsed = JSON.parse(value); + if (parsed.expiresAt && now > parsed.expiresAt) { + keysToRemove.push(key); + } + } + } catch (error) { + // Invalid cache item, remove it + keysToRemove.push(key); + } + } + } + + keysToRemove.forEach(key => localStorage.removeItem(key)); + loadCacheStats(); + }; + + return ( + + + + + Cache Management + + + Manage application cache to improve performance and free up storage + + + + {/* Cache Statistics */} +
+
+

{cacheStats.total}

+

Total Items

+
+
+

{cacheStats.expired}

+

Expired

+
+
+

{cacheStats.size}

+

Storage Used

+
+
+ + {/* Cache Actions */} +
+
+ + + +
+ + +
+ + {/* Cache Info */} +
+

Cache includes albums, artists, songs, and image URLs to improve loading times.

+ {lastCleared && ( +

Last cleared: {lastCleared}

+ )} +
+
+
+ ); +} diff --git a/app/components/FullScreenPlayer.tsx b/app/components/FullScreenPlayer.tsx index e4555af..4e0c2cf 100644 --- a/app/components/FullScreenPlayer.tsx +++ b/app/components/FullScreenPlayer.tsx @@ -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 = ({ isOpen, onCl {/* Overlay for better contrast */}
-
- {/* Header */} -
-

+
+ {/* Floating Header */} +
{onOpenQueue && (
{/* Main Content */} -
+
{/* Left Side - Album Art and Controls */}
{/* Album Art */} -
+
{currentTrack.album} = ({ isOpen, onCl
{/* Track Info */} -
+

{currentTrack.name}

@@ -348,7 +340,7 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl
{/* Progress */} -
+
@@ -359,7 +351,7 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl
{/* Controls */} -
+
{/* Volume and Lyrics Toggle */} -
+
+ +
+ setImportFile(e.target.files?.[0] || null)} + className="hidden" + /> + + + {importFile && ( + + )} +
+ + +
+ + {importFile && ( +
+ Selected: {importFile.name} +
+ )} + + {importError && ( +
+ Error: {importError} +
+ )} + + + ); +} diff --git a/app/components/SidebarCustomization.tsx b/app/components/SidebarCustomization.tsx new file mode 100644 index 0000000..95e2788 --- /dev/null +++ b/app/components/SidebarCustomization.tsx @@ -0,0 +1,244 @@ +'use client'; + +import React from 'react'; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { + useSortable, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Switch } from '@/components/ui/switch'; +import { + GripVertical, + Eye, + EyeOff, + Search, + Home, + List, + Radio, + Users, + Disc, + Music, + Heart, + Grid3X3, + Clock, + Settings +} from 'lucide-react'; +import { useSidebarLayout, SidebarItem, SidebarItemType } from '@/hooks/use-sidebar-layout'; + +// Icon mapping +const iconMap: Record = { + search: , + home: , + queue: , + radio: , + artists: , + albums: , + playlists: , + favorites: , + browse: , + songs: , + history: , + settings: , +}; + +interface SortableItemProps { + item: SidebarItem; + onToggleVisibility: (id: SidebarItemType) => void; + showIcons: boolean; +} + +function SortableItem({ item, onToggleVisibility, showIcons }: SortableItemProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: item.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + return ( +
+
+
+ +
+ + {showIcons && ( +
+ {iconMap[item.icon] ||
} +
+ )} + + + {item.label} + +
+ + +
+ ); +} + +export function SidebarCustomization() { + const { + settings, + hasUnsavedChanges, + reorderItems, + toggleItemVisibility, + updateShortcuts, + updateShowIcons, + applyChanges, + discardChanges, + } = useSidebarLayout(); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + reorderItems(active.id as string, over.id as string); + } + }; + + + + return ( + + + Sidebar Customization + + Customize the sidebar layout, reorder items, and manage visibility settings. + + + + {/* Show Icons Toggle */} +
+
+ +
+ Display icons next to navigation items +
+
+ +
+ + {/* Shortcut Type */} +
+ + updateShortcuts(value)} + > +
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Navigation Items Order */} +
+ +
+ Drag to reorder, click the eye icon to show/hide items +
+ + + item.id)} strategy={verticalListSortingStrategy}> +
+ {settings.items.map((item) => ( + + ))} +
+
+
+
+ + {/* Apply/Discard Changes */} + {hasUnsavedChanges() && ( +
+ +
+ + +
+
+ )} +
+
+ ); +} diff --git a/app/components/SidebarCustomizer.tsx b/app/components/SidebarCustomizer.tsx new file mode 100644 index 0000000..af64ac4 --- /dev/null +++ b/app/components/SidebarCustomizer.tsx @@ -0,0 +1,321 @@ +'use client'; + +import React, { useState } from 'react'; +import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { GripVertical, Eye, EyeOff, Download, Upload, RotateCcw } from 'lucide-react'; +import { useSidebarLayout, SidebarItem } from '@/hooks/use-sidebar-layout'; +import { Input } from '@/components/ui/input'; +import { useToast } from '@/hooks/use-toast'; + +export function SidebarCustomizer() { + const { + settings, + updateItemOrder, + toggleItemVisibility, + updateShortcuts, + updateShowIcons, + exportSettings, + importSettings, + resetToDefaults + } = useSidebarLayout(); + const { toast } = useToast(); + const [dragEnabled, setDragEnabled] = useState(false); + + const handleDragEnd = (result: DropResult) => { + if (!result.destination) return; + + const items = Array.from(settings.items); + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + updateItemOrder(items); + }; + + const handleFileImport = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + try { + await importSettings(file); + toast({ + title: "Settings imported", + description: "Your sidebar settings have been imported successfully.", + }); + } catch (error) { + toast({ + title: "Import failed", + description: "Failed to import settings. Please check the file format.", + variant: "destructive", + }); + } + + // Reset the input + event.target.value = ''; + }; + + const handleExport = () => { + exportSettings(); + toast({ + title: "Settings exported", + description: "Your settings have been downloaded as a JSON file.", + }); + }; + + const handleReset = () => { + resetToDefaults(); + toast({ + title: "Settings reset", + description: "Sidebar settings have been reset to defaults.", + }); + }; + + const getSidebarIcon = (iconId: string) => { + const iconMap: Record = { + search: ( + + + + + ), + home: ( + + + + + ), + queue: ( + + + + ), + radio: ( + + + + + ), + artists: ( + + + + + ), + albums: ( + + + + + + + ), + playlists: ( + + + + + + ), + favorites: ( + + + + ), + browse: ( + + + + + ), + songs: ( + + + + + ), + history: ( + + + + + ), + settings: ( + + + + + ), + }; + + return iconMap[iconId] || iconMap.home; + }; + + return ( +
+ {/* Global Settings */} + + + Sidebar Settings + + Customize your sidebar appearance and behavior + + + +
+ + +
+ +
+ +
+ + + +
+
+
+
+ + {/* Item Management */} + + + Sidebar Items + + Drag to reorder items and toggle visibility + + + +
+ + +
+ + + + {(provided) => ( +
+ {settings.items.map((item, index) => ( + + {(provided, snapshot) => ( +
+
+
+ +
+ {settings.showIcons && getSidebarIcon(item.icon)} + {item.label} + {!item.visible && Hidden} +
+ +
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+
+
+ + {/* Import/Export */} + + + Settings Management + + Export, import, or reset your settings + + + +
+ +
+ + +
+ +
+
+
+
+ ); +} diff --git a/app/components/SimilarArtists.tsx b/app/components/SimilarArtists.tsx index b94cb7d..f28ce12 100644 --- a/app/components/SimilarArtists.tsx +++ b/app/components/SimilarArtists.tsx @@ -68,7 +68,7 @@ export function SimilarArtists({ artistName }: SimilarArtistsProps) {
diff --git a/app/components/SongRecommendations.tsx b/app/components/SongRecommendations.tsx new file mode 100644 index 0000000..6062138 --- /dev/null +++ b/app/components/SongRecommendations.tsx @@ -0,0 +1,230 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { Song } from '@/lib/navidrome'; +import { useNavidrome } from '@/app/components/NavidromeContext'; +import { useAudioPlayer } from '@/app/components/AudioPlayerContext'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Play, Heart, Music, Shuffle } from 'lucide-react'; +import Image from 'next/image'; +import Link from 'next/link'; + +interface SongRecommendationsProps { + userName?: string; +} + +export function SongRecommendations({ userName }: SongRecommendationsProps) { + const { api, isConnected } = useNavidrome(); + const { playTrack, shuffle, toggleShuffle } = useAudioPlayer(); + const [recommendedSongs, setRecommendedSongs] = useState([]); + const [loading, setLoading] = useState(true); + const [songStates, setSongStates] = useState>({}); + const [imageLoadingStates, setImageLoadingStates] = useState>({}); + + // Get greeting based on time of day + const hour = new Date().getHours(); + const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening'; + + useEffect(() => { + const loadRecommendations = async () => { + if (!api || !isConnected) return; + + setLoading(true); + try { + // Get random albums and extract songs from them + const randomAlbums = await api.getAlbums('random', 10); // Get 10 random albums + const allSongs: Song[] = []; + + // Get songs from first few albums + for (let i = 0; i < Math.min(3, randomAlbums.length); i++) { + try { + const albumSongs = await api.getAlbumSongs(randomAlbums[i].id); + allSongs.push(...albumSongs); + } catch (error) { + console.error('Failed to get album songs:', error); + } + } + + // Shuffle and limit to 6 songs + const shuffled = allSongs.sort(() => Math.random() - 0.5); + const recommendations = shuffled.slice(0, 6); + setRecommendedSongs(recommendations); + + // Initialize starred states and image loading states + const states: Record = {}; + const imageStates: Record = {}; + recommendations.forEach((song: Song) => { + states[song.id] = !!song.starred; + imageStates[song.id] = true; // Start with loading state + }); + setSongStates(states); + setImageLoadingStates(imageStates); + } catch (error) { + console.error('Failed to load song recommendations:', error); + } finally { + setLoading(false); + } + }; + + loadRecommendations(); + }, [api, isConnected]); + + const handlePlaySong = async (song: Song) => { + if (!api) return; + + try { + const track = { + id: song.id, + name: song.title, + url: api.getStreamUrl(song.id), + artist: song.artist || 'Unknown Artist', + artistId: song.artistId || '', + album: song.album || 'Unknown Album', + albumId: song.albumId || '', + duration: song.duration || 0, + coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, + starred: !!song.starred + }; + await playTrack(track, true); + } catch (error) { + console.error('Failed to play song:', error); + } + }; + + const handleShuffleAll = async () => { + if (recommendedSongs.length === 0) return; + + // Enable shuffle if not already on + if (!shuffle) { + toggleShuffle(); + } + + // Play a random song from recommendations + const randomSong = recommendedSongs[Math.floor(Math.random() * recommendedSongs.length)]; + await handlePlaySong(randomSong); + }; + + const formatDuration = (duration: number): string => { + const minutes = Math.floor(duration / 60); + const seconds = duration % 60; + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + }; + + if (loading) { + return ( +
+
+
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+
+ ); + } + + return ( +
+
+
+

+ {greeting}{userName ? `, ${userName}` : ''}! +

+

+ Here are some songs you might enjoy +

+
+ {recommendedSongs.length > 0 && ( + + )} +
+ + {recommendedSongs.length > 0 ? ( +
+ {recommendedSongs.map((song) => ( + handlePlaySong(song)} + > + +
+
+ {song.coverArt && api ? ( + <> + {imageLoadingStates[song.id] && ( +
+ +
+ )} + {song.title} setImageLoadingStates(prev => ({ ...prev, [song.id]: false }))} + onError={() => setImageLoadingStates(prev => ({ ...prev, [song.id]: false }))} + /> + + ) : ( +
+ +
+ )} + {!imageLoadingStates[song.id] && ( +
+ +
+ )} +
+ +
+

{song.title}

+
+ e.stopPropagation()} + > + {song.artist} + + {song.duration && ( + <> + + {formatDuration(song.duration)} + + )} +
+
+ + {songStates[song.id] && ( + + )} +
+
+
+ ))} +
+ ) : ( + + + +

+ No songs available for recommendations +

+
+
+ )} +
+ ); +} diff --git a/app/components/ThemeProvider.tsx b/app/components/ThemeProvider.tsx index 77f685c..7ab4814 100644 --- a/app/components/ThemeProvider.tsx +++ b/app/components/ThemeProvider.tsx @@ -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(undefined); @@ -25,18 +27,25 @@ interface ThemeProviderProps { } export const ThemeProvider: React.FC = ({ children }) => { - const [theme, setTheme] = useState('blue'); + const [theme, setTheme] = useState('default'); + const [mode, setMode] = useState('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 = ({ 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 ( -
+
{children}
diff --git a/app/components/WhatsNewPopup.tsx b/app/components/WhatsNewPopup.tsx index 8ae0aa8..aa27782 100644 --- a/app/components/WhatsNewPopup.tsx +++ b/app/components/WhatsNewPopup.tsx @@ -7,11 +7,47 @@ import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; // Current app version from package.json -const APP_VERSION = '2025.07.02'; +const APP_VERSION = '2025.07.10'; // Changelog data - add new versions at the top const CHANGELOG = [ - { + { + version: '2025.07.10', + title: 'July Major Update', + changes: [ + // New Features + 'Support for Rich PWA Installs', + 'Added right-click shortcuts to the PWA icon', + 'Onboarding now suggests Navidrome\'s Demo Server', + 'User can export settings as a downloadable JSON', + 'New sidebar layout (compact design)', + 'New masonry-style grid in the settings page', + 'New options in settings to customize appearance', + 'Added 5 recently played albums and playlists created', + 'New loading screen', + 'New recommended songs section', + 'Enhanced playlist page', + 'Enhanced Home page layout and content', + 'Themes updated to use OKLCH (from HSL)', + 'All themes updated (light themes look similar)', + 'Caching system added (incomplete)', + 'Skeleton loading added across all pages' + ], + fixes: [ + 'Fixed skeleton loader on the Home screen', + 'Fixed album page not showing correct album art', + 'Fixed album page not showing correct artist', + 'Fixed album page not showing correct song count', + 'Fixed flash of onboarding when already onboarded', + 'Fixed issue with audio player not resuming playback after pause', + 'Resolved bug with search results not displaying correctly' + ], + breaking: [ + // Technically not breaking, but notable: + 'Removed extended sidebar layout for a cleaner look' + ] + }, + { version: '2025.07.02', title: 'July Mini Update', changes: [ diff --git a/app/components/album-artwork.tsx b/app/components/album-artwork.tsx index 3f7611d..78062a4 100644 --- a/app/components/album-artwork.tsx +++ b/app/components/album-artwork.tsx @@ -46,6 +46,8 @@ export function AlbumArtwork({ const router = useRouter(); const { addAlbumToQueue, playTrack, addToQueue } = useAudioPlayer(); const { playlists, starItem, unstarItem } = useNavidrome(); + const [imageLoading, setImageLoading] = useState(true); + const [imageError, setImageError] = useState(false); const handleClick = () => { router.push(`/album/${album.id}`); @@ -112,31 +114,57 @@ export function AlbumArtwork({
- handleClick()}> + handleClick()}>
{album.coverArt && api ? ( - {album.name} + <> + {imageLoading && ( +
+ +
+ )} + {album.name} setImageLoading(false)} + onError={() => { + setImageLoading(false); + setImageError(true); + }} + /> + ) : (
)} -
- handlePlayAlbum(album)}/> -
+ {!imageLoading && ( +
+ handlePlayAlbum(album)}/> +
+ )}
-

{album.name}

-

router.push(album.artistId)}>{album.artist}

-

- {album.songCount} songs • {Math.floor(album.duration / 60)} min -

+ {imageLoading ? ( + <> +
+
+
+ + ) : ( + <> +

{album.name}

+

router.push(album.artistId)}>{album.artist}

+

+ {album.songCount} songs • {Math.floor(album.duration / 60)} min +

+ + )} {/*
@@ -148,7 +176,7 @@ export function AlbumArtwork({ className={cn( "w-full h-full object-cover transition-all hover:scale-105", - aspectRatio === "portrait" ? "aspect-[3/4]" : "aspect-square" + aspectRatio === "portrait" ? "aspect-3/4" : "aspect-square" )} />
*/} diff --git a/app/components/artist-icon.tsx b/app/components/artist-icon.tsx index 59230f5..55970f2 100644 --- a/app/components/artist-icon.tsx +++ b/app/components/artist-icon.tsx @@ -25,12 +25,14 @@ interface ArtistIconProps extends React.HTMLAttributes { artist: Artist size?: number imageOnly?: boolean + responsive?: boolean } export function ArtistIcon({ artist, size = 150, imageOnly = false, + responsive = false, className, ...props }: ArtistIconProps) { @@ -54,16 +56,16 @@ 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 if (imageOnly) { return (
- handleClick()}> + handleClick()}>
{artist.name}
@@ -105,19 +118,6 @@ export function ArtistIcon({

- {/*
- {artist.name} -
*/}
diff --git a/app/components/ihateserverside.tsx b/app/components/ihateserverside.tsx index 073fd66..04fc6c0 100644 --- a/app/components/ihateserverside.tsx +++ b/app/components/ihateserverside.tsx @@ -5,7 +5,8 @@ import { Menu } from "@/app/components/menu"; import { Sidebar } from "@/app/components/sidebar"; import { useNavidrome } from "@/app/components/NavidromeContext"; import { AudioPlayer } from "./AudioPlayer"; -import { Toaster } from "@/components/ui/toaster" +import { Toaster } from "@/components/ui/toaster"; +import { useFavoriteAlbums } from "@/hooks/use-favorite-albums"; interface IhateserversideProps { children: React.ReactNode; @@ -18,12 +19,15 @@ const Ihateserverside: React.FC = ({ children }) => { const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [isClient, setIsClient] = useState(false); const { playlists } = useNavidrome(); + const { favoriteAlbums, removeFavoriteAlbum } = useFavoriteAlbums(); // Handle client-side hydration useEffect(() => { setIsClient(true); const savedCollapsed = localStorage.getItem('sidebar-collapsed') === 'true'; + const savedVisible = localStorage.getItem('sidebar-visible') !== 'false'; // Default to true setIsSidebarCollapsed(savedCollapsed); + setIsSidebarVisible(savedVisible); }, []); const toggleSidebarCollapse = () => { @@ -34,6 +38,14 @@ const Ihateserverside: React.FC = ({ children }) => { } }; + const toggleSidebarVisibility = () => { + const newVisible = !isSidebarVisible; + setIsSidebarVisible(newVisible); + if (typeof window !== 'undefined') { + localStorage.setItem('sidebar-visible', newVisible.toString()); + } + }; + const handleTransitionEnd = () => { if (!isSidebarVisible) { setIsSidebarHidden(true); // This will fully hide the sidebar after transition @@ -43,17 +55,17 @@ const Ihateserverside: React.FC = ({ children }) => { if (!isClient) { // Return a basic layout during SSR to match initial client render return ( -
+
{/* Top Menu */}
setIsSidebarVisible(!isSidebarVisible)} + toggleSidebar={toggleSidebarVisibility} isSidebarVisible={isSidebarVisible} toggleStatusBar={() => setIsStatusBarVisible(!isStatusBarVisible)} isStatusBarVisible={isStatusBarVisible} @@ -61,17 +73,19 @@ const Ihateserverside: React.FC = ({ children }) => {
{/* Main Content Area */} -
-
- -
-
+
+ {isSidebarVisible && ( +
+ +
+ )} +
{children}
@@ -83,17 +97,17 @@ const Ihateserverside: React.FC = ({ children }) => { ); } return ( -
+
{/* Top Menu */}
setIsSidebarVisible(!isSidebarVisible)} + toggleSidebar={toggleSidebarVisibility} isSidebarVisible={isSidebarVisible} toggleStatusBar={() => setIsStatusBarVisible(!isStatusBarVisible)} isStatusBarVisible={isStatusBarVisible} @@ -101,22 +115,22 @@ const Ihateserverside: React.FC = ({ children }) => {
{/* Main Content Area */} -
- {isSidebarVisible && ( -
- +
+ {isSidebarVisible && ( +
+ +
+ )} +
+
{children}
- )} -
-
{children}
-
{/* Floating Audio Player */} {isStatusBarVisible && ( diff --git a/app/components/menu.tsx b/app/components/menu.tsx index 83db205..995a0b7 100644 --- a/app/components/menu.tsx +++ b/app/components/menu.tsx @@ -111,9 +111,9 @@ export function Menu({ toggleSidebar, isSidebarVisible, toggleStatusBar, isStatu return ( <> -
+
-

j

File diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index 7f6fa5e..8478577 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -6,348 +6,238 @@ import { usePathname } from 'next/navigation'; import { Button } from "../../components/ui/button"; import { ScrollArea } from "../../components/ui/scroll-area"; import Link from "next/link"; -import { Playlist } from "@/lib/navidrome"; -import { ChevronLeft, ChevronRight } from "lucide-react"; +import Image from "next/image"; +import { Playlist, Album } from "@/lib/navidrome"; +import { + Search, + Home, + List, + Radio, + Users, + Disc, + Music, + Heart, + Grid3X3, + Clock, + Settings, + Circle +} from "lucide-react"; +import { useNavidrome } from "./NavidromeContext"; +import { useRecentlyPlayedAlbums } from "@/hooks/use-recently-played-albums"; +import { useSidebarShortcuts } from "@/hooks/use-sidebar-shortcuts"; +import { useSidebarLayout, SidebarItem } from "@/hooks/use-sidebar-layout"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; + +// Icon mapping for sidebar items +const iconMap: Record = { + search: , + home: , + queue: , + radio: , + artists: , + albums: , + playlists: , + favorites: , + browse: , + songs: , + history: , + settings: , +}; interface SidebarProps extends React.HTMLAttributes { playlists: Playlist[]; - collapsed?: boolean; - onToggle?: () => void; + visible?: boolean; + favoriteAlbums?: Array<{id: string, name: string, artist: string, coverArt?: string}>; + onRemoveFavoriteAlbum?: (albumId: string) => void; } -export function Sidebar({ className, playlists, collapsed = false, onToggle }: SidebarProps) { +export function Sidebar({ className, playlists, visible = true, favoriteAlbums = [], onRemoveFavoriteAlbum }: SidebarProps) { const pathname = usePathname(); - - // Define all routes and their active states - const routes = { - isRoot: pathname === "/", - isBrowse: pathname === "/browse", - isSearch: pathname === "/search", - isQueue: pathname === "/queue", - isRadio: pathname === "/radio", - isPlaylists: pathname === "/library/playlists", - isSongs: pathname === "/library/songs", - isArtists: pathname === "/library/artists", - isAlbums: pathname === "/library/albums", - isHistory: pathname === "/history", - isFavorites: pathname === "/favorites", - isSettings: pathname === "/settings", - // Handle dynamic routes - isAlbumPage: pathname.startsWith("/album/"), - isArtistPage: pathname.startsWith("/artist/"), - isPlaylistPage: pathname.startsWith("/playlist/"), - isNewPage: pathname === "/new", + const { api } = useNavidrome(); + const { recentAlbums } = useRecentlyPlayedAlbums(); + const { shortcutType } = useSidebarShortcuts(); + const { settings } = useSidebarLayout(); + + if (!visible) { + return null; + } + + // Check if a route is active + const isRouteActive = (href: string): boolean => { + if (href === '/') return pathname === '/'; + return pathname.startsWith(href); }; - // Helper function to determine if any sidebar route is active - // This prevents highlights on pages not defined in sidebar - const isAnySidebarRouteActive = Object.values(routes).some(Boolean); + // Get visible navigation items + const visibleItems = settings.items.filter(item => item.visible); return ( -
- {/* Collapse/Expand Button */} - - +
-

- Discover -

- - - - - - - - - - - - - - - -
-
-
-
-

- Library -

-
- + {/* Main Navigation Items */} + {visibleItems.map((item) => ( + - - - - - - - - - - - - - - - - -
-
-
-
-
- - -
+ ))} + + {/* Dynamic Shortcuts Section */} + {(shortcutType === 'albums' || shortcutType === 'both') && favoriteAlbums.length > 0 && ( + <> +
+ {favoriteAlbums.slice(0, 5).map((album) => ( + + + + + + + + { + e.preventDefault(); + e.stopPropagation(); + onRemoveFavoriteAlbum?.(album.id); + }} + className="text-destructive focus:text-destructive" + > + Remove from favorites + + + + ))} + + )} + + {/* Recently Played Albums */} + {(shortcutType === 'albums' || shortcutType === 'both') && recentAlbums.length > 0 && ( + <> +
+ {recentAlbums.slice(0, 5).map((album) => ( + + + + ))} + + )} + + {/* Playlists Section */} + {(shortcutType === 'playlists' || shortcutType === 'both') && playlists.length > 0 && ( + <> +
+ {playlists.slice(0, 5).map((playlist) => ( + + + + ))} + + )}
+
); diff --git a/app/components/start-screen.tsx b/app/components/start-screen.tsx index 31dda35..a2917cd 100644 --- a/app/components/start-screen.tsx +++ b/app/components/start-screen.tsx @@ -206,6 +206,49 @@ export function LoginForm({ setScrobblingEnabled(enabled); }; + const handleDemoSetup = async () => { + const demoCredentials = { + serverUrl: 'https://demo.navidrome.org', + username: 'demo', + password: 'demo' + }; + + // Set form data + setFormData(demoCredentials); + + setIsTesting(true); + try { + const success = await testConnection(demoCredentials); + + if (success) { + // Save the config + updateConfig(demoCredentials); + + toast({ + title: "Demo Server Connected", + description: "Successfully connected to the Navidrome demo server! Let's configure your preferences.", + }); + + // Move to settings step + setStep('settings'); + } else { + toast({ + title: "Demo Server Unavailable", + description: "The demo server is currently unavailable. Please try again later or enter your own server details.", + variant: "destructive" + }); + } + } catch (error) { + toast({ + title: "Connection Error", + description: "Could not connect to the demo server. Please check your internet connection.", + variant: "destructive" + }); + } finally { + setIsTesting(false); + } + }; + if (step === 'settings') { return (
@@ -400,7 +443,7 @@ export function LoginForm({ />
- {/* Demo Server Tip */} + {/* Demo Server Setup */}
@@ -410,14 +453,42 @@ export function LoginForm({

Don't have a Navidrome server?

-

- Try the demo server to explore mice: +

+ Try the demo server to explore mice with one click:

-
-
URL: https://demo.navidrome.org
-
Username: demo
-
Password: demo
+ +
+ This will automatically connect to: demo.navidrome.org
+
+ + Or enter demo credentials manually + +
+
URL: https://demo.navidrome.org
+
Username: demo
+
Password: demo
+
+
diff --git a/app/favorites/page.tsx b/app/favorites/page.tsx index ce27707..a1e761f 100644 --- a/app/favorites/page.tsx +++ b/app/favorites/page.tsx @@ -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"; @@ -119,24 +118,25 @@ const FavoritesPage = () => { if (!isConnected) { return ( -
-
-

Please connect to your Navidrome server to view favorites.

+
+
+
+

Favorites

+

Please connect to your Navidrome server to view favorites.

+
); } return ( -
+
-
-
-

Favorites

-

Your starred albums, songs, and artists

-
+
+

Favorites

+

Your starred albums, songs, and artists

- +
@@ -167,33 +167,14 @@ const FavoritesPage = () => { ) : (
{favoriteAlbums.map((album) => ( - -
- {album.coverArt && api ? ( - {album.name} - ) : ( -
- -
- )} -
- handlePlayAlbum(album)}/> -
-
- -

{album.name}

-

{album.artist}

-

- {album.songCount} songs • {Math.floor(album.duration / 60)} min -

-
-
+ ))}
)} @@ -217,7 +198,7 @@ const FavoritesPage = () => {
{index + 1}
-
+
{song.coverArt && api ? ( { ) : (
{favoriteArtists.map((artist) => ( - - -
- {artist.name} -
-

{artist.name}

-

- {artist.albumCount} albums -

-
-
+ ))}
)} +
); diff --git a/app/fonts/0xProtoNerdFont-Regular.ttf b/app/fonts/0xProtoNerdFont-Regular.ttf new file mode 100644 index 0000000..7436940 Binary files /dev/null and b/app/fonts/0xProtoNerdFont-Regular.ttf differ diff --git a/app/fonts/0xProtoNerdFontMono-Regular.ttf b/app/fonts/0xProtoNerdFontMono-Regular.ttf new file mode 100644 index 0000000..6094706 Binary files /dev/null and b/app/fonts/0xProtoNerdFontMono-Regular.ttf differ diff --git a/app/fonts/0xProtoNerdFontPropo-Regular.ttf b/app/fonts/0xProtoNerdFontPropo-Regular.ttf new file mode 100644 index 0000000..b42ef34 Binary files /dev/null and b/app/fonts/0xProtoNerdFontPropo-Regular.ttf differ diff --git a/app/globals.css b/app/globals.css index 30d4145..d0ebb2b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,345 +1,849 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; +@custom-variant dark (@media (prefers-color-scheme: dark)); -body { - font-family: Arial, Helvetica, sans-serif; +@theme { + --color-background: var(--background); + --color-foreground: var(--foreground); + + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-hover: var(--hover); + + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); +} + +/* + The default border color has changed to `currentcolor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } } @layer utilities { .text-balance { text-wrap: balance; } - + .animate-scroll { animation: scroll 8s linear infinite; } - + .animate-infinite-scroll { animation: infiniteScroll 10s linear infinite; } } -@keyframes scroll { - 0%, 20% { - transform: translateX(0); - } - 80%, 100% { - transform: translateX(-100%); +@layer utilities { + body { + font-family: Arial, Helvetica, sans-serif; } } -@keyframes infiniteScroll { - 0% { - transform: translateX(15%); +@layer utilities { + @keyframes scroll { + 0%, + 20% { + transform: translateX(0); + } + 80%, + 100% { + transform: translateX(-100%); + } } - 100% { - transform: translateX(-215%); + + @keyframes infiniteScroll { + 0% { + transform: translateX(15%); + } + 100% { + transform: translateX(-215%); + } } } @layer base { :root { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 0 0% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --hover: 240 27% 11%; --radius: 0.5rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --hover: oklch(0.97 0 0); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + } + + .dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --hover: oklch(0.269 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); + } + + /* Default/White Theme */ + .theme-default { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --hover: oklch(0.97 0 0); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + } + + .theme-default.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --hover: oklch(0.269 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); + } + + /* Blue Theme */ + .theme-blue { + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.623 0.214 259.815); + --primary-foreground: oklch(0.97 0.014 254.604); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.623 0.214 259.815); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --hover: oklch(0.967 0.001 286.375); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.623 0.214 259.815); + --sidebar-primary-foreground: oklch(0.97 0.014 254.604); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.623 0.214 259.815); } - /* Blue Theme Dark */ .theme-blue.dark { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 0 0% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; - --hover: 240 27% 11%; - --radius: 0.5rem; + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.546 0.245 262.881); + --primary-foreground: oklch(0.379 0.146 265.522); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.488 0.243 264.376); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --hover: oklch(0.274 0.006 286.033); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.546 0.245 262.881); + --sidebar-primary-foreground: oklch(0.379 0.146 265.522); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.488 0.243 264.376); + } + + /* Violet Theme */ + .theme-violet { + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.606 0.25 292.717); + --primary-foreground: oklch(0.969 0.016 293.756); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.606 0.25 292.717); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --hover: oklch(0.967 0.001 286.375); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.606 0.25 292.717); + --sidebar-primary-foreground: oklch(0.969 0.016 293.756); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.606 0.25 292.717); } - /* Violet Theme Dark */ .theme-violet.dark { - --background: 224 71.4% 4.1%; - --foreground: 210 20% 98%; - --card: 224 71.4% 4.1%; - --card-foreground: 210 20% 98%; - --popover: 224 71.4% 4.1%; - --popover-foreground: 210 20% 98%; - --primary: 263.4 70% 50.4%; - --primary-foreground: 210 20% 98%; - --secondary: 215 27.9% 16.9%; - --secondary-foreground: 210 20% 98%; - --muted: 215 27.9% 16.9%; - --muted-foreground: 217.9 10.6% 64.9%; - --accent: 215 27.9% 16.9%; - --accent-foreground: 210 20% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 20% 98%; - --border: 215 27.9% 16.9%; - --input: 215 27.9% 16.9%; - --ring: 263.4 70% 50.4%; - --radius: 0.5rem; + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.541 0.281 293.009); + --primary-foreground: oklch(0.969 0.016 293.756); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.541 0.281 293.009); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --hover: oklch(0.274 0.006 286.033); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.541 0.281 293.009); + --sidebar-primary-foreground: oklch(0.969 0.016 293.756); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.541 0.281 293.009); } - .theme-red.dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 72.2% 50.6%; - --primary-foreground: 0 85.7% 97.3%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 72.2% 50.6%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; + /* Red Theme */ + .theme-red { + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.637 0.237 25.331); + --primary-foreground: oklch(0.971 0.013 17.38); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.637 0.237 25.331); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --hover: oklch(0.967 0.001 286.375); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.637 0.237 25.331); + --sidebar-primary-foreground: oklch(0.971 0.013 17.38); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.637 0.237 25.331); + } + + .theme-red.dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.637 0.237 25.331); + --primary-foreground: oklch(0.971 0.013 17.38); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.637 0.237 25.331); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --hover: oklch(0.274 0.006 286.033); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.637 0.237 25.331); + --sidebar-primary-foreground: oklch(0.971 0.013 17.38); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.637 0.237 25.331); + } + + /* Rose Theme */ + .theme-rose { + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.645 0.246 16.439); + --primary-foreground: oklch(0.969 0.015 12.422); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.645 0.246 16.439); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --hover: oklch(0.967 0.001 286.375); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.645 0.246 16.439); + --sidebar-primary-foreground: oklch(0.969 0.015 12.422); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.645 0.246 16.439); } .theme-rose.dark { - --background: 20 14.3% 4.1%; - --foreground: 0 0% 95%; - --card: 24 9.8% 10%; - --card-foreground: 0 0% 95%; - --popover: 0 0% 9%; - --popover-foreground: 0 0% 95%; - --primary: 346.8 77.2% 49.8%; - --primary-foreground: 355.7 100% 97.3%; - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 15%; - --muted-foreground: 240 5% 64.9%; - --accent: 12 6.5% 15.1%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 85.7% 97.3%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 346.8 77.2% 49.8%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.645 0.246 16.439); + --primary-foreground: oklch(0.969 0.015 12.422); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.645 0.246 16.439); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --hover: oklch(0.274 0.006 286.033); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.645 0.246 16.439); + --sidebar-primary-foreground: oklch(0.969 0.015 12.422); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.645 0.246 16.439); } - + + /* Orange Theme */ + .theme-orange { + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.705 0.213 47.604); + --primary-foreground: oklch(0.98 0.016 73.684); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.705 0.213 47.604); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --hover: oklch(0.967 0.001 286.375); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.705 0.213 47.604); + --sidebar-primary-foreground: oklch(0.98 0.016 73.684); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.705 0.213 47.604); + } + .theme-orange.dark { - --background: 20 14.3% 4.1%; - --foreground: 60 9.1% 97.8%; - --card: 20 14.3% 4.1%; - --card-foreground: 60 9.1% 97.8%; - --popover: 20 14.3% 4.1%; - --popover-foreground: 60 9.1% 97.8%; - --primary: 20.5 90.2% 48.2%; - --primary-foreground: 60 9.1% 97.8%; - --secondary: 12 6.5% 15.1%; - --secondary-foreground: 60 9.1% 97.8%; - --muted: 12 6.5% 15.1%; - --muted-foreground: 24 5.4% 63.9%; - --accent: 12 6.5% 15.1%; - --accent-foreground: 60 9.1% 97.8%; - --destructive: 0 72.2% 50.6%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 12 6.5% 15.1%; - --input: 12 6.5% 15.1%; - --ring: 20.5 90.2% 48.2%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.646 0.222 41.116); + --primary-foreground: oklch(0.98 0.016 73.684); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.646 0.222 41.116); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --hover: oklch(0.274 0.006 286.033); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.646 0.222 41.116); + --sidebar-primary-foreground: oklch(0.98 0.016 73.684); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.646 0.222 41.116); + } + + /* Green Theme */ + .theme-green { + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.723 0.219 149.579); + --primary-foreground: oklch(0.982 0.018 155.826); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.723 0.219 149.579); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --hover: oklch(0.967 0.001 286.375); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.723 0.219 149.579); + --sidebar-primary-foreground: oklch(0.982 0.018 155.826); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.723 0.219 149.579); } .theme-green.dark { - --background: 20 14.3% 4.1%; - --foreground: 0 0% 95%; - --card: 24 9.8% 10%; - --card-foreground: 0 0% 95%; - --popover: 0 0% 9%; - --popover-foreground: 0 0% 95%; - --primary: 142.1 70.6% 45.3%; - --primary-foreground: 144.9 80.4% 10%; - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 15%; - --muted-foreground: 240 5% 64.9%; - --accent: 12 6.5% 15.1%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 85.7% 97.3%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 142.4 71.8% 29.2%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.696 0.17 162.48); + --primary-foreground: oklch(0.393 0.095 152.535); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.527 0.154 150.069); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --hover: oklch(0.274 0.006 286.033); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.696 0.17 162.48); + --sidebar-primary-foreground: oklch(0.393 0.095 152.535); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.527 0.154 150.069); + } + + /* Yellow Theme */ + .theme-yellow { + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.795 0.184 86.047); + --primary-foreground: oklch(0.421 0.095 57.708); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.795 0.184 86.047); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --hover: oklch(0.967 0.001 286.375); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.795 0.184 86.047); + --sidebar-primary-foreground: oklch(0.421 0.095 57.708); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.795 0.184 86.047); } .theme-yellow.dark { - --background: 20 14.3% 4.1%; - --foreground: 60 9.1% 97.8%; - --card: 20 14.3% 4.1%; - --card-foreground: 60 9.1% 97.8%; - --popover: 20 14.3% 4.1%; - --popover-foreground: 60 9.1% 97.8%; - --primary: 47.9 95.8% 53.1%; - --primary-foreground: 26 83.3% 14.1%; - --secondary: 12 6.5% 15.1%; - --secondary-foreground: 60 9.1% 97.8%; - --muted: 12 6.5% 15.1%; - --muted-foreground: 24 5.4% 63.9%; - --accent: 12 6.5% 15.1%; - --accent-foreground: 60 9.1% 97.8%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 12 6.5% 15.1%; - --input: 12 6.5% 15.1%; - --ring: 35.5 91.7% 32.9%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.795 0.184 86.047); + --primary-foreground: oklch(0.421 0.095 57.708); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.554 0.135 66.442); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --hover: oklch(0.274 0.006 286.033); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.795 0.184 86.047); + --sidebar-primary-foreground: oklch(0.421 0.095 57.708); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.554 0.135 66.442); } } - - - - - - - - - -/* BackUp */ - .dark { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 0 0% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --hover: 240 27% 11%; - --radius: 0.5rem; +@layer utilities { + body { + font-family: Arial, Helvetica, sans-serif; } + :focus-visible { + outline-color: rgb(59, 130, 246); + } + ::selection { + background-color: rgb(59, 130, 246); + } + ::marker { + color: rgb(59, 130, 246); + } - - - -:focus-visible { outline-color: rgb(59, 130, 246); } -::selection { background-color: rgb(59, 130, 246); } -::marker { color: rgb(59, 130, 246); } - - - -::selection { - background: var(--primary); + ::selection { + background: var(--primary); + } } @layer base { * { - @apply border-border; + border-color: var(--color-border); + outline-color: var(--color-ring); } body { - @apply bg-background text-foreground; + background-color: var(--color-background); + color: var(--color-foreground); } h1 { - @apply text-2xl; - @apply font-black; + font-size: 1.5rem; + font-weight: 900; } h2 { - @apply text-xl; - @apply font-black; + font-size: 1.25rem; + font-weight: 900; } h3 { - @apply text-lg; - @apply font-black; + font-size: 1.125rem; + font-weight: 900; } h4 { - @apply text-base; - @apply font-black; + font-size: 1rem; + font-weight: 900; } h5 { - @apply text-sm; - @apply font-black; + font-size: 0.875rem; + font-weight: 900; } h6 { - @apply text-xs; - @apply font-black; + font-size: 0.75rem; + font-weight: 900; } ul { - @apply list-disc; - @apply ml-9; + list-style-type: disc; + margin-left: 2.25rem; } - } \ No newline at end of file + } + +/* + ---break--- +*/ + +/* + +will delete after the new theme replaces the old one +since the new theme already has the sidebar colors defined + +:root { + --sidebar: hsl(0 0% 98%); + --sidebar-foreground: hsl(240 5.3% 26.1%); + --sidebar-primary: hsl(240 5.9% 10%); + --sidebar-primary-foreground: hsl(0 0% 98%); + --sidebar-accent: hsl(240 4.8% 95.9%); + --sidebar-accent-foreground: hsl(240 5.9% 10%); + --sidebar-border: hsl(220 13% 91%); + --sidebar-ring: hsl(217.2 91.2% 59.8%); +} + + +.dark { + --sidebar: hsl(240 5.9% 10%); + --sidebar-foreground: hsl(240 4.8% 95.9%); + --sidebar-primary: hsl(224.3 76.3% 48%); + --sidebar-primary-foreground: hsl(0 0% 100%); + --sidebar-accent: hsl(240 3.7% 15.9%); + --sidebar-accent-foreground: hsl(240 4.8% 95.9%); + --sidebar-border: hsl(240 3.7% 15.9%); + --sidebar-ring: hsl(217.2 91.2% 59.8%); +} */ + +/* + ---break--- +*/ \ No newline at end of file diff --git a/app/history/page.tsx b/app/history/page.tsx index 2a72663..71ec7c7 100644 --- a/app/history/page.tsx +++ b/app/history/page.tsx @@ -79,7 +79,7 @@ export default function HistoryPage() { return (
- +
@@ -155,7 +155,7 @@ export default function HistoryPage() {
{/* Album Art */} -
+
{track.album} +