Compare commits

..

No commits in common. "90ba6df611c5262598ad379408208f83e9731f8f" and "e248613abb106546e611d2fbd05b2b8849a94889" have entirely different histories.

7 changed files with 281 additions and 530 deletions

BIN
media.db

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

View File

@ -20,7 +20,6 @@ const FolderViewerPage = () => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const path = searchParams.get("path"); const path = searchParams.get("path");
const [items, setItems] = useState<FileSystemItem[]>([]); const [items, setItems] = useState<FileSystemItem[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (path) { if (path) {
@ -29,16 +28,9 @@ const FolderViewerPage = () => {
}, [path]); }, [path]);
const fetchItems = async (currentPath: string) => { const fetchItems = async (currentPath: string) => {
setLoading(true);
try {
const res = await fetch(`/api/files?path=${currentPath}`); const res = await fetch(`/api/files?path=${currentPath}`);
const data = await res.json(); const data = await res.json();
setItems(data); setItems(data);
} catch (error) {
console.error('Error fetching items:', error);
} finally {
setLoading(false);
}
}; };
const formatFileSize = (bytes: number) => { const formatFileSize = (bytes: number) => {
@ -72,113 +64,57 @@ const FolderViewerPage = () => {
} }
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800"> <div className="p-6">
{loading && ( <h1 className="text-3xl font-bold mb-6 truncate">{path}</h1>
<div className="fixed inset-0 bg-black/5 backdrop-blur-sm z-50 flex items-center justify-center"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
<div className="bg-white dark:bg-slate-800 rounded-xl p-6 shadow-xl">
<div className="flex items-center space-x-3">
<div className="animate-spin rounded-full h-5 w-5 border-2 border-slate-900 dark:border-white border-t-transparent"></div>
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">Loading directory...</span>
</div>
</div>
</div>
)}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{!path ? (
<div className="flex flex-col items-center justify-center min-h-[60vh]">
<div className="text-center max-w-md">
<div className="w-20 h-20 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl flex items-center justify-center mx-auto mb-6">
<Folder className="h-10 w-10 text-white" />
</div>
<h2 className="text-2xl font-bold text-slate-800 dark:text-slate-100 mb-2">Select a Library</h2>
<p className="text-slate-600 dark:text-slate-400">
Choose a media library from the sidebar to browse your files
</p>
</div>
</div>
) : (
<>
<div className="mb-8">
<nav className="flex items-center space-x-2 text-sm font-medium text-slate-600 dark:text-slate-400 mb-4">
<Link href="/folder-viewer" className="hover:text-slate-900 dark:hover:text-slate-200 transition-colors">
Libraries
</Link>
<span>/</span>
<span className="text-slate-900 dark:text-slate-100 font-semibold">{path.split('/').pop()}</span>
</nav>
<h1 className="text-3xl font-bold bg-gradient-to-r from-slate-900 to-slate-700 dark:from-white dark:to-slate-300 bg-clip-text text-transparent">
{path.split('/').pop()}
</h1>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4">
{items.map((item) => ( {items.map((item) => (
<div key={item.name} <Card key={item.name} className="overflow-hidden rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-300 ease-in-out">
className="group relative bg-white dark:bg-slate-800 rounded-xl shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 overflow-hidden"> <CardContent className="p-0">
<Link href={item.isDirectory ? `/folder-viewer?path=${item.path}` : '#'}
className="block">
<div className="aspect-square relative overflow-hidden">
{item.isDirectory ? ( {item.isDirectory ? (
<div className="absolute inset-0 bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-900/20 dark:to-indigo-900/20 flex items-center justify-center"> <Link href={`/folder-viewer?path=${item.path}`} className="block">
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-2xl flex items-center justify-center shadow-lg" <div className="flex items-center justify-center h-48 bg-gray-100">
style={{ transform: 'perspective(100px) rotateY(-5deg) rotateX(5deg)' }}> <Folder className="text-blue-500" size={64} />
<Folder className="h-8 w-8 text-white" />
</div>
</div> </div>
</Link>
) : isMediaFile(item) ? ( ) : isMediaFile(item) ? (
<div className="relative w-full h-full"> <div className="relative">
<img <img
src={item.thumbnail || '/placeholder.svg'} src={item.thumbnail || '/placeholder.svg'}
alt={item.name} alt={item.name}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105" className="w-full h-48 object-cover"
onError={(e) => {
(e.target as HTMLImageElement).src = '/placeholder.svg';
}}
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> <div className="absolute top-2 right-2">
<div className="absolute bottom-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
{item.type === 'video' ? ( {item.type === 'video' ? (
<div className="bg-white/90 backdrop-blur-sm rounded-full p-2 shadow-lg"> <Film className="text-white bg-black bg-opacity-50 rounded-full p-1" size={20} />
<Film className="h-4 w-4 text-slate-800" />
</div>
) : ( ) : (
<div className="bg-white/90 backdrop-blur-sm rounded-full p-2 shadow-lg"> <Image className="text-white bg-black bg-opacity-50 rounded-full p-1" size={20} />
<Image className="h-4 w-4 text-slate-800" />
</div>
)} )}
</div> </div>
</div> </div>
) : ( ) : (
<div className="absolute inset-0 bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-700 dark:to-slate-800 flex items-center justify-center" <div className="flex items-center justify-center h-48 bg-gray-100">
style={{ background: 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)' }}>
<div className="w-16 h-16 bg-gradient-to-br from-slate-400 to-slate-600 rounded-2xl flex items-center justify-center shadow-lg"
style={{ transform: 'perspective(100px) rotateY(-5deg) rotateX(5deg)' }}>
{getFileIcon(item)} {getFileIcon(item)}
</div> </div>
</div>
)} )}
</div> </CardContent>
<div className="p-3"> <CardHeader className="p-4">
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100 truncate mb-1">{item.name}</p> <CardTitle className="text-sm font-semibold truncate">{item.name}</CardTitle>
<p className="text-xs text-slate-600 dark:text-slate-400">{formatFileSize(item.size)}</p> <CardDescription className="text-xs">
</div> <div>Size: {formatFileSize(item.size)}</div>
</Link> {!item.isDirectory && <div className="truncate">Type: {item.type || 'File'}</div>}
</div> </CardDescription>
</CardHeader>
</Card>
))} ))}
</div> </div>
{items.length === 0 && (
{items.length === 0 && !loading && ( <div className="text-center py-12">
<div className="text-center py-20"> <p className="text-gray-500">No items found in this directory.</p>
<div className="max-w-sm mx-auto">
<div className="w-16 h-16 bg-slate-100 dark:bg-slate-800 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Folder className="h-8 w-8 text-slate-400" />
</div>
<h3 className="text-lg font-semibold text-slate-700 dark:text-slate-300 mb-2">Empty Directory</h3>
<p className="text-sm text-slate-500 dark:text-slate-400">No files or folders found in this location.</p>
</div>
</div> </div>
)} )}
</>
)}
</div>
</div> </div>
); );
}; };

