Commit Graph
1886 Commits
Author SHA1 Message Date
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
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 4e0cae9c20 refactor: extract shared helpers for repeated low-level patterns
Dart:
- notification_service: single _details() builder replaces 13 copies
  of the NotificationDetails block
- platform_bridge: _invokeMap() for 34 invoke+decode call sites,
  _cachedInvoke() unifies the three TTL/in-flight cache scaffolds
- ffmpeg_service: _promoteTempOutput(), _appendCoverInputArgs(),
  single _writeReplayGainTags() and _convertToLossless() for the
  ALAC/FLAC twins
- sqlite_helpers.dart: shared openAppDatabase/path-key/migration
  helpers for the three database classes
- library_collections: parametrized wishlist/loved/favorite CRUD
- extension_provider: one predicate-based replacedBuiltIn* lookup

Go:
- extension runtime: parseGojaHeaders/coerceGojaBody/doExtensionHTTP
  shared by httpGet/httpPost/httpRequest/shortcuts/fetch
- exports_metadata: applyAudioMetadataToResult + successMethodJSON,
  APE edit path reuses audioMetadataFromEditFields
- lyrics: lrclibGet() for both LRCLib fetchers
- extension_store: drop hand-rolled strings helpers
2026-07-12 14:10:23 +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 fca3039208 refactor(models): add Track.fromBackendMap and copyWith, dedupe track parsing
The provider and the redownload service each hand-rolled the same
backend-map parsing and duration extraction. The factory ports the richer
provider variant (cover normalization, native-vs-spotify preferredId), so
the redownload service gains those for free. checkAvailability's 30-line
manual reconstruction becomes copyWith, which also stops silently
dropping previewUrl, totalDiscs, and deezerId.
2026-07-11 16:36:09 +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 764f978897 refactor(android): consolidate SAF and filename helpers in SafDownloadHandler
MainActivity duplicated the whole SAF helper cluster (sanitizeFilename,
truncation, ensureDocumentDir, createOrReuseDocumentFile, ...) and both
classes synchronized on separate locks, so a foreground SAF write racing
a service-worker write into the same tree was never serialized and could
produce duplicate documents. One shared home, one lock. normalizeExt
adopts the trim-first variant; mimeTypeForExt is the union of all three
copies (adds wav/aiff/ogg coverage everywhere).
2026-07-11 16:34:37 +07:00
zarzet 8c2d2d5289 refactor(backend): dedupe response builders and MusicBrainz fetch
marshalJSONString collapses 62 identical marshal-and-return blocks in the
gomobile exports; jsError/jsSuccess collapse 135 goja extension-response
maps; the two MusicBrainz fetchers share one request+retry helper; the
extension-HTTP default User-Agent block and fileExists get single homes.
No exported signature changes.
2026-07-11 16:34:36 +07:00
zarzet 9580fafe4f feat(ui): add shared widgets, utils, and models for deduplicated screens
Single homes for logic that was copy-pasted across screens: the settings
collapsing header, album detail header (online screen's design as the
reference), selection pill button + bottom-bar chrome, disc separator
chip, error card, cover URL upgrade, byte/clock formatting, duration
extraction, UnifiedLibraryItem (moved out of the queue_tab library so
other screens can import it), and the batch convert/ReplayGain engine
keyed on it.
2026-07-11 16:34:16 +07:00
zarzet 49ecfe261a feat(metadata): route MP3, Ogg, and M4A edits to the native writers
EditFileMetadata now answers native_mp3/native_ogg/native_m4a and only
falls back to method=ffmpeg when a native editor rejects the file, so
every editor UI switches off the remux path without Dart changes. The
download embed path does the same for flac/mp3/opus/m4a. Side effect:
ReplayGain for MP3/Opus was silently never written (the old dispatch
answered ffmpeg and the caller ignored it) - it is now written natively.
Covers all three editors with synthetic-file tests, including foreign-tag
preservation and stco offset tracking.
2026-07-11 12:52:01 +07:00
zarzet 89e5f4bde9 feat(metadata): add native Ogg/Opus vorbis comment editor
EditOggFields rebuilds only the OpusTags/vorbis comment packet and
repaginates the header pages; audio pages are copied verbatim and only
renumbered (with fresh CRCs) when the header page count changes. Cover art
is written as METADATA_BLOCK_PICTURE from the shared FLAC picture builder.
The field semantics are extracted from EditFlacFields into a shared
applyVorbisFieldEdits so FLAC and Ogg interpret edits identically.
Unsupported layouts (multiplexed streams, unknown codecs) return an error
so callers can fall back.
2026-07-11 12:46:51 +07:00
zarzet 2eab955f5f feat(metadata): add native M4A ilst tag editor
EditM4AFields edits standard iTunes atoms, freeform ISRC/LABEL, and
ReplayGain in a single atomic rewrite, preserving atoms it does not
control. Creates the moov>udta>meta>ilst chain (with an mdir hdlr) when a
raw stream lacks one, and shifts stco/co64 offsets for faststart files.
2026-07-11 12:45:38 +07:00
zarzet 2bd671d305 feat(metadata): add native MP3 ID3v2 tag editor
EditMP3Fields parses the existing ID3v2.2/2.3/2.4 tag into raw frames,
replaces only the frames the edit controls, and re-serializes as ID3v2.4
with the audio bytes streamed through untouched (temp+fsync+rename).
Unlike the ffmpeg remux this preserves foreign frames - POPM ratings,
SYLT synced lyrics, chapters, and other taggers' TXXX entries.
2026-07-11 12:45:21 +07:00
zarzet e5a1ee2ac3 fix(metadata): shift stco/co64 chunk offsets when the ilst resizes
writeM4AFreeformTags changed the ilst size without updating chunk offsets,
which broke playback for faststart files (moov before mdat) after any
ISRC/label/ReplayGain write. Reuses the AC-4 path's shiftChunkOffsets.
2026-07-11 12:45:20 +07:00
zarzet 8f997314b0 fix(finalizer): stage conversion outputs and sync SAF streams
Decrypt, HIGH conversion, and container conversion wrote ffmpeg output
directly under a real audio name in the final directory, so a process kill
mid-conversion left a partial file that library scans listed as a corrupt
track. Outputs now stream into a '.partial<ext>' sibling (invisible to
scans, real trailing extension so ffmpeg still infers the muxer) and are
fsynced and renamed into place only on success.

