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 = `${displayName}: ${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'); }); } } });