View File

@ -1,10 +1,8 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Link from 'next/link';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Image as ImageIcon } from 'lucide-react';
interface Photo { interface Photo {
id: number; id: number;
@ -45,82 +43,43 @@ export default function PhotosPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 flex items-center justify-center"> <div className="flex items-center justify-center h-full">
<div className="text-center"> <div className="text-lg">Loading photos...</div>
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-500 rounded-2xl flex items-center justify-center mx-auto mb-4 animate-pulse">
<ImageIcon className="h-8 w-8 text-white" />
</div>
<p className="text-slate-600 dark:text-slate-400 font-medium">Loading photos...</p>
</div>
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800"> <div className="p-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <h1 className="text-3xl font-bold mb-6">Photos</h1>
<div className="mb-8"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
<h1 className="text-4xl font-bold bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent mb-2">
Photos
</h1>
<p className="text-slate-600 dark:text-slate-400">
{photos.length} {photos.length === 1 ? 'photo' : 'photos'} found
</p>
</div>
{photos.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4">
{photos.map((photo) => ( {photos.map((photo) => (
<div key={photo.id} <Card key={photo.id} className="overflow-hidden">
className="group relative bg-white dark:bg-slate-800 rounded-xl shadow-sm hover:shadow-xl transition-all duration-300 hover:-translate-y-1 overflow-hidden cursor-pointer"> <CardContent className="p-0">
<div className="aspect-square relative overflow-hidden">
<img <img
src={photo.thumbnail} src={photo.thumbnail}
alt={photo.title} alt={photo.title}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105" className="w-full h-48 object-cover"
onError={(e) => { onError={(e) => {
(e.target as HTMLImageElement).src = '/placeholder-image.jpg'; (e.target as HTMLImageElement).src = '/placeholder-image.jpg';
}} }}
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> </CardContent>
<div className="absolute bottom-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300"> <CardHeader className="p-4">
<div className="bg-white/90 backdrop-blur-sm rounded-full p-2 shadow-lg"> <CardTitle className="text-sm truncate">{photo.title}</CardTitle>
<ImageIcon className="h-4 w-4 text-slate-800" /> <CardDescription className="text-xs">
</div> <div>Size: {formatFileSize(photo.size)}</div>
</div> <div className="truncate">Path: {photo.path}</div>
</div> </CardDescription>
<div className="p-3"> </CardHeader>
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100 truncate mb-1"> </Card>
{photo.title}
</p>
<p className="text-xs text-slate-600 dark:text-slate-400">
{formatFileSize(photo.size)}
</p>
</div>
</div>
))} ))}
</div> </div>
) : ( {photos.length === 0 && (
<div className="text-center py-20"> <div className="text-center py-12">
<div className="max-w-sm mx-auto"> <p className="text-gray-500">No photos found. Add media libraries to scan for photos.</p>
<div className="w-20 h-20 bg-gradient-to-br from-purple-100 to-pink-100 dark:from-purple-900/20 dark:to-pink-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
<ImageIcon className="h-10 w-10 text-purple-600 dark:text-purple-400" />
</div>
<h3 className="text-xl font-semibold text-slate-700 dark:text-slate-300 mb-2">
No Photos Found
</h3>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-4">
Add media libraries to scan for photos
</p>
<Link href="/settings">
<button className="bg-gradient-to-r from-purple-600 to-pink-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:shadow-lg transition-shadow">
Add Library
</button>
</Link>
</div>
</div> </div>
)} )}
</div> </div>
</div>
); );
} }

