Wed Mar 4 09:32:00 AM CST 2026

This commit is contained in:
2026-03-04 09:32:00 +08:00
parent bf80e17514
commit 613df6824b
19 changed files with 589 additions and 196 deletions

View File

@@ -9,10 +9,10 @@ export function useFileUpload() {
onError?: (error: Error) => void onError?: (error: Error) => void
) => { ) => {
try { try {
// 验证文件大小限制为100MB // 验证文件大小限制为1000MB
const maxSize = 100 * 1024 * 1024; // 100MB const maxSize = 1000 * 1024 * 1024; // 1000MB
if (file.size > maxSize) { if (file.size > maxSize) {
throw new Error(`文件大小超过限制 (${(file.size / 1024 / 1024).toFixed(2)}MB > 100MB)`); throw new Error(`文件大小超过限制 (${(file.size / 1024 / 1024).toFixed(2)}MB > 1000MB)`);
} }
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);

View File

@@ -11,11 +11,11 @@ import { useSrtPlayerStore } from "../store";
*/ */
export function useSubtitleSync() { export function useSubtitleSync() {
const lastSubtitleRef = useRef<number | null>(null); const lastSubtitleRef = useRef<number | null>(null);
const hasAutoPausedRef = useRef<{ [key: number]: boolean }>({}); // 追踪每个字幕是否已触发自动暂停
const rafIdRef = useRef<number>(0); const rafIdRef = useRef<number>(0);
// 从 store 获取状态 // 从 store 获取状态
const subtitleData = useSrtPlayerStore((state) => state.subtitle.data); const subtitleData = useSrtPlayerStore((state) => state.subtitle.data);
const currentTime = useSrtPlayerStore((state) => state.video.currentTime);
const isPlaying = useSrtPlayerStore((state) => state.video.isPlaying); const isPlaying = useSrtPlayerStore((state) => state.video.isPlaying);
const autoPause = useSrtPlayerStore((state) => state.controls.autoPause); const autoPause = useSrtPlayerStore((state) => state.controls.autoPause);
@@ -27,6 +27,9 @@ export function useSubtitleSync() {
// 同步循环 // 同步循环
useEffect(() => { useEffect(() => {
const syncSubtitles = () => { const syncSubtitles = () => {
// 从 store 获取最新的 currentTime
const currentTime = useSrtPlayerStore.getState().video.currentTime;
// 获取当前时间对应的字幕索引 // 获取当前时间对应的字幕索引
const getCurrentSubtitleIndex = (time: number): number | null => { const getCurrentSubtitleIndex = (time: number): number | null => {
for (let i = 0; i < subtitleData.length; i++) { for (let i = 0; i < subtitleData.length; i++) {
@@ -38,11 +41,6 @@ export function useSubtitleSync() {
return null; return null;
}; };
// 检查是否需要自动暂停
const shouldAutoPause = (subtitle: { start: number; end: number }, time: number): boolean => {
return autoPause && time >= subtitle.end - 0.2 && time < subtitle.end;
};
const currentIndex = getCurrentSubtitleIndex(currentTime); const currentIndex = getCurrentSubtitleIndex(currentTime);
// 检查字幕是否发生变化 // 检查字幕是否发生变化
@@ -57,14 +55,26 @@ export function useSubtitleSync() {
} }
} }
// 检查是否需要自动暂停 // 检查是否需要自动暂停(每个字幕只触发一次)
const currentSubtitle = currentIndex !== null ? subtitleData[currentIndex] : null; if (autoPause && currentIndex !== null) {
if (currentSubtitle && shouldAutoPause(currentSubtitle, currentTime)) { 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); seek(currentSubtitle.start);
// 使用 setTimeout 确保在 seek 之后暂停
setTimeout(() => {
pause(); pause();
}, 0);
}
} }
// 如果视频正在播放,继续循环
if (useSrtPlayerStore.getState().video.isPlaying) {
rafIdRef.current = requestAnimationFrame(syncSubtitles); rafIdRef.current = requestAnimationFrame(syncSubtitles);
}
}; };
if (subtitleData.length > 0 && isPlaying) { if (subtitleData.length > 0 && isPlaying) {
@@ -76,10 +86,11 @@ export function useSubtitleSync() {
cancelAnimationFrame(rafIdRef.current); cancelAnimationFrame(rafIdRef.current);
} }
}; };
}, [subtitleData, currentTime, isPlaying, autoPause, setCurrentSubtitle, seek, pause]); }, [subtitleData, isPlaying, autoPause, setCurrentSubtitle, seek, pause]);
// 重置最后字幕引用 // 重置最后字幕引用
useEffect(() => { useEffect(() => {
lastSubtitleRef.current = null; lastSubtitleRef.current = null;
hasAutoPausedRef.current = {};
}, [subtitleData]); }, [subtitleData]);
} }

