document.addEventListener('DOMContentLoaded', () => { const gridContainer = document.getElementById('character-grid'); const audioElement = document.getElementById('main-audio'); // 新しい右カラムの要素 const mainPlayBtn = document.getElementById('main-play-btn'); const progressBar = document.getElementById('progress-bar'); const currentTimeEl = document.getElementById('current-time'); const durationEl = document.getElementById('duration'); const previewImage = document.getElementById('preview-image'); const previewName = document.getElementById('preview-name'); const previewNameReading = document.getElementById('preview-name-reading'); const previewMeta = document.getElementById('preview-meta'); let voiceActors = []; // ファイル名の指定 const jsonFile = 'voice_actors_local.json'; // JSONデータの読み込み fetch(jsonFile) .then(response => { if (!response.ok) { return fetch('voice_actors.json').then(res => res.json()); } return response.json(); }) .then(data => { voiceActors = data; renderGrid(); // 初期選択 if (voiceActors.length > 0) { selectActor(0); } }) .catch(error => { console.error('Error:', error); gridContainer.innerHTML = '
データの読み込みに失敗しました。
'; }); // グリッドの描画 function renderGrid() { gridContainer.innerHTML = ''; voiceActors.forEach((actor, index) => { const card = document.createElement('div'); card.className = `voice-card bg-white rounded-2xl overflow-hidden border-2 border-transparent transition-all duration-200 cursor-pointer hover:shadow-lg`; card.dataset.index = index; const tagsHtml = actor.tags ? actor.tags.slice(0, 3).map(tag => `${tag}` ).join('') : ''; card.innerHTML = `
${actor.name}

${actor.nameReading || ''}

${actor.name}

${actor.age}歳

${tagsHtml}
試聴
`; // クリックイベント card.addEventListener('click', () => { selectActor(index); // クリック時に再生したい場合はここを有効化 playAudio(actor.audio_url); }); gridContainer.appendChild(card); }); } // キャラクター選択処理 function selectActor(index) { const actor = voiceActors[index]; // グリッドの選択状態更新 document.querySelectorAll('.voice-card').forEach(el => el.classList.remove('border-indigo-500', 'shadow-xl')); const selectedCard = gridContainer.children[index]; if(selectedCard) selectedCard.classList.add('border-indigo-500', 'shadow-xl'); // 右カラムの更新 previewImage.src = actor.image_url; previewName.textContent = actor.name; previewNameReading.textContent = actor.nameReading || ''; previewMeta.textContent = `${actor.age}歳`; // 音声ソースの更新 if (audioElement.src !== new URL(actor.audio_url, document.baseURI).href) { audioElement.src = actor.audio_url; resetPlayerUI(); } } // オーディオ制御 function playAudio(url) { const absoluteUrl = new URL(url, document.baseURI).href; if(audioElement.src !== absoluteUrl) { audioElement.src = url; } audioElement.play().catch(e => console.log("再生エラー:", e)); updateBtnState(true); } // メインボタンクリック mainPlayBtn.addEventListener('click', () => { if (audioElement.paused) { audioElement.play(); } else { audioElement.pause(); } }); audioElement.addEventListener('play', () => updateBtnState(true)); audioElement.addEventListener('pause', () => updateBtnState(false)); audioElement.addEventListener('ended', () => updateBtnState(false)); audioElement.addEventListener('timeupdate', () => { if (audioElement.duration) { const percent = (audioElement.currentTime / audioElement.duration) * 100; progressBar.style.width = `${percent}%`; currentTimeEl.textContent = formatTime(audioElement.currentTime); durationEl.textContent = formatTime(audioElement.duration); } }); audioElement.addEventListener('loadedmetadata', () => { durationEl.textContent = formatTime(audioElement.duration); }); function updateBtnState(isPlaying) { const textSpan = mainPlayBtn.lastChild; // ボタンのテキストノード if (isPlaying) { textSpan.textContent = " 停止する"; mainPlayBtn.classList.add('bg-indigo-600'); mainPlayBtn.classList.remove('bg-gray-900'); } else { textSpan.textContent = " サンプルボイスを再生"; mainPlayBtn.classList.remove('bg-indigo-600'); mainPlayBtn.classList.add('bg-gray-900'); } } function resetPlayerUI() { progressBar.style.width = '0%'; currentTimeEl.textContent = '0:00'; durationEl.textContent = '0:00'; updateBtnState(false); } function formatTime(seconds) { if (!seconds || isNaN(seconds)) return "0:00"; const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, '0')}`; } });