View File

@ -2,7 +2,6 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
@ -112,33 +111,35 @@ const SettingsPage = () => {
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800"> <div className="p-6 max-w-4xl mx-auto">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <Header title="Settings" />
<div className="mb-8">
<h1 className="text-4xl font-bold bg-gradient-to-r from-slate-900 to-slate-700 dark:from-white dark:to-slate-300 bg-clip-text text-transparent mb-2">
Settings
</h1>
<p className="text-slate-600 dark:text-slate-400">
Configure your media libraries and system preferences
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> {error && (
<div className="lg:col-span-2 space-y-8"> <Alert variant="destructive" className="mb-4">
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-6"> <AlertDescription>{error}</AlertDescription>
<div className="flex items-center gap-3 mb-6"> </Alert>
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center"> )}
<Folder className="h-5 w-5 text-white" />
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">Media Libraries</h2>
<p className="text-sm text-slate-600 dark:text-slate-400">Manage your media source directories</p>
</div>
</div>
<div className="space-y-4"> {scanStatus && (
<div className="flex gap-3"> <Alert className="mb-4">
<input <AlertDescription>{scanStatus}</AlertDescription>
</Alert>
)}
<div className="space-y-8">
<Card>
<CardHeader>
<CardTitle className="text-2xl flex items-center gap-2">
<Folder className="h-6 w-6" />
Manage Media Libraries
</CardTitle>
<CardDescription>
Add or remove media library paths to scan for videos and photos
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-2 mb-6">
<Input
type="text" type="text"
placeholder="/mnt/media or /path/to/media" placeholder="/mnt/media or /path/to/media"
value={newLibraryPath} value={newLibraryPath}
@ -147,146 +148,83 @@ const SettingsPage = () => {
setError(null); setError(null);
}} }}
onKeyPress={(e) => e.key === 'Enter' && addLibrary()} onKeyPress={(e) => e.key === 'Enter' && addLibrary()}
className="flex-1 px-4 py-3 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" className="flex-1"
/> />
<button <Button onClick={addLibrary} className="gap-2">
onClick={addLibrary}
disabled={!newLibraryPath.trim()}
className="px-4 py-3 bg-gradient-to-r from-blue-500 to-indigo-600 text-white font-medium rounded-xl hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<Plus size={16} /> <Plus size={16} />
Add Add Library
</button> </Button>
</div> </div>
{error && ( {libraries.length > 0 ? (
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl">
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
{libraries.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wider">
{libraries.length} {libraries.length === 1 ? 'Library' : 'Libraries'}
</h3>
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-lg font-semibold mb-3">Existing Libraries ({libraries.length})</h3>
{libraries.map((lib) => ( {libraries.map((lib) => (
<div key={lib.id} className="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-700 group hover:border-slate-300 dark:hover:border-slate-600 transition-all"> <div key={lib.id} className="flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50">
<div className="flex items-center gap-3 flex-1 min-w-0"> <div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-gradient-to-br from-slate-400 to-slate-600 rounded-lg flex items-center justify-center"> <HardDrive className="h-5 w-5 text-gray-500 flex-shrink-0" />
<HardDrive className="h-4 w-4 text-white" /> <span className="font-mono text-sm truncate">{lib.path}</span>
</div> </div>
<div className="min-w-0"> <Button
<p className="text-sm font-mono text-slate-900 dark:text-slate-100 truncate">{lib.path}</p> variant="destructive"
<p className="text-xs text-slate-500 dark:text-slate-400">Ready to scan</p> size="sm"
</div>
</div>
<button
onClick={() => deleteLibrary(lib.id)} onClick={() => deleteLibrary(lib.id)}
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-all" className="gap-2"
> >
<Trash2 size={16} /> <Trash2 size={14} />
</button> Delete
</Button>
</div> </div>
))} ))}
</div> </div>
) : (
<div className="text-center py-8 text-gray-500">
<Folder className="h-12 w-12 mx-auto mb-2 text-gray-300" />
<p>No media libraries configured yet.</p>
<p className="text-sm">Add your first library above to get started.</p>
</div> </div>
)} )}
</CardContent>
</Card>
{libraries.length === 0 && ( <Card>
<div className="text-center py-12"> <CardHeader>
<div className="w-16 h-16 bg-slate-100 dark:bg-slate-800 rounded-2xl flex items-center justify-center mx-auto mb-4"> <CardTitle className="text-2xl">Media Scanner</CardTitle>
<Folder className="h-8 w-8 text-slate-400" /> <CardDescription>
</div> Scan your libraries for new videos and photos
<p className="text-slate-600 dark:text-slate-400">No libraries configured</p> </CardDescription>
<p className="text-sm text-slate-500 dark:text-slate-500 mt-1">Add your first library to get started</p> </CardHeader>
</div> <CardContent>
)} <Button
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 bg-gradient-to-br from-green-500 to-emerald-600 rounded-xl flex items-center justify-center">
<svg className="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 dark:text-slate-100">Media Scanner</h2>
<p className="text-sm text-slate-600 dark:text-slate-400">Discover new media files</p>
</div>
</div>
<div className="space-y-4">
<button
onClick={scanLibraries} onClick={scanLibraries}
disabled={isScanning || libraries.length === 0} disabled={isScanning || libraries.length === 0}
className="w-full px-4 py-3 bg-gradient-to-r from-green-500 to-emerald-600 text-white font-medium rounded-xl hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2" className="gap-2"
> >
{isScanning ? ( {isScanning ? "Scanning..." : "Scan Libraries"}
<> </Button>
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div> <p className="text-sm text-gray-500 mt-2">
Scanning...
</>
) : (
<>Scan Libraries</>
)}
</button>
{scanStatus && (
<div className={`p-3 rounded-xl ${scanStatus.includes('success') ? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800' : 'bg-slate-50 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-700'}`}>
<p className={`text-sm ${scanStatus.includes('success') ? 'text-green-600 dark:text-green-400' : 'text-slate-600 dark:text-slate-400'}`}>
{scanStatus}
</p>
</div>
)}
<p className="text-sm text-slate-500 dark:text-slate-400 text-center">
{libraries.length === 0 {libraries.length === 0
? "Add at least one library to enable scanning" ? "Add at least one library to enable scanning"
: "Scan will discover new videos and photos"} : "This will scan all configured libraries for new media files"}
</p> </p>
</div> </CardContent>
</div> </Card>
</div>
<div className="space-y-6"> <Card>
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-6"> <CardHeader>
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-4">System Status</h3> <CardTitle className="text-2xl">System Information</CardTitle>
<div className="space-y-3"> </CardHeader>
<div className="flex justify-between items-center p-3 bg-slate-50 dark:bg-slate-900/50 rounded-lg"> <CardContent className="space-y-2">
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">Libraries</span> <div className="flex justify-between">
<span className="text-sm font-bold text-slate-900 dark:text-slate-100">{libraries.length}</span> <span className="text-gray-600">Libraries:</span>
</div> <span className="font-semibold">{libraries.length}</span>
<div className="flex justify-between items-center p-3 bg-slate-50 dark:bg-slate-900/50 rounded-lg">
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">Database</span>
<span className="text-sm font-bold text-slate-900 dark:text-slate-100">SQLite</span>
</div>
<div className="flex justify-between items-center p-3 bg-slate-50 dark:bg-slate-900/50 rounded-lg">
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">Status</span>
<span className="text-sm font-bold text-green-600 dark:text-green-400">Active</span>
</div>
</div>
</div>
<div className="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-6">
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-4">Quick Actions</h3>
<div className="space-y-2">
<Link href="/videos" className="block w-full px-3 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 rounded-lg transition-colors">
View Videos
</Link>
<Link href="/photos" className="block w-full px-3 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 rounded-lg transition-colors">
View Photos
</Link>
<Link href="/folder-viewer" className="block w-full px-3 py-2 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 rounded-lg transition-colors">
Browse Files
</Link>
</div>
</div>
</div> </div>
<div className="flex justify-between">
<span className="text-gray-600">Database:</span>
<span className="font-semibold">SQLite</span>
</div> </div>
</CardContent>
</Card>
</div> </div>
</div> </div>
); );

