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:
tigeren 2025-08-26 19:12:08 +00:00
parent 224b898bcd
commit 6744a2736b
3 changed files with 217 additions and 15 deletions

View File

@ -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>

View File

@ -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>
))}

View File

@ -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>