Move Mornye Settings to the Home avatar and keep search above the profile card. Center the metadata artwork and actions, and make artist names clickable in the Material player.
Add default-off local beat analysis, pitch-preserving tempo matching, crossfade fallback, and cancellation-safe two-deck playback. Expose AutoMix in settings and the Mornye queue.
Separate Search from Home, remove the Mornye greeting, and keep Home/Search icons moving continuously between full and compact navigation. Restore expansion on upward Library scrolling.
Validate beat analysis, transport handoff and cancellation, settings persistence, and navigation behavior. Flutter analysis and the signed iOS simulator build pass.
Clear cached buckets, sorted results and history lookups when the source track list changes. This releases the previous search even when empty, loading or error branches skip building results.
Add a selectable Mornye theme with shared glass controls, dialogs, sheets, notifications, and collection headers while retaining Material components.
Update player artwork, lyrics transitions and highlighting, synchronized rolling time labels, volume controls, and compact navigation. Unify metadata, Library, search, and extension surfaces; preserve restored artwork and report extension load failures.
Validation: full Flutter analyzer clean; 713 tests passed and 4 skipped; pinned Dart formatting and diff checks passed; signed ARM64 and ARM32 release APKs built and verified.
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.
Fixes#368.
Reopening a Spotify playlist from "recent access" showed no tracks,
while the first view (right after pasting the URL) worked fine.
Root cause traced across two layers:
1. Go: ExtURLHandleResult (the parsed shape of an extension's handleUrl()
return value) never captured a top-level `id` for the handled
resource. Track/album/artist results carry their own id inside their
nested metadata, but a plain playlist result has no such object, so
its id was silently dropped everywhere from the goja parser through
to the JSON the Dart side receives.
2. Dart: TrackState had nowhere to put that id even if it existed
(only playlistName), so recording a "recent" playlist access stored
the playlist's *name* as if it were its id. Reopening it later fed
that name into PlaylistScreen's provider-guessing logic
(legacyProviderIdFromResourceId, which only recognizes legacy
"provider:id" prefixes), which naturally failed and fell through to
a hardcoded Deezer metadata fetch using a Spotify playlist's name as
the resource id - guaranteed to return nothing.
Fix, matching the "generic API, not per-provider checks" architecture
in CONTRIBUTING.md:
- go_backend/extension_provider_wrapper.go: add ExtURLHandleResult.ID.
- go_backend/extension_goja_convert.go: parse it from the handler's
return value.
- go_backend/exports_extensions.go: surface it in the JSON response.
- lib/providers/track_provider.dart: add TrackState.playlistId,
populated from the response's `id` field for playlist results.
- lib/screens/home_tab.dart: record the real playlist id (falling back
to the name only if a provider never supplies one) and pass both the
id and the already-known provider id forward to PlaylistScreen.
- lib/screens/playlist_screen.dart: PlaylistScreen gains a
metadataProviderId param that takes priority over guessing from the
id's shape.
- lib/screens/home_tab_recent.dart: pass the recent-access entry's
stored providerId through when reopening a playlist.
- lib/utils/provider_resource_ids.dart: extract the
known-id-vs-guessed-id preference into a small, directly testable
resolvePreferredMetadataProviderId helper.
Note: this fixes the common case (an extension already reported its
own id as the source provider for the URL it handled). If a provider
never supplies an id for its handleUrl() playlist result, playlistId
stays null and behavior is unchanged from before this fix - no
regression, just not a complete fix for that narrower case, since
that would require changes in extension-side JS code outside this
repo.
Tests:
- go_backend/extension_goja_convert_url_handle_test.go: the new ID
field round-trips from a handler's return value, and stays empty
when the handler doesn't supply one.
- test/provider_resource_ids_test.dart: resolvePreferredMetadataProviderId
prefers a known provider id, falls back to the legacy-prefix guess,
treats a blank known id as unknown, and returns null for the
unprefixed-id-with-no-known-provider case from #368 itself.
Verification: go build/vet/test and flutter analyze/test all green.
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 hero-era loading machinery survived the hero-flight revert: the
loading scaffold rendered the real cover, _RouteSettled deferred the
swap until the push transition ended, and SkeletonCrossfade faded
between the two. The loaded header decodes the cover under a different
mem-cache key (viewport-sized vs full-res), so opening an album from
the artist screen flashed the cover: shown, blanked, shown again.
Restore the pre-hero flow: shimmer skeleton while loading, hard cut to
AlbumScreen so the cover appears exactly once. Removes the now-unused
_AlbumLoadingScaffold and three imports that only existed for it.
TrackState.searchArtists/searchAlbums/searchPlaylists were only ever written
empty (the metadata-search parse loops iterated const [] lists and customSearch
set []), so the home feed's separate artist/album/playlist result sections
never rendered. Drop the fields, the dead parse loops and _parseSearch* helpers,
the SearchArtist/Album/Playlist models, the sort caches/methods, and the
_Search* result widgets. The live tracks->buckets sections (_CollectionItemWidget
via artistItems/albumItems/playlistItems) and the search filter tabs are kept.
While the home feed loads, show section-shaped skeletons (album cards +
artist circles) instead of the generic track-row skeleton, and shimmer
each explore cover while its image loads — matching the artist screen.
- SettingsGroup centers itself at 720dp via its incoming constraint
(not screen width, so groups inside clamped sheets are never
over-inset; unbounded-width contexts guarded); priority scaffold,
bespoke hero cards, and donate page get the same inset - all 18+
settings pages clamp from shared widgets
- queue list-mode, local/downloaded album track lists, favorite
artists, home recent-access, and extension detail sections adopt
wideListInset like the screens fixed earlier
- track metadata content clamped at 720dp, which also tames the
width-derived spectrogram height
- announcement dialog capped at 480dp wide; setup wizard and
tutorial content at 560dp with a 400dp action button
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.
- shell switches to a NavigationRail at >=600dp (same destinations,
badges included); the bottom bar and its blur stay phone-only while
the mini player remains anchored bottom in both modes
- now-playing gains a two-pane landscape layout (artwork left,
metadata/controls right) built from the same widget pieces
- track lists on album/playlist/library-folder screens and search
result sections center at 720dp via wideListInset instead of
stretching across the full tablet width; explore carousels stay
full-bleed intentionally
Opening an extension album/artist showed a bare skeleton, so the cover
had no Hero destination and only flew on the way back. Loading states
now render the actual header (title + Hero cover in its final slot,
skeleton placeholders for the remaining rows) above the shimmering list.
SkeletonCrossfade mutes the outgoing child's heroes to avoid duplicate
tags if the user pops mid-fade.
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.
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.
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/.
_importCsv guards every other post-await BuildContext use with the
State's own `mounted`/`this.context`, but one spot read the l10n
strings off the function's `context` parameter instead, requiring a
`// ignore: use_build_context_synchronously` to silence the analyzer.
Switching to `this.context` matches the surrounding guard and lets the
lint verify the usage on its own, so the suppression is no longer
needed.
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