'use client'; import React, { useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useToast } from '@/hooks/use-toast'; import { useOfflineLibrary } from '@/hooks/use-offline-library'; import { useNavidrome } from '@/app/components/NavidromeContext'; import { Download, Trash2, RefreshCw, Wifi, WifiOff, Database, Clock, AlertCircle, CheckCircle, Music, User, List, HardDrive, Disc, Search, Filter, SlidersHorizontal } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { ScrollArea } from '@/components/ui/scroll-area'; import Image from 'next/image'; import { Album, Playlist } from '@/lib/navidrome'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { OfflineManagement } from './OfflineManagement'; import { Skeleton } from '@/components/ui/skeleton'; // Helper functions function formatBytes(bytes: number): string { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function formatDate(date: Date | null): string { if (!date) return 'Never'; return date.toLocaleDateString() + ' at ' + date.toLocaleTimeString(); } // Album card for selection function AlbumSelectionCard({ album, isSelected, onToggleSelection, isDownloading, downloadProgress, estimatedSize }: { album: Album; isSelected: boolean; onToggleSelection: () => void; isDownloading: boolean; downloadProgress?: number; estimatedSize: string; }) { const { api } = useNavidrome(); return (
{album.name}

{album.name}

{album.artist}

{album.songCount} songs • {estimatedSize}
{isDownloading && downloadProgress !== undefined && ( )}
); } // Playlist selection card function PlaylistSelectionCard({ playlist, isSelected, onToggleSelection, isDownloading, downloadProgress, estimatedSize }: { playlist: Playlist; isSelected: boolean; onToggleSelection: () => void; isDownloading: boolean; downloadProgress?: number; estimatedSize: string; }) { const { api } = useNavidrome(); return (

{playlist.name}

by {playlist.owner}

