3244 Commits
Author SHA1 Message Date
zarzet 22f3ec52f9 Merge remote-tracking branch 'origin/main' v5.2.0 2026-10-19 17:00:58 +07:00
zarzet 36694c6e27 fix(crash): match the Home crash card to the error card style
Use the error container surface, leading icon, card radius and spacing
shared by ErrorCard, align the card with the Home search bar on tablets,
and open the full report in the scrollable app sheet instead of an inline
ExpansionTile nested inside the Home scroll view.
2026-10-19 16:34:08 +07:00
zarzet cb51d08939 feat(crash): show persistent crash reports on Home 2026-10-19 16:17:58 +07:00
zarzet 378c8f3749 fix(library): recover interrupted scans and stale notifications
Serialize scan notifications and surface failures even when database error persistence fails. Recover interrupted scans independently of auto-scan, retain their checkpoints, and collect matching Android process-exit details on restart.
2026-10-19 15:43:00 +07:00
zarzet 18fd556869 chore(lyrics): rename first-party provider to Zarz-LRC
Update settings, About, provider descriptions, and lyric source credits. Preserve the stored provider ID and recognize both current and legacy credits as direct sources.
2026-10-19 11:18:45 +07:00
zarzet fbaf7fa4ee feat(about): link the lyrics website and service status
Add lyrics.zarz.moe and status.zarz.moe to Links, and remove the Telegram
beta group link along with its strings.
2026-10-19 11:03:46 +07:00
zarzet 1bc8c05ef9 fix(downloads): offer folder recovery for permission failures
Preserve stored folder access errors when rendering queue failures, and route local permission retries through folder reselection. Cover restored failures and normal network retries in Material and Mornye dialogs.
2026-10-19 10:59:11 +07:00
zarzet d3a51a6a36 docs(contributing): add the codebase map and architecture guide
Owner decision D1 keeps docs/ ignored, so the architecture guide lives in
the tracked CONTRIBUTING.md as an extension of Project Boundaries: a map of
lib/, the Rust crates and the native folders, where each feature lives, the
download and playback data flows, recipes for adding a setting, a screen, a
channel method, a lyrics provider and an extension-facing API, test layers,
and code conventions. Folder READMEs written by unit W5-B are linked, not
duplicated.

Facts were taken from the code, not from audits: the lib/ dependency column
comes from scanning every import line per folder, crate edges from each
Cargo.toml, and each data-flow step from reading the call path function by
function. A script checked that every backticked path added here exists
(at the root or as a tracked-file suffix) and that every identifier is found
with a whole-word grep; the remaining tokens are templates or literals that
were confirmed by hand.
2026-10-19 05:35:08 +07:00
zarzet c6f8ac21b5 docs(test): describe test layout, contracts and host tools
Add test/README.md (TS-07): file naming, test/support/ and test/fixtures/, the notifier-subclass override pattern, what the source-text contract tests pin, FFmpeg, python3 and SMB requirements, running a subset, and the timing of library_browse_sqlite_paging_test.

