void
) => {
try {
- // 验证文件大小(限制为1000MB)
- const maxSize = 1000 * 1024 * 1024; // 1000MB
+ const maxSize = 1000 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error(`文件大小超过限制 (${(file.size / 1024 / 1024).toFixed(2)}MB > 1000MB)`);
}
@@ -34,7 +33,6 @@ export function useFileUpload() {
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
- // 验证文件类型
if (!file.type.startsWith('video/')) {
onError?.(new Error('请选择有效的视频文件'));
return;
@@ -61,7 +59,6 @@ export function useFileUpload() {
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
- // 验证文件扩展名
if (!file.name.toLowerCase().endsWith('.srt')) {
onError?.(new Error('请选择.srt格式的字幕文件'));
return;
@@ -80,6 +77,5 @@ export function useFileUpload() {
return {
uploadVideo,
uploadSubtitle,
- uploadFile,
};
-}
\ No newline at end of file
+}
diff --git a/src/app/(features)/_srt-player/hooks/useKeyboardShortcuts.ts b/src/app/(features)/srt-player/hooks/useKeyboardShortcuts.ts
similarity index 87%
rename from src/app/(features)/_srt-player/hooks/useKeyboardShortcuts.ts
rename to src/app/(features)/srt-player/hooks/useKeyboardShortcuts.ts
index 42507c8..90e849b 100644
--- a/src/app/(features)/_srt-player/hooks/useKeyboardShortcuts.ts
+++ b/src/app/(features)/srt-player/hooks/useKeyboardShortcuts.ts
@@ -1,14 +1,8 @@
"use client";
import { useEffect } from "react";
-import { useSrtPlayerStore } from "../store";
+import { useSrtPlayerStore } from "../stores/srtPlayerStore";
-/**
- * useSrtPlayerShortcuts - SRT 播放器快捷键 Hook
- *
- * 自动为 SRT 播放器设置键盘快捷键,无需传入参数。
- * 直接使用 Zustand store 中的 actions。
- */
export function useSrtPlayerShortcuts(enabled: boolean = true) {
const togglePlayPause = useSrtPlayerStore((state) => state.togglePlayPause);
const nextSubtitle = useSrtPlayerStore((state) => state.nextSubtitle);
@@ -20,7 +14,6 @@ export function useSrtPlayerShortcuts(enabled: boolean = true) {
const handleKeyDown = (event: globalThis.KeyboardEvent) => {
if (!enabled) return;
- // 防止在输入框中触发快捷键
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
return;
@@ -61,7 +54,6 @@ export function useSrtPlayerShortcuts(enabled: boolean = true) {
}, [enabled, togglePlayPause, nextSubtitle, previousSubtitle, restartSubtitle, toggleAutoPause]);
}
-// 保留通用快捷键 Hook 用于其他场景
export function useKeyboardShortcuts(
shortcuts: Array<{ key: string; action: () => void }>,
isEnabled: boolean = true
diff --git a/src/app/(features)/srt-player/hooks/useSubtitleSync.ts b/src/app/(features)/srt-player/hooks/useSubtitleSync.ts
new file mode 100644
index 0000000..748b463
--- /dev/null
+++ b/src/app/(features)/srt-player/hooks/useSubtitleSync.ts
@@ -0,0 +1,101 @@
+"use client";
+
+import { useEffect, useRef, useCallback } from "react";
+import { useSrtPlayerStore } from "../stores/srtPlayerStore";
+
+export function useSubtitleSync() {
+ const timeoutRef = useRef
(null);
+ const lastIndexRef = useRef(null);
+
+ const subtitleData = useSrtPlayerStore((state) => state.subtitle.data);
+ const isPlaying = useSrtPlayerStore((state) => state.video.isPlaying);
+ const autoPause = useSrtPlayerStore((state) => state.controls.autoPause);
+ const playbackRate = useSrtPlayerStore((state) => state.video.playbackRate);
+ const currentTime = useSrtPlayerStore((state) => state.video.currentTime);
+
+ const setCurrentSubtitle = useSrtPlayerStore((state) => state.setCurrentSubtitle);
+ const pause = useSrtPlayerStore((state) => state.pause);
+
+ const scheduleAutoPause = useCallback(() => {
+ if (!autoPause || !isPlaying) {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ timeoutRef.current = null;
+ }
+ return;
+ }
+
+ const currentTimeNow = useSrtPlayerStore.getState().video.currentTime;
+ const currentIndexNow = useSrtPlayerStore.getState().subtitle.currentIndex;
+
+ if (currentIndexNow === null || !subtitleData[currentIndexNow]) {
+ return;
+ }
+
+ const subtitle = subtitleData[currentIndexNow];
+ const timeUntilEnd = subtitle.end - currentTimeNow;
+
+ if (timeUntilEnd <= 0) {
+ return;
+ }
+
+ const advanceTime = 0.15;
+ const realTimeUntilPause = (timeUntilEnd - advanceTime) / playbackRate;
+
+ if (realTimeUntilPause > 0) {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+
+ timeoutRef.current = setTimeout(() => {
+ pause();
+ }, realTimeUntilPause * 1000);
+ }
+ }, [autoPause, isPlaying, subtitleData, playbackRate, pause]);
+
+ useEffect(() => {
+ if (!subtitleData || subtitleData.length === 0) {
+ setCurrentSubtitle('', null);
+ lastIndexRef.current = null;
+ return;
+ }
+
+ let newIndex: number | null = null;
+
+ for (let i = 0; i < subtitleData.length; i++) {
+ const subtitle = subtitleData[i];
+ if (currentTime >= subtitle.start && currentTime <= subtitle.end) {
+ newIndex = i;
+ break;
+ }
+ }
+
+ if (newIndex !== lastIndexRef.current) {
+ lastIndexRef.current = newIndex;
+ if (newIndex !== null) {
+ setCurrentSubtitle(subtitleData[newIndex].text, newIndex);
+ } else {
+ setCurrentSubtitle('', null);
+ }
+ }
+ }, [subtitleData, currentTime, setCurrentSubtitle]);
+
+ useEffect(() => {
+ scheduleAutoPause();
+ }, [isPlaying, autoPause]);
+
+ useEffect(() => {
+ if (isPlaying && autoPause) {
+ scheduleAutoPause();
+ }
+ }, [playbackRate, currentTime]);
+
+ useEffect(() => {
+ return () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ timeoutRef.current = null;
+ }
+ };
+ }, []);
+}
diff --git a/src/app/(features)/_srt-player/hooks/useVideoSync.ts b/src/app/(features)/srt-player/hooks/useVideoSync.ts
similarity index 95%
rename from src/app/(features)/_srt-player/hooks/useVideoSync.ts
rename to src/app/(features)/srt-player/hooks/useVideoSync.ts
index 9909e69..20f7a90 100644
--- a/src/app/(features)/_srt-player/hooks/useVideoSync.ts
+++ b/src/app/(features)/srt-player/hooks/useVideoSync.ts
@@ -1,7 +1,7 @@
"use client";
import { useEffect, type RefObject } from 'react';
-import { useSrtPlayerStore } from '../store';
+import { useSrtPlayerStore } from '../stores/srtPlayerStore';
export function useVideoSync(videoRef: RefObject) {
const setCurrentTime = useSrtPlayerStore((state) => state.setCurrentTime);
diff --git a/src/app/(features)/srt-player/page.tsx b/src/app/(features)/srt-player/page.tsx
index 15dd3aa..e12f123 100644
--- a/src/app/(features)/srt-player/page.tsx
+++ b/src/app/(features)/srt-player/page.tsx
@@ -1,97 +1,177 @@
"use client";
-import { LightButton, PageLayout } from "@/components/ui";
-import { useVideoStore } from "./stores/videoStore";
-import { useEffect, useRef } from "react";
-import Link from "next/link";
+import { useRef, useEffect } from "react";
+import { useTranslations } from "next-intl";
+import { toast } from "sonner";
+import { PageLayout } from "@/components/ui/PageLayout";
+import { LightButton } from "@/design-system/base/button";
import { HStack } from "@/design-system/layout/stack";
-import { MessageSquareQuote, Video } from "lucide-react";
-import { useFileUpload } from "./useFileUpload";
-import { useSubtitleStore } from "./stores/substitleStore";
-import { getCurrentIndex } from "./subtitleParser";
+import { Video, FileText, ChevronLeft, ChevronRight, RotateCcw, Pause, Play } from "lucide-react";
+import { useVideoSync } from "./hooks/useVideoSync";
+import { useSubtitleSync } from "./hooks/useSubtitleSync";
+import { useSrtPlayerShortcuts } from "./hooks/useKeyboardShortcuts";
+import { loadSubtitle } from "./utils/subtitleParser";
+import { useSrtPlayerStore } from "./stores/srtPlayerStore";
+import { useFileUpload } from "./hooks/useFileUpload";
+import { setVideoRef } from "./stores/srtPlayerStore";
+import Link from "next/link";
+
+export default function SrtPlayerPage() {
+ const t = useTranslations("home");
+ const srtT = useTranslations("srt_player");
-export default function SRTPlayerPage() {
const videoRef = useRef(null);
- const { setVideoRef, pause, currentSrc, isPlaying, loadVideo, loaded, getCurrentTime, getDuration, play, setOnTimeUpdate } = useVideoStore();
- const {
- uploadVideo,
- uploadSubtitle,
- } = useFileUpload();
- const {
- sub,
- setSub,
- index,
- setIndex
- } = useSubtitleStore();
+ const { uploadVideo, uploadSubtitle } = useFileUpload();
+
+ const subtitleUrl = useSrtPlayerStore((state) => state.subtitle.url);
+ const setSubtitleData = useSrtPlayerStore((state) => state.setSubtitleData);
+ const setSubtitleUrl = useSrtPlayerStore((state) => state.setSubtitleUrl);
+ const setVideoUrl = useSrtPlayerStore((state) => state.setVideoUrl);
+
+ const videoUrl = useSrtPlayerStore((state) => state.video.url);
+ const subtitleData = useSrtPlayerStore((state) => state.subtitle.data);
+ const currentIndex = useSrtPlayerStore((state) => state.subtitle.currentIndex);
+ const isPlaying = useSrtPlayerStore((state) => state.video.isPlaying);
+ const playbackRate = useSrtPlayerStore((state) => state.video.playbackRate);
+ const autoPause = useSrtPlayerStore((state) => state.controls.autoPause);
+
+ const togglePlayPause = useSrtPlayerStore((state) => state.togglePlayPause);
+ const nextSubtitle = useSrtPlayerStore((state) => state.nextSubtitle);
+ const previousSubtitle = useSrtPlayerStore((state) => state.previousSubtitle);
+ const restartSubtitle = useSrtPlayerStore((state) => state.restartSubtitle);
+ const setPlaybackRate = useSrtPlayerStore((state) => state.setPlaybackRate);
+ const toggleAutoPause = useSrtPlayerStore((state) => state.toggleAutoPause);
+ const seek = useSrtPlayerStore((state) => state.seek);
+
+ useVideoSync(videoRef);
+ useSubtitleSync();
+ useSrtPlayerShortcuts();
useEffect(() => {
setVideoRef(videoRef);
- setOnTimeUpdate((time) => {
- setIndex(getCurrentIndex(sub, time));
+ }, [videoRef]);
+
+ const canPlay = !!videoUrl && !!subtitleUrl && subtitleData.length > 0;
+
+ useEffect(() => {
+ if (subtitleUrl) {
+ loadSubtitle(subtitleUrl)
+ .then((subtitleData) => {
+ setSubtitleData(subtitleData);
+ toast.success(srtT("subtitleLoadSuccess"));
+ })
+ .catch((error) => {
+ toast.error(srtT("subtitleLoadFailed") + ": " + error.message);
+ });
+ }
+ }, [srtT, subtitleUrl, setSubtitleData]);
+
+ const handleVideoUpload = () => {
+ uploadVideo((url) => {
+ setVideoUrl(url);
+ }, (error) => {
+ toast.error(t('videoUploadFailed') + ': ' + error.message);
});
- return () => {
- setVideoRef();
- setOnTimeUpdate(() => { });
- };
- }, [setVideoRef, setOnTimeUpdate, sub, setIndex]);
+ };
+
+ const handleSubtitleUpload = () => {
+ uploadSubtitle((url) => {
+ setSubtitleUrl(url);
+ }, (error) => {
+ toast.error(t('subtitleUploadFailed') + ': ' + error.message);
+ });
+ };
+
+ const handlePlaybackRateChange = () => {
+ const rates = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];
+ const currentIndexRate = rates.indexOf(playbackRate);
+ const nextIndexRate = (currentIndexRate + 1) % rates.length;
+ setPlaybackRate(rates[nextIndexRate]);
+ };
+
+ const currentSubtitle = currentIndex !== null ? subtitleData[currentIndex] : null;
return (
-
-
-
- {
- sub[index] && sub[index].text.split(" ").map((s, i) =>
-
- {s}
-
- )}
+
+
+ {t("srtPlayer.name")}
+
+
+ {t("srtPlayer.description")}
+
+
+
+
+
+
+ {currentSubtitle && currentSubtitle.text.split(" ").map((s, i) => (
+
+ {s}
+
+ ))}
- {/* 上传区域 */}
视频文件
-
uploadVideo((url) => {
- loadVideo(url);
- })}>{loaded ? currentSrc?.split("/").pop() : "视频未上传"}
+
+ {videoUrl ? '已上传' : '上传视频'}
+
-
- {sub.length > 0 ? `字幕已上传 (${sub.length} 条)` : "字幕未上传"}
+
+
+ {subtitleData.length > 0 ? `字幕已上传 (${subtitleData.length} 条)` : "字幕未上传"}
+
-
- uploadSubtitle((sub) => {
- setSub(sub);
- })
- }>上传字幕
+
+ {subtitleUrl ? '已上传' : '上传字幕'}
+
- {
- /* 控制面板 */
- sub.length > 0 && loaded &&
+ {canPlay && (
- {isPlaying() ?
- LightButton({ children: "pause", onClick: () => pause() }) :
- LightButton({ children: "play", onClick: () => play() })}
- previous
- next
- restart
- 1x
- ap(on)
+ {isPlaying ? (
+ }>
+ {srtT('pause')}
+
+ ) : (
+ }>
+ {srtT('play')}
+
+ )}
+ }>
+ {srtT('previous')}
+
+ }>
+ {srtT('next')}
+
+ }>
+ {srtT('restart')}
+
+
+ {playbackRate}x
+
+
+ {srtT('autoPause', { enabled: autoPause ? srtT('on') : srtT('off') })}
+
- }
-
+ )}
);
}
diff --git a/src/app/(features)/_srt-player/store.ts b/src/app/(features)/srt-player/stores/srtPlayerStore.ts
similarity index 94%
rename from src/app/(features)/_srt-player/store.ts
rename to src/app/(features)/srt-player/stores/srtPlayerStore.ts
index b8ab86c..faddcd3 100644
--- a/src/app/(features)/_srt-player/store.ts
+++ b/src/app/(features)/srt-player/stores/srtPlayerStore.ts
@@ -10,7 +10,7 @@ import type {
ControlState,
SubtitleSettings,
SubtitleEntry,
-} from './types';
+} from '../types';
import type { RefObject } from 'react';
let videoRef: RefObject
| null;
@@ -19,7 +19,6 @@ export function setVideoRef(ref: RefObject | null) {
videoRef = ref;
}
-// 初始状态
const initialVideoState: VideoState = {
url: null,
isPlaying: false,
@@ -55,12 +54,10 @@ const initialControlState: ControlState = {
export const useSrtPlayerStore = create()(
devtools(
(set, get) => ({
- // ==================== Initial State ====================
video: initialVideoState,
subtitle: initialSubtitleState,
controls: initialControlState,
- // ==================== Video Actions ====================
setVideoUrl: (url) =>
set((state) => {
if (videoRef?.current) {
@@ -111,7 +108,6 @@ export const useSrtPlayerStore = create()(
pause: () => {
if (videoRef?.current) {
- // 只有在视频正在播放时才暂停,避免重复调用
if (!videoRef.current.paused) {
videoRef.current.pause();
}
@@ -146,7 +142,6 @@ export const useSrtPlayerStore = create()(
}
},
- // ==================== Subtitle Actions ====================
setSubtitleUrl: (url) =>
set((state) => ({ subtitle: { ...state.subtitle, url } })),
@@ -202,7 +197,6 @@ export const useSrtPlayerStore = create()(
}
},
- // ==================== Controls Actions ====================
toggleAutoPause: () =>
set((state) => ({
controls: { ...state.controls, autoPause: !state.controls.autoPause },
diff --git a/src/app/(features)/srt-player/stores/substitleStore.ts b/src/app/(features)/srt-player/stores/substitleStore.ts
deleted file mode 100644
index afda49d..0000000
--- a/src/app/(features)/srt-player/stores/substitleStore.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { create } from "zustand/react";
-import { SubtitleEntry } from "../types";
-import { devtools } from "zustand/middleware";
-
-interface SubstitleStore {
- sub: SubtitleEntry[];
- index: number;
- setSub: (sub: SubtitleEntry[]) => void;
- setIndex: (index: number) => void;
-}
-
-export const useSubtitleStore = create()(
- devtools((set) => ({
- sub: [],
- index: 0,
- setSub: (sub) => set({ sub, index: 0 }),
- setIndex: (index) => set({ index }),
- }))
-);
diff --git a/src/app/(features)/srt-player/stores/videoStore.ts b/src/app/(features)/srt-player/stores/videoStore.ts
deleted file mode 100644
index 6a5d79e..0000000
--- a/src/app/(features)/srt-player/stores/videoStore.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { create } from 'zustand';
-import { devtools } from 'zustand/middleware';
-
-interface VideoStore {
- videoRef?: React.RefObject;
- currentSrc: string | null;
- loaded: boolean;
- onTimeUpdate: (time: number) => void;
- setOnTimeUpdate: (handler: (time: number) => void) => void;
- setVideoRef: (ref?: React.RefObject) => void;
- loadVideo: (url: string, options?: { autoplay?: boolean; muted?: boolean; }) => void;
- play: () => void;
- pause: () => void;
- togglePlay: () => void;
- seekTo: (time: number) => void;
- setVolume: (vol: number) => void;
- getCurrentTime: () => number | undefined;
- getDuration: () => number | undefined;
- isPlaying: () => boolean;
-}
-
-export const useVideoStore = create()(
- devtools((set, get) => ({
- videoRef: null,
- currentSrc: null,
- loaded: false,
- onTimeUpdate: (time) => { },
- setOnTimeUpdate: (handler) => {
- set({ onTimeUpdate: handler });
- },
- setVideoRef: (ref) => {
- set({ videoRef: ref });
- ref?.current?.addEventListener("timeupdate", () => {
- const currentTime = get().videoRef?.current?.currentTime;
- if (currentTime !== undefined) {
- get().onTimeUpdate(currentTime);
- }
- });
- },
- loadVideo: (url: string, options = { autoplay: false, muted: false }) => {
- const { videoRef } = get();
- const video = videoRef?.current;
-
- if (!url) {
- console.warn('loadVideo: empty url provided');
- return;
- }
-
- if (!video) {
- console.debug('loadVideo: video ref not ready yet');
- return;
- }
-
- try {
- video.pause();
- video.currentTime = 0;
- video.src = url;
- if (options.autoplay) {
- video
- .play()
- .then(() => {
- console.debug('Auto play succeeded after src change');
- })
- .catch((err) => {
- console.warn('Auto play failed after src change:', err);
- });
- }
- set({ currentSrc: url, loaded: true });
- } catch (err) {
- console.error('Failed to load video:', err);
- set({ loaded: false });
- }
- },
-
- play: () => {
- const video = get().videoRef?.current;
- if (video) video.play().catch(() => { });
- },
-
- pause: () => {
- const video = get().videoRef?.current;
- if (video) video.pause();
- },
-
- togglePlay: () => {
- const video = get().videoRef?.current;
- if (!video) return;
- if (video.paused) {
- video.play().catch(() => { });
- } else {
- video.pause();
- }
- },
-
- seekTo: (time: number) => {
- const video = get().videoRef?.current;
- if (video) video.currentTime = time;
- },
-
- setVolume: (vol: number) => {
- const video = get().videoRef?.current;
- if (video) video.volume = Math.max(0, Math.min(1, vol));
- },
- getCurrentTime: () => get().videoRef?.current?.currentTime,
- getDuration: () => get().videoRef?.current?.duration,
- isPlaying: () => {
- const video = get().videoRef?.current;
- if (!video) return false;
- return !video.paused && !video.ended && video.readyState > 2;
- }
- }))
-);
\ No newline at end of file
diff --git a/src/app/(features)/srt-player/subtitleParser.ts b/src/app/(features)/srt-player/subtitleParser.ts
deleted file mode 100644
index 8a72125..0000000
--- a/src/app/(features)/srt-player/subtitleParser.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { SubtitleEntry } from "./types";
-
-export function parseSrt(data: string): SubtitleEntry[] {
- const lines = data.split(/\r?\n/);
- const result: SubtitleEntry[] = [];
- 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(),
- index: result.length,
- });
- i++;
- }
-
- return result;
-}
-
-export function getNearestIndex(
- subtitles: SubtitleEntry[],
- currentTime: number,
-): number | null {
- for (let i = 0; i < subtitles.length; i++) {
- const subtitle = subtitles[i];
- const isBefore = currentTime - subtitle.start >= 0;
- const isAfter = currentTime - subtitle.end >= 0;
-
- if (!isBefore || !isAfter) return i - 1;
- if (isBefore && !isAfter) return i;
- }
- return null;
-}
-
-export function getCurrentIndex(
- subtitles: SubtitleEntry[],
- currentTime: number,
-): number {
- for (let index = 0; index < subtitles.length; index++) {
- if (subtitles[index].start <= currentTime && subtitles[index].end >= currentTime) {
- return index;
- }
- }
- return -1;
-}
-
-function toSeconds(timeStr: string): number {
- const [h, m, s] = timeStr.replace(",", ".").split(":");
- return parseFloat(
- (parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s)).toFixed(3),
- );
-}
-
-export function loadSubtitle(url: string): Promise {
- return fetch(url)
- .then(response => response.text())
- .then(data => parseSrt(data))
- .catch(error => {
- console.error('加载字幕失败', error);
- return [];
- });
-}
\ No newline at end of file
diff --git a/src/app/(features)/srt-player/types.ts b/src/app/(features)/srt-player/types.ts
index 6e397d4..e0d35be 100644
--- a/src/app/(features)/srt-player/types.ts
+++ b/src/app/(features)/srt-player/types.ts
@@ -1,6 +1,132 @@
+// ==================== 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/useFileUpload.ts b/src/app/(features)/srt-player/useFileUpload.ts
deleted file mode 100644
index a22ce99..0000000
--- a/src/app/(features)/srt-player/useFileUpload.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-"use client";
-
-import { loadSubtitle } from "./subtitleParser";
-
-const createUploadHandler = (
- accept: string,
- validate: (file: File) => boolean,
- errorMessage: string,
- processFile: (file: File) => T | Promise
-) => {
- return ((
- onSuccess: (result: T) => void,
- onError?: (error: Error) => void
- ) => {
- const input = document.createElement('input');
- input.type = 'file';
- input.accept = accept;
-
- input.onchange = async (e) => {
- const file = (e.target as HTMLInputElement).files?.[0];
- if (file) {
- if (!validate(file)) {
- onError?.(new Error(errorMessage));
- return;
- }
- try {
- const result = await processFile(file);
- onSuccess(result);
- } catch (error) {
- onError?.(error instanceof Error ? error : new Error('文件处理失败'));
- }
- }
- };
-
- input.onerror = () => {
- onError?.(new Error('文件选择失败'));
- };
-
- input.click();
- });
-};
-
-export function useFileUpload() {
- const uploadVideo = createUploadHandler(
- 'video/*',
- (file) => file.type.startsWith('video/'),
- '请选择有效的视频文件',
- (file) => URL.createObjectURL(file)
- );
-
- const uploadSubtitle = createUploadHandler(
- '.srt',
- (file) => file.name.toLowerCase().endsWith('.srt'),
- '请选择.srt格式的字幕文件',
- async (file) => {
- const url = URL.createObjectURL(file);
- return loadSubtitle(url);
- }
- );
-
- return {
- uploadVideo,
- uploadSubtitle,
- };
-}
\ No newline at end of file
diff --git a/src/app/(features)/_srt-player/utils/subtitleParser.ts b/src/app/(features)/srt-player/utils/subtitleParser.ts
similarity index 99%
rename from src/app/(features)/_srt-player/utils/subtitleParser.ts
rename to src/app/(features)/srt-player/utils/subtitleParser.ts
index 33be08b..b49eba6 100644
--- a/src/app/(features)/_srt-player/utils/subtitleParser.ts
+++ b/src/app/(features)/srt-player/utils/subtitleParser.ts
@@ -96,4 +96,4 @@ export async function loadSubtitle(url: string): Promise {
console.error('加载字幕失败', error);
return [];
}
-}
\ No newline at end of file
+}