[Update/TMP] Subtitle system v1.0(It works)

This commit is contained in:
Sakamoto Shiina
2025-02-14 22:09:56 +09:00
parent eb81fae2a0
commit 8360a7e955
2 changed files with 250 additions and 162 deletions

View File

@@ -11,7 +11,7 @@ export const SubtitleSystemContainer = () => {
// 再生モード ("relative": ボタン押下から、"absolute": 指定時刻から) // 再生モード ("relative": ボタン押下から、"absolute": 指定時刻から)
const [playbackMode, setPlaybackMode] = useState("relative"); const [playbackMode, setPlaybackMode] = useState("relative");
// 絶対モード用の再生開始時刻ドロップダウンで選択、HH:MM // 絶対モード用の再生開始時刻ドロップダウンで選択、HH:MM
const [targetHour, setTargetHour] = useState("19"); const [targetHour, setTargetHour] = useState("23");
const [targetMinute, setTargetMinute] = useState("00"); const [targetMinute, setTargetMinute] = useState("00");
// カウントダウン状態 // カウントダウン状態
@@ -26,6 +26,8 @@ export const SubtitleSystemContainer = () => {
// タイマーsetTimeout/setIntervalのID管理用 // タイマーsetTimeout/setIntervalのID管理用
const timersRef = useRef([]); const timersRef = useRef([]);
// カウントダウンタイマー専用の ref
const countdownIntervalRef = useRef(null);
// ファイル入力リセット用の ref // ファイル入力リセット用の ref
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
@@ -79,6 +81,10 @@ export const SubtitleSystemContainer = () => {
clearInterval(timerId); clearInterval(timerId);
}); });
timersRef.current = []; timersRef.current = [];
if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current);
countdownIntervalRef.current = null;
}
console.log("再生を停止しました。"); console.log("再生を停止しました。");
setIsPlaying(false); setIsPlaying(false);
setInitialCountdown(null); setInitialCountdown(null);
@@ -87,7 +93,7 @@ export const SubtitleSystemContainer = () => {
setCuesScheduled(false); setCuesScheduled(false);
}; };
// cues のスケジュールを行う(字幕開始時のオフセットは countdownAdjustment * 1000 // cues のスケジュールを行う(字幕開始時のオフセットは調整後のタイミングに合わせる
const scheduleCues = (offset) => { const scheduleCues = (offset) => {
cues.forEach((cue) => { cues.forEach((cue) => {
const startDelay = cue.startTime * 1000 + offset; const startDelay = cue.startTime * 1000 + offset;
@@ -103,19 +109,24 @@ export const SubtitleSystemContainer = () => {
}); });
}; };
// カウントダウンタイマーの開始 // カウントダウンタイマーの開始再登録指定した値から1秒ごとに減らす
const startCountdownInterval = (initialValue) => { const startCountdownInterval = (startValue) => {
setEffectiveCountdown(initialValue + countdownAdjustment); // 既存のタイマーがあればクリア
const countdownInterval = setInterval(() => { if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current);
}
// 新たな開始値を設定
setEffectiveCountdown(startValue);
countdownIntervalRef.current = setInterval(() => {
setEffectiveCountdown((prev) => { setEffectiveCountdown((prev) => {
if (prev <= 1) { if (prev <= 1) {
clearInterval(countdownInterval); clearInterval(countdownIntervalRef.current);
return 0; return 0;
} }
return prev - 1; return prev - 1;
}); });
}, 1000); }, 1000);
timersRef.current.push(countdownInterval); timersRef.current.push(countdownIntervalRef.current);
}; };
// 「再生開始」ボタン押下時の処理 // 「再生開始」ボタン押下時の処理
@@ -146,9 +157,10 @@ export const SubtitleSystemContainer = () => {
computedCountdown = 10; // relative モードの場合は固定値 computedCountdown = 10; // relative モードの場合は固定値
} }
setInitialCountdown(computedCountdown); setInitialCountdown(computedCountdown);
setEffectiveCountdown(computedCountdown + countdownAdjustment); // 調整値を反映した開始値
sendTextToOverlay((computedCountdown + countdownAdjustment).toString()); const startValue = computedCountdown + countdownAdjustment;
startCountdownInterval(computedCountdown); startCountdownInterval(startValue);
sendTextToOverlay(startValue.toString());
}; };
// effectiveCountdown が 0 になったとき、字幕開始 // effectiveCountdown が 0 になったとき、字幕開始
@@ -161,10 +173,13 @@ export const SubtitleSystemContainer = () => {
) { ) {
sendTextToOverlay("Start."); sendTextToOverlay("Start.");
console.log("Start."); console.log("Start.");
// オフセットは countdownAdjustment × 1000 を字幕に反映 // 調整後のタイミングで字幕スケジュールを開始
scheduleCues(countdownAdjustment * 1000); scheduleCues(0);
setCuesScheduled(true); setCuesScheduled(true);
} }
console.log(secToDayTime(effectiveCountdown));
sendTextToOverlay(secToDayTime(effectiveCountdown));
}, [effectiveCountdown, isPlaying, cuesScheduled, countdownAdjustment]); }, [effectiveCountdown, isPlaying, cuesScheduled, countdownAdjustment]);
// テーブル内の字幕行をクリックrelative モードのみ)でジャンプ // テーブル内の字幕行をクリックrelative モードのみ)でジャンプ
@@ -211,7 +226,7 @@ export const SubtitleSystemContainer = () => {
className={styles.input} className={styles.input}
/> />
</label> </label>
<button onClick={handleClearFile} className={styles.fileClear}> <button onClick={handleClearFile} className={styles.file_clear}>
ファイルクリア ファイルクリア
</button> </button>
</div> </div>
@@ -228,13 +243,13 @@ export const SubtitleSystemContainer = () => {
</select> </select>
</label> </label>
{playbackMode === "absolute" && ( {playbackMode === "absolute" && (
<div className={styles.timeSection}> <div className={styles.time_section}>
<label className={styles.label}>再生開始時刻 (HH:MM):</label> <label className={styles.label}>再生開始時刻 (HH:MM):</label>
<div className={styles.timeSelects}> <div className={styles.time_selects}>
<select <select
value={targetHour} value={targetHour}
onChange={(e) => setTargetHour(e.target.value)} onChange={(e) => setTargetHour(e.target.value)}
className={styles.select} className={styles.time_selects_item}
> >
{Array.from({ length: 24 }, (_, i) => { {Array.from({ length: 24 }, (_, i) => {
const hour = i.toString().padStart(2, "0"); const hour = i.toString().padStart(2, "0");
@@ -249,7 +264,7 @@ export const SubtitleSystemContainer = () => {
<select <select
value={targetMinute} value={targetMinute}
onChange={(e) => setTargetMinute(e.target.value)} onChange={(e) => setTargetMinute(e.target.value)}
className={styles.select} className={styles.time_selects_item}
> >
{Array.from({ length: 60 }, (_, i) => { {Array.from({ length: 60 }, (_, i) => {
const minute = i.toString().padStart(2, "0"); const minute = i.toString().padStart(2, "0");
@@ -278,23 +293,55 @@ export const SubtitleSystemContainer = () => {
{/* カウントダウン表示:字幕開始前は常に表示 */} {/* カウントダウン表示:字幕開始前は常に表示 */}
{effectiveCountdown !== null && !cuesScheduled && ( {effectiveCountdown !== null && !cuesScheduled && (
<div className={styles.countdown}> <div className={styles.countdown}>
<span>カウントダウン: {effectiveCountdown} </span> <span>カウントダウン: {secToDayTime(effectiveCountdown)}</span>
<button <div className={styles.adjust_button_wrapper}>
onClick={() => {/* 1分単位の調整ボタン */}
setEffectiveCountdown((prev) => prev + 1) <div>
} <button
className={styles.adjustButton} onClick={() => {
> const newValue = effectiveCountdown + 60;
setCountdownAdjustment((prev) => prev + 60);
</button> startCountdownInterval(newValue);
<button }}
onClick={() => className={styles.adjust_button}
setEffectiveCountdown((prev) => prev - 1) >
} 1
className={styles.adjustButton} </button>
> <button
onClick={() => {
</button> const newValue = effectiveCountdown - 60;
setCountdownAdjustment((prev) => prev - 60);
startCountdownInterval(newValue);
}}
className={styles.adjust_button}
>
1
</button>
</div>
{/* 1秒単位の調整ボタン */}
<div>
<button
onClick={() => {
const newValue = effectiveCountdown + 1;
setCountdownAdjustment((prev) => prev + 1);
startCountdownInterval(newValue);
}}
className={styles.adjust_button}
>
1
</button>
<button
onClick={() => {
const newValue = effectiveCountdown - 1;
setCountdownAdjustment((prev) => prev - 1);
startCountdownInterval(newValue);
}}
className={styles.adjust_button}
>
1
</button>
</div>
</div>
</div> </div>
)} )}
{/* 字幕一覧の表示relative モードの場合、クリックでジャンプ) */} {/* 字幕一覧の表示relative モードの場合、クリックでジャンプ) */}
@@ -408,3 +455,22 @@ const parseTime = (timeString) => {
const [hours, minutes, seconds] = hms.split(":").map(Number); const [hours, minutes, seconds] = hms.split(":").map(Number);
return hours * 3600 + minutes * 60 + seconds + Number(ms) / 1000; 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;
};

View File

@@ -1,157 +1,179 @@
.container { .container {
padding: 4rem; padding: 4rem;
background: #1e1e1e; background: #1e1e1e;
color: #ffffff; color: #ffffff;
border-radius: 1rem; border-radius: 1rem;
flex-shrink: 0; flex-shrink: 0;
height: 100%; height: 100%;
overflow: auto; 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;
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 1rem; gap: 2rem;
span { h1 {
font-weight: bold; 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 { button {
font-size: 1.6rem; font-size: 1.8rem;
padding: 0.5rem 1rem; padding: 1rem 2rem;
border: none; border: none;
border-radius: 0.4rem; border-radius: 0.5rem;
background: #28a745; cursor: pointer;
color: #fff; transition: background 0.3s;
cursor: pointer; margin-right: 1rem;
transition: background 0.3s;
&:hover { &:focus {
background: #218838; outline: none;
} box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
} }
}
// 再生モードや時刻指定の select 関連(横並びの場合などに調整 // 再生開始用ボタン(通常時
.time-selects { .primary {
display: flex; background: #007bff;
align-items: center; color: #fff;
gap: 0.5rem; &:hover {
} background: #0056b3;
}
}
// 字幕一覧のテーブル // 再生停止用ボタン
table { .secondary {
width: 100%; background: #dc3545;
border-collapse: collapse; color: #fff;
margin-top: 2rem; &: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, th,
td { td {
padding: 1rem; padding: 1rem;
border: 0.1rem solid #444; border: 0.1rem solid #444;
text-align: left; text-align: left;
font-size: 1.4rem; font-size: 1.4rem;
} }
th { th {
background: #555; background: #555;
} }
tbody { tbody {
tr { tr {
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
&:nth-child(even) { &:nth-child(even) {
background: #2a2a2a; background: #2a2a2a;
} }
&:hover { &: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;
}