Derived from test/support/*.dart, test/fixtures/, test/source_contracts_test.dart, test/design_system_test.dart, the FFmpeg and SMB suites, test/library_browse_sqlite_paging_test.dart (timed at 25 s locally) and .github/workflows/ci.yml.
2026-10-19 05:27:37 +07:00
zarzet c4cf56fb9b docs(theme): explain the Material and Mornye theme layer
Add lib/theme/README.md (UI-73): what Mornye is, which files form the theme layer, how DynamicColorWrapper builds each style, how Mornye maps onto Material 3 ColorScheme roles and AppTokens, and how widgets branch with context.isMornye and context.adaptiveIcon.

Derived from every file in lib/theme/, lib/models/theme_settings.dart, lib/providers/theme_provider.dart, lib/providers/runtime_profile_provider.dart, lib/widgets/mornye_chrome.dart and test/design_system_test.dart.
2026-10-19 05:27:36 +07:00
zarzet 44cbd4532b docs(third-party): complete the vendored plugin patch notes
Complete the audio_service and audioplayers_android notes and add one for audioplayers_darwin: how each copy is wired into pubspec.yaml, every patch, the changed files, and an upgrade procedure. Each list was checked with diff -ru against the matching pub.dev package in the local pub cache (audio_service 0.18.19, audioplayers_android 5.3.0, audioplayers_darwin 6.5.0). No plugin code changed.

Derived from those diffs, pubspec.yaml, the plugins' Gradle and podspec files, and the Dart callers lib/services/music_player_service.dart and lib/services/music_playback_deck.dart.
2026-10-19 05:27:36 +07:00
zarzet 00d476c146 docs(rust): describe the backend workspace, builds and bindings
Add rust_backend/README.md: crate map and dependencies, vendored patches, toolchain, host and Android/iOS build commands (all pass --locked; none pass --offline), NDK version and ABIs, UniFFI binding outputs and consumers, and the test layout.

Derived from rust_backend/Cargo.toml, rust-toolchain.toml, crate Cargo.toml files, uniffi.toml, the scripts/ build and check scripts, android/app/build.gradle.kts, ios/Podfile and .github/workflows/ci.yml.
2026-10-19 05:27:36 +07:00
zarzet 7d2c1e6a21 docs(integration-test): index the device suites and their defines
Add integration_test/README.md: how to run one suite, why not to run the folder, performance_driver.dart usage, emulator and fixture requirements, every --dart-define and environment variable, and an index of all 54 files.

Derived from every file in integration_test/, the aggregate entry points, performance_driver.dart, performance_probe.dart and scripts/smb2_fixture_server.py.
2026-10-19 05:27:36 +07:00
zarzet a70abf8f76 docs(android): document the native channel contract
Add android/README.md: backend channel routing, the 70 nativeBackendMethods with handler file and reply kind, host-to-Dart events, the __json_file spill, progress EventChannels (400 ms Android, 800 ms iOS), the two foreground services, the SAF model, the Rust bridge files, and the iOS routing differences.

Derived from MainActivity.kt and the MainActivity*.kt handler files, MainActivityProgressStreams.kt, SelectedCoreBackend.kt, RustCoreBackendApplication.kt, NativeDataJobs.kt, SpillJsonWriter, DownloadService.kt, BackgroundWorkService.kt, MainActivitySafScan.kt, MainActivitySafIo.kt, android/app/build.gradle.kts, proguard-rules.pro, ios/Runner/AppDelegate.swift, CoreBackend.swift and lib/services/platform_bridge*.dart.
2026-10-19 05:27:36 +07:00
zarzet e80ae1b1da refactor(android): share the SAF scan percent and CUE helpers
The two SAF scans repeated the same progress arithmetic and CUE steps.
Only the runs that are identical are shared:

- safScanProgressPct(done, total) is the Int/Int to Double * 100.0
  expression all six percent sites used. The incremental scan keeps its
  zero-total guard (100.0), the full scan's call sites keep relying on a
  non-zero total as before.
- reportScanned() replaces the two identical scanned/error/percent
  updates in scanSafTree. The CUE-referenced audio skip keeps its own
  update, which never set errorCount.
- reportProcessed() moves above the changed-CUE loop, whose inline copy
  of its body it now replaces.
- resolveCueSheetAudio() covers the FILE line extraction plus sibling
  lookup that both changed-CUE loops and the unchanged-CUE loop ran.
- scanSafCueTracks() covers the audio name, timestamp, cover key and
  scanCueFromUri steps. The scans fall back to different timestamps
  when the audio's own cannot be read, so the fallback is a lambda and
  still runs only on that failure.

Cancellation checks, checkpoint writes, flushes, the copy/delete of the
temporary CUE file and every counter update stay in place and order;
progress values and the scan output are unchanged.
2026-10-19 05:16:33 +07:00
zarzet eeda955363 refactor(android): move SAF batch exists and modtime queries out of MainActivitySafScan.kt
safExistsBatch and getSafFileModTimes answer per-URI document queries
for library and history checks and do not take part in the scan, so
they move verbatim into SafBatchQueries.kt. The file header of
MainActivitySafScan.kt now describes what is left there.
2026-10-19 05:16:33 +07:00
zarzet 671b5a2502 refactor(android): move SAF tree listing and executors out of MainActivitySafScan.kt
Child listing (with the provider-loading retry), readable tree
resolution, the listing prefetcher and the ordered read pool are the
I/O layer under both scans and the history inspection, so they move
verbatim into SafTreeListing.kt together with their pool size
constants (4 listing threads, 6 read workers, unchanged).
SafTreeLister and runSafReadsInOrder become internal because the scans
stay behind. The pause poll constant stays with safScanStopRequested.
2026-10-19 05:16:33 +07:00
zarzet 2af0fa95e5 refactor(android): move SAF CUE helpers out of MainActivitySafScan.kt
The CUE sheet helpers used by both SAF scans (FILE line extraction,
the per-directory child lookup cache and the audio sibling resolver)
move verbatim into SafCueScan.kt. rememberCueDirectoryListing becomes
internal because the scans stay behind; the sibling extension list and
the lookup builder are only used in the new file and stay private.
2026-10-19 05:16:33 +07:00
zarzet bb930827fa refactor(android): move SAF scan checkpointing out of MainActivitySafScan.kt
The resumable-scan checkpoint helpers (state file path, load,
reconcile against written rows, row count and output repair) are a
self-contained unit, so they move verbatim into SafScanCheckpoint.kt.
They were file-private and become internal because scanSafTree stays
in MainActivitySafScan.kt. The checkpoint file name, its line format
and the reconciled NDJSON keys are unchanged.
2026-10-19 05:16:33 +07:00
zarzet 461e449eb2 refactor(android): move SAF file inspection out of MainActivitySafScan.kt
MainActivitySafScan.kt mixes the library scan with unrelated SAF
helpers. resolveSafFile and inspectSafFiles (with its private request
class) serve history file checks, not the scan, so they move verbatim
into SafFileInspection.kt. Signatures and visibility are unchanged.
2026-10-19 05:16:33 +07:00
zarzet fe60a8ac46 test(player): split music_player_automix_test.dart by group
The 2,100-line file mixed four unrelated groups with the AutoMix tests.
The native-channel fake, analyzer and renderer fakes, the shared setUp
and tearDown, and fakeTimeTest move to
test/support/music_player_automix_harness.dart with public names. Each
top-level group gets its own file (haptics transport, playback failures,
system volume, USB transport); the ungrouped AutoMix, Autoplay, queue
and notification tests stay in music_player_automix_test.dart.

Test bodies are moved verbatim; each file aliases the shared tracks and
wait helper to their previous private names. The test count is
unchanged at 81 (50 + 4 + 5 + 6 + 16).
2026-10-19 05:10:50 +07:00
zarzet 8ef3bca69f test(queue): remove real waits from the startup flush test
The startup flush test slept 1.24 s, mostly three 400 ms windows that
must outlast the 350 ms persistence debounce. The body now runs in
VirtualTime, so those waits and the debounce timer advance on virtual
time while platform-channel replies stay real. No production change;
the test body and assertions are unchanged.
2026-10-19 05:10:50 +07:00
zarzet 31999a15c7 test(player): drive AutoMix transport and haptics with fake time
The AutoMix tests slept about 17 s of wall time waiting for fades, tempo
recovery and "nothing happens" windows, and the haptics tests 1.15 s.

fake_async is not a direct dependency, so the analyzer rejects importing
it. Haptics tests run as testWidgets on the binding's FakeAsync clock,
which also drives the PlaybackState position extrapolation.

The AutoMix player shares process-wide runtime state (the listening
recorder and personalization write chains), so a FakeAsync zone that
stops after one test would strand later tests. test/support/virtual_time
adds a zone with virtual timers and real microtasks instead: setUp builds
the player in that zone, and fakeTimeTest runs a body with its timers
fired in due order, skipping idle time. Pending timers continue on real
time afterwards.

AutoMix reads its fade and readiness clocks through autoMixStopwatch,
which defaults to Stopwatch.new; tests point it, and the native fake's
deck clocks, at the virtual clock. Test bodies and assertions are
unchanged.
2026-10-19 05:10:50 +07:00
zarzet c7cccd4edb test(bridge): inject the lookup cache persist debounce
The lookup cache test waited out the 500 ms production debounce four
times (2.7 s). A @visibleForTesting setter lets the test use a 10 ms
debounce while keeping the same 50 to 250 ms margin after the timer
fires for the background encode. The production default stays 500 ms
and the assertions are unchanged.
2026-10-19 05:10:50 +07:00
zarzet 202923fbd9 test(android): pin the extension and MIME tables
ExtensionMimeTablesTest fixes the outputs of extFromFileName,
extFromMimeType, SafDownloadHandler.normalizeExt and mimeTypeForExt, and
NativeFinalizationPolicy.normalizeExtension for every table entry plus
unknown, blank, uppercase and dotted inputs. The expected values come
from reading the code; the tables are not inverse maps, so each keeps
its own entries.

The two normalizers trim, lowercase with Locale.ROOT and add one leading
dot, with "" for blank input; checking the dot before or after
lowercasing cannot differ. normalizeExt now delegates to
normalizeExtension, which becomes internal. The test passed against
both original bodies before the delegation.
2026-10-19 04:58:25 +07:00
zarzet a19a528735 refactor(android): put the SAF IO helpers on Context receivers
The MainActivitySafIo helpers declared a MainActivity receiver although
they only use Context members and the core backend. They now take a
Context receiver, and the ones that reach the backend take it as an
explicit first parameter, so a service can call them with its own
backend. buildLibraryCoverCacheKey used neither and is now a plain
function.

Bodies are unchanged apart from passing the backend through to the
helpers they call. Activity callers pass their own coreBackend, so the
same instances reach the same code. The finalizer keeps using
SafDownloadHandler.copyContentUriToTemp; switching it would add the
MediaStore fallback and MIME-based extension, which is a behavior change.
2026-10-19 04:58:25 +07:00
zarzet 071d72b255 refactor(android): share the foreground wake lock and start helper between the two services
DownloadService and BackgroundWorkService each built the same partial
wake lock (30-minute timeout, 15-minute renew, not reference-counted),
the same dataSync startForeground SDK branch and the same content
PendingIntent to MainActivity. ForegroundWorkLock now holds the lock and
the shared constants, and two small helpers cover the start branch and
the intent.

The renew strategies stay per service: DownloadService renews on its
progress and maintenance calls once 15 minutes have passed, while
BackgroundWorkService re-acquires from a main-thread Handler timer.
Wake-lock tags, channel IDs, notification IDs, intent flags and request
codes are unchanged, and the verification intent keeps its own request
code and extras.
2026-10-19 04:58:25 +07:00
zarzet 4791f55ad8 refactor(providers): share one Coalescer for single-flight request coalescing
The lyrics, MusicBrainz, platform resolver and availability services each
carried their own copy of the same single-flight machinery: a Flight with
an atomic waiter count, a result slot and condvar, a Waiter drop guard, a
worker check that fails once the last waiter leaves, a 25 ms wait_timeout
poll loop, a MAX_FLIGHTS (64) bound and a finish step that publishes the
result, removes the current flight, decrements the active count and wakes
shutdown.

Move that into crate::coalescer (Flight, Waiter, Coalescer). The table
stays inside each service's own state mutex, so cache lookups, joins and
finish remain atomic with that service's cache and generation. Per-site
rules stay at the call sites: cache keys, what finish caches and when,
error values, thread names, worker post-checks, panic messages,
MusicBrainz's atomic try_join, its spawn under the state lock and its
uncached spawn-failure path. Existing tests only change their access path
to the waiter and active counts.
2026-10-19 04:32:25 +07:00
zarzet 4557e36165 chore(l10n): remove 63 unused localization keys
Drop 63 keys (and their @metadata) that no hand-written code references,
from app_en.arb and every locale file. Groups: 24 upgradeIntro*, 11
downloadLossy*, 10 hiResCheck*, 4 storageMode*, and 14 single keys
(aboutKeepAndroidOpen, actionGo, audioInfoSamples,
backupRestoreConfirmMessage, discographySelectedCount,
editMetadataAutoFillDone/Fetch, historySearchHint, homeSubtitle,
libraryBuiltInPlayerInfo, libraryFolderNotExist, mornyeShowTabs,
selectionShareNoFiles, trackReEnrichOnlineSubtitle).

Usage was verified per key with a whole-word search over lib (excluding
lib/l10n), test, integration_test, android, ios, rust_backend, assets and
third_party. selectionShareCount stays: test/downloaded_album_reenrich_test.dart
calls the generated getter in a findsNothing assertion, so removing it would
break compilation.

flutter gen-l10n, flutter analyze and the l10n-using tests pass. Crowdin
will drop the removed strings on the next sync.
2026-10-19 04:29:36 +07:00
zarzet f88217dde4 refactor(android): move the lifecycle backend calls into MainActivityLifecycleCalls.kt
MainActivity.kt is now the dispatcher only. The screen wake,
verification notification, install marker, runtime state, exit,
backend implementation and memory release cases keep their labels and
order in the backend `when` and now call MainActivity.handle<Name>
functions in MainActivityLifecycleCalls.kt; processPendingExtensionCallbacks
stays inline.

Bodies move verbatim. prepareRuntimeState takes the backend that
configureFlutterEngine selected as a parameter, as the case body
captured it before, and flutterBackCallback is widened to internal for
exitApp.
2026-10-19 01:24:48 +07:00
zarzet 00b661be21 refactor(android): move the download service and worker backend calls into MainActivityServiceCalls.kt
MainActivity.kt should be the dispatcher only. downloadByStrategy, the
download foreground service, background work and native download worker
cases keep their labels and order in the backend `when`; the multi-line
ones now call MainActivity.handle<Name> functions in
MainActivityServiceCalls.kt, and the one-line stop/pause/resume/cancel
cases stay inline.

Bodies move verbatim apart from the receiver label for this@MainActivity.
2026-10-19 01:24:48 +07:00
zarzet cbf41441ff refactor(android): move the library scan backend calls into MainActivityLibraryScanCalls.kt
MainActivity.kt should be the dispatcher only. The cover cache, folder
and SAF tree scan, and scan progress/cancel/pause/resume cases keep
their labels and order in the backend `when` and now call
MainActivity.handle<Name> functions in MainActivityLibraryScanCalls.kt.
Bodies move verbatim. Every handler takes (call, result) so the
dispatch lines stay uniform, even where a body ignores the call.
2026-10-19 01:24:48 +07:00
zarzet 5d0f274414 refactor(android): move the metadata and tag backend calls into MainActivityMetadataCalls.kt
MainActivity.kt should be the dispatcher only. The metadata, tag,
cover, re-enrich, post-processing and CUE cases keep their labels and
order in the backend `when` and now call MainActivity.handle<Name>
functions in MainActivityMetadataCalls.kt.

Bodies move verbatim apart from the receiver label for this@MainActivity
and re-indenting the FLAC copy-back lines of editFileMetadata, which sat
at the wrong depth.
2026-10-19 01:24:48 +07:00
zarzet 01c51bf751 refactor(android): move the SAF backend calls into MainActivitySafCalls.kt
MainActivity.kt should be the dispatcher only. The backend `when` keeps
every case label in the same order, so the routing set check and the
dispatch order are unchanged, and each SAF case now calls a
MainActivity.handle<Name> function in MainActivitySafCalls.kt.

Bodies move verbatim; the only edit is this@MainActivity becoming the
extension function's own receiver label.
2026-10-19 01:24:48 +07:00
zarzet cf4c05cd93 refactor(android): move install state, extension intents, SAF leases and small channels out of MainActivity.kt
MainActivity.kt mixed the Activity lifecycle and the backend dispatcher
with several self-contained helpers. Give each its own file as
MainActivity extension functions, following the W2-N split:

- MainActivityInstallState.kt: ensureInstallMarker, prepareRuntimeState
- MainActivityExtensionIntents.kt: verification and OAuth intent
  handling, processPendingExtensionCallbacks, session-grant events
- MainActivitySafLeases.kt: SAF playback descriptor leases
- MainActivitySmallChannels.kt: the concert_calendar and audio_output
  MethodChannels, still registered at the same point and in the same
  order in configureFlutterEngine

The state these helpers touch stays in MainActivity and is widened from
private to internal. Bodies move verbatim except that companion members
are qualified with MainActivity. outside the class.
2026-10-19 01:24:48 +07:00
zarzet ff7afa5b3c refactor(library): split local_library_provider.dart into source, scan and cover parts
The provider file mixed source management, the scan pipeline and the
cover lookup providers. Move the private notifier methods into
extensions in two parts (_LocalLibrarySources, _LocalLibraryScan) and the
cover request types and providers into a third part. The moves are
verbatim except for one reference to the static heartbeat constant,
which an extension must qualify with the class name. Public notifier
methods, which test subclasses override, stay on the class in the
library file.
2026-10-19 01:18:02 +07:00
zarzet de142abae6 refactor(library): share the scan completion tail as _finishScan
The full-scan and incremental branches of _runScan ended with the same
sequence: source state update, last-scanned prefs, summary refresh, the
mounted check, the final state, log and completion notification. Move
that sequence into _finishScan. The only differences, the cover cache
prune after a full scan and the summary text, become parameters; the
summary stays a closure so it still reads the state after the final
update. Each call is the last statement of its branch, so the early
return on unmount keeps the same effect.
2026-10-19 01:18:02 +07:00
zarzet b0600c1ff7 refactor(library): remove the unreachable snapshot sub-branch of the incremental scan
The Dart incremental ingestion branch runs only when
PlatformBridge.supportsCoreBackend (Platform.isAndroid || Platform.isIOS)
is false. Its useSnapshotBridge flag required Platform.isAndroid, so it
could never be true there, and the native handlers for
scanSafTreeIncrementalFromSnapshot and
scanLibraryFolderIncrementalFromSnapshot are already gone.

Remove the flag, both PlatformBridge methods, and
LibraryDatabase.writeFileModTimesSnapshot(Map), whose only other user was
test/library_scan_snapshot_test.dart (deleted with it). The live native
snapshot path (writeNativeFileModTimesSnapshot) and the host-test branch
with _backfillLegacyFileModTimes are unchanged.
2026-10-19 01:18:02 +07:00
zarzet c1c7d421cc refactor(convert): route single-track conversion through the shared pipeline
The track metadata screen's _performConversion re-implemented the batch
conversion pipeline step by step (metadata merge, lyrics, cover temp file,
SAF temp copy, FFmpeg, SAF publish, history or library update, temp
cleanup). Both now call convertLibraryTrack in the batch controller, which
returns a LibraryTrackConversionResult that each caller maps to its own
reporting: batch failure summaries and log lines, or the screen's
existing snackbars.

Caller differences stay as parameters: the cover temp prefix
(batch_cover_ or convert_cover_), whether an empty SAF temp path fails,
the history SAF file name fallback, the screen's eager temp deletes, its
provider reload right after the row is written, its warnings, its cleanup
log text and its lazy artistTagMode read.

One deliberate difference: the screen now reads the track path, item and
audio fields once before the first await instead of re-reading widget state
after each await. Swiping to another track mid-conversion previously published
and recorded the output against the newly shown track; it now stays on the original.
2026-10-19 01:15:18 +07:00
zarzet 5b3a2b9575 refactor(queue): move the completion bridge into CompletionBridgeTracker
The completion bridge maps, the precache set, the History row matcher and the
bodies of the queue lookup and History index listeners lived on the Library
tab State. CompletionBridgeTracker in lib/controllers now owns them. The two
ref.listen registrations stay in build at the same positions and call into
the tracker; the State forwards the old field names as getters so the part
files read the same collections.

Guards, the setState that drops removed items, the file check refresh order
(file cache, then playable probe), the offset reset and the mounted rebuild
after a History index change are unchanged.
2026-10-19 01:12:21 +07:00
zarzet 830dee31ed refactor(queue): move library paging state into QueueLibraryPager
The Library tab State held the page offsets, the load-more guard, the count
and page snapshot caches, the blank-library repair throttle and their trim
and scope rules inline. QueueLibraryPager in lib/controllers now owns that
state and logic; the State keeps one _pager field and forwards, passing
setState, mounted and the provider reloads as callbacks.

The page request and page data types implement two small interfaces so the
pager can compare scopes and test emptiness without seeing the private
types. Provider reads, page size, trim limits, the repair throttle and the
order of cache writes and setState calls are unchanged.
2026-10-19 01:12:21 +07:00
zarzet db4643c26e refactor(bridge): share one generic cache entry and put/prune helper
The map and list lookup caches had twin entry classes and twin put and
prune helpers that differed only in the value type. Replace them with
_BridgeCacheEntry<T>, _putMemoryCached<T> and _pruneExpiredBridgeCache<T>.
Each cache keeps its own map, TTL and budget accounting; the byte estimate,
shared budget constants and oldest-first eviction are unchanged.
2026-10-19 01:06:12 +07:00
zarzet 857c2954d8 refactor(collections): move playlist cover file I/O into PlaylistCoverStore
LibraryCollectionsNotifier carried about 130 lines of File and Directory
work for playlist covers. PlaylistCoverStore now owns the playlist_covers
directory, the <playlistId>.jpg normalization with its temp file, the
backup restore copy or base64 write, and the delete helpers. The
directory is still resolved through getApplicationSupportDirectory on
each call, at the same points as before.

The notifier keeps setPlaylistCover, removePlaylistCover,
exportPlaylistCoverFiles and restoreFromBackup with unchanged signatures,
mutation ordering and error handling: a failed restore of one cover still
drops that cover path, removePlaylistCover still propagates delete
errors, and replacing a cover still deletes the old file quietly.
2026-10-19 01:01:25 +07:00
zarzet 8fffb9230a refactor(cache): move directory scanning and clearing into CacheMaintenanceService
The cache management page walked and deleted directories itself. Move
_scanDirectory and _clearDirectoryContents verbatim into
CacheMaintenanceService.scanDirectory and clearDirectoryContents in
lib/services/cache_maintenance_service.dart, with _DirectoryStats
becoming the public DirectoryStats it returns.

The page calls the service at the same points with the same arguments,
so scan concurrency, the 24-entity delete chunks, the directory
re-creation after clearing and every action key are unchanged.
2026-10-19 00:59:24 +07:00
zarzet 755c907982 refactor(analysis): move the FFmpeg runner out of the widget
The card ran ffprobe, both FFmpeg sessions, SAF temp copies, the
native spectral-cutoff job and its cancellation inside State. Move that
pipeline verbatim into AudioAnalysisRunner in
lib/services/audio_analysis_runner.dart together with the private
per-run records from the widget's models part, which is now empty and
removed.

The card keeps request ids and every setState. The runner takes a
`bool Function() isCancelled` that the card builds from the same
`!mounted || requestId != _spectrogramRequestId` test, polled at the
same points that used to call _checkAnalysisRequest. cancel() and
dispose() run the jobs invalidate/dispose, network cancel and native
job cancel in the original order.

Subprocess order, temp file names and cleanup, SAF copy and the native
data job payload are unchanged.
2026-10-19 00:59:24 +07:00
zarzet b575a4ea38 refactor(analysis): move the analysis disk cache into audio_analysis_cache.dart
The card owned the bundle storage (key derivation, {key}.json,
{key}.png and {key}_ch{n}.png, the shared trim budget) as static
members, and the cache page spelled the audio_analysis_cache directory
literal twice more.

Move the storage verbatim into AudioAnalysisDiskCache next to the
existing lease/trim policy. AudioAnalysisDiskCache.directoryIn is now
the only place that names the directory; the card's cacheDir and both
cache page call sites use it, with the page keeping its non-creating
behavior and await order. loadFromCache takes the card's network input
as a parameter instead of reading the field.

The 63-bit FNV-1a cache key stays local: lib/utils/hash_utils.dart
fnv1a32 uses a different basis, prime and width, so reusing it would
rename every cached bundle on devices.
2026-10-19 00:59:24 +07:00
zarzet 34a004f96f refactor(analysis): move the pure FFmpeg builders and parsers to a service
The audio analysis card mixed FFmpeg argument builders, astats and
metadata parsers and the spectral cutoff estimator with widget state.
Move them verbatim into lib/services/audio_analysis_ffmpeg.dart so the
runner and cache can depend on them without importing a widget.

The parsers return ChannelAnalysisStats, so the two public result
models (AudioAnalysisData, ChannelAnalysisStats) move verbatim from the
widget's models part into lib/services/audio_analysis_models.dart. The
per-run private records stay in the part for now.

Only two names change: the unsupported-codec exception loses its
leading underscore because the widget and the future runner live in
different libraries, and the in-class _parseChannelStats becomes the
top-level parseChannelStats. The widget re-exports both service files,
so tests and importers are unchanged.
2026-10-19 00:59:24 +07:00
zarzet 7628800604 refactor(setup): move SharedPreferences access behind SetupProgressStore and SafMigrationPromptStore
The setup draft and the SAF migration prompt flag were read and written
directly from screens. Keys, JSON shape and defaults are unchanged.
2026-10-19 00:56:52 +07:00
zarzet 8af7efb6ce refactor(providers): move UI-declared providers to lib/providers
The listening summary and system volume providers lived in UI files.
They move verbatim to lib/providers with the same names; the old files
re-export them so tests and other importers are unchanged.
2026-10-19 00:56:52 +07:00
zarzet c10c0ab5f5 docs(rust): add module docs to large modules without one
More than half of the crate files had no module doc, so a contributor had to
read a large file top to bottom to learn what it owns. Every file over 300
lines now states its responsibility in one or two lines.
2026-10-19 00:56:51 +07:00