Also fixes the delete-then-rename in the ffmpeg tag embed (a kill between
the two lost the file entirely - now renames over the original) and
flushes+fsyncs SAF output streams before staged documents are published.
2026-07-11 12:44:53 +07:00
zarzet 2192e68684 fix(download): fsync staged downloads before the promote rename
The staged-write-then-rename protocol was atomic against process kills but
not against power loss: f2fs/ext4 can persist the rename before the data,
leaving a zero-length or truncated file under the final name. Sync the
staged file before promoting and fsync the directory after.
2026-07-11 12:44:19 +07:00
zarzet 21aeaafced fix(metadata): write FLAC, M4A, and AC-4 tags atomically with fsync
go-flac's Save(samePath) rewrites the file in place by shifting the audio
body within the same inode, so a process kill or power loss mid-save
destroyed the file with no recovery copy. All tag writers now stream to a
sibling temp, fsync, and rename over the target, so an interruption leaves
either the old intact file or the new complete one. The M4A freeform and
AC-4 whole-file rewrites get the same treatment, and the WAV/AIFF writer
gains the missing fsync before its rename.
2026-07-11 12:44:01 +07:00
zarzet 270496b3d5 refactor(lyrics): classify provider errors by type, not message text
Replace the substring signal lists in isLyricsProviderUnavailableError
with typed errors (errLyricsNotFound / errLyricsServiceUnavailable in
the new lyrics_errors.go). Providers now classify failures at the point
of origin: HTTP statuses via lyricsHTTPStatusError (429/5xx cooldown),
netease API codes and paxsenix proxy failures as explicit unavailable,
and every "no lyrics/songs found" path as typed not-found.

Substring matching survives only for error payloads from third-party
proxies (rate limit, missing parameters), whose messages genuinely
arrive as free text.

Two deliberate behavior changes: LyricsPlus 429/5xx responses now cool
the provider down (previously unclassified), and proxy payloads saying
"not found" are treated as not-found instead of disabling the provider.
2026-07-10 15:11:34 +07:00
zarzet db477f970c chore: bump version to 4.8.0-beta.1 2026-07-10 14:59:32 +07:00
zarzet 0d39002af0 perf(native-worker): stop shipping the full item payload on every poll
The 1s snapshot poll re-read and re-parsed the whole worker state file
— which retains every completed item's result (history row, possibly
full synced lyrics; 1.5-5 KB each) for the entire run — then
re-serialized it across the channel and re-iterated it on the Dart UI
isolate. Late in a 1000-track batch that is megabytes parsed several
times per second, on the Android main thread.

