mirror of
https://github.com/azaion/ui.git
synced 2026-04-23 00:56:34 +00:00
Refactor project structure and dependencies; rename package to azaion-ui, update version to 0.0.1, and remove unused files. Introduce new routing and authentication features in App component.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { useRef, useState, useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { api } from '../../api/client'
|
||||
import { getToken } from '../../api/client'
|
||||
import type { Media } from '../../types'
|
||||
|
||||
interface Props {
|
||||
media: Media
|
||||
onTimeUpdate: (time: number) => void
|
||||
selectedClassNum: number
|
||||
}
|
||||
|
||||
export default function VideoPlayer({ media, onTimeUpdate, selectedClassNum }: Props) {
|
||||
const { t } = useTranslation()
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const [playing, setPlaying] = useState(false)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
const [muted, setMuted] = useState(false)
|
||||
|
||||
const token = getToken()
|
||||
const videoUrl = `/api/annotations/media/${media.id}/file`
|
||||
|
||||
const stepFrames = useCallback((count: number) => {
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
const fps = 30
|
||||
video.currentTime = Math.max(0, Math.min(video.duration, video.currentTime + count / fps))
|
||||
}, [])
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
const v = videoRef.current
|
||||
if (!v) return
|
||||
if (v.paused) { v.play(); setPlaying(true) }
|
||||
else { v.pause(); setPlaying(false) }
|
||||
}, [])
|
||||
|
||||
const stop = useCallback(() => {
|
||||
const v = videoRef.current
|
||||
if (!v) return
|
||||
v.pause()
|
||||
v.currentTime = 0
|
||||
setPlaying(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
|
||||
switch (e.key) {
|
||||
case ' ': e.preventDefault(); togglePlay(); break
|
||||
case 'ArrowLeft': e.preventDefault(); stepFrames(e.ctrlKey ? -150 : -1); break
|
||||
case 'ArrowRight': e.preventDefault(); stepFrames(e.ctrlKey ? 150 : 1); break
|
||||
case 'm': case 'M': setMuted(m => !m); break
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
}, [togglePlay, stepFrames])
|
||||
|
||||
const formatTime = (s: number) => {
|
||||
const m = Math.floor(s / 60)
|
||||
const sec = Math.floor(s % 60)
|
||||
return `${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-black flex flex-col">
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={videoUrl}
|
||||
muted={muted}
|
||||
className="w-full max-h-[50vh] object-contain"
|
||||
onTimeUpdate={e => {
|
||||
const t = (e.target as HTMLVideoElement).currentTime
|
||||
setCurrentTime(t)
|
||||
onTimeUpdate(t)
|
||||
}}
|
||||
onLoadedMetadata={e => setDuration((e.target as HTMLVideoElement).duration)}
|
||||
onClick={togglePlay}
|
||||
/>
|
||||
{/* Progress bar */}
|
||||
<div
|
||||
className="h-1 bg-az-border cursor-pointer"
|
||||
onClick={e => {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const pct = (e.clientX - rect.left) / rect.width
|
||||
if (videoRef.current) videoRef.current.currentTime = pct * duration
|
||||
}}
|
||||
>
|
||||
<div className="h-full bg-az-orange" style={{ width: `${duration ? (currentTime / duration) * 100 : 0}%` }} />
|
||||
</div>
|
||||
{/* Controls */}
|
||||
<div className="flex items-center gap-1 px-2 py-1 bg-az-header text-xs">
|
||||
<button onClick={togglePlay} className="text-az-text hover:text-white px-1">{playing ? '⏸' : '▶'}</button>
|
||||
<button onClick={stop} className="text-az-text hover:text-white px-1">⏹</button>
|
||||
{[1, 5, 10, 30, 60].map(n => (
|
||||
<button key={`prev-${n}`} onClick={() => stepFrames(-n)} className="text-az-muted hover:text-white px-0.5">-{n}</button>
|
||||
))}
|
||||
<span className="text-az-muted mx-1">|</span>
|
||||
{[1, 5, 10, 30, 60].map(n => (
|
||||
<button key={`next-${n}`} onClick={() => stepFrames(n)} className="text-az-muted hover:text-white px-0.5">+{n}</button>
|
||||
))}
|
||||
<div className="flex-1" />
|
||||
<button onClick={() => setMuted(m => !m)} className="text-az-text hover:text-white px-1">
|
||||
{muted ? '🔇' : '🔊'}
|
||||
</button>
|
||||
<span className="text-az-muted">{formatTime(currentTime)} / {formatTime(duration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user