{"owner":"HapeLee","repo":"legado-with-MD3","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","CLAUDE.md",".github/copilot-instructions.md"],"skills":{"AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to Codex (Codex.ai/code) when working with code in this repository.\n\n## Coding Guidelines\n\n**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.\n\n### Think Before Coding\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n### Simplicity First\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n\n### Surgical Changes\n\n- Don't \"improve\" adjacent code, comments, or formatting.\n- Don't refactor things that aren't broken.\n- Match existing style, even if you'd do it differently.\n- If you notice unrelated dead code, mention it — don't delete it.\n- Remove imports/variables/functions that YOUR changes made unused.\n- Don't remove pre-existing dead code unless asked.\n\n### Settings Gateway Conventions\n\n- Ordinary settings gateways mutate state through `update { current -> current.copy(...) }`.\n- Do not introduce `*SettingsUpdate` dispatch types or `updateAll` on settings gateways.\n- Submit related multi-field changes in one `copy(...)` transform so the SSOT can apply them atomically.\n- Keep specialized APIs such as `ReadStyleMutation`, `ThemePackageSettingsGateway.applyAndAwait`,\n  `ThemeStateTransaction`, and `AppUiConfigurationGateway` in their dedicated shapes.\n\n### Goal-Driven Execution\n\nTransform tasks into verifiable goals:\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\n## Build / Test / Run\n\n```bash\n# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# Assemble all variants\n./gradlew assembleAppRelease\n\n# Assemble without R8 (for crash debugging — no minification/shrinking)\n./gradlew assembleAppNoR8\n\n# Debug build\n./gradlew assembleAppDebug\n\n# Run unit tests (JVM, local)\n./gradlew test\n\n# Run a single test class\n./gradlew test --tests \"io.legado.app.model.cache.CacheDownloadQueueTest\"\n\n# Run connected Android tests\n./gradlew connectedAndroidTest\n\n# Lint\n./gradlew lint\n\n# Update Cronet (after changing CronetVersion in gradle.properties)\n./gradlew app:downloadCronet\n```\n\nThe project uses JDK 21 for development (set in `build.gradle.kts` via `jvmToolchain`). CI uses JDK 17 for building.\n\nGradle properties: 8 GB heap, configuration cache disabled (`gradle.properties:31`), non-transitive R classes, precise resource shrinking enabled.\n\n## Architecture\n\nThis 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:\n\n| Layer | Package | Role |\n|---|---|---|\n| Data | `data/` | Room DB (`AppDatabase`, version 85, ~22 DAOs, ~25 entities), repository implementations |\n| Domain | `domain/` | Gateway interfaces, use cases (14), domain models — no framework dependencies |\n| UI | `ui/` | Jetpack Compose screens, Navigation 3 routes, ViewModels |\n\nAdditional top-level packages:\n- **`help/`** — Infrastructure \"glue\": HTTP (OkHttp + Cronet), book content processing, backup/WebDAV, JS engine, config\n- **`model/`** — Runtime state coordinators (not entities): `ReadBook`, `AudioPlay`, `CacheBook`, `BookCover`, etc.\n- **`service/`** — Android foreground/background services (audio playback, TTS, download, web server)\n- **`web/`** — Embedded HTTP server (Ktor) for remote bookshelf/source editing\n- **`lib/`** — Third-party library wrappers (MOBI parser, WebDAV client, legacy View theme system, cronet)\n- **`base/`** — Abstract Activity/Fragment/ViewModel base classes\n- **`utils/`** — Extension functions and utility classes (~70 files)\n\nModules: `: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).\n\n## Dependency Injection (Koin)\n\nTwo modules loaded in `App.onCreate()`:\n\n```kotlin\nstartKoin {\n    modules(appDatabaseModule, appModule)\n}\n```\n\n- **`di/appDatabaseModule.kt`** — Singleton `AppDatabase` + factory bindings for all 22 DAOs\n- **`di/appModule.kt`** — Singletons (repositories, use cases, gateways, Coil `ImageLoader`), `viewModelOf` / `viewModel { }` for all ViewModels, some parameterized definitions\n\nGateways are bound to their repository implementations explicitly (e.g., `single<LocalBookGateway> { LocalBookRepository(get()) }`), not through `singleOf`.\n\n## Navigation\n\nUses **Jetpack Navigation 3** (`androidx.navigation3`) with type-safe `@Serializable` sealed interfaces for route keys:\n\n```kotlin\n@Serializable\nprivate sealed interface MainRoute : NavKey\n@Serializable\nprivate data object MainRouteHome : MainRoute\n@Serializable\nprivate data class MainRouteCache(val groupId: Long) : MainRoute\n```\n\n`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.\n\n## Theme System\n\nA multi-engine theming system in `ui/theme/`:\n\n1. **Material 3 Expressive** (default): Uses `MaterialExpressiveTheme` with `MotionScheme.expressive()`\n2. **Miuix** (alternative): Uses `top.yukonga.miuix.kmp` theming engine\n\n14 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).\n\nLegacy View-based theme still exists in `lib/theme/` (used by non-migrated screens like `ReadBookActivity`).\n\n## Hybrid Compose + View\n\nThe 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.\n\n## Jetpack Compose Requirements (new screens MUST follow)\n\nAll **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not\n** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain\nuntil migrated.\n\n### MVI/UDF Architecture\n\nEvery Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in\na `*Contract.kt` file:\n\n```\nui/{feature}/\n├── XxxContract.kt      // UiState, Intent, Effect (and optionally Sheet/Dialog)\n├── XxxViewModel.kt     // ViewModel\n├── XxxScreen.kt        // Screen composable\n└── XxxRouteScreen.kt   // (optional) outer wrapper for activity results / lifecycle\n```\n\n**Contract definitions:**\n\n```kotlin\n// @Stable data class — all screen state in one place\n@Stable\ndata class XxxUiState(\n    val loading: Boolean = false,\n    val items: ImmutableList<ItemUi> = persistentListOf(),\n    val activeSheet: XxxSheet? = null,\n    val activeDialog: XxxDialog? = null,\n)\n\n// sealed interface — every user action is an Intent\nsealed interface XxxIntent {\n    data class LoadData(val id: Long) : XxxIntent\n    data object Refresh : XxxIntent\n}\n\n// sealed interface — one-shot side effects (navigation, toast, etc.)\nsealed interface XxxEffect {\n    data class ShowToast(val message: String) : XxxEffect\n    data class NavigateTo(val route: MainRoute) : XxxEffect\n}\n\n// (optional) sealed interface for multi-sheet/dialog scenarios\nsealed interface XxxSheet { data object Filter : XxxSheet }\nsealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }\n```\n\n**Naming rules:**\n\n- State: `{Feature}UiState` — `@Stable data class`\n- Intent: `{Feature}Intent` — `sealed interface` with `data class` / `data object` members\n- Effect: `{Feature}Effect` — `sealed interface`\n- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog` — `sealed interfaces` stored in UiState\n\n### ViewModel\n\n```kotlin\nclass XxxViewModel(/* injected dependencies */) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(XxxUiState())\n    val uiState = _uiState.asStateFlow()\n\n    private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)\n    val effects = _effects.asSharedFlow()\n\n    fun onIntent(intent: XxxIntent) {\n        when (intent) {\n            is XxxIntent.LoadData -> loadData(intent.id)\n            is XxxIntent.Refresh -> refresh()\n        }\n    }\n\n    private fun loadData(id: Long) {\n        // Use viewModelScope, update _uiState via update { it.copy(...) }\n    }\n}\n```\n\nKey rules:\n\n- Extend `ViewModel()` directly (not `BaseViewModel`).\n- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.\n- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.\n- Emit effects via `_effects.tryEmit(...)`.\n- Single `onIntent()` entry point, dispatched via `when`.\n\n### Screen Composable\n\n```kotlin\n// Stateless screen — ViewModel wired in entry provider or RouteScreen\n@Composable\nfun XxxScreen(\n    state: XxxUiState,\n    onIntent: (XxxIntent) -> Unit,\n    effects: Flow<XxxEffect>,                   // one-shot effects from ViewModel\n    onBack: () -> Unit,\n    onNavigateToYyy: (YyyRoute) -> Unit,\n) {\n    // Collect effects\n    LaunchedEffect(Unit) {\n        effects.collectLatest { effect ->\n            when (effect) {\n                is XxxEffect.ShowToast -> { /* ... */ }\n                is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)\n            }\n        }\n    }\n\n    AppScaffold(\n        topBar = {\n            GlassMediumFlexibleTopAppBar(\n                title = { Text(\"Title\") },\n                scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),\n                navigationButton = { TopBarNavigationButton(onBack) },\n            )\n        },\n    ) { contentPadding ->\n        // UI content, no business logic here\n    }\n}\n```\n\nKey rules:\n\n- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel\n  directly.\n- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.\n- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen\n  doesn't need them directly.\n- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,\n  `AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,\n  `TopBarActionButton`, etc.\n- No business logic, no direct DB/network calls in composables.\n\nTwo input patterns are acceptable:\n\n- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit` —\n  ViewModel wired in entry provider or RouteScreen.\n- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for\n  standalone screens.\n\n### Stability\n\n- All `UiState` and UI item data classes **must** be annotated with `@Stable`.\n- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,\n  not `List` or `MutableList`.\n- Prefer `persistentListOf()` / `toImmutableList()` for default values.\n\n### Navigation\n\nUses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:\n\n```kotlin\n// In MainNavKey.kt\n@Serializable\ndata class MainRouteXxx(val id: Long) : MainRoute\n```\n\nEntry registered in `MainNavGraph.kt`:\n\n```kotlin\nentry<MainRouteXxx> { route ->\n    val viewModel = koinViewModel<XxxViewModel>()\n    XxxScreen(\n        state = viewModel.uiState.collectAsStateWithLifecycle().value,\n        onIntent = viewModel::onIntent,\n        onBack = { onNavigateBack() },\n        onNavigateToYyy = { onNavigateToRoute(it) },\n    )\n}\n```\n\nKey rules:\n\n- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.\n- Navigation is callback-based, wired by the entry provider.\n- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.\n\n### Koin DI\n\n- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.\n- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).\n- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.\n- Repositories/gateways/use cases registered as `singleOf(::...)`.\n\n### Activity Base Class\n\nNew standalone Compose activities extend `BaseComposeActivity`:\n\n```kotlin\nclass XxxActivity : BaseComposeActivity() {\n    @Composable\n    override fun Content() {\n        // Screen content — AppTheme is already applied by the base class\n    }\n}\n```\n\n### RouteScreen Wrapper\n\nFor screens needing activity result handling, lifecycle observation, or permission requests, use a\ntwo-layer pattern:\n\n- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,\n  permission requests. Wires ViewModel.\n- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.\n\n### Material 3 vs Miuix\n\nThe project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:\n\n```kotlin\nif (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {\n    // Miuix implementation\n} else {\n    // Material 3 implementation\n}\n```\n\nFor detailed Compose review conventions and migration patterns, see\n`.Codex/skills/legado-compose-review/`.\n\n## Rhino JavaScript Engine\n\nBook 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/`.\n\n## Important Constraints\n\n- **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\n- **Do not update hutool** beyond 5.8.22 — pinned in `libs.versions.toml:42`\n- Package name discrepancy: code namespace is `io.legado.app` but `applicationId` is `io.legato.kazusa`\n- Min SDK 26, target SDK 37, compile SDK 37\n- Release builds enable R8 minification + resource shrinking; `noR8` variant disables both for crash debugging\n- APK is split by ABI (`armeabi-v7a`, `arm64-v8a`, plus universal)\n- Firebase Analytics and Performance are included; `google-services` plugin applied\n\n## Web Frontend\n\nLocated 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:\n\n```bash\ncd modules/web\npnpm install\npnpm dev       # dev server\npnpm build     # production build\n```\n\nSet `VITE_API` in `.env.development` to the app's web service IP.\n","CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Coding Guidelines\n\n**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.\n\n### Think Before Coding\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n### Simplicity First\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n\n### Surgical Changes\n\n- Don't \"improve\" adjacent code, comments, or formatting.\n- Don't refactor things that aren't broken.\n- Match existing style, even if you'd do it differently.\n- If you notice unrelated dead code, mention it — don't delete it.\n- Remove imports/variables/functions that YOUR changes made unused.\n- Don't remove pre-existing dead code unless asked.\n\n### Goal-Driven Execution\n\nTransform tasks into verifiable goals:\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\n## Build / Test / Run\n\n```bash\n# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# Assemble all variants\n./gradlew assembleAppRelease\n\n# Assemble without R8 (for crash debugging — no minification/shrinking)\n./gradlew assembleAppNoR8\n\n# Debug build\n./gradlew assembleAppDebug\n\n# Run unit tests (JVM, local)\n./gradlew test\n\n# Run a single test class\n./gradlew test --tests \"io.legado.app.model.cache.CacheDownloadQueueTest\"\n\n# Run connected Android tests\n./gradlew connectedAndroidTest\n\n# Lint\n./gradlew lint\n\n# Update Cronet (after changing CronetVersion in gradle.properties)\n./gradlew app:downloadCronet\n```\n\nThe project uses JDK 21 for development (set in `build.gradle.kts` via `jvmToolchain`). CI uses JDK 17 for building.\n\nGradle properties: 8 GB heap, configuration cache disabled (`gradle.properties:31`), non-transitive R classes, precise resource shrinking enabled.\n\n## Architecture\n\nThis 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:\n\n| Layer | Package | Role |\n|---|---|---|\n| Data | `data/` | Room DB (`AppDatabase`, version 85, ~22 DAOs, ~25 entities), repository implementations |\n| Domain | `domain/` | Gateway interfaces, use cases (14), domain models — no framework dependencies |\n| UI | `ui/` | Jetpack Compose screens, Navigation 3 routes, ViewModels |\n\nAdditional top-level packages:\n- **`help/`** — Infrastructure \"glue\": HTTP (OkHttp + Cronet), book content processing, backup/WebDAV, JS engine, config\n- **`model/`** — Runtime state coordinators (not entities): `ReadBook`, `AudioPlay`, `CacheBook`, `BookCover`, etc.\n- **`service/`** — Android foreground/background services (audio playback, TTS, download, web server)\n- **`web/`** — Embedded HTTP server (Ktor) for remote bookshelf/source editing\n- **`lib/`** — Third-party library wrappers (MOBI parser, WebDAV client, legacy View theme system, cronet)\n- **`base/`** — Abstract Activity/Fragment/ViewModel base classes\n- **`utils/`** — Extension functions and utility classes (~70 files)\n\nModules: `: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).\n\n## Dependency Injection (Koin)\n\nTwo modules loaded in `App.onCreate()`:\n\n```kotlin\nstartKoin {\n    modules(appDatabaseModule, appModule)\n}\n```\n\n- **`di/appDatabaseModule.kt`** — Singleton `AppDatabase` + factory bindings for all 22 DAOs\n- **`di/appModule.kt`** — Singletons (repositories, use cases, gateways, Coil `ImageLoader`), `viewModelOf` / `viewModel { }` for all ViewModels, some parameterized definitions\n\nGateways are bound to their repository implementations explicitly (e.g., `single<LocalBookGateway> { LocalBookRepository(get()) }`), not through `singleOf`.\n\n## Navigation\n\nUses **Jetpack Navigation 3** (`androidx.navigation3`) with type-safe `@Serializable` sealed interfaces for route keys:\n\n```kotlin\n@Serializable\nprivate sealed interface MainRoute : NavKey\n@Serializable\nprivate data object MainRouteHome : MainRoute\n@Serializable\nprivate data class MainRouteCache(val groupId: Long) : MainRoute\n```\n\n`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.\n\n## Theme System\n\nA multi-engine theming system in `ui/theme/`:\n\n1. **Material 3 Expressive** (default): Uses `MaterialExpressiveTheme` with `MotionScheme.expressive()`\n2. **Miuix** (alternative): Uses `top.yukonga.miuix.kmp` theming engine\n\n14 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).\n\nLegacy View-based theme still exists in `lib/theme/` (used by non-migrated screens like `ReadBookActivity`).\n\n## Hybrid Compose + View\n\nThe 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.\n\n## Jetpack Compose Requirements (new screens MUST follow)\n\nAll **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not\n** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain\nuntil migrated.\n\n### MVI/UDF Architecture\n\nEvery Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in\na `*Contract.kt` file:\n\n```\nui/{feature}/\n├── XxxContract.kt      // UiState, Intent, Effect (and optionally Sheet/Dialog)\n├── XxxViewModel.kt     // ViewModel\n├── XxxScreen.kt        // Screen composable\n└── XxxRouteScreen.kt   // (optional) outer wrapper for activity results / lifecycle\n```\n\n**Contract definitions:**\n\n```kotlin\n// @Stable data class — all screen state in one place\n@Stable\ndata class XxxUiState(\n    val loading: Boolean = false,\n    val items: ImmutableList<ItemUi> = persistentListOf(),\n    val activeSheet: XxxSheet? = null,\n    val activeDialog: XxxDialog? = null,\n)\n\n// sealed interface — every user action is an Intent\nsealed interface XxxIntent {\n    data class LoadData(val id: Long) : XxxIntent\n    data object Refresh : XxxIntent\n}\n\n// sealed interface — one-shot side effects (navigation, toast, etc.)\nsealed interface XxxEffect {\n    data class ShowToast(val message: String) : XxxEffect\n    data class NavigateTo(val route: MainRoute) : XxxEffect\n}\n\n// (optional) sealed interface for multi-sheet/dialog scenarios\nsealed interface XxxSheet { data object Filter : XxxSheet }\nsealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }\n```\n\n**Naming rules:**\n\n- State: `{Feature}UiState` — `@Stable data class`\n- Intent: `{Feature}Intent` — `sealed interface` with `data class` / `data object` members\n- Effect: `{Feature}Effect` — `sealed interface`\n- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog` — `sealed interfaces` stored in UiState\n\n### ViewModel\n\n```kotlin\nclass XxxViewModel(/* injected dependencies */) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(XxxUiState())\n    val uiState = _uiState.asStateFlow()\n\n    private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)\n    val effects = _effects.asSharedFlow()\n\n    fun onIntent(intent: XxxIntent) {\n        when (intent) {\n            is XxxIntent.LoadData -> loadData(intent.id)\n            is XxxIntent.Refresh -> refresh()\n        }\n    }\n\n    private fun loadData(id: Long) {\n        // Use viewModelScope, update _uiState via update { it.copy(...) }\n    }\n}\n```\n\nKey rules:\n\n- Extend `ViewModel()` directly (not `BaseViewModel`).\n- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.\n- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.\n- Emit effects via `_effects.tryEmit(...)`.\n- Single `onIntent()` entry point, dispatched via `when`.\n\n### Screen Composable\n\n```kotlin\n// Stateless screen — ViewModel wired in entry provider or RouteScreen\n@Composable\nfun XxxScreen(\n    state: XxxUiState,\n    onIntent: (XxxIntent) -> Unit,\n    effects: Flow<XxxEffect>,                   // one-shot effects from ViewModel\n    onBack: () -> Unit,\n    onNavigateToYyy: (YyyRoute) -> Unit,\n) {\n    // Collect effects\n    LaunchedEffect(Unit) {\n        effects.collectLatest { effect ->\n            when (effect) {\n                is XxxEffect.ShowToast -> { /* ... */ }\n                is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)\n            }\n        }\n    }\n\n    AppScaffold(\n        topBar = {\n            GlassMediumFlexibleTopAppBar(\n                title = { Text(\"Title\") },\n                scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),\n                navigationButton = { TopBarNavigationButton(onBack) },\n            )\n        },\n    ) { contentPadding ->\n        // UI content, no business logic here\n    }\n}\n```\n\nKey rules:\n\n- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel\n  directly.\n- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.\n- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen\n  doesn't need them directly.\n- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,\n  `AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,\n  `TopBarActionButton`, etc.\n- No business logic, no direct DB/network calls in composables.\n\nTwo input patterns are acceptable:\n\n- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit` —\n  ViewModel wired in entry provider or RouteScreen.\n- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for\n  standalone screens.\n\n### Stability\n\n- All `UiState` and UI item data classes **must** be annotated with `@Stable`.\n- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,\n  not `List` or `MutableList`.\n- Prefer `persistentListOf()` / `toImmutableList()` for default values.\n\n### Navigation\n\nUses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:\n\n```kotlin\n// In MainNavKey.kt\n@Serializable\ndata class MainRouteXxx(val id: Long) : MainRoute\n```\n\nEntry registered in `MainNavGraph.kt`:\n\n```kotlin\nentry<MainRouteXxx> { route ->\n    val viewModel = koinViewModel<XxxViewModel>()\n    XxxScreen(\n        state = viewModel.uiState.collectAsStateWithLifecycle().value,\n        onIntent = viewModel::onIntent,\n        onBack = { onNavigateBack() },\n        onNavigateToYyy = { onNavigateToRoute(it) },\n    )\n}\n```\n\nKey rules:\n\n- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.\n- Navigation is callback-based, wired by the entry provider.\n- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.\n\n### Koin DI\n\n- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.\n- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).\n- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.\n- Repositories/gateways/use cases registered as `singleOf(::...)`.\n\n### Activity Base Class\n\nNew standalone Compose activities extend `BaseComposeActivity`:\n\n```kotlin\nclass XxxActivity : BaseComposeActivity() {\n    @Composable\n    override fun Content() {\n        // Screen content — AppTheme is already applied by the base class\n    }\n}\n```\n\n### RouteScreen Wrapper\n\nFor screens needing activity result handling, lifecycle observation, or permission requests, use a\ntwo-layer pattern:\n\n- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,\n  permission requests. Wires ViewModel.\n- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.\n\n### Material 3 vs Miuix\n\nThe project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:\n\n```kotlin\nif (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {\n    // Miuix implementation\n} else {\n    // Material 3 implementation\n}\n```\n\nFor detailed Compose review conventions and migration patterns, see\n`.claude/skills/legado-compose-review/`.\n\n## Rhino JavaScript Engine\n\nBook 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/`.\n\n## Important Constraints\n\n- **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\n- **Do not update hutool** beyond 5.8.22 — pinned in `libs.versions.toml:42`\n- Package name discrepancy: code namespace is `io.legado.app` but `applicationId` is `io.legato.kazusa`\n- Min SDK 26, target SDK 37, compile SDK 37\n- Release builds enable R8 minification + resource shrinking; `noR8` variant disables both for crash debugging\n- APK is split by ABI (`armeabi-v7a`, `arm64-v8a`, plus universal)\n- Firebase Analytics and Performance are included; `google-services` plugin applied\n\n## Web Frontend\n\nLocated 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:\n\n```bash\ncd modules/web\npnpm install\npnpm dev       # dev server\npnpm build     # production build\n```\n\nSet `VITE_API` in `.env.development` to the app's web service IP.\n",".github/copilot-instructions.md":"# Copilot Instructions for AI Coding Agents\n\n## 项目架构概览\n- 本项目为 Android 阅读器（Material Design 3 风格重构版），包含主 app（Kotlin/Java）、配套 web 端（Vue3）、多模块扩展。\n- 主要目录：\n  - `app/`：Android 主程序，核心逻辑、服务、UI、数据层。\n  - `modules/web/`：Web 端书架与源编辑，需与 app 后端联动。\n  - `modules/book/`、`modules/rhino/`：功能扩展模块。\n\n## 关键组件与数据流\n- 书籍、书源、订阅源等核心数据结构定义于 `app/src/main/java/io/legato/kazusa/data/`。\n- 解析规则、导入/导出逻辑见 `model/`、`model/localBook/`。\n- 服务（如音频播放、下载、朗读、Web 服务）在 `service/`，通过广播或接口与 UI、数据层交互。\n- UI 组件分布于 `ui/`，按功能细分（如书架、阅读、搜索、订阅等）。\n- Web 端通过 REST API 与 app 通信，需配置 `.env.development` 的 `VITE_API` 指向 app 的 web 服务 IP。\n\n## 构建与开发流程\n- **Android 构建**：\n  - 使用 Gradle，入口为 `build.gradle`、`app/build.gradle`。\n  - 常用命令：`./gradlew assembleRelease`、`./gradlew test`。\n  - ProGuard 混淆规则见 `proguard-rules.pro`、`cronet-proguard-rules.pro`。\n- **Web 端开发**：\n  - 进入 `modules/web/`，使用 `pnpm dev` 启动开发，`pnpm build` 打包。\n  - 调试需保证手机与电脑同网段，手机端开启 web 服务。\n\n## 项目约定与特殊模式\n- 书源、订阅源规则高度自定义，相关解析逻辑集中于 `model/analyzeRule`、`model/rss`。\n- 加密/解密相关辅助函数见 `help/crypto/`，RhinoJs 调用 Java 方法有特殊重载（见 README）。\n- 主题、权限、网络存储等通用功能在 `lib/` 下有独立实现。\n- 书籍文件支持 TXT、EPUB、PDF、UMD，解析入口为 `model/localBook/LocalBook.kt`。\n\n## 外部依赖与集成\n- 主要依赖：JsoupXpath、json-path、rhino-android、okhttp、glide、ktor、bga-qrcode-zxing、colorpicker、commons-text、markwon、hanlp、epublib-core 等。\n- 依赖声明见 `build.gradle`、`app/build.gradle`，部分第三方库源码在 `lib/`。\n\n## 典型开发场景示例\n- 新增书源解析：扩展 `model/analyzeRule`，同步更新 UI 相关界面。\n- 增加服务：在 `service/` 新建服务类，注册广播或接口，UI 层调用。\n- Web 端联动：确保 API 路由与 app web 服务一致，调试时修改 `.env.development`。\n\n## 参考文档与社区\n- 官方帮助文档：https://www.yuque.com/legado/wiki\n- 书源规则教程：https://mgz0227.github.io/The-tutorial-of-Legado/\n- 社区交流：Telegram、Discord、语雀社区\n\n---\n如需补充或有疑问，请反馈具体场景或模块。"},"files":{"AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to Codex (Codex.ai/code) when working with code in this repository.\n\n## Coding Guidelines\n\n**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.\n\n### Think Before Coding\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n### Simplicity First\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n\n### Surgical Changes\n\n- Don't \"improve\" adjacent code, comments, or formatting.\n- Don't refactor things that aren't broken.\n- Match existing style, even if you'd do it differently.\n- If you notice unrelated dead code, mention it — don't delete it.\n- Remove imports/variables/functions that YOUR changes made unused.\n- Don't remove pre-existing dead code unless asked.\n\n### Settings Gateway Conventions\n\n- Ordinary settings gateways mutate state through `update { current -> current.copy(...) }`.\n- Do not introduce `*SettingsUpdate` dispatch types or `updateAll` on settings gateways.\n- Submit related multi-field changes in one `copy(...)` transform so the SSOT can apply them atomically.\n- Keep specialized APIs such as `ReadStyleMutation`, `ThemePackageSettingsGateway.applyAndAwait`,\n  `ThemeStateTransaction`, and `AppUiConfigurationGateway` in their dedicated shapes.\n\n### Goal-Driven Execution\n\nTransform tasks into verifiable goals:\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\n## Build / Test / Run\n\n```bash\n# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# Assemble all variants\n./gradlew assembleAppRelease\n\n# Assemble without R8 (for crash debugging — no minification/shrinking)\n./gradlew assembleAppNoR8\n\n# Debug build\n./gradlew assembleAppDebug\n\n# Run unit tests (JVM, local)\n./gradlew test\n\n# Run a single test class\n./gradlew test --tests \"io.legado.app.model.cache.CacheDownloadQueueTest\"\n\n# Run connected Android tests\n./gradlew connectedAndroidTest\n\n# Lint\n./gradlew lint\n\n# Update Cronet (after changing CronetVersion in gradle.properties)\n./gradlew app:downloadCronet\n```\n\nThe project uses JDK 21 for development (set in `build.gradle.kts` via `jvmToolchain`). CI uses JDK 17 for building.\n\nGradle properties: 8 GB heap, configuration cache disabled (`gradle.properties:31`), non-transitive R classes, precise resource shrinking enabled.\n\n## Architecture\n\nThis 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:\n\n| Layer | Package | Role |\n|---|---|---|\n| Data | `data/` | Room DB (`AppDatabase`, version 85, ~22 DAOs, ~25 entities), repository implementations |\n| Domain | `domain/` | Gateway interfaces, use cases (14), domain models — no framework dependencies |\n| UI | `ui/` | Jetpack Compose screens, Navigation 3 routes, ViewModels |\n\nAdditional top-level packages:\n- **`help/`** — Infrastructure \"glue\": HTTP (OkHttp + Cronet), book content processing, backup/WebDAV, JS engine, config\n- **`model/`** — Runtime state coordinators (not entities): `ReadBook`, `AudioPlay`, `CacheBook`, `BookCover`, etc.\n- **`service/`** — Android foreground/background services (audio playback, TTS, download, web server)\n- **`web/`** — Embedded HTTP server (Ktor) for remote bookshelf/source editing\n- **`lib/`** — Third-party library wrappers (MOBI parser, WebDAV client, legacy View theme system, cronet)\n- **`base/`** — Abstract Activity/Fragment/ViewModel base classes\n- **`utils/`** — Extension functions and utility classes (~70 files)\n\nModules: `: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).\n\n## Dependency Injection (Koin)\n\nTwo modules loaded in `App.onCreate()`:\n\n```kotlin\nstartKoin {\n    modules(appDatabaseModule, appModule)\n}\n```\n\n- **`di/appDatabaseModule.kt`** — Singleton `AppDatabase` + factory bindings for all 22 DAOs\n- **`di/appModule.kt`** — Singletons (repositories, use cases, gateways, Coil `ImageLoader`), `viewModelOf` / `viewModel { }` for all ViewModels, some parameterized definitions\n\nGateways are bound to their repository implementations explicitly (e.g., `single<LocalBookGateway> { LocalBookRepository(get()) }`), not through `singleOf`.\n\n## Navigation\n\nUses **Jetpack Navigation 3** (`androidx.navigation3`) with type-safe `@Serializable` sealed interfaces for route keys:\n\n```kotlin\n@Serializable\nprivate sealed interface MainRoute : NavKey\n@Serializable\nprivate data object MainRouteHome : MainRoute\n@Serializable\nprivate data class MainRouteCache(val groupId: Long) : MainRoute\n```\n\n`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.\n\n## Theme System\n\nA multi-engine theming system in `ui/theme/`:\n\n1. **Material 3 Expressive** (default): Uses `MaterialExpressiveTheme` with `MotionScheme.expressive()`\n2. **Miuix** (alternative): Uses `top.yukonga.miuix.kmp` theming engine\n\n14 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).\n\nLegacy View-based theme still exists in `lib/theme/` (used by non-migrated screens like `ReadBookActivity`).\n\n## Hybrid Compose + View\n\nThe 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.\n\n## Jetpack Compose Requirements (new screens MUST follow)\n\nAll **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not\n** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain\nuntil migrated.\n\n### MVI/UDF Architecture\n\nEvery Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in\na `*Contract.kt` file:\n\n```\nui/{feature}/\n├── XxxContract.kt      // UiState, Intent, Effect (and optionally Sheet/Dialog)\n├── XxxViewModel.kt     // ViewModel\n├── XxxScreen.kt        // Screen composable\n└── XxxRouteScreen.kt   // (optional) outer wrapper for activity results / lifecycle\n```\n\n**Contract definitions:**\n\n```kotlin\n// @Stable data class — all screen state in one place\n@Stable\ndata class XxxUiState(\n    val loading: Boolean = false,\n    val items: ImmutableList<ItemUi> = persistentListOf(),\n    val activeSheet: XxxSheet? = null,\n    val activeDialog: XxxDialog? = null,\n)\n\n// sealed interface — every user action is an Intent\nsealed interface XxxIntent {\n    data class LoadData(val id: Long) : XxxIntent\n    data object Refresh : XxxIntent\n}\n\n// sealed interface — one-shot side effects (navigation, toast, etc.)\nsealed interface XxxEffect {\n    data class ShowToast(val message: String) : XxxEffect\n    data class NavigateTo(val route: MainRoute) : XxxEffect\n}\n\n// (optional) sealed interface for multi-sheet/dialog scenarios\nsealed interface XxxSheet { data object Filter : XxxSheet }\nsealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }\n```\n\n**Naming rules:**\n\n- State: `{Feature}UiState` — `@Stable data class`\n- Intent: `{Feature}Intent` — `sealed interface` with `data class` / `data object` members\n- Effect: `{Feature}Effect` — `sealed interface`\n- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog` — `sealed interfaces` stored in UiState\n\n### ViewModel\n\n```kotlin\nclass XxxViewModel(/* injected dependencies */) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(XxxUiState())\n    val uiState = _uiState.asStateFlow()\n\n    private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)\n    val effects = _effects.asSharedFlow()\n\n    fun onIntent(intent: XxxIntent) {\n        when (intent) {\n            is XxxIntent.LoadData -> loadData(intent.id)\n            is XxxIntent.Refresh -> refresh()\n        }\n    }\n\n    private fun loadData(id: Long) {\n        // Use viewModelScope, update _uiState via update { it.copy(...) }\n    }\n}\n```\n\nKey rules:\n\n- Extend `ViewModel()` directly (not `BaseViewModel`).\n- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.\n- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.\n- Emit effects via `_effects.tryEmit(...)`.\n- Single `onIntent()` entry point, dispatched via `when`.\n\n### Screen Composable\n\n```kotlin\n// Stateless screen — ViewModel wired in entry provider or RouteScreen\n@Composable\nfun XxxScreen(\n    state: XxxUiState,\n    onIntent: (XxxIntent) -> Unit,\n    effects: Flow<XxxEffect>,                   // one-shot effects from ViewModel\n    onBack: () -> Unit,\n    onNavigateToYyy: (YyyRoute) -> Unit,\n) {\n    // Collect effects\n    LaunchedEffect(Unit) {\n        effects.collectLatest { effect ->\n            when (effect) {\n                is XxxEffect.ShowToast -> { /* ... */ }\n                is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)\n            }\n        }\n    }\n\n    AppScaffold(\n        topBar = {\n            GlassMediumFlexibleTopAppBar(\n                title = { Text(\"Title\") },\n                scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),\n                navigationButton = { TopBarNavigationButton(onBack) },\n            )\n        },\n    ) { contentPadding ->\n        // UI content, no business logic here\n    }\n}\n```\n\nKey rules:\n\n- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel\n  directly.\n- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.\n- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen\n  doesn't need them directly.\n- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,\n  `AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,\n  `TopBarActionButton`, etc.\n- No business logic, no direct DB/network calls in composables.\n\nTwo input patterns are acceptable:\n\n- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit` —\n  ViewModel wired in entry provider or RouteScreen.\n- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for\n  standalone screens.\n\n### Stability\n\n- All `UiState` and UI item data classes **must** be annotated with `@Stable`.\n- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,\n  not `List` or `MutableList`.\n- Prefer `persistentListOf()` / `toImmutableList()` for default values.\n\n### Navigation\n\nUses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:\n\n```kotlin\n// In MainNavKey.kt\n@Serializable\ndata class MainRouteXxx(val id: Long) : MainRoute\n```\n\nEntry registered in `MainNavGraph.kt`:\n\n```kotlin\nentry<MainRouteXxx> { route ->\n    val viewModel = koinViewModel<XxxViewModel>()\n    XxxScreen(\n        state = viewModel.uiState.collectAsStateWithLifecycle().value,\n        onIntent = viewModel::onIntent,\n        onBack = { onNavigateBack() },\n        onNavigateToYyy = { onNavigateToRoute(it) },\n    )\n}\n```\n\nKey rules:\n\n- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.\n- Navigation is callback-based, wired by the entry provider.\n- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.\n\n### Koin DI\n\n- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.\n- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).\n- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.\n- Repositories/gateways/use cases registered as `singleOf(::...)`.\n\n### Activity Base Class\n\nNew standalone Compose activities extend `BaseComposeActivity`:\n\n```kotlin\nclass XxxActivity : BaseComposeActivity() {\n    @Composable\n    override fun Content() {\n        // Screen content — AppTheme is already applied by the base class\n    }\n}\n```\n\n### RouteScreen Wrapper\n\nFor screens needing activity result handling, lifecycle observation, or permission requests, use a\ntwo-layer pattern:\n\n- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,\n  permission requests. Wires ViewModel.\n- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.\n\n### Material 3 vs Miuix\n\nThe project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:\n\n```kotlin\nif (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {\n    // Miuix implementation\n} else {\n    // Material 3 implementation\n}\n```\n\nFor detailed Compose review conventions and migration patterns, see\n`.Codex/skills/legado-compose-review/`.\n\n## Rhino JavaScript Engine\n\nBook 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/`.\n\n## Important Constraints\n\n- **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\n- **Do not update hutool** beyond 5.8.22 — pinned in `libs.versions.toml:42`\n- Package name discrepancy: code namespace is `io.legado.app` but `applicationId` is `io.legato.kazusa`\n- Min SDK 26, target SDK 37, compile SDK 37\n- Release builds enable R8 minification + resource shrinking; `noR8` variant disables both for crash debugging\n- APK is split by ABI (`armeabi-v7a`, `arm64-v8a`, plus universal)\n- Firebase Analytics and Performance are included; `google-services` plugin applied\n\n## Web Frontend\n\nLocated 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:\n\n```bash\ncd modules/web\npnpm install\npnpm dev       # dev server\npnpm build     # production build\n```\n\nSet `VITE_API` in `.env.development` to the app's web service IP.\n","CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Coding Guidelines\n\n**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.\n\n### Think Before Coding\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n### Simplicity First\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n\n### Surgical Changes\n\n- Don't \"improve\" adjacent code, comments, or formatting.\n- Don't refactor things that aren't broken.\n- Match existing style, even if you'd do it differently.\n- If you notice unrelated dead code, mention it — don't delete it.\n- Remove imports/variables/functions that YOUR changes made unused.\n- Don't remove pre-existing dead code unless asked.\n\n### Goal-Driven Execution\n\nTransform tasks into verifiable goals:\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\n## Build / Test / Run\n\n```bash\n# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# Assemble all variants\n./gradlew assembleAppRelease\n\n# Assemble without R8 (for crash debugging — no minification/shrinking)\n./gradlew assembleAppNoR8\n\n# Debug build\n./gradlew assembleAppDebug\n\n# Run unit tests (JVM, local)\n./gradlew test\n\n# Run a single test class\n./gradlew test --tests \"io.legado.app.model.cache.CacheDownloadQueueTest\"\n\n# Run connected Android tests\n./gradlew connectedAndroidTest\n\n# Lint\n./gradlew lint\n\n# Update Cronet (after changing CronetVersion in gradle.properties)\n./gradlew app:downloadCronet\n```\n\nThe project uses JDK 21 for development (set in `build.gradle.kts` via `jvmToolchain`). CI uses JDK 17 for building.\n\nGradle properties: 8 GB heap, configuration cache disabled (`gradle.properties:31`), non-transitive R classes, precise resource shrinking enabled.\n\n## Architecture\n\nThis 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:\n\n| Layer | Package | Role |\n|---|---|---|\n| Data | `data/` | Room DB (`AppDatabase`, version 85, ~22 DAOs, ~25 entities), repository implementations |\n| Domain | `domain/` | Gateway interfaces, use cases (14), domain models — no framework dependencies |\n| UI | `ui/` | Jetpack Compose screens, Navigation 3 routes, ViewModels |\n\nAdditional top-level packages:\n- **`help/`** — Infrastructure \"glue\": HTTP (OkHttp + Cronet), book content processing, backup/WebDAV, JS engine, config\n- **`model/`** — Runtime state coordinators (not entities): `ReadBook`, `AudioPlay`, `CacheBook`, `BookCover`, etc.\n- **`service/`** — Android foreground/background services (audio playback, TTS, download, web server)\n- **`web/`** — Embedded HTTP server (Ktor) for remote bookshelf/source editing\n- **`lib/`** — Third-party library wrappers (MOBI parser, WebDAV client, legacy View theme system, cronet)\n- **`base/`** — Abstract Activity/Fragment/ViewModel base classes\n- **`utils/`** — Extension functions and utility classes (~70 files)\n\nModules: `: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).\n\n## Dependency Injection (Koin)\n\nTwo modules loaded in `App.onCreate()`:\n\n```kotlin\nstartKoin {\n    modules(appDatabaseModule, appModule)\n}\n```\n\n- **`di/appDatabaseModule.kt`** — Singleton `AppDatabase` + factory bindings for all 22 DAOs\n- **`di/appModule.kt`** — Singletons (repositories, use cases, gateways, Coil `ImageLoader`), `viewModelOf` / `viewModel { }` for all ViewModels, some parameterized definitions\n\nGateways are bound to their repository implementations explicitly (e.g., `single<LocalBookGateway> { LocalBookRepository(get()) }`), not through `singleOf`.\n\n## Navigation\n\nUses **Jetpack Navigation 3** (`androidx.navigation3`) with type-safe `@Serializable` sealed interfaces for route keys:\n\n```kotlin\n@Serializable\nprivate sealed interface MainRoute : NavKey\n@Serializable\nprivate data object MainRouteHome : MainRoute\n@Serializable\nprivate data class MainRouteCache(val groupId: Long) : MainRoute\n```\n\n`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.\n\n## Theme System\n\nA multi-engine theming system in `ui/theme/`:\n\n1. **Material 3 Expressive** (default): Uses `MaterialExpressiveTheme` with `MotionScheme.expressive()`\n2. **Miuix** (alternative): Uses `top.yukonga.miuix.kmp` theming engine\n\n14 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).\n\nLegacy View-based theme still exists in `lib/theme/` (used by non-migrated screens like `ReadBookActivity`).\n\n## Hybrid Compose + View\n\nThe 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.\n\n## Jetpack Compose Requirements (new screens MUST follow)\n\nAll **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not\n** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain\nuntil migrated.\n\n### MVI/UDF Architecture\n\nEvery Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in\na `*Contract.kt` file:\n\n```\nui/{feature}/\n├── XxxContract.kt      // UiState, Intent, Effect (and optionally Sheet/Dialog)\n├── XxxViewModel.kt     // ViewModel\n├── XxxScreen.kt        // Screen composable\n└── XxxRouteScreen.kt   // (optional) outer wrapper for activity results / lifecycle\n```\n\n**Contract definitions:**\n\n```kotlin\n// @Stable data class — all screen state in one place\n@Stable\ndata class XxxUiState(\n    val loading: Boolean = false,\n    val items: ImmutableList<ItemUi> = persistentListOf(),\n    val activeSheet: XxxSheet? = null,\n    val activeDialog: XxxDialog? = null,\n)\n\n// sealed interface — every user action is an Intent\nsealed interface XxxIntent {\n    data class LoadData(val id: Long) : XxxIntent\n    data object Refresh : XxxIntent\n}\n\n// sealed interface — one-shot side effects (navigation, toast, etc.)\nsealed interface XxxEffect {\n    data class ShowToast(val message: String) : XxxEffect\n    data class NavigateTo(val route: MainRoute) : XxxEffect\n}\n\n// (optional) sealed interface for multi-sheet/dialog scenarios\nsealed interface XxxSheet { data object Filter : XxxSheet }\nsealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }\n```\n\n**Naming rules:**\n\n- State: `{Feature}UiState` — `@Stable data class`\n- Intent: `{Feature}Intent` — `sealed interface` with `data class` / `data object` members\n- Effect: `{Feature}Effect` — `sealed interface`\n- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog` — `sealed interfaces` stored in UiState\n\n### ViewModel\n\n```kotlin\nclass XxxViewModel(/* injected dependencies */) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(XxxUiState())\n    val uiState = _uiState.asStateFlow()\n\n    private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)\n    val effects = _effects.asSharedFlow()\n\n    fun onIntent(intent: XxxIntent) {\n        when (intent) {\n            is XxxIntent.LoadData -> loadData(intent.id)\n            is XxxIntent.Refresh -> refresh()\n        }\n    }\n\n    private fun loadData(id: Long) {\n        // Use viewModelScope, update _uiState via update { it.copy(...) }\n    }\n}\n```\n\nKey rules:\n\n- Extend `ViewModel()` directly (not `BaseViewModel`).\n- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.\n- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.\n- Emit effects via `_effects.tryEmit(...)`.\n- Single `onIntent()` entry point, dispatched via `when`.\n\n### Screen Composable\n\n```kotlin\n// Stateless screen — ViewModel wired in entry provider or RouteScreen\n@Composable\nfun XxxScreen(\n    state: XxxUiState,\n    onIntent: (XxxIntent) -> Unit,\n    effects: Flow<XxxEffect>,                   // one-shot effects from ViewModel\n    onBack: () -> Unit,\n    onNavigateToYyy: (YyyRoute) -> Unit,\n) {\n    // Collect effects\n    LaunchedEffect(Unit) {\n        effects.collectLatest { effect ->\n            when (effect) {\n                is XxxEffect.ShowToast -> { /* ... */ }\n                is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)\n            }\n        }\n    }\n\n    AppScaffold(\n        topBar = {\n            GlassMediumFlexibleTopAppBar(\n                title = { Text(\"Title\") },\n                scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),\n                navigationButton = { TopBarNavigationButton(onBack) },\n            )\n        },\n    ) { contentPadding ->\n        // UI content, no business logic here\n    }\n}\n```\n\nKey rules:\n\n- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel\n  directly.\n- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.\n- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen\n  doesn't need them directly.\n- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,\n  `AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,\n  `TopBarActionButton`, etc.\n- No business logic, no direct DB/network calls in composables.\n\nTwo input patterns are acceptable:\n\n- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit` —\n  ViewModel wired in entry provider or RouteScreen.\n- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for\n  standalone screens.\n\n### Stability\n\n- All `UiState` and UI item data classes **must** be annotated with `@Stable`.\n- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,\n  not `List` or `MutableList`.\n- Prefer `persistentListOf()` / `toImmutableList()` for default values.\n\n### Navigation\n\nUses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:\n\n```kotlin\n// In MainNavKey.kt\n@Serializable\ndata class MainRouteXxx(val id: Long) : MainRoute\n```\n\nEntry registered in `MainNavGraph.kt`:\n\n```kotlin\nentry<MainRouteXxx> { route ->\n    val viewModel = koinViewModel<XxxViewModel>()\n    XxxScreen(\n        state = viewModel.uiState.collectAsStateWithLifecycle().value,\n        onIntent = viewModel::onIntent,\n        onBack = { onNavigateBack() },\n        onNavigateToYyy = { onNavigateToRoute(it) },\n    )\n}\n```\n\nKey rules:\n\n- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.\n- Navigation is callback-based, wired by the entry provider.\n- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.\n\n### Koin DI\n\n- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.\n- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).\n- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.\n- Repositories/gateways/use cases registered as `singleOf(::...)`.\n\n### Activity Base Class\n\nNew standalone Compose activities extend `BaseComposeActivity`:\n\n```kotlin\nclass XxxActivity : BaseComposeActivity() {\n    @Composable\n    override fun Content() {\n        // Screen content — AppTheme is already applied by the base class\n    }\n}\n```\n\n### RouteScreen Wrapper\n\nFor screens needing activity result handling, lifecycle observation, or permission requests, use a\ntwo-layer pattern:\n\n- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,\n  permission requests. Wires ViewModel.\n- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.\n\n### Material 3 vs Miuix\n\nThe project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:\n\n```kotlin\nif (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {\n    // Miuix implementation\n} else {\n    // Material 3 implementation\n}\n```\n\nFor detailed Compose review conventions and migration patterns, see\n`.claude/skills/legado-compose-review/`.\n\n## Rhino JavaScript Engine\n\nBook 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/`.\n\n## Important Constraints\n\n- **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\n- **Do not update hutool** beyond 5.8.22 — pinned in `libs.versions.toml:42`\n- Package name discrepancy: code namespace is `io.legado.app` but `applicationId` is `io.legato.kazusa`\n- Min SDK 26, target SDK 37, compile SDK 37\n- Release builds enable R8 minification + resource shrinking; `noR8` variant disables both for crash debugging\n- APK is split by ABI (`armeabi-v7a`, `arm64-v8a`, plus universal)\n- Firebase Analytics and Performance are included; `google-services` plugin applied\n\n## Web Frontend\n\nLocated 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:\n\n```bash\ncd modules/web\npnpm install\npnpm dev       # dev server\npnpm build     # production build\n```\n\nSet `VITE_API` in `.env.development` to the app's web service IP.\n",".github/copilot-instructions.md":"# Copilot Instructions for AI Coding Agents\n\n## 项目架构概览\n- 本项目为 Android 阅读器（Material Design 3 风格重构版），包含主 app（Kotlin/Java）、配套 web 端（Vue3）、多模块扩展。\n- 主要目录：\n  - `app/`：Android 主程序，核心逻辑、服务、UI、数据层。\n  - `modules/web/`：Web 端书架与源编辑，需与 app 后端联动。\n  - `modules/book/`、`modules/rhino/`：功能扩展模块。\n\n## 关键组件与数据流\n- 书籍、书源、订阅源等核心数据结构定义于 `app/src/main/java/io/legato/kazusa/data/`。\n- 解析规则、导入/导出逻辑见 `model/`、`model/localBook/`。\n- 服务（如音频播放、下载、朗读、Web 服务）在 `service/`，通过广播或接口与 UI、数据层交互。\n- UI 组件分布于 `ui/`，按功能细分（如书架、阅读、搜索、订阅等）。\n- Web 端通过 REST API 与 app 通信，需配置 `.env.development` 的 `VITE_API` 指向 app 的 web 服务 IP。\n\n## 构建与开发流程\n- **Android 构建**：\n  - 使用 Gradle，入口为 `build.gradle`、`app/build.gradle`。\n  - 常用命令：`./gradlew assembleRelease`、`./gradlew test`。\n  - ProGuard 混淆规则见 `proguard-rules.pro`、`cronet-proguard-rules.pro`。\n- **Web 端开发**：\n  - 进入 `modules/web/`，使用 `pnpm dev` 启动开发，`pnpm build` 打包。\n  - 调试需保证手机与电脑同网段，手机端开启 web 服务。\n\n## 项目约定与特殊模式\n- 书源、订阅源规则高度自定义，相关解析逻辑集中于 `model/analyzeRule`、`model/rss`。\n- 加密/解密相关辅助函数见 `help/crypto/`，RhinoJs 调用 Java 方法有特殊重载（见 README）。\n- 主题、权限、网络存储等通用功能在 `lib/` 下有独立实现。\n- 书籍文件支持 TXT、EPUB、PDF、UMD，解析入口为 `model/localBook/LocalBook.kt`。\n\n## 外部依赖与集成\n- 主要依赖：JsoupXpath、json-path、rhino-android、okhttp、glide、ktor、bga-qrcode-zxing、colorpicker、commons-text、markwon、hanlp、epublib-core 等。\n- 依赖声明见 `build.gradle`、`app/build.gradle`，部分第三方库源码在 `lib/`。\n\n## 典型开发场景示例\n- 新增书源解析：扩展 `model/analyzeRule`，同步更新 UI 相关界面。\n- 增加服务：在 `service/` 新建服务类，注册广播或接口，UI 层调用。\n- Web 端联动：确保 API 路由与 app web 服务一致，调试时修改 `.env.development`。\n\n## 参考文档与社区\n- 官方帮助文档：https://www.yuque.com/legado/wiki\n- 书源规则教程：https://mgz0227.github.io/The-tutorial-of-Legado/\n- 社区交流：Telegram、Discord、语雀社区\n\n---\n如需补充或有疑问，请反馈具体场景或模块。"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# AGENTS.md\n\nThis file provides guidance to Codex (Codex.ai/code) when working with code in this repository.\n\n## Coding Guidelines\n\n**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.\n\n### Think Before Coding\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n### Simplicity First\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n\n### Surgical Changes\n\n- Don't \"improve\" adjacent code, comments, or formatting.\n- Don't refactor things that aren't broken.\n- Match existing style, even if you'd do it differently.\n- If you notice unrelated dead code, mention it — don't delete it.\n- Remove imports/variables/functions that YOUR changes made unused.\n- Don't remove pre-existing dead code unless asked.\n\n### Settings Gateway Conventions\n\n- Ordinary settings gateways mutate state through `update { current -> current.copy(...) }`.\n- Do not introduce `*SettingsUpdate` dispatch types or `updateAll` on settings gateways.\n- Submit related multi-field changes in one `copy(...)` transform so the SSOT can apply them atomically.\n- Keep specialized APIs such as `ReadStyleMutation`, `ThemePackageSettingsGateway.applyAndAwait`,\n  `ThemeStateTransaction`, and `AppUiConfigurationGateway` in their dedicated shapes.\n\n### Goal-Driven Execution\n\nTransform tasks into verifiable goals:\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\n## Build / Test / Run\n\n```bash\n# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# Assemble all variants\n./gradlew assembleAppRelease\n\n# Assemble without R8 (for crash debugging — no minification/shrinking)\n./gradlew assembleAppNoR8\n\n# Debug build\n./gradlew assembleAppDebug\n\n# Run unit tests (JVM, local)\n./gradlew test\n\n# Run a single test class\n./gradlew test --tests \"io.legado.app.model.cache.CacheDownloadQueueTest\"\n\n# Run connected Android tests\n./gradlew connectedAndroidTest\n\n# Lint\n./gradlew lint\n\n# Update Cronet (after changing CronetVersion in gradle.properties)\n./gradlew app:downloadCronet\n```\n\nThe project uses JDK 21 for development (set in `build.gradle.kts` via `jvmToolchain`). CI uses JDK 17 for building.\n\nGradle properties: 8 GB heap, configuration cache disabled (`gradle.properties:31`), non-transitive R classes, precise resource shrinking enabled.\n\n## Architecture\n\nThis 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:\n\n| Layer | Package | Role |\n|---|---|---|\n| Data | `data/` | Room DB (`AppDatabase`, version 85, ~22 DAOs, ~25 entities), repository implementations |\n| Domain | `domain/` | Gateway interfaces, use cases (14), domain models — no framework dependencies |\n| UI | `ui/` | Jetpack Compose screens, Navigation 3 routes, ViewModels |\n\nAdditional top-level packages:\n- **`help/`** — Infrastructure \"glue\": HTTP (OkHttp + Cronet), book content processing, backup/WebDAV, JS engine, config\n- **`model/`** — Runtime state coordinators (not entities): `ReadBook`, `AudioPlay`, `CacheBook`, `BookCover`, etc.\n- **`service/`** — Android foreground/background services (audio playback, TTS, download, web server)\n- **`web/`** — Embedded HTTP server (Ktor) for remote bookshelf/source editing\n- **`lib/`** — Third-party library wrappers (MOBI parser, WebDAV client, legacy View theme system, cronet)\n- **`base/`** — Abstract Activity/Fragment/ViewModel base classes\n- **`utils/`** — Extension functions and utility classes (~70 files)\n\nModules: `: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).\n\n## Dependency Injection (Koin)\n\nTwo modules loaded in `App.onCreate()`:\n\n```kotlin\nstartKoin {\n    modules(appDatabaseModule, appModule)\n}\n```\n\n- **`di/appDatabaseModule.kt`** — Singleton `AppDatabase` + factory bindings for all 22 DAOs\n- **`di/appModule.kt`** — Singletons (repositories, use cases, gateways, Coil `ImageLoader`), `viewModelOf` / `viewModel { }` for all ViewModels, some parameterized definitions\n\nGateways are bound to their repository implementations explicitly (e.g., `single<LocalBookGateway> { LocalBookRepository(get()) }`), not through `singleOf`.\n\n## Navigation\n\nUses **Jetpack Navigation 3** (`androidx.navigation3`) with type-safe `@Serializable` sealed interfaces for route keys:\n\n```kotlin\n@Serializable\nprivate sealed interface MainRoute : NavKey\n@Serializable\nprivate data object MainRouteHome : MainRoute\n@Serializable\nprivate data class MainRouteCache(val groupId: Long) : MainRoute\n```\n\n`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.\n\n## Theme System\n\nA multi-engine theming system in `ui/theme/`:\n\n1. **Material 3 Expressive** (default): Uses `MaterialExpressiveTheme` with `MotionScheme.expressive()`\n2. **Miuix** (alternative): Uses `top.yukonga.miuix.kmp` theming engine\n\n14 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).\n\nLegacy View-based theme still exists in `lib/theme/` (used by non-migrated screens like `ReadBookActivity`).\n\n## Hybrid Compose + View\n\nThe 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.\n\n## Jetpack Compose Requirements (new screens MUST follow)\n\nAll **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not\n** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain\nuntil migrated.\n\n### MVI/UDF Architecture\n\nEvery Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in\na `*Contract.kt` file:\n\n```\nui/{feature}/\n├── XxxContract.kt      // UiState, Intent, Effect (and optionally Sheet/Dialog)\n├── XxxViewModel.kt     // ViewModel\n├── XxxScreen.kt        // Screen composable\n└── XxxRouteScreen.kt   // (optional) outer wrapper for activity results / lifecycle\n```\n\n**Contract definitions:**\n\n```kotlin\n// @Stable data class — all screen state in one place\n@Stable\ndata class XxxUiState(\n    val loading: Boolean = false,\n    val items: ImmutableList<ItemUi> = persistentListOf(),\n    val activeSheet: XxxSheet? = null,\n    val activeDialog: XxxDialog? = null,\n)\n\n// sealed interface — every user action is an Intent\nsealed interface XxxIntent {\n    data class LoadData(val id: Long) : XxxIntent\n    data object Refresh : XxxIntent\n}\n\n// sealed interface — one-shot side effects (navigation, toast, etc.)\nsealed interface XxxEffect {\n    data class ShowToast(val message: String) : XxxEffect\n    data class NavigateTo(val route: MainRoute) : XxxEffect\n}\n\n// (optional) sealed interface for multi-sheet/dialog scenarios\nsealed interface XxxSheet { data object Filter : XxxSheet }\nsealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }\n```\n\n**Naming rules:**\n\n- State: `{Feature}UiState` — `@Stable data class`\n- Intent: `{Feature}Intent` — `sealed interface` with `data class` / `data object` members\n- Effect: `{Feature}Effect` — `sealed interface`\n- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog` — `sealed interfaces` stored in UiState\n\n### ViewModel\n\n```kotlin\nclass XxxViewModel(/* injected dependencies */) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(XxxUiState())\n    val uiState = _uiState.asStateFlow()\n\n    private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)\n    val effects = _effects.asSharedFlow()\n\n    fun onIntent(intent: XxxIntent) {\n        when (intent) {\n            is XxxIntent.LoadData -> loadData(intent.id)\n            is XxxIntent.Refresh -> refresh()\n        }\n    }\n\n    private fun loadData(id: Long) {\n        // Use viewModelScope, update _uiState via update { it.copy(...) }\n    }\n}\n```\n\nKey rules:\n\n- Extend `ViewModel()` directly (not `BaseViewModel`).\n- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.\n- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.\n- Emit effects via `_effects.tryEmit(...)`.\n- Single `onIntent()` entry point, dispatched via `when`.\n\n### Screen Composable\n\n```kotlin\n// Stateless screen — ViewModel wired in entry provider or RouteScreen\n@Composable\nfun XxxScreen(\n    state: XxxUiState,\n    onIntent: (XxxIntent) -> Unit,\n    effects: Flow<XxxEffect>,                   // one-shot effects from ViewModel\n    onBack: () -> Unit,\n    onNavigateToYyy: (YyyRoute) -> Unit,\n) {\n    // Collect effects\n    LaunchedEffect(Unit) {\n        effects.collectLatest { effect ->\n            when (effect) {\n                is XxxEffect.ShowToast -> { /* ... */ }\n                is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)\n            }\n        }\n    }\n\n    AppScaffold(\n        topBar = {\n            GlassMediumFlexibleTopAppBar(\n                title = { Text(\"Title\") },\n                scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),\n                navigationButton = { TopBarNavigationButton(onBack) },\n            )\n        },\n    ) { contentPadding ->\n        // UI content, no business logic here\n    }\n}\n```\n\nKey rules:\n\n- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel\n  directly.\n- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.\n- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen\n  doesn't need them directly.\n- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,\n  `AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,\n  `TopBarActionButton`, etc.\n- No business logic, no direct DB/network calls in composables.\n\nTwo input patterns are acceptable:\n\n- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit` —\n  ViewModel wired in entry provider or RouteScreen.\n- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for\n  standalone screens.\n\n### Stability\n\n- All `UiState` and UI item data classes **must** be annotated with `@Stable`.\n- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,\n  not `List` or `MutableList`.\n- Prefer `persistentListOf()` / `toImmutableList()` for default values.\n\n### Navigation\n\nUses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:\n\n```kotlin\n// In MainNavKey.kt\n@Serializable\ndata class MainRouteXxx(val id: Long) : MainRoute\n```\n\nEntry registered in `MainNavGraph.kt`:\n\n```kotlin\nentry<MainRouteXxx> { route ->\n    val viewModel = koinViewModel<XxxViewModel>()\n    XxxScreen(\n        state = viewModel.uiState.collectAsStateWithLifecycle().value,\n        onIntent = viewModel::onIntent,\n        onBack = { onNavigateBack() },\n        onNavigateToYyy = { onNavigateToRoute(it) },\n    )\n}\n```\n\nKey rules:\n\n- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.\n- Navigation is callback-based, wired by the entry provider.\n- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.\n\n### Koin DI\n\n- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.\n- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).\n- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.\n- Repositories/gateways/use cases registered as `singleOf(::...)`.\n\n### Activity Base Class\n\nNew standalone Compose activities extend `BaseComposeActivity`:\n\n```kotlin\nclass XxxActivity : BaseComposeActivity() {\n    @Composable\n    override fun Content() {\n        // Screen content — AppTheme is already applied by the base class\n    }\n}\n```\n\n### RouteScreen Wrapper\n\nFor screens needing activity result handling, lifecycle observation, or permission requests, use a\ntwo-layer pattern:\n\n- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,\n  permission requests. Wires ViewModel.\n- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.\n\n### Material 3 vs Miuix\n\nThe project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:\n\n```kotlin\nif (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {\n    // Miuix implementation\n} else {\n    // Material 3 implementation\n}\n```\n\nFor detailed Compose review conventions and migration patterns, see\n`.Codex/skills/legado-compose-review/`.\n\n## Rhino JavaScript Engine\n\nBook 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/`.\n\n## Important Constraints\n\n- **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\n- **Do not update hutool** beyond 5.8.22 — pinned in `libs.versions.toml:42`\n- Package name discrepancy: code namespace is `io.legado.app` but `applicationId` is `io.legato.kazusa`\n- Min SDK 26, target SDK 37, compile SDK 37\n- Release builds enable R8 minification + resource shrinking; `noR8` variant disables both for crash debugging\n- APK is split by ABI (`armeabi-v7a`, `arm64-v8a`, plus universal)\n- Firebase Analytics and Performance are included; `google-services` plugin applied\n\n## Web Frontend\n\nLocated 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:\n\n```bash\ncd modules/web\npnpm install\npnpm dev       # dev server\npnpm build     # production build\n```\n\nSet `VITE_API` in `.env.development` to the app's web service IP.\n","category":"root","tokens":3829},{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Coding Guidelines\n\n**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.\n\n### Think Before Coding\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n### Simplicity First\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n\n### Surgical Changes\n\n- Don't \"improve\" adjacent code, comments, or formatting.\n- Don't refactor things that aren't broken.\n- Match existing style, even if you'd do it differently.\n- If you notice unrelated dead code, mention it — don't delete it.\n- Remove imports/variables/functions that YOUR changes made unused.\n- Don't remove pre-existing dead code unless asked.\n\n### Goal-Driven Execution\n\nTransform tasks into verifiable goals:\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\n## Build / Test / Run\n\n```bash\n# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)\n.\\gradlew.bat :app:compileAppDebugKotlin\n\n# Assemble all variants\n./gradlew assembleAppRelease\n\n# Assemble without R8 (for crash debugging — no minification/shrinking)\n./gradlew assembleAppNoR8\n\n# Debug build\n./gradlew assembleAppDebug\n\n# Run unit tests (JVM, local)\n./gradlew test\n\n# Run a single test class\n./gradlew test --tests \"io.legado.app.model.cache.CacheDownloadQueueTest\"\n\n# Run connected Android tests\n./gradlew connectedAndroidTest\n\n# Lint\n./gradlew lint\n\n# Update Cronet (after changing CronetVersion in gradle.properties)\n./gradlew app:downloadCronet\n```\n\nThe project uses JDK 21 for development (set in `build.gradle.kts` via `jvmToolchain`). CI uses JDK 17 for building.\n\nGradle properties: 8 GB heap, configuration cache disabled (`gradle.properties:31`), non-transitive R classes, precise resource shrinking enabled.\n\n## Architecture\n\nThis 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:\n\n| Layer | Package | Role |\n|---|---|---|\n| Data | `data/` | Room DB (`AppDatabase`, version 85, ~22 DAOs, ~25 entities), repository implementations |\n| Domain | `domain/` | Gateway interfaces, use cases (14), domain models — no framework dependencies |\n| UI | `ui/` | Jetpack Compose screens, Navigation 3 routes, ViewModels |\n\nAdditional top-level packages:\n- **`help/`** — Infrastructure \"glue\": HTTP (OkHttp + Cronet), book content processing, backup/WebDAV, JS engine, config\n- **`model/`** — Runtime state coordinators (not entities): `ReadBook`, `AudioPlay`, `CacheBook`, `BookCover`, etc.\n- **`service/`** — Android foreground/background services (audio playback, TTS, download, web server)\n- **`web/`** — Embedded HTTP server (Ktor) for remote bookshelf/source editing\n- **`lib/`** — Third-party library wrappers (MOBI parser, WebDAV client, legacy View theme system, cronet)\n- **`base/`** — Abstract Activity/Fragment/ViewModel base classes\n- **`utils/`** — Extension functions and utility classes (~70 files)\n\nModules: `: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).\n\n## Dependency Injection (Koin)\n\nTwo modules loaded in `App.onCreate()`:\n\n```kotlin\nstartKoin {\n    modules(appDatabaseModule, appModule)\n}\n```\n\n- **`di/appDatabaseModule.kt`** — Singleton `AppDatabase` + factory bindings for all 22 DAOs\n- **`di/appModule.kt`** — Singletons (repositories, use cases, gateways, Coil `ImageLoader`), `viewModelOf` / `viewModel { }` for all ViewModels, some parameterized definitions\n\nGateways are bound to their repository implementations explicitly (e.g., `single<LocalBookGateway> { LocalBookRepository(get()) }`), not through `singleOf`.\n\n## Navigation\n\nUses **Jetpack Navigation 3** (`androidx.navigation3`) with type-safe `@Serializable` sealed interfaces for route keys:\n\n```kotlin\n@Serializable\nprivate sealed interface MainRoute : NavKey\n@Serializable\nprivate data object MainRouteHome : MainRoute\n@Serializable\nprivate data class MainRouteCache(val groupId: Long) : MainRoute\n```\n\n`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.\n\n## Theme System\n\nA multi-engine theming system in `ui/theme/`:\n\n1. **Material 3 Expressive** (default): Uses `MaterialExpressiveTheme` with `MotionScheme.expressive()`\n2. **Miuix** (alternative): Uses `top.yukonga.miuix.kmp` theming engine\n\n14 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).\n\nLegacy View-based theme still exists in `lib/theme/` (used by non-migrated screens like `ReadBookActivity`).\n\n## Hybrid Compose + View\n\nThe 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.\n\n## Jetpack Compose Requirements (new screens MUST follow)\n\nAll **new** UI screens must be implemented in Jetpack Compose following the patterns below. Do **not\n** create new View-based Activities/Fragments/XML layouts. Existing View-based screens can remain\nuntil migrated.\n\n### MVI/UDF Architecture\n\nEvery Compose screen follows a strict **Model-View-Intent** pattern with three artifacts defined in\na `*Contract.kt` file:\n\n```\nui/{feature}/\n├── XxxContract.kt      // UiState, Intent, Effect (and optionally Sheet/Dialog)\n├── XxxViewModel.kt     // ViewModel\n├── XxxScreen.kt        // Screen composable\n└── XxxRouteScreen.kt   // (optional) outer wrapper for activity results / lifecycle\n```\n\n**Contract definitions:**\n\n```kotlin\n// @Stable data class — all screen state in one place\n@Stable\ndata class XxxUiState(\n    val loading: Boolean = false,\n    val items: ImmutableList<ItemUi> = persistentListOf(),\n    val activeSheet: XxxSheet? = null,\n    val activeDialog: XxxDialog? = null,\n)\n\n// sealed interface — every user action is an Intent\nsealed interface XxxIntent {\n    data class LoadData(val id: Long) : XxxIntent\n    data object Refresh : XxxIntent\n}\n\n// sealed interface — one-shot side effects (navigation, toast, etc.)\nsealed interface XxxEffect {\n    data class ShowToast(val message: String) : XxxEffect\n    data class NavigateTo(val route: MainRoute) : XxxEffect\n}\n\n// (optional) sealed interface for multi-sheet/dialog scenarios\nsealed interface XxxSheet { data object Filter : XxxSheet }\nsealed interface XxxDialog { data class Confirm(val msg: String) : XxxDialog }\n```\n\n**Naming rules:**\n\n- State: `{Feature}UiState` — `@Stable data class`\n- Intent: `{Feature}Intent` — `sealed interface` with `data class` / `data object` members\n- Effect: `{Feature}Effect` — `sealed interface`\n- Sheet/Dialog: `{Feature}Sheet`, `{Feature}Dialog` — `sealed interfaces` stored in UiState\n\n### ViewModel\n\n```kotlin\nclass XxxViewModel(/* injected dependencies */) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(XxxUiState())\n    val uiState = _uiState.asStateFlow()\n\n    private val _effects = MutableSharedFlow<XxxEffect>(extraBufferCapacity = 16)\n    val effects = _effects.asSharedFlow()\n\n    fun onIntent(intent: XxxIntent) {\n        when (intent) {\n            is XxxIntent.LoadData -> loadData(intent.id)\n            is XxxIntent.Refresh -> refresh()\n        }\n    }\n\n    private fun loadData(id: Long) {\n        // Use viewModelScope, update _uiState via update { it.copy(...) }\n    }\n}\n```\n\nKey rules:\n\n- Extend `ViewModel()` directly (not `BaseViewModel`).\n- `_uiState` is `MutableStateFlow`, exposed as `StateFlow` via `.asStateFlow()`.\n- `_effects` is `MutableSharedFlow(extraBufferCapacity = 16)`, exposed via `.asSharedFlow()`.\n- Emit effects via `_effects.tryEmit(...)`.\n- Single `onIntent()` entry point, dispatched via `when`.\n\n### Screen Composable\n\n```kotlin\n// Stateless screen — ViewModel wired in entry provider or RouteScreen\n@Composable\nfun XxxScreen(\n    state: XxxUiState,\n    onIntent: (XxxIntent) -> Unit,\n    effects: Flow<XxxEffect>,                   // one-shot effects from ViewModel\n    onBack: () -> Unit,\n    onNavigateToYyy: (YyyRoute) -> Unit,\n) {\n    // Collect effects\n    LaunchedEffect(Unit) {\n        effects.collectLatest { effect ->\n            when (effect) {\n                is XxxEffect.ShowToast -> { /* ... */ }\n                is XxxEffect.NavigateTo -> onNavigateToYyy(effect.route)\n            }\n        }\n    }\n\n    AppScaffold(\n        topBar = {\n            GlassMediumFlexibleTopAppBar(\n                title = { Text(\"Title\") },\n                scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior(),\n                navigationButton = { TopBarNavigationButton(onBack) },\n            )\n        },\n    ) { contentPadding ->\n        // UI content, no business logic here\n    }\n}\n```\n\nKey rules:\n\n- Screen is **stateless** — receives `state`, `onIntent`, `effects`, never accesses ViewModel\n  directly.\n- Effects collected in `LaunchedEffect(Unit) { ... }` using `collectLatest`.\n- Alternatively, effects can be collected in the outer `RouteScreen` or entry provider if the screen\n  doesn't need them directly.\n- Use project custom widgets: `AppScaffold`, `AppText`, `AppIcon`, `AppIcons`, `AppAlertDialog`,\n  `AppModalBottomSheet`, `NormalCard`, `GlassMediumFlexibleTopAppBar`, `TopBarNavigationButton`,\n  `TopBarActionButton`, etc.\n- No business logic, no direct DB/network calls in composables.\n\nTwo input patterns are acceptable:\n\n- **Stateless (preferred for new screens):** `state: XxxUiState` + `onIntent: (XxxIntent) -> Unit` —\n  ViewModel wired in entry provider or RouteScreen.\n- **ViewModel as default param:** `viewModel: XxxViewModel = koinViewModel()` — simpler for\n  standalone screens.\n\n### Stability\n\n- All `UiState` and UI item data classes **must** be annotated with `@Stable`.\n- Use `ImmutableList` (from `kotlinx.collections.immutable`) for list properties in state classes,\n  not `List` or `MutableList`.\n- Prefer `persistentListOf()` / `toImmutableList()` for default values.\n\n### Navigation\n\nUses **Navigation 3** (`androidx.navigation3`). Routes are `@Serializable` sealed interfaces:\n\n```kotlin\n// In MainNavKey.kt\n@Serializable\ndata class MainRouteXxx(val id: Long) : MainRoute\n```\n\nEntry registered in `MainNavGraph.kt`:\n\n```kotlin\nentry<MainRouteXxx> { route ->\n    val viewModel = koinViewModel<XxxViewModel>()\n    XxxScreen(\n        state = viewModel.uiState.collectAsStateWithLifecycle().value,\n        onIntent = viewModel::onIntent,\n        onBack = { onNavigateBack() },\n        onNavigateToYyy = { onNavigateToRoute(it) },\n    )\n}\n```\n\nKey rules:\n\n- Screens **never** reference the navigator directly — receive `onBack`, `onNavigateToXxx` lambdas.\n- Navigation is callback-based, wired by the entry provider.\n- New routes added to the `MainRoute` sealed interface in `MainNavKey.kt`.\n\n### Koin DI\n\n- Register ViewModels in `di/appModule.kt` with `viewModelOf(::XxxViewModel)`.\n- Inject in Compose via `koinViewModel()` (default param or explicit in entry provider).\n- For keyed ViewModels (e.g. per-book): `koinViewModel<XxxViewModel>(key = route.bookUrl)`.\n- Repositories/gateways/use cases registered as `singleOf(::...)`.\n\n### Activity Base Class\n\nNew standalone Compose activities extend `BaseComposeActivity`:\n\n```kotlin\nclass XxxActivity : BaseComposeActivity() {\n    @Composable\n    override fun Content() {\n        // Screen content — AppTheme is already applied by the base class\n    }\n}\n```\n\n### RouteScreen Wrapper\n\nFor screens needing activity result handling, lifecycle observation, or permission requests, use a\ntwo-layer pattern:\n\n- Outer `XxxRouteScreen`: handles `ActivityResultLauncher`, lifecycle callbacks, file pickers,\n  permission requests. Wires ViewModel.\n- Inner `XxxScreen`: pure UI, stateless with `state` + `onIntent`.\n\n### Material 3 vs Miuix\n\nThe project supports two Compose theme engines. If a screen needs engine-specific UI, branch on:\n\n```kotlin\nif (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {\n    // Miuix implementation\n} else {\n    // Material 3 implementation\n}\n```\n\nFor detailed Compose review conventions and migration patterns, see\n`.claude/skills/legado-compose-review/`.\n\n## Rhino JavaScript Engine\n\nBook 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/`.\n\n## Important Constraints\n\n- **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\n- **Do not update hutool** beyond 5.8.22 — pinned in `libs.versions.toml:42`\n- Package name discrepancy: code namespace is `io.legado.app` but `applicationId` is `io.legato.kazusa`\n- Min SDK 26, target SDK 37, compile SDK 37\n- Release builds enable R8 minification + resource shrinking; `noR8` variant disables both for crash debugging\n- APK is split by ABI (`armeabi-v7a`, `arm64-v8a`, plus universal)\n- Firebase Analytics and Performance are included; `google-services` plugin applied\n\n## Web Frontend\n\nLocated 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:\n\n```bash\ncd modules/web\npnpm install\npnpm dev       # dev server\npnpm build     # production build\n```\n\nSet `VITE_API` in `.env.development` to the app's web service IP.\n","category":"root","tokens":3704},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","title":"copilot-instructions.md","content":"# Copilot Instructions for AI Coding Agents\n\n## 项目架构概览\n- 本项目为 Android 阅读器（Material Design 3 风格重构版），包含主 app（Kotlin/Java）、配套 web 端（Vue3）、多模块扩展。\n- 主要目录：\n  - `app/`：Android 主程序，核心逻辑、服务、UI、数据层。\n  - `modules/web/`：Web 端书架与源编辑，需与 app 后端联动。\n  - `modules/book/`、`modules/rhino/`：功能扩展模块。\n\n## 关键组件与数据流\n- 书籍、书源、订阅源等核心数据结构定义于 `app/src/main/java/io/legato/kazusa/data/`。\n- 解析规则、导入/导出逻辑见 `model/`、`model/localBook/`。\n- 服务（如音频播放、下载、朗读、Web 服务）在 `service/`，通过广播或接口与 UI、数据层交互。\n- UI 组件分布于 `ui/`，按功能细分（如书架、阅读、搜索、订阅等）。\n- Web 端通过 REST API 与 app 通信，需配置 `.env.development` 的 `VITE_API` 指向 app 的 web 服务 IP。\n\n## 构建与开发流程\n- **Android 构建**：\n  - 使用 Gradle，入口为 `build.gradle`、`app/build.gradle`。\n  - 常用命令：`./gradlew assembleRelease`、`./gradlew test`。\n  - ProGuard 混淆规则见 `proguard-rules.pro`、`cronet-proguard-rules.pro`。\n- **Web 端开发**：\n  - 进入 `modules/web/`，使用 `pnpm dev` 启动开发，`pnpm build` 打包。\n  - 调试需保证手机与电脑同网段，手机端开启 web 服务。\n\n## 项目约定与特殊模式\n- 书源、订阅源规则高度自定义，相关解析逻辑集中于 `model/analyzeRule`、`model/rss`。\n- 加密/解密相关辅助函数见 `help/crypto/`，RhinoJs 调用 Java 方法有特殊重载（见 README）。\n- 主题、权限、网络存储等通用功能在 `lib/` 下有独立实现。\n- 书籍文件支持 TXT、EPUB、PDF、UMD，解析入口为 `model/localBook/LocalBook.kt`。\n\n## 外部依赖与集成\n- 主要依赖：JsoupXpath、json-path、rhino-android、okhttp、glide、ktor、bga-qrcode-zxing、colorpicker、commons-text、markwon、hanlp、epublib-core 等。\n- 依赖声明见 `build.gradle`、`app/build.gradle`，部分第三方库源码在 `lib/`。\n\n## 典型开发场景示例\n- 新增书源解析：扩展 `model/analyzeRule`，同步更新 UI 相关界面。\n- 增加服务：在 `service/` 新建服务类，注册广播或接口，UI 层调用。\n- Web 端联动：确保 API 路由与 app web 服务一致，调试时修改 `.env.development`。\n\n## 参考文档与社区\n- 官方帮助文档：https://www.yuque.com/legado/wiki\n- 书源规则教程：https://mgz0227.github.io/The-tutorial-of-Legado/\n- 社区交流：Telegram、Discord、语雀社区\n\n---\n如需补充或有疑问，请反馈具体场景或模块。","category":".github","tokens":416}]}