172 lines
6.1 KiB
TypeScript
172 lines
6.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { JefflixLogo } from '@/components/jefflix-logo'
|
|
import { SongRow } from '@/components/music/search-results'
|
|
import { useMusicPlayer } from '@/components/music/music-provider'
|
|
import { useOffline } from '@/lib/stores/offline'
|
|
import {
|
|
ArrowLeft,
|
|
Download,
|
|
HardDrive,
|
|
Loader2,
|
|
Play,
|
|
RefreshCw,
|
|
Trash2,
|
|
WifiOff,
|
|
} from 'lucide-react'
|
|
import Link from 'next/link'
|
|
|
|
function formatBytes(bytes: number) {
|
|
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 `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
|
|
}
|
|
|
|
export default function OfflinePage() {
|
|
const { state, playTrack } = useMusicPlayer()
|
|
const {
|
|
offlineTracks,
|
|
queue,
|
|
activeDownloadId,
|
|
storageUsed,
|
|
clearAll,
|
|
sync,
|
|
loading,
|
|
} = useOffline()
|
|
const [syncing, setSyncing] = useState(false)
|
|
const [clearing, setClearing] = useState(false)
|
|
const hasPlayer = !!state.currentTrack
|
|
|
|
const handleSync = async () => {
|
|
setSyncing(true)
|
|
await sync()
|
|
setSyncing(false)
|
|
}
|
|
|
|
const handleClearAll = async () => {
|
|
if (!confirm('Remove all downloaded songs? They can be re-downloaded later.')) return
|
|
setClearing(true)
|
|
await clearAll()
|
|
setClearing(false)
|
|
}
|
|
|
|
const playAllOffline = () => {
|
|
if (offlineTracks.length > 0) {
|
|
playTrack(offlineTracks[0], offlineTracks, 0)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className={`min-h-screen bg-background ${hasPlayer ? 'pb-20' : ''}`}>
|
|
{/* Header */}
|
|
<div className="border-b border-border">
|
|
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
|
<Link href="/" className="inline-block">
|
|
<JefflixLogo size="small" />
|
|
</Link>
|
|
<Link href="/music">
|
|
<Button variant="ghost" size="sm">
|
|
<ArrowLeft className="h-4 w-4 mr-1.5" />
|
|
Music
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="container mx-auto px-4 py-12 md:py-16">
|
|
<div className="max-w-2xl mx-auto">
|
|
{/* Hero */}
|
|
<div className="text-center space-y-4 mb-8">
|
|
<div className="inline-block p-4 bg-blue-100 dark:bg-blue-900/30 rounded-full">
|
|
<WifiOff className="h-10 w-10 text-blue-600 dark:text-blue-400" />
|
|
</div>
|
|
<h1 className="text-3xl font-bold font-marker">Offline Library</h1>
|
|
<p className="text-muted-foreground">
|
|
Songs downloaded for offline playback. Syncs across all your devices.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stats + Actions */}
|
|
<div className="flex flex-wrap items-center gap-3 mb-6">
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground bg-muted/50 px-3 py-1.5 rounded-lg">
|
|
<HardDrive className="h-4 w-4" />
|
|
{formatBytes(storageUsed)} used
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground bg-muted/50 px-3 py-1.5 rounded-lg">
|
|
<Download className="h-4 w-4" />
|
|
{offlineTracks.length} songs
|
|
</div>
|
|
<div className="flex-1" />
|
|
<Button variant="outline" size="sm" onClick={handleSync} disabled={syncing}>
|
|
{syncing ? <Loader2 className="h-4 w-4 animate-spin mr-1.5" /> : <RefreshCw className="h-4 w-4 mr-1.5" />}
|
|
Sync
|
|
</Button>
|
|
{offlineTracks.length > 0 && (
|
|
<>
|
|
<Button size="sm" onClick={playAllOffline}>
|
|
<Play className="h-4 w-4 mr-1.5" />
|
|
Play All
|
|
</Button>
|
|
<Button variant="destructive" size="sm" onClick={handleClearAll} disabled={clearing}>
|
|
{clearing ? <Loader2 className="h-4 w-4 animate-spin mr-1.5" /> : <Trash2 className="h-4 w-4 mr-1.5" />}
|
|
Clear All
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Download queue */}
|
|
{queue.length > 0 && (
|
|
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
|
<h3 className="text-sm font-semibold mb-2 flex items-center gap-2">
|
|
<Loader2 className="h-4 w-4 animate-spin text-blue-600" />
|
|
Downloading ({queue.length} remaining)
|
|
</h3>
|
|
<div className="space-y-1 text-sm text-muted-foreground">
|
|
{queue.slice(0, 5).map((t) => (
|
|
<div key={t.id} className="flex items-center gap-2">
|
|
{t.id === activeDownloadId && <Loader2 className="h-3 w-3 animate-spin" />}
|
|
<span className="truncate">{t.title} — {t.artist}</span>
|
|
</div>
|
|
))}
|
|
{queue.length > 5 && (
|
|
<div className="text-xs">...and {queue.length - 5} more</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Songs list */}
|
|
{loading ? (
|
|
<div className="flex justify-center py-12">
|
|
<Loader2 className="h-6 w-6 animate-spin text-blue-600" />
|
|
</div>
|
|
) : offlineTracks.length === 0 ? (
|
|
<div className="text-center py-12 space-y-4">
|
|
<WifiOff className="h-12 w-12 mx-auto text-muted-foreground/30" />
|
|
<p className="text-muted-foreground">
|
|
No songs downloaded yet. Tap the download icon on any song to save it for offline.
|
|
</p>
|
|
<Link href="/music">
|
|
<Button variant="outline">
|
|
Browse Music
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div className="border border-border rounded-lg divide-y divide-border overflow-hidden">
|
|
{offlineTracks.map((song, i) => (
|
|
<SongRow key={song.id} song={song} songs={offlineTracks} index={i} showDownload />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|