/** * WP FediStream - Frontend Scripts * * @package WP_FediStream */ (function() { 'use strict'; /** * FediStream Audio Player * Handles audio playback, queue management, and UI updates */ class FediStreamPlayer { constructor() { this.audio = new Audio(); this.queue = []; this.currentIndex = -1; this.isPlaying = false; this.isShuffle = false; this.repeatMode = 'none'; // none, all, one this.volume = 0.8; this.originalQueue = []; this.init(); } /** * Initialize the player */ init() { this.audio.volume = this.volume; this.bindAudioEvents(); this.bindUIEvents(); this.initNowPlayingWidgets(); this.loadVolumeFromStorage(); } /** * Bind audio element events */ bindAudioEvents() { this.audio.addEventListener('timeupdate', () => this.onTimeUpdate()); this.audio.addEventListener('loadedmetadata', () => this.onLoadedMetadata()); this.audio.addEventListener('ended', () => this.onEnded()); this.audio.addEventListener('play', () => this.onPlay()); this.audio.addEventListener('pause', () => this.onPause()); this.audio.addEventListener('error', (e) => this.onError(e)); this.audio.addEventListener('waiting', () => this.onWaiting()); this.audio.addEventListener('canplay', () => this.onCanPlay()); } /** * Bind UI event listeners */ bindUIEvents() { // Play buttons on cards and tracklists document.addEventListener('click', (e) => { const playBtn = e.target.closest('[data-track-id]'); if (playBtn && (playBtn.classList.contains('fedistream-card__play-overlay') || playBtn.classList.contains('fedistream-tracklist__play') || playBtn.classList.contains('fedistream-widget__play') || playBtn.classList.contains('fedistream-single__play-overlay') || playBtn.classList.contains('fedistream-track__play-overlay'))) { e.preventDefault(); const trackId = playBtn.dataset.trackId; this.playTrackById(trackId); } // Tracklist item click (not on play button or link) const tracklistItem = e.target.closest('.fedistream-tracklist__item'); if (tracklistItem && !e.target.closest('a') && !e.target.closest('button')) { e.preventDefault(); const trackId = tracklistItem.dataset.trackId; if (trackId) { this.playTrackById(trackId); } } // Play all button if (e.target.closest('.fedistream-btn--play-all')) { e.preventDefault(); const btn = e.target.closest('.fedistream-btn--play-all'); const albumId = btn.dataset.albumId; const playlistId = btn.dataset.playlistId; if (albumId) { this.playAlbum(albumId); } else if (playlistId) { this.playPlaylist(playlistId); } } // Shuffle button if (e.target.closest('.fedistream-btn--shuffle')) { e.preventDefault(); const btn = e.target.closest('.fedistream-btn--shuffle'); const albumId = btn.dataset.albumId; const playlistId = btn.dataset.playlistId; if (albumId) { this.playAlbum(albumId, true); } else if (playlistId) { this.playPlaylist(playlistId, true); } } }); // Player controls document.addEventListener('click', (e) => { // Main play/pause button if (e.target.closest('.fedistream-player__btn--play')) { e.preventDefault(); this.togglePlayPause(); } // Previous button if (e.target.closest('.fedistream-player__btn--prev')) { e.preventDefault(); this.previous(); } // Next button if (e.target.closest('.fedistream-player__btn--next')) { e.preventDefault(); this.next(); } // Shuffle button if (e.target.closest('.fedistream-player__btn--shuffle')) { e.preventDefault(); this.toggleShuffle(); e.target.closest('.fedistream-player__btn--shuffle').classList.toggle('is-active', this.isShuffle); } // Repeat button if (e.target.closest('.fedistream-player__btn--repeat')) { e.preventDefault(); this.toggleRepeat(); const btn = e.target.closest('.fedistream-player__btn--repeat'); btn.classList.remove('is-active', 'is-repeat-one'); if (this.repeatMode === 'all') { btn.classList.add('is-active'); } else if (this.repeatMode === 'one') { btn.classList.add('is-active', 'is-repeat-one'); } } // Volume button (mute toggle) if (e.target.closest('.fedistream-player__btn--volume')) { e.preventDefault(); this.toggleMute(); } }); // Seek slider document.addEventListener('input', (e) => { if (e.target.classList.contains('fedistream-player__seek')) { const value = e.target.value; const duration = this.audio.duration || 0; this.audio.currentTime = (value / 100) * duration; } // Volume slider if (e.target.classList.contains('fedistream-player__volume-slider')) { const value = e.target.value / 100; this.setVolume(value); } }); // Click on progress bar document.addEventListener('click', (e) => { if (e.target.classList.contains('fedistream-player__bar')) { const rect = e.target.getBoundingClientRect(); const percent = (e.clientX - rect.left) / rect.width; const duration = this.audio.duration || 0; this.audio.currentTime = percent * duration; } }); // Queue item click (in multi-track player) document.addEventListener('click', (e) => { const queueItem = e.target.closest('.fedistream-tracklist--queue .fedistream-tracklist__item'); if (queueItem && !e.target.closest('a')) { e.preventDefault(); const index = parseInt(queueItem.dataset.trackIndex, 10); if (!isNaN(index)) { this.playAtIndex(index); } } }); // Now playing widget controls document.addEventListener('click', (e) => { if (e.target.closest('.fedistream-now-playing__btn--play')) { e.preventDefault(); this.togglePlayPause(); } if (e.target.closest('.fedistream-now-playing__btn--prev')) { e.preventDefault(); this.previous(); } if (e.target.closest('.fedistream-now-playing__btn--next')) { e.preventDefault(); this.next(); } }); } /** * Initialize inline players */ initInlinePlayers() { const players = document.querySelectorAll('.fedistream-player[data-track-id][data-audio-url]'); players.forEach(player => { const trackId = player.dataset.trackId; const audioUrl = player.dataset.audioUrl; // Store reference for later player._fedistream = { trackId, audioUrl }; }); } /** * Initialize multi-track players */ initMultiTrackPlayers() { const players = document.querySelectorAll('.fedistream-player--multi[data-tracks]'); players.forEach(player => { try { const tracks = JSON.parse(player.dataset.tracks); player._fedistream = { tracks }; } catch (e) { console.error('Failed to parse tracks data', e); } }); } /** * Initialize now playing widgets */ initNowPlayingWidgets() { this.nowPlayingWidgets = document.querySelectorAll('[data-widget="now-playing"]'); } /** * Play a track by ID */ async playTrackById(trackId) { // Check if we have track data in the DOM const trackElement = document.querySelector(`[data-track-id="${trackId}"]`); let audioUrl = null; // Try to get audio URL from inline player const inlinePlayer = document.querySelector(`.fedistream-player[data-track-id="${trackId}"]`); if (inlinePlayer && inlinePlayer.dataset.audioUrl) { audioUrl = inlinePlayer.dataset.audioUrl; } // If no audio URL, try to fetch from API if (!audioUrl) { const trackData = await this.fetchTrackData(trackId); if (trackData && trackData.audio_url) { audioUrl = trackData.audio_url; } } if (!audioUrl) { console.error('No audio URL found for track', trackId); return; } // Build track object const track = { id: trackId, audio_url: audioUrl, title: this.getTrackTitle(trackId), artist: this.getTrackArtist(trackId), thumbnail: this.getTrackThumbnail(trackId) }; // Clear queue and play single track this.clearQueue(); this.addToQueue(track); this.playAtIndex(0); } /** * Fetch track data from API */ async fetchTrackData(trackId) { if (!window.wpFediStream || !window.wpFediStream.ajaxUrl) { return null; } try { const formData = new FormData(); formData.append('action', 'fedistream_get_track'); formData.append('track_id', trackId); formData.append('nonce', window.wpFediStream.nonce); const response = await fetch(window.wpFediStream.ajaxUrl, { method: 'POST', body: formData }); const data = await response.json(); if (data.success) { return data.data; } } catch (e) { console.error('Failed to fetch track data', e); } return null; } /** * Get track title from DOM */ getTrackTitle(trackId) { const el = document.querySelector(`[data-track-id="${trackId}"] .fedistream-tracklist__title, [data-track-id="${trackId}"] .fedistream-card__title`); return el ? el.textContent.trim() : 'Unknown Track'; } /** * Get track artist from DOM */ getTrackArtist(trackId) { const el = document.querySelector(`[data-track-id="${trackId}"] .fedistream-tracklist__artist, [data-track-id="${trackId}"] .fedistream-card__artist`); return el ? el.textContent.trim() : 'Unknown Artist'; } /** * Get track thumbnail from DOM */ getTrackThumbnail(trackId) { const el = document.querySelector(`[data-track-id="${trackId}"] .fedistream-tracklist__artwork, [data-track-id="${trackId}"] .fedistream-card__image img`); return el ? el.src : ''; } /** * Play album */ async playAlbum(albumId, shuffle = false) { const tracklist = document.querySelector(`.fedistream-btn--play-all[data-album-id="${albumId}"]`)?.closest('.fedistream-single')?.querySelector('.fedistream-tracklist'); if (tracklist) { this.playTracklist(tracklist, shuffle); } } /** * Play playlist */ async playPlaylist(playlistId, shuffle = false) { const tracklist = document.querySelector(`.fedistream-btn--play-all[data-playlist-id="${playlistId}"]`)?.closest('.fedistream-single')?.querySelector('.fedistream-tracklist'); if (tracklist) { this.playTracklist(tracklist, shuffle); } } /** * Play all tracks from a tracklist element */ playTracklist(tracklistElement, shuffle = false) { const items = tracklistElement.querySelectorAll('.fedistream-tracklist__item[data-track-id]'); const tracks = []; items.forEach(item => { const trackId = item.dataset.trackId; const titleEl = item.querySelector('.fedistream-tracklist__title'); const artistEl = item.querySelector('.fedistream-tracklist__artist'); const artworkEl = item.querySelector('.fedistream-tracklist__artwork'); tracks.push({ id: trackId, title: titleEl ? titleEl.textContent.trim() : 'Unknown Track', artist: artistEl ? artistEl.textContent.trim() : '', thumbnail: artworkEl ? artworkEl.src : '' }); }); if (tracks.length === 0) return; this.clearQueue(); tracks.forEach(track => this.addToQueue(track)); if (shuffle) { this.shuffleQueue(); } this.playAtIndex(0); } /** * Add track to queue */ addToQueue(track) { this.queue.push(track); this.originalQueue.push(track); } /** * Clear queue */ clearQueue() { this.queue = []; this.originalQueue = []; this.currentIndex = -1; } /** * Shuffle queue */ shuffleQueue() { const currentTrack = this.currentIndex >= 0 ? this.queue[this.currentIndex] : null; // Fisher-Yates shuffle for (let i = this.queue.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [this.queue[i], this.queue[j]] = [this.queue[j], this.queue[i]]; } // Keep current track at current position if playing if (currentTrack) { const newIndex = this.queue.indexOf(currentTrack); if (newIndex !== this.currentIndex) { [this.queue[this.currentIndex], this.queue[newIndex]] = [this.queue[newIndex], this.queue[this.currentIndex]]; } } this.isShuffle = true; } /** * Restore original queue order */ restoreQueueOrder() { const currentTrack = this.currentIndex >= 0 ? this.queue[this.currentIndex] : null; this.queue = [...this.originalQueue]; if (currentTrack) { this.currentIndex = this.queue.findIndex(t => t.id === currentTrack.id); } this.isShuffle = false; } /** * Play track at index */ async playAtIndex(index) { if (index < 0 || index >= this.queue.length) return; const track = this.queue[index]; this.currentIndex = index; // Get audio URL if not present if (!track.audio_url) { const data = await this.fetchTrackData(track.id); if (data && data.audio_url) { track.audio_url = data.audio_url; } else { console.error('Could not load audio for track', track.id); this.next(); return; } } this.audio.src = track.audio_url; this.audio.load(); this.audio.play().catch(e => { console.error('Playback failed', e); }); this.updateNowPlaying(track); this.updateTracklistHighlight(); this.recordPlay(track.id); } /** * Toggle play/pause */ togglePlayPause() { if (this.isPlaying) { this.audio.pause(); } else { if (this.audio.src) { this.audio.play().catch(e => console.error('Playback failed', e)); } else if (this.queue.length > 0) { this.playAtIndex(this.currentIndex >= 0 ? this.currentIndex : 0); } } } /** * Play next track */ next() { if (this.queue.length === 0) return; let nextIndex = this.currentIndex + 1; if (nextIndex >= this.queue.length) { if (this.repeatMode === 'all') { nextIndex = 0; } else { return; } } this.playAtIndex(nextIndex); } /** * Play previous track */ previous() { if (this.queue.length === 0) return; // If more than 3 seconds into track, restart it if (this.audio.currentTime > 3) { this.audio.currentTime = 0; return; } let prevIndex = this.currentIndex - 1; if (prevIndex < 0) { if (this.repeatMode === 'all') { prevIndex = this.queue.length - 1; } else { this.audio.currentTime = 0; return; } } this.playAtIndex(prevIndex); } /** * Toggle shuffle mode */ toggleShuffle() { if (this.isShuffle) { this.restoreQueueOrder(); } else { this.shuffleQueue(); } } /** * Toggle repeat mode */ toggleRepeat() { const modes = ['none', 'all', 'one']; const currentModeIndex = modes.indexOf(this.repeatMode); this.repeatMode = modes[(currentModeIndex + 1) % modes.length]; } /** * Set volume */ setVolume(value) { this.volume = Math.max(0, Math.min(1, value)); this.audio.volume = this.volume; this.audio.muted = false; this.saveVolumeToStorage(); this.updateVolumeUI(); } /** * Toggle mute */ toggleMute() { this.audio.muted = !this.audio.muted; this.updateVolumeUI(); } /** * Save volume to localStorage */ saveVolumeToStorage() { try { localStorage.setItem('fedistream_volume', this.volume.toString()); } catch (e) { // Storage not available } } /** * Load volume from localStorage */ loadVolumeFromStorage() { try { const stored = localStorage.getItem('fedistream_volume'); if (stored !== null) { this.volume = parseFloat(stored); this.audio.volume = this.volume; this.updateVolumeUI(); } } catch (e) { // Storage not available } } /** * Update volume UI */ updateVolumeUI() { const sliders = document.querySelectorAll('.fedistream-player__volume-slider'); sliders.forEach(slider => { slider.value = this.audio.muted ? 0 : this.volume * 100; }); } /** * Format time in mm:ss */ formatTime(seconds) { if (isNaN(seconds) || !isFinite(seconds)) return '0:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } /** * Update now playing info in all widgets */ updateNowPlaying(track) { // Update all now playing widgets this.nowPlayingWidgets.forEach(widget => { const idle = widget.querySelector('.fedistream-now-playing__idle'); const active = widget.querySelector('.fedistream-now-playing__active'); if (idle) idle.style.display = 'none'; if (active) active.style.display = 'block'; const artwork = widget.querySelector('.fedistream-now-playing__artwork'); const title = widget.querySelector('.fedistream-now-playing__title'); const artist = widget.querySelector('.fedistream-now-playing__artist'); if (artwork && track.thumbnail) artwork.src = track.thumbnail; if (title) title.textContent = track.title; if (artist) artist.textContent = track.artist; }); // Update multi-track player current info const multiPlayers = document.querySelectorAll('.fedistream-player--multi'); multiPlayers.forEach(player => { const artwork = player.querySelector('.fedistream-player__artwork--current'); const title = player.querySelector('.fedistream-player__title--current'); const artist = player.querySelector('.fedistream-player__artist--current'); if (artwork && track.thumbnail) artwork.src = track.thumbnail; if (title) title.textContent = track.title; if (artist) artist.textContent = track.artist; }); // Update document title if (track.title) { document.title = `${track.title}${track.artist ? ' - ' + track.artist : ''} | ${document.title.split('|').pop().trim()}`; } } /** * Update tracklist item highlighting */ updateTracklistHighlight() { // Remove all highlights document.querySelectorAll('.fedistream-tracklist__item.is-playing').forEach(el => { el.classList.remove('is-playing'); }); // Add highlight to current track if (this.currentIndex >= 0 && this.queue[this.currentIndex]) { const trackId = this.queue[this.currentIndex].id; document.querySelectorAll(`.fedistream-tracklist__item[data-track-id="${trackId}"]`).forEach(el => { el.classList.add('is-playing'); }); } } /** * Record track play via AJAX */ recordPlay(trackId) { if (!window.wpFediStream || !window.wpFediStream.ajaxUrl) return; const formData = new FormData(); formData.append('action', 'fedistream_record_play'); formData.append('track_id', trackId); formData.append('nonce', window.wpFediStream.nonce); fetch(window.wpFediStream.ajaxUrl, { method: 'POST', body: formData }).catch(e => console.error('Failed to record play', e)); } // Audio event handlers onTimeUpdate() { const current = this.audio.currentTime; const duration = this.audio.duration || 0; const percent = duration ? (current / duration) * 100 : 0; // Update progress bars document.querySelectorAll('.fedistream-player__bar-progress').forEach(el => { el.style.width = `${percent}%`; }); document.querySelectorAll('.fedistream-player__seek').forEach(el => { el.value = percent; }); // Update time displays document.querySelectorAll('.fedistream-player__time--current').forEach(el => { el.textContent = this.formatTime(current); }); // Update now playing widgets progress this.nowPlayingWidgets.forEach(widget => { const progress = widget.querySelector('.fedistream-now-playing__bar-progress'); const currentTime = widget.querySelector('.fedistream-now-playing__time--current'); const totalTime = widget.querySelector('.fedistream-now-playing__time--total'); if (progress) progress.style.width = `${percent}%`; if (currentTime) currentTime.textContent = this.formatTime(current); if (totalTime) totalTime.textContent = this.formatTime(duration); }); } onLoadedMetadata() { const duration = this.audio.duration || 0; document.querySelectorAll('.fedistream-player__time--total').forEach(el => { el.textContent = this.formatTime(duration); }); } onEnded() { if (this.repeatMode === 'one') { this.audio.currentTime = 0; this.audio.play().catch(e => console.error('Playback failed', e)); } else { this.next(); } } onPlay() { this.isPlaying = true; // Update all player UIs document.querySelectorAll('.fedistream-player').forEach(el => { el.classList.add('is-playing'); }); document.querySelectorAll('.fedistream-now-playing').forEach(el => { el.classList.add('is-playing'); }); } onPause() { this.isPlaying = false; document.querySelectorAll('.fedistream-player').forEach(el => { el.classList.remove('is-playing'); }); document.querySelectorAll('.fedistream-now-playing').forEach(el => { el.classList.remove('is-playing'); }); } onError(e) { console.error('Audio error', e); // Try next track on error if (this.queue.length > 1) { this.next(); } } onWaiting() { document.querySelectorAll('.fedistream-player').forEach(el => { el.classList.add('is-loading'); }); } onCanPlay() { document.querySelectorAll('.fedistream-player').forEach(el => { el.classList.remove('is-loading'); }); } } /** * Initialize when DOM is ready */ document.addEventListener('DOMContentLoaded', function() { // Initialize the global player window.fediStreamPlayer = new FediStreamPlayer(); // Initialize any inline players window.fediStreamPlayer.initInlinePlayers(); window.fediStreamPlayer.initMultiTrackPlayers(); }); // Media Session API for system controls if ('mediaSession' in navigator) { navigator.mediaSession.setActionHandler('play', () => { if (window.fediStreamPlayer) { window.fediStreamPlayer.togglePlayPause(); } }); navigator.mediaSession.setActionHandler('pause', () => { if (window.fediStreamPlayer) { window.fediStreamPlayer.togglePlayPause(); } }); navigator.mediaSession.setActionHandler('previoustrack', () => { if (window.fediStreamPlayer) { window.fediStreamPlayer.previous(); } }); navigator.mediaSession.setActionHandler('nexttrack', () => { if (window.fediStreamPlayer) { window.fediStreamPlayer.next(); } }); } })();