feat(blacklist): add grayout styling for blacklisted artist tracks
- TrackRow: Add isBlacklisted prop with grayout styling and ban icon - VirtualizedTrackList: Check blacklist status for each track - PlaylistDetailView: Gray out and restrict actions for blacklisted tracks - AlbumDetailView: Apply album-level blacklist check to all tracks - HomeView: Gray out blacklisted tracks in Continue Listening section - ArtistDetailView: Banner shown when viewing blacklisted artist page Blacklisted tracks show reduced opacity, ban icon instead of track number, and restricted menu actions (navigation only, no playback).
This commit is contained in:
1 parent
bdffe6c709
commit
6ff6269f74
6 files changed
+136
-24
No files matched your search
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Play, Pause, Heart, HardDrive, AlertCircle } from 'lucide-svelte';
|
||||
import { Play, Pause, Heart, HardDrive, AlertCircle, Ban } from 'lucide-svelte';
|
||||
import TrackMenu from './TrackMenu.svelte';
|
||||
import DownloadButton from './DownloadButton.svelte';
|
||||
import {
|
||||
@@ -26,6 +26,7 @@
|
||||
isLocal?: boolean; // Whether this is a local library track
|
||||
isUnavailable?: boolean; // Track removed from Qobuz or otherwise unavailable
|
||||
unavailableTooltip?: string; // Tooltip for unavailable indicator
|
||||
isBlacklisted?: boolean; // Artist is blacklisted
|
||||
isFavoriteOverride?: boolean; // Optional override for favorite state
|
||||
downloadStatus?: OfflineCacheStatus;
|
||||
downloadProgress?: number;
|
||||
@@ -73,6 +74,7 @@
|
||||
isUnavailable = false,
|
||||
unavailableTooltip,
|
||||
isFavoriteOverride,
|
||||
isBlacklisted = false,
|
||||
downloadStatus = 'none',
|
||||
downloadProgress = 0,
|
||||
hideDownload = false,
|
||||
@@ -136,19 +138,24 @@
|
||||
<div
|
||||
class="track-row"
|
||||
class:playing={isPlaying}
|
||||
class:hovered={isHovered && !isPlaying}
|
||||
class:hovered={isHovered && !isPlaying && !isBlacklisted}
|
||||
class:compact
|
||||
class:blacklisted={isBlacklisted}
|
||||
data-track-id={trackId ?? undefined}
|
||||
onmouseenter={() => (isHovered = true)}
|
||||
onmouseleave={() => (isHovered = false)}
|
||||
onclick={onPlay}
|
||||
onclick={isBlacklisted ? undefined : onPlay}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => e.key === 'Enter' && onPlay?.()}
|
||||
tabindex={isBlacklisted ? -1 : 0}
|
||||
onkeydown={(e) => e.key === 'Enter' && !isBlacklisted && onPlay?.()}
|
||||
>
|
||||
<!-- Track Number / Play Button / Unavailable Indicator -->
|
||||
<div class="track-number" class:unavailable={isUnavailable}>
|
||||
{#if isUnavailable}
|
||||
<div class="track-number" class:unavailable={isUnavailable} class:blacklisted={isBlacklisted}>
|
||||
{#if isBlacklisted}
|
||||
<span class="blacklisted-icon" title="Artist is blacklisted">
|
||||
<Ban size={14} />
|
||||
</span>
|
||||
{:else if isUnavailable}
|
||||
<span class="unavailable-icon" title={unavailableTooltip}>
|
||||
<AlertCircle size={16} />
|
||||
</span>
|
||||
@@ -334,6 +341,27 @@
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Blacklisted track styles */
|
||||
.track-row.blacklisted {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.track-row.blacklisted:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.track-number.blacklisted {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.blacklisted-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.track-number :global(.play-icon) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import TrackRow from './TrackRow.svelte';
|
||||
import type { OfflineCacheStatus } from '$lib/stores/offlineCacheState';
|
||||
import { isArtistBlacklisted } from '$lib/stores/blacklistStore';
|
||||
|
||||
// Use generic types to match whatever caller passes
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -315,6 +316,7 @@
|
||||
{@const albumId = getAlbumId?.(item.track)}
|
||||
{@const downloadInfo = getOfflineCacheStatus?.(trackId) ?? { status: 'none' as const, progress: 0 }}
|
||||
{@const isTrackDownloaded = downloadInfo.status === 'ready'}
|
||||
{@const trackBlacklisted = !isLocal && artistId ? isArtistBlacklisted(artistId) : false}
|
||||
<TrackRow
|
||||
trackId={trackId}
|
||||
number={getTrackNumber(item.track, item.index)}
|
||||
@@ -324,18 +326,24 @@
|
||||
duration={formatDuration(getTrackDuration(item.track))}
|
||||
quality={getQualityBadge(item.track)}
|
||||
isPlaying={isPlaybackActive && activeTrackId === trackId}
|
||||
isBlacklisted={trackBlacklisted}
|
||||
{isLocal}
|
||||
{hideDownload}
|
||||
{hideFavorite}
|
||||
hideDownload={hideDownload || trackBlacklisted}
|
||||
hideFavorite={hideFavorite || trackBlacklisted}
|
||||
isFavoriteOverride={isFavoriteOverride}
|
||||
downloadStatus={downloadInfo.status}
|
||||
downloadProgress={downloadInfo.progress}
|
||||
onArtistClick={trackArtist && onArtistClick ? () => onArtistClick(trackArtist) : undefined}
|
||||
onAlbumClick={albumKey && onAlbumClick ? () => onAlbumClick(item.track) : undefined}
|
||||
onPlay={() => onTrackPlay(item.track)}
|
||||
onDownload={onDownload ? () => onDownload(item.track) : undefined}
|
||||
onPlay={trackBlacklisted ? undefined : () => onTrackPlay(item.track)}
|
||||
onDownload={onDownload && !trackBlacklisted ? () => onDownload(item.track) : undefined}
|
||||
onRemoveDownload={onRemoveDownload ? () => onRemoveDownload(trackId) : undefined}
|
||||
menuActions={{
|
||||
menuActions={trackBlacklisted ? {
|
||||
// Only navigation actions for blacklisted tracks
|
||||
onGoToAlbum: albumId && onGoToAlbum ? () => onGoToAlbum(albumId) : undefined,
|
||||
onGoToArtist: artistId && onGoToArtist ? () => onGoToArtist(artistId) : undefined,
|
||||
onShowInfo: !isLocal && onShowInfo ? () => onShowInfo(trackId) : undefined,
|
||||
} : {
|
||||
onPlayNow: () => onTrackPlay(item.track),
|
||||
onPlayNext: onTrackPlayNext ? () => onTrackPlayNext(item.track) : undefined,
|
||||
onPlayLater: onTrackPlayLater ? () => onTrackPlayLater(item.track) : undefined,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
loadAlbumFavorites,
|
||||
toggleAlbumFavorite
|
||||
} from '$lib/stores/albumFavoritesStore';
|
||||
import { isBlacklisted as isArtistBlacklisted } from '$lib/stores/artistBlacklistStore';
|
||||
|
||||
interface Track {
|
||||
id: number;
|
||||
@@ -413,6 +414,7 @@
|
||||
|
||||
<!-- Track Rows -->
|
||||
<div class="tracks">
|
||||
{@const albumArtistBlacklisted = album.artistId ? isArtistBlacklisted(album.artistId) : false}
|
||||
{#each album.tracks as track, trackIndex (`${track.id}-${downloadStateVersion}`)}
|
||||
{@const downloadInfo = getTrackOfflineCacheStatus?.(track.id) ?? { status: 'none' as const, progress: 0 }}
|
||||
{@const isTrackDownloaded = downloadInfo.status === 'ready'}
|
||||
@@ -424,14 +426,21 @@
|
||||
duration={track.duration}
|
||||
quality={track.quality}
|
||||
isPlaying={activeTrackId === track.id}
|
||||
isBlacklisted={albumArtistBlacklisted}
|
||||
downloadStatus={downloadInfo.status}
|
||||
downloadProgress={downloadInfo.progress}
|
||||
onPlay={() => {
|
||||
hideFavorite={albumArtistBlacklisted}
|
||||
hideDownload={albumArtistBlacklisted}
|
||||
onPlay={albumArtistBlacklisted ? undefined : () => {
|
||||
onTrackPlay?.(track);
|
||||
}}
|
||||
onDownload={onTrackDownload ? () => onTrackDownload(track) : undefined}
|
||||
onRemoveDownload={onTrackRemoveDownload ? () => onTrackRemoveDownload(track.id) : undefined}
|
||||
menuActions={{
|
||||
onDownload={!albumArtistBlacklisted && onTrackDownload ? () => onTrackDownload(track) : undefined}
|
||||
onRemoveDownload={!albumArtistBlacklisted && onTrackRemoveDownload ? () => onTrackRemoveDownload(track.id) : undefined}
|
||||
menuActions={albumArtistBlacklisted ? {
|
||||
// Blacklisted: only navigation and info
|
||||
onGoToArtist: album.artistId && onTrackGoToArtist ? () => onTrackGoToArtist(album.artistId!) : undefined,
|
||||
onShowInfo: onTrackShowInfo ? () => onTrackShowInfo(track.id) : undefined
|
||||
} : {
|
||||
onPlayNow: () => {
|
||||
onTrackPlay?.(track);
|
||||
},
|
||||
|
||||
@@ -1317,6 +1317,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blacklist Warning Banner -->
|
||||
{#if artistIsBlacklisted}
|
||||
<div class="blacklist-banner">
|
||||
<Ban size={18} />
|
||||
<span>This artist is blacklisted. Their music is hidden from search, radio, and suggestions.</span>
|
||||
<button class="unblock-btn" onclick={toggleBlacklist} disabled={isBlacklistLoading}>
|
||||
Unblock
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showJumpNav}
|
||||
<div class="jump-nav">
|
||||
<div class="jump-nav-left">
|
||||
@@ -2614,6 +2625,44 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.blacklist-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: 8px;
|
||||
margin: 0 24px 16px 24px;
|
||||
color: #ef4444;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.blacklist-banner span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.unblock-btn {
|
||||
padding: 6px 14px;
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
border-radius: 6px;
|
||||
color: #ef4444;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.unblock-btn:hover:not(:disabled) {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.unblock-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.floating-message {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import HomeSettingsModal from '../HomeSettingsModal.svelte';
|
||||
import GenreFilterButton from '../GenreFilterButton.svelte';
|
||||
import { formatDuration, formatQuality, getQobuzImage } from '$lib/adapters/qobuzAdapters';
|
||||
import { isArtistBlacklisted } from '$lib/stores/blacklistStore';
|
||||
import {
|
||||
subscribe as subscribeHomeSettings,
|
||||
getSettings,
|
||||
@@ -907,6 +908,7 @@
|
||||
{@const isActiveTrack = isPlaybackActive && activeTrackId === track.id}
|
||||
{@const cacheStatus = getTrackOfflineCacheStatus?.(track.id) ?? { status: 'none', progress: 0 }}
|
||||
{@const isTrackDownloaded = cacheStatus.status === 'ready'}
|
||||
{@const trackBlacklisted = track.artistId ? isArtistBlacklisted(track.artistId) : false}
|
||||
<TrackRow
|
||||
trackId={track.id}
|
||||
number={index + 1}
|
||||
@@ -916,13 +918,21 @@
|
||||
duration={track.duration}
|
||||
quality={getTrackQuality(track)}
|
||||
isPlaying={isActiveTrack}
|
||||
isBlacklisted={trackBlacklisted}
|
||||
compact={true}
|
||||
hideDownload={trackBlacklisted}
|
||||
hideFavorite={trackBlacklisted}
|
||||
downloadStatus={cacheStatus.status}
|
||||
downloadProgress={cacheStatus.progress}
|
||||
onArtistClick={track.artistId && onArtistClick ? () => onArtistClick(track.artistId!) : undefined}
|
||||
onAlbumClick={track.albumId && onAlbumClick ? () => onAlbumClick(track.albumId!) : undefined}
|
||||
onPlay={() => handleContinueTrackPlay(track, index)}
|
||||
menuActions={{
|
||||
onPlay={trackBlacklisted ? undefined : () => handleContinueTrackPlay(track, index)}
|
||||
menuActions={trackBlacklisted ? {
|
||||
// Only navigation actions for blacklisted tracks
|
||||
onGoToAlbum: track.albumId && onTrackGoToAlbum ? () => onTrackGoToAlbum(track.albumId!) : undefined,
|
||||
onGoToArtist: track.artistId && onTrackGoToArtist ? () => onTrackGoToArtist(track.artistId!) : undefined,
|
||||
onShowInfo: onTrackShowInfo ? () => onTrackShowInfo(track.id) : undefined
|
||||
} : {
|
||||
onPlayNow: () => handleContinueTrackPlay(track, index),
|
||||
onPlayNext: onTrackPlayNext ? () => onTrackPlayNext(track) : undefined,
|
||||
onPlayLater: onTrackPlayLater ? () => onTrackPlayLater(track) : undefined,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
} from '$lib/stores/offlineStore';
|
||||
import { consumeContextTrackFocus, setPlaybackContext } from '$lib/stores/playbackContextStore';
|
||||
import { isTrackUnavailable, clearTrackUnavailable, subscribe as subscribeUnavailable } from '$lib/stores/unavailableTracksStore';
|
||||
import { isBlacklisted as isArtistBlacklisted } from '$lib/stores/artistBlacklistStore';
|
||||
import { showToast } from '$lib/stores/toastStore';
|
||||
import { t } from '$lib/i18n';
|
||||
import { onMount, tick } from 'svelte';
|
||||
@@ -1809,6 +1810,7 @@
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{@const trackBlacklisted = !track.isLocal && track.artistId ? isArtistBlacklisted(track.artistId) : false}
|
||||
<TrackRow
|
||||
trackId={track.isLocal ? undefined : track.id}
|
||||
number={idx + 1}
|
||||
@@ -1825,18 +1827,24 @@
|
||||
isLocal={track.isLocal}
|
||||
isUnavailable={removedFromQobuz && isOwnPlaylist}
|
||||
unavailableTooltip={removedFromQobuz ? $t('player.trackUnavailable') : undefined}
|
||||
hideFavorite={track.isLocal || removedFromQobuz}
|
||||
hideDownload={track.isLocal || removedFromQobuz}
|
||||
isBlacklisted={trackBlacklisted}
|
||||
hideFavorite={track.isLocal || removedFromQobuz || trackBlacklisted}
|
||||
hideDownload={track.isLocal || removedFromQobuz || trackBlacklisted}
|
||||
downloadStatus={downloadInfo.status}
|
||||
downloadProgress={downloadInfo.progress}
|
||||
onPlay={available ? () => handleTrackClick(track, idx) : undefined}
|
||||
onDownload={available && !track.isLocal && onTrackDownload ? () => onTrackDownload(track) : undefined}
|
||||
onRemoveDownload={available && !track.isLocal && onTrackRemoveDownload ? () => onTrackRemoveDownload(track.id) : undefined}
|
||||
onPlay={available && !trackBlacklisted ? () => handleTrackClick(track, idx) : undefined}
|
||||
onDownload={available && !track.isLocal && !trackBlacklisted && onTrackDownload ? () => onTrackDownload(track) : undefined}
|
||||
onRemoveDownload={available && !track.isLocal && !trackBlacklisted && onTrackRemoveDownload ? () => onTrackRemoveDownload(track.id) : undefined}
|
||||
menuActions={removedFromQobuz ? (isOwnPlaylist ? {
|
||||
// Only allow remove from playlist and find replacement for tracks removed from Qobuz (owned playlists only)
|
||||
onRemoveFromPlaylist: () => removeTrackFromPlaylist(track),
|
||||
onFindReplacement: () => openReplacementModal(track)
|
||||
} : {}) : available ? {
|
||||
} : {}) : trackBlacklisted ? {
|
||||
// Blacklisted: only allow navigation and info, no playback
|
||||
onGoToAlbum: !track.isLocal && track.albumId && onTrackGoToAlbum ? () => onTrackGoToAlbum(track.albumId!) : undefined,
|
||||
onGoToArtist: !track.isLocal && track.artistId && onTrackGoToArtist ? () => onTrackGoToArtist(track.artistId!) : undefined,
|
||||
onShowInfo: !track.isLocal && onTrackShowInfo ? () => onTrackShowInfo(track.id) : undefined
|
||||
} : available ? {
|
||||
onPlayNow: () => handleTrackClick(track, idx),
|
||||
onPlayNext: track.isLocal ? () => handleTrackPlayNext(track) : (onTrackPlayNext ? () => onTrackPlayNext(track) : undefined),
|
||||
onPlayLater: track.isLocal ? () => handleTrackPlayLater(track) : (onTrackPlayLater ? () => onTrackPlayLater(track) : undefined),
|
||||
|
||||
Reference in new issue
Block a user