View File

@@ -0,0 +1,63 @@
"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<HTMLVideoElement>(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 (
<PageLayout>
{/* Title */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-800 mb-2">
{t("srtPlayer.name")}
</h1>
<p className="text-lg text-gray-600">
{t("srtPlayer.description")}
</p>
</div>
{/* Video Player */}
<VideoPlayerPanel ref={videoRef} />
{/* Control Panel */}
<ControlPanel />
</PageLayout>
);
}

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { create } from 'zustand'; import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware'; import { devtools } from 'zustand/middleware';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { import type {
SrtPlayerStore, SrtPlayerStore,
@@ -11,12 +11,11 @@ import type {
SubtitleSettings, SubtitleSettings,
SubtitleEntry, SubtitleEntry,
} from './types'; } from './types';
import type { MutableRefObject } from 'react'; import type { RefObject } from 'react';
// 声明视频 ref 的全局类型(用于 store 访问 video element let videoRef: RefObject<HTMLVideoElement | null> | null;
let videoRef: MutableRefObject<HTMLVideoElement | null> | null = null;
export function setVideoRef(ref: MutableRefObject<HTMLVideoElement | null>) { export function setVideoRef(ref: RefObject<HTMLVideoElement | null> | null) {
videoRef = ref; videoRef = ref;
} }
@@ -112,7 +111,10 @@ export const useSrtPlayerStore = create<SrtPlayerStore>()(
pause: () => { pause: () => {
if (videoRef?.current) { if (videoRef?.current) {
// 只有在视频正在播放时才暂停,避免重复调用
if (!videoRef.current.paused) {
videoRef.current.pause(); videoRef.current.pause();
}
set((state) => ({ video: { ...state.video, isPlaying: false } })); set((state) => ({ video: { ...state.video, isPlaying: false } }));
} }
}, },

View File

@@ -0,0 +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<SubtitleSettings>) => 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,
}),
};

View File

