Files
wp-fedistream/assets/js/frontend.js

840 lines
29 KiB
JavaScript
Raw Permalink Normal View History

/**
* 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();
}
});
}
})();