From 2ac68f9a69143f91be253250e9681773adff2530 Mon Sep 17 00:00:00 2001 From: tigeren Date: Tue, 26 Aug 2025 18:03:43 +0000 Subject: [PATCH] feat: refactor media viewers for improved functionality and UI - Replaced inline video player with a dedicated VideoViewer component for enhanced video playback experience. - Updated the PhotosPage and FolderViewerPage to utilize the new PhotoViewer and VideoViewer components, streamlining the media viewing process. - Removed unnecessary loading states and modal implementations, simplifying the code structure and improving performance. - Enhanced the PhotoViewer and VideoViewer with bookmarking and rating features for better user interaction. --- src/app/folder-viewer/page.tsx | 109 +++-------- src/app/photos/page.tsx | 106 ++--------- src/app/videos/page.tsx | 26 +-- src/components/photo-viewer.tsx | 223 ++++++++++++++++++++++ src/components/video-viewer.tsx | 315 ++++++++++++++++++++++++++++++++ 5 files changed, 592 insertions(+), 187 deletions(-) create mode 100644 src/components/photo-viewer.tsx create mode 100644 src/components/video-viewer.tsx diff --git a/src/app/folder-viewer/page.tsx b/src/app/folder-viewer/page.tsx index fdc0a4a..00ce6b2 100644 --- a/src/app/folder-viewer/page.tsx +++ b/src/app/folder-viewer/page.tsx @@ -6,10 +6,9 @@ import { useSearchParams } from "next/navigation"; import { Folder, File, Image, Film, Play } from "lucide-react"; import Link from "next/link"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import InlineVideoPlayer from "@/components/inline-video-player"; -import { createPortal } from "react-dom"; +import PhotoViewer from "@/components/photo-viewer"; +import VideoViewer from "@/components/video-viewer"; import { Button } from "@/components/ui/button"; -import { X, ChevronLeft, ChevronRight } from "lucide-react"; interface FileSystemItem { name: string; @@ -31,7 +30,6 @@ const FolderViewerPage = () => { const [isVideoLoading, setIsVideoLoading] = useState(false); const [selectedPhoto, setSelectedPhoto] = useState(null); const [isPhotoViewerOpen, setIsPhotoViewerOpen] = useState(false); - const [isPhotoLoading, setIsPhotoLoading] = useState(false); const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); useEffect(() => { @@ -92,14 +90,12 @@ const FolderViewerPage = () => { setSelectedPhoto(item); setCurrentPhotoIndex(index); setIsPhotoViewerOpen(true); - setIsPhotoLoading(true); } }; const handleClosePhotoViewer = () => { setIsPhotoViewerOpen(false); setSelectedPhoto(null); - setIsPhotoLoading(false); }; const handleNextPhoto = () => { @@ -108,7 +104,6 @@ const FolderViewerPage = () => { const nextIndex = (currentPhotoIndex + 1) % photoItems.length; setCurrentPhotoIndex(nextIndex); setSelectedPhoto(photoItems[nextIndex]); - setIsPhotoLoading(true); } }; @@ -118,7 +113,6 @@ const FolderViewerPage = () => { const prevIndex = (currentPhotoIndex - 1 + photoItems.length) % photoItems.length; setCurrentPhotoIndex(prevIndex); setSelectedPhoto(photoItems[prevIndex]); - setIsPhotoLoading(true); } }; @@ -272,85 +266,28 @@ const FolderViewerPage = () => { )} - {/* Photo Viewer Modal */} - {selectedPhoto && isPhotoViewerOpen && typeof window !== 'undefined' && createPortal( -
-
e.stopPropagation()}> -
- -
+ {/* Photo Viewer */} + item.type === 'photo' && item.id).length > 1} + showBookmarks={false} + showRatings={false} + formatFileSize={formatFileSize} + /> - {/* Navigation Arrows */} - {items.filter(item => item.type === 'photo' && item.id).length > 1 && ( - <> - - - - )} - - {isPhotoLoading && ( -
-
-
- )} - {selectedPhoto.name} setIsPhotoLoading(false)} - onError={() => setIsPhotoLoading(false)} - /> - - {/* Photo Info Bar */} -
-
-
-

{selectedPhoto.name}

-

{formatFileSize(selectedPhoto.size)}

