From f6dae2223fcb2707d27b956af1b62b6f63412e7a Mon Sep 17 00:00:00 2001 From: Eduard Prigoana Date: Tue, 10 Feb 2026 21:03:48 +0000 Subject: [PATCH] artist blocking --- index.html | 97 ++++++++++++++++++++++++ js/events.js | 106 +++++++++++++++++++++++++- js/player.js | 76 ++++++++++++------- js/settings.js | 139 ++++++++++++++++++++++++++++++++++ js/storage.js | 169 ++++++++++++++++++++++++++++++++++++++++++ js/ui-interactions.js | 15 +++- js/ui.js | 99 +++++++++++++++++++++---- styles.css | 107 ++++++++++++++++++++++++++ 8 files changed, 759 insertions(+), 49 deletions(-) diff --git a/index.html b/index.html index 8c02e38..fe3cd7e 100644 --- a/index.html +++ b/index.html @@ -47,6 +47,31 @@
  • Track info
  • Open original URL
  • Download
  • +
  • +
  • + Block track +
  • +
  • + Block album +
  • +
  • + Block artist +
  • @@ -3676,6 +3701,78 @@ + +
    +
    + Blocked Content + Manage artists, albums, and tracks you've blocked from + recommendations +
    +
    + + +
    +
    + diff --git a/js/events.js b/js/events.js index 756b8a3..ac02b5c 100644 --- a/js/events.js +++ b/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; } diff --git a/js/player.js b/js/player.js index 19eec19..44fa0f0 100644 --- a/js/player.js +++ b/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); + }); } } diff --git a/js/settings.js b/js/settings.js index c623866..8af9ce8 100644 --- a/js/settings.js +++ b/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) => ` +
  • +
    +
    ${escapeHtml(artist.name)}
    +
    ${new Date(artist.blockedAt).toLocaleDateString()}
    +
    + +
  • + ` + ) + .join(''); + } + + // Render albums + if (blockedAlbumsList) { + blockedAlbumsList.innerHTML = albums + .map( + (album) => ` +
  • +
    +
    ${escapeHtml(album.title)}
    +
    ${escapeHtml(album.artist || 'Unknown Artist')} • ${new Date(album.blockedAt).toLocaleDateString()}
    +
    + +
  • + ` + ) + .join(''); + } + + // Render tracks + if (blockedTracksList) { + blockedTracksList.innerHTML = tracks + .map( + (track) => ` +
  • +
    +
    ${escapeHtml(track.title)}
    +
    ${escapeHtml(track.artist || 'Unknown Artist')} • ${new Date(track.blockedAt).toLocaleDateString()}
    +
    + +
  • + ` + ) + .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; +} diff --git a/js/storage.js b/js/storage.js index e554188..a03ebce 100644 --- a/js/storage.js +++ b/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); + }, +}; diff --git a/js/ui-interactions.js b/js/ui-interactions.js index 3fea1f5..3b2a7bd 100644 --- a/js/ui-interactions.js +++ b/js/ui-interactions.js @@ -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 ` -
    +
    @@ -261,7 +265,7 @@ export function initializeUIInteractions(player, api, ui) {
    ${escapeHtml(trackArtists)}
    -
    ${formatTime(track.duration)}
    +
    ${isBlocked ? '--:--' : formatTime(track.duration)}
    @@ -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(); }); diff --git a/js/ui.js b/js/ui.js index bc2d8f9..0b76811 100644 --- a/js/ui.js +++ b/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 ? `Track Cover` : ''; @@ -296,11 +298,16 @@ export class UIRenderer { `; + const blockedTitle = isBlocked + ? `title="Blocked: ${contentBlockingSettings.isTrackBlocked(track.id) ? 'Track blocked' : contentBlockingSettings.isArtistBlocked(track.artist?.id) ? 'Artist blocked' : 'Album blocked'}"` + : ''; + return ` -
    + ${isUnavailable ? 'title="This track is currently unavailable"' : ''} + ${blockedTitle}> ${trackNumberHTML}
    @@ -312,7 +319,7 @@ export class UIRenderer {
    ${escapeHtml(trackArtists)}${yearDisplay}
    -
    ${isUnavailable ? '--:--' : track.duration ? formatTime(track.duration) : '--:--'}
    +
    ${isUnavailable || isBlocked ? '--:--' : track.duration ? formatTime(track.duration) : '--:--'}
    ${actionsHTML}
    @@ -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 { `, 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 { `, 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); diff --git a/styles.css b/styles.css index 9487a92..3d8169a 100644 --- a/styles.css +++ b/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;