You've already forked wp-fedistream
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>
555 lines
14 KiB
JavaScript
555 lines
14 KiB
JavaScript
/**
|
|
* 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 );
|