Commit Graph

1300 Commits

Author SHA1 Message Date
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 9b7f1631f0 fix(native-worker): keep batches and retries intact across seam races
- Verification challenge no longer abandons the batch: batch mates the
  worker cancel would mark skipped are requeued and fenced from later
  snapshot applications, and the adoption loop restarts the queue for
  requeued items when the run ends
- Pause vs cancel race: a cancelled result with no pause flag set waits
  up to 1.5s for the pause intent to land on the main looper before
  being classified as a permanent skip
- Stale Go cancel flag: new ResetDownloadCancel export (wired through
  Android and iOS bridges) drops a pre-registered cancel with no active
  download; retryItem/retryAllFailed call it so the first retry of a
  cancelled-before-start item no longer aborts instantly
- Reconcile-loop errors now cancel the native worker and clear the run
  id before marking items failed, instead of leaving the service
  downloading a batch the UI already wrote off
2026-07-10 10:18:03 +07:00
zarzet 22151b0988 fix(native-worker): recover from dead runs and make queue replacement safe
Stale snapshot (queue wedged forever): the service is START_NOT_STICKY,
so a process kill or reboot mid-batch froze the state file at
is_running:true with nothing ever rewriting it. Adoption then set
isProcessing and polled that flag indefinitely; restored items sat at
downloading/queued until the user intervened. Adoption and the
reconcile poll now check DownloadService.isServiceRunning() (three
consecutive dead polls in the loop), reconcile whatever the worker
finished, requeue in-flight items, clear the run id, and hand the batch
back to the Dart queue.

Adoption gate race (duplicate executors): adoption was gated on
_canUseAndroidNativeWorker, which reads extension state that loads
asynchronously after startup and settings toggles the user may have
flipped mid-run. A background batch surviving task removal was then
orphaned while the Dart queue re-downloaded the same items. Adoption
now only requires Android; request contexts that cannot be built yet
(extensions still loading) are retried on each reconcile poll.

Queue replacement race (superseded worker kept running): startNativeWorker
only cancelled the coroutine, which cannot interrupt the blocking
gomobile call, and it reset the shared cancel flag the old loop checks -
so a replaced worker kept downloading its old batch, and its finally
block wrote is_running:false over the new run's snapshot and stopSelf'd
the service underneath it. Replacement now cancels the old run's Go
downloads and FFmpeg work first, and workers carry a generation token:
a superseded generation exits at the next checkpoint and skips the
final snapshot write and service teardown. An invalid replacement
payload no longer stops the service while a run is active.
2026-07-10 10:18:01 +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 28b877f0eb refactor(providers): split download history out of download_queue_provider
download_queue_provider.dart (9,525 lines) hosted the entire download
history feature alongside the queue. Move DownloadHistoryItem/State/
Notifier and the history providers to download_history_provider.dart,
and promote the audio format/quality helpers both need into
lib/utils/audio_format_utils.dart. The queue provider re-exports the
history library, so existing imports keep working unchanged.
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 60bf871945 fix(downloads): flush queue persistence when app is backgrounded
Queue writes are debounced by 350 ms and the only guaranteed flush ran
in the provider's onDispose, which never fires when the OS kills the
backgrounded process - the most recent queue mutations were silently
lost, so items could vanish after the app sat minimised for a while.

Expose flushQueuePersistence() on the queue notifier and call it from
the app lifecycle observer on AppLifecycleState.paused, the last
reliable callback before a potential process kill.
2026-07-10 10:12:18 +07:00
zarzet d988b2c9d3 fix(downloads): defer verification challenge until app is foregrounded
When a download hit a verification challenge while the app was in the
background, the browser launch was attempted immediately (blocked or
silently dropped by the OS) and the 5-minute grant timeout kept running,
so the item failed without the user ever seeing the CAPTCHA.

