Commit Graph
162 Commits
Author SHA1 Message Date
zarzet 7d7716cd6f feat(albums): show formatted editorial descriptions 2026-09-26 03:50:37 +07:00
zarzet a9898e8b30 feat(artists): show provider-neutral upcoming concerts 2026-09-25 10:58:19 +07:00
zarzet 39273edd85 fix: retry verification browser launch and failed searches 2026-09-23 17:26:40 +07:00
zarzet 5958fe2719 feat(home): add large featured cards for extension feeds 2026-09-21 18:38:04 +07:00
zarzet d8d46108bc feat(profile): add local profiles with backup and restore
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.
2026-09-21 17:28:18 +07:00
zarzet 5b519070a3 feat(player): add beat-matched AutoMix and refine Mornye navigation
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.
2026-09-20 20:36:26 +07:00
zarzet 043c7bd874 perf(search): release derived caches when track results change
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.
2026-09-20 17:21:01 +07:00
zarzet dc25370350 feat(artists): add branded headers and incremental release loading 2026-09-20 11:46:16 +07:00
zarzet 63e1a9b9c8 feat(ui): add Mornye glass theme and adaptive music player
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.
2026-09-19 12:10:10 +07:00
zarzet ed51b5d6ea revert: remove download size estimates 2026-09-06 22:17:23 +07:00
zarzet 174852b056 fix(download): estimate sizes from track quality metadata 2026-09-06 22:11:37 +07:00
zarzet c6195bcd10 feat(download): estimate file sizes in quality picker 2026-09-06 21:40:25 +07:00
zarzet 67440925eb perf(ui): retain track history lookup snapshots
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.
2026-09-06 14:04:05 +07:00
zarzet 856139f908 fix(search): let latest live queries supersede cancelled requests 2026-09-06 02:34:49 +07:00
zarzet 958b79ebca fix(ui): improve adaptive phone and tablet layouts 2026-09-04 23:44:43 +07:00
zarzet 47deb9f34e fix(extensions): resume metadata after verification 2026-08-27 13:08:44 +07:00
zarzet 1a7c20111a feat(library): add lyrics filtering and metadata actions 2026-08-27 01:44:06 +07:00
Abelardo Ramirez bed95efb29 fix(playlists): thread the source provider id through to recent playlists
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.
2026-08-11 17:29:41 +07:00
zarzet e750c195ca feat(search): save max-quality cover on long press #511 2026-08-11 16:01:39 +07:00
zarzet dbdbf0ca41 refactor(ui): standardize modal sheets 2026-07-29 17:10:42 +07:00
zarzet 9b4a960324 fix(home): keep search visible during provider changes 2026-07-27 14:24:57 +07:00
zarzet 00d03de2b1 refactor(home): split explore, recent, import, and search-results into part files 2026-07-26 22:51:55 +07:00
zarzet cbee637501 perf(history): coalesce index bumps and batch per-row exists lookups 2026-07-26 22:25:04 +07:00
zarzet 74615a60b5 refactor: consolidate lyrics builders, cancel registries, and priority plumbing 2026-07-26 19:08:22 +07:00
zarzet 424ca5d77e feat(ux): unify added-to-queue snackbars and add fetch retry
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.
2026-07-26 19:00:31 +07:00
zarzet 20081b14bd refactor(dart): extract queue search and media policies 2026-07-26 01:13:52 +07:00
zarzet eabafecaeb fix(navigation): open download queue from snackbar actions 2026-07-24 22:19:03 +07:00
zarzet bd6e2b4472 fix(ui): restore plain loading skeleton on extension album screen
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.
2026-07-21 07:35:03 +07:00
zarzet 0961c09a6d feat(download): preserve quality variants 2026-07-16 08:45:01 +07:00
zarzet 87bf33321b refactor(search): remove unreachable artist/album/playlist result path
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.
2026-07-14 18:41:13 +07:00
zarzet 4ffd5cc701 feat(ui): shimmer skeletons for the home feed artist/album sections
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.
2026-07-14 17:01:14 +07:00
zarzet 1893f3faa8 feat(tablet): clamp remaining screens - settings, queue lists, wizard
- 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
2026-07-14 09:10:16 +07:00
zarzet 8966de6d0a revert(ui): drop hero cover flights from home/search to detail screens
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.
2026-07-14 09:10:14 +07:00
zarzet f5b5af6eea feat(tablet): navigation rail, landscape now-playing, clamped track lists
- 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
2026-07-14 09:10:00 +07:00
zarzet 93b173cff8 feat(ui): real headers on extension loading screens so hero covers fly on open
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.
2026-07-14 09:09:54 +07:00
zarzet c580280890 feat(ui): shared-element hero covers from search and recents to detail screens
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.
2026-07-14 09:09:49 +07:00
zarzet 44de61a06a refactor(screens): share album-screen scaffolding and track flows
- SelectionModeMixin + CollapsingHeaderScrollMixin extracted;
  local/downloaded album screens now share AlbumTrackTile,
  AlbumScaffoldBody, DestructiveSelectionButton, HeaderMetaRow,
  and confirmAndDeleteTracks
