Skip to content

Commit 343b86d

Browse files
committed
Record Desktop packaging closeout
1 parent ccc9f3f commit 343b86d

4 files changed

Lines changed: 13 additions & 1 deletion

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ EasyWatermark (`me.rosuh.easywatermark`) — a privacy-focused Android app that
4040
- **Desktop minimal product flow (S4d-119 readiness -> S4d-130, accepted 2026-06-27):** S4d-119 proved `:desktopApp` was still a CLI witness and selected the smallest non-speculative Desktop product-flow path. S4d-120 added desktop `createWaterMarkDataStore(...)`, a Desktop `WaterMarkRepository`/`WatermarkConfigEditor` roundtrip test, and a headless **open -> edit -> render -> save** flow writing `desktopApp/build/s4d120-desktop-headless/watermarked.png` (or CLI output path). S4d-121 added the minimal Compose Desktop `Window`: no-arg `:desktopApp:run` opens `EasyWatermark — Desktop (S4d-121)` and the sample button runs the same save spine; `--headless` remains the bounded automation path. **S4d-122 made Desktop text rendering and `composeOverRealImage` consume persisted `WaterMark.textColor` + `TextTypeface`** (color now defaults to shared amber `WaterMark.default.textColor`, and typeface maps to Compose `FontWeight`/`FontStyle`) and passed `TextPaintStyle` through. **S4d-123 completed shared drawStyle threading**, so `TextPaintStyle.Stroke` is raster-honored on Desktop/iOS Skiko; Android text production remains native. **S4d-124 selected the next Desktop slice, and S4d-125 added the first interactive file input:** the Desktop window now has an **"Open image..."** button using native JDK/AWT `FileDialog` to pick a real image, read bytes off the UI thread, and call the existing `DesktopWatermarkFlow.runSaveFlow(inputBytes, inputLabel)` spine. Cancel is a no-op; `--headless` stays unchanged. **S4d-126/S4d-130 split output-format safely:** S4d-127 added the Desktop encoder primitive (`ImageFormat.PNG` byte-identical `encodePng`; `ImageFormat.JPEG` ARGB -> opaque RGB flatten + JDK `ImageIO` quality), with `composeOverRealImage` still defaulting PNG for composer goldens. S4d-128 then wired the Desktop save spine to `UserConfigRepository.userPreferences.first()`: empty Desktop stores now follow shared/Android defaults (`JPEG/80`) and default output paths become format-aware (`watermarked.jpg` or `watermarked.png`), while PNG remains available when prefs select PNG. S4d-130 added the minimal window selector: two preset buttons (`JPEG / 80`, `PNG / 100`) write through the shared `OutputPrefsEditor` over the same store the save flow reads. **Still not a full Desktop editor and not 1:1 parity:** no drag/drop, interactive image preview, templates, share substitute, icon watermark, arbitrary quality selector, or final Android v2.10.0 UI parity.
4141
- **Desktop product-flow continuation (S4d-132 -> S4d-143b, accepted 2026-06-27/28):** S4d-132..S4d-137 completed the Desktop icon/source mini-epic at functional-code level: icon composition primitive, persisted Image-mode save branch, "Open icon...", "Use text watermark", and remembered source-image reuse. S4d-138/S4d-140 then added the release-code map, `DesktopSaveDecision` harness, and destination-only "Save as..." control. **S4d-141..S4d-143b closed the first templates persistence lane:** Desktop has an empty-store Room builder (`buildTemplateDatabase(dir)` with `BundledSQLiteDriver`), a `:desktopApp --headless` TemplateRepository/TemplateEditor witness, and a read-only proof that both Android seed DBs match generated Room identity hash `72366f557b971b39675d0f26cbc46e0a`. Seed packaging via `createFromFile` is technically viable but intentionally deferred until default templates or a visible templates UI become product priority. **Still not a full Desktop editor and not 1:1 parity:** no drag/drop, interactive image preview/editor, templates UI, share substitute, arbitrary quality selector, batch, or final Android v2.10.0 UI parity.
4242
- **Desktop window controls + share substitute + drag/drop + templates UI (S4d-145 -> S4d-160, accepted 2026-06-28):** Desktop window enrichment stayed one-file and functional: manual watermark text control, preview output, degree/color/opacity/gap/text-size/tile/typeface/style controls, scrolling, a no-new-dependency share substitute, dropped-image input, and a minimal Templates section. The share substitute tracks only the last successful real save from Render & Save / Save as / Open image / image drop; Preview temp output is deliberately not share state. Drag/drop uses Compose Desktop `dragAndDropTarget` + AWT file-list flavor and reuses the Open-image save spine. **S4d-159** chose templates UI over reactive preview because `Template.content` is just watermark text; reactive preview would reverse the deliberate manual-preview contract. **S4d-160** added Save current text / list / Use / Delete over `buildTemplateDatabase(dir)` + `TemplateRepository` + `TemplateEditor`; Use applies via `WatermarkConfigEditor.updateText` and still requires manual Preview/Render. **Still not 1:1 parity:** reactive preview/editor polish, template edit-in-place/seed defaults, batch, and final Android v2.10.0 screenshot/recording parity remain separate gates.
43+
- **Desktop release hygiene + packaging proof (S4d-161 -> S4d-163, 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`. **Overclaim guard:** unsigned app-image proof ONLY — no installer formats (DMG/PKG/MSI/DEB), signing/notarization, app icon, CI packaging gate, or real product-version source yet (`packageVersion = "1.0.0"` is a placeholder); still not Android v2.10.0 1:1 UI/UX parity.
4344
- 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).
4445
- **`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**.
4546
- **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.

findings.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Compose Migration Findings
22

3-
## Desktop product flow should prove the spine before widening the window (S4d-119..160, 2026-06-27/28)
3+
## Desktop product flow should prove the spine before widening the window (S4d-119..163, 2026-06-27/28)
44

55
- **A headless product spine is less speculative than a first Compose Desktop dependency.** S4d-119 found `:desktopApp` was still a console witness, so the smallest real Desktop product step was not "add a window"; it was to prove the same core path a window will later drive: persistent watermark config -> shared editor update -> persisted readback -> real-image render -> saved output. S4d-120 did exactly that without a build-file change, making the future `Window` slice a thin UI wrapper over a proven flow instead of a UI+storage+render bundle.
66
- **Desktop off-Android `WaterMarkRepository` uses the same edge pattern as iOS.** Fresh Desktop stores should inject platform-neutral edges: default text string, pure `WatermarkTileMode.fromStorageId` (Android's SDK-gated legacy DECAL-id-3 -> REPEAT mapper is Android-only legacy-data handling), and a simple logger. `createWaterMarkDataStore(dir, name = WaterMarkRepository.SP_NAME)` mirrors `createUserConfigDataStore`, stays a plain function (not `expect`/`actual`), and leaves Android byte-faithful `Context`+migration store creation untouched.
@@ -12,6 +12,7 @@
1212
- **Desktop flow glue now has the no-dependency harness it needed before more logic widening.** S4d-138 found `:desktopApp` has no test source set; the window/flow glue was compile + `--headless` witnessed only, while `shared/src/desktopTest` covers renderer/encode/decode/DataStore primitives structurally. S4d-139 took the lazy route: extract only the pure `runSaveFlow` decisions into `desktopMain` (`DesktopSaveDecision`) and test them from existing `desktopTest`. S4d-140 then correctly stayed a window destination slice: it calls existing `runSaveFlow(outputFile = target)` and does not widen flow/shared logic. Do not add a `:desktopApp` test source set or new test dependency for this lane yet; keep the next Desktop slice read-only templates readiness before changing Room/templates code.
1313
- **Desktop templates persistence is proven for empty stores; seed packaging is viable but optional.** S4d-141 mapped the templates path after Save-As and correctly split it into empty builder -> app witness -> seed decision. S4d-142 added the Desktop Room builder with `BundledSQLiteDriver` and tested `TemplateRepository`/`TemplateEditor` add/list/delete on an empty DB; S4d-143a proved the same path from `:desktopApp --headless` after adding the needed app-level `room.runtime` declaration. S4d-143b proved both Android seed DBs and generated Room share identity hash `72366f557b971b39675d0f26cbc46e0a`, so reuse is technically viable. S4d-160 surfaced the empty-store path in the Desktop window; seed packaging still waits for default-template parity/product priority, not merely because the UI can list templates.
1414
- **Shared renderer fixes should land once, below platform windows.** S4d-122 discovered that commonMain `WatermarkCellComposer.composeTextCell` called `multiParagraph.paint(canvas, content.color)`, dropping `TextStyle.drawStyle`, so Desktop and iOS `TextPaintStyle.Stroke` were value-wired but still painted like Fill. S4d-123 fixed that at the commonMain paint call and tightened Desktop/iOS stroke tests. Lesson: when a field is already mapped at both platform edges, fix the shared renderer instead of adding another desktopApp/window workaround.
15+
- **Desktop packaging is proven as an unsigned app image, and needs a supported (non-Homebrew) JDK.** S4d-162 finished the release hygiene (release-facing window title; blank-template-save guard). S4d-163 then made `:desktopApp` minimally packageable by swapping the plain Gradle `application` plugin for the Compose Desktop `compose.desktop { application { … nativeDistributions { … } } }` DSL (one build file; reuses the already-applied `org.jetbrains.compose` plugin — no new dependency). The chief risk was the `run` task: the Compose DSL re-provides it and forwards `--args`, so `:desktopApp:run --args='--headless'` stays the witness gate. **Toolchain caveat:** `:desktopApp:createDistributable` fails under the jenv/Homebrew OpenJDK (Compose Desktop's `checkRuntime` rejects Homebrew vendors, CMP issue #3107) and succeeds under the already-installed Amazon Corretto 17 — use a supported packaging JDK; do **not** bypass with `compose.desktop.packaging.checkJdkVendor=false`. Keep claims narrow: this is an unsigned `.app` image proof only — installer formats, signing/notarization, app icon, a real `packageVersion` source, and a CI packaging gate are separate later slices, and none of this is Android v2.10.0 1:1 parity.
1516

1617
## First off-Android consumer of the shared watermark editor: iOS watermark editor fields (S4d-101..S4d-113, 2026-06-27)
1718

progress.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Progress Log
22

3+
## 2026-06-28 — S4d-162/S4d-163 Desktop release hygiene + packaging proof accepted
4+
5+
- **S4d-161 (read-only decision, accepted):** chose Desktop release-grade hygiene + a packaging proof over speculative UI widening (reactive preview, template edit-in-place, batch were deferred). Evidence: no Compose Desktop packaging task existed, and the window title still carried a dev marker.
6+
- **S4d-162 (implementation, accepted; commit `bb5e770f Polish Desktop release hygiene`, `DesktopWindow.kt` only):** the window title is now release-facing `EasyWatermark — Desktop` (dropped the `(S4d-121)` marker), and the Templates "Save current text" button is disabled when the watermark text is blank. Headless witnesses unchanged.
7+
- **S4d-163 (implementation, accepted; commit `ccc9f3f7 Add Desktop packaging proof`, `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. `createDistributable`/`runDistributable`/`packageDistributionForCurrentOS` now exist; `:desktopApp:run --args='--headless'` still works (the Compose `run` forwards `--args`); `createDistributable` builds an unsigned `EasyWatermark.app` (native launcher + `Info.plist` + bundled JRE + app jars) under `desktopApp/build/compose/binaries/main/app/`.
8+
- **JDK caveat (recorded honestly):** `createDistributable` fails under the jenv/Homebrew OpenJDK (Compose Desktop's `checkRuntime` rejects Homebrew vendors — CMP #3107) and succeeds under the already-installed Amazon Corretto 17 (`/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home`). The accepted path is a supported packaging JDK; `compose.desktop.packaging.checkJdkVendor=false` was NOT used. `packageVersion = "1.0.0"` is a placeholder.
9+
- **Scope guard (no overclaim):** unsigned app-image proof only — no installer formats, signing/notarization, app icon, CI packaging gate, real product-version source, or Android v2.10.0 1:1 UI/UX parity.
10+
- **Next:** Desktop is functionally complete (picker/drop -> edit -> preview -> save/save-as -> share substitute -> templates) AND minimally packageable. Remaining gaps: packaging hardening (real version, installer formats, signing, app icon, CI gate), reactive preview/editor polish, template edit-in-place/default seeding, batch, and final Android v2.10.0 screenshot/recording parity after release-grade migration.
11+
312
## 2026-06-28 — S4d-159/S4d-160 Desktop templates UI accepted
413

514
- **S4d-159 (read-only decision, accepted; ACSP `20260628-052647...`):** chose a minimal Desktop templates UI over reactive/live preview. Evidence: Android `Template.content` is watermark text applied through `updateText`, Desktop already had `buildTemplateDatabase`, `TemplateRepository`, and `TemplateEditor`, while reactive preview would reverse the current manual Preview/Render contract.

0 commit comments

Comments
 (0)