{playlist.songCount} songs • {estimatedSize}
{isDownloading && downloadProgress !== undefined && ( )}
); } export default function EnhancedOfflineManager() { const { toast } = useToast(); const [activeTab, setActiveTab] = useState('overview'); const [albums, setAlbums] = useState([]); const [playlists, setPlaylists] = useState([]); const [loading, setLoading] = useState({ albums: false, playlists: false }); const [searchQuery, setSearchQuery] = useState(''); const [selectedAlbums, setSelectedAlbums] = useState>(new Set()); const [selectedPlaylists, setSelectedPlaylists] = useState>(new Set()); const [downloadingItems, setDownloadingItems] = useState>(new Map()); // Filter state const [sortBy, setSortBy] = useState('recent'); const [filtersVisible, setFiltersVisible] = useState(false); const offline = useOfflineLibrary(); const { api } = useNavidrome(); // Load albums and playlists // ...existing code... // ...existing code... // Place useEffect after the first (and only) declarations of loadAlbums and loadPlaylists // Load albums data const loadAlbums = async () => { setLoading(prev => ({ ...prev, albums: true })); try { const albumData = await offline.getAlbums(); setAlbums(albumData); // Load previously selected albums from localStorage const savedSelections = localStorage.getItem('navidrome-offline-albums'); if (savedSelections) { setSelectedAlbums(new Set(JSON.parse(savedSelections))); } } catch (error) { console.error('Failed to load albums:', error); toast({ title: 'Error', description: 'Failed to load albums. Please try again.', variant: 'destructive' }); } finally { setLoading(prev => ({ ...prev, albums: false })); } }; // Load playlists data const loadPlaylists = async () => { setLoading(prev => ({ ...prev, playlists: true })); try { const playlistData = await offline.getPlaylists(); setPlaylists(playlistData); // Load previously selected playlists from localStorage const savedSelections = localStorage.getItem('navidrome-offline-playlists'); if (savedSelections) { setSelectedPlaylists(new Set(JSON.parse(savedSelections))); } } catch (error) { console.error('Failed to load playlists:', error); toast({ title: 'Error', description: 'Failed to load playlists. Please try again.', variant: 'destructive' }); } finally { setLoading(prev => ({ ...prev, playlists: false })); } }; // Toggle album selection const toggleAlbumSelection = (albumId: string) => { setSelectedAlbums(prev => { const newSelection = new Set(prev); if (newSelection.has(albumId)) { newSelection.delete(albumId); } else { newSelection.add(albumId); } // Save to localStorage localStorage.setItem('navidrome-offline-albums', JSON.stringify([...newSelection])); return newSelection; }); }; // Toggle playlist selection const togglePlaylistSelection = (playlistId: string) => { setSelectedPlaylists(prev => { const newSelection = new Set(prev); if (newSelection.has(playlistId)) { newSelection.delete(playlistId); } else { newSelection.add(playlistId); } // Save to localStorage localStorage.setItem('navidrome-offline-playlists', JSON.stringify([...newSelection])); return newSelection; }); }; // Download selected items const downloadSelected = async () => { // Mock implementation - in a real implementation, you'd integrate with the download system const selectedIds = [...selectedAlbums, ...selectedPlaylists]; if (selectedIds.length === 0) { toast({ title: 'No items selected', description: 'Please select albums or playlists to download.', }); return; } toast({ title: 'Download Started', description: `Downloading ${selectedIds.length} items for offline use.`, }); // Mock download progress const downloadMap = new Map(); selectedIds.forEach(id => downloadMap.set(id, 0)); setDownloadingItems(downloadMap); // Simulate download progress const interval = setInterval(() => { setDownloadingItems(prev => { const updated = new Map(prev); let allComplete = true; for (const [id, progress] of prev.entries()) { if (progress < 100) { updated.set(id, Math.min(progress + Math.random() * 10, 100)); allComplete = false; } } if (allComplete) { clearInterval(interval); toast({ title: 'Download Complete', description: `${selectedIds.length} items are now available offline.`, }); setTimeout(() => { setDownloadingItems(new Map()); }, 1000); } return updated; }); }, 500); }; // Filter and sort albums const filteredAlbums = albums .filter(album => { if (!searchQuery) return true; return album.name.toLowerCase().includes(searchQuery.toLowerCase()) || album.artist.toLowerCase().includes(searchQuery.toLowerCase()); }) .sort((a, b) => { switch (sortBy) { case 'recent': return new Date(b.created || '').getTime() - new Date(a.created || '').getTime(); case 'name': return a.name.localeCompare(b.name); case 'artist': return a.artist.localeCompare(b.artist); default: return 0; } }); // Filter and sort playlists const filteredPlaylists = playlists .filter(playlist => { if (!searchQuery) return true; return playlist.name.toLowerCase().includes(searchQuery.toLowerCase()); }) .sort((a, b) => { switch (sortBy) { case 'recent': return new Date(b.changed || '').getTime() - new Date(a.changed || '').getTime(); case 'name': return a.name.localeCompare(b.name); default: return 0; } }); // Estimate album size (mock implementation) const estimateSize = (songCount: number) => { const averageSongSizeMB = 8; const totalSizeMB = songCount * averageSongSizeMB; if (totalSizeMB > 1000) { return `${(totalSizeMB / 1000).toFixed(1)} GB`; } return `${totalSizeMB.toFixed(0)} MB`; }; return ( Overview Albums Playlists Select Albums Choose albums to make available offline
setSearchQuery(e.target.value)} className="pl-8" />
{filtersVisible && (
Sort By
)}
{selectedAlbums.size} album{selectedAlbums.size !== 1 ? 's' : ''} selected
{loading.albums ? ( // Loading skeletons Array.from({ length: 5 }).map((_, i) => (
)) ) : filteredAlbums.length > 0 ? ( filteredAlbums.map(album => ( toggleAlbumSelection(album.id)} isDownloading={downloadingItems.has(album.id)} downloadProgress={downloadingItems.get(album.id)} estimatedSize={estimateSize(album.songCount)} /> )) ) : (

{searchQuery ? 'No albums found matching your search' : 'No albums available'}

)}
Select Playlists Choose playlists to make available offline
setSearchQuery(e.target.value)} className="pl-8" />
{filtersVisible && (
Sort By
)}
{selectedPlaylists.size} playlist{selectedPlaylists.size !== 1 ? 's' : ''} selected
{loading.playlists ? ( // Loading skeletons Array.from({ length: 5 }).map((_, i) => (
)) ) : filteredPlaylists.length > 0 ? ( filteredPlaylists.map(playlist => ( togglePlaylistSelection(playlist.id)} isDownloading={downloadingItems.has(playlist.id)} downloadProgress={downloadingItems.get(playlist.id)} estimatedSize={estimateSize(playlist.songCount)} /> )) ) : (

{searchQuery ? 'No playlists found matching your search' : 'No playlists available'}

)}
); }