[Update/TMP] Subtitle system v1.0(It works)
This commit is contained in:
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user