File size: 7,447 Bytes
ffcebc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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 = '<div class="col-span-full text-center text-red-500 py-10">データの読み込みに失敗しました。</div>';
        });

    // グリッドの描画
    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 => 
                `<span class="bg-black/20 backdrop-blur-sm text-white text-[10px] font-bold px-2 py-0.5 rounded">${tag}</span>`
            ).join('') : '';

            card.innerHTML = `
                <div class="card-image-wrapper bg-gray-200 relative pt-[150%]">
                    <img src="${actor.thumbnail_url || actor.image_url}" alt="${actor.name}" loading="lazy" class="absolute top-0 left-0 w-full h-full object-cover transition-transform duration-300 hover:scale-105">
                    <div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent flex flex-col justify-end p-3 md:p-4 pointer-events-none">
                        <p class="text-[10px] text-gray-300 leading-tight mb-0.5">${actor.nameReading || ''}</p>
                        <h3 class="text-base md:text-lg font-bold text-white leading-tight mb-1">${actor.name}</h3>
                        <p class="text-[10px] md:text-xs text-gray-200 mb-2 font-medium">${actor.age}歳</p>
                        <div class="flex flex-wrap gap-1">
                            ${tagsHtml}
                        </div>
                    </div>
                </div>
                <div class="p-3 border-t border-gray-100 flex justify-between items-center bg-white">
                     <span class="text-xs font-bold text-gray-500 flex items-center gap-1">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
                        試聴
                     </span>
                </div>
            `;

            // クリックイベント
            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')}`;
    }
});