Commit Graph
514 Commits
Author SHA1 Message Date
zarzet 556399d354 feat(ui): surface the audio-analysis cache in cache management 2026-07-14 09:10:05 +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 8f7ede4730 feat(tablet): clamp dialogs and sheets, adaptive selection-card columns
- bottom sheets capped at 640dp via bottomSheetTheme (centered on
  wide screens, no effect on phones)
- update dialog and SAF migration dialog text capped at 480/400dp
- selection action cards and service chips grow from 2 to up to 4
  columns based on available width instead of stretching
2026-07-14 09:09:59 +07:00
zarzet a10aeb76ee fix(ui): match loading-header placeholder heights to the loaded rows
The subtitle/meta placeholders were shorter than the real artist and
meta rows, so the cover shifted ~12px during the loading-to-content
crossfade and read as a ghosted double image.
2026-07-14 09:09:58 +07:00
zarzet 2d9df7e1bc fix(ui): hero continuity for motion (Apple Music) headers
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.
2026-07-14 09:09:57 +07:00
zarzet 16e2046d0f fix(ui): defer loaded-screen swap until the push transition settles
With cached albums the fetch finishes mid-Hero-flight; the crossfade
then mounted a second hero with the same tag, which the flight doesn't
adopt — a static cover appeared next to the flying one. The extension
screens now keep the loading header until the route animation completes.
2026-07-14 09:09:56 +07:00
zarzet 306c34a32a style(ui): unify header meta rows on the dot-separated album style
Track metadata, playlist, and library folder headers dropped their pill
badges for the same HeaderMetaRow/HeaderMetaItem used by the album
screen.
2026-07-14 09:09:56 +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 9a205987ee feat(metadata): album-style header with centered square cover
The full-bleed cover made the Hero flight from square list thumbnails
morph into a fullscreen rect and back. The header now blurs the cover
as a backdrop and shows a centered rounded square (album header layout),
so the flight stays square end to end.
2026-07-14 09:09:53 +07:00
zarzet a11cff8b28 fix(player): pause motion header video when its screen is hidden
The banner only reacted to app lifecycle, so the HEVC decoder and HLS
stream kept running behind covered routes and inactive tabs. It now
follows TickerMode: the Navigator already disables it for covered
routes, and the shell disables it for non-current tabs (which also
mutes stray animations on kept-alive pages).
2026-07-14 09:09:52 +07:00
zarzet 23488a9c05 feat(player): fade the sheet in place on close, letting only the cover travel
Button/back pops no longer slide the sheet down: it dissolves where it
stands while the Hero artwork flies back to the mini player. Drag
dismissals keep sliding from the finger's position since freezing a
half-dragged sheet mid-screen would look broken.
2026-07-14 09:09:51 +07:00
zarzet 8610f18ff7 fix(player): dissolve the sheet while it slides down on close
After the content fade, popping left an empty dark scaffold traveling
the full screen. The sheet now fades out over the second half of the
pop; opening and dragging keep it fully opaque.
2026-07-14 09:09:51 +07:00
zarzet 7a0be15359 fix(artist): drop blurBackground stretch mode on the header
Dormant until bouncing physics enabled real overscroll; the stretch zoom
stays, but blurring the artist image while it grows looked broken.
2026-07-14 09:09:50 +07:00
zarzet 8a332c566a fix(player): fade Now Playing content with the expand transition
The app bar and body slid up as a block while the Hero artwork flew its
own path, so the title/controls looked detached from the cover. Content
now fades in over the last 60% of the transition (and fades out first on
pop/drag), leaving only the sheet surface and the flying cover in motion.
2026-07-14 09:09:50 +07:00
zarzet 8c33a86df5 feat(ui): crossfade skeleton into content on extension detail screens
SkeletonCrossfade (AnimatedSwitcher, 250ms) replaces the single-frame
hard swap when the extension album/artist/playlist fetch completes.
2026-07-14 09:09:49 +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 702fc9825e feat(player): expand transition from mini player with drag-to-dismiss
Slide-up route with M3 emphasized easing, live finger-tracking dismissal
on the app bar and artwork, and a shared-element Hero on the artwork so
the mini player cover grows into the full player.
2026-07-14 09:09:48 +07:00
zarzet f69f7e7bf6 perf(images): decode completion-bridge cover precache at cell size 2026-07-14 09:09:45 +07:00
zarzet 86d5ae3a2b refactor(dart): rename store provider and screen folder to repo
store_provider.dart -> repo_provider.dart, screens/store/ ->
screens/repo/; Store* classes and storeProvider renamed to Repo*.
Method-channel names in platform_bridge stay unchanged (bridge ABI
matching the Kotlin handler).
2026-07-14 09:09:22 +07:00
zarzet 4fc6a6b45d feat(update): force update when 3+ stable releases behind
UpdateChecker now derives the latest release and a releasesBehind
count from one releases-list call. When the installed version is
forceUpdateThreshold (3) or more stable releases behind, the update
dialog becomes mandatory: barrier and back-press are blocked, the
later/don't-remind actions are hidden, backing out of the installer
returns to the dialog, and the check bypasses the user's
check-for-updates opt-out. Prereleases never count toward the
threshold.
2026-07-13 09:44:59 +07:00
zarzet 8579f68554 feat(artist): shimmer skeleton for album and popular-track cover placeholders
Covers loaded as flat gray boxes while the image downloaded; the
album cards and popular list now shimmer like the rest of the
loading states.
2026-07-13 07:30:34 +07:00
zarzet 62015a4e2d refactor(screens): migrate remaining screens to shared header and selection components
- 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
2026-07-12 19:38:50 +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 4b9853eef7 refactor(ui): consolidate duplicated sheets, rows, and cover widgets
- single-track convert sheet now reuses BatchConvertSheet
  (confirmLabelBuilder + sourceIsLossless params) instead of a
  full inline copy