-
-
-
-
-
, - document.body - )} - - {/* Inline Video Player - Rendered as Portal */} - {selectedVideo && isPlayerOpen && typeof window !== 'undefined' && createPortal( - , - document.body - )} + {/* Video Viewer */} + ); }; diff --git a/src/app/photos/page.tsx b/src/app/photos/page.tsx index 1eb7b6e..b4a76e5 100644 --- a/src/app/photos/page.tsx +++ b/src/app/photos/page.tsx @@ -2,11 +2,11 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; -import { Image as ImageIcon, Search, Filter, Star, Bookmark, HardDrive, X, ZoomIn, ChevronLeft, ChevronRight } from 'lucide-react'; +import { Image as ImageIcon, Search, Filter, Star, Bookmark, HardDrive } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { createPortal } from 'react-dom'; +import PhotoViewer from '@/components/photo-viewer'; interface Photo { id: number; @@ -27,7 +27,6 @@ export default function PhotosPage() { const [selectedPhoto, setSelectedPhoto] = useState(null); const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); const [isViewerOpen, setIsViewerOpen] = useState(false); - const [isPhotoLoading, setIsPhotoLoading] = useState(false); useEffect(() => { fetchPhotos(); @@ -62,13 +61,11 @@ export default function PhotosPage() { setSelectedPhoto(photo); setCurrentPhotoIndex(index); setIsViewerOpen(true); - setIsPhotoLoading(true); }; const handleCloseViewer = () => { setIsViewerOpen(false); setSelectedPhoto(null); - setIsPhotoLoading(false); }; const handleNextPhoto = () => { @@ -76,7 +73,6 @@ export default function PhotosPage() { const nextIndex = (currentPhotoIndex + 1) % filteredPhotos.length; setCurrentPhotoIndex(nextIndex); setSelectedPhoto(filteredPhotos[nextIndex]); - setIsPhotoLoading(true); } }; @@ -85,7 +81,6 @@ export default function PhotosPage() { const prevIndex = (currentPhotoIndex - 1 + filteredPhotos.length) % filteredPhotos.length; setCurrentPhotoIndex(prevIndex); setSelectedPhoto(filteredPhotos[prevIndex]); - setIsPhotoLoading(true); } }; @@ -278,88 +273,21 @@ export default function PhotosPage() { - {/* Photo Viewer Modal */} - {selectedPhoto && isViewerOpen && typeof window !== 'undefined' && createPortal( -
-
e.stopPropagation()}> -
- -
- - {/* Navigation Arrows */} - {filteredPhotos.length > 1 && ( - <> - - - - )} - - {isPhotoLoading && ( -
-
-
- )} - {selectedPhoto.title} setIsPhotoLoading(false)} - onError={() => setIsPhotoLoading(false)} - /> - - {/* Photo Info Bar */} -
-
-
-

{selectedPhoto.title}

-

{formatFileSize(selectedPhoto.size)}

-
-
- -
- {[1, 2, 3, 4, 5].map((rating) => ( - - ))} -
-
-
-
-
-
, - document.body - )} + {/* Photo Viewer */} + 1} + showBookmarks={true} + showRatings={true} + formatFileSize={formatFileSize} + onBookmark={handleBookmark} + onUnbookmark={handleUnbookmark} + onRate={handleRate} + /> ); } \ No newline at end of file diff --git a/src/app/videos/page.tsx b/src/app/videos/page.tsx index 5b9935b..327f596 100644 --- a/src/app/videos/page.tsx +++ b/src/app/videos/page.tsx @@ -6,8 +6,7 @@ import { Film, Play, Clock, HardDrive, Search, Filter } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import InlineVideoPlayer from "@/components/inline-video-player"; -import { createPortal } from "react-dom"; +import VideoViewer from "@/components/video-viewer"; interface Video { id: number; @@ -15,6 +14,10 @@ interface Video { path: string; size: number; thumbnail: string; + type: string; + bookmark_count: number; + avg_rating: number; + star_count: number; } const VideosPage = () => { @@ -207,16 +210,15 @@ const VideosPage = () => { - {/* Inline Video Player - Rendered as Portal */} - {selectedVideo && isPlayerOpen && typeof window !== 'undefined' && createPortal( - , - document.body - )} + {/* Video Viewer */} + ); }; diff --git a/src/components/photo-viewer.tsx b/src/components/photo-viewer.tsx new file mode 100644 index 0000000..4780131 --- /dev/null +++ b/src/components/photo-viewer.tsx @@ -0,0 +1,223 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { X, ChevronLeft, ChevronRight, Star, Bookmark } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { createPortal } from 'react-dom'; + +interface Photo { + id: number; + path: string; + title: string; + size: number; + thumbnail: string; + type: string; + bookmark_count: number; + avg_rating: number; + star_count: number; +} + +interface FileSystemItem { + name: string; + path: string; + isDirectory: boolean; + size: number; + thumbnail?: string; + type?: string; + id?: number; +} + +interface PhotoViewerProps { + photo: Photo | FileSystemItem; + isOpen: boolean; + onClose: () => void; + onNext?: () => void; + onPrev?: () => void; + showNavigation?: boolean; + showBookmarks?: boolean; + showRatings?: boolean; + formatFileSize?: (bytes: number) => string; + onBookmark?: (photoId: number) => void; + onUnbookmark?: (photoId: number) => void; + onRate?: (photoId: number, rating: number) => void; +} + +export default function PhotoViewer({ + photo, + isOpen, + onClose, + onNext, + onPrev, + showNavigation = false, + showBookmarks = false, + showRatings = false, + formatFileSize, + onBookmark, + onUnbookmark, + onRate +}: PhotoViewerProps) { + const [isPhotoLoading, setIsPhotoLoading] = useState(false); + + useEffect(() => { + if (isOpen) { + setIsPhotoLoading(true); + } + }, [isOpen, photo]); + + const handleClose = () => { + setIsPhotoLoading(false); + onClose(); + }; + + const handleNext = () => { + if (onNext) { + setIsPhotoLoading(true); + onNext(); + } + }; + + const handlePrev = () => { + if (onPrev) { + setIsPhotoLoading(true); + onPrev(); + } + }; + + const handleBookmark = () => { + if (onBookmark && 'id' in photo) { + onBookmark(photo.id); + } + }; + + const handleUnbookmark = () => { + if (onUnbookmark && 'id' in photo) { + onUnbookmark(photo.id); + } + }; + + const handleRate = (rating: number) => { + if (onRate && 'id' in photo) { + onRate(photo.id, rating); + } + }; + + const getPhotoTitle = () => { + if ('title' in photo) return photo.title; + if ('name' in photo) return photo.name; + return 'Photo'; + }; + + const getPhotoSize = () => { + if (formatFileSize) { + return formatFileSize(photo.size); + } + // Default format function + const bytes = photo.size; + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const getBookmarkCount = () => { + if ('bookmark_count' in photo) return photo.bookmark_count; + return 0; + }; + + const getAvgRating = () => { + if ('avg_rating' in photo) return photo.avg_rating; + return 0; + }; + + if (!isOpen || typeof window === 'undefined') return null; + + return createPortal( +
+
e.stopPropagation()}> +
+ +
+ + {/* Navigation Arrows */} + {showNavigation && onNext && onPrev && ( + <> + + + + )} + + {isPhotoLoading && ( +
+
+
+ )} + + {getPhotoTitle()} setIsPhotoLoading(false)} + onError={() => setIsPhotoLoading(false)} + /> + + {/* Photo Info Bar */} +
+
+
+

{getPhotoTitle()}

+

{getPhotoSize()}

+
+ {(showBookmarks || showRatings) && ( +
+ {showBookmarks && ( + + )} + {showRatings && ( +
+ {[1, 2, 3, 4, 5].map((rating) => ( + + ))} +
+ )} +
+ )} +
+
+
+
, + document.body + ); +} diff --git a/src/components/video-viewer.tsx b/src/components/video-viewer.tsx new file mode 100644 index 0000000..7165931 --- /dev/null +++ b/src/components/video-viewer.tsx @@ -0,0 +1,315 @@ +'use client'; + +import { useState, useEffect, useRef } from 'react'; +import { X, Play, Pause, Volume2, VolumeX, Maximize, Star, Bookmark } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { createPortal } from 'react-dom'; + +interface Video { + id: number; + title: string; + path: string; + size: number; + thumbnail: string; + type: string; + bookmark_count: number; + avg_rating: number; + star_count: number; +} + +interface FileSystemItem { + name: string; + path: string; + isDirectory: boolean; + size: number; + thumbnail?: string; + type?: string; + id?: number; +} + +interface VideoViewerProps { + video: Video | FileSystemItem; + isOpen: boolean; + onClose: () => void; + showBookmarks?: boolean; + showRatings?: boolean; + formatFileSize?: (bytes: number) => string; + onBookmark?: (videoId: number) => void; + onUnbookmark?: (videoId: number) => void; + onRate?: (videoId: number, rating: number) => void; +} + +export default function VideoViewer({ + video, + isOpen, + onClose, + showBookmarks = false, + showRatings = false, + formatFileSize, + onBookmark, + onUnbookmark, + onRate +}: VideoViewerProps) { + const [isPlaying, setIsPlaying] = useState(false); + const [isMuted, setIsMuted] = useState(false); + const [volume, setVolume] = useState(1); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const [showControls, setShowControls] = useState(true); + const videoRef = useRef(null); + + useEffect(() => { + if (isOpen && videoRef.current) { + videoRef.current.src = `/api/stream/${('id' in video ? video.id : video.id) || ''}`; + videoRef.current.load(); + + // Auto-play when video is loaded + videoRef.current.addEventListener('loadeddata', () => { + if (videoRef.current) { + videoRef.current.play().then(() => { + setIsPlaying(true); + }).catch((error) => { + console.log('Auto-play prevented by browser:', error); + }); + } + }); + } + }, [isOpen, video]); + + const handlePlayPause = () => { + if (videoRef.current) { + if (isPlaying) { + videoRef.current.pause(); + } else { + videoRef.current.play(); + } + setIsPlaying(!isPlaying); + } + }; + + const handleMute = () => { + if (videoRef.current) { + videoRef.current.muted = !isMuted; + setIsMuted(!isMuted); + } + }; + + const handleVolumeChange = (e: React.ChangeEvent) => { + const newVolume = parseFloat(e.target.value); + setVolume(newVolume); + if (videoRef.current) { + videoRef.current.volume = newVolume; + } + }; + + const handleTimeUpdate = () => { + if (videoRef.current) { + setCurrentTime(videoRef.current.currentTime); + } + }; + + const handleLoadedMetadata = () => { + if (videoRef.current) { + setDuration(videoRef.current.duration); + } + }; + + const handleSeek = (e: React.ChangeEvent) => { + const newTime = parseFloat(e.target.value); + if (videoRef.current) { + videoRef.current.currentTime = newTime; + setCurrentTime(newTime); + } + }; + + const handleFullscreen = () => { + if (videoRef.current) { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + videoRef.current.requestFullscreen(); + } + } + }; + + const handleBookmark = () => { + if (onBookmark && 'id' in video) { + onBookmark(video.id); + } + }; + + const handleUnbookmark = () => { + if (onUnbookmark && 'id' in video) { + onUnbookmark(video.id); + } + }; + + const handleRate = (rating: number) => { + if (onRate && 'id' in video) { + onRate(video.id, rating); + } + }; + + const getVideoTitle = () => { + if ('title' in video) return video.title; + if ('name' in video) return video.name; + return 'Video'; + }; + + const getVideoSize = () => { + if (formatFileSize) { + return formatFileSize(video.size); + } + // Default format function + const bytes = video.size; + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const getBookmarkCount = () => { + if ('bookmark_count' in video) return video.bookmark_count; + return 0; + }; + + const getAvgRating = () => { + if ('avg_rating' in video) return video.avg_rating; + return 0; + }; + + const formatTime = (time: number) => { + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + }; + + if (!isOpen || typeof window === 'undefined') return null; + + return createPortal( +
+
+ {/* Close button */} + + + {/* Video container */} +
setShowControls(true)} + onMouseLeave={() => setShowControls(false)} + > + + + {/* Title overlay */} +
+
+

{getVideoTitle()}

+ {(showBookmarks || showRatings) && ( +
+ {showBookmarks && ( + + )} + {showRatings && ( +
+ {[1, 2, 3, 4, 5].map((rating) => ( + + ))} +
+ )} +
+ )} +
+
+ + {/* Controls overlay */} +
+ {/* Progress bar */} +
+ +
+ {formatTime(currentTime)} + {formatTime(duration)} +
+
+ + {/* Control buttons */} +
+
+ + +
+ + +
+
+ +
+ +
+
+
+
+
+
, + document.body + ); +}