Commit Graph
2495 Commits
Author SHA1 Message Date
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 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 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 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 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 bff5123c0e refactor(concerts): move the calendar channel into a service
The screen built the method channel inline. ConcertCalendarService owns it
with the same channel name, method and payload keys.
2026-10-19 00:48:31 +07:00
zarzet ba376d69fc refactor(repo): prepare install paths in the repo notifier
The store list and the detail screen both built the temp and extension
directories before calling the notifier. The notifier now offers
installExtensionToDefaultPaths and updateExtensionToDefaultPaths; the
existing methods keep their signatures.
2026-10-19 00:48:31 +07:00
zarzet b10370e4fc refactor(share): share the widget capture helper
The lyrics card and the rewind slide both rendered a RepaintBoundary to a
temp PNG. captureBoundaryToPngFile does that; pixel ratio, file name, frame
waiting and share parameters stay with each caller.
2026-10-19 00:48:31 +07:00
zarzet da2fc2c9c9 refactor(extensions): share the extension health color and icon helpers
The detail page and the list page carried identical color mappings. Only
color and icon move; each screen keeps its own label mapping.
2026-10-19 00:48:31 +07:00
zarzet fe29f304aa refactor(playlists): share the playlist name prompt and dispose its controller
The create, picker and rename dialogs were three copies of one form. They
now call promptPlaylistName, whose controller lives in a dialog state and is
disposed with it.
2026-10-19 00:48:31 +07:00
zarzet 485be2f468 refactor(artist): move the discography download into a part
artist_screen.dart mixed the artist page with the whole discography
download flow (option sheet, album fetching, queueing and its progress
dialog). Moving that flow into its own part keeps one responsibility per
file and shrinks the screen file. Bodies are moved verbatim.
2026-10-19 00:48:05 +07:00
zarzet 263a10171d refactor(home): use downloadSingleTrack for search results
The search result download re-implemented the shared flow with the same
provider calls and snackbars; the helper only adds a mounted check.
2026-10-19 00:47:55 +07:00
zarzet 4598773cd3 refactor(storage): share the SAF download tree pick
Three screens repeated the pick, parse and store sequence. The helper keeps
each site's call order; the SAF migration prompt skips the storage mode
switch as before.
2026-10-19 00:47:55 +07:00
zarzet 78799e1bbc refactor(layout): share responsiveScale and effectiveTextScale
The same clamp helpers were copied into four screens. They now live in
adaptive_layout.dart with identical bodies and clamp values.
2026-10-19 00:47:54 +07:00
zarzet 1521c49508 docs(selection): correct the SelectionModeMixin doc
The doc named queue_tab as a user, but queue_tab has its own selection state.
List the screens that actually mix it in.
2026-10-19 00:37:42 +07:00
zarzet 99ccc8826f refactor(layering): move logic files out of screens
Search bucketing, refresh policy, folder data, history snapshot and the
selection and collapsing-header mixins hold no widgets, so they move to
controllers; the settings search catalog is pure presentation metadata and
moves to utils. Files with many importers keep a re-export in screens.
2026-10-19 00:37:42 +07:00
zarzet ce21896ff5 refactor(layering): move model files to models and fix inverted imports
library_database_models and ffmpeg_models are plain data types, so they live in
models (re-exports stay at the old paths). deleted_library_files uses providers
and download_progress uses provider state, so both move to providers, and
ffmpeg_reenrich is a service. models no longer import from services for these.
2026-10-19 00:37:42 +07:00
zarzet ab8a329d2d refactor(layering): move UI action helpers from services and utils to controllers
Batch actions, clickable metadata, playback, auth launcher, action forms and
the glass warmup drive UI flows, so they belong with the controllers. Files
with more than three importers (or an importer that must stay untouched) keep
a one-line re-export at the old path.
2026-10-19 00:37:42 +07:00
zarzet ff3f752014 refactor(metadata): share the sidecar publish path between save-cover and save-lyrics
_saveCoverArt and _saveLyrics repeated the same SAF/local publish
skeleton: render into a temp dir and publish it to the download tree
with createSafFileFromPath (or write next to the audio file), clean the
temp dir, then report the outcome. Move that skeleton into
_publishSidecarFile with parameters for the temp prefix, extension, mime
type, success message and whether a snackbar hides the current one.