Wait for AppLifecycleState.resumed before opening the challenge and
before starting the grant timeout, and post a local notification asking
the user to return to the app. If the process is killed while waiting,
the persisted queue already restores the item as queued on next launch.
2026-07-10 10:12:18 +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
zarzet 8abb9d119b fix(store): harden extension repo registry handling
- Persist registry URL inside store_cache.json and restore it on init so
  the disk cache survives app restarts instead of being wiped every launch
- Fall back to the cached registry when the fetched body fails to parse,
  matching the existing network/HTTP error fallbacks
- Detect HTML responses and surface a clear message instead of the raw
  Go JSON error (invalid character '<') seen in issue #474
- Validate the registry loads before persisting a new URL, and roll the
  backend back to the previous URL on failure, so a broken link never
  survives a restart or Android auto-backup restore
2026-07-10 10:12:13 +07:00
zarzet 6b30eba947 feat(extensions): complete signed-session grants from help dialog
Add a platform bridge to finish session grants on Android and iOS with
JSON success validation, let users paste callback URLs from the
clipboard, and auto-dismiss the verification help dialog after grant.
2026-07-10 10:12:13 +07:00
zarzet edb241faf7 fix(download): suffix batch filenames with track number when needed
Mark multi-track queue additions as batch downloads and append
{track:02} - {title} when the configured format has no uniqueness
token, instead of relying on playlist position alone.
2026-07-10 10:12:12 +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 5f11f5b114 chore(release): bump version to 4.7.1+137 2026-07-02 01:24:22 +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 0be6455d46 fix(download): hand off native worker verification to interactive queue
Detect verification-required failures from the Android native worker,
cancel the worker, and route the item back through the interactive
verification flow with the correct service identifier.
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 dcfd95f276 feat(extensions): manual verification help when browser launch fails
Expose a root navigator for global dialogs, show a fallback help sheet
with copy and reopen actions when verification URLs cannot launch, and
schedule the same prompt after a timeout during pending grants.
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
zarzet 08c738dc69 feat(settings): add extension verification browser mode preference
Let users choose whether signed-session verification opens in the
external browser or in-app browser first, with automatic fallback to
the other mode when launch fails.
2026-07-02 01:24:19 +07:00
zarzet 408895b607 fix(download): track verification retries per service
Key verification retry state by item and service so a completed challenge
for one provider does not block retries when another service also requires
verification for the same download.
2026-07-01 02:03:19 +07:00
zarzet eb40a88437 fix(lyrics): sync provider priority to backend on save
Await backend lyrics provider sync when saving the priority page so
fetch order changes take effect immediately instead of only updating
local settings.
2026-06-30 11:13:23 +07:00
zarzet c0c1d745f3 refactor: hoist lossless labels before async convert paths
Capture lossless conversion labels before await boundaries in queue,
downloaded album, and track metadata flows to avoid BuildContext usage
across async gaps.
2026-06-30 06:20:57 +07:00
zarzet c2b38a7c5a fix(banner): allow motion artwork video to mix with other audio
Enable mixWithOthers on HLS motion header playback so preview audio and
other apps are less likely to be ducked or interrupted.
2026-06-30 06:20:55 +07:00
zarzet ae8638a4b2 chore(player): log internal pause and audio interruption events
Add debug/info logging around audio focus interruptions, becoming-noisy
events, and user pause requests to make preview playback issues easier
to diagnose.
2026-06-30 06:20:51 +07:00
zarzet b864fafa82 feat(ui): stabilize album and playlist header layouts
Keep header metadata and action buttons visible while track lists are
still loading, disable download-all when empty, and show consistent
placeholder artwork when cover art is missing.
2026-06-30 06:20:47 +07:00
zarzet ee5ab1a751 fix(queue): resolve local album tracks by album_key
Load queue local album tracks from album_key when available and fall back
to album/artist matching with empty album_artist handling. Thread album_key
through grouped local album rows for more reliable navigation.
2026-06-30 06:20:37 +07:00
zarzet 64b884e27a fix(lyrics): sync provider settings to backend before fetch
Expose an awaitable lyrics settings sync on SettingsNotifier and call it
before metadata re-enrich, lyrics autofill, and queue/local embed flows so
the backend uses the latest provider order and fetch options.
2026-06-30 06:20:25 +07:00
zarzet dc8bb2cbc2 feat(extension-health): lengthen cache TTL and honor per-check minimum
Raise default extension health cache to 10 minutes with a 1-minute floor
and a shorter TTL for unknown status. Mirror the TTL rules in the Go
backend and stop force-refreshing health checks from the download
service picker on every open.
2026-06-30 06:20:10 +07:00
zarzet d882fc292c l10n: localize settings, store, and metadata UI strings
Wire remaining hardcoded strings in about, download region picker, file
organization settings, extension repo snackbars, release type badges,
metadata queue actions, online cover labels, and update changelog fallback.
2026-06-30 05:10:37 +07:00
zarzet 5dc0980ced l10n: localize audio conversion labels and confirmations
Pass localized lossless conversion labels through shared helpers and
replace hardcoded capped-lossless confirmation text in single-track and
batch convert flows across metadata, queue, and album screens.
2026-06-30 05:10:36 +07:00
zarzet 1cd668c869 l10n: localize library settings and announcements
Move hardcoded strings in library settings, announcement link errors,
and unknown track title/artist fallbacks to AppLocalizations. Sync
locale-dependent fallback strings from MainShell.
2026-06-30 05:10:34 +07:00
zarzet a827ebf6f4 l10n: add localization keys for hardcoded UI strings
Add English template strings and Indonesian translations for playback,
audio conversion, library settings, metadata actions, store snackbars,
SongLink region names, and other previously hardcoded user-facing text.
Regenerate AppLocalizations from the updated ARB files.
2026-06-30 03:40:13 +07:00
zarzet 3917ae02e2 feat: apply lossless conversion quality cap to album screen batch convert
Wire bit depth/sample rate caps and real converted-quality persistence
into the downloaded/local album batch conversion paths, matching the
queue tab behavior.
2026-06-29 06:47:36 +07:00
zarzet e0e28aee38 fix: preview player lifecycle and minor safety cleanups
- 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
2026-06-29 06:46:47 +07:00
zarzet 1550eedc12 feat: reorderable up-next queue + playback stability
- Up Next sheet supports drag-to-reorder via ReorderableListView with
  drag handles; MusicPlayerHandler.moveQueueItem keeps current track active
