// Music player — three tracks, play/pause/skip/seek/volume. const TRACKS = [ { title: 'Grace Ascent', artist: 'Meridian OST', src: 'https://cdn.pixabay.com/audio/2022/10/18/audio_31c1679d6c.mp3' }, { title: 'Siege Breaker', artist: 'Meridian OST', src: 'https://cdn.pixabay.com/audio/2022/11/17/audio_febc508520.mp3' }, { title: 'Final Whistle', artist: 'Meridian OST', src: 'https://cdn.pixabay.com/audio/2023/06/13/audio_2c8e3b31b0.mp3' }, ]; export default { id: 'music', title: 'Music', render(container, api) { const audio = new Audio(); let currentTrack = 0; let playedSeconds = 0; let playSecondsTimer = null; const root = document.createElement('div'); root.style.cssText = 'height:100%;display:flex;overflow:hidden;'; // Left panel: track list const listPanel = document.createElement('div'); listPanel.style.cssText = 'width:220px;background:var(--bg-2);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow-y:auto;flex-shrink:0;'; const trackList = TRACKS.map((track, i) => { const row = document.createElement('div'); row.style.cssText = 'padding:10px 14px;cursor:pointer;display:flex;flex-direction:column;border-left:2px solid transparent;border-bottom:1px solid rgba(255,255,255,0.04);transition:all 150ms ease-out;'; const titleSpan = document.createElement('div'); titleSpan.style.cssText = 'font-size:13px;font-weight:500;color:var(--text);'; titleSpan.textContent = track.title; const artistSpan = document.createElement('div'); artistSpan.style.cssText = 'font-size:11px;color:var(--text-dim);margin-top:2px;'; artistSpan.textContent = track.artist; row.appendChild(titleSpan); row.appendChild(artistSpan); row.addEventListener('click', () => loadTrack(i)); return { row, titleSpan, artistSpan }; }); TRACKS.forEach((_, i) => listPanel.appendChild(trackList[i].row)); // Right panel: now playing const nowPanel = document.createElement('div'); nowPanel.style.cssText = 'flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 24px;background:var(--bg-1);overflow-y:auto;'; const cover = document.createElement('div'); cover.style.cssText = 'width:120px;height:120px;background:rgba(212,162,76,0.15);border:1px solid rgba(212,162,76,0.2);display:flex;align-items:center;justify-content:center;font-size:48px;font-weight:600;color:var(--accent);margin-bottom:24px;'; const nowTitle = document.createElement('div'); nowTitle.style.cssText = 'font-size:28px;font-weight:600;color:var(--text);margin-bottom:6px;text-align:center;'; const nowArtist = document.createElement('div'); nowArtist.style.cssText = 'font-size:14px;color:var(--text-dim);margin-bottom:32px;text-align:center;'; // Controls row const controls = document.createElement('div'); controls.style.cssText = 'display:flex;align-items:center;gap:20px;margin-bottom:28px;'; const btnStyle = 'width:40px;height:40px;border-radius:50%;background:var(--bg-3);border:1px solid var(--border);color:var(--text);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:background 150ms ease-out;'; const prevBtn = document.createElement('button'); prevBtn.style.cssText = btnStyle; prevBtn.textContent = '<<'; prevBtn.addEventListener('click', () => { currentTrack = (currentTrack - 1 + TRACKS.length) % TRACKS.length; loadTrack(currentTrack); }); const playBtn = document.createElement('button'); playBtn.style.cssText = 'width:48px;height:48px;border-radius:50%;background:var(--accent);border:none;color:#0f1114;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:700;transition:opacity 150ms ease-out;'; playBtn.textContent = '>'; const nextBtn = document.createElement('button'); nextBtn.style.cssText = btnStyle; nextBtn.textContent = '>>'; nextBtn.addEventListener('click', () => { currentTrack = (currentTrack + 1) % TRACKS.length; loadTrack(currentTrack); }); controls.appendChild(prevBtn); controls.appendChild(playBtn); controls.appendChild(nextBtn); // Seek bar const seekContainer = document.createElement('div'); seekContainer.style.cssText = 'width:100%;max-width:400px;margin-bottom:16px;'; const seekLabel = document.createElement('div'); seekLabel.style.cssText = 'font-size:11px;color:var(--text-dim);margin-bottom:4px;display:flex;justify-content:space-between;'; const seekLabelLeft = document.createElement('span'); seekLabelLeft.textContent = '0:00'; const seekLabelRight = document.createElement('span'); seekLabelRight.textContent = '0:00'; seekLabel.appendChild(seekLabelLeft); seekLabel.appendChild(seekLabelRight); const seekInput = document.createElement('input'); seekInput.type = 'range'; seekInput.min = '0'; seekInput.max = '100'; seekInput.value = '0'; seekInput.style.cssText = 'width:100%;height:4px;-webkit-appearance:none;appearance:none;background:var(--bg-3);border-radius:2px;outline:none;cursor:pointer;'; seekInput.addEventListener('input', () => { if (audio.duration) { audio.currentTime = (seekInput.value / 100) * audio.duration; } }); seekContainer.appendChild(seekLabel); seekContainer.appendChild(seekInput); // Volume slider const volContainer = document.createElement('div'); volContainer.style.cssText = 'display:flex;align-items:center;gap:8px;margin-top:12px;'; const volLabel = document.createElement('span'); volLabel.style.cssText = 'font-size:12px;color:var(--text-dim);'; volLabel.textContent = '\u{25A0}\u{25A0}'; const volInput = document.createElement('input'); volInput.type = 'range'; volInput.min = '0'; volInput.max = '100'; volInput.value = '70'; volInput.style.cssText = 'width:100px;height:4px;-webkit-appearance:none;appearance:none;background:var(--bg-3);border-radius:2px;outline:none;cursor:pointer;'; volInput.addEventListener('input', () => { audio.volume = volInput.value / 100; }); volContainer.appendChild(volLabel); volContainer.appendChild(volInput); nowPanel.appendChild(cover); nowPanel.appendChild(nowTitle); nowPanel.appendChild(nowArtist); nowPanel.appendChild(controls); nowPanel.appendChild(seekContainer); nowPanel.appendChild(volContainer); root.appendChild(listPanel); root.appendChild(nowPanel); container.appendChild(root); function formatTime(s) { if (!isFinite(s)) return '0:00'; return Math.floor(s / 60) + ':' + String(Math.floor(s % 60)).padStart(2, '0'); } function updateUI() { const track = TRACKS[currentTrack]; const urlOk = urlAvailable(track.src); cover.textContent = track.title.charAt(0); nowTitle.textContent = track.title; nowArtist.textContent = track.artist; playBtn.textContent = audio.paused ? '>' : '||'; // Highlight selected track TRACKS.forEach((_, i) => { const row = trackList[i].row; if (i === currentTrack) { row.style.borderLeftColor = 'var(--accent)'; row.style.background = 'var(--bg-3)'; } else { row.style.borderLeftColor = 'transparent'; row.style.background = 'transparent'; } }); seekLabelLeft.textContent = formatTime(audio.currentTime); seekLabelRight.textContent = formatTime(audio.duration); seekInput.value = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0; } function loadTrack(index) { stopPlaySecondsTimer(); currentTrack = index; const track = TRACKS[index]; if (!urlAvailable(track.src)) { nowTitle.textContent = track.title; nowArtist.textContent = 'Track unavailable (offline)'; cover.textContent = track.title.charAt(0); cover.style.background = 'rgba(150,150,150,0.15)'; playBtn.textContent = '>'; playBtn.disabled = true; seekInput.value = 0; seekLabelLeft.textContent = '0:00'; seekLabelRight.textContent = '0:00'; return; } cover.style.background = 'rgba(212,162,76,0.15)'; playBtn.disabled = false; audio.src = track.src; audio.volume = volInput.value / 100; audio.load(); updateUI(); } playBtn.addEventListener('click', () => { if (playBtn.disabled) return; if (!audio.src) return; if (audio.paused) { audio.play().then(() => { api.bus.emit('music:play'); updateUI(); }).catch(() => { updateUI(); }); startPlaySecondsTimer(); } else { audio.pause(); updateUI(); stopPlaySecondsTimer(); } }); audio.addEventListener('timeupdate', updateUI); audio.addEventListener('ended', () => { currentTrack = (currentTrack + 1) % TRACKS.length; loadTrack(currentTrack); audio.play().then(() => { updateUI(); startPlaySecondsTimer(); }).catch(() => { updateUI(); }); }); audio.addEventListener('error', () => { nowArtist.textContent = 'Track unavailable (offline)'; playBtn.disabled = true; updateUI(); stopPlaySecondsTimer(); }); updateUI(); loadTrack(0); audio.pause(); function urlAvailable(url) { try { const u = new URL(url); return true; } catch { return false; } } function startPlaySecondsTimer() { stopPlaySecondsTimer(); playSecondsTimer = setInterval(() => { playedSeconds++; api.bus.emit('music:played', { seconds: playedSeconds }); }, 5000); } function stopPlaySecondsTimer() { clearInterval(playSecondsTimer); } }, };