PunimTag Web Application - Major Feature Release #1
@ -20,6 +20,7 @@ import UserTaggedPhotos from './pages/UserTaggedPhotos'
|
||||
import ManagePhotos from './pages/ManagePhotos'
|
||||
import Settings from './pages/Settings'
|
||||
import Help from './pages/Help'
|
||||
import VideoPlayer from './pages/VideoPlayer'
|
||||
import Layout from './components/Layout'
|
||||
import PasswordChangeModal from './components/PasswordChangeModal'
|
||||
import AdminRoute from './components/AdminRoute'
|
||||
@ -93,6 +94,14 @@ function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/video/:id"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<VideoPlayer />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
|
||||
@ -682,15 +682,15 @@ export default function Search() {
|
||||
|
||||
const openPhoto = (photoId: number, mediaType?: string) => {
|
||||
const isVideo = mediaType === 'video'
|
||||
let photoUrl: string
|
||||
if (isVideo) {
|
||||
// Use video endpoint for videos
|
||||
photoUrl = `${apiClient.defaults.baseURL}/api/v1/videos/${photoId}/video`
|
||||
// Open video in VideoPlayer page with Play button
|
||||
const videoPlayerUrl = `/video/${photoId}`
|
||||
window.open(videoPlayerUrl, '_blank')
|
||||
} else {
|
||||
// Use image endpoint for images
|
||||
photoUrl = `${apiClient.defaults.baseURL}/api/v1/photos/${photoId}/image`
|
||||
const photoUrl = `${apiClient.defaults.baseURL}/api/v1/photos/${photoId}/image`
|
||||
window.open(photoUrl, '_blank')
|
||||
}
|
||||
window.open(photoUrl, '_blank')
|
||||
}
|
||||
|
||||
const openFolder = async (photoId: number) => {
|
||||
|
||||
108
admin-frontend/src/pages/VideoPlayer.tsx
Normal file
108
admin-frontend/src/pages/VideoPlayer.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import videosApi from '../api/videos'
|
||||
|
||||
export default function VideoPlayer() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const videoId = id ? parseInt(id, 10) : null
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [showPlayButton, setShowPlayButton] = useState(true)
|
||||
|
||||
const videoUrl = videoId ? videosApi.getVideoUrl(videoId) : ''
|
||||
|
||||
const handlePlay = () => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.play()
|
||||
setIsPlaying(true)
|
||||
setShowPlayButton(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePause = () => {
|
||||
setIsPlaying(false)
|
||||
setShowPlayButton(true)
|
||||
}
|
||||
|
||||
const handlePlayClick = () => {
|
||||
handlePlay()
|
||||
}
|
||||
|
||||
// Hide play button when video starts playing
|
||||
useEffect(() => {
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
|
||||
const handlePlayEvent = () => {
|
||||
setIsPlaying(true)
|
||||
setShowPlayButton(false)
|
||||
}
|
||||
|
||||
const handlePauseEvent = () => {
|
||||
setIsPlaying(false)
|
||||
setShowPlayButton(true)
|
||||
}
|
||||
|
||||
const handleEnded = () => {
|
||||
setIsPlaying(false)
|
||||
setShowPlayButton(true)
|
||||
}
|
||||
|
||||
video.addEventListener('play', handlePlayEvent)
|
||||
video.addEventListener('pause', handlePauseEvent)
|
||||
video.addEventListener('ended', handleEnded)
|
||||
|
||||
return () => {
|
||||
video.removeEventListener('play', handlePlayEvent)
|
||||
video.removeEventListener('pause', handlePauseEvent)
|
||||
video.removeEventListener('ended', handleEnded)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!videoId || !videoUrl) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-black">
|
||||
<div className="text-white text-xl">Video not found</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black flex items-center justify-center p-4">
|
||||
<div className="relative w-full max-w-full" style={{ maxHeight: 'calc(100vh - 2rem)' }}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={videoUrl}
|
||||
className="w-full h-auto max-h-[calc(100vh-2rem)] object-contain"
|
||||
controls={true}
|
||||
controlsList="nodownload"
|
||||
onPause={handlePause}
|
||||
preload="metadata"
|
||||
/>
|
||||
|
||||
{/* Play button overlay - centered, positioned above video controls */}
|
||||
{showPlayButton && (
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div
|
||||
className="absolute inset-0 bg-black bg-opacity-30 hover:bg-opacity-20 transition-all pointer-events-none"
|
||||
/>
|
||||
<button
|
||||
className="relative z-10 w-20 h-20 bg-white bg-opacity-90 rounded-full flex items-center justify-center hover:bg-opacity-100 transition-all shadow-lg hover:scale-110 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-black pointer-events-auto cursor-pointer"
|
||||
aria-label="Play video"
|
||||
onClick={handlePlayClick}
|
||||
>
|
||||
<svg
|
||||
className="w-12 h-12 text-gray-900 ml-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user