diff --git a/index.html b/index.html
index 8c02e38..fe3cd7e 100644
--- a/index.html
+++ b/index.html
@@ -47,6 +47,31 @@
+
-
${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
? `
})
`
: '';
@@ -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;