Skip to content

Commit fb4f78b

Browse files
committed
Document iOS template seed packaging
1 parent 44be9f7 commit fb4f78b

5 files changed

Lines changed: 17 additions & 8 deletions

File tree

โ€ŽAGENTS.mdโ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ EasyWatermark (`me.rosuh.easywatermark`) โ€” a privacy-focused Android app that
4545
- **Desktop release hygiene + packaging proof (S4d-161 -> S4d-166, accepted 2026-06-28):** S4d-161 (read-only decision) chose release-grade hygiene + a packaging proof over speculative UI widening. **S4d-162** (commit `bb5e770f`, `DesktopWindow.kt` only) made the window title release-facing (`EasyWatermark โ€” Desktop`, dropping the `(S4d-121)` dev marker) and disabled the Templates "Save current text" button when the watermark text is blank. **S4d-163** (commit `ccc9f3f7`, `desktopApp/build.gradle.kts` only) replaced the plain Gradle `application` plugin with the Compose Desktop `compose.desktop { application { mainClass; nativeDistributions { packageName = "EasyWatermark"; packageVersion = "1.0.0" } } }` DSL, so the packaging tasks now exist (`createDistributable`/`runDistributable`/`packageDistributionForCurrentOS`) and `createDistributable` builds an **unsigned `.app` image** (native launcher + `Info.plist` + bundled JRE + app jars, under `desktopApp/build/compose/binaries/main/app/`). The `run` task moved into the Compose DSL and `:desktopApp:run --args='--headless'` still works (witnesses unchanged). **JDK caveat:** `createDistributable` **fails** under the jenv/Homebrew OpenJDK because Compose Desktop's `checkRuntime` rejects Homebrew vendors (CMP issue #3107); it **succeeds** with the already-installed Amazon Corretto 17 (`/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home`). The accepted path is to use a **supported packaging JDK** โ€” do **NOT** add `compose.desktop.packaging.checkJdkVendor=false`. **S4d-166** adds `.github/workflows/desktop_packaging.yml`, a narrow `workflow_dispatch` + path-filtered PR job using Ubuntu + Zulu 17 that runs `:desktopApp:run --args='--headless'` before `:desktopApp:createDistributable`; the green Actions run is the proof that Zulu satisfies the vendor guard. **Overclaim guard:** unsigned app-image proof/workflow ONLY โ€” no installer formats (DMG/PKG/MSI/DEB), signing/notarization, app icon, real product-version source yet (`packageVersion = "1.0.0"` is a placeholder), macOS packaging, or Android v2.10.0 1:1 UI/UX parity.
4646
- Typed `@Serializable` Navigation routes (`ui/Routes.kt`); `ImageFormat` replaces `Bitmap.CompressFormat` in the model (encode mapping at the edge, `utils/ktx/ImageFormatExt`); `AboutViewModel` is StateFlow (C1.1 started).
4747
- **`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**.
48-
- **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); `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`). **Off-Android builders now exist where there is a real consumer path:** Desktop has `buildTemplateDatabase(dir)` plus locale-aware seed packaging from S4d-224/S4d-225; **S4d-231 (accepted 2026-06-29, commit `278d585`) added the iOS empty-store builder** in `:shared/iosMain` (`BundledSQLiteDriver`, `Dispatchers.Default` query context, `NSDocumentDirectory` overload) and an iOS simulator roundtrip over `TemplateRepository`/`TemplateEditor`. iOS seed packaging/default templates and Swift Templates UI remain deferred. **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.
48+
- **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); `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`). **Off-Android builders now exist where there is a real consumer path:** Desktop has `buildTemplateDatabase(dir)` plus locale-aware seed packaging from S4d-224/S4d-225; **S4d-231 (accepted 2026-06-29, commit `278d585`) added the iOS empty-store builder** in `:shared/iosMain` (`BundledSQLiteDriver`, `Dispatchers.Default` query context, `NSDocumentDirectory` overload) and an iOS simulator roundtrip over `TemplateRepository`/`TemplateEditor`; **S4d-232 (accepted 2026-06-29, commit `44be9f7`) added iOS locale-aware seed packaging** by bundling byte-identical Android seed DBs into `iosApp.app`, loading them with `NSBundle`, and seed-copying to `$NSDocumentDirectory/ewm-db` before Room opens on first creation. Swift Templates UI remains deferred. **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.
4949
- **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.
50-
- **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 seed-DB packaging/default-template parity remains deferred; Desktop locale-aware seeding landed in S4d-225 (English default landed in S4d-224) (S4d-90..S4d-92); the full Compose Desktop editor beyond the current minimal product-flow window; Desktop renderer feature parity (Desktop reactive preview landed in S4d-198); and the remaining real PHPicker grid-cell proof. Android text/icon/composition/decode production remains native by default. KMP/CMP migration is NOT complete.
50+
- **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 Swift Templates UI remains deferred after S4d-231/S4d-232 completed builder + seed packaging; Desktop locale-aware seeding landed in S4d-225 (English default landed in S4d-224) (S4d-90..S4d-92); the full Compose Desktop editor beyond the current minimal product-flow window; Desktop renderer feature parity (Desktop reactive preview landed in S4d-198); and the remaining real PHPicker grid-cell proof. Android text/icon/composition/decode production remains native by default. KMP/CMP migration is NOT complete.
5151

5252
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.
5353

0 commit comments

Comments
ย (0)