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";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, Suspense } from "react";
|
import { useState, useEffect, Suspense } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
import { Folder, File, Image, Film, Play } from "lucide-react";
|
import { Folder, File, Image, Film, Play, ChevronLeft, Home } from "lucide-react";
|
||||||
import Link from "next/link";
|
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 PhotoViewer from "@/components/photo-viewer";
|
import PhotoViewer from "@/components/photo-viewer";
|
||||||
|
|
@ -20,8 +20,14 @@ interface FileSystemItem {
|
||||||
id?: number;
|
id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BreadcrumbItem {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
const FolderViewerPage = () => {
|
const FolderViewerPage = () => {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
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);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
@ -59,6 +65,85 @@ const FolderViewerPage = () => {
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
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) => {
|
const getFileIcon = (item: FileSystemItem) => {
|
||||||
if (item.isDirectory) return <Folder className="text-blue-500" size={48} />;
|
if (item.isDirectory) return <Folder className="text-blue-500" size={48} />;
|
||||||
if (item.type === 'photo') return <Image className="text-green-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">
|
<div className="mb-8">
|
||||||
<nav className="flex items-center space-x-2 text-sm font-medium text-zinc-400 mb-4">
|
{/* Back Button */}
|
||||||
<Link href="/folder-viewer" className="hover:text-white transition-colors">
|
<div className="flex items-center gap-4 mb-4">
|
||||||
Libraries
|
<Button
|
||||||
</Link>
|
variant="ghost"
|
||||||
<span>/</span>
|
size="sm"
|
||||||
<span className="text-white font-semibold">{path.split('/').pop()}</span>
|
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>
|
</nav>
|
||||||
|
|
||||||
|
{/* Current Directory Title */}
|
||||||
<h1 className="text-3xl font-bold text-white tracking-tight">
|
<h1 className="text-3xl font-bold text-white tracking-tight">
|
||||||
{path.split('/').pop()}
|
{path ? path.split('/').pop() : 'Libraries'}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -244,7 +364,17 @@ const FolderViewerPage = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<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-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>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,39 @@ export default function PhotosPage() {
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
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 =>
|
const filteredPhotos = photos.filter(photo =>
|
||||||
photo.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
photo.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
photo.path.toLowerCase().includes(searchTerm.toLowerCase())
|
photo.path.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
|
@ -230,9 +263,12 @@ export default function PhotosPage() {
|
||||||
<span>{formatFileSize(photo.size)}</span>
|
<span>{formatFileSize(photo.size)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">
|
<p
|
||||||
{photo.path}
|
className="text-xs text-muted-foreground mt-1 line-clamp-1 cursor-help"
|
||||||
</p>
|
title={photo.path}
|
||||||
|
>
|
||||||
|
{formatFilePath(photo.path)}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,39 @@ const VideosPage = () => {
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
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 =>
|
const filteredVideos = videos.filter(video =>
|
||||||
video.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
video.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
video.path.toLowerCase().includes(searchTerm.toLowerCase())
|
video.path.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
|
@ -198,8 +231,11 @@ const VideosPage = () => {
|
||||||
<span>{formatFileSize(video.size)}</span>
|
<span>{formatFileSize(video.size)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-2 line-clamp-1">
|
<p
|
||||||
{video.path}
|
className="text-xs text-muted-foreground mt-2 line-clamp-1 cursor-help"
|
||||||
|
title={video.path}
|
||||||
|
>
|
||||||
|
{formatFilePath(video.path)}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue