diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f27c7c..ba99dc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Fixed +- Fixed locate track functionality to check current playßlist first +- Fixed continuous playback selecting next track from wrong context when navigating between views - Fixed discovery row buttons (import and open URL) not working in playlist view ## [0.2.8] - 2026-03-15 diff --git a/src-tauri/.cargo/audit.toml b/src-tauri/.cargo/audit.toml new file mode 100644 index 0000000..41f32fd --- /dev/null +++ b/src-tauri/.cargo/audit.toml @@ -0,0 +1,6 @@ +[advisories] +ignore = [ + # glib unsoundness in VariantStrIter - transitive dep from Tauri's GTK/webkit2gtk on Linux. + # Cannot upgrade independently; must wait for Tauri to update gtk-rs dependencies. + "RUSTSEC-2024-0429", +] diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 75aa361..7ad4a25 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -99,7 +99,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -110,7 +110,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1221,7 +1221,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1451,7 +1451,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3443,7 +3443,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.45.0", ] [[package]] @@ -4435,7 +4435,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4491,7 +4491,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4502,9 +4502,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -4922,7 +4922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5437,9 +5437,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -5829,7 +5829,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6252,7 +6252,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6790,7 +6790,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/src/lib/hooks/useAppSetup.ts b/src/lib/hooks/useAppSetup.ts index 36c9c35..25657a0 100644 --- a/src/lib/hooks/useAppSetup.ts +++ b/src/lib/hooks/useAppSetup.ts @@ -106,6 +106,7 @@ export interface AppSetupResult { deviceController: ReturnType exportController: ReturnType playlistController: ReturnType + playPreview: (release: DiscoveryRelease, trackIndex: number) => void playNextTrack: () => void playPreviousTrack: () => void onMountSetup: () => Promise<() => void> @@ -147,7 +148,7 @@ export function createAppSetup(config: AppSetupConfig): AppSetupResult { getActiveView: () => get(activeView), }) - const trackController = createTrackController( + const rawTrackController = createTrackController( { playerStore, libraryStore, @@ -169,6 +170,19 @@ export function createAppSetup(config: AppSetupConfig): AppSetupResult { } ) + // Wrap trackController.play to capture the playback queue context when + // the user initiates library playback (double-click, Enter, etc.) + const trackController = { + ...rawTrackController, + play(track: Track) { + hasLibraryQueueContext = true + libraryQueueContextActiveView = get(activeView) + libraryQueueContextPlaylistId = get(libraryStore).selectedPlaylistId + libraryQueueTracks = get(displayedTracks) + rawTrackController.play(track) + }, + } + const deviceController = createDeviceController( { devicesStore, settingsStore, toastStore }, { @@ -228,6 +242,77 @@ export function createAppSetup(config: AppSetupConfig): AppSetupResult { } ) + // ========================================================================= + // Playback Queue + // ========================================================================= + // Tracks the playback context so continuous playback, next/previous use the + // correct track list even when the user navigates to a different view. + // When the current view matches the playback context, live data is used + // (so sort changes, track adds/removes are reflected immediately). + // When navigated away, a frozen snapshot is used instead. + + // Library track queue + let hasLibraryQueueContext = false + let libraryQueueContextActiveView: ActiveView = 'library' + let libraryQueueContextPlaylistId: string | null = null + let libraryQueueTracks: Track[] = [] + + // Discovery release queue + let hasDiscoveryQueueContext = false + let discoveryQueueContextPlaylistId: string | null = null + let discoveryQueueReleases: DiscoveryRelease[] = [] + + // Keep the frozen queue snapshot up-to-date while the context view is active. + // When the user navigates away, the snapshot freezes at the last known state. + displayedTracks.subscribe((tracks) => { + if (!hasLibraryQueueContext) return + if (get(activeView) !== libraryQueueContextActiveView) return + if (get(libraryStore).selectedPlaylistId === libraryQueueContextPlaylistId) { + libraryQueueTracks = tracks + } + }) + + displayedReleases.subscribe((releases) => { + if (!hasDiscoveryQueueContext) return + const ui = get(uiStore) + if (ui.activeView !== 'discovery') return + if ((ui.selectedPlaylistId ?? null) === discoveryQueueContextPlaylistId) { + discoveryQueueReleases = releases + } + }) + + /** Get the current library track queue, using live data when the view matches. */ + function getLibraryQueue(): Track[] { + if (!hasLibraryQueueContext) return get(displayedTracks) + const currentPlaylistId = get(libraryStore).selectedPlaylistId + if (get(activeView) === libraryQueueContextActiveView && currentPlaylistId === libraryQueueContextPlaylistId) { + return get(displayedTracks) + } + return libraryQueueTracks + } + + /** Get the current discovery release queue, using live data when the view matches. */ + function getDiscoveryQueue(): DiscoveryRelease[] { + if (!hasDiscoveryQueueContext) return get(displayedReleases) + const ui = get(uiStore) + if (ui.activeView === 'discovery' && (ui.selectedPlaylistId ?? null) === discoveryQueueContextPlaylistId) { + return get(displayedReleases) + } + return discoveryQueueReleases + } + + /** + * Play a discovery preview, capturing the release queue context. + * Use this instead of playerStore.playPreview() for user-initiated preview playback. + */ + function playPreview(release: DiscoveryRelease, trackIndex: number) { + hasDiscoveryQueueContext = true + const ui = get(uiStore) + discoveryQueueContextPlaylistId = ui.activeView === 'discovery' ? (ui.selectedPlaylistId ?? null) : null + discoveryQueueReleases = get(displayedReleases) + playerStore.playPreview(release, trackIndex) + } + // ========================================================================= // Track Navigation // ========================================================================= @@ -264,8 +349,8 @@ export function createAppSetup(config: AppSetupConfig): AppSetupResult { return } - // Move to next release in the filtered view, wrapping around - const releases = get(displayedReleases) + // Move to next release in the queue (or current view as fallback), wrapping around + const releases = getDiscoveryQueue() const releaseIdx = releases.findIndex((r) => r.id === preview.releaseId) if (releaseIdx === -1 || releases.length === 0) return @@ -281,9 +366,10 @@ export function createAppSetup(config: AppSetupConfig): AppSetupResult { } const id = get(currentTrack)?.id if (!id) return - const tracks = get(displayedTracks) + const tracks = getLibraryQueue() + if (tracks.length === 0) return const idx = tracks.findIndex((t) => t.id === id) - if (idx >= 0 && idx < tracks.length - 1) trackController.play(tracks[idx + 1]) + if (idx >= 0) playerStore.play(tracks[(idx + 1) % tracks.length]) } function playPreviousTrack() { @@ -299,8 +385,8 @@ export function createAppSetup(config: AppSetupConfig): AppSetupResult { return } - // Move to previous release in the filtered view, wrapping around - const releases = get(displayedReleases) + // Move to previous release in the queue (or current view as fallback), wrapping around + const releases = getDiscoveryQueue() const releaseIdx = releases.findIndex((r) => r.id === preview.releaseId) if (releaseIdx === -1 || releases.length === 0) return @@ -316,9 +402,10 @@ export function createAppSetup(config: AppSetupConfig): AppSetupResult { } const id = get(currentTrack)?.id if (!id) return - const tracks = get(displayedTracks) + const tracks = getLibraryQueue() + if (tracks.length === 0) return const idx = tracks.findIndex((t) => t.id === id) - if (idx > 0) trackController.play(tracks[idx - 1]) + if (idx >= 0) playerStore.play(tracks[(idx - 1 + tracks.length) % tracks.length]) } // ========================================================================= @@ -342,7 +429,7 @@ export function createAppSetup(config: AppSetupConfig): AppSetupResult { for (const release of releases) { const trackIdx = findPreviewableTrackIndex(release, 'first') if (trackIdx !== -1) { - playerStore.playPreview(release, trackIdx) + playPreview(release, trackIdx) return } } @@ -732,6 +819,7 @@ export function createAppSetup(config: AppSetupConfig): AppSetupResult { deviceController, exportController, playlistController, + playPreview, playNextTrack, playPreviousTrack, onMountSetup, diff --git a/src/lib/stores/discovery.ts b/src/lib/stores/discovery.ts index 792f2fc..56c7af7 100644 --- a/src/lib/stores/discovery.ts +++ b/src/lib/stores/discovery.ts @@ -413,32 +413,11 @@ export const discoveryStore = createDiscoveryStore() export const likedOnly = derived(discoveryStore, ($discovery) => $discovery.likedOnly) -export const sortedReleases = derived(discoveryStore, ($discovery) => { - let releases = [...$discovery.releases] - - // Apply liked filter - if ($discovery.likedOnly) { - releases = releases.filter((r) => r.tracks.some((t) => t.is_liked)) - } - - // Apply client-side search filter - if ($discovery.filter.search) { - const search = $discovery.filter.search.toLowerCase() - releases = releases.filter( - (r) => - r.artist?.toLowerCase().includes(search) || - r.title?.toLowerCase().includes(search) || - r.label?.toLowerCase().includes(search) || - r.notes?.toLowerCase().includes(search) || - r.tracks.some((t) => t.name?.toLowerCase().includes(search)) - ) - } - - // Apply sorting - const { field, direction } = $discovery.sort +function sortReleases(releases: DiscoveryRelease[], sort: DiscoverySortConfig): DiscoveryRelease[] { + const { field, direction } = sort const dir = direction === 'asc' ? 1 : -1 - releases.sort((a, b) => { + return [...releases].sort((a, b) => { let cmp = 0 if (field === 'release_date') { const aDate = a.release_date ? new Date(a.release_date).getTime() : NaN @@ -456,12 +435,34 @@ export const sortedReleases = derived(discoveryStore, ($discovery) => { if (aVal < bVal) cmp = -1 * dir else if (aVal > bVal) cmp = 1 * dir } - // Tiebreaker: sort by id for deterministic order when values are equal if (cmp !== 0) return cmp return a.id < b.id ? -1 : a.id > b.id ? 1 : 0 }) +} - return releases +export const sortedReleases = derived(discoveryStore, ($discovery) => { + let releases = [...$discovery.releases] + + // Apply liked filter + if ($discovery.likedOnly) { + releases = releases.filter((r) => r.tracks.some((t) => t.is_liked)) + } + + // Apply client-side search filter + if ($discovery.filter.search) { + const search = $discovery.filter.search.toLowerCase() + releases = releases.filter( + (r) => + r.artist?.toLowerCase().includes(search) || + r.title?.toLowerCase().includes(search) || + r.label?.toLowerCase().includes(search) || + r.notes?.toLowerCase().includes(search) || + r.tracks.some((t) => t.name?.toLowerCase().includes(search)) + ) + } + + // Apply sorting + return sortReleases(releases, $discovery.sort) }) export const displayedReleases = derived( @@ -500,7 +501,7 @@ export const displayedReleases = derived( ) } - return releases + return sortReleases(releases, $discovery.sort) } ) diff --git a/src/lib/stores/player.ts b/src/lib/stores/player.ts index d56c767..f2ce1c9 100644 --- a/src/lib/stores/player.ts +++ b/src/lib/stores/player.ts @@ -319,6 +319,10 @@ function createPlayerStore() { const track = release.tracks[trackIndex] if (!track) return + // Clear stale preview events before the async gap to prevent the old + // error handler from firing when audio.src='' triggers an error event + clearPreviewEvents() + // Stop library audio if playing if (state.playbackSource === 'library' && state.playbackState.is_playing) { try { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 30f709a..b07701a 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -23,7 +23,6 @@ sortedTracks, displayedTracks, trackCount, - playerStore, currentTrack, tagsStore, playlistsStore, @@ -101,6 +100,7 @@ deviceController, exportController, playlistController, + playPreview, playNextTrack, playPreviousTrack, onMountSetup, @@ -280,10 +280,10 @@ const preview = get(previewInfo) if (!preview) return + // If already on discovery view (main or playlist), check current displayed releases first const currentView = get(activeView) - if (currentView === 'discovery' && !selectedPlaylistId && !selectedFolderId) { - // Already on main discovery view — check if release is visible - const releases = get(sortedReleases) + if (currentView === 'discovery' && !selectedFolderId) { + const releases = get(displayedReleases) if (releases.some((r) => r.id === preview.releaseId)) { uiStore.setSelectedReleases(new Set([preview.releaseId])) expandedReleaseIds.expand(preview.releaseId) @@ -292,7 +292,7 @@ } } - // Navigate to main discovery view and await data load + // Not found in current view — fall back to main discovery list await navigateToMainView('discovery') const releases = get(sortedReleases) @@ -307,9 +307,9 @@ const track = get(currentTrack) if (!track) return + // If already on library view (main or playlist), check current displayed tracks first const currentView = get(activeView) - if (currentView === 'library' && !selectedPlaylistId && !selectedFolderId) { - // Already on main library view — check if track is visible + if (currentView === 'library' && !selectedFolderId) { const tracks = get(displayedTracks) if (tracks.some((t) => t.id === track.id)) { uiStore.setSelectedTracks(new Set([track.id])) @@ -318,7 +318,7 @@ } } - // Navigate to main library view and await data load + // Not found in current view — fall back to main library list await navigateToMainView('library') const tracks = get(displayedTracks) @@ -422,7 +422,7 @@ return true }) if (firstPlayable >= 0) { - playerStore.playPreview(release, firstPlayable) + playPreview(release, firstPlayable) return } } @@ -436,7 +436,7 @@ track?.duration_ms && (PREVIEWABLE_SOURCES.has(release.source_type) || (release.source_type === 'discogs' && track?.video_id !== null)) if (canPlay && release.tracks.length > 0) { - playerStore.playPreview(release, trackIndex) + playPreview(release, trackIndex) } }