feat: enhance photo viewer navigation and state management

- Implemented next and previous photo navigation in the photo viewer, allowing users to cycle through images while skipping videos.
- Introduced state management for current items in the folder viewer to support navigation functionality.
- Updated the InfiniteVirtualGrid and VirtualizedFolderGrid components to handle item clicks with index support for better user experience.
This commit is contained in:
tigeren 2025-08-29 17:26:51 +00:00
parent fdf8ab2a39
commit 854afd4c41
4 changed files with 105 additions and 13 deletions

View File

@ -148,7 +148,6 @@ const FolderViewerPage = () => {
const handlePhotoClick = (item: FileSystemItem, index: number) => { const handlePhotoClick = (item: FileSystemItem, index: number) => {
if (item.type === 'photo' && item.id) { if (item.type === 'photo' && item.id) {
setSelectedPhoto(item); setSelectedPhoto(item);
setCurrentPhotoIndex(index);
setIsPhotoViewerOpen(true); setIsPhotoViewerOpen(true);
} }
}; };
@ -158,16 +157,40 @@ const FolderViewerPage = () => {
setSelectedPhoto(null); setSelectedPhoto(null);
}; };
const [currentItems, setCurrentItems] = useState<FileSystemItem[]>([]);
const handleNextPhoto = () => { const handleNextPhoto = () => {
// This would need to be implemented with the current items list // Navigate to next photo, skipping videos
// For now, just close the viewer const photos = currentItems.filter(item => item.type === 'photo' && item.id);
handleClosePhotoViewer(); if (photos.length === 0) return;
const currentPhotoId = selectedPhoto?.id;
if (!currentPhotoId) return;
const currentIndexInPhotos = photos.findIndex(p => p.id === currentPhotoId);
if (currentIndexInPhotos === -1) return;
const nextIndex = (currentIndexInPhotos + 1) % photos.length;
const nextPhoto = photos[nextIndex];
setSelectedPhoto(nextPhoto);
}; };
const handlePrevPhoto = () => { const handlePrevPhoto = () => {
// This would need to be implemented with the current items list // Navigate to previous photo, skipping videos
// For now, just close the viewer const photos = currentItems.filter(item => item.type === 'photo' && item.id);
handleClosePhotoViewer(); if (photos.length === 0) return;
const currentPhotoId = selectedPhoto?.id;
if (!currentPhotoId) return;
const currentIndexInPhotos = photos.findIndex(p => p.id === currentPhotoId);
if (currentIndexInPhotos === -1) return;
const prevIndex = (currentIndexInPhotos - 1 + photos.length) % photos.length;
const prevPhoto = photos[prevIndex];
setSelectedPhoto(prevPhoto);
}; };
if (!path) { if (!path) {
@ -200,6 +223,7 @@ const FolderViewerPage = () => {
onBreadcrumbClick={handleBreadcrumbClick} onBreadcrumbClick={handleBreadcrumbClick}
breadcrumbs={getBreadcrumbs(path)} breadcrumbs={getBreadcrumbs(path)}
libraries={libraries} libraries={libraries}
onItemsLoaded={setCurrentItems}
/> />
{/* Photo Viewer */} {/* Photo Viewer */}
@ -209,7 +233,7 @@ const FolderViewerPage = () => {
onClose={handleClosePhotoViewer} onClose={handleClosePhotoViewer}
onNext={handleNextPhoto} onNext={handleNextPhoto}
onPrev={handlePrevPhoto} onPrev={handlePrevPhoto}
showNavigation={false} showNavigation={true}
showBookmarks={false} showBookmarks={false}
showRatings={false} showRatings={false}
formatFileSize={formatFileSize} formatFileSize={formatFileSize}

View File

@ -19,12 +19,49 @@ interface Photo {
export default function PhotosPage() { export default function PhotosPage() {
const [selectedPhoto, setSelectedPhoto] = useState<Photo | null>(null); const [selectedPhoto, setSelectedPhoto] = useState<Photo | null>(null);
const [isViewerOpen, setIsViewerOpen] = useState(false); const [isViewerOpen, setIsViewerOpen] = useState(false);
const [photosList, setPhotosList] = useState<Photo[]>([]);
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
const handlePhotoClick = (photo: Photo) => { const handlePhotoClick = (photo: Photo, index?: number) => {
setSelectedPhoto(photo); setSelectedPhoto(photo);
if (index !== undefined) {
setCurrentPhotoIndex(index);
}
setIsViewerOpen(true); setIsViewerOpen(true);
}; };
const handlePhotosData = (photos: Photo[]) => {
setPhotosList(photos);
};
const handleNextPhoto = () => {
if (photosList.length === 0) return;
const currentPhotoId = selectedPhoto?.id;
if (!currentPhotoId) return;
const currentIndex = photosList.findIndex(p => p.id === currentPhotoId);
if (currentIndex === -1) return;
const nextIndex = (currentIndex + 1) % photosList.length;
setSelectedPhoto(photosList[nextIndex]);
setCurrentPhotoIndex(nextIndex);
};
const handlePrevPhoto = () => {
if (photosList.length === 0) return;
const currentPhotoId = selectedPhoto?.id;
if (!currentPhotoId) return;
const currentIndex = photosList.findIndex(p => p.id === currentPhotoId);
if (currentIndex === -1) return;
const prevIndex = (currentIndex - 1 + photosList.length) % photosList.length;
setSelectedPhoto(photosList[prevIndex]);
setCurrentPhotoIndex(prevIndex);
};
const handleCloseViewer = () => { const handleCloseViewer = () => {
setIsViewerOpen(false); setIsViewerOpen(false);
setSelectedPhoto(null); setSelectedPhoto(null);
@ -66,6 +103,7 @@ export default function PhotosPage() {
onBookmark={handleBookmark} onBookmark={handleBookmark}
onUnbookmark={handleUnbookmark} onUnbookmark={handleUnbookmark}
onRate={handleRate} onRate={handleRate}
onDataUpdate={handlePhotosData}
/> />
{/* Photo Viewer */} {/* Photo Viewer */}
@ -73,6 +111,9 @@ export default function PhotosPage() {
photo={selectedPhoto!} photo={selectedPhoto!}
isOpen={isViewerOpen} isOpen={isViewerOpen}
onClose={handleCloseViewer} onClose={handleCloseViewer}
onNext={handleNextPhoto}
onPrev={handlePrevPhoto}
showNavigation={true}
showBookmarks={true} showBookmarks={true}
showRatings={true} showRatings={true}
onBookmark={handleBookmark} onBookmark={handleBookmark}

View File

@ -21,10 +21,11 @@ interface MediaItem {
interface InfiniteVirtualGridProps { interface InfiniteVirtualGridProps {
type: 'video' | 'photo' | 'bookmark'; type: 'video' | 'photo' | 'bookmark';
onItemClick: (item: MediaItem) => void; onItemClick: (item: MediaItem, index?: number) => void;
onBookmark: (id: number) => Promise<void>; onBookmark: (id: number) => Promise<void>;
onUnbookmark: (id: number) => Promise<void>; onUnbookmark: (id: number) => Promise<void>;
onRate: (id: number, rating: number) => Promise<void>; onRate: (id: number, rating: number) => Promise<void>;
onDataUpdate?: (items: MediaItem[]) => void;
} }
const ITEM_HEIGHT = 220; const ITEM_HEIGHT = 220;
@ -36,7 +37,8 @@ export default function InfiniteVirtualGrid({
onItemClick, onItemClick,
onBookmark, onBookmark,
onUnbookmark, onUnbookmark,
onRate onRate,
onDataUpdate
}: InfiniteVirtualGridProps) { }: InfiniteVirtualGridProps) {
const [totalItems, setTotalItems] = useState(0); const [totalItems, setTotalItems] = useState(0);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
@ -324,6 +326,23 @@ export default function InfiniteVirtualGrid({
fetchItems(nextBatchStart, batchEnd); fetchItems(nextBatchStart, batchEnd);
} }
} }
// Collect all available items and notify parent
if (onDataUpdate) {
const allItems: MediaItem[] = [];
const sortedKeys = Array.from(dataCacheRef.current.keys()).sort((a, b) => a - b);
for (const batchKey of sortedKeys) {
const items = dataCacheRef.current.get(batchKey);
if (items) {
allItems.push(...items);
}
}
if (allItems.length > 0) {
onDataUpdate(allItems);
}
}
}, [totalItems, fetchItems, getColumnCount]); }, [totalItems, fetchItems, getColumnCount]);
const Cell = ({ columnIndex, rowIndex, style }: any) => { const Cell = ({ columnIndex, rowIndex, style }: any) => {
@ -352,7 +371,7 @@ export default function InfiniteVirtualGrid({
<div style={style} className="p-2"> <div style={style} className="p-2">
<Card <Card
className="group hover:shadow-lg transition-all duration-300 hover:-translate-y-1 cursor-pointer border-border overflow-hidden h-full" className="group hover:shadow-lg transition-all duration-300 hover:-translate-y-1 cursor-pointer border-border overflow-hidden h-full"
onClick={() => onItemClick(item)} onClick={() => onItemClick(item, index)}
> >
<div className="relative overflow-hidden bg-muted aspect-video"> <div className="relative overflow-hidden bg-muted aspect-video">
<img <img

View File

@ -34,6 +34,7 @@ interface VirtualizedFolderGridProps {
onBreadcrumbClick: (path: string) => void; onBreadcrumbClick: (path: string) => void;
breadcrumbs: BreadcrumbItem[]; breadcrumbs: BreadcrumbItem[];
libraries: {id: number, path: string}[]; libraries: {id: number, path: string}[];
onItemsLoaded?: (items: FileSystemItem[]) => void;
} }
const ITEM_HEIGHT = 280; // Increased for folder cards const ITEM_HEIGHT = 280; // Increased for folder cards
@ -45,7 +46,8 @@ export default function VirtualizedFolderGrid({
onBackClick, onBackClick,
onBreadcrumbClick, onBreadcrumbClick,
breadcrumbs, breadcrumbs,
libraries libraries,
onItemsLoaded
}: VirtualizedFolderGridProps) { }: VirtualizedFolderGridProps) {
const [items, setItems] = useState<FileSystemItem[]>([]); const [items, setItems] = useState<FileSystemItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -203,6 +205,12 @@ export default function VirtualizedFolderGrid({
} }
}, [currentPath, fetchItems]); }, [currentPath, fetchItems]);
useEffect(() => {
if (onItemsLoaded) {
onItemsLoaded(items);
}
}, [items, onItemsLoaded]);
const Cell = ({ columnIndex, rowIndex, style }: any) => { const Cell = ({ columnIndex, rowIndex, style }: any) => {
const columnCount = getColumnCount(); const columnCount = getColumnCount();
const index = rowIndex * columnCount + columnIndex; const index = rowIndex * columnCount + columnIndex;