View File

@ -2,8 +2,14 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Link from "next/link"; import {
import { Film } from "lucide-react"; Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Header } from "@/components/ui/header";
interface Video { interface Video {
id: number; id: number;
@ -43,83 +49,44 @@ const VideosPage = () => {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 flex items-center justify-center"> <div className="flex items-center justify-center h-full">
<div className="text-center"> <div className="text-lg">Loading videos...</div>
<div className="w-16 h-16 bg-gradient-to-br from-red-500 to-orange-500 rounded-2xl flex items-center justify-center mx-auto mb-4 animate-pulse">
<Film className="h-8 w-8 text-white" />
</div>
<p className="text-slate-600 dark:text-slate-400 font-medium">Loading videos...</p>
</div>
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800"> <div className="p-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <Header title="Videos" />
<div className="mb-8"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
<h1 className="text-4xl font-bold bg-gradient-to-r from-red-600 to-orange-600 bg-clip-text text-transparent mb-2">
Videos
</h1>
<p className="text-slate-600 dark:text-slate-400">
{videos.length} {videos.length === 1 ? 'video' : 'videos'} found
</p>
</div>
{videos.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4">
{videos.map((video) => ( {videos.map((video) => (
<div key={video.id} <Card key={video.id} className="overflow-hidden rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-300 ease-in-out">
className="group relative bg-white dark:bg-slate-800 rounded-xl shadow-sm hover:shadow-xl transition-all duration-300 hover:-translate-y-1 overflow-hidden cursor-pointer"> <CardContent className="p-0">
<div className="aspect-video relative overflow-hidden">
<img <img
src={video.thumbnail || "/placeholder.svg"} src={video.thumbnail || "/placeholder.svg"}
alt={video.title} alt={video.title}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105" className="w-full h-48 object-cover"
onError={(e) => { onError={(e) => {
(e.target as HTMLImageElement).src = '/placeholder.svg'; (e.target as HTMLImageElement).src = '/placeholder.svg';
}} }}
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> </CardContent>
<div className="absolute bottom-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300"> <CardHeader className="p-4">
<div className="bg-white/90 backdrop-blur-sm rounded-full p-2 shadow-lg"> <CardTitle className="text-sm font-semibold truncate">{video.title}</CardTitle>
<Film className="h-4 w-4 text-slate-800" /> <CardDescription className="text-xs">
</div> <div>Size: {formatFileSize(video.size)}</div>
</div> <div className="truncate">Path: {video.path}</div>
</div> </CardDescription>
<div className="p-3"> </CardHeader>
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100 truncate mb-1"> </Card>
{video.title}
</p>
<p className="text-xs text-slate-600 dark:text-slate-400">
{formatFileSize(video.size)}
</p>
</div>
</div>
))} ))}
</div> </div>
) : ( {videos.length === 0 && (
<div className="text-center py-20"> <div className="text-center py-12">
<div className="max-w-sm mx-auto"> <p className="text-gray-500">No videos found. Add media libraries to scan for videos.</p>
<div className="w-20 h-20 bg-gradient-to-br from-red-100 to-orange-100 dark:from-red-900/20 dark:to-orange-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
<Film className="h-10 w-10 text-red-600 dark:text-red-400" />
</div>
<h3 className="text-xl font-semibold text-slate-700 dark:text-slate-300 mb-2">
No Videos Found
</h3>
<p className="text-sm text-slate-500 dark:text-slate-400 mb-4">
Add media libraries to scan for videos
</p>
<Link href="/settings">
<button className="bg-gradient-to-r from-red-600 to-orange-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:shadow-lg transition-shadow">
Add Library
</button>
</Link>
</div>
</div> </div>
)} )}
</div> </div>
</div>
); );
}; };

