Merge branch 'bugfix/unavailable-track-row-dim' into pre-release
This commit is contained in:
7 files changed
+139
-6
No files matched your search
@@ -255,24 +255,25 @@
|
||||
<div
|
||||
class="track-row"
|
||||
class:playing={isActiveTrack || isPlaying}
|
||||
class:hovered={isHovered && !isActiveTrack && !isPlaying && !isBlacklisted}
|
||||
class:hovered={isHovered && !isActiveTrack && !isPlaying && !isBlacklisted && !isUnavailable}
|
||||
class:compact
|
||||
class:blacklisted={isBlacklisted}
|
||||
class:unavailable={isUnavailable}
|
||||
class:selected
|
||||
data-track-id={trackId ?? undefined}
|
||||
draggable={!!trackId && !isBlacklisted}
|
||||
draggable={!!trackId && !isBlacklisted && !isUnavailable}
|
||||
ondragstart={handleDragStart}
|
||||
onmouseenter={() => (isHovered = true)}
|
||||
onmouseleave={() => (isHovered = false)}
|
||||
onclick={selectable ? onToggleSelect : (isBlacklisted ? undefined : onPlay)}
|
||||
onclick={selectable ? onToggleSelect : (isBlacklisted || isUnavailable ? undefined : onPlay)}
|
||||
oncontextmenu={(e) => {
|
||||
if (isBlacklisted || selectable) return;
|
||||
if (isBlacklisted || isUnavailable || selectable) return;
|
||||
e.preventDefault();
|
||||
contextMenuPos = { x: e.clientX, y: e.clientY };
|
||||
}}
|
||||
role="button"
|
||||
tabindex={isBlacklisted ? -1 : 0}
|
||||
onkeydown={(e) => e.key === 'Enter' && !isBlacklisted && (selectable ? onToggleSelect?.(e as unknown as MouseEvent) : onPlay?.())}
|
||||
tabindex={isBlacklisted || isUnavailable ? -1 : 0}
|
||||
onkeydown={(e) => e.key === 'Enter' && !isBlacklisted && !isUnavailable && (selectable ? onToggleSelect?.(e as unknown as MouseEvent) : onPlay?.())}
|
||||
>
|
||||
<!-- Checkbox (select mode) — always rendered, animates in/out so the
|
||||
column doesn't pop when select mode toggles. -->
|
||||
@@ -652,6 +653,18 @@
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Unavailable track styles — mirrors blacklisted: dim the whole row, kill
|
||||
hover background, kill pointer cursor. The track-number cell still shows
|
||||
the red CircleAlert icon (handled by .track-number.unavailable). */
|
||||
.track-row.unavailable {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.track-row.unavailable:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.track-number :global(.play-icon) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import TrackRow from './TrackRow.svelte';
|
||||
import type { OfflineCacheStatus } from '$lib/stores/offlineCacheState';
|
||||
import { isBlacklisted as isArtistBlacklisted } from '$lib/stores/artistBlacklistStore';
|
||||
import {
|
||||
isTrackRemovedFromQobuz,
|
||||
subscribe as subscribeUnavailable
|
||||
} from '$lib/stores/unavailableTracksStore';
|
||||
import { restoreScrollOnBackForward } from '$lib/utils/scrollRestore';
|
||||
import { formatTrackTitle } from '$lib/utils/trackTitle';
|
||||
|
||||
@@ -341,6 +346,19 @@
|
||||
}
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
// Counter incremented when the unavailable-tracks store mutates so reactive
|
||||
// reads in {@const} blocks re-evaluate. Skipped for `isLocal` lists since
|
||||
// local files don't carry the streamable concept.
|
||||
let unavailableVersion = $state(0);
|
||||
let unsubscribeUnavailable: (() => void) | null = null;
|
||||
// Reading `unavailableVersion` here registers the dependency so the {@const}
|
||||
// calling this re-evaluates when the store changes. Local lists short-circuit
|
||||
// to false since streamable doesn't apply to local files.
|
||||
function checkTrackUnavailable(track: { id: number; streamable?: boolean }): boolean {
|
||||
if (isLocal) return false;
|
||||
void unavailableVersion;
|
||||
return isTrackRemovedFromQobuz(track);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (containerEl) {
|
||||
@@ -355,10 +373,17 @@
|
||||
}
|
||||
|
||||
restoreScrollOnBackForward(containerEl, (v) => scrollTop = v);
|
||||
|
||||
if (!isLocal) {
|
||||
unsubscribeUnavailable = subscribeUnavailable(() => {
|
||||
unavailableVersion++;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
resizeObserver?.disconnect();
|
||||
unsubscribeUnavailable?.();
|
||||
});
|
||||
|
||||
// Public method to scroll to a group
|
||||
@@ -404,6 +429,7 @@
|
||||
{@const downloadInfo = getOfflineCacheStatus?.(trackId) ?? { status: 'none' as const, progress: 0 }}
|
||||
{@const isTrackDownloaded = downloadInfo.status === 'ready'}
|
||||
{@const trackBlacklisted = !isLocal && artistId ? isArtistBlacklisted(artistId) : false}
|
||||
{@const trackUnavailable = checkTrackUnavailable(item.track)}
|
||||
<TrackRow
|
||||
trackId={trackId}
|
||||
number={getTrackNumber(item.track, item.index)}
|
||||
@@ -418,6 +444,8 @@
|
||||
isPlaying={isPlaybackActive && activeTrackId === trackId}
|
||||
isActiveTrack={activeTrackId === trackId}
|
||||
isBlacklisted={trackBlacklisted}
|
||||
isUnavailable={trackUnavailable}
|
||||
unavailableTooltip={trackUnavailable ? $t('player.trackUnavailable') : undefined}
|
||||
selectable={selectable}
|
||||
selected={selectedIds?.has(trackId) ?? false}
|
||||
dragTrackIds={selectable && selectedIds?.has(trackId) && selectedIds.size > 1 ? [...selectedIds] : undefined}
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
toggleAlbumFavorite
|
||||
} from '$lib/stores/albumFavoritesStore';
|
||||
import { isBlacklisted as isArtistBlacklisted } from '$lib/stores/artistBlacklistStore';
|
||||
import {
|
||||
isTrackRemovedFromQobuz,
|
||||
subscribe as subscribeUnavailable
|
||||
} from '$lib/stores/unavailableTracksStore';
|
||||
import ImageLightbox from '../ImageLightbox.svelte';
|
||||
import BookletViewer from '../BookletViewer.svelte';
|
||||
import type { QobuzGoody } from '$lib/types';
|
||||
@@ -59,6 +63,8 @@
|
||||
samplingRate?: number;
|
||||
isrc?: string;
|
||||
parental_warning?: boolean;
|
||||
/** API streamable flag — false when the track has been removed from Qobuz. */
|
||||
streamable?: boolean;
|
||||
}
|
||||
|
||||
interface ArtistAlbum {
|
||||
@@ -203,6 +209,15 @@
|
||||
|
||||
let isFavorite = $state(false);
|
||||
let isFavoriteLoading = $state(false);
|
||||
// Counter incremented when the unavailable-tracks store mutates so reactive
|
||||
// reads in {@const} blocks re-evaluate.
|
||||
let unavailableVersion = $state(0);
|
||||
// Reading `unavailableVersion` here registers the dependency so the {@const}
|
||||
// calling this re-evaluates when the store changes.
|
||||
function checkTrackUnavailable(track: { id: number; streamable?: boolean }): boolean {
|
||||
void unavailableVersion;
|
||||
return isTrackRemovedFromQobuz(track);
|
||||
}
|
||||
let lightboxOpen = $state(false);
|
||||
let bookletOpen = $state(false);
|
||||
let descriptionExpanded = $state(false);
|
||||
@@ -563,9 +578,14 @@
|
||||
gradientEnabled = isAlbumHeaderGradientEnabled();
|
||||
});
|
||||
|
||||
const unsubscribeUnavailable = subscribeUnavailable(() => {
|
||||
unavailableVersion++;
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
unsubscribeAppearance();
|
||||
unsubscribeUnavailable();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -866,6 +886,7 @@
|
||||
{@const isTrackDownloaded = downloadInfo.status === 'ready'}
|
||||
{@const trackArtistId = track.artistId ?? album.artistId}
|
||||
{@const trackBlacklisted = trackArtistId ? isArtistBlacklisted(trackArtistId) : false}
|
||||
{@const trackUnavailable = checkTrackUnavailable(track)}
|
||||
<TrackRow
|
||||
trackId={track.id}
|
||||
number={track.number}
|
||||
@@ -877,6 +898,8 @@
|
||||
isPlaying={isPlaybackActive && activeTrackId === track.id}
|
||||
isActiveTrack={activeTrackId === track.id}
|
||||
isBlacklisted={trackBlacklisted}
|
||||
isUnavailable={trackUnavailable}
|
||||
unavailableTooltip={trackUnavailable ? $t('player.trackUnavailable') : undefined}
|
||||
selectable={multiSelectMode}
|
||||
selected={multiSelectedIds.has(track.id)}
|
||||
dragTrackIds={multiSelectMode && multiSelectedIds.has(track.id) ? [...multiSelectedIds] : undefined}
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
import { showToast } from '$lib/stores/toastStore';
|
||||
import { setPlaybackContext } from '$lib/stores/playbackContextStore';
|
||||
import { isBlacklisted as isArtistBlacklisted } from '$lib/stores/artistBlacklistStore';
|
||||
import {
|
||||
isTrackRemovedFromQobuz,
|
||||
subscribe as subscribeUnavailable
|
||||
} from '$lib/stores/unavailableTracksStore';
|
||||
import { getUserItem, setUserItem } from '$lib/utils/userStorage';
|
||||
import { applyShiftRange, isSelectAllShortcut } from '$lib/utils/multiSelect';
|
||||
import type { OfflineCacheStatus } from '$lib/stores/offlineCacheState';
|
||||
@@ -477,16 +481,28 @@
|
||||
return `${mins}m`;
|
||||
});
|
||||
|
||||
// Unavailable-tracks reactivity
|
||||
let unavailableVersion = $state(0);
|
||||
let unsubscribeUnavailable: (() => void) | null = null;
|
||||
function checkTrackUnavailable(track: { id: number; streamable?: boolean }): boolean {
|
||||
void unavailableVersion;
|
||||
return isTrackRemovedFromQobuz(track);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (autoRunDone) return;
|
||||
autoRunDone = true;
|
||||
void generateDailyQ('read-write');
|
||||
document.addEventListener('click', handleGlobalClick);
|
||||
unsubscribeUnavailable = subscribeUnavailable(() => {
|
||||
unavailableVersion++;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
dismissAlgoTooltip();
|
||||
document.removeEventListener('click', handleGlobalClick);
|
||||
unsubscribeUnavailable?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -603,6 +619,7 @@
|
||||
|
||||
{#each filteredTracks as track, index}
|
||||
{@const trackBlacklisted = track.performer?.id ? isArtistBlacklisted(track.performer.id) : false}
|
||||
{@const trackUnavailable = checkTrackUnavailable(track)}
|
||||
{@const cacheStatus = getTrackOfflineCacheStatus?.(track.id) ?? { status: 'none' as const, progress: 0 }}
|
||||
{@const isTrackDownloaded = cacheStatus.status === 'ready'}
|
||||
{@const displayTrack = toDisplayTrack(track)}
|
||||
@@ -619,6 +636,8 @@
|
||||
isPlaying={isPlaybackActive && activeTrackId === track.id}
|
||||
isActiveTrack={activeTrackId === track.id}
|
||||
isBlacklisted={trackBlacklisted}
|
||||
isUnavailable={trackUnavailable}
|
||||
unavailableTooltip={trackUnavailable ? $t('player.trackUnavailable') : undefined}
|
||||
selectable={multiSelectMode}
|
||||
selected={multiSelectedIds.has(track.id)}
|
||||
dragTrackIds={multiSelectMode && multiSelectedIds.has(track.id) ? [...multiSelectedIds] : undefined}
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
import { showToast } from '$lib/stores/toastStore';
|
||||
import { setPlaybackContext } from '$lib/stores/playbackContextStore';
|
||||
import { isBlacklisted as isArtistBlacklisted } from '$lib/stores/artistBlacklistStore';
|
||||
import {
|
||||
isTrackRemovedFromQobuz,
|
||||
subscribe as subscribeUnavailable
|
||||
} from '$lib/stores/unavailableTracksStore';
|
||||
import { getUserItem, setUserItem } from '$lib/utils/userStorage';
|
||||
import { applyShiftRange, isSelectAllShortcut } from '$lib/utils/multiSelect';
|
||||
import type { OfflineCacheStatus } from '$lib/stores/offlineCacheState';
|
||||
@@ -532,16 +536,29 @@
|
||||
return `${mins} min`;
|
||||
});
|
||||
|
||||
// Unavailable-tracks reactivity: bumped on store mutations so {@const}
|
||||
// reads in the track list re-evaluate the dim state.
|
||||
let unavailableVersion = $state(0);
|
||||
let unsubscribeUnavailable: (() => void) | null = null;
|
||||
function checkTrackUnavailable(track: { id: number; streamable?: boolean }): boolean {
|
||||
void unavailableVersion;
|
||||
return isTrackRemovedFromQobuz(track);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (autoRunDone) return;
|
||||
autoRunDone = true;
|
||||
void generateTopQ('read-write');
|
||||
document.addEventListener('click', handleGlobalClick);
|
||||
unsubscribeUnavailable = subscribeUnavailable(() => {
|
||||
unavailableVersion++;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
dismissAlgoTooltip();
|
||||
document.removeEventListener('click', handleGlobalClick);
|
||||
unsubscribeUnavailable?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -659,6 +676,7 @@
|
||||
|
||||
{#each filteredTracks as track, index}
|
||||
{@const trackBlacklisted = track.performer?.id ? isArtistBlacklisted(track.performer.id) : false}
|
||||
{@const trackUnavailable = checkTrackUnavailable(track)}
|
||||
{@const cacheStatus = getTrackOfflineCacheStatus?.(track.id) ?? { status: 'none' as const, progress: 0 }}
|
||||
{@const isTrackDownloaded = cacheStatus.status === 'ready'}
|
||||
{@const displayTrack = toDisplayTrack(track)}
|
||||
@@ -675,6 +693,8 @@
|
||||
isPlaying={isPlaybackActive && activeTrackId === track.id}
|
||||
isActiveTrack={activeTrackId === track.id}
|
||||
isBlacklisted={trackBlacklisted}
|
||||
isUnavailable={trackUnavailable}
|
||||
unavailableTooltip={trackUnavailable ? $t('player.trackUnavailable') : undefined}
|
||||
selectable={multiSelectMode}
|
||||
selected={multiSelectedIds.has(track.id)}
|
||||
dragTrackIds={multiSelectMode && multiSelectedIds.has(track.id) ? [...multiSelectedIds] : undefined}
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
import { showToast } from '$lib/stores/toastStore';
|
||||
import { setPlaybackContext } from '$lib/stores/playbackContextStore';
|
||||
import { isBlacklisted as isArtistBlacklisted } from '$lib/stores/artistBlacklistStore';
|
||||
import {
|
||||
isTrackRemovedFromQobuz,
|
||||
subscribe as subscribeUnavailable
|
||||
} from '$lib/stores/unavailableTracksStore';
|
||||
import { getUserItem, setUserItem } from '$lib/utils/userStorage';
|
||||
import { applyShiftRange, isSelectAllShortcut } from '$lib/utils/multiSelect';
|
||||
import type { OfflineCacheStatus } from '$lib/stores/offlineCacheState';
|
||||
@@ -475,16 +479,28 @@
|
||||
return `${mins} ` + $t('time.minutes');
|
||||
});
|
||||
|
||||
// Unavailable-tracks reactivity
|
||||
let unavailableVersion = $state(0);
|
||||
let unsubscribeUnavailable: (() => void) | null = null;
|
||||
function checkTrackUnavailable(track: { id: number; streamable?: boolean }): boolean {
|
||||
void unavailableVersion;
|
||||
return isTrackRemovedFromQobuz(track);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (autoRunDone) return;
|
||||
autoRunDone = true;
|
||||
void generateDailyQ('read-write');
|
||||
document.addEventListener('click', handleGlobalClick);
|
||||
unsubscribeUnavailable = subscribeUnavailable(() => {
|
||||
unavailableVersion++;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
dismissAlgoTooltip();
|
||||
document.removeEventListener('click', handleGlobalClick);
|
||||
unsubscribeUnavailable?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -598,6 +614,7 @@
|
||||
|
||||
{#each filteredTracks as track, index}
|
||||
{@const trackBlacklisted = track.performer?.id ? isArtistBlacklisted(track.performer.id) : false}
|
||||
{@const trackUnavailable = checkTrackUnavailable(track)}
|
||||
{@const cacheStatus = getTrackOfflineCacheStatus?.(track.id) ?? { status: 'none' as const, progress: 0 }}
|
||||
{@const isTrackDownloaded = cacheStatus.status === 'ready'}
|
||||
{@const displayTrack = toDisplayTrack(track)}
|
||||
@@ -614,6 +631,8 @@
|
||||
isPlaying={isPlaybackActive && activeTrackId === track.id}
|
||||
isActiveTrack={activeTrackId === track.id}
|
||||
isBlacklisted={trackBlacklisted}
|
||||
isUnavailable={trackUnavailable}
|
||||
unavailableTooltip={trackUnavailable ? $t('player.trackUnavailable') : undefined}
|
||||
selectable={multiSelectMode}
|
||||
selected={multiSelectedIds.has(track.id)}
|
||||
dragTrackIds={multiSelectMode && multiSelectedIds.has(track.id) ? [...multiSelectedIds] : undefined}
|
||||
|
||||
@@ -85,6 +85,17 @@ export function getUnavailableTrackIds(): Set<number> {
|
||||
return new Set(unavailableTrackIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined check: a track counts as removed-from-Qobuz when either the API
|
||||
* marks it unstreamable or playback hit a TrackUnavailable error this session
|
||||
* (cached in the store). Consumer views pass the result to TrackRow's
|
||||
* `isUnavailable` prop. Subscribe to the store separately for reactivity.
|
||||
*/
|
||||
export function isTrackRemovedFromQobuz(track: { id: number; streamable?: boolean }): boolean {
|
||||
if (track.streamable === false) return true;
|
||||
return isTrackUnavailable(track.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to changes in unavailable tracks
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user