artist blocking
This commit is contained in:
parent
ea005c68ad
commit
f6dae2223f
8 changed files with 759 additions and 49 deletions
97
index.html
97
index.html
|
|
@ -47,6 +47,31 @@
|
|||
<li data-action="track-info" data-type-filter="track">Track info</li>
|
||||
<li data-action="open-original-url" data-type-filter="track">Open original URL</li>
|
||||
<li data-action="download">Download</li>
|
||||
<li class="separator"></li>
|
||||
<li
|
||||
data-action="block-track"
|
||||
data-type-filter="track"
|
||||
data-label-block="Block track"
|
||||
data-label-unblock="Unblock track"
|
||||
>
|
||||
Block track
|
||||
</li>
|
||||
<li
|
||||
data-action="block-album"
|
||||
data-type-filter="album,track"
|
||||
data-label-block="Block album"
|
||||
data-label-unblock="Unblock album"
|
||||
>
|
||||
Block album
|
||||
</li>
|
||||
<li
|
||||
data-action="block-artist"
|
||||
data-type-filter="track,album,artist"
|
||||
data-label-block="Block artist"
|
||||
data-label-unblock="Unblock artist"
|
||||
>
|
||||
Block artist
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
@ -3676,6 +3701,78 @@
|
|||
</div>
|
||||
<ul id="api-instance-list"></ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="setting-item"
|
||||
style="padding-bottom: 1rem; border-top: 1px solid var(--border)"
|
||||
>
|
||||
<div class="info">
|
||||
<span class="label">Blocked Content</span>
|
||||
<span class="description"
|
||||
>Manage artists, albums, and tracks you've blocked from
|
||||
recommendations</span
|
||||
>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem">
|
||||
<button id="manage-blocked-btn" class="btn-secondary">Manage</button>
|
||||
<button
|
||||
id="clear-all-blocked-btn"
|
||||
class="btn-secondary danger"
|
||||
style="display: none"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="blocked-content-list" style="display: none">
|
||||
<div id="blocked-artists-section" style="margin-bottom: 1rem">
|
||||
<h4
|
||||
style="
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--muted-foreground);
|
||||
"
|
||||
>
|
||||
Blocked Artists
|
||||
</h4>
|
||||
<ul id="blocked-artists-list" class="blocked-items-list"></ul>
|
||||
</div>
|
||||
<div id="blocked-albums-section" style="margin-bottom: 1rem">
|
||||
<h4
|
||||
style="
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--muted-foreground);
|
||||
"
|
||||
>
|
||||
Blocked Albums
|
||||
</h4>
|
||||
<ul id="blocked-albums-list" class="blocked-items-list"></ul>
|
||||
</div>
|
||||
<div id="blocked-tracks-section" style="margin-bottom: 1rem">
|
||||
<h4
|
||||
style="
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--muted-foreground);
|
||||
"
|
||||
>
|
||||
Blocked Tracks
|
||||
</h4>
|
||||
<ul id="blocked-tracks-list" class="blocked-items-list"></ul>
|
||||
</div>
|
||||
<div
|
||||
id="blocked-empty-message"
|
||||
style="
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
color: var(--muted-foreground);
|
||||
display: none;
|
||||
"
|
||||
>
|
||||
No blocked content
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
106
js/events.js
106
js/events.js
|
|
@ -726,6 +726,13 @@ export async function handleTrackAction(
|
|||
|
||||
if (isCollection && collectionActions.includes(action)) {
|
||||
try {
|
||||
// Check if album/artist is blocked
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
if (type === 'album' && contentBlockingSettings.shouldHideAlbum(item)) {
|
||||
showNotification('This album is blocked');
|
||||
return;
|
||||
}
|
||||
|
||||
let tracks = [];
|
||||
let collectionItem = item;
|
||||
|
||||
|
|
@ -780,6 +787,9 @@ export async function handleTrackAction(
|
|||
return;
|
||||
}
|
||||
|
||||
// Filter blocked tracks from collections
|
||||
tracks = contentBlockingSettings.filterTracks(tracks);
|
||||
|
||||
if (action === 'add-to-queue') {
|
||||
player.addToQueue(tracks);
|
||||
if (window.renderQueueFunction) window.renderQueueFunction();
|
||||
|
|
@ -835,6 +845,13 @@ export async function handleTrackAction(
|
|||
}
|
||||
|
||||
// Individual Track Actions
|
||||
// Check if track/artist is blocked
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
if (type === 'track' && contentBlockingSettings.shouldHideTrack(item)) {
|
||||
showNotification('This track is blocked');
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'add-to-queue') {
|
||||
player.addToQueue(item);
|
||||
if (window.renderQueueFunction) window.renderQueueFunction();
|
||||
|
|
@ -1242,6 +1259,57 @@ export async function handleTrackAction(
|
|||
} else {
|
||||
showNotification('No original URL available for this track.');
|
||||
}
|
||||
} else if (action === 'block-track') {
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
if (contentBlockingSettings.isTrackBlocked(item.id)) {
|
||||
contentBlockingSettings.unblockTrack(item.id);
|
||||
showNotification(`Unblocked track: ${item.title}`);
|
||||
} else {
|
||||
contentBlockingSettings.blockTrack(item);
|
||||
showNotification(`Blocked track: ${item.title}`);
|
||||
}
|
||||
} else if (action === 'block-album') {
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
const albumId = type === 'album' ? item.id : item.album?.id;
|
||||
const albumTitle = type === 'album' ? item.title : item.album?.title;
|
||||
const albumArtist = type === 'album' ? item.artist : item.album?.artist;
|
||||
|
||||
if (!albumId) {
|
||||
showNotification('No album information available');
|
||||
return;
|
||||
}
|
||||
|
||||
if (contentBlockingSettings.isAlbumBlocked(albumId)) {
|
||||
contentBlockingSettings.unblockAlbum(albumId);
|
||||
showNotification(`Unblocked album: ${albumTitle || 'Unknown Album'}`);
|
||||
} else {
|
||||
contentBlockingSettings.blockAlbum({
|
||||
id: albumId,
|
||||
title: albumTitle,
|
||||
artist: albumArtist,
|
||||
});
|
||||
showNotification(`Blocked album: ${albumTitle || 'Unknown Album'}`);
|
||||
}
|
||||
} else if (action === 'block-artist') {
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
const artistId = item.artist?.id || item.artists?.[0]?.id;
|
||||
const artistName = item.artist?.name || item.artists?.[0]?.name || item.name;
|
||||
|
||||
if (!artistId) {
|
||||
showNotification('No artist information available');
|
||||
return;
|
||||
}
|
||||
|
||||
if (contentBlockingSettings.isArtistBlocked(artistId)) {
|
||||
contentBlockingSettings.unblockArtist(artistId);
|
||||
showNotification(`Unblocked artist: ${artistName || 'Unknown Artist'}`);
|
||||
} else {
|
||||
contentBlockingSettings.blockArtist({
|
||||
id: artistId,
|
||||
name: artistName,
|
||||
});
|
||||
showNotification(`Blocked artist: ${artistName || 'Unknown Artist'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1267,8 +1335,37 @@ async function updateContextMenuLikeState(contextMenu, contextTrack) {
|
|||
openOriginalUrlItem.style.display = isUnreleased ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Filter items based on type
|
||||
// Update block/unblock labels
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
const type = contextMenu._contextType || 'track';
|
||||
|
||||
const blockTrackItem = contextMenu.querySelector('li[data-action="block-track"]');
|
||||
if (blockTrackItem) {
|
||||
const isBlocked = contentBlockingSettings.isTrackBlocked(contextTrack.id);
|
||||
blockTrackItem.textContent = isBlocked
|
||||
? blockTrackItem.dataset.labelUnblock || 'Unblock track'
|
||||
: blockTrackItem.dataset.labelBlock || 'Block track';
|
||||
}
|
||||
|
||||
const blockAlbumItem = contextMenu.querySelector('li[data-action="block-album"]');
|
||||
if (blockAlbumItem) {
|
||||
const albumId = type === 'album' ? contextTrack.id : contextTrack.album?.id;
|
||||
const isBlocked = albumId ? contentBlockingSettings.isAlbumBlocked(albumId) : false;
|
||||
blockAlbumItem.textContent = isBlocked
|
||||
? blockAlbumItem.dataset.labelUnblock || 'Unblock album'
|
||||
: blockAlbumItem.dataset.labelBlock || 'Block album';
|
||||
}
|
||||
|
||||
const blockArtistItem = contextMenu.querySelector('li[data-action="block-artist"]');
|
||||
if (blockArtistItem) {
|
||||
const artistId = contextTrack.artist?.id || contextTrack.artists?.[0]?.id;
|
||||
const isBlocked = artistId ? contentBlockingSettings.isArtistBlocked(artistId) : false;
|
||||
blockArtistItem.textContent = isBlocked
|
||||
? blockArtistItem.dataset.labelUnblock || 'Unblock artist'
|
||||
: blockArtistItem.dataset.labelBlock || 'Block artist';
|
||||
}
|
||||
|
||||
// Filter items based on type
|
||||
contextMenu.querySelectorAll('li[data-action]').forEach((item) => {
|
||||
const filter = item.dataset.typeFilter;
|
||||
if (filter) {
|
||||
|
|
@ -1389,7 +1486,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
}
|
||||
|
||||
const trackItem = e.target.closest('.track-item');
|
||||
if (trackItem && trackItem.classList.contains('unavailable')) {
|
||||
if (trackItem && (trackItem.classList.contains('unavailable') || trackItem.classList.contains('blocked'))) {
|
||||
return;
|
||||
}
|
||||
if (trackItem && !trackItem.dataset.queueIndex && !e.target.closest('.remove-from-playlist-btn')) {
|
||||
|
|
@ -1409,6 +1506,11 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
|
||||
const card = e.target.closest('.card');
|
||||
if (card) {
|
||||
// Don't navigate if card is blocked (unless clicking menu button)
|
||||
if (card.classList.contains('blocked') && !e.target.closest('.card-menu-btn')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.closest('.edit-playlist-btn') || e.target.closest('.delete-playlist-btn')) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
76
js/player.js
76
js/player.js
|
|
@ -327,6 +327,14 @@ export class Player {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if track is blocked
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
if (contentBlockingSettings.shouldHideTrack(track)) {
|
||||
console.warn(`Attempted to play blocked track: ${track.title}. Skipping...`);
|
||||
this.playNext();
|
||||
return;
|
||||
}
|
||||
|
||||
this.saveQueueState();
|
||||
|
||||
this.currentTrack = track;
|
||||
|
|
@ -574,33 +582,42 @@ export class Player {
|
|||
const isLastTrack = this.currentQueueIndex >= currentQueue.length - 1;
|
||||
|
||||
if (recursiveCount > currentQueue.length) {
|
||||
console.error('All tracks in queue are unavailable.');
|
||||
console.error('All tracks in queue are unavailable or blocked.');
|
||||
this.audio.pause();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.repeatMode === REPEAT_MODE.ONE && !currentQueue[this.currentQueueIndex]?.isUnavailable) {
|
||||
// Import blocking settings dynamically
|
||||
import('./storage.js').then(({ contentBlockingSettings }) => {
|
||||
if (
|
||||
this.repeatMode === REPEAT_MODE.ONE &&
|
||||
!currentQueue[this.currentQueueIndex]?.isUnavailable &&
|
||||
!contentBlockingSettings.shouldHideTrack(currentQueue[this.currentQueueIndex])
|
||||
) {
|
||||
this.playTrackFromQueue(0, recursiveCount);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLastTrack) {
|
||||
this.currentQueueIndex++;
|
||||
const track = currentQueue[this.currentQueueIndex];
|
||||
// Skip unavailable and blocked tracks
|
||||
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
|
||||
return this.playNext(recursiveCount + 1);
|
||||
}
|
||||
} else if (this.repeatMode === REPEAT_MODE.ALL) {
|
||||
this.currentQueueIndex = 0;
|
||||
const track = currentQueue[this.currentQueueIndex];
|
||||
// Skip unavailable and blocked tracks
|
||||
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
|
||||
return this.playNext(recursiveCount + 1);
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this.playTrackFromQueue(0, recursiveCount);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLastTrack) {
|
||||
this.currentQueueIndex++;
|
||||
// Skip unavailable tracks
|
||||
if (currentQueue[this.currentQueueIndex].isUnavailable) {
|
||||
return this.playNext(recursiveCount + 1);
|
||||
}
|
||||
} else if (this.repeatMode === REPEAT_MODE.ALL) {
|
||||
this.currentQueueIndex = 0;
|
||||
// Skip unavailable tracks
|
||||
if (currentQueue[this.currentQueueIndex].isUnavailable) {
|
||||
return this.playNext(recursiveCount + 1);
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this.playTrackFromQueue(0, recursiveCount);
|
||||
});
|
||||
}
|
||||
|
||||
playPrev(recursiveCount = 0) {
|
||||
|
|
@ -609,19 +626,22 @@ export class Player {
|
|||
this.updateMediaSessionPositionState();
|
||||
} else if (this.currentQueueIndex > 0) {
|
||||
this.currentQueueIndex--;
|
||||
// Skip unavailable tracks
|
||||
// Skip unavailable and blocked tracks
|
||||
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
|
||||
|
||||
if (recursiveCount > currentQueue.length) {
|
||||
console.error('All tracks in queue are unavailable.');
|
||||
console.error('All tracks in queue are unavailable or blocked.');
|
||||
this.audio.pause();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentQueue[this.currentQueueIndex].isUnavailable) {
|
||||
return this.playPrev(recursiveCount + 1);
|
||||
}
|
||||
this.playTrackFromQueue(0, recursiveCount);
|
||||
import('./storage.js').then(({ contentBlockingSettings }) => {
|
||||
const track = currentQueue[this.currentQueueIndex];
|
||||
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
|
||||
return this.playPrev(recursiveCount + 1);
|
||||
}
|
||||
this.playTrackFromQueue(0, recursiveCount);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
139
js/settings.js
139
js/settings.js
|
|
@ -29,6 +29,7 @@ import {
|
|||
audioEffectsSettings,
|
||||
settingsUiState,
|
||||
pwaUpdateSettings,
|
||||
contentBlockingSettings,
|
||||
} from './storage.js';
|
||||
import { audioContextManager, EQ_PRESETS } from './audio-context.js';
|
||||
import { getButterchurnPresets } from './visualizers/butterchurn.js';
|
||||
|
|
@ -1858,6 +1859,9 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
|||
|
||||
// Settings Search functionality
|
||||
setupSettingsSearch();
|
||||
|
||||
// Blocked Content Management
|
||||
initializeBlockedContentManager();
|
||||
}
|
||||
|
||||
function initializeFontSettings() {
|
||||
|
|
@ -2107,3 +2111,138 @@ function filterSettings(query) {
|
|||
group.style.display = hasMatch ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function initializeBlockedContentManager() {
|
||||
const manageBtn = document.getElementById('manage-blocked-btn');
|
||||
const clearAllBtn = document.getElementById('clear-all-blocked-btn');
|
||||
const blockedListContainer = document.getElementById('blocked-content-list');
|
||||
const blockedArtistsList = document.getElementById('blocked-artists-list');
|
||||
const blockedAlbumsList = document.getElementById('blocked-albums-list');
|
||||
const blockedTracksList = document.getElementById('blocked-tracks-list');
|
||||
const blockedArtistsSection = document.getElementById('blocked-artists-section');
|
||||
const blockedAlbumsSection = document.getElementById('blocked-albums-section');
|
||||
const blockedTracksSection = document.getElementById('blocked-tracks-section');
|
||||
const blockedEmptyMessage = document.getElementById('blocked-empty-message');
|
||||
|
||||
if (!manageBtn || !blockedListContainer) return;
|
||||
|
||||
function renderBlockedLists() {
|
||||
const artists = contentBlockingSettings.getBlockedArtists();
|
||||
const albums = contentBlockingSettings.getBlockedAlbums();
|
||||
const tracks = contentBlockingSettings.getBlockedTracks();
|
||||
const totalCount = artists.length + albums.length + tracks.length;
|
||||
|
||||
// Update manage button text
|
||||
manageBtn.textContent = totalCount > 0 ? `Manage (${totalCount})` : 'Manage';
|
||||
|
||||
// Show/hide clear all button
|
||||
if (clearAllBtn) {
|
||||
clearAllBtn.style.display = totalCount > 0 ? 'inline-block' : 'none';
|
||||
}
|
||||
|
||||
// Show/hide sections
|
||||
blockedArtistsSection.style.display = artists.length > 0 ? 'block' : 'none';
|
||||
blockedAlbumsSection.style.display = albums.length > 0 ? 'block' : 'none';
|
||||
blockedTracksSection.style.display = tracks.length > 0 ? 'block' : 'none';
|
||||
blockedEmptyMessage.style.display = totalCount === 0 ? 'block' : 'none';
|
||||
|
||||
// Render artists
|
||||
if (blockedArtistsList) {
|
||||
blockedArtistsList.innerHTML = artists
|
||||
.map(
|
||||
(artist) => `
|
||||
<li data-id="${artist.id}" data-type="artist">
|
||||
<div class="item-info">
|
||||
<div class="item-name">${escapeHtml(artist.name)}</div>
|
||||
<div class="item-meta">${new Date(artist.blockedAt).toLocaleDateString()}</div>
|
||||
</div>
|
||||
<button class="unblock-btn" data-id="${artist.id}" data-type="artist">Unblock</button>
|
||||
</li>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// Render albums
|
||||
if (blockedAlbumsList) {
|
||||
blockedAlbumsList.innerHTML = albums
|
||||
.map(
|
||||
(album) => `
|
||||
<li data-id="${album.id}" data-type="album">
|
||||
<div class="item-info">
|
||||
<div class="item-name">${escapeHtml(album.title)}</div>
|
||||
<div class="item-meta">${escapeHtml(album.artist || 'Unknown Artist')} • ${new Date(album.blockedAt).toLocaleDateString()}</div>
|
||||
</div>
|
||||
<button class="unblock-btn" data-id="${album.id}" data-type="album">Unblock</button>
|
||||
</li>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// Render tracks
|
||||
if (blockedTracksList) {
|
||||
blockedTracksList.innerHTML = tracks
|
||||
.map(
|
||||
(track) => `
|
||||
<li data-id="${track.id}" data-type="track">
|
||||
<div class="item-info">
|
||||
<div class="item-name">${escapeHtml(track.title)}</div>
|
||||
<div class="item-meta">${escapeHtml(track.artist || 'Unknown Artist')} • ${new Date(track.blockedAt).toLocaleDateString()}</div>
|
||||
</div>
|
||||
<button class="unblock-btn" data-id="${track.id}" data-type="track">Unblock</button>
|
||||
</li>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// Add unblock button handlers
|
||||
blockedListContainer.querySelectorAll('.unblock-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const id = btn.dataset.id;
|
||||
const type = btn.dataset.type;
|
||||
|
||||
if (type === 'artist') {
|
||||
contentBlockingSettings.unblockArtist(id);
|
||||
} else if (type === 'album') {
|
||||
contentBlockingSettings.unblockAlbum(id);
|
||||
} else if (type === 'track') {
|
||||
contentBlockingSettings.unblockTrack(id);
|
||||
}
|
||||
|
||||
renderBlockedLists();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle blocked list visibility
|
||||
manageBtn.addEventListener('click', () => {
|
||||
const isVisible = blockedListContainer.style.display !== 'none';
|
||||
blockedListContainer.style.display = isVisible ? 'none' : 'block';
|
||||
if (!isVisible) {
|
||||
renderBlockedLists();
|
||||
}
|
||||
});
|
||||
|
||||
// Clear all blocked content
|
||||
if (clearAllBtn) {
|
||||
clearAllBtn.addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to unblock all artists, albums, and tracks?')) {
|
||||
contentBlockingSettings.clearAllBlocked();
|
||||
renderBlockedLists();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initial render
|
||||
renderBlockedLists();
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
|
|
|||
169
js/storage.js
169
js/storage.js
|
|
@ -1706,3 +1706,172 @@ export const pwaUpdateSettings = {
|
|||
localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
};
|
||||
|
||||
export const contentBlockingSettings = {
|
||||
BLOCKED_ARTISTS_KEY: 'blocked-artists',
|
||||
BLOCKED_TRACKS_KEY: 'blocked-tracks',
|
||||
BLOCKED_ALBUMS_KEY: 'blocked-albums',
|
||||
|
||||
// Blocked Artists
|
||||
getBlockedArtists() {
|
||||
try {
|
||||
const data = localStorage.getItem(this.BLOCKED_ARTISTS_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
setBlockedArtists(artists) {
|
||||
localStorage.setItem(this.BLOCKED_ARTISTS_KEY, JSON.stringify(artists));
|
||||
},
|
||||
|
||||
isArtistBlocked(artistId) {
|
||||
if (!artistId) return false;
|
||||
return this.getBlockedArtists().some((a) => a.id === artistId);
|
||||
},
|
||||
|
||||
blockArtist(artist) {
|
||||
if (!artist || !artist.id) return;
|
||||
const blocked = this.getBlockedArtists();
|
||||
if (!blocked.some((a) => a.id === artist.id)) {
|
||||
blocked.push({
|
||||
id: artist.id,
|
||||
name: artist.name || 'Unknown Artist',
|
||||
blockedAt: Date.now(),
|
||||
});
|
||||
this.setBlockedArtists(blocked);
|
||||
}
|
||||
},
|
||||
|
||||
unblockArtist(artistId) {
|
||||
const blocked = this.getBlockedArtists().filter((a) => a.id !== artistId);
|
||||
this.setBlockedArtists(blocked);
|
||||
},
|
||||
|
||||
// Blocked Tracks
|
||||
getBlockedTracks() {
|
||||
try {
|
||||
const data = localStorage.getItem(this.BLOCKED_TRACKS_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
setBlockedTracks(tracks) {
|
||||
localStorage.setItem(this.BLOCKED_TRACKS_KEY, JSON.stringify(tracks));
|
||||
},
|
||||
|
||||
isTrackBlocked(trackId) {
|
||||
if (!trackId) return false;
|
||||
return this.getBlockedTracks().some((t) => t.id === trackId);
|
||||
},
|
||||
|
||||
blockTrack(track) {
|
||||
if (!track || !track.id) return;
|
||||
const blocked = this.getBlockedTracks();
|
||||
if (!blocked.some((t) => t.id === track.id)) {
|
||||
blocked.push({
|
||||
id: track.id,
|
||||
title: track.title || 'Unknown Track',
|
||||
artist: track.artist?.name || track.artist || 'Unknown Artist',
|
||||
blockedAt: Date.now(),
|
||||
});
|
||||
this.setBlockedTracks(blocked);
|
||||
}
|
||||
},
|
||||
|
||||
unblockTrack(trackId) {
|
||||
const blocked = this.getBlockedTracks().filter((t) => t.id !== trackId);
|
||||
this.setBlockedTracks(blocked);
|
||||
},
|
||||
|
||||
// Blocked Albums
|
||||
getBlockedAlbums() {
|
||||
try {
|
||||
const data = localStorage.getItem(this.BLOCKED_ALBUMS_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
setBlockedAlbums(albums) {
|
||||
localStorage.setItem(this.BLOCKED_ALBUMS_KEY, JSON.stringify(albums));
|
||||
},
|
||||
|
||||
isAlbumBlocked(albumId) {
|
||||
if (!albumId) return false;
|
||||
return this.getBlockedAlbums().some((a) => a.id === albumId);
|
||||
},
|
||||
|
||||
blockAlbum(album) {
|
||||
if (!album || !album.id) return;
|
||||
const blocked = this.getBlockedAlbums();
|
||||
if (!blocked.some((a) => a.id === album.id)) {
|
||||
blocked.push({
|
||||
id: album.id,
|
||||
title: album.title || 'Unknown Album',
|
||||
artist: album.artist?.name || album.artist || 'Unknown Artist',
|
||||
blockedAt: Date.now(),
|
||||
});
|
||||
this.setBlockedAlbums(blocked);
|
||||
}
|
||||
},
|
||||
|
||||
unblockAlbum(albumId) {
|
||||
const blocked = this.getBlockedAlbums().filter((a) => a.id !== albumId);
|
||||
this.setBlockedAlbums(blocked);
|
||||
},
|
||||
|
||||
// Check if track should be hidden (blocked track or by blocked artist)
|
||||
shouldHideTrack(track) {
|
||||
if (!track) return true;
|
||||
if (this.isTrackBlocked(track.id)) return true;
|
||||
if (track.artist?.id && this.isArtistBlocked(track.artist.id)) return true;
|
||||
if (track.artists?.some((a) => this.isArtistBlocked(a.id))) return true;
|
||||
if (track.album?.id && this.isAlbumBlocked(track.album.id)) return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
// Check if album should be hidden
|
||||
shouldHideAlbum(album) {
|
||||
if (!album) return true;
|
||||
if (this.isAlbumBlocked(album.id)) return true;
|
||||
if (album.artist?.id && this.isArtistBlocked(album.artist.id)) return true;
|
||||
if (album.artists?.some((a) => this.isArtistBlocked(a.id))) return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
// Check if artist should be hidden
|
||||
shouldHideArtist(artist) {
|
||||
if (!artist) return true;
|
||||
return this.isArtistBlocked(artist.id);
|
||||
},
|
||||
|
||||
// Filter arrays
|
||||
filterTracks(tracks) {
|
||||
return tracks.filter((t) => !this.shouldHideTrack(t));
|
||||
},
|
||||
|
||||
filterAlbums(albums) {
|
||||
return albums.filter((a) => !this.shouldHideAlbum(a));
|
||||
},
|
||||
|
||||
filterArtists(artists) {
|
||||
return artists.filter((a) => !this.shouldHideArtist(a));
|
||||
},
|
||||
|
||||
// Get all blocked items count
|
||||
getTotalBlockedCount() {
|
||||
return this.getBlockedArtists().length + this.getBlockedTracks().length + this.getBlockedAlbums().length;
|
||||
},
|
||||
|
||||
// Clear all blocked items
|
||||
clearAllBlocked() {
|
||||
localStorage.removeItem(this.BLOCKED_ARTISTS_KEY);
|
||||
localStorage.removeItem(this.BLOCKED_TRACKS_KEY);
|
||||
localStorage.removeItem(this.BLOCKED_ALBUMS_KEY);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
createQualityBadgeHTML,
|
||||
} from './utils.js';
|
||||
import { sidePanelManager } from './side-panel.js';
|
||||
import { downloadQualitySettings } from './storage.js';
|
||||
import { downloadQualitySettings, contentBlockingSettings } from './storage.js';
|
||||
import { db } from './db.js';
|
||||
import { syncManager } from './accounts/pocketbase.js';
|
||||
import { showNotification, downloadTracks } from './downloads.js';
|
||||
|
|
@ -241,12 +241,16 @@ export function initializeUIInteractions(player, api, ui) {
|
|||
const html = currentQueue
|
||||
.map((track, index) => {
|
||||
const isPlaying = index === player.currentQueueIndex;
|
||||
const isBlocked = contentBlockingSettings?.shouldHideTrack(track);
|
||||
const trackTitle = getTrackTitle(track);
|
||||
const trackArtists = getTrackArtists(track, { fallback: 'Unknown' });
|
||||
const qualityBadge = createQualityBadgeHTML(track);
|
||||
const blockedTitle = isBlocked
|
||||
? `title="Blocked: ${contentBlockingSettings.isTrackBlocked(track.id) ? 'Track blocked' : contentBlockingSettings.isArtistBlocked(track.artist?.id) ? 'Artist blocked' : 'Album blocked'}"`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="queue-track-item ${isPlaying ? 'playing' : ''}" data-queue-index="${index}" data-track-id="${track.id}" draggable="true">
|
||||
<div class="queue-track-item ${isPlaying ? 'playing' : ''} ${isBlocked ? 'blocked' : ''}" data-queue-index="${index}" data-track-id="${track.id}" draggable="${isBlocked ? 'false' : 'true'}" ${blockedTitle}>
|
||||
<div class="drag-handle">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="5" y1="8" x2="19" y2="8"></line>
|
||||
|
|
@ -261,7 +265,7 @@ export function initializeUIInteractions(player, api, ui) {
|
|||
<div class="artist">${escapeHtml(trackArtists)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="track-item-duration">${formatTime(track.duration)}</div>
|
||||
<div class="track-item-duration">${isBlocked ? '--:--' : formatTime(track.duration)}</div>
|
||||
<button class="queue-like-btn" data-action="toggle-like" title="Add to Liked">
|
||||
${SVG_HEART}
|
||||
</button>
|
||||
|
|
@ -319,6 +323,11 @@ export function initializeUIInteractions(player, api, ui) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Don't play blocked tracks
|
||||
if (item.classList.contains('blocked')) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.playAtIndex(index);
|
||||
refreshQueuePanel();
|
||||
});
|
||||
|
|
|
|||
99
js/ui.js
99
js/ui.js
|
|
@ -28,6 +28,7 @@ import {
|
|||
visualizerSettings,
|
||||
homePageSettings,
|
||||
fontSettings,
|
||||
contentBlockingSettings,
|
||||
} from './storage.js';
|
||||
import { db } from './db.js';
|
||||
import { getVibrantColorFromImage } from './vibrant-color.js';
|
||||
|
|
@ -261,6 +262,7 @@ export class UIRenderer {
|
|||
|
||||
createTrackItemHTML(track, index, showCover = false, hasMultipleDiscs = false, useTrackNumber = false) {
|
||||
const isUnavailable = track.isUnavailable;
|
||||
const isBlocked = contentBlockingSettings?.shouldHideTrack(track);
|
||||
const trackImageHTML = showCover
|
||||
? `<img src="${this.api.getCoverUrl(track.album?.cover)}" alt="Track Cover" class="track-item-cover" loading="lazy">`
|
||||
: '';
|
||||
|
|
@ -296,11 +298,16 @@ export class UIRenderer {
|
|||
</button>
|
||||
`;
|
||||
|
||||
const blockedTitle = isBlocked
|
||||
? `title="Blocked: ${contentBlockingSettings.isTrackBlocked(track.id) ? 'Track blocked' : contentBlockingSettings.isArtistBlocked(track.artist?.id) ? 'Artist blocked' : 'Album blocked'}"`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="track-item ${isCurrentTrack ? 'playing' : ''} ${isUnavailable ? 'unavailable' : ''}"
|
||||
<div class="track-item ${isCurrentTrack ? 'playing' : ''} ${isUnavailable ? 'unavailable' : ''} ${isBlocked ? 'blocked' : ''}"
|
||||
data-track-id="${track.id}"
|
||||
${track.isLocal ? 'data-is-local="true"' : ''}
|
||||
${isUnavailable ? 'title="This track is currently unavailable"' : ''}>
|
||||
${isUnavailable ? 'title="This track is currently unavailable"' : ''}
|
||||
${blockedTitle}>
|
||||
${trackNumberHTML}
|
||||
<div class="track-item-info">
|
||||
<div class="track-item-details">
|
||||
|
|
@ -312,7 +319,7 @@ export class UIRenderer {
|
|||
<div class="artist">${escapeHtml(trackArtists)}${yearDisplay}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="track-item-duration">${isUnavailable ? '--:--' : track.duration ? formatTime(track.duration) : '--:--'}</div>
|
||||
<div class="track-item-duration">${isUnavailable || isBlocked ? '--:--' : track.duration ? formatTime(track.duration) : '--:--'}</div>
|
||||
<div class="track-item-actions">
|
||||
${actionsHTML}
|
||||
</div>
|
||||
|
|
@ -496,6 +503,7 @@ export class UIRenderer {
|
|||
createAlbumCardHTML(album) {
|
||||
const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
|
||||
const qualityBadge = createQualityBadgeHTML(album);
|
||||
const isBlocked = contentBlockingSettings?.shouldHideAlbum(album);
|
||||
let yearDisplay = '';
|
||||
if (album.releaseDate) {
|
||||
const date = new Date(album.releaseDate);
|
||||
|
|
@ -521,11 +529,16 @@ export class UIRenderer {
|
|||
</button>
|
||||
`,
|
||||
isCompact,
|
||||
extraClasses: isBlocked ? 'blocked' : '',
|
||||
extraAttributes: isBlocked
|
||||
? `title="Blocked: ${contentBlockingSettings.isAlbumBlocked(album.id) ? 'Album blocked' : 'Artist blocked'}"`
|
||||
: '',
|
||||
});
|
||||
}
|
||||
|
||||
createArtistCardHTML(artist) {
|
||||
const isCompact = cardSettings.isCompactArtist();
|
||||
const isBlocked = contentBlockingSettings?.shouldHideArtist(artist);
|
||||
|
||||
return this.createBaseCardHTML({
|
||||
type: 'artist',
|
||||
|
|
@ -540,7 +553,8 @@ export class UIRenderer {
|
|||
</button>
|
||||
`,
|
||||
isCompact,
|
||||
extraClasses: 'artist',
|
||||
extraClasses: `artist${isBlocked ? ' blocked' : ''}`,
|
||||
extraAttributes: isBlocked ? 'title="Blocked: Artist blocked"' : '',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1590,6 +1604,19 @@ export class UIRenderer {
|
|||
return;
|
||||
}
|
||||
|
||||
// Filter out blocked content
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
items = items.filter((item) => {
|
||||
if (item.type === 'track') {
|
||||
return !contentBlockingSettings.shouldHideTrack(item);
|
||||
} else if (item.type === 'album') {
|
||||
return !contentBlockingSettings.shouldHideAlbum(item);
|
||||
} else if (item.type === 'artist') {
|
||||
return !contentBlockingSettings.shouldHideArtist(item);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Shuffle items if enabled
|
||||
if (homePageSettings.shouldShuffleEditorsPicks()) {
|
||||
items = [...items].sort(() => Math.random() - 0.5);
|
||||
|
|
@ -1808,6 +1835,18 @@ export class UIRenderer {
|
|||
async filterUserContent(items, type) {
|
||||
if (!items || items.length === 0) return [];
|
||||
|
||||
// Import blocking settings
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
|
||||
// First filter out blocked content
|
||||
if (type === 'track') {
|
||||
items = contentBlockingSettings.filterTracks(items);
|
||||
} else if (type === 'album') {
|
||||
items = contentBlockingSettings.filterAlbums(items);
|
||||
} else if (type === 'artist') {
|
||||
items = contentBlockingSettings.filterArtists(items);
|
||||
}
|
||||
|
||||
const favorites = await db.getFavorites(type);
|
||||
const favoriteIds = new Set(favorites.map((i) => i.id));
|
||||
|
||||
|
|
@ -2138,12 +2177,24 @@ export class UIRenderer {
|
|||
// Similar Artists
|
||||
this.api
|
||||
.getSimilarArtists(album.artist.id)
|
||||
.then((similar) => {
|
||||
if (similar && similar.length > 0 && similarArtistsContainer && similarArtistsSection) {
|
||||
similarArtistsContainer.innerHTML = similar
|
||||
.then(async (similar) => {
|
||||
// Filter out blocked artists
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
const filteredSimilar = contentBlockingSettings.filterArtists(similar || []);
|
||||
|
||||
if (filteredSimilar.length > 0 && similarArtistsContainer && similarArtistsSection) {
|
||||
similarArtistsContainer.innerHTML = filteredSimilar
|
||||
.map((a) => this.createArtistCardHTML(a))
|
||||
.join('');
|
||||
similarArtistsSection.style.display = 'block';
|
||||
|
||||
filteredSimilar.forEach((a) => {
|
||||
const el = similarArtistsContainer.querySelector(`[data-artist-id="${a.id}"]`);
|
||||
if (el) {
|
||||
trackDataStore.set(el, a);
|
||||
this.updateLikeState(el, 'artist', a.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => console.warn('Failed to load similar artists:', e));
|
||||
|
|
@ -2151,12 +2202,18 @@ export class UIRenderer {
|
|||
// Similar Albums
|
||||
this.api
|
||||
.getSimilarAlbums(albumId)
|
||||
.then((similar) => {
|
||||
if (similar && similar.length > 0 && similarAlbumsContainer && similarAlbumsSection) {
|
||||
similarAlbumsContainer.innerHTML = similar.map((a) => this.createAlbumCardHTML(a)).join('');
|
||||
.then(async (similar) => {
|
||||
// Filter out blocked albums
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
const filteredSimilar = contentBlockingSettings.filterAlbums(similar || []);
|
||||
|
||||
if (filteredSimilar.length > 0 && similarAlbumsContainer && similarAlbumsSection) {
|
||||
similarAlbumsContainer.innerHTML = filteredSimilar
|
||||
.map((a) => this.createAlbumCardHTML(a))
|
||||
.join('');
|
||||
similarAlbumsSection.style.display = 'block';
|
||||
|
||||
similar.forEach((a) => {
|
||||
filteredSimilar.forEach((a) => {
|
||||
const el = similarAlbumsContainer.querySelector(`[data-album-id="${a.id}"]`);
|
||||
if (el) {
|
||||
trackDataStore.set(el, a);
|
||||
|
|
@ -2185,7 +2242,11 @@ export class UIRenderer {
|
|||
}
|
||||
|
||||
try {
|
||||
const recommendedTracks = await this.api.getRecommendedTracksForPlaylist(tracks, 20);
|
||||
let recommendedTracks = await this.api.getRecommendedTracksForPlaylist(tracks, 20);
|
||||
|
||||
// Filter out blocked tracks
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
recommendedTracks = contentBlockingSettings.filterTracks(recommendedTracks);
|
||||
|
||||
if (recommendedTracks.length > 0) {
|
||||
this.renderListWithTracks(recommendedContainer, recommendedTracks, true);
|
||||
|
|
@ -2739,12 +2800,18 @@ export class UIRenderer {
|
|||
if (similarContainer && similarSection) {
|
||||
this.api
|
||||
.getSimilarArtists(artistId)
|
||||
.then((similar) => {
|
||||
if (similar && similar.length > 0) {
|
||||
similarContainer.innerHTML = similar.map((a) => this.createArtistCardHTML(a)).join('');
|
||||
.then(async (similar) => {
|
||||
// Filter out blocked artists
|
||||
const { contentBlockingSettings } = await import('./storage.js');
|
||||
const filteredSimilar = contentBlockingSettings.filterArtists(similar || []);
|
||||
|
||||
if (filteredSimilar.length > 0) {
|
||||
similarContainer.innerHTML = filteredSimilar
|
||||
.map((a) => this.createArtistCardHTML(a))
|
||||
.join('');
|
||||
similarSection.style.display = 'block';
|
||||
|
||||
similar.forEach((a) => {
|
||||
filteredSimilar.forEach((a) => {
|
||||
const el = similarContainer.querySelector(`[data-artist-id="${a.id}"]`);
|
||||
if (el) {
|
||||
trackDataStore.set(el, a);
|
||||
|
|
|
|||
107
styles.css
107
styles.css
|
|
@ -2431,6 +2431,113 @@ input:checked + .slider::before {
|
|||
color: var(--foreground);
|
||||
}
|
||||
|
||||
#context-menu li.separator {
|
||||
height: 1px;
|
||||
background-color: var(--border);
|
||||
margin: 0.5rem 0;
|
||||
padding: 0;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#context-menu li.separator:hover {
|
||||
background-color: var(--border);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.blocked-items-list {
|
||||
list-style: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.blocked-items-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--accent);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.blocked-items-list li:hover {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
.blocked-items-list .item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.blocked-items-list .item-name {
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.blocked-items-list .item-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.blocked-items-list .unblock-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.blocked-items-list .unblock-btn:hover {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
/* Blocked content styling */
|
||||
.track-item.blocked {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.track-item.blocked .track-item-info {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.track-item.blocked .track-menu-btn {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card.blocked {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.card.blocked .card-menu-btn {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card.blocked .card-title,
|
||||
.card.blocked .card-subtitle {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.queue-track-item.blocked {
|
||||
opacity: 0.4;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.queue-track-item.blocked .track-item-details {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
#queue-modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
|
|
|
|||
Loading…
Reference in a new issue