Pollers now echo back the state_serial of the last snapshot they fully
processed; when the state has not advanced, the native side serves a
cached compact header (no items, no results, no settings) merged with
the small progress delta — steady-state polls are O(1) regardless of
batch size, and the full payload is delivered exactly once per state
transition. The channel handler also reads and parses off the main
thread now, matching its neighbours.
2026-07-10 13:08:57 +07:00
zarzet 87b46a9694 perf(dedupe): make the ISRC index incremental, cover-free, and batch-stable
The index was rebuilt from scratch every 5 minutes even while
AddToISRCIndex kept it write-consistent after every download, and each
rebuild parsed the FULL metadata of every FLAC in the library —
including multi-megabyte embedded cover art — sequentially, with the
triggering download blocked behind the build lock. For a few thousand
files that is gigabytes of flash I/O per rebuild, repeated throughout
a long batch.

- Indexing now reads only the Vorbis comment block (block headers are
  walked and picture/padding payloads seeked past, never loaded)
- A per-file (size, mtime) -> ISRC cache carries across rebuilds, so a
  rebuild is normally a stat walk that re-reads only changed files
- Add() refreshes the index timestamp, so the TTL no longer forces a
  rebuild in the middle of the workload the index exists to serve
- Cold builds parse with 4 workers, matching the library scanner
2026-07-10 13:08:55 +07:00
zarzet 364b2aa138 fix(metadata): stop the endless FLAC-write failures on misnamed files
When a provider delivers a lossy/unknown stream, the native finalizer
preserved the container but kept the requested .flac name, so every
metadata/ReplayGain write treated the file as FLAC and failed with
"failed to write FLAC metadata: failed to parse FLAC file: fLaC head
incorrect" — on the first download, on every retry (the file is never
deleted from public storage), on every manual edit attempt, and the
history backfill re-probed the same file on every app launch forever.

- Finalizer now renames a preserved lossy/unknown container away from
  its .flac name to match the real content (aac/MP4 -> .m4a, mp3, opus),
  mirroring the Dart pipeline, so the correct tag writer is picked
- EditFileMetadata sniffs MP4 content before taking the .flac branch
  and reports what is actually wrong (rename to .m4a) instead of the
  cryptic parse error
- The history audio-metadata backfill remembers paths whose probe
  failed permanently (unparseable content) and stops reselecting them
  on every launch; transient failures (missing file, unmounted volume)
  still retry
2026-07-10 12:07:11 +07:00
zarzet df1ac30e07 Merge Crowdin translation updates (l10n_main, #428)
Three-way per-key merge: Crowdin wins for keys it updated, locally
added keys that Crowdin has not exported yet are kept. Also normalizes
@@locale to underscore form (es_ES, pt_PT, zh_CN, zh_TW) and repairs
two translations whose placeholders diverged from the template
(ar extensionsInstallPartialSuccess, es_ES cueSplitSplitting).
2026-07-10 11:37:53 +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 b4f66ff4eb chore(backend): align merged test file and fd stubs with lint conventions 2026-07-10 10:25:12 +07:00
zarzet b1f8224310 test(extensions): skip POSIX permission assertion on Windows
The signed-session test from #462 asserts the session file is 0600,
but Windows does not preserve Unix permission bits, so the suite failed
on Windows dev machines while passing in CI.
2026-07-10 10:24:25 +07:00
zarzet f808a6a369 fix(android): stop double-destroying the shared audio_service engine
MainActivity only overrode provideFlutterEngine, so the activity's
fragment delegate treated the plugin-cached engine as its own
(createFlutterFragment forwards shouldDestroyEngineWithHost, default
true) and destroyed it when the activity finished, while the engine
stayed registered in AudioServicePlugin's FlutterEngineCache. When
AudioService stopped afterwards, disposeFlutterEngine() called
destroy() on the already destroyed engine and crashed the app with
"Cannot execute operation because FlutterJNI is not attached to
native".

Mirror audio_service's own AudioServiceFragmentActivity: expose the
plugin engine as a cached engine (getCachedEngineId), never destroy it
with the host (shouldDestroyEngineWithHost = false), and pre-create it
in onCreate. The plugin remains the engine's sole owner and disposes
it only once, when the service stops with no activity attached.
2026-07-10 10:22:39 +07:00