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:
2026-01-28 23:23:05 +01:00
commit 4a5d7b9f4d
91 changed files with 22750 additions and 0 deletions

7
assets/css/admin.css Normal file
View 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

File diff suppressed because it is too large Load Diff

1
assets/css/index.php Normal file
View File

@@ -0,0 +1 @@
<?php // Silence is golden.

1
assets/images/index.php Normal file
View File

@@ -0,0 +1 @@
<?php // Silence is golden.

1
assets/index.php Normal file
View File

@@ -0,0 +1 @@
<?php // Silence is golden.

14
assets/js/admin.js Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
<?php // Silence is golden.

554
assets/js/library.js Normal file
View 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 ) + '">&laquo; 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 &raquo;</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
View 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 );