Commit Graph
1918 Commits
Author SHA1 Message Date
zarzet 547e6493dd perf(net): fetch embed covers once per release instead of per track 2026-07-14 09:10:01 +07:00
zarzet 3cdeeb2f5f fix(extensions): remove extension data dir on uninstall 2026-07-14 09:10:00 +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 a287d6b815 fix(playlists): keep imported playlist track order (#482)
Playlist tracks were read newest-first (added_at DESC, rowid DESC),
so a batch import - inserted in playlist order with one timestamp -
came back reversed, and the in-memory prepend matched that. Playlists
now use insertion order: snapshot reads ASC, single adds append, and
batch adds append in order. Existing rows were already stored in
playlist order, so old libraries display correctly without migration.
The preview-cover query follows the first visible track.
2026-07-14 09:09:53 +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 3cf9418f47 feat(ui): fluid bouncing scroll physics on capable devices 2026-07-14 09:09:48 +07:00
zarzet 8d914d967c feat(android): request highest display refresh rate at current resolution 2026-07-14 09:09:47 +07:00
zarzet e8c9df20b3 perf(analysis): stream PCM from disk instead of buffering the whole track 2026-07-14 09:09:46 +07:00
zarzet f69f7e7bf6 perf(images): decode completion-bridge cover precache at cell size 2026-07-14 09:09:45 +07:00
zarzet eeb0298d13 perf(scan): stream SAF library scan results to a spill file 2026-07-14 09:09:43 +07:00
zarzet 96eb412fe5 perf(memory): release Go heap and decoded image caches on OS memory pressure 2026-07-14 09:09:42 +07:00
zarzet f983a303fd perf(lyrics): bound the in-memory lyrics cache 2026-07-14 09:09:41 +07:00
zarzet 08007f493c perf(extensions): reuse isolated goja download runtimes via per-extension pool 2026-07-14 09:09:39 +07:00
zarzet e302fac390 perf(ogg): stream tag rewrites instead of buffering every page 2026-07-14 09:09:35 +07:00
zarzet 4d0f1120d2 perf(m4a): stream tag rewrites instead of buffering the whole file 2026-07-14 09:09:32 +07:00
zarzet 9b53d662f7 perf: hoist per-call regex compiles to package level 2026-07-14 09:09:30 +07:00
zarzet c4e266a5b9 refactor!: rename extension store bridge ABI to repo across all layers
Coordinated rename of the nine store-named bridge surfaces: Go
exports (InitExtensionStoreJSON -> InitExtensionRepoJSON etc.),
method-channel strings, the Kotlin and Swift handlers, and the Dart
PlatformBridge methods. Channel strings verified 1:1 across
Dart/Kotlin/Swift; gomobile bindings match the new export names.
Android MediaStore APIs untouched. store_registry_url pref key kept
for existing users. Local Android/iOS builds need the gobackend
AAR/xcframework rebuilt; CI does this in the release workflow.
2026-07-14 09:09:29 +07:00
zarzet 2273d9ba48 feat(download): resume interrupted downloads via Range/If-Range 2026-07-14 09:09:26 +07:00
zarzet 034b505d15 perf(network): enable gzip on the metadata transport 2026-07-14 09:09:25 +07:00
zarzet 122b16133a perf(tls): enable session resumption on the stdlib transports 2026-07-14 09:09:24 +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 fd4348b803 refactor(go): rename extension store files and internals to repo
extension_store.go -> extension_repo.go, exports_store.go ->
exports_repo.go; unexported types/functions/log tag renamed to match
the user-facing repo terminology. The gomobile-exported function
names (InitExtensionStoreJSON etc.) are the bridge ABI called from
Kotlin/Swift and stay unchanged, as does the store_registry_url pref
key that guards existing users' settings.
2026-07-14 09:09:20 +07:00
zarzet 34c7b33368 chore(l10n): finish store -> repo wording sweep
The es/pt/zh base locales still shipped the old Store labels, the
backup-restore hint said 'from the store' in every locale (boutique
in fr, store loanword in ko), and the Go store lookup error said
'not found in store'. All user-visible surfaces now say repo;
translator descriptions in the template updated to match.
2026-07-14 09:09:19 +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 aae48cc47c ci: track latest stable Flutter in all workflows 2026-07-13 09:08:23 +07:00
zarzet 4b7834e090 ci: cache pub deps, filter jobs by path, and speed up release builds
CI (every push/PR):
- pub-cache cached keyed on pubspec.lock
- dorny/paths-filter skips the Flutter job for Go-only changes and
  vice versa (skipped required checks count as passing)

Release:
- gomobile installed from the go.mod-pinned x/mobile version instead
  of @latest (reproducible, covered by the setup-go cache; verified
  locally that go.sum already satisfies cmd/gomobile)
- Android bind targets arm/arm64/amd64 only - 386 dropped, amd64 kept
  so the universal APK still runs on x86_64 emulators
- NDK r29 cached; docker prune dropped from the free-space step
- Flutter pinned to 3.41.5 (matches .fvmrc/ci.yml) in both build jobs
  plus pub-cache caching; xcodeproj gem installed only when missing
2026-07-13 09:07:23 +07:00
zarzet 9b3b586006 fix(network): harden audit findings in transport, uTLS pool, and cover cache
- sharedTransport ResponseHeaderTimeout 45s -> 120s: downloads ride
  this transport and some providers prepare files server-side before
  the first byte; the stall watchdog still bounds dead transfers
- uTLS pool: a request that fails on a pooled (possibly silently
  dead) connection now re-dials once when the body is repeatable,
  matching the old dial-per-request reliability
- cover singleflight completes via defer with a default error so a
  panicking fetch can never strand waiters in (nil, nil) or leave a
  permanent in-flight entry
2026-07-13 08:45:30 +07:00
zarzet 0f0ebe5565 chore: bump version to 4.8.0-beta.1; fix ICU plural syntax in ar/fr 2026-07-13 08:39:06 +07:00
zarzet 2fa4aa5b70 perf(network): mobile stability and data-usage fixes from network audit
Stability:
- transient timeouts no longer classified as ISP blocking; they fall
  through to retry backoff (hard blocks - DNS/RST/cert - still abort)
- connectivity change now closes idle Go connections in every network
  mode (debounced), so pooled sockets from the old interface are not
  reused after a wifi/cellular handoff
- download body reads get a 60s stall watchdog that cancels and
  surfaces a retryable network error instead of hanging; distinct
  from user cancellation
- ResponseHeaderTimeout 45s on all transports; IdleConnTimeout 90->60s;
  dial timeout 30->10s; retry backoff gains full jitter; Retry-After
  honored on 5xx and Deezer 429

Data usage / speed:
- cover downloads deduplicated with singleflight plus a 24MB/15min
  in-memory cache keyed by final URL (album batches fetched the same
  1800px cover once per track before)
- song.link availability cached (30min positive / 5min negative)
  in front of the 9-req/min rate limiter
- uTLS Cloudflare path now pools one HTTP/2 connection per host with
  a shared TLS session cache instead of a full handshake per request

Deezer artist track-count N+1 kept: counts feed the discography
download UI and are already amortized by the artist TTL cache.
2026-07-13 08:39:05 +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 fb7d101019 fix(queue): restore pre-refactor guarantees found by commit audit
- _replaceSafFileVia: op now registers produced files via addCleanup
  the moment they exist, so converted temp files are deleted even
  when a later step throws (old code's outer-variable finally)
- native worker history: recompute format from the final file path
  at history-write time and gate bitrate on lossy output again -
  the extracted helper was recording pre-conversion format/bitrate
  (FLAC conversions logged as m4a with nulled bit depth on SAF)
- drop dead ignore_for_file header in the finalization part
2026-07-13 06:21:42 +07:00
zarzet 2d52f3251e refactor(queue): split download_queue_provider into concern part files
Pure move: paths, native worker, finalization, ReplayGain, and
metadata embedding move to part files as private extensions on
DownloadQueueNotifier; queue orchestration, state, and public API
stay in the main file (7598 -> 4022 lines).
2026-07-12 21:31:38 +07:00
zarzet de32b8b5f6 refactor(queue): extract shared payload, history, and polling helpers
- _buildDownloadRequestPayload/_historyItemFromResult shared by the
  inline and native-worker paths; _buildOutputDir now derives from
  _buildRelativeOutputDir
- shouldAbortWork extended (deleteFileOnAbort, null-item check) and
  used at all ten guard sites; _purgeAlbumRgEntry replaces six copies
- ProgressStreamPoller shared by download queue and local library;
  openVerificationAndAwaitGrant shared by queue and track provider
2026-07-12 21:09:31 +07:00
zarzet e1af1c8dc9 chore(l10n): remove 70 unused localization keys
1,303 stale entries dropped across 19 locales (largest groups:
Spotify credentials flow, collection playlists, notification
channels, backup strings); generated localizations rebuilt.

Crowdin sources need a resync after this lands.
2026-07-12 20:23:19 +07:00
zarzet 85949eef8c refactor: unify extension call skeleton and download finalization paths
Go:
- callExtensionScript generic replaces the ~40-line perf/lock/run/
  timeout skeleton in ten wrapper methods; PostProcess V1/V2 share
  postProcessCommon (V1 keeps its postProcess-only probe)
- attemptExtensionDownload extracts the duplicated provider download
  attempt from the source-extension and priority-loop paths;
  overlayStr/overlayInt collapse the enrichment overlay chains;
  buildDownloadFilename shared by both output-path builders

Dart (download_queue_provider):
- _replaceSafFileVia wraps the SAF copy/operate/publish/cleanup
  roundtrip used at eight sites
- decrypt and external-LRC finalization unified between the inline
  and native-worker pipelines (divergences parametrized: AC-4 repair,
  per-stage error messages, base-name resolution, extension-state
  snapshot); HIGH/container conversion keep separate lenient/strict
  policies and share only the SAF mechanics
2026-07-12 20:19:54 +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