Share the retained history lookup snapshot across albums, playlists, artists, home search results, and library folders. Reuse derived lookups until the track list is replaced.
Test reuse with 4,000 items and invalidation after replacement, sorting, and filtering.
Every single-track enqueue now shows the shared added-to-queue
snackbar with a View action into Library (six call sites through one
helper). URL fetch failures in Home and via share intent gain a Retry
action; unrecognized-URL errors deliberately keep none since retrying
is deterministic.
The list-to-header cover flights read as a gimmick and add per-frame
cost during navigation. Removed the heroTag plumbing end to end
(recents, search rows, discography cards, Album/Artist/Downloaded
headers, motion banners). Kept: the mini player expand hero, the
library-to-track-metadata hero, real loading headers, and the
loading crossfade.
Albums/artists with a motion banner have no square cover, so the Hero
had no destination: the open flight ended nowhere and there was no
return flight. The Hero now rides the full-bleed banner, flying the
still cover image as the shuttle so the video doesn't restart mid-air.
Tab navigators get a HeroController (nested navigators have none, so
Heroes never flew inside tabs). Search result rows, recent-access items,
and artist discography cards tag their covers; Album/Artist/Downloaded
screens accept a heroTag and mount the matching Hero on the header
cover. Tags are scoped per list section to stay unique per page.
- album/playlist/library-folder/track-metadata adopt
CollapsingHeaderScrollMixin; artist/library-folder adopt
SelectionModeMixin
- HeaderCircleButton and HeaderMetaRow replace per-screen copies
in album and playlist headers; dead _tallHeader flag removed
- SelectionBottomBar gains optional subtitle labels and backs the
playlist selection bar in queue tab
The local/downloaded album screens had drifted copies of the album
header, selection bottom bar, disc chip, batch convert/ReplayGain trio,
and assorted small helpers. All now delegate to the shared widgets, with
the online album screen's design as the reference, and both album screens
run batch actions through the queue_tab engine via UnifiedLibraryItem
(strict-superset implementation covering both DB writebacks and SAF
paths). Also folds the remaining per-screen helper copies (cover URL,
error card, byte/clock formatting, readPositiveInt) into their shared
homes.
Intentional deltas: selection-bar strings follow queue_tab's l10n keys,
both providers reload after a conversion, and audio-analysis durations
round instead of floor.
legacyProviderIdFromResourceId and stripPrefixedResourceId were
copy-pasted in the album, artist, and playlist screens with slightly
different null semantics; the shared nullable variant plus explicit
?? 'spotify' fallbacks makes the difference visible at the call sites.
Extract InLibraryBadge (pasted verbatim in five screens) and
playLocalIfAvailable (duplicated in three) into shared code, and delete
SearchScreen, LibraryPlaylistsScreen, and CollapsingHeader, which are
imported nowhere in lib/.
- search_screen caches PreviewPlayerController in initState to avoid
ref access during dispose
- extension_provider: convert enabledExtensions/searchProviders getters
to methods
- artist_screen: drop redundant null assertions on motion banner inputs
Add a MotionHeaderBanner (video_player) that plays a looping muted HLS header video with a static image fallback for artist, album, and playlist screens. The Go backend now exposes header_video, header_image, and audio_traits from extensions. Album/playlist headers show the release year and Dolby Atmos / Lossless badges inline, full date and song count in a footer, a centered square cover when no video is present, and a full-bleed video when one is.
- app: clear displayFeatures so bottom sheets/dialogs center on large/foldable screens
- edit metadata sheet: card sections, modern label-above inputs, elegant collapsible headers, removed title icon
- convert + batch convert: modern card-based sheets; shared BatchConvertSheet widget
- queue: keep selection toolbar hidden until modal close animation finishes
- 24-bit and In Library badges now use primary dynamic color
- artist skeleton: remove duplicate name/listeners lines, keep cover placeholder
- files settings: own filename-format controller in a StatefulWidget to fix use-after-dispose crash
The HIGH-quality lossy format picker can now produce an AAC/M4A 320 kbps output alongside MP3 and Opus. FFmpegService.convertM4aToLossy/convertAudioFormat, the Dart queue pipeline, the Kotlin finalizer, and the library database format helper all route .m4a through a unified aac codec path and tag the resulting file with the M4A metadata writer. The Lossy Format setting gains a new option, and the track metadata convert dialog lists AAC next to the other targets.
Apple Music lyrics gain a 'eLRC word sync' switch (default off). When disabled the pax-to-LRC formatter strips inline word timestamps, producing line-synced LRC that is safer for players that choke on eLRC; enabling it restores the previous word-by-word behaviour. The change propagates through SetLyricsFetchOptions and invalidates the global lyrics cache on toggle.
Broad l10n migration: roughly 400 previously hardcoded English strings across queue, settings, track metadata, repo, audio analysis, setup and extension screens now live in the ARB catalog, with matching plural/placeholder forms. No behaviour change beyond localisation. Existing and new unit tests (lyrics eLRC toggle and Dart settings round-trip) pass.
Replace in-memory list merging in the queue tab with fully database-
backed pagination using ATTACH DATABASE to join library and history
tables in a single UNION ALL query.
Queue tab:
- Remove localLibraryAllItemsProvider and _queueHistoryStatsProvider
- Add _queueLibraryPageProvider and _queueLibraryCountsProvider backed
by LibraryDatabase.getQueueTrackPage/getQueueCounts/getQueueAlbumPage
- Implement infinite scroll via _handleLibraryScrollNotification with
_libraryPageLimit growing by 300 per batch
- Album/single/total counts computed via SQL GROUP BY aggregates
History database (v5 -> v8):
- v6: add idx_history_track_artist index
- v7: add history_path_keys table for cross-DB dedup, backfill from
existing rows
- v8: add spotify_id_norm, isrc_norm, match_key normalized columns
with indexes, backfill from existing data
- Add getAlbumTracks, findByTrackAndArtist, getGroupedCounts,
existsTrack, findExistingTrack, existingTrackKeys batch lookup
- deleteBySpotifyId now returns deleted count for accurate totalCount
- All write paths maintain history_path_keys consistency
Library database (v7 -> v8):
- v8: add library_path_keys table for cross-DB dedup
- Add getQueueTrackPage, getQueueCounts, getQueueAlbumPage with
ATTACH DATABASE for cross-DB UNION ALL queries
- Dedup local items against history via path_keys JOIN
- All write/delete paths maintain library_path_keys consistency
Download history provider:
- Load only 100 recent items into state.items at startup
- Store lookupItems as immutable List field instead of recomputing
from maps on every access
- Add async fallback to DB in _putInMemoryHistory for items outside
the 100-item window
- Add downloadHistoryPageProvider, downloadHistoryGroupedCountsProvider,
downloadedAlbumTracksProvider, downloadHistoryBatchExistsProvider
- Add catchError to adoptNativeHistoryItem async block
- Fix removeBySpotifyId to query actual DB count instead of decrement
Screen migrations:
- album/artist/playlist/home screens use async DB lookups instead of
sync in-memory state for track existence and playback resolution
- downloaded_album_screen uses downloadedAlbumTracksProvider
- library_tracks_folder_screen uses downloadHistoryBatchExistsProvider
for skip-downloaded checks and cover resolution
Replace the full in-memory List<LocalLibraryItem> in LocalLibraryState
with a lightweight lookup index (ISRCs, matchKeys, filePathById) and
database-backed FutureProvider.family pagination providers.
Database changes:
- Add library schema v7 with normalized lookup columns (track_name_norm,
artist_name_norm, album_name_norm, album_artist_norm, match_key,
album_key) and corresponding indexes
- Backfill normalized columns on migration from v6
- Add getPage, getPageCount, getAlbumPage, getAlbumCount, getLookupIndex,
getCoverPaths, getByFilePath, findFirstByTrackAndArtist DB methods
Provider changes:
- LocalLibraryState no longer holds items list; uses totalCount and
loadedIndexVersion for change tracking
- Deprecate synchronous getByIsrc/findByTrackAndArtist (return null);
add async findExistingAsync, getByIsrcAsync, getById on notifier
- Add localLibraryPageProvider, localLibraryAlbumPageProvider,
localLibraryAllItemsProvider family providers for paginated access
- Add localLibraryCoverProvider and localLibraryFirstCoverProvider
for async cover path resolution from DB
Screen migrations:
- album/artist/playlist screens use findExistingAsync for playback
- library_tracks_folder_screen uses async cover providers and
existsInLibrary for local library indicator
- queue_tab watches localLibraryAllItemsProvider instead of state.items
- library_settings_page uses state.totalCount
- playback_provider uses findExistingAsync
Track metadata screen:
- Replace pushReplacement navigation with in-place state swap using
AnimatedSwitcher for smooth cross-fade transitions on track swipe
- Add metadataLoadGeneration counter to prevent stale async callbacks
- Reset all transient state (lyrics, cover, file check) on track change
- Add CollectionArtistEntry model with toJson/fromJson and artistCollectionKey helper
- Create favorite_artists table in SQLite with DB migration v1→v2
- Implement toggleFavoriteArtist/removeFavoriteArtist in LibraryCollectionsNotifier
- Add FavoriteArtistsScreen with list view, empty state, and artist navigation
- Add heart toggle button on ArtistScreen header (reactive via Riverpod selector)
- Integrate favorite artists folder in queue_tab collection grid/list views
- Add 8 new localization strings across all 13 locale files
- Extract home_tab.dart helpers/widgets into home_tab_helpers.dart and home_tab_widgets.dart using Dart part files
- Extract queue_tab.dart helpers/widgets into queue_tab_helpers.dart and queue_tab_widgets.dart using Dart part files
- Extract track_metadata_edit_sheet.dart from track_metadata_screen.dart using Dart part file
- Refactor _FileExistsListenableCache into a standalone class in queue_tab_helpers.dart
- Fix artist_screen.dart: replace unreliable findAncestorStateOfType with GlobalKey for _FetchingProgressDialog progress updates
- DRY platform_bridge.dart: extract common JSON decode patterns into reusable helper methods (_decodeRequiredMapResult, _decodeNullableMapResult, _decodeMapListResult, _decodeStringListResult)
- Replace hardcoded provider prefix checks with resolveEffectiveMetadataProvider using replacesBuiltInProviders manifest capability
- Add preparing/downloading/finalizing progress status constants and SetItemPreparing/SetItemDownloading APIs
- Expose setDownloadStatus to extension JS runtime for fine-grained progress control
- Skip lyrics search for instrumental tracks detected by title heuristic
- Pass tidal/qobuz IDs to extension checkAvailability for richer matching
- Add shouldAbortCancelledFallback helper for robust cancellation propagation
- Add resolvePreferredTrackIDForExtension for intelligent track ID selection per extension
- Remove ambiguous Auto/Default search provider option, always resolve to concrete provider
- Add tests for shouldAbortCancelledFallback and progress status transitions
Delete the entire Qobuz downloader implementation (qobuz.go, qobuz_test.go)
including all API clients, search, metadata, download, and track matching
code. Empty the builtInProviderRegistry now that all built-in providers are
retired. Remove Qobuz-specific exports (SearchQobuzAll, GetQobuzMetadata,
ParseQobuzURLExport) and the downloadWithBuiltInQobuz adapter. Stub out
PreWarmTrackCache and cache management since no built-in providers remain.
Move qobuz cover upgrade regex to cover.go. Update Dart screens, providers,
and localization strings for the provider-agnostic UI.
Remove Tidal from built-in provider registry (metadata, search, download,
URL parsing) and delete tidal.go. Introduce extension runtime APIs for
lyrics lookup (getLyricsLRC), ISRC existence check (checkISRCExists), and
ISRC index management (addToISRCIndex). Refactor extension download response
construction into normalizeExtensionDownloadResult/overlayExtensionDownloadMetadata
helpers with AlreadyExists support and ISRC indexing. Switch download mirrors
to DoRequestWithUserAgent for ISP blocking detection. Add 50+ new
localization keys and accessibility labels across all supported locales.
Replace hardcoded Tidal/Qobuz switch/case with builtInProviderSpec registry
pattern. Unify searchTidalAll/searchQobuzAll into searchProviderAll,
getDeezerMetadata/getTidalMetadata/getQobuzMetadata into getProviderMetadata,
and parseDeezerUrl/parseQobuzUrl/parseTidalUrl into parseProviderUrl. Remove
extension-specific getAlbum/Playlist/ArtistWithExtension in favor of generic
getProviderMetadata routing. Extract provider UI helpers into
provider_ui_utils.dart. Preserve track_number fallback for zero-value
TrackNumber in album/playlist track lists.
- Add reference-counted cancel entries to prevent premature cleanup when multiple operations share the same itemID
- Propagate cancellation to DownloadTrack, DownloadWithFallback, DownloadWithExtensionsJSON, extension providers, and ISRC search
- Fetch album artist from MusicBrainz when missing during download and re-enrich
- Make ALBUMARTIST tag nullable to avoid writing artistName as album artist
- Add home feed 'Off' option in extension settings
- Skip deezer in download provider priority sanitization
Resolve API (api.zarz.moe):
- Refactor songlink.go: Spotify URLs use resolve API, non-Spotify uses SongLink API
- Add SongLink fallback when resolve API fails for Spotify (two-layer resilience)
- Remove dead code: page parser, XOR-obfuscated keys, legacy helpers
Multi-artist tag fix (#288):
- Add RewriteSplitArtistTags() in Go to rewrite ARTIST/ALBUMARTIST as split Vorbis comments
- Wire method channel handler in Android (MainActivity.kt) and iOS (AppDelegate.swift)
- Add PlatformBridge.rewriteSplitArtistTags() in Dart
- Call native FLAC rewriter after FFmpeg embed when split_vorbis mode is active
- Extract deezerTrackArtistDisplay() helper to use Contributors in album/playlist tracks
Code cleanup:
- Remove unused imports, dead code, and redundant comments across Go and Dart
- Fix build: remove stale getQobuzDebugKey() reference in deezer_download.go
Enable strict-casts, strict-inference, and strict-raw-types in
analysis_options.yaml. Add custom_lint with riverpod_lint. Fix all
resulting type warnings with explicit type parameters and safer casts.
Also improves APK update checker to detect device ABIs for correct
variant selection and fixes Deezer artist name parsing edge case.
- Add animation_utils.dart with skeleton loaders, staggered list animations,
animated checkboxes, badge bump, download success overlay, and shared
page route helper
- Replace CircularProgressIndicator with shimmer skeleton loaders across
album, artist, playlist, search, store, and extension screens
- Unify page transitions via slidePageRoute (MaterialPageRoute) for
Android predictive back gesture support
- Extract AnimatedSelectionCheckbox with configurable unselectedColor
to preserve original transparent/opaque backgrounds per context
- Add swipe-to-dismiss on download queue items with confirmDismiss
dialog for active downloads to prevent accidental cancellation
- Add Hero animations for cover art transitions between list and detail
- Add AnimatedBadge bump on navigation bar badge count changes
- Add DownloadSuccessOverlay green flash on download completion
- Restore fine-grained ref.watch(.select()) in _CollectionTrackTile
to avoid full list rebuilds on download history changes
- Fix DownloadSuccessOverlay re-flashing on widget recreation by
initialising _wasSuccess from initial widget state
- Remove orphan Hero tag in search_screen that had no matching pair
- Chip borderRadius updated from 8 to 20 for consistency