[Update] Plugins(VRCT Subtitles as testing one): Provide-able store and functions from main app.
This commit is contained in:
@@ -23,8 +23,6 @@ import { SnackbarController } from "./snackbar_controller/SnackbarController";
|
||||
import styles from "./App.module.scss";
|
||||
import { useIsBackendReady, useIsSoftwareUpdating, useIsVrctAvailable, useWindow } from "@logics_common";
|
||||
|
||||
import { SubtitlesController } from "./main_page/main_section/subtitle_system_container/_controllers/subtitlesController.jsx";
|
||||
|
||||
export const App = () => {
|
||||
const { currentIsVrctAvailable } = useIsVrctAvailable();
|
||||
const { currentIsBackendReady } = useIsBackendReady();
|
||||
@@ -59,7 +57,6 @@ const Contents = ({ fetchPluginsHasRunRef }) => {
|
||||
return (
|
||||
<>
|
||||
<PluginsController fetchPluginsHasRunRef={fetchPluginsHasRunRef} />
|
||||
<SubtitlesController />
|
||||
|
||||
<WindowTitleBar />
|
||||
{currentIsSoftwareUpdating.data === false
|
||||
|
||||
@@ -8,7 +8,6 @@ import { LanguageSelector } from "./language_selector/LanguageSelector";
|
||||
import { useStore_IsOpenedLanguageSelector } from "@store";
|
||||
import { useLanguageSettings } from "@logics_main";
|
||||
import { useEffect } from "react";
|
||||
import { SubtitleSystemContainer } from "./subtitle_system_container/SubtitleSystemContainer";
|
||||
|
||||
import { PluginHost } from "./PluginHost";
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import styles from "./SubtitleSystemContainer.module.scss";
|
||||
import { InputFileContainer } from "./input_file_container/InputFileContainer";
|
||||
import { ModeSelectorContainer } from "./mode_selector_container/ModeSelectorContainer";
|
||||
import { PlayControlContainer } from "./play_control_container/PlayControlContainer";
|
||||
import { CountdownContainer } from "./countdown_container/CountdownContainer";
|
||||
import { SubtitlesListContainer } from "./subtitles_list_container/SubtitlesListContainer";
|
||||
|
||||
export const SubtitleSystemContainer = () => {
|
||||
// const [srtContent, setSrtContent] = useState("");
|
||||
// const [cues, setCues] = useState([]);
|
||||
// const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
// 再生モード ("relative": ボタン押下から、"absolute": 指定時刻から)
|
||||
// const [playbackMode, setPlaybackMode] = useState("relative");
|
||||
// 絶対モード用の再生開始時刻(ドロップダウンで選択、HH:MM)
|
||||
// const [targetHour, setTargetHour] = useState("23");
|
||||
// const [targetMinute, setTargetMinute] = useState("00");
|
||||
|
||||
// カウントダウン状態
|
||||
// // initialCountdown: 再生開始ボタン押下時に算出される元の残り秒数
|
||||
// const [initialCountdown, setInitialCountdown] = useState(null);
|
||||
// countdownAdjustment: ユーザーが上下ボタンで調整する値(秒単位)
|
||||
// const [countdownAdjustment, setCountdownAdjustment] = useState(0);
|
||||
// effectiveCountdown: (initialCountdown + countdownAdjustment) から経過秒数を差し引いた表示用の値
|
||||
// const [effectiveCountdown, setEffectiveCountdown] = useState(null);
|
||||
// cuesScheduled: 字幕タイマーが一度スケジュールされたか
|
||||
// const [cuesScheduled, setCuesScheduled] = useState(false);
|
||||
|
||||
// // タイマー(setTimeout/setInterval)のID管理用
|
||||
// const timersRef = useRef([]);
|
||||
// // カウントダウンタイマー専用の ref
|
||||
// const countdownIntervalRef = useRef(null);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.title}>字幕プレイヤー</h1>
|
||||
<InputFileContainer />
|
||||
<ModeSelectorContainer />
|
||||
<PlayControlContainer />
|
||||
<div className={styles.border}></div>
|
||||
<CountdownContainer />
|
||||
<SubtitlesListContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
.container {
|
||||
padding: 2rem 4rem;
|
||||
background: var(--dark_900_color);
|
||||
border-radius: 1rem;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.8rem;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.border {
|
||||
width: 100%;
|
||||
height: 0.2rem;
|
||||
background-color: var(--dark_800_color);
|
||||
flex-shrink: 0;
|
||||
|
||||
}
|
||||
|
||||
// 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;
|
||||
// }
|
||||
|
||||
|
||||
// ボタンの基本スタイル
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 「再生中」状態(クリック不可)用のスタイル
|
||||
.is_playing {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
// 字幕一覧のテーブル
|
||||
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;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle_lists {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useSendTextToOverlay } from "@logics_configs";
|
||||
import { useSubtitles } from "../_logics/useSubtitles";
|
||||
import { secToDayTime } from "../_subtitles_utils"
|
||||
import { useEffect } from "react";
|
||||
export const SubtitlesController = () => {
|
||||
const { sendTextToOverlay } = useSendTextToOverlay();
|
||||
const {
|
||||
currentIsSubtitlePlaying,
|
||||
currentIsCuesScheduled,
|
||||
updateIsCuesScheduled,
|
||||
currentCountdownAdjustment,
|
||||
currentEffectiveCountdown,
|
||||
scheduleCues,
|
||||
} = useSubtitles();
|
||||
|
||||
// currentEffectiveCountdown.data が 0 になったとき、字幕開始
|
||||
useEffect(() => {
|
||||
if (
|
||||
currentIsSubtitlePlaying.data &&
|
||||
currentEffectiveCountdown.data !== null &&
|
||||
currentEffectiveCountdown.data <= 0 &&
|
||||
!currentIsCuesScheduled.data
|
||||
) {
|
||||
sendTextToOverlay("スタート!");
|
||||
console.log("スタート!");
|
||||
// 調整後のタイミングで字幕スケジュールを開始
|
||||
scheduleCues(0);
|
||||
updateIsCuesScheduled(true);
|
||||
}
|
||||
|
||||
if (currentEffectiveCountdown.data > 0) {
|
||||
console.log(secToDayTime(currentEffectiveCountdown.data));
|
||||
sendTextToOverlay(secToDayTime(currentEffectiveCountdown.data));
|
||||
}
|
||||
|
||||
}, [currentEffectiveCountdown.data, currentIsSubtitlePlaying.data, currentIsCuesScheduled.data, currentCountdownAdjustment.data]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,193 +0,0 @@
|
||||
import { useSendTextToOverlay } from "@logics_configs";
|
||||
import {
|
||||
useStore_SubtitleFileName,
|
||||
useStore_IsSubtitlePlaying,
|
||||
useStore_SubtitlePlaybackMode,
|
||||
useStore_SubtitleAbsoluteTargetTime,
|
||||
useStore_IsCuesScheduled,
|
||||
useStore_CountdownAdjustment,
|
||||
useStore_EffectiveCountdown,
|
||||
useStore_SubtitleCues,
|
||||
|
||||
useStore_SubtitleTimers,
|
||||
useStore_SubtitleCountdownTimerId,
|
||||
} from "@store";
|
||||
|
||||
export const useSubtitles = () => {
|
||||
const { sendTextToOverlay } = useSendTextToOverlay();
|
||||
const { currentSubtitleFileName, updateSubtitleFileName } = useStore_SubtitleFileName();
|
||||
const { currentIsSubtitlePlaying, updateIsSubtitlePlaying } = useStore_IsSubtitlePlaying();
|
||||
const { currentSubtitlePlaybackMode, updateSubtitlePlaybackMode } = useStore_SubtitlePlaybackMode();
|
||||
const { currentSubtitleAbsoluteTargetTime, updateSubtitleAbsoluteTargetTime } = useStore_SubtitleAbsoluteTargetTime();
|
||||
const { currentIsCuesScheduled, updateIsCuesScheduled } = useStore_IsCuesScheduled();
|
||||
|
||||
const { currentCountdownAdjustment, updateCountdownAdjustment } = useStore_CountdownAdjustment();
|
||||
const { currentEffectiveCountdown, updateEffectiveCountdown } = useStore_EffectiveCountdown();
|
||||
const { currentSubtitleCues, updateSubtitleCues } = useStore_SubtitleCues();
|
||||
|
||||
// タイマー(setTimeout/setInterval)のID管理用
|
||||
const { currentSubtitleTimers, updateSubtitleTimers, addSubtitleTimers } = useStore_SubtitleTimers();
|
||||
// const timersRef = useRef([]);
|
||||
// カウントダウンタイマー専用の ref
|
||||
const { currentSubtitleCountdownTimerId, updateSubtitleCountdownTimerId, AddSubtitleCountdownTimerId } = useStore_SubtitleCountdownTimerId();
|
||||
|
||||
// cues のスケジュールを行う(字幕開始時のオフセットは調整後のタイミングに合わせる)
|
||||
const scheduleCues = (offset) => {
|
||||
// 字幕開始時の処理
|
||||
const startFunction = (cue) => {
|
||||
let send_text = "";
|
||||
if (cue.actor !== "") {
|
||||
send_text = `[${cue.actor}] ${cue.text}`;
|
||||
} else {
|
||||
send_text = `${cue.text}`;
|
||||
}
|
||||
console.log(`字幕開始 (index: ${cue.index}) send_text:${send_text}`);
|
||||
sendTextToOverlay(send_text);
|
||||
};
|
||||
|
||||
// 字幕終了時の処理
|
||||
const endFunction = (cue) => {
|
||||
console.log(`字幕終了 (index: ${cue.index}): ${cue.text}`);
|
||||
// 必要に応じた終了処理(例:テキストクリア)を実装可能
|
||||
// sendTextToOverlay("");
|
||||
};
|
||||
|
||||
currentSubtitleCues.data.forEach((cue) => {
|
||||
const startDelay = cue.startTime * 1000 + offset;
|
||||
const endDelay = cue.endTime * 1000 + offset;
|
||||
if (startDelay >= 0) {
|
||||
const timerId = setTimeout(() => startFunction(cue), startDelay);
|
||||
addSubtitleTimers(timerId);
|
||||
}
|
||||
if (endDelay >= 0) {
|
||||
const timerId = setTimeout(() => endFunction(cue), endDelay);
|
||||
addSubtitleTimers(timerId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// カウントダウンタイマーの開始/再登録(指定した値から1秒ごとに減らす)
|
||||
const startCountdownInterval = (startValue) => {
|
||||
// 既存のタイマーがあればクリア
|
||||
if (currentSubtitleCountdownTimerId.data) {
|
||||
clearInterval(currentSubtitleCountdownTimerId.data);
|
||||
}
|
||||
// 新たな開始値を設定
|
||||
updateEffectiveCountdown(startValue);
|
||||
const countdown_timer_id = setInterval(() => {
|
||||
updateEffectiveCountdown((prev) => {
|
||||
if (prev.data <= 1) {
|
||||
clearInterval(currentSubtitleCountdownTimerId.data);
|
||||
return 0;
|
||||
}
|
||||
return prev.data - 1;
|
||||
});
|
||||
}, 1000);
|
||||
updateSubtitleCountdownTimerId(countdown_timer_id);
|
||||
addSubtitleTimers(currentSubtitleCountdownTimerId.data);
|
||||
};
|
||||
|
||||
|
||||
// 字幕一覧の表示(relative モードの場合、クリックでジャンプ)
|
||||
// テーブル内の字幕行をクリック(relative モードのみ)でジャンプ
|
||||
const handleJump = (jumpCue) => {
|
||||
if (currentSubtitlePlaybackMode.data !== "relative") return;
|
||||
handleSubtitlesStop();
|
||||
const offset = -jumpCue.startTime * 1000;
|
||||
scheduleCues(offset);
|
||||
updateIsSubtitlePlaying(true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 「再生開始」ボタン押下時の処理
|
||||
const handleSubtitlesStart = () => {
|
||||
handleSubtitlesStop();
|
||||
updateIsSubtitlePlaying(true);
|
||||
updateIsCuesScheduled(false);
|
||||
const target_time = currentSubtitleAbsoluteTargetTime.data;
|
||||
|
||||
let computedCountdown = 0;
|
||||
if (currentSubtitlePlaybackMode.data === "absolute") {
|
||||
const now = new Date();
|
||||
const hour = parseInt(target_time.hour, 10);
|
||||
const minute = parseInt(target_time.minute, 10);
|
||||
let targetDate = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
hour,
|
||||
minute,
|
||||
0,
|
||||
0
|
||||
);
|
||||
if (targetDate.getTime() < now.getTime()) {
|
||||
targetDate.setDate(targetDate.getDate() + 1);
|
||||
}
|
||||
computedCountdown = Math.ceil((targetDate.getTime() - now.getTime()) / 1000);
|
||||
} else {
|
||||
computedCountdown = 10; // relative モードの場合は固定値
|
||||
}
|
||||
// setInitialCountdown(computedCountdown);
|
||||
// 調整値を反映した開始値
|
||||
const startValue = computedCountdown + currentCountdownAdjustment.data;
|
||||
startCountdownInterval(startValue);
|
||||
sendTextToOverlay(startValue.toString());
|
||||
};
|
||||
|
||||
|
||||
// すべてのタイマーを停止し、各状態を初期化する
|
||||
const handleSubtitlesStop = () => {
|
||||
currentSubtitleTimers.data.forEach((timerId) => {
|
||||
clearTimeout(timerId);
|
||||
clearInterval(timerId);
|
||||
});
|
||||
|
||||
updateSubtitleTimers([]);
|
||||
if (currentSubtitleCountdownTimerId.data) {
|
||||
clearInterval(currentSubtitleCountdownTimerId.data);
|
||||
updateSubtitleCountdownTimerId(null);
|
||||
}
|
||||
console.log("再生を停止しました。");
|
||||
updateIsSubtitlePlaying(false);
|
||||
// setInitialCountdown(null);
|
||||
updateEffectiveCountdown(null);
|
||||
updateCountdownAdjustment(0);
|
||||
updateIsCuesScheduled(false);
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
currentSubtitleFileName,
|
||||
updateSubtitleFileName,
|
||||
|
||||
currentIsSubtitlePlaying,
|
||||
updateIsSubtitlePlaying,
|
||||
|
||||
currentSubtitlePlaybackMode,
|
||||
updateSubtitlePlaybackMode,
|
||||
|
||||
currentSubtitleAbsoluteTargetTime,
|
||||
updateSubtitleAbsoluteTargetTime,
|
||||
|
||||
currentIsCuesScheduled,
|
||||
updateIsCuesScheduled,
|
||||
|
||||
currentCountdownAdjustment,
|
||||
updateCountdownAdjustment,
|
||||
|
||||
currentEffectiveCountdown,
|
||||
updateEffectiveCountdown,
|
||||
|
||||
currentSubtitleCues,
|
||||
updateSubtitleCues,
|
||||
|
||||
handleSubtitlesStart,
|
||||
handleSubtitlesStop,
|
||||
startCountdownInterval,
|
||||
scheduleCues,
|
||||
handleJump,
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
|
||||
/**
|
||||
* SRT形式の文字列を解析する関数
|
||||
* 改行コードを正規化し、空行で分割して解析する
|
||||
* (actor は存在しないため、空文字列をセット)
|
||||
*/
|
||||
export const parseSRT = (data) => {
|
||||
const cues = [];
|
||||
const normalizedData = data.replace(/\r\n/g, "\n").trim();
|
||||
const blocks = normalizedData.split(/\n\s*\n/);
|
||||
blocks.forEach((block) => {
|
||||
const lines = block.split("\n").filter((line) => line.trim() !== "");
|
||||
if (lines.length >= 3) {
|
||||
const index = parseInt(lines[0], 10);
|
||||
const timeMatch = lines[1].match(/([\d:,]+)\s+-->\s+([\d:,]+)/);
|
||||
if (!timeMatch) return;
|
||||
const start = parseTime(timeMatch[1]);
|
||||
const end = parseTime(timeMatch[2]);
|
||||
const text = lines.slice(2).join("\n");
|
||||
cues.push({ index, startTime: start, endTime: end, actor: "", text });
|
||||
}
|
||||
});
|
||||
return cues;
|
||||
};
|
||||
|
||||
/**
|
||||
* ASS形式の文字列を解析する関数
|
||||
* [Events] セクション内の "Dialogue:" 行から、
|
||||
* フォーマット "Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"
|
||||
* に沿って分割する。
|
||||
* ここでは Name を actor、Text を text として抽出する。
|
||||
*/
|
||||
export const parseASS = (data) => {
|
||||
const cues = [];
|
||||
const lines = data.split(/\r?\n/);
|
||||
let index = 1;
|
||||
lines.forEach((line) => {
|
||||
if (line.startsWith("Dialogue:")) {
|
||||
const dialogueLine = line.substring("Dialogue:".length).trim();
|
||||
const parts = dialogueLine.split(",");
|
||||
// parts[0]: Layer, parts[1]: Start, parts[2]: End, parts[3]: Style, parts[4]: Name, parts[5]: MarginL, parts[6]: MarginR, parts[7]: MarginV, parts[8]: Effect, parts[9]~: Text
|
||||
if (parts.length < 10) return;
|
||||
const startTime = parseASSTime(parts[1].trim());
|
||||
const endTime = parseASSTime(parts[2].trim());
|
||||
const actor = parts[4].trim();
|
||||
const text = parts.slice(9).join(",").trim();
|
||||
cues.push({ index: index++, startTime, endTime, actor, text });
|
||||
}
|
||||
});
|
||||
return cues;
|
||||
};
|
||||
|
||||
/**
|
||||
* "H:MM:SS.cc" 形式の ASS 時刻文字列を秒数に変換する関数
|
||||
* 例: "0:00:10.52" → 10.52 秒
|
||||
*/
|
||||
export const parseASSTime = (timeString) => {
|
||||
const parts = timeString.split(":");
|
||||
if (parts.length !== 3) return 0;
|
||||
const hours = parseFloat(parts[0]);
|
||||
const minutes = parseFloat(parts[1]);
|
||||
const seconds = parseFloat(parts[2]);
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
};
|
||||
|
||||
/**
|
||||
* "HH:MM:SS,mmm" 形式の SRT 時刻文字列を秒数に変換する関数
|
||||
*/
|
||||
export const parseTime = (timeString) => {
|
||||
const [hms, ms] = timeString.split(",");
|
||||
const [hours, minutes, seconds] = hms.split(":").map(Number);
|
||||
return hours * 3600 + minutes * 60 + seconds + Number(ms) / 1000;
|
||||
};
|
||||
|
||||
const padTime = (int) => {
|
||||
return String(int).padStart(2, "0");
|
||||
};
|
||||
|
||||
export 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 = `${padTime(hour)}:${padTime(min)}:${padTime(sec)}`;
|
||||
} else {
|
||||
time = `${padTime(min)}:${padTime(sec)}`;
|
||||
}
|
||||
// } else {
|
||||
// time = `${padTime(sec)}`;
|
||||
// }
|
||||
return time;
|
||||
};
|
||||
|
||||
|
||||
// HH:MM:SS 形式に変換する補助関数
|
||||
export const formatTime = (timeInSeconds) => {
|
||||
const hours = Math.floor(timeInSeconds / 3600);
|
||||
const minutes = Math.floor((timeInSeconds % 3600) / 60);
|
||||
const seconds = Math.floor(timeInSeconds % 60);
|
||||
return (
|
||||
String(hours).padStart(2, "0") +
|
||||
":" +
|
||||
String(minutes).padStart(2, "0") +
|
||||
":" +
|
||||
String(seconds).padStart(2, "0")
|
||||
);
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import styles from "./CountdownContainer.module.scss";
|
||||
import { secToDayTime } from "../_subtitles_utils";
|
||||
import { useSubtitles } from "../_logics/useSubtitles";
|
||||
|
||||
export const CountdownContainer = () => {
|
||||
const {
|
||||
updateCountdownAdjustment,
|
||||
currentEffectiveCountdown,
|
||||
currentIsCuesScheduled,
|
||||
startCountdownInterval,
|
||||
} = useSubtitles();
|
||||
// カウントダウン表示:字幕開始前は常に表示
|
||||
|
||||
// if (currentEffectiveCountdown.data === 0) return null;
|
||||
if (currentEffectiveCountdown.data === null && currentIsCuesScheduled.data) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<span>カウントダウン: {secToDayTime(currentEffectiveCountdown.data)}</span>
|
||||
<div className={styles.adjust_button_container}>
|
||||
{/* 1分単位の調整ボタン */}
|
||||
<div className={styles.adjust_button_wrapper}>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newValue = currentEffectiveCountdown.data + 60;
|
||||
updateCountdownAdjustment((prev) => prev.data + 60);
|
||||
startCountdownInterval(newValue);
|
||||
}}
|
||||
className={styles.adjust_button}
|
||||
>
|
||||
▲ 1分
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newValue = currentEffectiveCountdown.data - 60;
|
||||
updateCountdownAdjustment((prev) => prev.data - 60);
|
||||
startCountdownInterval(newValue);
|
||||
}}
|
||||
className={styles.adjust_button}
|
||||
>
|
||||
▼ 1分
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.adjust_button_border}></div>
|
||||
{/* 1秒単位の調整ボタン */}
|
||||
<div className={styles.adjust_button_wrapper}>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newValue = currentEffectiveCountdown.data + 1;
|
||||
updateCountdownAdjustment((prev) => prev.data + 1);
|
||||
startCountdownInterval(newValue);
|
||||
}}
|
||||
className={styles.adjust_button}
|
||||
>
|
||||
▲ 1秒
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newValue = currentEffectiveCountdown.data - 1;
|
||||
updateCountdownAdjustment((prev) => prev.data - 1);
|
||||
startCountdownInterval(newValue);
|
||||
}}
|
||||
className={styles.adjust_button}
|
||||
>
|
||||
▼ 1秒
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
.container {
|
||||
margin-top: 1rem;
|
||||
font-size: 2.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.adjust_button_container {
|
||||
display: flex;
|
||||
gap: 10rem;
|
||||
}
|
||||
|
||||
.adjust_button_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.adjust_button {
|
||||
padding: 1rem 1.4rem;
|
||||
font-size: 1.8rem;
|
||||
border-radius: 0.4rem;
|
||||
background: var(--primary_600_color);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary_400_color);
|
||||
}
|
||||
&:active {
|
||||
background: var(--primary_650_color);
|
||||
}
|
||||
}
|
||||
|
||||
.adjust_button_border {
|
||||
background-color: var(--dark_600_color);
|
||||
width: 0.2rem;
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import styles from "./InputFileContainer.module.scss";
|
||||
import { useSubtitles } from "../_logics/useSubtitles";
|
||||
import { parseSRT, parseASS } from "../_subtitles_utils";
|
||||
|
||||
export const InputFileContainer = () => {
|
||||
const {
|
||||
updateSubtitleFileName,
|
||||
currentSubtitleFileName,
|
||||
updateSubtitleCues,
|
||||
handleSubtitlesStop
|
||||
} = useSubtitles();
|
||||
|
||||
// ファイルアップロード時の処理
|
||||
const handleFileUpload = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target.result;
|
||||
let parsedCues = [];
|
||||
// 拡張子により ASS と SRT を判定
|
||||
if (file.name.toLowerCase().endsWith(".ass")) {
|
||||
parsedCues = parseASS(content);
|
||||
} else {
|
||||
parsedCues = parseSRT(content);
|
||||
}
|
||||
updateSubtitleCues(parsedCues);
|
||||
console.log("Parsed cues:", parsedCues);
|
||||
updateSubtitleFileName(file.name);
|
||||
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
|
||||
// ファイルクリア
|
||||
const handleClearFile = () => {
|
||||
handleSubtitlesStop();
|
||||
updateSubtitleFileName("ファイルが選択されていません");
|
||||
updateSubtitleCues([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.input_file_wrapper}>
|
||||
<label htmlFor="subtitle_file_input" className={styles.input_file_label}>SRT/ASSファイルを選択</label>
|
||||
<input
|
||||
id="subtitle_file_input"
|
||||
type="file"
|
||||
accept=".srt,.ass"
|
||||
onChange={handleFileUpload}
|
||||
className={styles.input_file_i}
|
||||
/>
|
||||
<p className={styles.file_name}>{currentSubtitleFileName.data}</p>
|
||||
</div>
|
||||
<button onClick={handleClearFile} className={styles.file_clear}>
|
||||
ファイルクリア
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6rem;
|
||||
}
|
||||
|
||||
.input_file_wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
.input_file_label {
|
||||
font-size: 1.4rem;
|
||||
border-radius: 0.4rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--dark_850_color);
|
||||
border: 0.1rem solid var(--dark_400_color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.input_file_i {
|
||||
display: none;
|
||||
}
|
||||
.file_name {
|
||||
font-size: 1.6rem;
|
||||
padding: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 60rem;
|
||||
}
|
||||
|
||||
.file_clear {
|
||||
background: var(--dark_800_color);
|
||||
color: var(--dark_200_color);
|
||||
border-radius: 0.4rem;
|
||||
font-size: 1.2rem;
|
||||
padding: 0.6rem 1rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--error_bc_color);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import styles from "./ModeSelectorContainer.module.scss";
|
||||
import { useSubtitles } from "../_logics/useSubtitles";
|
||||
export const ModeSelectorContainer = () => {
|
||||
const {
|
||||
currentSubtitlePlaybackMode,
|
||||
updateSubtitlePlaybackMode,
|
||||
currentSubtitleAbsoluteTargetTime,
|
||||
updateSubtitleAbsoluteTargetTime,
|
||||
} = useSubtitles();
|
||||
|
||||
const target_time = currentSubtitleAbsoluteTargetTime.data;
|
||||
|
||||
const handleOnchangeTargetTime = (key, value) => {
|
||||
updateSubtitleAbsoluteTargetTime((old_value) => {
|
||||
return {
|
||||
...old_value.data,
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.mode_selector_wrapper}>
|
||||
<select
|
||||
value={currentSubtitlePlaybackMode.data}
|
||||
onChange={(e) => updateSubtitlePlaybackMode(e.target.value)}
|
||||
className={styles.mode_selector}
|
||||
>
|
||||
<option className={styles.mode_selector_item} value="relative">相対モード(ボタン押下から)</option>
|
||||
<option className={styles.mode_selector_item} value="absolute">絶対モード(指定時刻から)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{currentSubtitlePlaybackMode.data === "absolute" && (
|
||||
<div className={styles.time_section}>
|
||||
<label className={styles.absolute_time_label}>再生開始時刻 (HH:MM):</label>
|
||||
<div className={styles.time_selects}>
|
||||
<select
|
||||
value={target_time.hour}
|
||||
onChange={(e) => handleOnchangeTargetTime("hour", e.target.value)}
|
||||
className={styles.time_selects_item}
|
||||
>
|
||||
{Array.from({ length: 24 }, (_, i) => {
|
||||
const hour = i.toString().padStart(2, "0");
|
||||
return (
|
||||
<option key={i} value={hour}>
|
||||
{hour}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span>:</span>
|
||||
<select
|
||||
value={target_time.minute}
|
||||
onChange={(e) => handleOnchangeTargetTime("minute", e.target.value)}
|
||||
className={styles.time_selects_item}
|
||||
>
|
||||
{Array.from({ length: 60 }, (_, i) => {
|
||||
const minute = i.toString().padStart(2, "0");
|
||||
return (
|
||||
<option key={i} value={minute}>
|
||||
{minute}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
.container {
|
||||
// background-color: red;
|
||||
display: flex;
|
||||
gap: 4rem;
|
||||
}
|
||||
.mode_selector_wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
.absolute_time_label {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.mode_selector {
|
||||
font-size: 1.6rem;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 0.1rem solid var(--dark_400_color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mode_selector_item {
|
||||
background-color: var(--dark_800_color);
|
||||
}
|
||||
|
||||
|
||||
.time_section {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.time_selects {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.time_selects_item {
|
||||
width: 6rem;
|
||||
text-align: center;
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 1.8rem;
|
||||
background-color: var(--dark_850_color);
|
||||
border: 0.1rem solid var(--dark_400_color);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// import React, { useState, useRef, useEffect } from "react";
|
||||
import styles from "./PlayControlContainer.module.scss";
|
||||
import { useSubtitles } from "../_logics/useSubtitles";
|
||||
import { clsx } from "clsx";
|
||||
|
||||
export const PlayControlContainer = () => {
|
||||
const {
|
||||
currentIsSubtitlePlaying,
|
||||
handleSubtitlesStart,
|
||||
handleSubtitlesStop,
|
||||
} = useSubtitles();
|
||||
|
||||
const is_playing = currentIsSubtitlePlaying.data;
|
||||
|
||||
const playback_button_classname = clsx(styles.playback_button, {
|
||||
[styles.is_playing]: is_playing,
|
||||
});
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<button
|
||||
onClick={handleSubtitlesStart}
|
||||
className={playback_button_classname}
|
||||
>
|
||||
{is_playing ? "再生中" : "字幕を登録・再生"}
|
||||
</button>
|
||||
{is_playing &&
|
||||
<button onClick={handleSubtitlesStop} className={styles.playback_stop_button}>
|
||||
停止
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 4rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.playback_button, .playback_stop_button {
|
||||
font-size: 1.6rem;
|
||||
padding: 1rem 2rem;
|
||||
cursor: pointer;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
|
||||
.playback_button {
|
||||
background-color: var(--primary_550_color);
|
||||
&:hover {
|
||||
background-color: var(--primary_400_color);
|
||||
}
|
||||
&.is_playing {
|
||||
background-color: var(--primary_650_color);
|
||||
pointer-events: none;
|
||||
color: var(--dark_400_color);
|
||||
}
|
||||
}
|
||||
.playback_stop_button {
|
||||
background-color: var(--dark_800_color);
|
||||
&:hover {
|
||||
background-color: var(--error_bc_color);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import styles from "./SubtitlesListContainer.module.scss";
|
||||
import { useSubtitles } from "../_logics/useSubtitles";
|
||||
import { formatTime } from "../_subtitles_utils";
|
||||
|
||||
export const SubtitlesListContainer = () => {
|
||||
const { currentSubtitleCues, handleJump } = useSubtitles();
|
||||
|
||||
if (currentSubtitleCues.data.length < 0 ) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.subtitleSection}>
|
||||
<h2>字幕一覧</h2>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>番号</th>
|
||||
<th>開始</th>
|
||||
<th>終了</th>
|
||||
<th>Actor</th>
|
||||
<th>テキスト</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className={styles.subtitle_lists}>
|
||||
{currentSubtitleCues.data.map((cue) => (
|
||||
<tr
|
||||
key={cue.index}
|
||||
onClick={() => handleJump(cue)}
|
||||
className={styles.tableRow}
|
||||
>
|
||||
<td>{cue.index}</td>
|
||||
<td>{formatTime(cue.startTime)}</td>
|
||||
<td>{formatTime(cue.endTime)}</td>
|
||||
<td>{cue.actor}</td>
|
||||
<td>{cue.text}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<p className={styles.note}>
|
||||
※ 行をクリックすると、その字幕の位置にジャンプします。(相対モードのみ)
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user