VCT / static /script.js
filmy's picture
Update static/script.js
75992c8 verified
Raw History Blame Contribute Delete
16.6 kB
document.addEventListener('DOMContentLoaded', () => {
const socket = io();
// HTML要素の取得
const startForm = document.getElementById('startForm');
const urlInput = document.getElementById('url');
const stopBtn = document.getElementById('stopBtn');
const videoFrame = document.getElementById('video-frame');
const subtitleContainer = document.getElementById('subtitle-container');
const historyList = document.getElementById('history-list');
// 設定メニュー関連
const hamburger = document.querySelector('.hamburger');
const navMenu = document.getElementById('nav-menu');
const subtitleSizeSelect = document.getElementById('subtitle_size');
const subtitleLangSelect = document.getElementById('subtitle_lang');
const bgSelect = document.getElementById('backgroundColor');
// 文字色設定関連
const textColorInput = document.getElementById('textColor');
const textFgPreview = document.getElementById('textFgPreview');
const textColorSettingItem = document.getElementById('textColorSettingItem');
// 背景設定関連
const textBackgroundCheck = document.getElementById('textBackground_check');
const textBackground = document.getElementById('textBackground');
const textBgPreview = document.getElementById('textBgPreview');
const transparency = document.getElementById('textBackground_transparency');
// シアターモード関連
const theaterBtn = document.getElementById('theaterToggle');
const closeTheaterBtn = document.getElementById('closeTheaterBtn');
const videoContainer = document.getElementById('video-container');
// 話者識別関連
const identifyCheck = document.getElementById('identify_speakers_check');
const speakerFeaturesArea = document.getElementById('speaker_features_area');
const maxSpeakersSelect = document.getElementById('max_speakers');
const resetSpeakersBtn = document.getElementById('resetSpeakersBtn');
const speakerColorContainer = document.getElementById("speakerColorSettings");
let speakerData = {};
let currentFontSize = '24px';
// --- 初期化処理 ---
if(localStorage.getItem("savedSubtitleSize")) {
const s = localStorage.getItem("savedSubtitleSize");
if(subtitleSizeSelect) subtitleSizeSelect.value = s;
currentFontSize = s + 'px';
}
if(urlInput && localStorage.getItem("savedUrlInput")) {
urlInput.value = localStorage.getItem("savedUrlInput");
}
// --- 話者識別ON/OFFロジック ---
if (identifyCheck) {
identifyCheck.checked = false; // デフォルトOFF
// 初期表示設定
toggleSpeakerSettings();
identifyCheck.addEventListener('change', () => {
toggleSpeakerSettings();
sendSettings(); // 設定変更を即時送信
});
}
// 設定項目の表示/非表示を切り替える関数
function toggleSpeakerSettings() {
if (!identifyCheck) return;
if (identifyCheck.checked) {
// ONのとき: 話者設定を表示、文字色設定を隠す
if (speakerFeaturesArea) speakerFeaturesArea.style.display = 'block';
if (textColorSettingItem) textColorSettingItem.style.display = 'none';
} else {
// OFFのとき: 話者設定を隠す、文字色設定を表示
if (speakerFeaturesArea) speakerFeaturesArea.style.display = 'none';
if (textColorSettingItem) textColorSettingItem.style.display = 'block';
}
}
// --- 文字色プレビュー反映 ---
if (textColorInput && textFgPreview) {
textColorInput.addEventListener('input', (e) => {
textFgPreview.style.backgroundColor = e.target.value;
});
textFgPreview.style.backgroundColor = textColorInput.value;
}
// --- テーマ設定 ---
function updateTheme() {
const val = bgSelect.value;
const body = document.body;
body.classList.remove('light-mode', 'dark-mode');
if (val === 'black') {
body.classList.add('dark-mode');
} else if (val === 'device') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
body.classList.add('dark-mode');
} else {
body.classList.add('light-mode');
}
} else {
body.classList.add('light-mode');
}
}
if (bgSelect) bgSelect.addEventListener('change', updateTheme);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme);
updateTheme();
// --- Socketイベント ---
socket.on('connect', () => {
socket.emit('request_speaker_profiles');
});
socket.on('initial_speaker_profiles', (profiles) => {
speakerData = profiles || {};
updateSpeakerColorUI();
});
// 字幕受信処理
socket.on('new_subtitle', (data) => {
if (!data || !subtitleContainer) return;
if (!speakerData[data.speaker_id]) {
socket.emit('request_speaker_profiles');
}
const info = speakerData[data.speaker_id] || { name: data.speaker, color: data.color };
const displayName = info.name;
const displayColor = info.color;
// 表示制御ロジック
const isIdentityEnabled = identifyCheck && identifyCheck.checked;
let finalDisplayText = "";
let finalColor = "#FFFFFF";
if (isIdentityEnabled) {
// ON: 名前表示 + 色反映
finalDisplayText = `${displayName}: ${data.translated}`;
finalColor = displayColor;
} else {
// OFF: 名前表示なし + 設定した文字色
finalDisplayText = data.translated;
if (textColorInput) {
finalColor = textColorInput.value;
}
}
// 履歴に追加
if (historyList) {
const li = document.createElement('li');
if (isIdentityEnabled) {
li.innerHTML = `<span style="font-weight:bold; color:${displayColor}">${displayName}:</span> ${data.translated}`;
} else {
li.textContent = data.translated;
}
historyList.prepend(li);
if (historyList.children.length > 30) historyList.removeChild(historyList.lastChild);
}
// 字幕要素作成
const p = document.createElement('p');
p.className = 'subtitle-line';
p.style.fontSize = currentFontSize;
p.style.color = finalColor;
p.textContent = finalDisplayText;
applySubtitleBackground(p);
subtitleContainer.appendChild(p);
// 古い字幕削除
while (subtitleContainer.children.length > 5) {
subtitleContainer.removeChild(subtitleContainer.firstChild);
}
setTimeout(() => p.style.opacity = '0', 4500);
setTimeout(() => p.remove(), 5000);
});
socket.on("speaker_name_changed", data => {
const { id, new_name } = data;
if (speakerData[id]) {
speakerData[id].name = new_name;
const input = document.getElementById(`name-input-${id}`);
if (input && input.value !== new_name) input.value = new_name;
}
});
socket.on("speaker_color_changed", data => {
const { id, color } = data;
if (speakerData[id]) {
speakerData[id].color = color;
const bgDiv = document.getElementById(`color-box-${id}`);
const input = document.getElementById(`color-input-${id}`);
if (bgDiv) bgDiv.style.backgroundColor = color;
if (input) input.value = color;
}
});
socket.on('error_msg', (d) => alert(d.msg));
// --- 話者設定UI更新 ---
function updateSpeakerColorUI() {
if (!speakerColorContainer) return;
const existingIds = new Set();
speakerColorContainer.querySelectorAll('.speaker-setting-row').forEach(el => {
existingIds.add(el.dataset.speakerId);
});
Object.keys(speakerData).forEach(speakerId => {
const info = speakerData[speakerId];
if (existingIds.has(speakerId)) {
existingIds.delete(speakerId);
return;
}
const row = document.createElement("div");
row.className = "speaker-setting-row";
row.dataset.speakerId = speakerId;
row.style.marginBottom = "5px";
row.style.display = "flex";
row.style.alignItems = "center";
const nameInput = document.createElement("input");
nameInput.type = "text";
nameInput.value = info.name;
nameInput.id = `name-input-${speakerId}`;
nameInput.className = "speaker-name-input";
nameInput.style.flex = "1";
nameInput.style.marginRight = "5px";
nameInput.addEventListener("change", () => {
const newName = nameInput.value.trim();
if (newName) {
socket.emit("update_speaker_name", { id: speakerId, new_name: newName });
} else {
nameInput.value = speakerData[speakerId].name;
}
});
const colorBox = document.createElement("div");
colorBox.className = "color-preview-box";
colorBox.id = `color-box-${speakerId}`;
colorBox.style.backgroundColor = info.color;
colorBox.style.width = "30px";
colorBox.style.height = "30px";
colorBox.style.border = "1px solid #ccc";
colorBox.style.position = "relative";
colorBox.style.cursor = "pointer";
const colorInput = document.createElement("input");
colorInput.type = "color";
colorInput.value = info.color;
colorInput.id = `color-input-${speakerId}`;
colorInput.className = "color-picker-hidden";
colorInput.style.opacity = 0;
colorInput.style.width = "100%";
colorInput.style.height = "100%";
colorInput.style.position = "absolute";
colorInput.style.top = 0;
colorInput.style.left = 0;
colorInput.style.cursor = "pointer";
colorInput.addEventListener("input", (e) => {
colorBox.style.backgroundColor = e.target.value;
});
colorInput.addEventListener("change", (e) => {
const newColor = e.target.value;
speakerData[speakerId].color = newColor;
socket.emit("update_speaker_color", { id: speakerId, color: newColor });
});
colorBox.appendChild(colorInput);
row.appendChild(nameInput);
row.appendChild(colorBox);
speakerColorContainer.appendChild(row);
});
existingIds.forEach(oldId => {
const el = speakerColorContainer.querySelector(`.speaker-setting-row[data-speaker-id="${oldId}"]`);
if (el) el.remove();
});
}
if (resetSpeakersBtn) {
resetSpeakersBtn.addEventListener('click', () => {
if(confirm("話者設定をリセットしますか?")) {
speakerData = {};
speakerColorContainer.innerHTML = "";
socket.emit('reset_speakers');
}
});
}
// --- フォーム送信・制御 ---
if (startForm) {
startForm.addEventListener('submit', (e) => {
e.preventDefault();
const url = urlInput.value.trim();
if (!url) return alert("URLを入力してください");
localStorage.setItem("savedUrlInput", url);
let embed = url;
if(url.includes('twitch.tv')) {
const m = url.match(/twitch\.tv\/([^\/]+)/);
if(m) embed = `https://player.twitch.tv/?channel=${m[1]}&parent=${location.hostname}&autoplay=true&muted=true`;
} else if(url.includes('youtube.com') || url.includes('youtu.be')) {
const m = url.match(/(?:v=|\/)([a-zA-Z0-9_-]{11})/);
if(m) embed = `https://www.youtube.com/embed/${m[1]}?autoplay=1`;
}
videoFrame.src = embed;
subtitleContainer.innerHTML = '';
historyList.innerHTML = '';
const isIdentityEnabled = identifyCheck && identifyCheck.checked;
const maxSpk = isIdentityEnabled ? parseInt(maxSpeakersSelect.value) : 1;
const targetLang = subtitleLangSelect ? subtitleLangSelect.value : 'ja';
socket.emit('start_stream', {
url: url,
target_lang: targetLang,
max_speakers: maxSpk
});
});
}
if (stopBtn) {
stopBtn.addEventListener('click', () => {
videoFrame.src = '';
socket.emit('stop_stream');
});
}
function sendSettings() {
if (!subtitleLangSelect || !maxSpeakersSelect) return;
const isIdentityEnabled = identifyCheck && identifyCheck.checked;
const maxSpk = isIdentityEnabled
? parseInt(maxSpeakersSelect.value, 10)
: 1;
socket.emit('update_settings', {
target_lang: subtitleLangSelect.value || 'ja',
max_speakers: maxSpk
});
}
if (subtitleLangSelect) subtitleLangSelect.addEventListener('change', sendSettings);
if (maxSpeakersSelect) maxSpeakersSelect.addEventListener('change', sendSettings);
// --- スタイル反映 ---
function hexToRgba(hex, alpha) {
hex = hex.replace(/^#/, '');
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
const r = parseInt(hex.slice(0, 2), 16), g = parseInt(hex.slice(2, 4), 16), b = parseInt(hex.slice(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha / 100})`;
}
function applySubtitleBackground(el) {
if (!textBackgroundCheck.checked) {
el.style.backgroundColor = 'transparent';
el.style.padding = '0';
return;
}
const alpha = transparency.value;
el.style.backgroundColor = hexToRgba(textBackground.value, alpha);
el.style.padding = '4px 8px';
el.style.borderRadius = '4px';
el.style.marginBottom = '4px';
el.style.display = 'inline-block';
}
const updateAll = () => document.querySelectorAll('.subtitle-line').forEach(applySubtitleBackground);
const updatePreview = () => { if(textBgPreview) textBgPreview.style.backgroundColor = textBackground.value; };
textBackgroundCheck.addEventListener('change', updateAll);
textBackground.addEventListener('input', () => { updateAll(); updatePreview(); });
transparency.addEventListener('input', updateAll);
updatePreview();
if (subtitleSizeSelect) {
subtitleSizeSelect.addEventListener('input', () => {
currentFontSize = subtitleSizeSelect.value + 'px';
localStorage.setItem("savedSubtitleSize", subtitleSizeSelect.value);
document.querySelectorAll('.subtitle-line').forEach(p => p.style.fontSize = currentFontSize);
});
}
// --- ハンバーガーメニュー ---
if (hamburger && navMenu) {
hamburger.addEventListener('click', () => {
hamburger.classList.toggle('active');
navMenu.classList.toggle('active');
});
document.addEventListener('click', (e) => {
if (!e.target.closest('.nav') && !e.target.closest('.hamburger') && navMenu.classList.contains('active')) {
hamburger.classList.remove('active');
navMenu.classList.remove('active');
}
});
}
// --- シアターモード ---
if (videoContainer) {
if (theaterBtn) {
theaterBtn.addEventListener('click', () => {
document.body.classList.add('theater-active');
videoContainer.classList.add('theater-mode');
videoContainer.classList.remove('normal-mode');
});
}
if (closeTheaterBtn) {
closeTheaterBtn.addEventListener('click', () => {
document.body.classList.remove('theater-active');
videoContainer.classList.remove('theater-mode');
videoContainer.classList.add('normal-mode');
});
}
}
});