- Active lyric line centers higher in viewport (0.35) so it sits visually
  centered instead of leaning low
- Audio focus rework: explicit music AudioContext, ignore duck
  interruptions, drop the fragile ignore-complete timer, recover playback
  on transient stop/complete during track switches
2026-06-29 06:46:31 +07:00
zarzet b2074dfd02 feat: cap bit depth/sample rate on lossless conversion + WAV/AIFF
- LosslessConversionQuality model with bit depth/sample rate caps,
  applied only when they reduce source quality
- FFmpegService probes sample rate and appends codec-specific args
  (-ar, -sample_fmt, -bits_per_raw_sample) for FLAC/ALAC/WAV/AIFF
- Batch + single-track convert sheets expose quality cap options
- Persist real converted bit depth/sample rate to history/library DB
- track_metadata: recognize and convert to WAV/AIFF targets
- convertedAudioQualityLabel reflects actual output quality
2026-06-29 06:46:19 +07:00
zarzet e9171d6f21 feat(ui): hide audio quality badge on downloading queue items 2026-06-29 06:45:54 +07:00
zarzet ef60bba2e1 feat(ui): redesign local/downloaded album and folder screen headers
- Consistent header design across all local screens: blurred cover,
  black overlay, bottom gradient, centered square cover, adaptive title,
  inline meta line, Play + Shuffle buttons
- Height normalized to 0.6x (clamp 400-580) everywhere
2026-06-28 22:23:08 +07:00