You've already forked wp-fedistream
feat: Initial release v0.1.0
WP FediStream - Stream music over ActivityPub Features: - Custom post types: Artist, Album, Track, Playlist - Custom taxonomies: Genre, Mood, License - User roles: Artist, Label - Admin dashboard with statistics - Frontend templates and shortcodes - Audio player with queue management - ActivityPub integration with actor support - WooCommerce product types for albums/tracks - User library with favorites and history - Notification system (in-app and email) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
7
assets/css/admin.css
Normal file
7
assets/css/admin.css
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* WP FediStream - Admin Styles
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
/* Admin styles will be added here */
|
||||
1408
assets/css/frontend.css
Normal file
1408
assets/css/frontend.css
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/css/index.php
Normal file
1
assets/css/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
1
assets/images/index.php
Normal file
1
assets/images/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
1
assets/index.php
Normal file
1
assets/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
14
assets/js/admin.js
Normal file
14
assets/js/admin.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* WP FediStream - Admin Scripts
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
$(document).ready(function() {
|
||||
// Admin scripts will be added here
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
839
assets/js/frontend.js
Normal file
839
assets/js/frontend.js
Normal file
@@ -0,0 +1,839 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
1
assets/js/index.php
Normal file
1
assets/js/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
554
assets/js/library.js
Normal file
554
assets/js/library.js
Normal file
@@ -0,0 +1,554 @@
|
||||
/**
|
||||
* FediStream Library JavaScript
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
( function( $ ) {
|
||||
'use strict';
|
||||
|
||||
const Library = {
|
||||
currentTab: 'favorites',
|
||||
currentPage: { favorites: 1, artists: 1, history: 1 },
|
||||
currentFilter: 'all',
|
||||
isLoading: false,
|
||||
|
||||
init: function() {
|
||||
this.bindEvents();
|
||||
this.loadInitialTab();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
const self = this;
|
||||
|
||||
// Tab navigation.
|
||||
$( '.fedistream-library-nav .tab-btn' ).on( 'click', function() {
|
||||
const tab = $( this ).data( 'tab' );
|
||||
self.switchTab( tab );
|
||||
} );
|
||||
|
||||
// Filter change.
|
||||
$( '.fedistream-library-filters .filter-type' ).on( 'change', function() {
|
||||
self.currentFilter = $( this ).val();
|
||||
self.currentPage.favorites = 1;
|
||||
self.loadFavorites();
|
||||
} );
|
||||
|
||||
// Clear history.
|
||||
$( '.btn-clear-history' ).on( 'click', function() {
|
||||
self.clearHistory();
|
||||
} );
|
||||
|
||||
// Pagination clicks.
|
||||
$( document ).on( 'click', '.library-pagination .page-btn', function() {
|
||||
const page = $( this ).data( 'page' );
|
||||
const tab = $( this ).closest( '.library-pagination' ).data( 'tab' );
|
||||
self.goToPage( tab, page );
|
||||
} );
|
||||
|
||||
// Unfavorite button.
|
||||
$( document ).on( 'click', '.unfavorite-btn', function() {
|
||||
const btn = $( this );
|
||||
const contentType = btn.data( 'content-type' );
|
||||
const contentId = btn.data( 'content-id' );
|
||||
self.toggleFavorite( contentType, contentId, btn.closest( '.library-item' ) );
|
||||
} );
|
||||
|
||||
// Unfollow button.
|
||||
$( document ).on( 'click', '.unfollow-btn', function() {
|
||||
const btn = $( this );
|
||||
const artistId = btn.data( 'artist-id' );
|
||||
self.toggleFollow( artistId, btn.closest( '.library-item' ) );
|
||||
} );
|
||||
|
||||
// Play button.
|
||||
$( document ).on( 'click', '.play-btn', function( e ) {
|
||||
e.preventDefault();
|
||||
const trackId = $( this ).data( 'track-id' );
|
||||
self.playTrack( trackId );
|
||||
} );
|
||||
},
|
||||
|
||||
loadInitialTab: function() {
|
||||
const initialTab = $( '.fedistream-library' ).data( 'initial-tab' ) || 'favorites';
|
||||
this.switchTab( initialTab );
|
||||
},
|
||||
|
||||
switchTab: function( tab ) {
|
||||
this.currentTab = tab;
|
||||
|
||||
// Update nav.
|
||||
$( '.fedistream-library-nav .tab-btn' ).removeClass( 'active' );
|
||||
$( '.fedistream-library-nav .tab-btn[data-tab="' + tab + '"]' ).addClass( 'active' );
|
||||
|
||||
// Update content.
|
||||
$( '.tab-content' ).removeClass( 'active' );
|
||||
$( '#tab-' + tab ).addClass( 'active' );
|
||||
|
||||
// Show/hide filters.
|
||||
if ( tab === 'favorites' ) {
|
||||
$( '.fedistream-library-filters' ).show();
|
||||
} else {
|
||||
$( '.fedistream-library-filters' ).hide();
|
||||
}
|
||||
|
||||
// Load content.
|
||||
this.loadTabContent( tab );
|
||||
},
|
||||
|
||||
loadTabContent: function( tab ) {
|
||||
switch ( tab ) {
|
||||
case 'favorites':
|
||||
this.loadFavorites();
|
||||
break;
|
||||
case 'artists':
|
||||
this.loadArtists();
|
||||
break;
|
||||
case 'history':
|
||||
this.loadHistory();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
loadFavorites: function() {
|
||||
const self = this;
|
||||
|
||||
if ( this.isLoading ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLoading();
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamLibrary.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_get_library',
|
||||
nonce: fedistreamLibrary.nonce,
|
||||
type: this.currentFilter,
|
||||
page: this.currentPage.favorites
|
||||
},
|
||||
success: function( response ) {
|
||||
self.hideLoading();
|
||||
|
||||
if ( response.success ) {
|
||||
self.renderFavorites( response.data );
|
||||
} else {
|
||||
self.showError( response.data.message );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.hideLoading();
|
||||
self.showError( fedistreamLibrary.i18n.error );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
loadArtists: function() {
|
||||
const self = this;
|
||||
|
||||
if ( this.isLoading ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLoading();
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamLibrary.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_get_followed_artists',
|
||||
nonce: fedistreamLibrary.nonce,
|
||||
page: this.currentPage.artists
|
||||
},
|
||||
success: function( response ) {
|
||||
self.hideLoading();
|
||||
|
||||
if ( response.success ) {
|
||||
self.renderArtists( response.data );
|
||||
} else {
|
||||
self.showError( response.data.message );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.hideLoading();
|
||||
self.showError( fedistreamLibrary.i18n.error );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
loadHistory: function() {
|
||||
const self = this;
|
||||
|
||||
if ( this.isLoading ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLoading();
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamLibrary.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_get_history',
|
||||
nonce: fedistreamLibrary.nonce,
|
||||
page: this.currentPage.history
|
||||
},
|
||||
success: function( response ) {
|
||||
self.hideLoading();
|
||||
|
||||
if ( response.success ) {
|
||||
self.renderHistory( response.data );
|
||||
} else {
|
||||
self.showError( response.data.message );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.hideLoading();
|
||||
self.showError( fedistreamLibrary.i18n.error );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
renderFavorites: function( data ) {
|
||||
const container = $( '.favorites-grid' );
|
||||
container.empty();
|
||||
|
||||
if ( ! data.items || data.items.length === 0 ) {
|
||||
container.html( '<p class="empty-message">' + fedistreamLibrary.i18n.noFavorites + '</p>' );
|
||||
$( '.library-pagination[data-tab="favorites"]' ).empty();
|
||||
return;
|
||||
}
|
||||
|
||||
data.items.forEach( function( item ) {
|
||||
container.append( this.createFavoriteItem( item ) );
|
||||
}, this );
|
||||
|
||||
this.renderPagination( 'favorites', data );
|
||||
},
|
||||
|
||||
renderArtists: function( data ) {
|
||||
const container = $( '.artists-grid' );
|
||||
container.empty();
|
||||
|
||||
if ( ! data.artists || data.artists.length === 0 ) {
|
||||
container.html( '<p class="empty-message">' + fedistreamLibrary.i18n.noArtists + '</p>' );
|
||||
$( '.library-pagination[data-tab="artists"]' ).empty();
|
||||
return;
|
||||
}
|
||||
|
||||
data.artists.forEach( function( artist ) {
|
||||
container.append( this.createArtistItem( artist ) );
|
||||
}, this );
|
||||
|
||||
this.renderPagination( 'artists', data );
|
||||
},
|
||||
|
||||
renderHistory: function( data ) {
|
||||
const container = $( '.history-list' );
|
||||
container.empty();
|
||||
|
||||
if ( ! data.tracks || data.tracks.length === 0 ) {
|
||||
container.html( '<p class="empty-message">' + fedistreamLibrary.i18n.noHistory + '</p>' );
|
||||
$( '.library-pagination[data-tab="history"]' ).empty();
|
||||
return;
|
||||
}
|
||||
|
||||
data.tracks.forEach( function( track ) {
|
||||
container.append( this.createHistoryItem( track ) );
|
||||
}, this );
|
||||
|
||||
this.renderPagination( 'history', data );
|
||||
},
|
||||
|
||||
createFavoriteItem: function( item ) {
|
||||
const thumbnail = item.thumbnail
|
||||
? '<img src="' + item.thumbnail + '" alt="' + this.escapeHtml( item.title ) + '">'
|
||||
: '<div class="placeholder-thumbnail"><span class="dashicons dashicons-format-audio"></span></div>';
|
||||
|
||||
const playBtn = item.type === 'track'
|
||||
? '<button class="play-btn" data-track-id="' + item.id + '"><span class="dashicons dashicons-controls-play"></span></button>'
|
||||
: '';
|
||||
|
||||
const artistInfo = item.artist
|
||||
? '<p class="item-artist">' + this.escapeHtml( item.artist ) + '</p>'
|
||||
: '';
|
||||
|
||||
let metaInfo = '';
|
||||
if ( item.type === 'track' && item.duration ) {
|
||||
metaInfo = '<p class="item-duration">' + this.formatDuration( item.duration ) + '</p>';
|
||||
} else if ( item.type === 'album' && item.track_count ) {
|
||||
metaInfo = '<p class="item-tracks">' + item.track_count + ' tracks</p>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="library-item favorite-item" data-type="${item.type}" data-id="${item.id}">
|
||||
<div class="item-thumbnail">
|
||||
${thumbnail}
|
||||
${playBtn}
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-title">
|
||||
<a href="${item.permalink}">${this.escapeHtml( item.title )}</a>
|
||||
</h4>
|
||||
${artistInfo}
|
||||
${metaInfo}
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="unfavorite-btn" data-content-type="${item.type}" data-content-id="${item.id}" title="Remove from library">
|
||||
<span class="dashicons dashicons-heart"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
createArtistItem: function( artist ) {
|
||||
const thumbnail = artist.thumbnail
|
||||
? '<img src="' + artist.thumbnail + '" alt="' + this.escapeHtml( artist.name ) + '">'
|
||||
: '<div class="placeholder-thumbnail"><span class="dashicons dashicons-admin-users"></span></div>';
|
||||
|
||||
const typeLabel = artist.type === 'band' ? 'Band' : 'Artist';
|
||||
|
||||
return `
|
||||
<div class="library-item artist-item" data-id="${artist.id}">
|
||||
<div class="item-thumbnail artist-avatar">
|
||||
${thumbnail}
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-title">
|
||||
<a href="${artist.permalink}">${this.escapeHtml( artist.name )}</a>
|
||||
</h4>
|
||||
<p class="item-type">${typeLabel}</p>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="unfollow-btn" data-artist-id="${artist.id}" title="Unfollow">
|
||||
<span class="dashicons dashicons-minus"></span>
|
||||
<span class="button-text">Unfollow</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
createHistoryItem: function( track ) {
|
||||
const thumbnail = track.thumbnail
|
||||
? '<img src="' + track.thumbnail + '" alt="' + this.escapeHtml( track.title ) + '">'
|
||||
: '<div class="placeholder-thumbnail"><span class="dashicons dashicons-format-audio"></span></div>';
|
||||
|
||||
const artistInfo = track.artist
|
||||
? '<p class="item-artist">' + this.escapeHtml( track.artist ) + '</p>'
|
||||
: '';
|
||||
|
||||
const duration = track.duration
|
||||
? '<span class="item-duration">' + this.formatDuration( track.duration ) + '</span>'
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="library-item history-item" data-id="${track.id}">
|
||||
<div class="item-thumbnail">
|
||||
${thumbnail}
|
||||
<button class="play-btn" data-track-id="${track.id}">
|
||||
<span class="dashicons dashicons-controls-play"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-title">
|
||||
<a href="${track.permalink}">${this.escapeHtml( track.title )}</a>
|
||||
</h4>
|
||||
${artistInfo}
|
||||
<p class="item-played">${this.formatPlayedTime( track.played_at )}</p>
|
||||
</div>
|
||||
<div class="item-meta">
|
||||
${duration}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
renderPagination: function( tab, data ) {
|
||||
const container = $( '.library-pagination[data-tab="' + tab + '"]' );
|
||||
container.empty();
|
||||
|
||||
if ( data.total_pages <= 1 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="pagination-controls">';
|
||||
|
||||
// Previous button.
|
||||
if ( data.page > 1 ) {
|
||||
html += '<button class="page-btn prev" data-page="' + ( data.page - 1 ) + '">« Previous</button>';
|
||||
}
|
||||
|
||||
// Page numbers.
|
||||
html += '<span class="page-info">Page ' + data.page + ' of ' + data.total_pages + '</span>';
|
||||
|
||||
// Next button.
|
||||
if ( data.page < data.total_pages ) {
|
||||
html += '<button class="page-btn next" data-page="' + ( data.page + 1 ) + '">Next »</button>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
container.html( html );
|
||||
},
|
||||
|
||||
goToPage: function( tab, page ) {
|
||||
this.currentPage[ tab ] = page;
|
||||
this.loadTabContent( tab );
|
||||
},
|
||||
|
||||
toggleFavorite: function( contentType, contentId, element ) {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamLibrary.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_toggle_favorite',
|
||||
nonce: fedistreamLibrary.nonce,
|
||||
content_type: contentType,
|
||||
content_id: contentId
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success && response.data.action === 'removed' ) {
|
||||
element.fadeOut( 300, function() {
|
||||
$( this ).remove();
|
||||
self.updateFavoriteCount();
|
||||
} );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.showError( fedistreamLibrary.i18n.error );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
toggleFollow: function( artistId, element ) {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamLibrary.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_toggle_follow',
|
||||
nonce: fedistreamLibrary.nonce,
|
||||
artist_id: artistId
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success && response.data.action === 'unfollowed' ) {
|
||||
element.fadeOut( 300, function() {
|
||||
$( this ).remove();
|
||||
self.updateFollowingCount();
|
||||
} );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.showError( fedistreamLibrary.i18n.error );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
clearHistory: function() {
|
||||
const self = this;
|
||||
|
||||
if ( ! confirm( fedistreamLibrary.i18n.confirmClear ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamLibrary.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_clear_history',
|
||||
nonce: fedistreamLibrary.nonce
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
$( '.history-list' ).html( '<p class="empty-message">' + fedistreamLibrary.i18n.noHistory + '</p>' );
|
||||
$( '.library-pagination[data-tab="history"]' ).empty();
|
||||
} else {
|
||||
self.showError( response.data.message );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.showError( fedistreamLibrary.i18n.error );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
playTrack: function( trackId ) {
|
||||
// Trigger global player if available.
|
||||
if ( window.FediStreamPlayer && typeof window.FediStreamPlayer.playTrack === 'function' ) {
|
||||
window.FediStreamPlayer.playTrack( trackId );
|
||||
} else {
|
||||
// Fallback: redirect to track page.
|
||||
window.location.href = '?p=' + trackId;
|
||||
}
|
||||
},
|
||||
|
||||
updateFavoriteCount: function() {
|
||||
const count = $( '.favorites-grid .library-item' ).length;
|
||||
$( '.tab-btn[data-tab="favorites"] .count' ).text( count );
|
||||
},
|
||||
|
||||
updateFollowingCount: function() {
|
||||
const count = $( '.artists-grid .library-item' ).length;
|
||||
$( '.tab-btn[data-tab="artists"] .count' ).text( count );
|
||||
},
|
||||
|
||||
showLoading: function() {
|
||||
this.isLoading = true;
|
||||
$( '.fedistream-library-loading' ).show();
|
||||
},
|
||||
|
||||
hideLoading: function() {
|
||||
this.isLoading = false;
|
||||
$( '.fedistream-library-loading' ).hide();
|
||||
},
|
||||
|
||||
showError: function( message ) {
|
||||
alert( message );
|
||||
},
|
||||
|
||||
formatDuration: function( seconds ) {
|
||||
const mins = Math.floor( seconds / 60 );
|
||||
const secs = seconds % 60;
|
||||
return mins + ':' + ( secs < 10 ? '0' : '' ) + secs;
|
||||
},
|
||||
|
||||
formatPlayedTime: function( dateString ) {
|
||||
const date = new Date( dateString );
|
||||
const now = new Date();
|
||||
const diff = Math.floor( ( now - date ) / 1000 );
|
||||
|
||||
if ( diff < 60 ) {
|
||||
return 'Just now';
|
||||
} else if ( diff < 3600 ) {
|
||||
const mins = Math.floor( diff / 60 );
|
||||
return mins + ' minute' + ( mins !== 1 ? 's' : '' ) + ' ago';
|
||||
} else if ( diff < 86400 ) {
|
||||
const hours = Math.floor( diff / 3600 );
|
||||
return hours + ' hour' + ( hours !== 1 ? 's' : '' ) + ' ago';
|
||||
} else {
|
||||
const days = Math.floor( diff / 86400 );
|
||||
return days + ' day' + ( days !== 1 ? 's' : '' ) + ' ago';
|
||||
}
|
||||
},
|
||||
|
||||
escapeHtml: function( text ) {
|
||||
const div = document.createElement( 'div' );
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on document ready.
|
||||
$( document ).ready( function() {
|
||||
if ( $( '.fedistream-library' ).length ) {
|
||||
Library.init();
|
||||
}
|
||||
} );
|
||||
|
||||
} )( jQuery );
|
||||
353
assets/js/notifications.js
Normal file
353
assets/js/notifications.js
Normal file
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* FediStream Notifications JavaScript
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
( function( $ ) {
|
||||
'use strict';
|
||||
|
||||
const Notifications = {
|
||||
unreadCount: 0,
|
||||
isOpen: false,
|
||||
pollTimer: null,
|
||||
|
||||
init: function() {
|
||||
this.createDropdown();
|
||||
this.bindEvents();
|
||||
this.loadNotifications();
|
||||
this.startPolling();
|
||||
},
|
||||
|
||||
createDropdown: function() {
|
||||
const dropdown = `
|
||||
<div class="fedistream-notifications-dropdown" style="display: none;">
|
||||
<div class="notifications-header">
|
||||
<h4>${fedistreamNotifications.i18n.viewAll || 'Notifications'}</h4>
|
||||
<button class="mark-all-read" title="${fedistreamNotifications.i18n.markAllRead}">
|
||||
<span class="dashicons dashicons-yes-alt"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="notifications-list">
|
||||
<div class="loading">${fedistreamNotifications.i18n.loading || 'Loading...'}</div>
|
||||
</div>
|
||||
<div class="notifications-footer">
|
||||
<a href="#" class="view-all">${fedistreamNotifications.i18n.viewAll}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$( '.fedistream-notifications-menu' ).append( dropdown );
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
const self = this;
|
||||
|
||||
// Toggle dropdown.
|
||||
$( document ).on( 'click', '#wp-admin-bar-fedistream-notifications > a', function( e ) {
|
||||
e.preventDefault();
|
||||
self.toggleDropdown();
|
||||
} );
|
||||
|
||||
// Close dropdown when clicking outside.
|
||||
$( document ).on( 'click', function( e ) {
|
||||
if ( ! $( e.target ).closest( '.fedistream-notifications-menu' ).length ) {
|
||||
self.closeDropdown();
|
||||
}
|
||||
} );
|
||||
|
||||
// Mark all as read.
|
||||
$( document ).on( 'click', '.fedistream-notifications-dropdown .mark-all-read', function( e ) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
self.markAllRead();
|
||||
} );
|
||||
|
||||
// Mark single as read.
|
||||
$( document ).on( 'click', '.notification-item', function() {
|
||||
const id = $( this ).data( 'id' );
|
||||
const isRead = $( this ).hasClass( 'is-read' );
|
||||
|
||||
if ( ! isRead ) {
|
||||
self.markRead( id, $( this ) );
|
||||
}
|
||||
} );
|
||||
|
||||
// Delete notification.
|
||||
$( document ).on( 'click', '.notification-item .delete-btn', function( e ) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const item = $( this ).closest( '.notification-item' );
|
||||
const id = item.data( 'id' );
|
||||
self.deleteNotification( id, item );
|
||||
} );
|
||||
},
|
||||
|
||||
toggleDropdown: function() {
|
||||
if ( this.isOpen ) {
|
||||
this.closeDropdown();
|
||||
} else {
|
||||
this.openDropdown();
|
||||
}
|
||||
},
|
||||
|
||||
openDropdown: function() {
|
||||
$( '.fedistream-notifications-dropdown' ).show();
|
||||
this.isOpen = true;
|
||||
this.loadNotifications();
|
||||
},
|
||||
|
||||
closeDropdown: function() {
|
||||
$( '.fedistream-notifications-dropdown' ).hide();
|
||||
this.isOpen = false;
|
||||
},
|
||||
|
||||
loadNotifications: function() {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamNotifications.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_get_notifications',
|
||||
nonce: fedistreamNotifications.nonce,
|
||||
limit: 10
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
self.renderNotifications( response.data.notifications );
|
||||
self.updateUnreadCount( response.data.unread_count );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$( '.notifications-list' ).html(
|
||||
'<p class="error">' + fedistreamNotifications.i18n.error + '</p>'
|
||||
);
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
renderNotifications: function( notifications ) {
|
||||
const container = $( '.notifications-list' );
|
||||
container.empty();
|
||||
|
||||
if ( ! notifications || notifications.length === 0 ) {
|
||||
container.html(
|
||||
'<p class="empty">' + fedistreamNotifications.i18n.noNotifications + '</p>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.forEach( function( notification ) {
|
||||
container.append( this.createNotificationItem( notification ) );
|
||||
}, this );
|
||||
},
|
||||
|
||||
createNotificationItem: function( notification ) {
|
||||
const isRead = notification.is_read ? 'is-read' : '';
|
||||
const icon = this.getNotificationIcon( notification.type );
|
||||
const time = this.formatTime( notification.created_at );
|
||||
const link = this.getNotificationLink( notification );
|
||||
|
||||
return `
|
||||
<div class="notification-item ${isRead}" data-id="${notification.id}">
|
||||
<div class="notification-icon">
|
||||
<span class="dashicons dashicons-${icon}"></span>
|
||||
</div>
|
||||
<div class="notification-content">
|
||||
<div class="notification-title">${this.escapeHtml( notification.title )}</div>
|
||||
<div class="notification-message">${this.escapeHtml( notification.message )}</div>
|
||||
<div class="notification-time">${time}</div>
|
||||
</div>
|
||||
<div class="notification-actions">
|
||||
<button class="delete-btn" title="Delete">
|
||||
<span class="dashicons dashicons-no-alt"></span>
|
||||
</button>
|
||||
</div>
|
||||
${link ? `<a href="${link}" class="notification-link"></a>` : ''}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
getNotificationIcon: function( type ) {
|
||||
const icons = {
|
||||
'new_release': 'album',
|
||||
'new_follower': 'admin-users',
|
||||
'fediverse_like': 'heart',
|
||||
'fediverse_boost': 'megaphone',
|
||||
'playlist_added': 'playlist-audio',
|
||||
'purchase': 'cart',
|
||||
'system': 'info'
|
||||
};
|
||||
return icons[ type ] || 'bell';
|
||||
},
|
||||
|
||||
getNotificationLink: function( notification ) {
|
||||
const data = notification.data || {};
|
||||
|
||||
if ( data.album_url ) {
|
||||
return data.album_url;
|
||||
}
|
||||
if ( data.track_url ) {
|
||||
return data.track_url;
|
||||
}
|
||||
if ( data.artist_url ) {
|
||||
return data.artist_url;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
markRead: function( notificationId, element ) {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamNotifications.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_mark_notification_read',
|
||||
nonce: fedistreamNotifications.nonce,
|
||||
notification_id: notificationId
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
element.addClass( 'is-read' );
|
||||
self.updateUnreadCount( response.data.unread_count );
|
||||
}
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
markAllRead: function() {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamNotifications.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_mark_all_notifications_read',
|
||||
nonce: fedistreamNotifications.nonce
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
$( '.notification-item' ).addClass( 'is-read' );
|
||||
self.updateUnreadCount( 0 );
|
||||
}
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
deleteNotification: function( notificationId, element ) {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamNotifications.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_delete_notification',
|
||||
nonce: fedistreamNotifications.nonce,
|
||||
notification_id: notificationId
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
element.fadeOut( 200, function() {
|
||||
$( this ).remove();
|
||||
|
||||
// Check if list is empty.
|
||||
if ( $( '.notification-item' ).length === 0 ) {
|
||||
$( '.notifications-list' ).html(
|
||||
'<p class="empty">' + fedistreamNotifications.i18n.noNotifications + '</p>'
|
||||
);
|
||||
}
|
||||
} );
|
||||
self.updateUnreadCount( response.data.unread_count );
|
||||
}
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
updateUnreadCount: function( count ) {
|
||||
this.unreadCount = count;
|
||||
|
||||
const badge = $( '.fedistream-notification-count' );
|
||||
|
||||
if ( count > 0 ) {
|
||||
if ( badge.length ) {
|
||||
badge.text( count );
|
||||
} else {
|
||||
$( '#wp-admin-bar-fedistream-notifications .ab-icon' ).after(
|
||||
'<span class="fedistream-notification-count">' + count + '</span>'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
badge.remove();
|
||||
}
|
||||
},
|
||||
|
||||
startPolling: function() {
|
||||
const self = this;
|
||||
const interval = fedistreamNotifications.pollInterval || 60000;
|
||||
|
||||
this.pollTimer = setInterval( function() {
|
||||
if ( ! self.isOpen ) {
|
||||
self.checkForNewNotifications();
|
||||
}
|
||||
}, interval );
|
||||
},
|
||||
|
||||
checkForNewNotifications: function() {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamNotifications.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_get_notifications',
|
||||
nonce: fedistreamNotifications.nonce,
|
||||
unread_only: true,
|
||||
limit: 1
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
self.updateUnreadCount( response.data.unread_count );
|
||||
}
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
formatTime: function( dateString ) {
|
||||
const date = new Date( dateString );
|
||||
const now = new Date();
|
||||
const diff = Math.floor( ( now - date ) / 1000 );
|
||||
|
||||
if ( diff < 60 ) {
|
||||
return fedistreamNotifications.i18n.justNow || 'Just now';
|
||||
} else if ( diff < 3600 ) {
|
||||
const mins = Math.floor( diff / 60 );
|
||||
return mins + 'm ago';
|
||||
} else if ( diff < 86400 ) {
|
||||
const hours = Math.floor( diff / 3600 );
|
||||
return hours + 'h ago';
|
||||
} else if ( diff < 604800 ) {
|
||||
const days = Math.floor( diff / 86400 );
|
||||
return days + 'd ago';
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
},
|
||||
|
||||
escapeHtml: function( text ) {
|
||||
const div = document.createElement( 'div' );
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on document ready.
|
||||
$( document ).ready( function() {
|
||||
if ( $( '.fedistream-notifications-menu' ).length ) {
|
||||
Notifications.init();
|
||||
}
|
||||
} );
|
||||
|
||||
} )( jQuery );
|
||||
Reference in New Issue
Block a user