import React, { useState, useRef, useEffect } from "react"; import styles from "./PluginsSection.module.scss"; import { useSendTextToOverlay } from "@logics_configs"; export const PluginsSection = () => { const { sendTextToOverlay } = useSendTextToOverlay(); const [srtContent, setSrtContent] = useState(""); const [cues, setCues] = useState([]); const [isPlaying, setIsPlaying] = useState(false); // 再生モード ("relative": ボタン押下から、"absolute": 指定時刻から) const [playbackMode, setPlaybackMode] = useState("relative"); // 絶対モード用の再生開始時刻(ドロップダウンで選択、HH:MM) const [targetHour, setTargetHour] = useState("19"); const [targetMinute, setTargetMinute] = useState("00"); // カウントダウン状態 // initialCountdown: 再生開始ボタン押下時に算出される元の残り秒数 const [initialCountdown, setInitialCountdown] = useState(null); // countdownAdjustment: ユーザーが上下ボタンで調整する値(秒単位) const [countdownAdjustment, setCountdownAdjustment] = useState(0); // effectiveCountdown: (initialCountdown + countdownAdjustment) から経過秒数を差し引いた表示用の値 const [effectiveCountdown, setEffectiveCountdown] = useState(null); // cuesScheduled: 字幕タイマーが一度スケジュールされたか const [cuesScheduled, setCuesScheduled] = useState(false); // タイマー(setTimeout/setInterval)のID管理用 const timersRef = useRef([]); // ファイル入力リセット用の ref const fileInputRef = useRef(null); // ファイルアップロード時の処理 const handleFileUpload = (event) => { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const content = e.target.result; setSrtContent(content); let parsedCues = []; // 拡張子により ASS と SRT を判定 if (file.name.toLowerCase().endsWith(".ass")) { parsedCues = parseASS(content); } else { parsedCues = parseSRT(content); } setCues(parsedCues); console.log("Parsed cues:", parsedCues); if (fileInputRef.current) { fileInputRef.current.value = ""; } }; reader.readAsText(file); }; // 字幕開始時の処理 const startFunction = (cue) => { let send_text = ""; if (cue.actor !== "") { send_text = `[${cue.actor}] ${cue.text}`; } else { send_text = `${cue.text}`; } console.log(`字幕開始 (index: ${cue.index}) send_text:${send_text}`); sendTextToOverlay(send_text); }; // 字幕終了時の処理 const endFunction = (cue) => { console.log(`字幕終了 (index: ${cue.index}): ${cue.text}`); // 必要に応じた終了処理(例:テキストクリア)を実装可能 // sendTextToOverlay(""); }; // すべてのタイマーを停止し、各状態を初期化する const handleStop = () => { timersRef.current.forEach((timerId) => { clearTimeout(timerId); clearInterval(timerId); }); timersRef.current = []; console.log("再生を停止しました。"); setIsPlaying(false); setInitialCountdown(null); setEffectiveCountdown(null); setCountdownAdjustment(0); setCuesScheduled(false); }; // cues のスケジュールを行う(字幕開始時のオフセットは countdownAdjustment * 1000) const scheduleCues = (offset) => { cues.forEach((cue) => { const startDelay = cue.startTime * 1000 + offset; const endDelay = cue.endTime * 1000 + offset; if (startDelay >= 0) { const timerId = setTimeout(() => startFunction(cue), startDelay); timersRef.current.push(timerId); } if (endDelay >= 0) { const timerId = setTimeout(() => endFunction(cue), endDelay); timersRef.current.push(timerId); } }); }; // カウントダウンタイマーの開始 const startCountdownInterval = (initialValue) => { setEffectiveCountdown(initialValue + countdownAdjustment); const countdownInterval = setInterval(() => { setEffectiveCountdown((prev) => { if (prev <= 1) { clearInterval(countdownInterval); return 0; } return prev - 1; }); }, 1000); timersRef.current.push(countdownInterval); }; // 「再生開始」ボタン押下時の処理 const handleStart = () => { handleStop(); setIsPlaying(true); setCuesScheduled(false); let computedCountdown = 0; if (playbackMode === "absolute") { const now = new Date(); const hour = parseInt(targetHour, 10); const minute = parseInt(targetMinute, 10); let targetDate = new Date( now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, 0, 0 ); if (targetDate.getTime() < now.getTime()) { targetDate.setDate(targetDate.getDate() + 1); } computedCountdown = Math.ceil((targetDate.getTime() - now.getTime()) / 1000); } else { computedCountdown = 10; // relative モードの場合は固定値 } setInitialCountdown(computedCountdown); setEffectiveCountdown(computedCountdown + countdownAdjustment); sendTextToOverlay((computedCountdown + countdownAdjustment).toString()); startCountdownInterval(computedCountdown); }; // effectiveCountdown が 0 になったとき、字幕開始 useEffect(() => { if ( isPlaying && effectiveCountdown !== null && effectiveCountdown <= 0 && !cuesScheduled ) { sendTextToOverlay("Start."); console.log("Start."); // オフセットは countdownAdjustment × 1000 を字幕に反映 scheduleCues(countdownAdjustment * 1000); setCuesScheduled(true); } }, [effectiveCountdown, isPlaying, cuesScheduled, countdownAdjustment]); // テーブル内の字幕行をクリック(relative モードのみ)でジャンプ const handleJump = (jumpCue) => { if (playbackMode !== "relative") return; handleStop(); const offset = -jumpCue.startTime * 1000; scheduleCues(offset); setIsPlaying(true); }; // HH:MM:SS 形式に変換する補助関数 const formatTime = (timeInSeconds) => { const hours = Math.floor(timeInSeconds / 3600); const minutes = Math.floor((timeInSeconds % 3600) / 60); const seconds = Math.floor(timeInSeconds % 60); return ( String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + String(seconds).padStart(2, "0") ); }; // ファイルクリア const handleClearFile = () => { handleStop(); setSrtContent(""); setCues([]); }; return (
| 番号 | 開始 | 終了 | Actor | テキスト |
|---|---|---|---|---|
| {cue.index} | {formatTime(cue.startTime)} | {formatTime(cue.endTime)} | {cue.actor} | {cue.text} |
※ 行をクリックすると、その字幕の位置にジャンプします。(相対モードのみ)