|
|
@@ -1,397 +1,185 @@
|
|
|
# AGENTS.md
|
|
|
|
|
|
-This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
|
|
-
|
|
|
-## Coding Guidelines
|
|
|
-
|
|
|
-**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
|
|
-
|
|
|
-### Think Before Coding
|
|
|
-
|
|
|
-- State assumptions explicitly. If uncertain, ask.
|
|
|
-- If multiple interpretations exist, present them — don't pick silently.
|
|
|
-- If a simpler approach exists, say so. Push back when warranted.
|
|
|
-- If something is unclear, stop. Name what's confusing. Ask.
|
|
|
-
|
|
|
-### Simplicity First
|
|
|
-
|
|
|
-- No features beyond what was asked.
|
|
|
-- No abstractions for single-use code.
|
|
|
-- No "flexibility" or "configurability" that wasn't requested.
|
|
|
-- No error handling for impossible scenarios.
|
|
|
-- If you write 200 lines and it could be 50, rewrite it.
|
|
|
-
|
|
|
-### Surgical Changes
|
|
|
-
|
|
|
-- Don't "improve" adjacent code, comments, or formatting.
|
|
|
-- Don't refactor things that aren't broken.
|
|
|
-- Match existing style, even if you'd do it differently.
|
|
|
-- If you notice unrelated dead code, mention it — don't delete it.
|
|
|
-- Remove imports/variables/functions that YOUR changes made unused.
|
|
|
-- Don't remove pre-existing dead code unless asked.
|
|
|
-
|
|
|
-### Settings Gateway Conventions
|
|
|
-
|
|
|
-- Ordinary settings gateways mutate state through `update { current -> current.copy(...) }`.
|
|
|
-- Do not introduce `*SettingsUpdate` dispatch types or `updateAll` on settings gateways.
|
|
|
-- Submit related multi-field changes in one `copy(...)` transform so the SSOT can apply them atomically.
|
|
|
-- Keep specialized APIs such as `ReadStyleMutation`, `ThemePackageSettingsGateway.applyAndAwait`,
|
|
|
- `ThemeStateTransaction`, and `AppUiConfigurationGateway` in their dedicated shapes.
|
|
|
-
|
|
|
-### Goal-Driven Execution
|
|
|
-
|
|
|
-Transform tasks into verifiable goals:
|
|
|
-- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
|
|
-- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
|
|
-- "Refactor X" → "Ensure tests pass before and after"
|
|
|
-
|
|
|
-## Build / Test / Run
|
|
|
-
|
|
|
-```bash
|
|
|
-# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)
|
|
|
-.\gradlew.bat :app:compileAppDebugKotlin
|
|
|
-
|
|
|
-# Assemble all variants
|
|
|
-./gradlew assembleAppRelease
|
|
|
-
|
|
|
-# Assemble without R8 (for crash debugging — no minification/shrinking)
|
|
|
-./gradlew assembleAppNoR8
|
|
|
-
|
|
|
-# Debug build
|
|
|
-./gradlew assembleAppDebug
|
|
|
-
|
|
|
-# Run unit tests (JVM, local)
|
|
|
-./gradlew test
|
|
|
-
|
|
|
-# Run a single test class
|
|
|
-./gradlew test --tests "io.legado.app.model.cache.CacheDownloadQueueTest"
|
|
|
-
|
|
|
-# Run connected Android tests
|
|
|
-./gradlew connectedAndroidTest
|
|
|
-
|
|
|
-# Lint
|
|
|
-./gradlew lint
|
|
|
-
|
|
|
-# Update Cronet (after changing CronetVersion in gradle.properties)
|
|
|
-./gradlew app:downloadCronet
|
|
|
-```
|
|
|
-
|
|
|
-The project uses JDK 21 for development (set in `build.gradle.kts` via `jvmToolchain`). CI uses JDK 17 for building.
|
|
|
-
|
|
|
-Gradle properties: 8 GB heap, configuration cache disabled (`gradle.properties:31`), non-transitive R classes, precise resource shrinking enabled.
|
|
|
-
|
|
|
-## Architecture
|
|
|
-
|
|
|
-This is a Material Design 3 fork of [Legado](https://github.com/gedoor/legado). `app/src/main/java/io/legado/app/` uses **Clean Architecture** with three layers:
|
|
|
-
|
|
|
-| Layer | Package | Role |
|
|
|
-|---|---|---|
|
|
|
-| Data | `data/` | Room DB (`AppDatabase`, version 85, ~22 DAOs, ~25 entities), repository implementations |
|
|
|
-| Domain | `domain/` | Gateway interfaces, use cases (14), domain models — no framework dependencies |
|
|
|
-| UI | `ui/` | Jetpack Compose screens, Navigation 3 routes, ViewModels |
|
|
|
-
|
|
|
-Additional top-level packages:
|
|
|
-- **`help/`** — Infrastructure "glue": HTTP (OkHttp + Cronet), book content processing, backup/WebDAV, JS engine, config
|
|
|
-- **`model/`** — Runtime state coordinators (not entities): `ReadBook`, `AudioPlay`, `CacheBook`, `BookCover`, etc.
|
|
|
-- **`service/`** — Android foreground/background services (audio playback, TTS, download, web server)
|
|
|
-- **`web/`** — Embedded HTTP server (Ktor) for remote bookshelf/source editing
|
|
|
-- **`lib/`** — Third-party library wrappers (MOBI parser, WebDAV client, legacy View theme system, cronet)
|
|
|
-- **`base/`** — Abstract Activity/Fragment/ViewModel base classes
|
|
|
-- **`utils/`** — Extension functions and utility classes (~70 files)
|
|
|
-
|
|
|
-Modules: `:app`, `:modules:book` (epub/TXT parsing, namespace `me.ag2s`), `:modules:rhino` (Rhino JS wrapper, namespace `com.script`). There is also a Vue 3 web frontend in `modules/web/` (pnpm, separate from the Android build).
|
|
|
-
|
|
|
-## Dependency Injection (Koin)
|
|
|
-
|
|
|
-Two modules loaded in `App.onCreate()`:
|
|
|
-
|
|
|
-```kotlin
|
|
|
-startKoin {
|
|
|
- modules(appDatabaseModule, appModule)
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-- **`di/appDatabaseModule.kt`** — Singleton `AppDatabase` + factory bindings for all 22 DAOs
|
|
|
-- **`di/appModule.kt`** — Singletons (repositories, use cases, gateways, Coil `ImageLoader`), `viewModelOf` / `viewModel { }` for all ViewModels, some parameterized definitions
|
|
|
-
|
|
|
-Gateways are bound to their repository implementations explicitly (e.g., `single<LocalBookGateway> { LocalBookRepository(get()) }`), not through `singleOf`.
|
|
|
-
|
|
|
-## Navigation
|
|
|
-
|
|
|
-Uses **Jetpack Navigation 3** (`androidx.navigation3`) with type-safe `@Serializable` sealed interfaces for route keys:
|
|
|
-
|
|
|
-```kotlin
|
|
|
-@Serializable
|
|
|
-private sealed interface MainRoute : NavKey
|
|
|
-@Serializable
|
|
|
-private data object MainRouteHome : MainRoute
|
|
|
-@Serializable
|
|
|
-private data class MainRouteCache(val groupId: Long) : MainRoute
|
|
|
-```
|
|
|
-
|
|
|
-`MainActivity` holds a single `NavDisplay` with `entryProvider { ... }` defining all composable entries. `Launcher0` through `LauncherW` extend `MainActivity` to provide multiple launcher icon alias entries. Separate activities handle the reader (`ReadBookActivity` — still View-based), book info, source management, replace rules, file manager, QR scanner, etc.
|
|
|
-
|
|
|
-## Theme System
|
|
|
-
|
|
|
-A multi-engine theming system in `ui/theme/`:
|
|
|
-
|
|
|
-1. **Material 3 Expressive** (default): Uses `MaterialExpressiveTheme` with `MotionScheme.expressive()`
|
|
|
-2. **Miuix** (alternative): Uses `top.yukonga.miuix.kmp` theming engine
|
|
|
-
|
|
|
-14 theme modes (`AppThemeMode` enum) — Dynamic (Monet), 12 named presets, Custom (MaterialKolor seed-color generation), Transparent. `CustomColorScheme` wraps `com.materialkolor` with configurable `PaletteStyle` (TonalSpot, Neutral, Vibrant, Expressive, Rainbow, etc.) and `ColorSpec` (2021 vs 2025).
|
|
|
-
|
|
|
-Legacy View-based theme still exists in `lib/theme/` (used by non-migrated screens like `ReadBookActivity`).
|
|
|
-
|
|
|
-## Hybrid Compose + View
|
|
|
-
|
|
|
-The app is mid-migration from Views to Compose. View-based screens (reader, book info, source management) coexist with Compose screens (main tabs, settings, search, RSS, cache management). XML layouts, `viewBinding`, and traditional Activities are still heavily used. The `viewBinding` build feature is enabled but Compose screens are the target.
|
|
|
-
|
|
|
-## Jetpack Compose Requirements (new screens MUST follow)
|
|
|
-
|
|
|
-All **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not
|
|
|
-** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain
|
|
|
-until migrated.
|
|
|
-
|
|
|
-### MVI/UDF Architecture
|
|
|
-
|
|
|
-Every Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in
|
|
|
-a `*Contract.kt` file:
|
|
|
-
|
|
|
-```
|
|
|
-ui/{feature}/
|
|
|
-├── XxxContract.kt // UiState, Intent, Effect (and optionally Sheet/Dialog)
|
|
|
-├── XxxViewModel.kt // ViewModel
|
|
|
-├── XxxScreen.kt // Screen composable
|
|
|
-└── XxxRouteScreen.kt // (optional) outer wrapper for activity results / lifecycle
|
|
|
-```
|
|
|
-
|
|
|
-**Contract definitions:**
|
|
|
-
|
|
|
-```kotlin
|
|
|
-// @Stable data class — all screen state in one place
|
|
|
-@Stable
|
|
|
-data class XxxUiState(
|
|
|
- val loading: Boolean = false,
|
|
|
- val items: ImmutableList<ItemUi> = persistentListOf(),
|
|
|
- val activeSheet: XxxSheet? = null,
|
|
|
- val activeDialog: XxxDialog? = null,
|
|
|
-)
|
|
|
-
|
|
|
-// sealed interface — every user action is an Intent
|
|
|
-sealed interface XxxIntent {
|
|
|
- data class LoadData(val id: Long) : XxxIntent
|
|
|
- data object Refresh : XxxIntent
|
|
|
-}
|
|
|
-
|
|
|
-// sealed interface — one-shot side effects (navigation, toast, etc.)
|
|
|
-sealed interface XxxEffect {
|
|
|
- data class ShowToast(val message: String) : XxxEffect
|
|
|
- data class NavigateTo(val route: MainRoute) : XxxEffect
|
|
|
-}
|
|
|
-
|
|
|
-// (optional) sealed interface for multi-sheet/dialog scenarios
|
|
|
-sealed interface XxxSheet { data object Filter : XxxSheet }
|
|
|
-sealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }
|
|
|
-```
|
|
|
-
|
|
|
-**Naming rules:**
|
|
|
-
|
|
|
-- State: `{Feature}UiState` — `@Stable data class`
|
|
|
-- Intent: `{Feature}Intent` — `sealed interface` with `data class` / `data object` members
|
|
|
-- Effect: `{Feature}Effect` — `sealed interface`
|
|
|
-- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog` — `sealed interfaces` stored in UiState
|
|
|
-
|
|
|
-### ViewModel
|
|
|
-
|
|
|
-```kotlin
|
|
|
-class XxxViewModel(/* injected dependencies */) : ViewModel() {
|
|
|
-
|
|
|
- private val _uiState = MutableStateFlow(XxxUiState())
|
|
|
- val uiState = _uiState.asStateFlow()
|
|
|
-
|
|
|
- private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)
|
|
|
- val effects = _effects.asSharedFlow()
|
|
|
-
|
|
|
- fun onIntent(intent: XxxIntent) {
|
|
|
- when (intent) {
|
|
|
- is XxxIntent.LoadData -> loadData(intent.id)
|
|
|
- is XxxIntent.Refresh -> refresh()
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private fun loadData(id: Long) {
|
|
|
- // Use viewModelScope, update _uiState via update { it.copy(...) }
|
|
|
- }
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-Key rules:
|
|
|
-
|
|
|
-- Extend `ViewModel()` directly (not `BaseViewModel`).
|
|
|
-- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.
|
|
|
-- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.
|
|
|
-- Emit effects via `_effects.tryEmit(...)`.
|
|
|
-- Single `onIntent()` entry point, dispatched via `when`.
|
|
|
-
|
|
|
-### Screen Composable
|
|
|
-
|
|
|
-```kotlin
|
|
|
-// Stateless screen — ViewModel wired in entry provider or RouteScreen
|
|
|
-@Composable
|
|
|
-fun XxxScreen(
|
|
|
- state: XxxUiState,
|
|
|
- onIntent: (XxxIntent) -> Unit,
|
|
|
- effects: Flow<XxxEffect>, // one-shot effects from ViewModel
|
|
|
- onBack: () -> Unit,
|
|
|
- onNavigateToYyy: (YyyRoute) -> Unit,
|
|
|
-) {
|
|
|
- // Collect effects
|
|
|
- LaunchedEffect(Unit) {
|
|
|
- effects.collectLatest { effect ->
|
|
|
- when (effect) {
|
|
|
- is XxxEffect.ShowToast -> { /* ... */ }
|
|
|
- is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- AppScaffold(
|
|
|
- topBar = {
|
|
|
- GlassMediumFlexibleTopAppBar(
|
|
|
- title = { Text("Title") },
|
|
|
- scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),
|
|
|
- navigationButton = { TopBarNavigationButton(onBack) },
|
|
|
- )
|
|
|
- },
|
|
|
- ) { contentPadding ->
|
|
|
- // UI content, no business logic here
|
|
|
- }
|
|
|
-}
|
|
|
+本文件定义仓库级不变量。专项流程放在 `.agents/skills/`,长期方案放在 `docs/dev/`
|
|
|
+;不要把临时任务、讨论过程或一次性结论继续堆进本文件。
|
|
|
+
|
|
|
+## 资料优先级
|
|
|
+
|
|
|
+执行任务前按以下顺序建立上下文:
|
|
|
+
|
|
|
+1. 用户本次目标、范围与验收条件。
|
|
|
+2. 本文件的仓库级约束。
|
|
|
+3. 与任务匹配的 `.agents/skills/*/SKILL.md` 及其明确要求读取的 reference。
|
|
|
+4. `docs/dev/` 中的专项设计或迁移计划。
|
|
|
+5. 当前源码、测试、Gradle 配置和相邻实现;它们用于确认当前版本的真实 API 与行为。
|
|
|
+6. 外部资料只作为设计依据;涉及会变化的 Android、Kotlin、Compose 或库 API 时,以官方当前文档和本仓库版本为准。
|
|
|
+
|
|
|
+文档与源码不一致时,先指出差异。不要为了让实现符合过期文档而静默改代码。
|
|
|
+
|
|
|
+## Skill 路由
|
|
|
+
|
|
|
+- XML/View/Activity/Fragment/RecyclerView 到 Compose 的页面迁移、新 Compose 页面:使用
|
|
|
+ `legado-compose-migration`。
|
|
|
+- Compose Screen、Contract、ViewModel、导航、状态与兼容边界审查:使用 `legado-compose-review`。
|
|
|
+- Gradle 模块拆分、`commonMain` 抽取、KMP/CMP、`expect/actual`、平台能力适配、迁移门禁或相关脚手架:使用
|
|
|
+ `legado-kmp-migration`。
|
|
|
+- 单个任务可组合 skill,但只读取与当前工作切片有关的 reference。
|
|
|
+
|
|
|
+## 工作方式
|
|
|
+
|
|
|
+- 开始前说明假设、成功条件和不在范围内的内容。存在多种会显著改变结果的解释时,不静默选择。
|
|
|
+- 优先做最小、可回滚、可独立验证的垂直切片;不得借任务重构无关代码。
|
|
|
+- 修 bug 时先建立可复现测试或其他可观察证据;重构前后保持同一行为验证通过。
|
|
|
+- 保留工作区中用户已有修改。只移除本次改动产生的无用代码和资源。
|
|
|
+- 不为了“架构完整”创建空模块、单方法包装、无调用方抽象或通用 `Utils/Common` 容器。
|
|
|
+
|
|
|
+## 当前工程事实与目标方向
|
|
|
+
|
|
|
+当前工程是 Android-first 的渐进迁移仓库,不是已经完成的 KMP 工程:
|
|
|
+
|
|
|
+- Gradle 模块:`:app`、`:modules:book`、`:modules:rhino`、`:baselineprofile`;`modules/web` 是独立 Vue 3
|
|
|
+ 工程。
|
|
|
+- `:app` 同时包含 Android UI、数据、领域、服务和大量遗留 View 代码;包目录只表达逻辑边界,尚无 Gradle
|
|
|
+ 编译隔离。
|
|
|
+- 新 UI 以 Jetpack Compose、Material 3、Navigation 3、Koin、StateFlow/SharedFlow 为默认;阅读器渲染核心等成熟
|
|
|
+ View 代码可作为 Android 专业渲染岛保留。
|
|
|
+- `domain/` 是迁移目标边界,但当前仍存在 Android/JVM 类型渗透;不要把“目标纯净度”描述成已完成事实。
|
|
|
+- 长期目标是渐进演进为 KMP/CMP。详细目标结构、阶段和门禁见 `docs/dev/kmp-cmp-modernization.md`。
|
|
|
+
|
|
|
+### Feature-first 过渡结构
|
|
|
+
|
|
|
+- 新 Compose-first Feature 和完成整页迁移的代码使用 `io.legado.app.feature.<name>`;目录、Gradle
|
|
|
+ path、Kotlin package 分别采用 `feature/book-info`、`:feature:book-info`、`feature.bookinfo`。
|
|
|
+- 在 `:app` 内先形成稳定 Feature 包,再提升为真实 `:feature:<name>` 模块;只有确有多宿主/多模块消费者时才拆
|
|
|
+ `api/impl`,只有确有跨平台价值时才改为 KMP/CMP source sets。
|
|
|
+- Feature 内共置 Contract、ViewModel、Route、Screen 和私有 components/dialog/sheet/model;文件的详细归属与迁移步骤见
|
|
|
+ `docs/dev/feature-first-structure.md`,现有业务 owner 见 `docs/dev/feature-catalog.md`。
|
|
|
+- App host 聚合导航和 DI;Feature 不直接依赖其他 Feature 实现。领域、数据、平台能力不能为了目录整齐塞入
|
|
|
+ Feature UI 包。
|
|
|
+- 旧 `ui/...` 是渐进迁移区,不做全量机械搬家;一个 Feature 的新 owner 建立后,禁止继续向旧包新增同职责文件。
|
|
|
+
|
|
|
+### 目标依赖方向
|
|
|
+
|
|
|
+```text
|
|
|
+app host / composition root
|
|
|
+ ↓
|
|
|
+feature implementation → feature API
|
|
|
+ ↓
|
|
|
+domain / model / platform contracts
|
|
|
+ ↓
|
|
|
+data abstractions
|
|
|
+ ↓
|
|
|
+platform implementations (Android/JVM/iOS/...)
|
|
|
```
|
|
|
|
|
|
-Key rules:
|
|
|
-
|
|
|
-- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel
|
|
|
- directly.
|
|
|
-- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.
|
|
|
-- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen
|
|
|
- doesn't need them directly.
|
|
|
-- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,
|
|
|
- `AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,
|
|
|
- `TopBarActionButton`, etc.
|
|
|
-- No business logic, no direct DB/network calls in composables.
|
|
|
-
|
|
|
-Two input patterns are acceptable:
|
|
|
-
|
|
|
-- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit` —
|
|
|
- ViewModel wired in entry provider or RouteScreen.
|
|
|
-- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for
|
|
|
- standalone screens.
|
|
|
-
|
|
|
-### Stability
|
|
|
-
|
|
|
-- All `UiState` and UI item data classes **must** be annotated with `@Stable`.
|
|
|
-- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,
|
|
|
- not `List` or `MutableList`.
|
|
|
-- Prefer `persistentListOf()` / `toImmutableList()` for default values.
|
|
|
-
|
|
|
-### Navigation
|
|
|
-
|
|
|
-Uses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:
|
|
|
-
|
|
|
-```kotlin
|
|
|
-// In MainNavKey.kt
|
|
|
-@Serializable
|
|
|
-data class MainRouteXxx(val id: Long) : MainRoute
|
|
|
-```
|
|
|
-
|
|
|
-Entry registered in `MainNavGraph.kt`:
|
|
|
-
|
|
|
-```kotlin
|
|
|
-entry<MainRouteXxx> { route ->
|
|
|
- val viewModel = koinViewModel<XxxViewModel>()
|
|
|
- XxxScreen(
|
|
|
- state = viewModel.uiState.collectAsStateWithLifecycle().value,
|
|
|
- onIntent = viewModel::onIntent,
|
|
|
- onBack = { onNavigateBack() },
|
|
|
- onNavigateToYyy = { onNavigateToRoute(it) },
|
|
|
- )
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-Key rules:
|
|
|
-
|
|
|
-- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.
|
|
|
-- Navigation is callback-based, wired by the entry provider.
|
|
|
-- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.
|
|
|
-
|
|
|
-### Koin DI
|
|
|
-
|
|
|
-- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.
|
|
|
-- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).
|
|
|
-- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.
|
|
|
-- Repositories/gateways/use cases registered as `singleOf(::...)`.
|
|
|
-
|
|
|
-### Activity Base Class
|
|
|
-
|
|
|
-New standalone Compose activities extend `BaseComposeActivity`:
|
|
|
-
|
|
|
-```kotlin
|
|
|
-class XxxActivity : BaseComposeActivity() {
|
|
|
- @Composable
|
|
|
- override fun Content() {
|
|
|
- // Screen content — AppTheme is already applied by the base class
|
|
|
- }
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-### RouteScreen Wrapper
|
|
|
-
|
|
|
-For screens needing activity result handling, lifecycle observation, or permission requests, use a
|
|
|
-two-layer pattern:
|
|
|
+- 应用宿主负责组装 Feature 实现、DI、平台生命周期和导航运行时。
|
|
|
+- Feature 之间通过稳定契约或导航 API 协作,不直接依赖其他 Feature 的实现。
|
|
|
+- `commonMain` 只容纳经过依赖审计、能被至少一个非 Android 目标编译验证的代码;移动目录不等于完成跨平台迁移。
|
|
|
+- Android `Context`、Room 实体/DAO、Service、Broadcast、Activity Result、Cronet、文件
|
|
|
+ URI、通知、媒体会话、成熟阅读器渲染和现有 Rhino 集成默认留在平台侧,通过窄接口进入共享层。
|
|
|
+- 平台能力不可用时必须显式建模为 capability/unsupported,不得用静默空实现伪造跨平台支持。
|
|
|
+
|
|
|
+## Compose 屏幕约束
|
|
|
+
|
|
|
+- 新屏幕使用 Compose;不要新增 XML/View Activity/Fragment。
|
|
|
+- Android Compose 新屏幕采用 UDF/MVI:`@Stable XxxUiState`、`XxxIntent`、`XxxEffect`;ViewModel 直接继承
|
|
|
+ `ViewModel`,私有 `MutableStateFlow` / `MutableSharedFlow(extraBufferCapacity = 16)`,对外只暴露只读
|
|
|
+ Flow,并由单一 `onIntent` 分发用户动作。共享 Feature 的状态宿主按 KMP skill 单独决策。
|
|
|
+- Screen/Content 保持无业务逻辑、无 DAO/网络/存储直连;ViewModel 通过 Gateway/Repository/UseCase
|
|
|
+ 访问业务能力。
|
|
|
+- 所有 UiState 与 UI item 数据类标注 `@Stable`。Compose 渲染边界中的集合使用
|
|
|
+ `kotlinx.collections.immutable`;内部计算和数据层不机械替换集合类型。
|
|
|
+- 导航、权限、文件选择和其他宿主动作通过回调或 Effect 处理。新目的地优先由 `MainActivity` 的 Navigation
|
|
|
+ 3 图组装。
|
|
|
+- 使用项目的 `AppScaffold`、主题、top bar、dialog/sheet、列表和设置组件。正确处理 edge-to-edge 与
|
|
|
+ predictive back。
|
|
|
+- Activity 仅在遗留 `Intent`/result 兼容确有需要时作为薄宿主保留。
|
|
|
+
|
|
|
+具体文件形态和审查清单由 Compose migration/review skill 维护,本文件不复制完整模板。
|
|
|
+
|
|
|
+## 数据与设置边界
|
|
|
+
|
|
|
+- UI(包括 ViewModel)不得新增 DAO、`appDb.*Dao`、网络客户端或旧偏好 API 直连;历史债务由
|
|
|
+ `verifyConfigArchitecture` 的基线棘轮冻结并逐步下调。
|
|
|
+- 普通设置 Gateway 通过 `update { current -> current.copy(...) }` 修改状态;关联字段在一次
|
|
|
+ `copy(...)` 中原子提交。
|
|
|
+- 不引入 `*SettingsUpdate` 分发类型或 `updateAll`。
|
|
|
+- `ReadStyleMutation`、`ThemePackageSettingsGateway.applyAndAwait`、`ThemeStateTransaction`、
|
|
|
+ `AppUiConfigurationGateway` 等专用 API 保持现有形态。
|
|
|
+- Koin 中 Gateway 接口到 Repository 实现保持显式绑定;不要用构造函数绑定掩盖接口归属。普通
|
|
|
+ Repository/UseCase/ViewModel 可继续使用项目既有 `singleOf` / `viewModelOf` 约定。
|
|
|
+- 新的共享领域契约不得暴露 `Context`、`File`、`Uri`、Room 类型、Android resource id 或 JVM-only
|
|
|
+ stream;用领域值、`ByteArray`/抽象 source-sink、序列化模型或平台接口表达。
|
|
|
+
|
|
|
+## KMP/CMP 迁移纪律
|
|
|
+
|
|
|
+- 先写行为/契约测试和依赖清单,再移动代码;一个 PR 只完成一个可说明的边界变化。
|
|
|
+- 优先抽取稳定模型、纯函数、规则解析契约和 UseCase;网络、数据库、JS 引擎、服务、阅读器渲染最后通过平台适配器处理。
|
|
|
+- `expect/actual` 只用于真正的平台原语。可用普通接口 + DI 表达的能力,不使用 `expect/actual`。
|
|
|
+- 平台实现由 Koin 和应用 composition root 组装。
|
|
|
+- 不因 KMP 目标同时替换数据库、网络、DI、导航和 UI 框架。每次只改变一个主要风险维度。
|
|
|
+- Feature `api/impl` 只有形成真实 Gradle 边界并存在跨 Feature/宿主调用时才创建,不能只建同名文件夹。
|
|
|
+- 首个非 Android 目标是架构证明,不承诺功能齐平;目标顺序由 capability matrix 和产品需求决定。
|
|
|
+
|
|
|
+## 代码生成
|
|
|
+
|
|
|
+- 生成器用于编码已稳定的约定,不用于发明架构。至少有两个手工迁移且通过审查的同类样本后,才能固化模板。
|
|
|
+- 脚手架必须可预览或 dry-run、拒绝覆盖已有文件、输出最小文件集,并在已有 Feature 上采用追加/AST
|
|
|
+ 感知修改,禁止覆盖 route graph 或 DI 文件。
|
|
|
+- 生成的可提交源码仍接受普通代码审查;真正的编译期生成物放入 `build/generated`,不得手工编辑或提交。
|
|
|
+- 模板和生成器变更必须有 fixture/snapshot 或编译验证,并与 convention plugin 的模块类型保持一致。
|
|
|
+
|
|
|
+## 重构审查门禁
|
|
|
+
|
|
|
+每个迁移 PR 至少回答:
|
|
|
+
|
|
|
+- 行为基线是什么,哪些测试或手工路径证明没有回退?
|
|
|
+- 依赖边界净变化是什么,是否减少了 Android/JVM 泄漏或历史基线?
|
|
|
+- 新模块/接口是否有真实调用方,能否用更小改动完成?
|
|
|
+- Android 实现与共享契约的错误、线程、取消、序列化语义是否一致?
|
|
|
+- 做了哪些验证,哪些真机、性能、外部服务或其他平台行为尚未验证?
|
|
|
+- CI 是否执行了受影响的共享测试、目标编译或 adapter contract test?
|
|
|
+
|
|
|
+减少历史违规时必须同步下调对应 baseline;门禁不接受“先放宽基线再迁移”。详细分级见 KMP/CMP 现代化计划。
|
|
|
+
|
|
|
+## 构建与验证
|
|
|
+
|
|
|
+开发与 CI 使用 JDK 21。按风险选择最小充分验证:
|
|
|
+
|
|
|
+```powershell
|
|
|
+# Kotlin/Compose 快速编译
|
|
|
+.\gradlew.bat :app:compileAppDebugKotlin
|
|
|
|
|
|
-- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,
|
|
|
- permission requests. Wires ViewModel.
|
|
|
-- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.
|
|
|
+# 当前主验证集
|
|
|
+.\gradlew.bat testAppDebugUnitTest lintAppDebug verifyConfigArchitecture assembleAppDebug --continue --no-configuration-cache
|
|
|
|
|
|
-### Material 3 vs Miuix
|
|
|
+# 资源、Manifest、生成绑定或打包变化
|
|
|
+.\gradlew.bat :app:assembleAppDebug
|
|
|
|
|
|
-The project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:
|
|
|
+# Release/R8 相关
|
|
|
+.\gradlew.bat assembleAppRelease
|
|
|
+.\gradlew.bat assembleAppNoR8
|
|
|
|
|
|
-```kotlin
|
|
|
-if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
|
|
|
- // Miuix implementation
|
|
|
-} else {
|
|
|
- // Material 3 implementation
|
|
|
-}
|
|
|
+# Web 前端
|
|
|
+pnpm --dir modules/web build
|
|
|
```
|
|
|
|
|
|
-For detailed Compose review conventions and migration patterns, see
|
|
|
-`.Codex/skills/legado-compose-review/`.
|
|
|
-
|
|
|
-## Rhino JavaScript Engine
|
|
|
-
|
|
|
-Book sources, RSS sources, and HTTP TTS use JavaScript rules. `initRhino()` in `App.kt` registers `NativeBaseSource` wrappers for `BookSource`, `RssSource`, `HttpTTS` (writable JS objects) and `ReadOnlyJavaObject` wrappers for rule entities. Rule parsing logic lives in `help/source/` and `model/analyzeRule/`.
|
|
|
+KMP 任务名在模块实际创建后才存在;不要假装运行尚未定义的 `commonTest`、目标编译或 API
|
|
|
+检查任务。新增模块时把其真实任务加入 CI,并在交付中列出准确命令。
|
|
|
|
|
|
-## Important Constraints
|
|
|
+所有文本改动至少运行 `git diff --check`。构建通过不替代架构边界、行为和真机性能复核。
|
|
|
|
|
|
-- **Do not update jsoup** beyond 1.16.2 — a breaking change in newer versions (see [jsoup#2017](https://github.com/jhy/jsoup/pull/2017)) affects `AnalyzeByJSoup.kt` and the JsoupXpath library
|
|
|
-- Hutool dependency removed — crypto/编码/日期工具已替换为 JCA (`javax.crypto`/`java.security`) 与 `java.time`,新增内部工具在 `help/crypto/CryptoUtils.kt`
|
|
|
-- Package name discrepancy: code namespace is `io.legado.app` but `applicationId` is `io.legato.kazusa`
|
|
|
-- Min SDK 26, target SDK 37, compile SDK 37
|
|
|
-- Release builds enable R8 minification + resource shrinking; `noR8` variant disables both for crash debugging
|
|
|
-- APK is split by ABI (`armeabi-v7a`, `arm64-v8a`, plus universal)
|
|
|
-- Firebase Analytics and Performance are included; `google-services` plugin applied
|
|
|
+## 重要项目约束
|
|
|
|
|
|
-## Web Frontend
|
|
|
+- 不将 jsoup 升级到 1.16.2 以上;新版行为会影响 `AnalyzeByJSoup.kt` 与 JsoupXpath。
|
|
|
+- 不重新引入 Hutool;加密、编码与日期使用现有 JCA、`java.time` 和 `help/crypto/CryptoUtils.kt` 路径,KMP
|
|
|
+ 抽取时再通过能力契约替换 JVM API。
|
|
|
+- 代码 namespace 为 `io.legado.app`,Android `applicationId` 为 `io.legato.kazusa`,不要混用。
|
|
|
+- 当前 minSdk 26、target/compile SDK 37;Release 启用 R8 与资源压缩,`noR8` 变体用于排障。
|
|
|
+- Rhino 书源/RSS/TTS 规则、Android 服务、Web 服务和阅读器渲染属于高行为风险平台能力,迁移前必须建立兼容测试或
|
|
|
+ capability 边界。
|
|
|
+- `modules/web` 必须连接应用内 Ktor WebService;开发环境通过 `VITE_API` 指向设备服务地址。
|
|
|
|
|
|
-Located in `modules/web/` — a Vue 3 + TypeScript + Vite project for remote bookshelf and source editing. Must connect to the app's built-in HTTP server (started via `WebService` in the main activity settings). Commands:
|
|
|
-
|
|
|
-```bash
|
|
|
-cd modules/web
|
|
|
-pnpm install
|
|
|
-pnpm dev # dev server
|
|
|
-pnpm build # production build
|
|
|
-```
|
|
|
+## 交付说明
|
|
|
|
|
|
-Set `VITE_API` in `.env.development` to the app's web service IP.
|
|
|
+最终说明应包含:修改的职责范围、关键设计取舍、实际运行的验证与结果、未验证风险。不要把“已生成代码”或“已移动文件”当作完成标准。
|