@@ -1,63 +1,95 @@
"use client"; "use client";
import { useRef, useEffect } from "react"; import { LightButton, PageLayout } from "@/components/ui";
import { useTranslations } from "next-intl"; import { useVideoStore } from "./stores/videoStore";
import { toast } from "sonner"; import { useEffect, useRef } from "react";
import { PageLayout } from "@/components/ui/PageLayout"; import Link from "next/link";
import { VideoPlayerPanel } from "./components/VideoPlayerPanel"; import { HStack } from "@/design-system/layout/stack";
import { ControlPanel } from "./components/ControlPanel"; import { MessageSquareQuote, Video } from "lucide-react";
import { useVideoSync } from "./hooks/useVideoSync"; import { useFileUpload } from "./useFileUpload";
import { useSubtitleSync } from "./hooks/useSubtitleSync"; import { useSubtitleStore } from "./stores/substitleStore";
import { useSrtPlayerShortcuts } from "./hooks/useKeyboardShortcuts"; import { getCurrentIndex } from "./subtitleParser";
import { loadSubtitle } from "./utils/subtitleParser";
import { useSrtPlayerStore } from "./store";
export default function SrtPlayerPage() {
const t = useTranslations("home");
const srtT = useTranslations("srt_player");
export default function SRTPlayerPage() {
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const { setVideoRef, currentSrc, loadVideo, loaded, getCurrentTime, getDuration, play, setOnTimeUpdate } = useVideoStore();
const {
uploadVideo,
uploadSubtitle,
} = useFileUpload();
const {
sub,
setSub,
index,
setIndex
} = useSubtitleStore();
// 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(() => { useEffect(() => {
if (subtitleUrl) { setVideoRef(videoRef);
loadSubtitle(subtitleUrl) setOnTimeUpdate((time) => {
.then((subtitleData) => { setIndex(getCurrentIndex(sub, time));
setSubtitleData(subtitleData);
toast.success(srtT("subtitleLoadSuccess"));
})
.catch((error) => {
toast.error(srtT("subtitleLoadFailed") + ": " + error.message);
}); });
} return () => {
}, [srtT, subtitleUrl, setSubtitleData]); setVideoRef();
setOnTimeUpdate(() => { });
};
}, [setVideoRef, setOnTimeUpdate, sub, setIndex]);
return ( return (
<PageLayout> <PageLayout>
{/* Title */} <video ref={videoRef} width="85%" className="mx-auto"></video>
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-800 mb-2"> <div className="shadow rounded h-20 w-[85%] mx-auto flex-wrap flex items-begin justify-center">
{t("srtPlayer.name")} {
</h1> sub[index] && sub[index].text.split(" ").map((s, i) =>
<p className="text-lg text-gray-600"> <Link key={i}
{t("srtPlayer.description")} href={`/dictionary?q=${s}`}
</p> className="px-1 h-fit hover:bg-gray-200 hover:cursor-pointer">
{s}
</Link>
)}
</div> </div>
{/* Video Player */} {/* 上传区域 */}
<VideoPlayerPanel ref={videoRef} /> <div className="mx-auto mt-4 flex items-center justify-center flex-wrap gap-2 w-[85%]">
<div className="border-gray-200 border-2 flex items p-2 justify-between items-center rounded gap-8">
<div className="flex items-center flex-col">
<Video size={16} />
<span className="text-sm"></span>
</div>
<LightButton
onClick={() => uploadVideo((url) => {
loadVideo(url);
})}>{loaded ? currentSrc?.split("/").pop() : "视频未上传"}</LightButton>
</div>
<div className="border-gray-200 border-2 flex items p-2 justify-between items-center rounded gap-8">
<div className="flex items-center flex-col">
<MessageSquareQuote size={16} />
<span className="text-sm"
>{sub.length > 0 ? `字幕已上传 (${sub.length} 条)` : "字幕未上传"}</span>
</div>
<LightButton
onClick={() =>
uploadSubtitle((sub) => {
setSub(sub);
})
}></LightButton>
</div>
</div>
{
/* 控制面板 */
sub.length > 0 && loaded &&
<HStack gap={2} className="mx-auto mt-4 w-[85%]" justify={"center"} wrap>
<LightButton onClick={play}>play</LightButton>
<LightButton>previous</LightButton>
<LightButton>next</LightButton>
<LightButton>restart</LightButton>
<LightButton>1x</LightButton>
<LightButton>ap(on)</LightButton>
</HStack>
}
{/* Control Panel */}
<ControlPanel />
</PageLayout> </PageLayout>
); );
} }

View File

@@ -0,0 +1,19 @@
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<SubstitleStore>()(
devtools((set) => ({
sub: [],
index: 0,
setSub: (sub) => set({ sub, index: 0 }),
setIndex: (index) => set({ index }),
}))
);

View File

@@ -0,0 +1,106 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface VideoStore {
videoRef?: React.RefObject<HTMLVideoElement | null>;
currentSrc: string | null;
loaded: boolean;
onTimeUpdate: (time: number) => void;
setOnTimeUpdate: (handler: (time: number) => void) => void;
setVideoRef: (ref?: React.RefObject<HTMLVideoElement | null>) => 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;
}
export const useVideoStore = create<VideoStore>()(
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,
}))
);

View File

@@ -0,0 +1,89 @@
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<SubtitleEntry[]> {
return fetch(url)
.then(response => response.text())
.then(data => parseSrt(data))
.catch(error => {
console.error('加载字幕失败', error);
return [];
});
}

View File

@@ -1,132 +1,6 @@
// ==================== 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 { export interface SubtitleEntry {
start: number; start: number;
end: number; end: number;
text: string; text: string;
index: number; 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<SubtitleSettings>) => 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,
}),
};

View File

@@ -0,0 +1,65 @@
"use client";
import { loadSubtitle } from "./subtitleParser";
const createUploadHandler = <T,>(
accept: string,
validate: (file: File) => boolean,
errorMessage: string,
processFile: (file: File) => T | Promise<T>
) => {
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,
};
}