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.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')}`;
}
});