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