feat: enhance folder viewer with breadcrumb navigation and back button
- Implemented breadcrumb navigation for easier path tracking within the folder viewer. - Added a back button to navigate to the parent directory, improving user experience. - Introduced a utility function to format file paths for better readability. - Updated the UI to display formatted file paths and current directory titles.
This commit is contained in:
parent
224b898bcd
commit
6744a2736b
|
|
@ -2,8 +2,8 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, Suspense } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Folder, File, Image, Film, Play } from "lucide-react";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { Folder, File, Image, Film, Play, ChevronLeft, Home } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import PhotoViewer from "@/components/photo-viewer";
|
||||
|
|
@ -20,8 +20,14 @@ interface FileSystemItem {
|
|||
id?: number;
|
||||
}
|
||||
|
||||
interface BreadcrumbItem {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
const FolderViewerPage = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const path = searchParams.get("path");
|
||||
const [items, setItems] = useState<FileSystemItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -59,6 +65,85 @@ const FolderViewerPage = () => {
|
|||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatFilePath = (path: string) => {
|
||||
if (!path) return '';
|
||||
|
||||
// Split path into directory and filename
|
||||
const lastSlashIndex = path.lastIndexOf('/');
|
||||
const lastBackslashIndex = path.lastIndexOf('\\');
|
||||
const lastSeparatorIndex = Math.max(lastSlashIndex, lastBackslashIndex);
|
||||
|
||||
if (lastSeparatorIndex === -1) {
|
||||
// No directory separator found, return as is
|
||||
return path;
|
||||
}
|
||||
|
||||
const directory = path.substring(0, lastSeparatorIndex);
|
||||
const filename = path.substring(lastSeparatorIndex + 1);
|
||||
|
||||
// If directory is short enough, show it all
|
||||
if (directory.length <= 30) {
|
||||
return `${directory}/${filename}`;
|
||||
}
|
||||
|
||||
// Truncate directory with ellipsis in the middle
|
||||
const maxDirLength = 25;
|
||||
const startLength = Math.floor(maxDirLength / 2);
|
||||
const endLength = maxDirLength - startLength - 3; // -3 for "..."
|
||||
|
||||
const truncatedDir = directory.length > maxDirLength
|
||||
? `${directory.substring(0, startLength)}...${directory.substring(directory.length - endLength)}`
|
||||
: directory;
|
||||
|
||||
return `${truncatedDir}/${filename}`;
|
||||
};
|
||||
|
||||
const getBreadcrumbs = (currentPath: string): BreadcrumbItem[] => {
|
||||
if (!currentPath) return [];
|
||||
|
||||
const pathParts = currentPath.split('/').filter(part => part.length > 0);
|
||||
const breadcrumbs: BreadcrumbItem[] = [];
|
||||
|
||||
// Add root/home
|
||||
breadcrumbs.push({ name: 'Home', path: '' });
|
||||
|
||||
// Build breadcrumbs for each path level
|
||||
let accumulatedPath = '';
|
||||
pathParts.forEach((part, index) => {
|
||||
accumulatedPath += (accumulatedPath ? '/' : '') + part;
|
||||
breadcrumbs.push({
|
||||
name: part,
|
||||
path: accumulatedPath
|
||||
});
|
||||
});
|
||||
|
||||
return breadcrumbs;
|
||||
};
|
||||
|
||||
const getParentPath = (currentPath: string): string => {
|
||||
if (!currentPath) return '';
|
||||
const pathParts = currentPath.split('/').filter(part => part.length > 0);
|
||||
if (pathParts.length <= 1) return '';
|
||||
return pathParts.slice(0, -1).join('/');
|
||||
};
|
||||
|
||||
const handleBackClick = () => {
|
||||
const parentPath = getParentPath(path || '');
|
||||
if (parentPath) {
|
||||
router.push(`/folder-viewer?path=${parentPath}`);
|
||||
} else {
|
||||
router.push('/folder-viewer');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBreadcrumbClick = (breadcrumbPath: string) => {
|
||||
if (breadcrumbPath === '') {
|
||||
router.push('/folder-viewer');
|
||||
} else {
|
||||
router.push(`/folder-viewer?path=${breadcrumbPath}`);
|
||||
}
|
||||
};
|
||||
|
||||
const getFileIcon = (item: FileSystemItem) => {
|
||||
if (item.isDirectory) return <Folder className="text-blue-500" size={48} />;
|
||||
if (item.type === 'photo') return <Image className="text-green-500" size={48} />;
|
||||
|
|
@ -156,15 +241,50 @@ const FolderViewerPage = () => {
|
|||
) : (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<nav className="flex items-center space-x-2 text-sm font-medium text-zinc-400 mb-4">
|
||||
<Link href="/folder-viewer" className="hover:text-white transition-colors">
|
||||
Libraries
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-white font-semibold">{path.split('/').pop()}</span>
|
||||
{/* Back Button */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBackClick}
|
||||
className="text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors"
|
||||
disabled={!getParentPath(path || '')}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 mr-2" />
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Breadcrumb Navigation */}
|
||||
<nav className="flex items-center flex-wrap gap-2 text-sm font-medium text-zinc-400 mb-4">
|
||||
{getBreadcrumbs(path || '').map((breadcrumb, index) => (
|
||||
<div key={breadcrumb.path} className="flex items-center gap-2">
|
||||
{index > 0 && <span className="text-zinc-600">/</span>}
|
||||
<button
|
||||
onClick={() => handleBreadcrumbClick(breadcrumb.path)}
|
||||
className={`hover:text-white transition-colors ${
|
||||
index === getBreadcrumbs(path || '').length - 1
|
||||
? 'text-white font-semibold cursor-default'
|
||||
: 'hover:underline cursor-pointer'
|
||||
}`}
|
||||
disabled={index === getBreadcrumbs(path || '').length - 1}
|
||||
>
|
||||
{breadcrumb.name === 'Home' ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Home className="h-3 w-3" />
|
||||
<span>Home</span>
|
||||
</div>
|
||||
) : (
|
||||
breadcrumb.name
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Current Directory Title */}
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">
|
||||
{path.split('/').pop()}
|
||||
{path ? path.split('/').pop() : 'Libraries'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
|
@ -244,7 +364,17 @@ const FolderViewerPage = () => {
|
|||
</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 className="flex items-center justify-between">
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">{formatFileSize(item.size)}</p>
|
||||
{!item.isDirectory && (
|
||||
<p
|
||||
className="text-xs text-slate-500 dark:text-slate-500 truncate ml-2 cursor-help"
|
||||
title={item.path}
|
||||
>
|
||||
{formatFilePath(item.path)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -52,6 +52,39 @@ export default function PhotosPage() {
|
|||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatFilePath = (path: string) => {
|
||||
if (!path) return '';
|
||||
|
||||
// Split path into directory and filename
|
||||
const lastSlashIndex = path.lastIndexOf('/');
|
||||
const lastBackslashIndex = path.lastIndexOf('\\');
|
||||
const lastSeparatorIndex = Math.max(lastSlashIndex, lastBackslashIndex);
|
||||
|
||||
if (lastSeparatorIndex === -1) {
|
||||
// No directory separator found, return as is
|
||||
return path;
|
||||
}
|
||||
|
||||
const directory = path.substring(0, lastSeparatorIndex);
|
||||
const filename = path.substring(lastSeparatorIndex + 1);
|
||||
|
||||
// If directory is short enough, show it all
|
||||
if (directory.length <= 30) {
|
||||
return `${directory}/${filename}`;
|
||||
}
|
||||
|
||||
// Truncate directory with ellipsis in the middle
|
||||
const maxDirLength = 25;
|
||||
const startLength = Math.floor(maxDirLength / 2);
|
||||
const endLength = maxDirLength - startLength - 3; // -3 for "..."
|
||||
|
||||
const truncatedDir = directory.length > maxDirLength
|
||||
? `${directory.substring(0, startLength)}...${directory.substring(directory.length - endLength)}`
|
||||
: directory;
|
||||
|
||||
return `${truncatedDir}/${filename}`;
|
||||
};
|
||||
|
||||
const filteredPhotos = photos.filter(photo =>
|
||||
photo.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
photo.path.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
|
|
@ -230,9 +263,12 @@ export default function PhotosPage() {
|
|||
<span>{formatFileSize(photo.size)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">
|
||||
{photo.path}
|
||||
</p>
|
||||
<p
|
||||
className="text-xs text-muted-foreground mt-1 line-clamp-1 cursor-help"
|
||||
title={photo.path}
|
||||
>
|
||||
{formatFilePath(photo.path)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,39 @@ const VideosPage = () => {
|
|||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatFilePath = (path: string) => {
|
||||
if (!path) return '';
|
||||
|
||||
// Split path into directory and filename
|
||||
const lastSlashIndex = path.lastIndexOf('/');
|
||||
const lastBackslashIndex = path.lastIndexOf('\\');
|
||||
const lastSeparatorIndex = Math.max(lastSlashIndex, lastBackslashIndex);
|
||||
|
||||
if (lastSeparatorIndex === -1) {
|
||||
// No directory separator found, return as is
|
||||
return path;
|
||||
}
|
||||
|
||||
const directory = path.substring(0, lastSeparatorIndex);
|
||||
const filename = path.substring(lastSeparatorIndex + 1);
|
||||
|
||||
// If directory is short enough, show it all
|
||||
if (directory.length <= 30) {
|
||||
return `${directory}/${filename}`;
|
||||
}
|
||||
|
||||
// Truncate directory with ellipsis in the middle
|
||||
const maxDirLength = 25;
|
||||
const startLength = Math.floor(maxDirLength / 2);
|
||||
const endLength = maxDirLength - startLength - 3; // -3 for "..."
|
||||
|
||||
const truncatedDir = directory.length > maxDirLength
|
||||
? `${directory.substring(0, startLength)}...${directory.substring(directory.length - endLength)}`
|
||||
: directory;
|
||||
|
||||
return `${truncatedDir}/${filename}`;
|
||||
};
|
||||
|
||||
const filteredVideos = videos.filter(video =>
|
||||
video.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
video.path.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
|
|
@ -198,8 +231,11 @@ const VideosPage = () => {
|
|||
<span>{formatFileSize(video.size)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2 line-clamp-1">
|
||||
{video.path}
|
||||
<p
|
||||
className="text-xs text-muted-foreground mt-2 line-clamp-1 cursor-help"
|
||||
title={video.path}
|
||||
>
|
||||
{formatFilePath(video.path)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
Loading…
Reference in New Issue