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.
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.
Update settings, About, provider descriptions, and lyric source credits. Preserve the stored provider ID and recognize both current and legacy credits as direct sources.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.