You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: AGENTS.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,7 +45,7 @@ EasyWatermark (`me.rosuh.easywatermark`) โ a privacy-focused Android app that
45
45
- **`WaterMarkRepository` now in commonMain (S4d-84..S4d-89, accepted 2026-06-27, final move commit `f23bf5d`):** the watermark-config DataStore consumer moved to `shared/src/commonMain/kotlin/me/rosuh/easywatermark/data/repo/WaterMarkRepository.kt` (same package/FQN) โ the **second commonMain DataStore Preferences consumer** after `UserConfigRepository`. It was reached by a staged Android-edge removal (each its own slice, all behavior-preserving): **S4d-84** `Color.parseColor("#FFB800")` โ const `0xFFFFB800.toInt()` (pinned by a Robolectric equality test) + `ArrayMap` โ `mutableMapOf()`; **S4d-85** `java.io.IOException` โ `okio.IOException`; **S4d-86** the localized default text injected as `defaultTextProvider: () -> String` (RepositoryModule passes `context.getString(R.string.config_default_water_mark_text)`, a per-emission lambda); **S4d-87** the persisted tile-id read mapper injected as `tileModeFromStorageId: (Int?) -> WatermarkTileMode` (RepositoryModule passes the **SDK-gated `toWatermarkTileMode`** in `TileModeExt.kt`, so the legacy **pre-Android-12 stored DECAL id 3 โ REPEAT** behavior is preserved โ NOT the pure `fromStorageId`); **S4d-88** the one diagnostic `Log.e` injected as `logError: (String) -> Unit` (RepositoryModule passes `Log.e("WaterMarkRepository", message)`, identical tag/message); **S4d-89** the byte-identical file relocation (git rename, 0 content changes). **Persisted bytes/keys unchanged** โ `SP_NAME = "sp_water_mark_config"`, all `SP_KEY_*` (incl. `SP_KEY_ICON_URI`, `SP_KEY_CHANGE_LOG`), `MAX_*`/legacy constants, storage ids, and the DataStore file naming + `SharedPreferencesMigration` semantics are identical; **no DataStore migration, no golden rebaseline** (strict 49/0). `RepositoryModule` (the three injected edges) and `DataStoreModule` (still creates the Android `waterMarkDataStore` from `WaterMarkRepository.SP_NAME` via the S4d-74 `createPreferencesDataStore`) are the Android wiring edges; `TileModeExt.kt`/`EditorScreen`/the test resolve the same FQN from `:shared` unchanged. Verified by shared Desktop/iOS/iOS-sim + android + app compile, assembleDebug + assembleRelease (R8 retained the class, APK size unchanged), strict goldens 49/0. These are **code-migration slices, not UI-parity slices**.
46
46
- **Room/templates now in commonMain (S4d-90 readiness โ S4d-91 toolchain proof โ S4d-92 move, accepted + committed `8d245d9`):** the production templates Room path moved to `:shared/commonMain` at the **same FQNs** โ `data/model/entity/Template`, `data/db/DateConverter`, `data/db/dao/TemplateDao`, `data/db/AppDatabase` (now `@ConstructedBy(AppDatabaseConstructor::class)` + `@Suppress("KotlinNoActualForExpect") expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase>`), and `data/repo/TemplateRepository`. **S4d-90** mapped readiness (Room is atomic; prepopulated-asset data-risk). **S4d-91** proved the Room KMP toolchain (Room Gradle plugin + KSP-MP, `kspAndroid`/`kspDesktop`/`kspIosArm64`/`kspIosSimulatorArm64`) compiles/links for all 4 targets, with the proof Room/SQLite **`compileOnly` in commonMain** so the unused `libsqliteJni.so` did **not** ship to `:app` (r1 confinement; proof package since deleted). **S4d-92** did the production move. **Android driver decision = compatibility mode (no explicit `SQLiteDriver`)** โ Android keeps the framework SupportSQLite open-helper + `createFromAsset`, so the prepackaged `ewm-db-ch.db`/`ewm-db-eng.db` open byte-identically and **no** `sqlite-bundled`/`libsqliteJni.so` (or extra `sqlite-framework`) ships; release APK unchanged. Android creation lives in `:shared/androidMain` `data/db/TemplateDatabaseBuilder.android.kt` `buildTemplateDatabase(context)` (locale `createFromAsset` + in-memory fallback, same `ewm-db` path); `AppModule`/`RepositoryModule` call it (`TemplateRepository` now takes an injected `ioContext: CoroutineContext` โ Koin passes `Dispatchers.IO`, since `Dispatchers.IO` isn't accessible in commonMain on Native). `:shared` commonMain Room dep is `implementation(room.runtime)` (no sqlite artifact); `sqlite-bundled`/`sqlite` removed from the catalog; `app/build.gradle.kts` untouched (its existing room deps cover the `RoomDatabase` supertype). `exportSchema=false`, `version=1` unchanged; no schema/migration change, no golden rebaseline. Proven by a Robolectric **prepopulated-DB smoke** (`TemplatePrepopulatedDbSmokeTest`, 2/0 โ both seed assets open + read seeded templates non-empty), all-target compile, iOS link, strict goldens 51/0, debug+release APK (no `libsqliteJni.so`). **Desktop/iOS production templates builders are intentionally unwired** (the DB type compiles/links on those targets but no builder is created โ no templates UI there yet). **Residual:** the release APK was not run on a device (release retention is covered by R8 keep-rule analysis + the debug smoke). Code-migration slices, not UI-parity.
47
47
- **First shared editor use-case extracted (S4d-94 โ S4d-95 โ S4d-96, accepted; S4d-96 committed `7ee7e77`):** **S4d-94** (read-only) decided the **Koin common/platform split stays deferred** โ no non-Android DI consumer exists (Koin is started only in `MyApp`; desktop/iOS construct their one repo manually), so a commonMain Koin module would be a framework layer with no consumer. **S4d-95** (read-only) mapped the ViewModels and found `MainViewModel` cannot move (deep `Bitmap`/`Uri`/`MediaStore`/`File`/`Context` IO) and both VMs extend Android `androidx.lifecycle.ViewModel`; the smallest non-speculative step is a commonMain **use-case** consumed by the Android VM. **S4d-96** landed it: `shared/src/commonMain/.../domain/WatermarkConfigEditor.kt` โ the **first small shared editor config use-case** over the commonMain `WaterMarkRepository`, with the 12 `update*` config edits as `suspend` methods and the three inline rules moved **verbatim** (text-size `coerceAtLeast(0f)`, `WatermarkConfigRules.alphaPercentToByte`, non-empty `MediaRef` icon guard). It is a **behavior-preserving extraction, NOT a shared ViewModel or reducer**: `MainViewModel` stays Android-side, still owns `viewModelScope`/`launch` and the save/compress/gallery/picker/crash/IO flows, and constructs the use-case from the already-injected `WaterMarkRepository` (**no Koin/AppModule/Gradle/lifecycle/renderer/storage change**). Public VM method names/signatures unchanged. Verified: all-target compile + `:app`, `:shared:linkDebugTestIosSimulatorArm64`, strict goldens **51/0** (no rebaseline), debug+release APK (release size unchanged), daemon stop. **S4d-97 (accepted, committed `ec9317c`)** added the sibling `shared/commonMain/.../domain/OutputPrefsEditor.kt` โ `class OutputPrefsEditor(repo: UserConfigRepository)` with one `suspend save(format, level)` wrapping `UserConfigRepository.updateFormat(format)` then `updateCompressLevel(level)` (same order, no validation/clamping added); `MainViewModel.saveOutput` keeps its signature/defaults and still owns `viewModelScope.launch` with `resetJobStatus()` after the launch/outside the coroutine, built from the already-injected `userRepo` (no DI change). Same gates green (51/0, release size unchanged). **S4d-98 (accepted, committed `d26c95b`)** added the third sibling `shared/commonMain/.../domain/TemplateEditor.kt` โ `class TemplateEditor(repo: TemplateRepository)` with `isDaoNull()` (wraps `checkIfIsDaoNull()` so the VM keeps the null-DAO โ `UiState.DatabaseError` mapping Android-side โ `UiState` is **not** in commonMain), `suspend add(content)` constructing `Template(0, content, Clock.System.now(), Clock.System.now())` (two separate `now()` calls, verbatim) then inserting, and `suspend update/delete` delegations. `MainViewModel` keeps its public signatures + the synchronous null-DAO-check structure, owns `viewModelScope`/`launch`/`UiState`, built from the already-injected `templateRepo` (no DI change). A **cheap** commonTest (`TemplateEditorTest`, 2/0) pins `isDaoNull()` true/false (possible here because `TemplateDao` is an interface); the suspend writes are NOT directly unit-tested (commonTest has no coroutine runner) โ compile/link + unchanged goldens are the build/regression net, not a unit test of those writes. The three small editor use-cases are now extracted; the remaining shared-state work (the deliberate multiplatform-`lifecycle` decision for moving `AboutViewModel`, and real Desktop/iOS editor UI consumers) stays separate.
48
-
- **NOT yet done** (the bulk โ months of work): the broader shared ViewModel/use-case extraction โ the three small shared editor use-cases landed (`WatermarkConfigEditor` S4d-96, `OutputPrefsEditor` S4d-97, `TemplateEditor` S4d-98), but `MainViewModel`/`AboutViewModel` themselves stay Android-side โ **S4d-99 (read-only readiness, accepted)** found `AboutViewModel` is technically ready but owner-gated/speculative until a real Desktop/iOS About consumer exists; the **golden test harness (C1.7)** that gates deeper engine rewrite (C2); the Koin common/platform split (deliberately deferred by S4d-94 while no non-Android DI consumer exists); iOS/Desktop seed-DB packaging/default-template parity beyond the Android Room compatibility-mode move (S4d-90..S4d-92); the full Compose Desktop editor beyond the current minimal product-flow window; Desktop renderer feature parity; reactive preview/editor polish; and the remaining real PHPicker grid-cell proof. Android text/icon/composition/decode production remains native by default. KMP/CMP migration is NOT complete.
48
+
- **NOT yet done** (the bulk โ months of work): the broader shared ViewModel/use-case extraction remains consumer-gated โ the three small shared editor use-cases landed (`WatermarkConfigEditor` S4d-96, `OutputPrefsEditor` S4d-97, `TemplateEditor` S4d-98), but `MainViewModel`/`AboutViewModel` themselves stay Android-side. **S4d-191 (read-only readiness, accepted)** confirmed there is no safe non-speculative `MainViewModel` business-IO slice right now: the consumed business logic is already shared, remaining pure-looking methods are Android `UiState` only, and the rest is Android IO/render (`ContentResolver`/`MediaStore`/`Bitmap`/`Canvas`/`Compressor`/native `WatermarkRenderer`). Do NOT create a shared ViewModel/navigation reducer/IO `expect` layer without a named real off-Android consumer or an explicit owner decision. **S4d-99 (read-only readiness, accepted)** found `AboutViewModel` is technically ready but owner-gated/speculative until a real Desktop/iOS About consumer exists; the **golden test harness (C1.7)** that gates deeper engine rewrite (C2); the Koin common/platform split (deliberately deferred by S4d-94 while no non-Android DI consumer exists); iOS/Desktop seed-DB packaging/default-template parity beyond the Android Room compatibility-mode move (S4d-90..S4d-92); the full Compose Desktop editor beyond the current minimal product-flow window; Desktop renderer feature parity; reactive preview/editor polish; and the remaining real PHPicker grid-cell proof. Android text/icon/composition/decode production remains native by default. KMP/CMP migration is NOT complete.
49
49
50
50
Session memory (planning-with-files): `task_plan.md`, `findings.md`, `progress.md` at repo root โ read them at session start, update them as you work.
- Modules: `:app` (everything), `:cmonet` (Material You dynamic-color gate, Android-only โ **now fronted by the platform-neutral `DynamicColorCapability`** for the live Compose consumers (S4d-43, accepted); `:cmonet` is **retained** behind the Android actual, full absorption/removal is an owner-gated follow-up โ ADR-0007), `:baseBenchmarks`/`:macrobenchmark` (Android-only perf), `buildSrc`.
66
-
- **Data flow:** DataStore Preferences (watermark config via `WaterMarkRepository` and user prefs via `UserConfigRepository` โ **both now commonMain** (S4d-84..S4d-89 and S4d-77 respectively), with their Android-only behavior injected at the `RepositoryModule` Koin edge; **store creation** is seamed into `:shared/androidMain` `createPreferencesDataStore(context, name)` since S4d-74, with `di/DataStoreModule.kt` keeping the `Context.userDataStore`/`waterMarkDataStore` property names) + Room (`Template` entity + `AppDatabase`/`TemplateDao`/`TemplateRepository` **now commonMain** per S4d-90..S4d-92; Android creation in `:shared/androidMain` `buildTemplateDatabase` keeps the **prepopulated DBs** `assets/ewm-db-ch.db`/`ewm-db-eng.db` selected by locale, in **Room compatibility mode** โ no explicit `SQLiteDriver`, framework SupportSQLite, no `sqlite-bundled` native payload) โ Koin DI (`di/`) โ `MainViewModel` (large Android-side state/workflow owner; source state is now StateFlow-only after S4d-63..S4d-69; the first shared editor use-case `WatermarkConfigEditor` was extracted in S4d-96, but the VM itself stays Android-side and the broader shared ViewModel/use-case extraction is still ahead) โ both UI stacks.
66
+
- **Data flow:** DataStore Preferences (watermark config via `WaterMarkRepository` and user prefs via `UserConfigRepository` โ **both now commonMain** (S4d-84..S4d-89 and S4d-77 respectively), with their Android-only behavior injected at the `RepositoryModule` Koin edge; **store creation** is seamed into `:shared/androidMain` `createPreferencesDataStore(context, name)` since S4d-74, with `di/DataStoreModule.kt` keeping the `Context.userDataStore`/`waterMarkDataStore` property names) + Room (`Template` entity + `AppDatabase`/`TemplateDao`/`TemplateRepository` **now commonMain** per S4d-90..S4d-92; Android creation in `:shared/androidMain` `buildTemplateDatabase` keeps the **prepopulated DBs** `assets/ewm-db-ch.db`/`ewm-db-eng.db` selected by locale, in **Room compatibility mode** โ no explicit `SQLiteDriver`, framework SupportSQLite, no `sqlite-bundled` native payload) โ Koin DI (`di/`) โ `MainViewModel` (large Android-side state/workflow owner; source state is now StateFlow-only after S4d-63..S4d-69; shared editor use-cases were extracted in S4d-96..S4d-98, and S4d-191 keeps the rest of this VM Android-side until a real off-Android consumer exists) โ both UI stacks.
67
67
- **Rendering engine (the product core):** `WatermarkRenderer` (Android-only seam, `:app/render`) builds a watermark "cell" offscreen (text via StaticLayout, icon via scaled bitmap, rotated), wraps it in a `BitmapShader` โ `REPEAT` tiles it across the photo, `CLAMP` ("decal") draws one draggable instance at a fractional offset. Both the Compose `Canvas` preview (`EditorScreen.WaterMarkCanvas`) and export (`MainViewModel.generateImage`) call the **same** `WatermarkRenderer.build*Shader` + `compose` seam, so they composite identically. Cell sizing is **image-space** (S3a: `textPx = textSize * imageWidth / REF_WIDTH`, ref width 1000), independent of any view matrix โ the old `ViewInfo`/`1/MSCALE_X` export-scale coupling was removed (S3c-1/S3c-3). The shared sizing **constants/math now live in commonMain** and are consumed by all three platforms: text image-space sizing via `WatermarkGeometry.REF_WIDTH` / `WatermarkGeometry.fontPx(textSize, imageWidth)` (Android `PainKtx.applyConfig`, Desktop, iOS โ S4d-181/182), icon scale via `WatermarkCellComposer.ICON_SCALE_REFERENCE_TEXT_SIZE` (Android/Desktop/iOS โ S4d-184), and cell *geometry* (gap/diagonal/rotated-AABB) via `WatermarkGeometry`. **Only the constants/math are shared โ Android text/icon production raster/draw stays native** (`StaticLayout` / `buildIconShader`; ADR-0004/S4d-8/S4d-17); the `WatermarkCellComposer`/`TextRasterEnv` raster primitives remain the Desktop/iOS path, not Android-production-wired. The legacy Android renderer `REF_WIDTH` const was deleted in S4d-188 (it was orphaned; `WatermarkGeometry.REF_WIDTH` is the live source). commonMain `WatermarkCellComposer` is the **Desktop/iOS** renderer (cell raster + `composeOverBackground` composition/tiling); **Android production raster AND composition stay native by closed decisions** โ icon (S4d-8), text (S4d-17), and composition (S4d-190 No-Go) โ see ADR-0004.
68
68
- **Gotchas encoded in data:** watermark config values and pure normalization rules have been moved toward commonMain without changing stored bytes. `WatermarkTileMode.storageId` still mirrors the historical Android `Shader.TileMode` ordinals; `WatermarkMode.Text/Image` mirrors the deleted `WaterMarkRepository.MarkMode` ints; `TextTypeface`/`TextPaintStyle` keep their old `serializeKey()` values; `WatermarkConfigRules` preserves the legacy clamps/conversions for text size, alpha, h/v gap, degree, and mode transitions. Android render types stay at the `:app` edge via `TileModeExt.kt` / `TextStyleExt.kt`; do not put `android.graphics.*`, `android.net.Uri`, or repo-nested types back into common model classes. **Note:** `WaterMark.iconUri` has already been migrated to the platform-neutral `MediaRef` (`@JvmInline value class`, `shared/commonMain`; needs `import kotlin.jvm.JvmInline`) with Android `Uri` only at the edge (`utils/ktx/MediaRefExt.kt`). **`ImageInfo.uri` and `WaterMarkRepository.imageInfoMap` are now `MediaRef` too โ S4d-52, accepted 2026-06-25** (the `Uri โ MediaRef` conversion lives at the picker/share-in/gallery construction and the decode/Coil/save edges; `MediaRef` value-class string equality keeps `imageInfoMap`/`isSameItem` semantics). `ImageInfo` itself moved to `shared/commonMain` in S4d-71; the old Android range annotation on `offsetX`/`offsetY` is gone, and the normalized 0f..1f invariant is documentation only. The dead `ImageInfo.shareUri` accessor was **removed (S4d-53, 2026-06-25)** โ it had zero call sites (the share-out button is an unwired empty lambda); the real export result stays in `ImageInfo.result`/`jobState`. **Deliberately kept as Android `Uri` edges (do NOT "fix" incidentally):** gallery `Image.uri`, `Action.SystemPickerImageSelected.uriList`, `SaveExportSheet.imageUris`, the picker contracts (`PickImageContract`/`MultiPickContract`), and `BitmapUtils`/`BitmapCache`/`FileUtils` decode signatures. The dead `KEY_URI`/`SP_KEY_URI` DataStore key declarations were **removed (S4d-54, 2026-06-25)** โ `KEY_ICON_URI` and all real watermark prefs are untouched. **No model-layer `Uri` hygiene remains**; the only `Uri` left is the deliberate Android edges above.
69
69
- Image IO: decode via `BitmapFactory` + inSampleSize with EXIF rotation baked in (`utils/bitmap/BitmapUtils.kt`); save via MediaStore `IS_PENDING` (API โฅ29) or pre-Q file path; export deliberately strips all EXIF metadata (privacy feature โ ADR-0009).
0 commit comments