Each caller keeps its own try/catch, base name, lyrics progress
snackbar and the render step. The cover render returns null after its
no-source snackbar so the helper stops at the same point as before
(without deleting the temp dir, as before). Await order, mounted checks,
messages and cleanup placement are unchanged.
2026-10-19 00:23:50 +07:00
zarzet adf2047df6 refactor(utils): share the FNV-1a 32-bit hash
Add fnv1a32 in hash_utils.dart for the scan output name (UTF-8 bytes) and
qualityVariantStagingLabel (UTF-16 code units). Unit tests pin outputs
computed from the original inline code.
2026-10-19 00:15:48 +07:00
zarzet 036589a4b1 refactor(notifications): reuse formatMegabytes
The progress notification formatted megabytes inline with the same
expression as formatMegabytes (one decimal, power-of-two scaling, so the
double result is identical). It now calls the helper. No output change.
2026-10-19 00:15:48 +07:00
zarzet d6fd05c3a7 refactor(io): use isContentUri instead of inline content:// checks
The same startsWith check was inlined in 21 places across services and
providers. They now call the existing helper, which has identical
semantics (no trim, no case folding). No behavior change.
2026-10-19 00:15:48 +07:00
zarzet 0797085385 refactor(io): use deleteFileQuietly for exists-then-delete
Add deleteFileQuietly to file_access.dart (exists check, delete, swallow
errors) and use it for the try/exists/delete/catch (_) {} copies.
2026-10-19 00:15:48 +07:00
zarzet 92d655fd1c refactor(player): move restorePlaybackSession to playback_session_restore.dart
The function only needed the file-level logger, which the new file declares
with the same tag. It is re-exported from music_player_service.dart.
2026-10-18 23:53:44 +07:00
zarzet 548567eff3 refactor(player): move PlayableMedia to playable_media.dart
The class moves verbatim and is re-exported from music_player_service.dart.
The toJson/fromJson artwork round trips stay: toJson drops non-positive
numbers, blank formats and empty artwork, so a copyWith would not be an
exact replacement for them.
2026-10-18 23:53:44 +07:00
zarzet 992e21e297 refactor(player): port tests to musicPlayerRuntime and delete the test-only wrappers
The wrappers only forwarded to musicPlayerRuntime and were referenced by one
test file. The forwarding helpers now live in that test.
2026-10-18 23:53:38 +07:00
zarzet 95666c1043 refactor(settings): move log_screen widgets into a part
The page file mixed the log_screen State with standalone private
widgets and helpers. They move verbatim into log_screen_widgets.dart so the
page file holds only the page and its State. Private visibility is
unchanged because parts share the library. No behavior change.

_domainPattern moves with its only user, _LogSummaryCard.
2026-10-18 23:49:17 +07:00
zarzet 0d2fef4c33 refactor(settings): move extensions_page widgets into a part
The page file mixed the extensions_page State with standalone private
widgets and helpers. They move verbatim into extensions_page_widgets.dart so the
page file holds only the page and its State. Private visibility is
unchanged because parts share the library. No behavior change.
2026-10-18 23:49:17 +07:00
zarzet 6e43c03691 refactor(settings): move appearance_settings_page widgets into a part
The page file mixed the appearance_settings_page State with standalone private
widgets and helpers. They move verbatim into appearance_settings_page_widgets.dart so the
page file holds only the page and its State. Private visibility is
unchanged because parts share the library. No behavior change.
2026-10-18 23:49:17 +07:00
zarzet a3961eeff5 refactor(settings): move files_settings_page widgets into a part
The page file mixed the files_settings_page State with standalone private
widgets and helpers. They move verbatim into files_settings_page_widgets.dart so the
page file holds only the page and its State. Private visibility is
unchanged because parts share the library. No behavior change.
2026-10-18 23:49:16 +07:00
zarzet 486fd3b6e7 refactor(settings): move download_settings_page widgets into a part
The page file mixed the download_settings_page State with standalone private
widgets and helpers. They move verbatim into download_settings_page_widgets.dart so the
page file holds only the page and its State. Private visibility is
unchanged because parts share the library. No behavior change.
2026-10-18 23:49:16 +07:00