- provider priority page migrated to PrioritySettingsScaffold;
  shared showDiscardChangesDialog and ReorderablePriorityItem
  replace per-page copies
- home tab search rows unified into one _SearchResultRowItem;
  _parseTrack copies rebuilt on Track.fromBackendMap with only
  the load-bearing local overrides kept; loading/error scaffold
  extracted
- PlayerArtwork widget shared by now-playing and mini player
- LocalOrNetworkCoverImage dispatches file vs network cover in
  one place (playlist picker, queue nav, library folder)
2026-07-12 18:58:16 +07:00
zarzet aa2cdef1d2 refactor: remove dead code across Dart and Go layers
- drop unused widgets (AnimatedStateSwitcher, GridSkeleton,
  adjacentHorizontalPageRoute, buildRemovalAnimation, SwingIcon,
  BottomSheetOptionTile) and empty _buildInfoCard placeholders
- drop test-only Go wrapper methods (GetDownloadURL, MatchTrack,
  CheckAvailability 7-arg, CustomSearchForItemID) and their parsers
- drop unused httputil getters/validators and hand-rolled base64
  decoder in favor of encoding/base64
- remove unused riverpod_generator dev dependency
2026-07-12 09:35:11 +07:00
zarzet cea6a58c18 refactor(playlist): parse tracks via Track.fromBackendMap
Removes the screen's private copy of the backend-map parser; the factory
is a strict superset (broader duration coercion, source/albumType/
itemType, and empty-spotify_id fallback to the native id).
2026-07-11 17:31:11 +07:00
zarzet 59a5ca622d refactor(screens): migrate playlist and library-folder headers to AlbumDetailHeader
Both carried yet another copy of the collapsing detail header.
AlbumDetailHeader gains appBarTitle (selection-aware toolbar title) and a
leading override for the folder screen's close/back switch. The playlist
header adopts the album layout (bottom-anchored content, 0.85 gradient,
length-adaptive title size, scrim only when blurred) instead of its
drifted centered variant; motion-banner and pill-chip meta are preserved
through the slots.
2026-07-11 17:18:01 +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 b12502c8f6 refactor(settings): replace 18 inline collapsing headers with SettingsSliverAppBar
Every settings-style page carried the same 40-line SliverAppBar block;
titles, custom leadings (discard-confirm back handlers), and action
buttons are preserved through the widget's slots.
2026-07-11 16:36:08 +07:00
zarzet 196fd0f651 feat(ios): run verification and OAuth through ASWebAuthenticationSession
The captcha/OAuth flow relied on the OS routing the spotiflac://
callback back into the app. In sideload containers (LiveContainer)
the guest app's URL scheme is never registered with iOS, so after
solving the challenge the redirect went nowhere and the user could
not return to the app (LiveContainer#242/#162 — unresolved upstream).

ASWebAuthenticationSession intercepts the callback scheme in-process,
so no OS-level scheme registration is involved: the session sheet
closes itself on completion and the callback URL is fed into the same
deep-link handler the OS path uses. Verification challenges, the help
dialog's open-browser action, and extension OAuth logins all prefer
the session on iOS, falling back to url_launcher if it fails to start.
Safari's cookie store is shared so captcha providers see an
established browsing context.
2026-07-10 11:25:55 +07:00
zarzet b28e8a83a8 fix(queue): keep create-playlist label at natural size
Making the button flexible let a tight layout squeeze its label away
entirely. Only the count text needs to give way — it ellipsizes while
the action buttons keep their full labels.
2026-07-10 11:13:58 +07:00
zarzet a8cc42ed30 fix(queue): prevent header row overflow on narrow screens
The track/album count header rows sized their text and action buttons
at natural width; on narrow layouts (or long localized labels) the
row overflowed by a few pixels. The count text and the create-playlist
button label now flex and ellipsize instead.
2026-07-10 10:36:02 +07:00
zarzet 1998a9300e feat(downloads): support up to 3 concurrent downloads
The Dart queue loop was already built around an activeDownloads map and
per-item progress tracking but was gated to one download at a time. Add
a concurrentDownloads setting (1-3, default 1) exposed in Download
Settings > Performance, and let the scheduler keep up to that many
items in flight. The experimental Android native worker is still
strictly sequential, so it is skipped in favor of the Dart queue while
concurrency is enabled.
2026-07-10 10:17:59 +07:00
zarzet 19f69a6090 refactor: dispatch on MusicServices constants instead of raw service ids
Service ids ('deezer', 'tidal', 'qobuz', 'spotify', 'amazon', 'local')
were compared as raw string literals in every dispatch chain. Introduce
MusicServices constants and use them at the comparison/switch sites so
a typo becomes a compile error instead of a silently dead branch.
2026-07-10 10:17:59 +07:00
zarzet c24a72c302 refactor(metadata): split _TrackMetadataScreenState into part files
track_metadata_screen.dart was 5,470 lines, most of it one State class
mixing five surfaces. Move four cohesive method clusters into private
extensions on _TrackMetadataScreenState in new part files: header/info
cards, lyrics + save/re-enrich flows, audio conversion + CUE split, and
file actions. Same library, so private state access is unchanged;
setState goes through a _setState forwarder as in queue_tab. The screen
file is now 1,575 lines.
2026-07-10 10:17:58 +07:00
zarzet 860649e3ac refactor(queue): split _QueueTabState method clusters into part files
queue_tab.dart was 7,567 lines with a single 7,300-line State class.
Continue the existing part-file split by moving six cohesive method
clusters into private extensions on _QueueTabState (same library, so
private state access is unchanged): selection mode, navigation/open
actions, collection item builders, filter UI, batch actions, and
queue/library item builders. setState calls in the parts go through a
_setState forwarder since @protected members cannot be called from
extensions. queue_tab.dart is now 2,462 lines.

# Conflicts:
#	lib/screens/queue_tab.dart
2026-07-10 10:15:56 +07:00
zarzet 2e7e2b1964 refactor(ui): share legacy provider-id resource helpers
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.
2026-07-10 10:12:20 +07:00
zarzet 70cba44164 refactor(ui): merge duplicated album/playlist track rows into TrackListTile
_AlbumTrackItem and _PlaylistTrackItem were line-for-line identical
except for the leading widget and whether the artist name is clickable;
both are now parameters of a shared TrackListTile.
2026-07-10 10:12:20 +07:00
zarzet 32892bd4bd chore(i18n): localize folder-access download errors, drop em dashes
The two folder-access failure messages are stored on items as fixed
English sentinels so code can match on them; translate the known
sentinels at display time (error dialog and failed-row message) via new
downloadErrorSafPermissionLost / downloadErrorFolderAccessLost keys.
Also replace em dashes in recently added comments.
2026-07-10 10:12:19 +07:00
zarzet bb66cc470a fix(queue): keep just-completed tracks pinned while the batch is running
A track that finished downloading dropped out of the active lead zone
and re-appeared in the history zone below every still-queued sibling,
visually jumping several positions during album downloads. The existing
completion bridge masked this for only 6 seconds.

Keep completion-bridge cells pinned in the lead zone for as long as
other downloads are still active, and hide their history rows while
pinned so tracks don't render twice. Once the queue goes idle the
bridge dismisses and items settle into their normal history position.
2026-07-10 10:12:19 +07:00
zarzet 927fc8abac feat(downloads): detect lost SAF grant and offer folder re-selection
A persisted SAF tree grant can die silently (folder deleted or renamed,
SD card removed, app data reset, system revocation). Downloads then fail
with no recovery path beyond digging through Settings.

- Add isSafTreeAccessible bridge method: the grant must still be in the
  system's persisted permission list and the tree writable
- Show a warning banner in Files & Folders when the saved SAF folder is
  no longer accessible, with an inline re-select action
- In the download error dialog, replace Retry with a Re-select folder
  action when the failure is a lost folder grant (Android SAF or iOS
  bookmark); a successful re-pick retries the item automatically
- Share the folder-access error messages as constants so the UI can
  match on them reliably
2026-07-10 10:12:16 +07:00
zarzet 53824b7ac2 fix(ios): create security-scoped bookmark inside native folder picker session
The download/library folder picker on iOS previously took the bare path
string returned by file_picker and rebuilt a URL from it to create a
security-scoped bookmark. By then the picker's access grant is gone, so
bookmark creation either failed (folder selection silently rejected) or
produced a bookmark that could not be re-opened, after which the first
download run overwrote the user's chosen folder back to app Documents.

- Add a native pickIosDirectory method channel that presents
  UIDocumentPickerViewController itself and creates the bookmark inside
  the delegate callback, while the security-scoped grant is active
- Use it for the download directory (settings + first-run setup) and
  the local library folder pickers
- Stop overwriting the saved download directory when the bookmark fails
  at queue start; fail queued items with a clear message instead
- Skip the launch-time iOS path normalizer when a bookmark exists, so
  it no longer rewrites external folder paths and drops their bookmark

Fixes #454
2026-07-10 10:12:16 +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
zarzet 4c83aeafb5 feat(metadata): surface explicit content flag with [E] badge
Port of the explicit-content indicator from SpotiFLAC-Web (issue #456):

- Go: add explicit to ExtTrackMetadata (parsed generically from any
  extension via explicit/is_explicit/isExplicit), TrackMetadata, and
  AlbumTrackMetadata; built-in Deezer maps explicit_lyrics and
  explicit_content_lyrics == 1
- Dart: add explicit to the Track model with a tolerant
  parseExplicitFlag helper wired into all backend track parsers
- UI: new ExplicitBadge rendered through buildQualityBadges on album,
  playlist, search, and home track rows
2026-07-10 10:12:14 +07:00
AmixandGitHub 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 BayatandGitHub e97ecf38c5 Use more specific MediaQuery methods to reduce unnecessary rebuilds (#467) 2026-07-10 10:11:00 +07:00
zarzet 61f62363b3 fix(convert): make convert bottom sheets draggable and scroll-controlled
Use DraggableScrollableSheet for single and batch convert flows and open
convert sheets with isScrollControlled so long option lists remain usable
on smaller screens.
2026-07-02 01:24:21 +07:00
zarzet 3278e32711 fix(extensions): default verification browser to in-app first
Prefer the in-app browser for signed-session verification challenges,
normalize invalid saved modes to the new default, and keep the help
dialog modal until the user explicitly dismisses it.
2026-07-02 01:24:21 +07:00
zarzet 5424648158 feat(audio): add dither and resampler options for lossless conversion
Let users choose FFmpeg dithering when reducing bit depth and SoXr or
SWR resampling when changing sample rate across single-track and batch
lossless conversion flows.
2026-07-02 01:24:20 +07:00
zarzet 4d6f7d8b08 l10n: add extension verification and lossless processing strings
Add localization keys for verification browser settings, manual
verification help dialog actions, and lossless conversion dithering and
resampler option labels.
2026-07-02 01:24:20 +07:00