diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.jsx b/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.jsx index 9970b2cf..17e1dc82 100644 --- a/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.jsx +++ b/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.jsx @@ -11,7 +11,7 @@ export const SubtitleSystemContainer = () => { // 再生モード ("relative": ボタン押下から、"absolute": 指定時刻から) const [playbackMode, setPlaybackMode] = useState("relative"); // 絶対モード用の再生開始時刻(ドロップダウンで選択、HH:MM) - const [targetHour, setTargetHour] = useState("19"); + const [targetHour, setTargetHour] = useState("23"); const [targetMinute, setTargetMinute] = useState("00"); // カウントダウン状態 @@ -26,6 +26,8 @@ export const SubtitleSystemContainer = () => { // タイマー(setTimeout/setInterval)のID管理用 const timersRef = useRef([]); + // カウントダウンタイマー専用の ref + const countdownIntervalRef = useRef(null); // ファイル入力リセット用の ref const fileInputRef = useRef(null); @@ -79,6 +81,10 @@ export const SubtitleSystemContainer = () => { clearInterval(timerId); }); timersRef.current = []; + if (countdownIntervalRef.current) { + clearInterval(countdownIntervalRef.current); + countdownIntervalRef.current = null; + } console.log("再生を停止しました。"); setIsPlaying(false); setInitialCountdown(null); @@ -87,7 +93,7 @@ export const SubtitleSystemContainer = () => { setCuesScheduled(false); }; - // cues のスケジュールを行う(字幕開始時のオフセットは countdownAdjustment * 1000) + // cues のスケジュールを行う(字幕開始時のオフセットは調整後のタイミングに合わせる) const scheduleCues = (offset) => { cues.forEach((cue) => { const startDelay = cue.startTime * 1000 + offset; @@ -103,19 +109,24 @@ export const SubtitleSystemContainer = () => { }); }; - // カウントダウンタイマーの開始 - const startCountdownInterval = (initialValue) => { - setEffectiveCountdown(initialValue + countdownAdjustment); - const countdownInterval = setInterval(() => { + // カウントダウンタイマーの開始/再登録(指定した値から1秒ごとに減らす) + const startCountdownInterval = (startValue) => { + // 既存のタイマーがあればクリア + if (countdownIntervalRef.current) { + clearInterval(countdownIntervalRef.current); + } + // 新たな開始値を設定 + setEffectiveCountdown(startValue); + countdownIntervalRef.current = setInterval(() => { setEffectiveCountdown((prev) => { if (prev <= 1) { - clearInterval(countdownInterval); + clearInterval(countdownIntervalRef.current); return 0; } return prev - 1; }); }, 1000); - timersRef.current.push(countdownInterval); + timersRef.current.push(countdownIntervalRef.current); }; // 「再生開始」ボタン押下時の処理 @@ -146,9 +157,10 @@ export const SubtitleSystemContainer = () => { computedCountdown = 10; // relative モードの場合は固定値 } setInitialCountdown(computedCountdown); - setEffectiveCountdown(computedCountdown + countdownAdjustment); - sendTextToOverlay((computedCountdown + countdownAdjustment).toString()); - startCountdownInterval(computedCountdown); + // 調整値を反映した開始値 + const startValue = computedCountdown + countdownAdjustment; + startCountdownInterval(startValue); + sendTextToOverlay(startValue.toString()); }; // effectiveCountdown が 0 になったとき、字幕開始 @@ -161,10 +173,13 @@ export const SubtitleSystemContainer = () => { ) { sendTextToOverlay("Start."); console.log("Start."); - // オフセットは countdownAdjustment × 1000 を字幕に反映 - scheduleCues(countdownAdjustment * 1000); + // 調整後のタイミングで字幕スケジュールを開始 + scheduleCues(0); setCuesScheduled(true); } + + console.log(secToDayTime(effectiveCountdown)); + sendTextToOverlay(secToDayTime(effectiveCountdown)); }, [effectiveCountdown, isPlaying, cuesScheduled, countdownAdjustment]); // テーブル内の字幕行をクリック(relative モードのみ)でジャンプ @@ -211,7 +226,7 @@ export const SubtitleSystemContainer = () => { className={styles.input} /> - @@ -228,13 +243,13 @@ export const SubtitleSystemContainer = () => { {playbackMode === "absolute" && ( -
+
-
+
setTargetMinute(e.target.value)} - className={styles.select} + className={styles.time_selects_item} > {Array.from({ length: 60 }, (_, i) => { const minute = i.toString().padStart(2, "0"); @@ -278,23 +293,55 @@ export const SubtitleSystemContainer = () => { {/* カウントダウン表示:字幕開始前は常に表示 */} {effectiveCountdown !== null && !cuesScheduled && (
- カウントダウン: {effectiveCountdown} 秒 - - + カウントダウン: {secToDayTime(effectiveCountdown)} +
+ {/* 1分単位の調整ボタン */} +
+ + +
+ {/* 1秒単位の調整ボタン */} +
+ + +
+
)} {/* 字幕一覧の表示(relative モードの場合、クリックでジャンプ) */} @@ -408,3 +455,22 @@ const parseTime = (timeString) => { const [hours, minutes, seconds] = hms.split(":").map(Number); return hours * 3600 + minutes * 60 + seconds + Number(ms) / 1000; }; + +const secToDayTime = (seconds) => { + const day = Math.floor(seconds / 86400); + const hour = Math.floor((seconds % 86400) / 3600); + const min = Math.floor((seconds % 3600) / 60); + const sec = seconds % 60; + let time = ""; + // day が 0 の場合は「日」は出力しない(hour や min も同様) + if (day !== 0) { + time = `${day}日${hour}時間${min}分${sec}秒`; + } else if (hour !== 0) { + time = `${hour}:${min}:${sec}`; + } else if (min !== 0) { + time = `${String(min).padStart(2, "0")}:${String(sec).padStart(2, "0")}`; + } else { + time = `${String(sec).padStart(2, "0")}`; + } + return time; +}; diff --git a/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss b/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss index 515842e7..ca0bc25d 100644 --- a/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss +++ b/src-ui/app/main_page/main_section/subtitle_system_container/SubtitleSystemContainer.module.scss @@ -1,157 +1,179 @@ .container { - padding: 4rem; - background: #1e1e1e; - color: #ffffff; - border-radius: 1rem; - flex-shrink: 0; - height: 100%; - overflow: auto; - - h1 { - font-size: 2.4rem; - margin-bottom: 1.5rem; - text-align: center; - } - - label { - display: block; - font-size: 1.6rem; - margin-bottom: 0.5rem; - } - - input, - select { - font-size: 1.6rem; - padding: 0.5rem; - border-radius: 0.5rem; - border: 0.1rem solid #ccc; - background: #333; - color: #fff; - } - - input[type="file"] { - padding: 0.5rem; - } - - // ボタンの基本スタイル - button { - font-size: 1.8rem; - padding: 1rem 2rem; - border: none; - border-radius: 0.5rem; - cursor: pointer; - transition: background 0.3s; - margin-right: 1rem; - - &:focus { - outline: none; - box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); - } - } - - // 再生開始用ボタン(通常時) - .primary { - background: #007bff; - color: #fff; - &:hover { - background: #0056b3; - } - } - - // 再生停止用ボタン - .secondary { - background: #dc3545; - color: #fff; - &:hover { - background: #a71d2a; - } - } - - // ファイルクリア用ボタン - .file-clear { - background: #6c757d; - color: #fff; - &:hover { - background: #5a6268; - } - } - - // 「再生中」状態(クリック不可)用のスタイル - .is_playing { - background: #6c757d; - cursor: not-allowed; - } - - // カウントダウン表示用エリア - .countdown { - margin-top: 1rem; - font-size: 1.6rem; + padding: 4rem; + background: #1e1e1e; + color: #ffffff; + border-radius: 1rem; + flex-shrink: 0; + height: 100%; + overflow: auto; display: flex; - align-items: center; - gap: 1rem; + flex-direction: column; + gap: 2rem; - span { - font-weight: bold; + h1 { + font-size: 2.4rem; + margin-bottom: 1.5rem; + text-align: center; } + label { + display: block; + font-size: 1.6rem; + margin-bottom: 0.5rem; + } + + input, + select { + font-size: 1.6rem; + padding: 0.5rem; + border-radius: 0.5rem; + border: 0.1rem solid #ccc; + background: #333; + color: #fff; + } + + input[type="file"] { + padding: 0.5rem; + } + + // ボタンの基本スタイル button { - font-size: 1.6rem; - padding: 0.5rem 1rem; - border: none; - border-radius: 0.4rem; - background: #28a745; - color: #fff; - cursor: pointer; - transition: background 0.3s; + font-size: 1.8rem; + padding: 1rem 2rem; + border: none; + border-radius: 0.5rem; + cursor: pointer; + transition: background 0.3s; + margin-right: 1rem; - &:hover { - background: #218838; - } + &:focus { + outline: none; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); + } } - } - // 再生モードや時刻指定の select 関連(横並びの場合などに調整) - .time-selects { - display: flex; - align-items: center; - gap: 0.5rem; - } + // 再生開始用ボタン(通常時) + .primary { + background: #007bff; + color: #fff; + &:hover { + background: #0056b3; + } + } - // 字幕一覧のテーブル - table { - width: 100%; - border-collapse: collapse; - margin-top: 2rem; + // 再生停止用ボタン + .secondary { + background: #dc3545; + color: #fff; + &:hover { + background: #a71d2a; + } + } + + // ファイルクリア用ボタン + .file_clear { + background: #6c757d; + color: #fff; + &:hover { + background: #5a6268; + } + } + + // 「再生中」状態(クリック不可)用のスタイル + .is_playing { + background: #6c757d; + cursor: not-allowed; + pointer-events: none; + } + + // カウントダウン表示用エリア + .countdown { + margin-top: 1rem; + font-size: 2.6rem; + display: flex; + align-items: center; + gap: 1rem; + flex-direction: column; + + span { + font-weight: bold; + } + + button { + padding: 0.5rem 1rem; + font-size: 1.6rem; + border: none; + border-radius: 0.4rem; + background: #28a745; + color: #fff; + cursor: pointer; + transition: background 0.3s; + + &:hover { + background: #218838; + } + } + } + + // 再生モードや時刻指定の select 関連(横並びの場合などに調整) + .time_selects { + display: flex; + align-items: center; + gap: 0.5rem; + } + + // 字幕一覧のテーブル + table { + width: 100%; + border-collapse: collapse; + margin-top: 2rem; th, td { - padding: 1rem; - border: 0.1rem solid #444; - text-align: left; - font-size: 1.4rem; + padding: 1rem; + border: 0.1rem solid #444; + text-align: left; + font-size: 1.4rem; } th { - background: #555; + background: #555; } tbody { - tr { + tr { cursor: pointer; transition: background 0.2s; &:nth-child(even) { - background: #2a2a2a; + background: #2a2a2a; } &:hover { - background: #444; + background: #444; + } } - } } - } + } - .subtitle_lists { - font-size: 1.4rem; - } } +.subtitle_lists { + font-size: 1.4rem; +} + +.time_section { + display: flex; + gap: 2rem; + justify-content: center; + align-items: center; +} +.time_selects_item { + gap: 0.4rem; + width: 6rem; + text-align: center; +} + +.adjust_button_wrapper { + display: flex; + gap: 2rem; +} \ No newline at end of file