View File

@ -11,32 +11,24 @@ import {
Video, Video,
Image, Image,
Folder, Folder,
Film,
Image as ImageIcon,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation"; import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Suspense } from "react";
const SidebarContent = () => { const Sidebar = () => {
const [isCollapsed, setIsCollapsed] = useState(false); const [isCollapsed, setIsCollapsed] = useState(false);
const [libraries, setLibraries] = useState<{ id: number; path: string }[]>([]); const [libraries, setLibraries] = useState<{ id: number; path: string }[]>([]);
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => { useEffect(() => {
fetchLibraries(); fetchLibraries();
}, []); }, []);
const fetchLibraries = async () => { const fetchLibraries = async () => {
try {
const res = await fetch("/api/libraries"); const res = await fetch("/api/libraries");
const data = await res.json(); const data = await res.json();
setLibraries(data); setLibraries(data);
} catch (error) {
console.error('Error fetching libraries:', error);
}
}; };
const toggleSidebar = () => { const toggleSidebar = () => {
@ -45,70 +37,48 @@ const SidebarContent = () => {
const navItems = [ const navItems = [
{ href: "/", label: "Home", icon: Home }, { href: "/", label: "Home", icon: Home },
{ href: "/videos", label: "Videos", icon: Film },
{ href: "/photos", label: "Photos", icon: ImageIcon },
{ href: "/settings", label: "Settings", icon: Settings }, { href: "/settings", label: "Settings", icon: Settings },
{ href: "/videos", label: "Videos", icon: Video },
{ href: "/photos", label: "Photos", icon: Image },
]; ];
return ( return (
<div <div
className={cn( className={cn(
"flex flex-col bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 transition-all duration-300 ease-in-out", "flex flex-col bg-gray-100 dark:bg-gray-900 text-gray-700 dark:text-gray-200 border-r border-gray-200 dark:border-gray-800 transition-all duration-300",
isCollapsed ? "w-20" : "w-72" isCollapsed ? "w-20" : "w-64"
)} )}
> >
{/* Header */} <div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800">
<div className="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-800">
{!isCollapsed && ( {!isCollapsed && (
<div className="flex items-center gap-2"> <h1 className="text-2xl font-bold text-primary">NextAV</h1>
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
<div className="w-4 h-4 bg-white rounded-sm" />
</div>
<h1 className="text-lg font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">NextAV</h1>
</div>
)} )}
<Button <Button onClick={toggleSidebar} variant="ghost" size="icon">
onClick={toggleSidebar} {isCollapsed ? <ChevronRight /> : <ChevronLeft />}
variant="ghost"
size="icon"
className="hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg"
>
{isCollapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
</Button> </Button>
</div> </div>
<nav className="flex-1 mt-4 space-y-2 px-4">
{/* Navigation */}
<nav className="flex-1 px-3 py-4 space-y-1">
{navItems.map((item) => ( {navItems.map((item) => (
<Link key={item.href} href={item.href} passHref> <Link href={item.href} key={item.href} passHref>
<Button <Button
variant="ghost" variant={pathname === item.href ? "secondary" : "ghost"}
className={cn( className="w-full justify-start"
"w-full justify-start text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-all",
pathname === item.href && "bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400"
)}
> >
<item.icon className={cn( <item.icon className="mr-4 h-5 w-5" />
"h-5 w-5 transition-colors", {!isCollapsed && item.label}
pathname === item.href ? "text-blue-600 dark:text-blue-400" : "text-slate-500 dark:text-slate-400"
)} />
{!isCollapsed && <span className="ml-3">{item.label}</span>}
</Button> </Button>
</Link> </Link>
))} ))}
{/* Libraries Section */}
{libraries.length > 0 && (
<div className="pt-4"> <div className="pt-4">
<h2 <h2
className={cn( className={cn(
"text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider px-3 mb-2", "text-lg font-semibold p-2",
isCollapsed && "text-center text-[10px]" isCollapsed && "text-center"
)} )}
> >
{!isCollapsed ? "Libraries" : "Libs"} {!isCollapsed ? "Folder Viewer" : "Folders"}
</h2> </h2>
<div className="space-y-1"> <div className="flex flex-col space-y-2">
{libraries.map((lib) => ( {libraries.map((lib) => (
<Link <Link
href={`/folder-viewer?path=${lib.path}`} href={`/folder-viewer?path=${lib.path}`}
@ -116,45 +86,26 @@ const SidebarContent = () => {
passHref passHref
> >
<Button <Button
variant="ghost" variant={
className={cn(
"w-full justify-start text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-all",
pathname === "/folder-viewer" && pathname === "/folder-viewer" &&
searchParams.get("path") === lib.path && new URLSearchParams(window.location.search).get("path") ===
"bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-slate-100" lib.path
)} ? "secondary"
: "ghost"
}
className="w-full justify-start"
> >
<Folder className="h-4 w-4 text-slate-400 dark:text-slate-500" /> <Folder className="mr-4 h-5 w-5" />
{!isCollapsed && ( {!isCollapsed && (
<span className="ml-3 text-sm truncate">{lib.path.split('/').pop() || lib.path}</span> <span className="truncate">{lib.path}</span>
)} )}
</Button> </Button>
</Link> </Link>
))} ))}
</div> </div>
</div> </div>
)}
</nav> </nav>
{/* Footer */}
<div className="p-4 border-t border-slate-200 dark:border-slate-800">
<div className={cn(
"text-center text-xs text-slate-500 dark:text-slate-400",
isCollapsed && "text-[10px]"
)}
>
{!isCollapsed ? "NextAV v1.0" : "v1.0"}
</div> </div>
</div>
</div>
);
};
const Sidebar = () => {
return (
<Suspense fallback={<div className="w-20 bg-slate-50 dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800" />}>
<SidebarContent />
</Suspense>
); );
}; };