mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 22:41:38 +02:00
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.
This commit is contained in:
1 parent
c6f8ac21b5
commit
d3a51a6a36
1 file changed
+327
+327
@@ -148,6 +148,333 @@ Do not add provider-name checks such as `if source == 'provider-name'` to the
|
||||
main app. The backend should parse and expose the generic declaration, and Dart
|
||||
should consume that declaration without knowing which extension uses it.
|
||||
|
||||
The sections below expand this layout. Folder guides hold the details that are
|
||||
not repeated here: [android/README.md](android/README.md) (native channel
|
||||
contract), [rust_backend/README.md](rust_backend/README.md) (crate map),
|
||||
[test/README.md](test/README.md),
|
||||
[integration_test/README.md](integration_test/README.md),
|
||||
[lib/theme/README.md](lib/theme/README.md), and the `SPOTIFLAC_PATCHES.md` file
|
||||
in each `third_party/` fork, for example
|
||||
[audio_service](third_party/audio_service/SPOTIFLAC_PATCHES.md).
|
||||
|
||||
## Codebase Map
|
||||
|
||||
### Flutter (`lib/`)
|
||||
|
||||
App files import each other as `package:spotiflac_android/...`. The intended
|
||||
direction is screens and widgets on top, then controllers, providers, services,
|
||||
and models and utilities at the bottom. The last column is the current state
|
||||
found by scanning import lines, including today's exceptions; do not add new
|
||||
ones.
|
||||
|
||||
| Folder | What belongs there | Imports today |
|
||||
| --- | --- | --- |
|
||||
| `main.dart`, `app.dart` | Startup, `ProviderScope`, `SpotiFLACApp`, and the `GoRouter` (`/`, `/setup`, `/tutorial`) | Any folder |
|
||||
| `screens/` | Routes and tab roots; large screens split into `<screen>_*.dart` files; settings pages in `screens/settings/` | Any folder |
|
||||
| `widgets/` | Reusable widgets and design-system components | Any folder except `screens/`; exceptions: `library_search_results.dart`, `artist_concerts_button.dart`, `mini_player.dart` |
|
||||
| `controllers/` | UI logic shared by screens and widgets: selection mixins, batch actions, the metadata editor | controllers, widgets, providers, services, models, utils, theme, l10n; `clickable_metadata.dart` also imports `screens/` |
|
||||
| `providers/` | Riverpod notifiers and state; large notifiers split into `part` files | providers, services, models, utils, constants |
|
||||
| `services/` | `PlatformBridge`, SQLite stores, the music player, FFmpeg, network storage, notifications | services, models, utils, constants; exceptions: re-export stubs into `providers/` and `controllers/`, and `notification_service.dart` imports `l10n/` |
|
||||
| `models/` | Data classes and their `json_serializable` output (`*.g.dart`) | models, utils; `library_database_models.dart` imports `sqlite_helpers.dart` from `services/` |
|
||||
| `utils/` | Small helpers, pure policies, and the settings search catalog | utils, models, constants, l10n; seven files import `services/` (for example `file_access.dart`, `logger.dart`), `saf_download_tree.dart` imports `providers/`, three re-export stubs point into `controllers/` |
|
||||
| `theme/` | `AppTheme`, `AppTokens`, motion, icons, and the Mornye theme | theme, models; `dynamic_color_wrapper.dart` imports `providers/`, `cover_palette.dart` imports `widgets/` and `services/` |
|
||||
| `l10n/` | ARB sources in `arb/`, the `context.l10n` extension in `l10n.dart`, and `supported_locales.dart`; `app_localizations*.dart` is generated | utils |
|
||||
| `constants/` | App info, language choices, `MusicServices` IDs | l10n |
|
||||
| `routes/` | `now_playing_route.dart` | services, theme |
|
||||
|
||||
No provider or service imports `screens/` or `widgets/`. Among services, only
|
||||
the navigation, orientation, music player, and player widget services import
|
||||
Flutter's `material.dart` or `widgets.dart`; among providers, the preview
|
||||
player, extension, download queue, download queue policy and theme providers
|
||||
do.
|
||||
|
||||
### Rust (`rust_backend/crates/`)
|
||||
|
||||
Workspace dependencies point only downward:
|
||||
|
||||
| Crate | Package | Role | Depends on |
|
||||
| --- | --- | --- | --- |
|
||||
| `core/` | `spotiflac-core` | Shared logic without bindings: tags, lyrics parsing and provider config, metadata, filenames, CUE, covers | none |
|
||||
| `network/` | `spotiflac-network` | Shared, cancellable HTTP transport | none |
|
||||
| `providers/` | `spotiflac-providers` | Built-in clients: Deezer, MusicBrainz, platform resolver, lyrics | core, network |
|
||||
| `extensions/` | `spotiflac-extensions` | Extension JavaScript runtime (rquickjs), host functions, manager, repository, and backend operations such as downloads, library scan, and tags | core, network, providers |
|
||||
| `mobile/` | `spotiflac-mobile` | UniFFI API, mostly methods on `ExtensionManager`; built as `cdylib` and `staticlib`; Kotlin package `com.spotiflac.backend`, Swift module `SpotiFLACBackend` | core, network, extensions |
|
||||
| `bindgen/` | `spotiflac-bindgen` | UniFFI bindgen CLI used by `scripts/build_rust_backend.sh` | none |
|
||||
|
||||
### Native
|
||||
|
||||
Android Kotlin lives in `android/app/src/main/kotlin/com/zarz/spotiflac/`.
|
||||
`android/app/build.gradle.kts` adds `android/app/src/rust/kotlin/com/zarz/spotiflac/`
|
||||
and the generated bindings to the same source set.
|
||||
|
||||
| Files | Role |
|
||||
| --- | --- |
|
||||
| `MainActivity.kt` | Backend channel `com.zarz.spotiflac/backend`, the `nativeBackendMethods` set, and the `when (call.method)` dispatch |
|
||||
| `MainActivity*Calls.kt`, `MainActivitySaf*.kt`, `MainActivityProgressStreams.kt`, `MainActivitySmallChannels.kt` | `MainActivity.handleX(call, result)` extension functions grouped by area, progress EventChannels, small channels |
|
||||
| `DownloadService*.kt`, `BackgroundWorkService.kt` | Foreground services: the native download worker and its snapshot, other background work |
|
||||
| `NativeDownloadFinalizer.kt`, `NativeFinalizer*.kt`, `NativeHistoryStore.kt` | Post-download finalization and the native history write |
|
||||
| `Saf*.kt` | Storage Access Framework helpers |
|
||||
| `UsbBitPerfectPlugin.kt`, `Usb*.kt`, `BitPerfectPcm.kt`, `Dsd*.kt`, `HiResPlayback.kt`, `NativeAudio.kt` | Bit-perfect and USB playback (`com.zarz.spotiflac/usb_pcm`) |
|
||||
| `PlayerWidget*.kt`, `DownloadQueueWidgetProvider.kt` | Home-screen widgets |
|
||||
| `CoreBackend.kt`; `SelectedCoreBackend.kt`, `RustCoreBackendApplication.kt`, `NativeDataJobs.kt` | The `CoreBackend` interface and its Rust implementation `RustCoreBackend`, which maps method names to UniFFI calls |
|
||||
|
||||
iOS code lives in `ios/Runner/`: `AppDelegate.swift` (backend channel,
|
||||
`handleMethodCall` and its `osMethods` set, progress EventChannels, background
|
||||
tasks), `CoreBackend.swift` (Rust calls through `SpotiFLACBackend`,
|
||||
`invokeApplication`), and smaller bridges such as `PlayerWidgetBridge.swift`
|
||||
and `ConcertCalendarBridge.swift`. The WidgetKit extension is in
|
||||
`ios/PlayerWidget/`.
|
||||
|
||||
## Where Each Feature Lives
|
||||
|
||||
Dart paths are under `lib/`, Rust paths under `rust_backend/crates/`, and
|
||||
Kotlin files in the Android folders above.
|
||||
|
||||
| Feature | Dart | Rust | Native |
|
||||
| --- | --- | --- | --- |
|
||||
| Search and URL import | `providers/track_provider.dart` (`TrackNotifier`), `controllers/home_search_logic.dart` | `extensions/src/backend/provider_metadata.rs`, `mobile/src/metadata.rs` | `RustCoreBackendApplication.kt`, `CoreBackend.swift` |
|
||||
| Download | `providers/download_queue_provider.dart` and its `download_queue_provider_*.dart` parts, `services/download_request_payload.dart` | `extensions/src/backend/downloads.rs`, `mobile/src/manager.rs` | `DownloadService.kt`, `SafDownloadHandler.kt`, `NativeDownloadFinalizer.kt` |
|
||||
| Queue persistence and history | `services/download_queue_persistence.dart`, `providers/download_history_provider.dart`, `services/history_database.dart`, `services/history_schema.dart` | none | `NativeHistoryStore.kt`, `NativeFinalizerHistory.kt`, `DownloadServiceSnapshot.kt` |
|
||||
| Local library scan | `providers/local_library_provider.dart` and its `local_library_provider_*.dart` parts, `services/library_database.dart` | `extensions/src/backend/library/scan.rs`, `mobile/src/tags.rs`, `mobile/src/native_library.rs` | `MainActivityLibraryScanCalls.kt`; SAF trees in `MainActivitySafScan.kt` and the `Saf*.kt` files |
|
||||
| Playback and queue | `providers/playback_provider.dart`, `providers/music_player_provider.dart`, `services/music_player_service.dart` (automix and autoplay parts), `services/music_player_runtime.dart`, `services/music_playback_deck.dart` | AutoMix analysis: `core/src/native_audio_analysis.rs` via `mobile/src/data_jobs.rs` | `UsbBitPerfectPlugin.kt`; forks in `third_party/audio_service`, `third_party/audioplayers_android`, `third_party/audioplayers_darwin` |
|
||||
| Lyrics | `utils/lyrics_parser.dart`, `utils/lyrics_metadata_helper.dart`, `controllers/player_metadata_controller.dart` | `providers/src/lyrics/`, `core/src/lyrics/`, `extensions/src/lyrics.rs`, `mobile/src/lyrics.rs`, `mobile/src/native_lyrics.rs` | `SelectedCoreBackend.kt`, `CoreBackend.swift` |
|
||||
| Metadata editing and conversion | `controllers/metadata_editor_controller.dart`, `screens/track_metadata_edit_sheet.dart`, `screens/track_metadata_convert.dart`, `services/ffmpeg_service.dart` (FFmpeg Kit through `services/ffmpeg_support.dart`) | `core/src/tags/`, `extensions/src/backend/tags.rs`, `mobile/src/tags.rs` | `MainActivityMetadataCalls.kt` |
|
||||
| Extensions and repository | `providers/extension_provider.dart`, `providers/repo_provider.dart`, `models/extension.dart`, `screens/settings/extensions_page.dart`, `screens/repo_tab.dart` | `extensions/src/manifest.rs`, `extensions/src/runtime.rs`, `extensions/src/manager/`, `extensions/src/repository/`, `mobile/src/extensions.rs`, `mobile/src/repository.rs` | `RustCoreBackendApplication.kt`, `ExtensionAuthCallbacks.kt`, `MainActivityExtensionIntents.kt`, `ExtensionCallbackParser.swift` |
|
||||
| Settings | `providers/settings_provider.dart`, `models/settings.dart`, `screens/settings/`, `utils/settings_search_catalog.dart` | none; values the backend needs are pushed by the `_sync*ToBackend` methods of `SettingsNotifier` | none |
|
||||
| Statistics and Rewind | `providers/listening_statistics_provider.dart`, `services/listening_statistics.dart`, `providers/listening_rewind_provider.dart`, `services/listening_rewind.dart` | none | none |
|
||||
| Widgets and notifications | `services/player_widget_service.dart`, `services/notification_service.dart`, `services/playback_notification.dart`, `providers/playback_notification_provider.dart` | none | `PlayerWidgetProvider.kt`, `DownloadQueueWidgetProvider.kt`, notifications in `DownloadService.kt`, `PlayerWidgetBridge.swift`, `ios/PlayerWidget/` |
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Download (Android native worker)
|
||||
|
||||
1. `downloadSingleTrack` (`lib/widgets/track_detail_actions.dart`) calls
|
||||
`DownloadQueueNotifier.addToQueue`, which appends a `DownloadItem`, saves
|
||||
the queue, and schedules `_processQueue`.
|
||||
2. `_processQueueSingleFlight` (`lib/providers/download_queue_provider.dart`)
|
||||
checks storage and network mode, then calls
|
||||
`_tryProcessQueueWithAndroidNativeWorker`
|
||||
(`lib/providers/download_queue_provider_native_worker.dart`). It proceeds
|
||||
only when `_canUseAndroidNativeWorker` holds: Android,
|
||||
`nativeDownloadWorkerEnabled` and `useExtensionProviders` on, an enabled
|
||||
extension with a download provider, a download tree URI when SAF storage is
|
||||
in use, and no network-folder items.
|
||||
3. It builds the first request with `_buildAndroidNativeWorkerRequest` and calls
|
||||
`PlatformBridge.startNativeDownloadWorker`, which writes requests and
|
||||
settings to JSON files and sends their paths. Remaining items follow with
|
||||
`appendNativeDownloadWorkerRequests` and
|
||||
`finishNativeDownloadWorkerPreparation`.
|
||||
4. `MainActivity.handleStartNativeDownloadWorker` (`MainActivityServiceCalls.kt`)
|
||||
calls `DownloadService.startNativeQueueFromFiles`, which starts the
|
||||
foreground service. `onStartCommand` runs `startNativeWorker`, then
|
||||
`runNativeWorkerConcurrent` and `processConcurrentNativeRequest` per item.
|
||||
5. `SafDownloadHandler.handle` calls `CoreBackend.downloadByStrategy`. The Rust
|
||||
implementation in `SelectedCoreBackend.kt` calls the UniFFI
|
||||
`ExtensionManager.downloadByStrategy` (`mobile/src/manager.rs`), which runs
|
||||
`download_by_strategy` in `extensions/src/backend/downloads.rs`.
|
||||
6. On success, `NativeDownloadFinalizer.finalize` finishes the file and, when
|
||||
`save_download_history` is set, writes the row with
|
||||
`NativeHistoryStore.upsert`. `writeNativeWorkerSnapshot`
|
||||
(`DownloadServiceSnapshot.kt`) publishes item state.
|
||||
7. `NativeDownloadWorkerMonitor.run` polls
|
||||
`PlatformBridge.getNativeDownloadWorkerSnapshot`.
|
||||
`_applyAndroidNativeWorkerSnapshot` updates the items and acknowledges them
|
||||
with `acknowledgeNativeDownloadWorkerItems`; `_completeAndroidNativeWorkerItem`
|
||||
calls `reconcileNativeDownloadHistory` (`lib/services/native_download_history.dart`)
|
||||
so `DownloadHistoryNotifier` adopts or reloads the native row.
|
||||
|
||||
Without the native worker (iOS, or when the gate fails), items run through
|
||||
`_downloadSingleItem` (`lib/providers/download_queue_provider_single_item.dart`),
|
||||
which calls `PlatformBridge.downloadByStrategy`, finalizes in Dart, and records
|
||||
history with `DownloadHistoryNotifier.addToHistory`. Android handles that call
|
||||
in `handleDownloadByStrategy`; iOS in `CoreBackend.swift`.
|
||||
|
||||
### Playback
|
||||
|
||||
1. A screen calls `PlaybackController` (`lib/providers/playback_provider.dart`),
|
||||
for example `playTrackList` from `library_tracks_folder_screen.dart`. When
|
||||
`playerMode` is not `internal` and the source is not `network://`, it opens
|
||||
the file in an external app instead.
|
||||
2. `MusicPlayerController.playAll` (`lib/providers/music_player_provider.dart`)
|
||||
calls `ensureInitialized`, which runs `MusicPlayerRuntime.initialize`
|
||||
(`lib/services/music_player_runtime.dart`),
|
||||
which creates the `MusicPlayerHandler` through `AudioService.init` from the
|
||||
`third_party/audio_service` fork, then calls `setQueueAndPlay`.
|
||||
3. `MusicPlayerHandler.setQueueAndPlay` (`lib/services/music_player_service.dart`)
|
||||
publishes `queue`, then `_playIndex` and `_loadIndex` publish `mediaItem`
|
||||
and call `_startResolvedSource`.
|
||||
4. `_startResolvedSource` calls `MusicPlaybackDeck.setSource` or
|
||||
`setNetworkSource` and `resume` (`lib/services/music_playback_deck.dart`).
|
||||
The deck wraps an audioplayers `AudioPlayer`, served by the
|
||||
`third_party/audioplayers_android` and `third_party/audioplayers_darwin`
|
||||
overrides; with USB bit-perfect enabled on Android it first tries to
|
||||
prepare the file through `UsbBitPerfectPlugin.kt`.
|
||||
5. Player events reach `_broadcastState`, which updates `playbackState`.
|
||||
`MusicPlayerRuntime.playbackStateEvents`, `mediaItemEvents`, and
|
||||
`queueEvents` feed `playbackStateProvider`, `currentMediaItemProvider`, and
|
||||
`playQueueProvider`, which the mini player and Now Playing screen watch.
|
||||
|
||||
## How to Add Things
|
||||
|
||||
### A Setting
|
||||
|
||||
- Add the field and its default to `AppSettings` (`lib/models/settings.dart`,
|
||||
constructor and `copyWith`), then run `build_runner` (see
|
||||
[Generated Files](#generated-files)) to refresh `settings.g.dart`.
|
||||
- `SettingsNotifier` stores `AppSettings.toJson()` as one JSON string under the
|
||||
SharedPreferences key `app_settings`, so the JSON key is the Dart field name.
|
||||
Renaming a field changes the stored key. Use a `@JsonKey(fromJson: ...)`
|
||||
helper to clamp stored values, as the AutoMix fields do.
|
||||
- Add a setter to `SettingsNotifier` (`lib/providers/settings_provider.dart`):
|
||||
`state = state.copyWith(...)` followed by `_saveSettings()`. If the backend
|
||||
needs the value, push it as `_syncLyricsSettingsToBackend` does.
|
||||
- Rewrites of existing stored values go in `_runMigrations`: bump
|
||||
`_currentMigrationVersion` and guard the step with `if (lastMigration < N)`.
|
||||
- Add the row to the owning page in `lib/screens/settings/` and a
|
||||
`SettingsSearchEntry` to the matching list in
|
||||
`lib/utils/settings_search_catalog.dart`.
|
||||
- Add strings to `lib/l10n/arb/app_en.arb` with an `@key` description and read
|
||||
them through `context.l10n`.
|
||||
- Test with the JSON round trip in `test/app_settings_model_test.dart`, setter
|
||||
and persistence cases like `test/settings_persistence_test.dart` (a
|
||||
`SettingsNotifier` subclass over a fake `SharedPreferences`), and
|
||||
`test/settings_search_test.dart`.
|
||||
|
||||
### A Screen
|
||||
|
||||
- Put it in `lib/screens/` (settings pages in `lib/screens/settings/`) and split
|
||||
a large screen into `<screen>_*.dart` files.
|
||||
- Push it with `Navigator.push` and a `MaterialPageRoute` or `slidePageRoute`
|
||||
(`lib/widgets/animation_utils.dart`). Tabs keep their own `Navigator` from
|
||||
`_TabNavigator` in `lib/screens/main_shell.dart`; `GoRouter` in `lib/app.dart`
|
||||
owns only `/`, `/setup`, and `/tutorial`.
|
||||
- Use the shared components: `AppSliverHeader`, `showAppBottomSheet` and
|
||||
`AppSheetHandle`, `SettingsGroup`, and the `AppTokens` scale.
|
||||
- A track collection screen uses `CollectionScaffold` and `SelectionModeMixin`,
|
||||
and its file name joins the `screens` list in the "every collection screen
|
||||
goes through the shared shell" test.
|
||||
- `test/design_system_test.dart` scans every file in `lib/`: each `IconButton`
|
||||
and `AppIconButton` passes `tooltip`, and only allowlisted files may contain
|
||||
`expandedTitleScale` (`app_sliver_header.dart`), a hand-built drag handle
|
||||
(`app_bottom_sheet.dart`), a modal sheet shape (`app_theme.dart`), or
|
||||
`OverlayEntry(` (`selection_bottom_bar.dart`, `app_menu_button.dart`).
|
||||
|
||||
### A Platform Channel Method
|
||||
|
||||
- Dart: add a static method to `PlatformBridge` in
|
||||
`lib/services/platform_bridge.dart` that calls `_channel.invokeMethod` with
|
||||
snake_case argument keys; helper classes and codecs can go in a part under
|
||||
`lib/services/platform_bridge/`. Decode JSON results with
|
||||
`_decodeJsonResultAsync` or `_decodeRequiredMapResultAsync`.
|
||||
- Rust-backed methods: on Android a name outside `nativeBackendMethods` goes
|
||||
through `dispatchBackendApplication` (`SelectedCoreBackend.kt`) to
|
||||
`RustCoreBackend.dispatchApplication` (`RustCoreBackendApplication.kt`); add
|
||||
the case there. On iOS add it to `invokeApplication` in
|
||||
`ios/Runner/CoreBackend.swift`.
|
||||
- Android-only methods: add the name to `nativeBackendMethods` and a case to
|
||||
the `when (call.method)` block in `MainActivity.kt`, calling a
|
||||
`MainActivity.handleX` function in the matching `MainActivity*Calls.kt` file.
|
||||
`MainActivityRoutingTest.kt` fails when the set and the cases differ.
|
||||
- iOS-only methods: add the name to `osMethods` in `handleMethodCall`
|
||||
(`ios/Runner/AppDelegate.swift`) and handle it there or in `invokeOsMethod`.
|
||||
`test/source_contracts_test.dart` pins parts of that file.
|
||||
- Large JSON: `bridgeJsonResult` in `MainActivity.kt` and `AppDelegate.swift`
|
||||
writes a result of 256 KiB or more to a temporary file and returns
|
||||
`{"__json_file": "<path>"}`; Dart reads and deletes it. Use it for any result
|
||||
that can grow with the library.
|
||||
- New Rust entry points are `#[uniffi::export]` items in
|
||||
`rust_backend/crates/mobile/src/`; the build regenerates bindings (see
|
||||
[Development Setup](#development-setup)).
|
||||
- Dart tests stub `com.zarz.spotiflac/backend` with `setMockMethodCallHandler`,
|
||||
for example `test/extension_initialization_test.dart`.
|
||||
|
||||
### A Lyrics Provider
|
||||
|
||||
- Provider-specific sources fit an extension first: installed lyrics
|
||||
extensions appear in the provider order as `extension:<id>` entries, and
|
||||
`InstalledLyricsFetcher` (`rust_backend/crates/extensions/src/lyrics.rs`)
|
||||
lists and calls them.
|
||||
- A built-in provider is a `pub(super) fn` on `BuiltinLyricsClient` in a file
|
||||
under `rust_backend/crates/providers/src/lyrics/builtin/` (`apple.rs`,
|
||||
`direct.rs`, `proxy.rs`, `ttml.rs`), taking `&SearchRequest` and
|
||||
`&Check<'_>` and returning `Result<LyricsResponse, LyricsError>`.
|
||||
- Add its match arm to `BuiltinLyricsClient::fetch_once`
|
||||
(`rust_backend/crates/providers/src/lyrics/builtin.rs`).
|
||||
- Add the ID to `BUILTIN_PROVIDERS` and `available_providers` in
|
||||
`rust_backend/crates/core/src/lyrics/config.rs`; `normalize_provider_order`
|
||||
drops unknown IDs. `DEFAULT_PROVIDERS` mirrors the Dart default
|
||||
`AppSettings.lyricsProviders`; changing either for existing users needs a
|
||||
settings migration (see Migration 13 in `_runMigrations`).
|
||||
- Dart UI: `_builtInProviderIds` and `_getLyricsProviderInfo` in
|
||||
`lib/screens/settings/lyrics_provider_priority_page.dart`,
|
||||
`_providerDisplayNames` in `lib/screens/settings/lyrics_settings_page.dart`,
|
||||
and a description key like `lyricsProviderLrclibDesc`.
|
||||
- Tests: `rust_backend/crates/providers/src/lyrics/service/tests.rs`, plus the
|
||||
shared usability cases described in [Validation](#validation).
|
||||
|
||||
### An Extension-Facing API
|
||||
|
||||
- The manifest contract is `rust_backend/crates/extensions/src/manifest.rs`.
|
||||
Host functions are registered by `register` in
|
||||
`rust_backend/crates/extensions/src/host.rs` and the `*_host.rs` modules; the
|
||||
JavaScript side is `rust_backend/crates/extensions/src/prelude.js`.
|
||||
- Surface results as generic fields, such as `has_download_provider` from
|
||||
`rust_backend/crates/extensions/src/manager/info.rs` read by
|
||||
`Extension.fromJson` (`lib/models/extension.dart`), never as extension-name
|
||||
checks (see [Project Boundaries](#project-boundaries)).
|
||||
- Treat existing host functions and manifest fields as a contract with
|
||||
installed extensions; add new ones rather than changing them.
|
||||
- Tests: `rust_backend/crates/extensions/src/runtime/tests.rs`,
|
||||
`rust_backend/crates/extensions/src/manager/tests.rs`,
|
||||
`rust_backend/crates/extensions/tests/`, and the `test/extension_*_test.dart`
|
||||
files.
|
||||
- Keep tests provider-neutral: generic extension IDs, display names,
|
||||
namespaces, and example URLs; no copied extension source and no dependency
|
||||
on the separate extension repository. Platform names stay only where the
|
||||
fixed API, metadata schema, URL parsing, or legacy compatibility needs them.
|
||||
|
||||
## Testing Each Layer
|
||||
|
||||
Commands are in [Validation](#validation).
|
||||
|
||||
| Layer | Location | Notes |
|
||||
| --- | --- | --- |
|
||||
| Dart unit and widget | `test/` | [test/README.md](test/README.md); fixtures in `test/fixtures/`, helpers in `test/support/` |
|
||||
| Device and performance | `integration_test/` | [integration_test/README.md](integration_test/README.md) |
|
||||
| Kotlin JVM | `android/app/src/test/kotlin/com/zarz/spotiflac/` | Case tables in `android/app/src/test/resources/` are also read by Dart tests |
|
||||
| Rust | `tests.rs` modules beside the code, `rust_backend/crates/extensions/tests/` | For example `rust_backend/crates/extensions/src/runtime/tests.rs` |
|
||||
| iOS | `ios/RunnerTests/` | `RunnerTests.swift` |
|
||||
|
||||
## Conventions
|
||||
|
||||
- Split large Dart classes with `part` files: the library declares
|
||||
`part 'name_area.dart';`, the part starts with `part of 'name.dart';` and
|
||||
holds `extension _Area on Name { ... }` or private helper classes. Parts that
|
||||
use the notifier's `state` start with
|
||||
`// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member`,
|
||||
as most `download_queue_provider_*.dart` parts do.
|
||||
- Public notifier methods stay on the class. More than 120 test classes
|
||||
subclass `DownloadQueueNotifier`, `DownloadHistoryNotifier`,
|
||||
`LocalLibraryNotifier`, `LibraryCollectionsNotifier`, `ExtensionNotifier`,
|
||||
and `SettingsNotifier` to override them; only private members move to parts.
|
||||
- After moving a file, leave a one-line `export` of the new path at the old one
|
||||
while other files import it, as `lib/services/batch_track_actions.dart` does.
|
||||
- Source-text contracts: `test/source_contracts_test.dart` pins literal text in
|
||||
files such as `ios/Runner/AppDelegate.swift`, `NativeDownloadFinalizer.kt`,
|
||||
`NativeHistoryStore.kt`, `DownloadServiceSnapshot.kt`, and
|
||||
`lib/services/history_schema.dart`; `test/design_system_test.dart` pins file
|
||||
names; `MainActivityRoutingTest.kt` reads `MainActivity.kt`. When code moves,
|
||||
update the path or assertion in the same commit and never delete the check.
|
||||
- Keep hand-written files under about 800 lines, split by responsibility.
|
||||
Exceptions: generated or frozen tables such as
|
||||
`rust_backend/crates/core/src/metadata/musicbrainz/case_data.rs`, the
|
||||
settings search catalog `lib/utils/settings_search_catalog.dart`, upstream
|
||||
code in `third_party/` and `rust_backend/vendor/`, ARB files, and generated
|
||||
`*.g.dart` and localization files.
|
||||
- Never write the em dash character (U+2014) in code, comments, strings, docs,
|
||||
or commit messages; use a comma, colon, parentheses, or a new sentence.
|
||||
- Commit messages follow [Code and Commit Style](#code-and-commit-style).
|
||||
|
||||
## Generated Files
|
||||
|
||||
- After changing ARB files, run `fvm flutter gen-l10n` (or `fvm flutter pub get`)
|
||||
|
||||
Reference in new issue
Block a user