- track_detail_actions.dart: shared downloadSingleTrack,
  queueTracksSkippingDownloaded, download-all confirm, queued
  snackbar, release-date formatter, list footer, love-all
- TtlCache<T> replaces the copied 10-minute static caches;
  album fetch branches share _applyAlbumMetadata
- playlist error card now uses ErrorCard; formatMegabytes shared
  by queue tab and update dialog
2026-07-12 19:19:22 +07:00
zarzet 05415d96d5 refactor(screens): unify album header, selection UI, and batch engine
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.
2026-07-11 16:36:29 +07:00
zarzet fd7424dd81 refactor(ui): extract shared widgets, drop dead screens
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/.
2026-07-10 10:12:15 +07:00
Amix c281763718 fix(ui): use State's context instead of stale parameter in CSV import (#460)
_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.
2026-07-10 10:11:04 +07:00
Hamidreza Bayat e97ecf38c5 Use more specific MediaQuery methods to reduce unnecessary rebuilds (#467) 2026-07-10 10:11:00 +07:00
zarzet 95f5ae610e feat(banner): HLS motion-artwork header banners and audio-quality badges
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.
2026-06-28 06:06:54 +07:00
zarzet 50509d0a16 chore: clean up redundant comments across Go backend and Flutter sources 2026-06-26 22:57:03 +07:00
zarzet c10c2a290c feat(ui): add bottom inset so scrollable content clears the transparent navbar 2026-06-13 20:31:39 +07:00
zarzet ca413a16fa fix(ui): center modals on large screens, modernize edit-metadata + convert sheets, themed badges, fix artist skeleton and format-editor crash
- 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
2026-06-13 02:08:06 +07:00
zarzet 421d5ffdc8 feat: polish search empty state and share caching 2026-06-02 00:58:42 +07:00
zarzet 904b45e8f6 chore: housekeeping cleanup and code deduplication
- Remove stray tracked files (root AndroidManifest.xml, build.gradle.bak, temp_project template)
- Move README-only images out of app asset bundle to reduce APK/IPA size (~1.68MB)
- Fix logo filename typo (transparant -> transparent)
- Deduplicate _readPositiveInt into shared int_utils.dart
- Deduplicate _themeModeFromString (reuse from theme_settings.dart)
- Remove deprecated LocalLibraryState.items getter
- Remove unused sqflite_common_ffi dependency
- Update apps.json version to 4.5.1
- Fix Flutter version in CONTRIBUTING.md (3.38.1 -> 3.41.5)
- Improve .gitignore patterns (NUL, *.bak, root AndroidManifest.xml)
2026-05-08 21:37:56 +07:00
zarzet ed7171133f fix: show missing extension state for returning users 2026-05-08 00:40:26 +07:00
zarzet 01a5b43613 perf: unify queue tab with DB-backed pagination and cross-database queries
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
2026-05-06 04:38:51 +07:00
zarzet 2143de3aa7 chore: remove redundant comments and boilerplate across codebase
Strip doc comments, section dividers, HTML comments, and Flutter
template boilerplate that add no informational value. No logic or
behavior changes.
2026-05-05 21:35:18 +07:00