[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": 指定時刻から)
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}
/>
</label>
<button onClick={handleClearFile} className={styles.fileClear}>
<button onClick={handleClearFile} className={styles.file_clear}>
ファイルクリア
</button>
</div>
@@ -228,13 +243,13 @@ export const SubtitleSystemContainer = () => {
</select>
</label>
{playbackMode === "absolute" && (
<div className={styles.timeSection}>
<div className={styles.time_section}>
<label className={styles.label}>再生開始時刻 (HH:MM):</label>
<div className={styles.timeSelects}>
<div className={styles.time_selects}>
<select
value={targetHour}
onChange={(e) => setTargetHour(e.target.value)}
className={styles.select}
className={styles.time_selects_item}
>
{Array.from({ length: 24 }, (_, i) => {
const hour = i.toString().padStart(2, "0");
@@ -249,7 +264,7 @@ export const SubtitleSystemContainer = () => {
<select
value={targetMinute}
onChange={(e) => 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 && (
<div className={styles.countdown}>
<span>カウントダウン: {effectiveCountdown} </span>
<button
onClick={() =>
setEffectiveCountdown((prev) => prev + 1)
}
className={styles.adjustButton}
>
</button>
<button
onClick={() =>
setEffectiveCountdown((prev) => prev - 1)
}
className={styles.adjustButton}
>
</button>
<span>カウントダウン: {secToDayTime(effectiveCountdown)}</span>
<div className={styles.adjust_button_wrapper}>
{/* 1分単位の調整ボタン */}
<div>
<button
onClick={() => {
const newValue = effectiveCountdown + 60;
setCountdownAdjustment((prev) => prev + 60);
startCountdownInterval(newValue);
}}
className={styles.adjust_button}
>
1
</button>
<button
onClick={() => {
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>
)}
{/* 字幕一覧の表示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;
};

View File

@@ -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;
}