From dd1c6a7b52b04d71db00aa926e9eaf8d6ea3a8a3 Mon Sep 17 00:00:00 2001 From: goddonebianu Date: Sun, 8 Mar 2026 09:25:22 +0800 Subject: [PATCH] Sun Mar 8 09:25:22 AM CST 2026 --- .../VideoPlayer/SubtitleDisplay.tsx | 22 -- .../_srt-player/VideoPlayer/VideoPanel.tsx | 220 ------------------ .../_srt-player/hooks/useSubtitleSync.ts | 96 -------- src/app/(features)/_srt-player/page.tsx | 63 ----- src/app/(features)/_srt-player/subtitle.ts | 74 ------ src/app/(features)/_srt-player/types.ts | 132 ----------- .../components/ControlPanel.tsx | 136 +++++++++-- .../components/VideoPlayerPanel.tsx | 8 +- .../hooks/useFileUpload.ts | 8 +- .../hooks/useKeyboardShortcuts.ts | 10 +- .../srt-player/hooks/useSubtitleSync.ts | 101 ++++++++ .../hooks/useVideoSync.ts | 2 +- src/app/(features)/srt-player/page.tsx | 210 +++++++++++------ .../stores/srtPlayerStore.ts} | 8 +- .../srt-player/stores/substitleStore.ts | 19 -- .../srt-player/stores/videoStore.ts | 112 --------- .../(features)/srt-player/subtitleParser.ts | 89 ------- src/app/(features)/srt-player/types.ts | 126 ++++++++++ .../(features)/srt-player/useFileUpload.ts | 65 ------ .../utils/subtitleParser.ts | 2 +- 20 files changed, 494 insertions(+), 1009 deletions(-) delete mode 100644 src/app/(features)/_srt-player/VideoPlayer/SubtitleDisplay.tsx delete mode 100644 src/app/(features)/_srt-player/VideoPlayer/VideoPanel.tsx delete mode 100644 src/app/(features)/_srt-player/hooks/useSubtitleSync.ts delete mode 100644 src/app/(features)/_srt-player/page.tsx delete mode 100644 src/app/(features)/_srt-player/subtitle.ts delete mode 100644 src/app/(features)/_srt-player/types.ts rename src/app/(features)/{_srt-player => srt-player}/components/ControlPanel.tsx (59%) rename src/app/(features)/{_srt-player => srt-player}/components/VideoPlayerPanel.tsx (92%) rename src/app/(features)/{_srt-player => srt-player}/hooks/useFileUpload.ts (91%) rename src/app/(features)/{_srt-player => srt-player}/hooks/useKeyboardShortcuts.ts (87%) create mode 100644 src/app/(features)/srt-player/hooks/useSubtitleSync.ts rename src/app/(features)/{_srt-player => srt-player}/hooks/useVideoSync.ts (95%) rename src/app/(features)/{_srt-player/store.ts => srt-player/stores/srtPlayerStore.ts} (94%) delete mode 100644 src/app/(features)/srt-player/stores/substitleStore.ts delete mode 100644 src/app/(features)/srt-player/stores/videoStore.ts delete mode 100644 src/app/(features)/srt-player/subtitleParser.ts delete mode 100644 src/app/(features)/srt-player/useFileUpload.ts rename src/app/(features)/{_srt-player => srt-player}/utils/subtitleParser.ts (99%) diff --git a/src/app/(features)/_srt-player/VideoPlayer/SubtitleDisplay.tsx b/src/app/(features)/_srt-player/VideoPlayer/SubtitleDisplay.tsx deleted file mode 100644 index 373a24a..0000000 --- a/src/app/(features)/_srt-player/VideoPlayer/SubtitleDisplay.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export function SubtitleDisplay({ subtitle }: { subtitle: string }) { - const words = subtitle.match(/\b[\w']+(?:-[\w']+)*\b/g) || []; - let i = 0; - return ( -
- {words.map((v) => ( - { - window.open( - `https://www.youdao.com/result?word=${v}&lang=en`, - "_blank", - ); - }} - key={i++} - className="hover:bg-gray-700 hover:underline hover:cursor-pointer" - > - {v + " "} - - ))} -
- ); -} diff --git a/src/app/(features)/_srt-player/VideoPlayer/VideoPanel.tsx b/src/app/(features)/_srt-player/VideoPlayer/VideoPanel.tsx deleted file mode 100644 index 063c313..0000000 --- a/src/app/(features)/_srt-player/VideoPlayer/VideoPanel.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { useState, useRef, forwardRef, useEffect, useCallback } from "react"; -import { SubtitleDisplay } from "./SubtitleDisplay"; -import { LightButton } from "@/design-system/base/button"; -import { RangeInput } from "@/design-system/base/range"; -import { getIndex, parseSrt, getNearistIndex } from "../subtitle"; -import { useTranslations } from "next-intl"; - -type VideoPanelProps = { - videoUrl: string | null; - srtUrl: string | null; -}; - -const VideoPanel = forwardRef( - ({ videoUrl, srtUrl }, videoRef) => { - const t = useTranslations("srt_player"); - videoRef = videoRef as React.RefObject; - const [isPlaying, setIsPlaying] = useState(false); - const [srtLength, setSrtLength] = useState(0); - const [progress, setProgress] = useState(-1); - const [autoPause, setAutoPause] = useState(true); - const [spanText, setSpanText] = useState(""); - const [subtitle, setSubtitle] = useState(""); - const parsedSrtRef = useRef< - { start: number; end: number; text: string; }[] | null - >(null); - const rafldRef = useRef(0); - const ready = useRef({ - vid: false, - sub: false, - all: function () { - return this.vid && this.sub; - }, - }); - - const togglePlayPause = useCallback(() => { - if (!videoUrl) return; - - const video = videoRef.current; - if (!video) return; - if (video.paused || video.currentTime === 0) { - video.play(); - } else { - video.pause(); - } - setIsPlaying(!video.paused); - }, [videoRef, videoUrl]); - - useEffect(() => { - const handleKeyDownEvent = (e: globalThis.KeyboardEvent) => { - if (e.key === "n") { - next(); - } else if (e.key === "p") { - previous(); - } else if (e.key === " ") { - togglePlayPause(); - } else if (e.key === "r") { - restart(); - } else if (e.key === "a") { - handleAutoPauseToggle(); - } - }; - document.addEventListener("keydown", handleKeyDownEvent); - return () => document.removeEventListener("keydown", handleKeyDownEvent); - }); - - useEffect(() => { - const cb = () => { - if (ready.current.all()) { - if (!parsedSrtRef.current) { - } else if (isPlaying) { - // 这里负责显示当前时间的字幕与自动暂停 - const srt = parsedSrtRef.current; - const ct = videoRef.current?.currentTime as number; - const index = getIndex(srt, ct); - if (index !== null) { - setSubtitle(srt[index].text); - if ( - autoPause && - ct >= srt[index].end - 0.05 && - ct < srt[index].end - ) { - videoRef.current!.currentTime = srt[index].start; - togglePlayPause(); - } - } else { - setSubtitle(""); - } - } else { - } - } - rafldRef.current = requestAnimationFrame(cb); - }; - rafldRef.current = requestAnimationFrame(cb); - return () => { - cancelAnimationFrame(rafldRef.current); - }; - }, [autoPause, isPlaying, togglePlayPause, videoRef]); - - useEffect(() => { - if (videoUrl && videoRef.current) { - videoRef.current.src = videoUrl; - videoRef.current.load(); - setIsPlaying(false); - ready.current["vid"] = true; - } - }, [videoRef, videoUrl]); - useEffect(() => { - if (srtUrl) { - fetch(srtUrl) - .then((response) => response.text()) - .then((data) => { - parsedSrtRef.current = parseSrt(data); - setSrtLength(parsedSrtRef.current.length); - ready.current["sub"] = true; - }); - } - }, [srtUrl]); - - const timeUpdate = () => { - if (!parsedSrtRef.current || !videoRef.current) return; - const index = getIndex( - parsedSrtRef.current, - videoRef.current.currentTime, - ); - if (!index) return; - setSpanText(`${index + 1}/${parsedSrtRef.current.length}`); - }; - - const handleSeek = (e: React.ChangeEvent) => { - if (videoRef.current && parsedSrtRef.current) { - const newProgress = parseInt(e.target.value); - videoRef.current.currentTime = - parsedSrtRef.current[newProgress]?.start || 0; - setProgress(newProgress); - } - }; - - const handleAutoPauseToggle = () => { - setAutoPause(!autoPause); - }; - - const next = () => { - if (!parsedSrtRef.current || !videoRef.current) return; - const i = getNearistIndex( - parsedSrtRef.current, - videoRef.current.currentTime, - ); - if (i != null && i + 1 < parsedSrtRef.current.length) { - videoRef.current.currentTime = parsedSrtRef.current[i + 1].start; - videoRef.current.play(); - setIsPlaying(true); - } - }; - - const previous = () => { - if (!parsedSrtRef.current || !videoRef.current) return; - const i = getNearistIndex( - parsedSrtRef.current, - videoRef.current.currentTime, - ); - if (i != null && i - 1 >= 0) { - videoRef.current.currentTime = parsedSrtRef.current[i - 1].start; - videoRef.current.play(); - setIsPlaying(true); - } - }; - - const restart = () => { - if (!parsedSrtRef.current || !videoRef.current) return; - const i = getNearistIndex( - parsedSrtRef.current, - videoRef.current.currentTime, - ); - if (i != null && i >= 0) { - videoRef.current.currentTime = parsedSrtRef.current[i].start; - videoRef.current.play(); - setIsPlaying(true); - } - }; - - return ( -
- - -
- - {isPlaying ? t("pause") : t("play")} - - {t("previous")} - {t("next")} - {t("restart")} - - {t("autoPause", { enabled: autoPause ? "Yes" : "No" })} - -
- { - if (videoRef.current && parsedSrtRef.current) { - videoRef.current.currentTime = parsedSrtRef.current[value]?.start || 0; - setProgress(value); - } - }} - value={progress} - /> - {spanText} -
- ); - }, -); - -VideoPanel.displayName = "VideoPanel"; - -export { VideoPanel }; diff --git a/src/app/(features)/_srt-player/hooks/useSubtitleSync.ts b/src/app/(features)/_srt-player/hooks/useSubtitleSync.ts deleted file mode 100644 index d42b65a..0000000 --- a/src/app/(features)/_srt-player/hooks/useSubtitleSync.ts +++ /dev/null @@ -1,96 +0,0 @@ -"use client"; - -import { useEffect, useRef } from "react"; -import { useSrtPlayerStore } from "../store"; - -/** - * useSubtitleSync - 字幕同步 Hook - * - * 自动同步视频播放时间与字幕显示,支持自动暂停功能。 - * 使用 Zustand store 获取状态,无需传入参数。 - */ -export function useSubtitleSync() { - const lastSubtitleRef = useRef(null); - const hasAutoPausedRef = useRef<{ [key: number]: boolean }>({}); // 追踪每个字幕是否已触发自动暂停 - const rafIdRef = useRef(0); - - // 从 store 获取状态 - const subtitleData = useSrtPlayerStore((state) => state.subtitle.data); - const isPlaying = useSrtPlayerStore((state) => state.video.isPlaying); - const autoPause = useSrtPlayerStore((state) => state.controls.autoPause); - - // Store actions - const setCurrentSubtitle = useSrtPlayerStore((state) => state.setCurrentSubtitle); - const seek = useSrtPlayerStore((state) => state.seek); - const pause = useSrtPlayerStore((state) => state.pause); - - // 同步循环 - useEffect(() => { - const syncSubtitles = () => { - // 从 store 获取最新的 currentTime - const currentTime = useSrtPlayerStore.getState().video.currentTime; - - // 获取当前时间对应的字幕索引 - const getCurrentSubtitleIndex = (time: number): number | null => { - for (let i = 0; i < subtitleData.length; i++) { - const subtitle = subtitleData[i]; - if (time >= subtitle.start && time <= subtitle.end) { - return i; - } - } - return null; - }; - - const currentIndex = getCurrentSubtitleIndex(currentTime); - - // 检查字幕是否发生变化 - if (currentIndex !== lastSubtitleRef.current) { - lastSubtitleRef.current = currentIndex; - - if (currentIndex !== null) { - const subtitle = subtitleData[currentIndex]; - setCurrentSubtitle(subtitle.text, currentIndex); - } else { - setCurrentSubtitle('', null); - } - } - - // 检查是否需要自动暂停(每个字幕只触发一次) - if (autoPause && currentIndex !== null) { - const currentSubtitle = subtitleData[currentIndex]; - const timeUntilEnd = currentSubtitle.end - currentTime; - - // 在字幕结束前 0.2 秒触发自动暂停 - if (timeUntilEnd <= 0.2 && timeUntilEnd > 0 && !hasAutoPausedRef.current[currentIndex]) { - hasAutoPausedRef.current[currentIndex] = true; - seek(currentSubtitle.start); - // 使用 setTimeout 确保在 seek 之后暂停 - setTimeout(() => { - pause(); - }, 0); - } - } - - // 如果视频正在播放,继续循环 - if (useSrtPlayerStore.getState().video.isPlaying) { - rafIdRef.current = requestAnimationFrame(syncSubtitles); - } - }; - - if (subtitleData.length > 0 && isPlaying) { - rafIdRef.current = requestAnimationFrame(syncSubtitles); - } - - return () => { - if (rafIdRef.current) { - cancelAnimationFrame(rafIdRef.current); - } - }; - }, [subtitleData, isPlaying, autoPause, setCurrentSubtitle, seek, pause]); - - // 重置最后字幕引用 - useEffect(() => { - lastSubtitleRef.current = null; - hasAutoPausedRef.current = {}; - }, [subtitleData]); -} diff --git a/src/app/(features)/_srt-player/page.tsx b/src/app/(features)/_srt-player/page.tsx deleted file mode 100644 index 213b0b7..0000000 --- a/src/app/(features)/_srt-player/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import { useRef, useEffect } from "react"; -import { useTranslations } from "next-intl"; -import { toast } from "sonner"; -import { PageLayout } from "@/components/ui/PageLayout"; -import { VideoPlayerPanel } from "./components/VideoPlayerPanel"; -import { ControlPanel } from "./components/ControlPanel"; -import { useVideoSync } from "./hooks/useVideoSync"; -import { useSubtitleSync } from "./hooks/useSubtitleSync"; -import { useSrtPlayerShortcuts } from "./hooks/useKeyboardShortcuts"; -import { loadSubtitle } from "./utils/subtitleParser"; -import { useSrtPlayerStore } from "./store"; - -export default function SrtPlayerPage() { - const t = useTranslations("home"); - const srtT = useTranslations("srt_player"); - - const videoRef = useRef(null); - - // Store state - const subtitleUrl = useSrtPlayerStore((state) => state.subtitle.url); - const setSubtitleData = useSrtPlayerStore((state) => state.setSubtitleData); - - // Hooks - useVideoSync(videoRef); - useSubtitleSync(); - useSrtPlayerShortcuts(); - - // Load subtitle when URL changes - useEffect(() => { - if (subtitleUrl) { - loadSubtitle(subtitleUrl) - .then((subtitleData) => { - setSubtitleData(subtitleData); - toast.success(srtT("subtitleLoadSuccess")); - }) - .catch((error) => { - toast.error(srtT("subtitleLoadFailed") + ": " + error.message); - }); - } - }, [srtT, subtitleUrl, setSubtitleData]); - - return ( - - {/* Title */} -
-

- {t("srtPlayer.name")} -

-

- {t("srtPlayer.description")} -

-
- - {/* Video Player */} - - - {/* Control Panel */} - -
- ); -} diff --git a/src/app/(features)/_srt-player/subtitle.ts b/src/app/(features)/_srt-player/subtitle.ts deleted file mode 100644 index 06dd833..0000000 --- a/src/app/(features)/_srt-player/subtitle.ts +++ /dev/null @@ -1,74 +0,0 @@ -export function parseSrt(data: string) { - const lines = data.split(/\r?\n/); - const result = []; - const re = new RegExp( - "(\\d{2}:\\d{2}:\\d{2},\\d{3})\\s*-->\\s*(\\d{2}:\\d{2}:\\d{2},\\d{3})", - ); - let i = 0; - while (i < lines.length) { - if (!lines[i].trim()) { - i++; - continue; - } - i++; - if (i >= lines.length) break; - const timeMatch = lines[i].match(re); - if (!timeMatch) { - i++; - continue; - } - const start = toSeconds(timeMatch[1]); - const end = toSeconds(timeMatch[2]); - i++; - let text = ""; - while (i < lines.length && lines[i].trim()) { - text += lines[i] + "\n"; - i++; - } - result.push({ start, end, text: text.trim() }); - i++; - } - return result; -} - -export function getNearistIndex( - srt: { start: number; end: number; text: string }[], - ct: number, -) { - for (let i = 0; i < srt.length; i++) { - const s = srt[i]; - const l = ct - s.start >= 0; - const r = ct - s.end >= 0; - if (!(l || r)) return i - 1; - if (l && !r) return i; - } -} - -export function getIndex( - srt: { start: number; end: number; text: string }[], - ct: number, -) { - for (let i = 0; i < srt.length; i++) { - if (ct >= srt[i].start && ct <= srt[i].end) { - return i; - } - } - return null; -} - -export function getSubtitle( - srt: { start: number; end: number; text: string }[], - currentTime: number, -) { - return ( - srt.find((sub) => currentTime >= sub.start && currentTime <= sub.end) || - null - ); -} - -function toSeconds(timeStr: string): number { - const [h, m, s] = timeStr.replace(",", ".").split(":"); - return parseFloat( - (parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s)).toFixed(3), - ); -} diff --git a/src/app/(features)/_srt-player/types.ts b/src/app/(features)/_srt-player/types.ts deleted file mode 100644 index e0d35be..0000000 --- a/src/app/(features)/_srt-player/types.ts +++ /dev/null @@ -1,132 +0,0 @@ -// ==================== Video Types ==================== - -export interface VideoState { - url: string | null; - isPlaying: boolean; - currentTime: number; - duration: number; - playbackRate: number; - volume: number; -} - -export interface VideoControls { - play: () => void; - pause: () => void; - togglePlayPause: () => void; - seek: (time: number) => void; - setPlaybackRate: (rate: number) => void; - setVolume: (volume: number) => void; - restart: () => void; -} - -// ==================== Subtitle Types ==================== - -export interface SubtitleEntry { - start: number; - end: number; - text: string; - index: number; -} - -export interface SubtitleState { - url: string | null; - data: SubtitleEntry[]; - currentText: string; - currentIndex: number | null; - settings: SubtitleSettings; -} - -export interface SubtitleSettings { - fontSize: number; - backgroundColor: string; - textColor: string; - position: 'top' | 'center' | 'bottom'; - fontFamily: string; - opacity: number; -} - -export interface SubtitleControls { - next: () => void; - previous: () => void; - goToIndex: (index: number) => void; - toggleAutoPause: () => void; -} - -// ==================== Controls Types ==================== - -export interface ControlState { - autoPause: boolean; - showShortcuts: boolean; - showSettings: boolean; -} - -export interface ControlActions { - toggleAutoPause: () => void; - toggleShortcuts: () => void; - toggleSettings: () => void; -} - -export interface KeyboardShortcut { - key: string; - description: string; - action: () => void; -} - -// ==================== Store Types ==================== - -export interface SrtPlayerStore { - // Video state - video: VideoState; - - // Subtitle state - subtitle: SubtitleState; - - // Controls state - controls: ControlState; - - // Video actions - setVideoUrl: (url: string | null) => void; - setPlaying: (playing: boolean) => void; - setCurrentTime: (time: number) => void; - setDuration: (duration: number) => void; - setPlaybackRate: (rate: number) => void; - setVolume: (volume: number) => void; - play: () => void; - pause: () => void; - togglePlayPause: () => void; - seek: (time: number) => void; - restart: () => void; - - // Subtitle actions - setSubtitleUrl: (url: string | null) => void; - setSubtitleData: (data: SubtitleEntry[]) => void; - setCurrentSubtitle: (text: string, index: number | null) => void; - updateSettings: (settings: Partial) => void; - nextSubtitle: () => void; - previousSubtitle: () => void; - restartSubtitle: () => void; - - // Controls actions - toggleAutoPause: () => void; - toggleShortcuts: () => void; - toggleSettings: () => void; -} - -// ==================== Selectors ==================== - -export const selectors = { - canPlay: (state: SrtPlayerStore) => - !!state.video.url && - !!state.subtitle.url && - state.subtitle.data.length > 0, - - currentSubtitle: (state: SrtPlayerStore) => - state.subtitle.currentIndex !== null - ? state.subtitle.data[state.subtitle.currentIndex] - : null, - - progress: (state: SrtPlayerStore) => ({ - current: state.subtitle.currentIndex ?? 0, - total: state.subtitle.data.length, - }), -}; diff --git a/src/app/(features)/_srt-player/components/ControlPanel.tsx b/src/app/(features)/srt-player/components/ControlPanel.tsx similarity index 59% rename from src/app/(features)/_srt-player/components/ControlPanel.tsx rename to src/app/(features)/srt-player/components/ControlPanel.tsx index 004c245..8bcaf43 100644 --- a/src/app/(features)/_srt-player/components/ControlPanel.tsx +++ b/src/app/(features)/srt-player/components/ControlPanel.tsx @@ -2,11 +2,11 @@ import { useCallback, useMemo } from 'react'; import { useTranslations } from 'next-intl'; -import { Video, FileText, ChevronLeft, ChevronRight, RotateCcw, Pause, Play } from 'lucide-react'; +import { Video, FileText, ChevronLeft, ChevronRight, RotateCcw, Pause, Play, Settings, Keyboard } from 'lucide-react'; import { Button, LightButton } from '@/design-system/base/button'; import { Range } from '@/design-system/base/range'; import { HStack, VStack } from '@/design-system/layout/stack'; -import { useSrtPlayerStore } from '../store'; +import { useSrtPlayerStore } from '../stores/srtPlayerStore'; import { useFileUpload } from '../hooks/useFileUpload'; import { toast } from 'sonner'; @@ -14,7 +14,6 @@ export function ControlPanel() { const t = useTranslations('srt_player'); const { uploadVideo, uploadSubtitle } = useFileUpload(); - // Store state const videoUrl = useSrtPlayerStore((state) => state.video.url); const subtitleUrl = useSrtPlayerStore((state) => state.subtitle.url); const subtitleData = useSrtPlayerStore((state) => state.subtitle.data); @@ -22,8 +21,10 @@ export function ControlPanel() { const isPlaying = useSrtPlayerStore((state) => state.video.isPlaying); const playbackRate = useSrtPlayerStore((state) => state.video.playbackRate); const autoPause = useSrtPlayerStore((state) => state.controls.autoPause); + const showSettings = useSrtPlayerStore((state) => state.controls.showSettings); + const showShortcuts = useSrtPlayerStore((state) => state.controls.showShortcuts); + const settings = useSrtPlayerStore((state) => state.subtitle.settings); - // Store actions const togglePlayPause = useSrtPlayerStore((state) => state.togglePlayPause); const nextSubtitle = useSrtPlayerStore((state) => state.nextSubtitle); const previousSubtitle = useSrtPlayerStore((state) => state.previousSubtitle); @@ -33,47 +34,45 @@ export function ControlPanel() { const setVideoUrl = useSrtPlayerStore((state) => state.setVideoUrl); const setSubtitleUrl = useSrtPlayerStore((state) => state.setSubtitleUrl); const seek = useSrtPlayerStore((state) => state.seek); + const toggleSettings = useSrtPlayerStore((state) => state.toggleSettings); + const toggleShortcuts = useSrtPlayerStore((state) => state.toggleShortcuts); + const updateSettings = useSrtPlayerStore((state) => state.updateSettings); - // Computed values const canPlay = useMemo(() => !!videoUrl && !!subtitleUrl && subtitleData.length > 0, [videoUrl, subtitleUrl, subtitleData]); const currentProgress = currentIndex ?? 0; const totalProgress = Math.max(0, subtitleData.length - 1); - // Handle video upload const handleVideoUpload = useCallback(() => { uploadVideo(setVideoUrl, (error) => { toast.error(t('videoUploadFailed') + ': ' + error.message); }); }, [uploadVideo, setVideoUrl, t]); - // Handle subtitle upload const handleSubtitleUpload = useCallback(() => { - uploadSubtitle(setSubtitleUrl, (error) => { + uploadSubtitle((url) => { + setSubtitleUrl(url); + }, (error) => { toast.error(t('subtitleUploadFailed') + ': ' + error.message); }); }, [uploadSubtitle, setSubtitleUrl, t]); - // Handle seek const handleSeek = useCallback((index: number) => { if (subtitleData[index]) { seek(subtitleData[index].start); } }, [subtitleData, seek]); - // Handle playback rate change const handlePlaybackRateChange = useCallback(() => { const rates = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0]; - const currentIndex = rates.indexOf(playbackRate); - const nextIndex = (currentIndex + 1) % rates.length; - setPlaybackRate(rates[nextIndex]); + const currentIndexRate = rates.indexOf(playbackRate); + const nextIndexRate = (currentIndexRate + 1) % rates.length; + setPlaybackRate(rates[nextIndexRate]); }, [playbackRate, setPlaybackRate]); return (
- {/* Upload Status Cards */} - {/* Video Upload Card */}
- {/* Subtitle Upload Card */}
- {/* Controls Area */} - {/* Playback Controls */} + + } + > + {t('settings')} + + + } + > + {t('shortcuts')} + - {/* Seek Bar */} - {/* Progress Stats */} {currentIndex !== null ? `${currentIndex + 1}/${subtitleData.length}` : '0/0'} - {/* Playback Rate Badge */} {playbackRate}x - {/* Auto Pause Badge */} + + {showSettings && ( +
+

{t('subtitleSettings')}

+ + + {t('fontSize')} + updateSettings({ fontSize: value })} + /> + {settings.fontSize}px + + + + {t('textColor')} + updateSettings({ textColor: e.target.value })} + className="w-8 h-8 rounded cursor-pointer" + /> + + + + {t('backgroundColor')} + updateSettings({ backgroundColor: e.target.value })} + className="w-8 h-8 rounded cursor-pointer" + /> + + + + {t('position')} + + {(['top', 'center', 'bottom'] as const).map((pos) => ( + + ))} + + + + + {t('opacity')} + updateSettings({ opacity: value })} + /> + {Math.round(settings.opacity * 100)}% + + +
+ )} + + {showShortcuts && ( +
+

{t('keyboardShortcuts')}

+ + {[ + { key: 'Space', desc: t('playPause') }, + { key: 'N', desc: t('next') }, + { key: 'P', desc: t('previous') }, + { key: 'R', desc: t('restart') }, + { key: 'A', desc: t('autoPauseToggle') }, + ].map((shortcut) => ( + + {shortcut.key} + {shortcut.desc} + + ))} + +
+ )}
); diff --git a/src/app/(features)/_srt-player/components/VideoPlayerPanel.tsx b/src/app/(features)/srt-player/components/VideoPlayerPanel.tsx similarity index 92% rename from src/app/(features)/_srt-player/components/VideoPlayerPanel.tsx rename to src/app/(features)/srt-player/components/VideoPlayerPanel.tsx index 05980b4..f52a069 100644 --- a/src/app/(features)/_srt-player/components/VideoPlayerPanel.tsx +++ b/src/app/(features)/srt-player/components/VideoPlayerPanel.tsx @@ -1,8 +1,8 @@ "use client"; import { useRef, useEffect, forwardRef } from 'react'; -import { useSrtPlayerStore } from '../store'; -import { setVideoRef } from '../store'; +import { useSrtPlayerStore } from '../stores/srtPlayerStore'; +import { setVideoRef } from '../stores/srtPlayerStore'; export const VideoPlayerPanel = forwardRef((_, ref) => { const localVideoRef = useRef(null); @@ -14,14 +14,12 @@ export const VideoPlayerPanel = forwardRef((_, ref) => { const currentText = useSrtPlayerStore((state) => state.subtitle.currentText); const settings = useSrtPlayerStore((state) => state.subtitle.settings); - // 设置 video ref 给 store useEffect(() => { setVideoRef(videoRef); }, [videoRef]); return (
- {/* 空状态提示 */} {(!videoUrl || !subtitleUrl || subtitleData.length === 0) && (
@@ -41,7 +39,6 @@ export const VideoPlayerPanel = forwardRef((_, ref) => {
)} - {/* 视频元素 */} {videoUrl && (