Compare commits
No commits in common. "90ba6df611c5262598ad379408208f83e9731f8f" and "e248613abb106546e611d2fbd05b2b8849a94889" have entirely different histories.
90ba6df611
...
e248613abb
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 MiB |
|
|
@ -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 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>
|
||||
)}
|
||||
|
||||
<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">
|
||||
<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) => (
|
||||
<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">
|
||||
<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 ? (
|
||||
<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>
|
||||
<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 w-full h-full">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={item.thumbnail || '/placeholder.svg'}
|
||||
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 bottom-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="absolute top-2 right-2">
|
||||
{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>
|
||||
<Film className="text-white bg-black bg-opacity-50 rounded-full p-1" size={20} />
|
||||
) : (
|
||||
<div className="bg-white/90 backdrop-blur-sm rounded-full p-2 shadow-lg">
|
||||
<Image className="h-4 w-4 text-slate-800" />
|
||||
</div>
|
||||
<Image className="text-white bg-black bg-opacity-50 rounded-full p-1" size={20} />
|
||||
)}
|
||||
</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)' }}>
|
||||
<div className="flex items-center justify-center h-48 bg-gray-100">
|
||||
{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>
|
||||
</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 && !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>
|
||||
{items.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500">No items found in this directory.</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<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) => (
|
||||
<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">
|
||||
<Card key={photo.id} className="overflow-hidden">
|
||||
<CardContent className="p-0">
|
||||
<img
|
||||
src={photo.thumbnail}
|
||||
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) => {
|
||||
(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>
|
||||
</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>
|
||||
) : (
|
||||
<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>
|
||||
{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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,33 +111,35 @@ 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
|
||||
{scanStatus && (
|
||||
<Alert className="mb-4">
|
||||
<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"
|
||||
placeholder="/mnt/media or /path/to/media"
|
||||
value={newLibraryPath}
|
||||
|
|
@ -147,146 +148,83 @@ const SettingsPage = () => {
|
|||
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"
|
||||
className="flex-1"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
<Button onClick={addLibrary} className="gap-2">
|
||||
<Plus size={16} />
|
||||
Add
|
||||
</button>
|
||||
Add Library
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
{libraries.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<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 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="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" />
|
||||
<HardDrive className="h-5 w-5 text-gray-500 flex-shrink-0" />
|
||||
<span className="font-mono text-sm truncate">{lib.path}</span>
|
||||
</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
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
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} />
|
||||
</button>
|
||||
<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>
|
||||
|
||||
{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>
|
||||
|
||||
<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
|
||||
<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="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 ? (
|
||||
<>
|
||||
<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">
|
||||
{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"
|
||||
: "Scan will discover new videos and photos"}
|
||||
: "This will scan all configured libraries for new media files"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<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 className="flex justify-between">
|
||||
<span className="text-gray-600">Database:</span>
|
||||
<span className="font-semibold">SQLite</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,83 +49,44 @@ 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">
|
||||
<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) => (
|
||||
<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">
|
||||
<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-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 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>
|
||||
</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>
|
||||
) : (
|
||||
<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>
|
||||
{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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 toggleSidebar = () => {
|
||||
|
|
@ -45,70 +37,48 @@ 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]"
|
||||
"text-lg font-semibold p-2",
|
||||
isCollapsed && "text-center"
|
||||
)}
|
||||
>
|
||||
{!isCollapsed ? "Libraries" : "Libs"}
|
||||
{!isCollapsed ? "Folder Viewer" : "Folders"}
|
||||
</h2>
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-col space-y-2">
|
||||
{libraries.map((lib) => (
|
||||
<Link
|
||||
href={`/folder-viewer?path=${lib.path}`}
|
||||
|
|
@ -116,45 +86,26 @@ const SidebarContent = () => {
|
|||
passHref
|
||||
>
|
||||
<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",
|
||||
variant={
|
||||
pathname === "/folder-viewer" &&
|
||||
searchParams.get("path") === lib.path &&
|
||||
"bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-slate-100"
|
||||
)}
|
||||
new URLSearchParams(window.location.search).get("path") ===
|
||||
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 && (
|
||||
<span className="ml-3 text-sm truncate">{lib.path.split('/').pop() || lib.path}</span>
|
||||
<span className="truncate">{lib.path}</span>
|
||||
)}
|
||||
</Button>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue