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 path = searchParams.get("path");
const [items, setItems] = useState<FileSystemItem[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (path) {
@ -29,16 +28,9 @@ const FolderViewerPage = () => {
}, [path]);
const fetchItems = async (currentPath: string) => {
setLoading(true);
try {
const res = await fetch(`/api/files?path=${currentPath}`);
const data = await res.json();
setItems(data);
} catch (error) {
console.error('Error fetching items:', error);
} finally {
setLoading(false);
}
const res = await fetch(`/api/files?path=${currentPath}`);
const data = await res.json();
setItems(data);
};
const formatFileSize = (bytes: number) => {
@ -72,113 +64,57 @@ const FolderViewerPage = () => {
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
{loading && (
<div className="fixed inset-0 bg-black/5 backdrop-blur-sm z-50 flex items-center justify-center">
<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 className="p-6">
<h1 className="text-3xl font-bold mb-6 truncate">{path}</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{items.map((item) => (
<Card key={item.name} className="overflow-hidden rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-300 ease-in-out">
<CardContent className="p-0">
{item.isDirectory ? (
<Link href={`/folder-viewer?path=${item.path}`} className="block">
<div className="flex items-center justify-center h-48 bg-gray-100">
<Folder className="text-blue-500" size={64} />
</div>
</Link>
) : isMediaFile(item) ? (
<div className="relative">
<img
src={item.thumbnail || '/placeholder.svg'}
alt={item.name}
className="w-full h-48 object-cover"
onError={(e) => {
(e.target as HTMLImageElement).src = '/placeholder.svg';
}}
/>
<div className="absolute top-2 right-2">
{item.type === 'video' ? (
<Film className="text-white bg-black bg-opacity-50 rounded-full p-1" size={20} />
) : (
<Image className="text-white bg-black bg-opacity-50 rounded-full p-1" size={20} />
)}
</div>
</div>
) : (
<div className="flex items-center justify-center h-48 bg-gray-100">
{getFileIcon(item)}
</div>
)}
</CardContent>
<CardHeader className="p-4">
<CardTitle className="text-sm font-semibold truncate">{item.name}</CardTitle>
<CardDescription className="text-xs">
<div>Size: {formatFileSize(item.size)}</div>
{!item.isDirectory && <div className="truncate">Type: {item.type || 'File'}</div>}
</CardDescription>
</CardHeader>
</Card>
))}
</div>
{items.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-500">No items found in this directory.</p>
</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) => (
<div key={item.name}
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">
<Link href={item.isDirectory ? `/folder-viewer?path=${item.path}` : '#'}
className="block">
<div className="aspect-square relative overflow-hidden">
{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">
<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"
style={{ transform: 'perspective(100px) rotateY(-5deg) rotateX(5deg)' }}>
<Folder className="h-8 w-8 text-white" />
</div>
</div>
) : isMediaFile(item) ? (
<div className="relative w-full h-full">
<img
src={item.thumbnail || '/placeholder.svg'}
alt={item.name}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
<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 bottom-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
{item.type === 'video' ? (
<div className="bg-white/90 backdrop-blur-sm rounded-full p-2 shadow-lg">
<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="h-4 w-4 text-slate-800" />
</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"
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)}
</div>
</div>
)}
</div>
<div className="p-3">
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100 truncate mb-1">{item.name}</p>
<p className="text-xs text-slate-600 dark:text-slate-400">{formatFileSize(item.size)}</p>
</div>
</Link>
</div>
))}
</div>
{items.length === 0 && !loading && (
<div className="text-center py-20">
<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>
);
};

View File

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

View File

@ -2,7 +2,6 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
@ -112,181 +111,120 @@ const SettingsPage = () => {
};
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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<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="p-6 max-w-4xl mx-auto">
<Header title="Settings" />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-8">
<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-6">
<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>
{error && (
<Alert variant="destructive" className="mb-4">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="space-y-4">
<div className="flex gap-3">
<input
type="text"
placeholder="/mnt/media or /path/to/media"
value={newLibraryPath}
onChange={(e) => {
setNewLibraryPath(e.target.value);
setError(null);
}}
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"
/>
<button
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} />
Add
</button>
</div>
{scanStatus && (
<Alert className="mb-4">
<AlertDescription>{scanStatus}</AlertDescription>
</Alert>
)}
{error && (
<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">
{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 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-4 w-4 text-white" />
</div>
<div className="min-w-0">
<p className="text-sm font-mono text-slate-900 dark:text-slate-100 truncate">{lib.path}</p>
<p className="text-xs text-slate-500 dark:text-slate-400">Ready to scan</p>
</div>
</div>
<button
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"
>
<Trash2 size={16} />
</button>
</div>
))}
</div>
</div>
)}
{libraries.length === 0 && (
<div className="text-center py-12">
<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>
<p className="text-slate-600 dark:text-slate-400">No libraries configured</p>
<p className="text-sm text-slate-500 dark:text-slate-500 mt-1">Add your first library to get started</p>
</div>
)}
</div>
<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"
placeholder="/mnt/media or /path/to/media"
value={newLibraryPath}
onChange={(e) => {
setNewLibraryPath(e.target.value);
setError(null);
}}
onKeyPress={(e) => e.key === 'Enter' && addLibrary()}
className="flex-1"
/>
<Button onClick={addLibrary} className="gap-2">
<Plus size={16} />
Add Library
</Button>
</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}
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"
>
{isScanning ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
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
? "Add at least one library to enable scanning"
: "Scan will discover new videos and photos"}
</p>
</div>
</div>
</div>
<div className="space-y-6">
<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">System Status</h3>
<div className="space-y-3">
<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">Libraries</span>
<span className="text-sm font-bold text-slate-900 dark:text-slate-100">{libraries.length}</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">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>
{libraries.length > 0 ? (
<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>
<h3 className="text-lg font-semibold mb-3">Existing Libraries ({libraries.length})</h3>
{libraries.map((lib) => (
<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">
<HardDrive className="h-5 w-5 text-gray-500 flex-shrink-0" />
<span className="font-mono text-sm truncate">{lib.path}</span>
</div>
<Button
variant="destructive"
size="sm"
onClick={() => deleteLibrary(lib.id)}
className="gap-2"
>
<Trash2 size={14} />
Delete
</Button>
</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>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Media Scanner</CardTitle>
<CardDescription>
Scan your libraries for new videos and photos
</CardDescription>
</CardHeader>
<CardContent>
<Button
onClick={scanLibraries}
disabled={isScanning || libraries.length === 0}
className="gap-2"
>
{isScanning ? "Scanning..." : "Scan Libraries"}
</Button>
<p className="text-sm text-gray-500 mt-2">
{libraries.length === 0
? "Add at least one library to enable scanning"
: "This will scan all configured libraries for new media files"}
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-2xl">System Information</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-600">Libraries:</span>
<span className="font-semibold">{libraries.length}</span>
</div>
</div>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Database:</span>
<span className="font-semibold">SQLite</span>
</div>
</CardContent>
</Card>
</div>
</div>
);

View File

@ -2,8 +2,14 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { Film } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Header } from "@/components/ui/header";
interface Video {
id: number;
@ -43,82 +49,43 @@ const VideosPage = () => {
if (loading) {
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="text-center">
<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 className="flex items-center justify-center h-full">
<div className="text-lg">Loading videos...</div>
</div>
);
}
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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<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) => (
<div key={video.id}
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">
<div className="aspect-video relative overflow-hidden">
<img
src={video.thumbnail || "/placeholder.svg"}
alt={video.title}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
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 bottom-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="bg-white/90 backdrop-blur-sm rounded-full p-2 shadow-lg">
<Film className="h-4 w-4 text-slate-800" />
</div>
</div>
</div>
<div className="p-3">
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100 truncate mb-1">
{video.title}
</p>
<p className="text-xs text-slate-600 dark:text-slate-400">
{formatFileSize(video.size)}
</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-20">
<div className="max-w-sm mx-auto">
<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 className="p-6">
<Header title="Videos" />
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{videos.map((video) => (
<Card key={video.id} className="overflow-hidden rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-300 ease-in-out">
<CardContent className="p-0">
<img
src={video.thumbnail || "/placeholder.svg"}
alt={video.title}
className="w-full h-48 object-cover"
onError={(e) => {
(e.target as HTMLImageElement).src = '/placeholder.svg';
}}
/>
</CardContent>
<CardHeader className="p-4">
<CardTitle className="text-sm font-semibold truncate">{video.title}</CardTitle>
<CardDescription className="text-xs">
<div>Size: {formatFileSize(video.size)}</div>
<div className="truncate">Path: {video.path}</div>
</CardDescription>
</CardHeader>
</Card>
))}
</div>
{videos.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-500">No videos found. Add media libraries to scan for videos.</p>
</div>
)}
</div>
);
};

View File

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