Compare commits

...

2 Commits

Author SHA1 Message Date
tigeren 854afd4c41 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.
2025-08-29 17:26:51 +00:00
tigeren fdf8ab2a39 feat: add keyboard shortcuts for video controls and enhance deployment instructions
- Implemented keyboard shortcuts for video playback, including controls for play/pause, seek, fullscreen, and mute.
- Added deployment instructions for using Docker, including the definition of Dockerfile and docker-compose.yml.
2025-08-29 16:43:59 +00:00
6 changed files with 157 additions and 13 deletions

View File

@ -28,6 +28,11 @@ UI:
8. the video card can be clicked, once clicked, a poped up video player will be displayed. it can be closed, fast forward, expand to full screen, etc. 8. the video card can be clicked, once clicked, a poped up video player will be displayed. it can be closed, fast forward, expand to full screen, etc.
9. can bookmark/un-bookmark the video, can star the video 9. can bookmark/un-bookmark the video, can star the video
Deployment:
1. use docker compose to deploy the service
2. Dockerfile should be defined
3. docker-compose.yml should be defined.
Development Rules: Development Rules:
1. Everytime after making all the changes, run 'pnpm build' to verify the changes are compiling correct. 1. Everytime after making all the changes, run 'pnpm build' to verify the changes are compiling correct.
2. Once added debug logs, don't delete it until told so. 2. Once added debug logs, don't delete it until told so.

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

@ -86,6 +86,53 @@ export default function VideoViewer({
} }
}, [isOpen, video]); }, [isOpen, video]);
// Keyboard shortcuts
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (!videoRef.current) return;
switch (e.key) {
case 'Escape':
e.preventDefault();
onClose();
break;
case 'ArrowRight':
e.preventDefault();
videoRef.current.currentTime = Math.min(
videoRef.current.currentTime + 10,
videoRef.current.duration || 0
);
break;
case 'ArrowLeft':
e.preventDefault();
videoRef.current.currentTime = Math.max(
videoRef.current.currentTime - 10,
0
);
break;
case ' ':
e.preventDefault();
handlePlayPause();
break;
case 'f':
case 'F':
e.preventDefault();
handleFullscreen();
break;
case 'm':
case 'M':
e.preventDefault();
handleMute();
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
const handlePlayPause = () => { const handlePlayPause = () => {
if (videoRef.current) { if (videoRef.current) {
if (isPlaying) { if (isPlaying) {

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;