feature/video-player-play-button #37

Merged
tanyar09 merged 2 commits from feature/video-player-play-button into dev 2026-02-11 12:18:35 -05:00
4 changed files with 138 additions and 5 deletions

View File

@ -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={

View File

@ -449,6 +449,22 @@ export default function AutoMatch() {
return
}
// Show informational message about bulk operation
const infoMessage = [
' Bulk Auto-Match Operation',
'',
'This operation will automatically match faces across your entire photo library.',
'While the system uses advanced matching algorithms, some matches may not be 100% accurate.',
'',
'Please review the results after completion to ensure accuracy.',
'',
'Do you want to proceed with the auto-match operation?'
].join('\n')
if (!confirm(infoMessage)) {
return
}
setBusy(true)
try {
const response = await facesApi.autoMatch({

View File

@ -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) => {

View 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>
)
}