From 42a1183b6f81d5378ab3cc150f8bc3d2adf9bf5c Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 8 Jun 2026 14:42:37 +0200 Subject: [PATCH] docs: add Plan 01 (Foundation & CI) implementation plan First of an 8-plan sequence to build V1. Plan 01 covers the buildable Android project scaffold: Gradle setup, Hilt, DataStore, Material 3 Expressive theme, adaptive launcher icon (statische "1" on slate squircle, referencing kalendae), DE+EN i18n infrastructure, ColorScheme unit tests, smoke UI test, Gitea CI workflow, F-Droid release workflow, F-Droid metadata stubs, and .planning/ project-tracking documents. 14 tasks, each ending in a commit. Output is a working APK with green CI before any feature code is written. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plans/2026-06-08-01-foundation.md | 2047 +++++++++++++++++ 1 file changed, 2047 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-08-01-foundation.md diff --git a/docs/superpowers/plans/2026-06-08-01-foundation.md b/docs/superpowers/plans/2026-06-08-01-foundation.md new file mode 100644 index 0000000..6c8d9dd --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-01-foundation.md @@ -0,0 +1,2047 @@ +# Calendula - Plan 01: Foundation & CI Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Liefere ein vollständig konfiguriertes Android-Projektgerüst für Calendula: build-/install-/testbar, mit Material 3 Expressive Theme, korrektem Adaptive-Icon, DE/EN-i18n-Grundgerüst, Hilt, DataStore, und grünen Gitea-CI-Workflows. Am Ende läuft die App und zeigt einen schlichten "Calendula"-Screen mit korrekt themed Surface, das Icon erscheint im Launcher als "1" auf Slate-Squircle, und `./gradlew test lint assembleDebug` ist grün. + +**Architecture:** Single Gradle module `:app`, Kotlin 2.1 + Jetpack Compose mit Material 3 Expressive (1.5+), Hilt für DI, DataStore-Preferences für App-Settings. Build via Gradle Kotlin DSL und Version Catalog (`libs.versions.toml`). CI über Gitea Workflows (adaptiert von HouseHoldKeaper, Flutter-Steps durch Gradle-Steps ersetzt). Alle Resourcen + Strings i18n-fähig ab Tag 1 (DE + EN). + +**Tech Stack:** +- Kotlin 2.1.0, Gradle 8.11.1, AGP 8.7.2, JVM Target 17 +- minSdk 29, targetSdk 36, compileSdk 36 +- Jetpack Compose BOM 2025.05.00, Material3 1.5.0 (Expressive APIs) +- Hilt 2.53 + KSP 2.1.0-1.0.29 +- AndroidX DataStore Preferences 1.1.1 +- Tests: JUnit5 5.11.x + Truth 1.4.4 + Compose UI Test (BOM) +- CI: Gitea Actions auf Docker-Runner (Java 17 + Android SDK 36) + +--- + +## File Structure + +Files this plan creates (all relative to project root `/home/jlmak/Projects/jlmak/cal/`): + +**Repo-Meta:** +- `LICENSE` — MIT +- `README.md` — Short app description + build instructions +- `CHANGELOG.md` — Started with `## [Unreleased]` section +- `.gitignore` — Android Studio + JetBrains + Kotlin/Gradle +- `.gitattributes` — Line ending normalization +- `.editorconfig` — Code style + +**.planning/ (project tracking docs, HouseHoldKeaper convention):** +- `.planning/PROJECT.md` — high-level summary +- `.planning/REQUIREMENTS.md` — feature requirements (validated/active/out-of-scope) +- `.planning/ROADMAP.md` — V1/V2/V3 milestones +- `.planning/STATE.md` — current implementation status + +**F-Droid metadata:** +- `fdroid-metadata/de.jeanlucmakiola.calendula.yml` +- `fdroid-metadata/de.jeanlucmakiola.calendula/en-US/short_description.txt` +- `fdroid-metadata/de.jeanlucmakiola.calendula/en-US/full_description.txt` +- `fdroid-metadata/de.jeanlucmakiola.calendula/de/short_description.txt` +- `fdroid-metadata/de.jeanlucmakiola.calendula/de/full_description.txt` + +**Gradle root:** +- `settings.gradle.kts` +- `build.gradle.kts` +- `gradle.properties` +- `gradle/libs.versions.toml` +- `gradle/wrapper/gradle-wrapper.properties` (jar generated by `gradle wrapper`) + +**App module:** +- `app/.gitignore` +- `app/build.gradle.kts` +- `app/proguard-rules.pro` +- `app/src/main/AndroidManifest.xml` +- `app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt` +- `app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt` +- `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Color.kt` +- `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt` +- `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Type.kt` + +**Resources:** +- `app/src/main/res/values/strings.xml` (English master) +- `app/src/main/res/values-de/strings.xml` (German) +- `app/src/main/res/values/colors.xml` +- `app/src/main/res/values/themes.xml` +- `app/src/main/res/drawable/ic_launcher_background.xml` +- `app/src/main/res/drawable/ic_launcher_foreground.xml` +- `app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml` +- `app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml` + +**Tests:** +- `app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/ColorSchemeTest.kt` +- `app/src/androidTest/java/de/jeanlucmakiola/calendula/MainActivitySmokeTest.kt` + +**CI:** +- `.gitea/workflows/ci.yaml` +- `.gitea/workflows/release.yaml` + +--- + +## Task 1: Repo Meta Files (LICENSE, README, CHANGELOG, gitignore) + +**Files:** +- Create: `LICENSE` +- Create: `README.md` +- Create: `CHANGELOG.md` +- Create: `.gitignore` +- Create: `.gitattributes` +- Create: `.editorconfig` + +- [ ] **Step 1: Create `LICENSE` (MIT)** + +``` +MIT License + +Copyright (c) 2026 Jean-Luc Makiola + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +- [ ] **Step 2: Create `README.md`** + +```markdown +# Calendula + +A modern Material 3 Expressive calendar app for Android. + +Calendula is named after the flower of the same name, whose name comes from +the Latin *kalendae* — the first day of the month — the same root as the +word "calendar". Calendula reads from Android's built-in `CalendarContract`, +so any calendar source synced to your device (CalDAV via DAVx5, Google, +local, WebCal subscriptions, ...) is shown. + +## Features (V1) + +- Month, Week, and Day views +- Read-only event details (write support comes in V2) +- Multi-calendar visibility toggle +- Material You Dynamic Color (Android 12+) +- Light/Dark theme follows system +- German + English UI + +## Building + +Requires Android SDK 36 and JDK 17. + +```bash +# Bootstrap the gradle wrapper (one-time, requires a host gradle install) +gradle wrapper --gradle-version 8.11.1 --distribution-type bin + +# Build debug APK +./gradlew assembleDebug + +# Run unit tests +./gradlew test + +# Run lint +./gradlew lint +``` + +## License + +[MIT](LICENSE) — Jean-Luc Makiola, 2026 +``` + +- [ ] **Step 3: Create `CHANGELOG.md`** + +```markdown +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Initial project scaffold with Material 3 Expressive theme +- Adaptive launcher icon (numeral "1" on slate squircle, referencing *kalendae*) +- German + English localization infrastructure +- Hilt + DataStore dependency injection and preferences setup +- Gitea CI/CD workflows for build, test, lint, and release +``` + +- [ ] **Step 4: Create `.gitignore`** + +``` +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log files +*.log + +# Android Studio / IntelliJ +*.iml +.idea/ +.navigation/ +captures/ +.externalNativeBuild/ +.cxx/ + +# Keystore files +*.jks +*.keystore +/key.properties + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# OS files +.DS_Store +Thumbs.db + +# F-Droid local artifacts (the pipeline generates them in CI) +fdroid/repo/ +fdroid/keystore.p12 + +# KSP +.ksp/ +``` + +- [ ] **Step 5: Create `.gitattributes`** + +``` +* text=auto eol=lf +*.bat text eol=crlf +*.jar binary +*.png binary +*.jpg binary +*.gif binary +*.webp binary +``` + +- [ ] **Step 6: Create `.editorconfig`** + +```ini +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,yaml,toml,json,md}] +indent_size = 2 + +[*.{kt,kts}] +ij_kotlin_packages_to_use_import_on_demand = unset + +[Makefile] +indent_style = tab +``` + +- [ ] **Step 7: Commit** + +```bash +git add LICENSE README.md CHANGELOG.md .gitignore .gitattributes .editorconfig +git commit -m "chore: add repo meta files (LICENSE, README, CHANGELOG, gitignore)" +``` + +--- + +## Task 2: Project Planning Docs (`.planning/`) + +Adopts the HouseHoldKeaper convention. These docs live alongside the spec and track higher-level project state. + +**Files:** +- Create: `.planning/PROJECT.md` +- Create: `.planning/REQUIREMENTS.md` +- Create: `.planning/ROADMAP.md` +- Create: `.planning/STATE.md` + +- [ ] **Step 1: Create `.planning/PROJECT.md`** + +```markdown +# Calendula + +## What This Is + +A modern Material 3 Expressive Android calendar app, read-only V1. Lives +entirely on top of Android's `CalendarContract` — any calendar synced to the +device (CalDAV via DAVx5, Google, local, WebCal, …) shows up automatically. +The differentiator is visual: real Material 3 Expressive design that no +existing FOSS calendar app delivers. + +## Core Value + +A calendar app that is genuinely pleasant to look at and use, without +re-inventing the calendar sync stack — leave that to DAVx5 and the system. + +## Current Milestone + +**v0.1 — Foundation & CI:** Buildable Android project scaffold with theme, +icon, i18n, Hilt, DataStore, green CI. + +## Stack + +Kotlin 2.1, Jetpack Compose + Material 3 Expressive, Hilt, DataStore. +Gradle Kotlin DSL with Version Catalog. + +Read-only V1, write support V2. + +Android-only (minSdk 29, targetSdk 36). No iOS. + +## Naming + +"Calendula" — Latin *kalendae* ("first day of the month", root of "calendar") +is also the etymological root of the marigold flower Calendula. The icon +shows a stylized "1" on a slate squircle. + +## Source + +Hosted on self-hosted Gitea, released through self-hosted F-Droid repo on +Hetzner. Same infrastructure as `HouseHoldKeaper`. +``` + +- [ ] **Step 2: Create `.planning/REQUIREMENTS.md`** + +```markdown +# Calendula — Requirements + +See full design spec: `docs/superpowers/specs/2026-06-08-calendar-app-design.md` + +## V1 Scope (Variant "B") + +### Validated (shipped) +- (none yet — first milestone in progress) + +### Active (V1) + +- [ ] Foundation & CI infrastructure +- [ ] Data Layer over `CalendarContract` +- [ ] Permission flow (`READ_CALENDAR`) +- [ ] Month view (S1) +- [ ] Week view (S2) +- [ ] Day view (S3) +- [ ] Event Detail Sheet (S4) +- [ ] Multi-Calendar Filter (M3) +- [ ] Today button + Jump-to-Date (M2) +- [ ] View-Switcher (M1) +- [ ] Settings screen (M4) +- [ ] Empty / no-permission / no-calendars states +- [ ] German + English localization +- [ ] Loading/Failure/Success states per screen (architectural pattern) + +### Out of Scope (V2+) + +- Event create / edit / delete (V2) +- Home-screen widget +- Full-text search +- Quick-add +- Custom notifications/reminders (system already handles these) +- Tablet/foldable-specific layouts +- iOS support (Android-only by design) + +## Constraints + +- **Tech stack:** Kotlin + Jetpack Compose + Material 3 Expressive, Hilt, DataStore +- **Platform:** Android 10+ (API 29 minimum), Android 16 (API 36) target +- **Offline-first:** all data lives in `CalendarContract`; no app-side network +- **Privacy:** zero telemetry, no analytics +- **i18n:** German + English from day one +- **Tests + CI from day one** +- **License:** MIT +``` + +- [ ] **Step 3: Create `.planning/ROADMAP.md`** + +```markdown +# Calendula — Roadmap + +## v0.x — Pre-Release + +| Version | Milestone | Status | +|---|---|---| +| v0.1 | Foundation & CI | in progress | +| v0.2 | Data Layer & Permission Flow | pending | +| v0.3 | Month view | pending | +| v0.4 | Week view | pending | +| v0.5 | Day view | pending | +| v0.6 | Event Detail Sheet | pending | +| v0.7 | Filter & Settings | pending | + +## v1.0 — First Public Release + +All V1 features shipped, polished, on F-Droid. Read-only calendar. + +## v2.0 — Write Support + +- Event create / edit / delete via `CalendarContract` writes +- Quick-add sheet +- Conflict UX (event modified externally during edit) + +## v3.0 — Power-User Features + +- Home-screen widget +- Full-text search +- Tablet / foldable layouts +- Optional: ICS file import (drag-and-drop) + +Order is indicative — community feedback after V1 may re-prioritize. +``` + +- [ ] **Step 4: Create `.planning/STATE.md`** + +```markdown +# Calendula — Current State + +*Last updated: 2026-06-08* + +## Status + +**Milestone:** v0.1 — Foundation & CI +**Phase:** Implementation starting + +## Progress + +- [x] Design spec written and committed (`docs/superpowers/specs/2026-06-08-calendar-app-design.md`) +- [x] V1 design decisions resolved (App name "Calendula", icon, seed color) +- [x] Plan 01 written (`docs/superpowers/plans/2026-06-08-01-foundation.md`) +- [ ] Plan 01 executed + +## Next + +1. Execute Plan 01 to land foundation +2. Write and execute Plan 02 (Data Layer & Permission Flow) +3. Iterate on UI design (mockups) before screens are built +``` + +- [ ] **Step 5: Commit** + +```bash +git add .planning/ +git commit -m "docs: add .planning/ project-tracking documents" +``` + +--- + +## Task 3: F-Droid Metadata Stub + +**Files:** +- Create: `fdroid-metadata/de.jeanlucmakiola.calendula.yml` +- Create: `fdroid-metadata/de.jeanlucmakiola.calendula/en-US/short_description.txt` +- Create: `fdroid-metadata/de.jeanlucmakiola.calendula/en-US/full_description.txt` +- Create: `fdroid-metadata/de.jeanlucmakiola.calendula/de/short_description.txt` +- Create: `fdroid-metadata/de.jeanlucmakiola.calendula/de/full_description.txt` + +- [ ] **Step 1: Create `fdroid-metadata/de.jeanlucmakiola.calendula.yml`** + +```yaml +AuthorName: Jean-Luc Makiola +License: MIT +Name: Calendula + +Categories: + - Time + +SourceCode: https://gitea.jeanlucmakiola.de/makiolaj/calendula +IssueTracker: https://gitea.jeanlucmakiola.de/makiolaj/calendula/issues +``` + +- [ ] **Step 2: Create `fdroid-metadata/de.jeanlucmakiola.calendula/en-US/short_description.txt`** + +``` +A modern Material 3 Expressive calendar for Android. +``` + +- [ ] **Step 3: Create `fdroid-metadata/de.jeanlucmakiola.calendula/en-US/full_description.txt`** + +``` +Calendula is a modern, open-source calendar app for Android. It reads from +the system calendar provider, so any source synced to your device — Nextcloud +via DAVx5, Google, local, WebCal subscriptions — shows up automatically. + +The differentiator is the design: real Material 3 Expressive throughout, with +dynamic color, expressive motion, and expressive shapes. + +V1 is read-only. Event creation, editing, and deletion are planned for V2. + +Privacy: zero telemetry, no analytics, no network access — your data never +leaves the device. +``` + +- [ ] **Step 4: Create `fdroid-metadata/de.jeanlucmakiola.calendula/de/short_description.txt`** + +``` +Ein moderner Material-3-Expressive-Kalender für Android. +``` + +- [ ] **Step 5: Create `fdroid-metadata/de.jeanlucmakiola.calendula/de/full_description.txt`** + +``` +Calendula ist eine moderne, quelloffene Kalender-App für Android. Sie liest +direkt aus dem System-Kalender-Provider — jede Quelle, die mit deinem Gerät +synchronisiert ist (Nextcloud über DAVx5, Google, lokal, WebCal-Subscriptions) +erscheint automatisch. + +Der Unterschied liegt im Design: echtes Material 3 Expressive durchgehend, +mit Dynamic Color, expressiven Animationen und neuen Shape-Sprachen. + +V1 ist read-only. Erstellen, Bearbeiten und Löschen von Events kommt mit V2. + +Datenschutz: keinerlei Telemetrie, kein Tracking, kein Netzwerkzugriff — deine +Daten bleiben auf dem Gerät. +``` + +- [ ] **Step 6: Commit** + +```bash +git add fdroid-metadata/ +git commit -m "chore: add F-Droid metadata for de.jeanlucmakiola.calendula" +``` + +--- + +## Task 4: Gradle Root Setup + +**Files:** +- Create: `settings.gradle.kts` +- Create: `build.gradle.kts` +- Create: `gradle.properties` +- Create: `gradle/libs.versions.toml` + +- [ ] **Step 1: Create `settings.gradle.kts`** + +```kotlin +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "Calendula" +include(":app") +``` + +- [ ] **Step 2: Create `gradle/libs.versions.toml`** + +```toml +[versions] +agp = "8.7.2" +kotlin = "2.1.0" +ksp = "2.1.0-1.0.29" +hilt = "2.53" +coreKtx = "1.15.0" +lifecycleRuntime = "2.8.7" +activityCompose = "1.9.3" +composeBom = "2025.05.00" +material3 = "1.5.0" +datastore = "1.1.1" +junit = "5.11.4" +junitJupiterPlatform = "1.11.4" +truth = "1.4.4" +androidxJunit = "1.2.1" +espressoCore = "3.6.1" + +[libraries] +# AndroidX core +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntime" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } + +# Compose BOM + libs +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } + +# Material 3 (Expressive lives in this artifact for 1.5+) +androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } + +# Hilt +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } + +# DataStore +androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } + +# Unit tests +junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } +junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" } +junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junitJupiterPlatform" } +truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } + +# Android tests +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +``` + +- [ ] **Step 3: Create `build.gradle.kts` (root)** + +```kotlin +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.hilt) apply false +} +``` + +- [ ] **Step 4: Create `gradle.properties`** + +```properties +# Project-wide Gradle settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 + +# Required for AndroidX. +android.useAndroidX=true + +# Kotlin code style for this project: "official" or "obsolete". +kotlin.code.style=official + +# Enables namespacing of each library's R class, less RAM, faster builds. +android.nonTransitiveRClass=true + +# Use new K2 compiler defaults (Kotlin 2.x). +kotlin.incremental=true + +# Reproducible builds for F-Droid. +android.uniquePackageNames=true +``` + +- [ ] **Step 5: Bootstrap the Gradle wrapper** + +This requires a host `gradle` installation. Run once: + +```bash +gradle wrapper --gradle-version 8.11.1 --distribution-type bin +``` + +Expected: creates `gradle/wrapper/gradle-wrapper.jar`, `gradle/wrapper/gradle-wrapper.properties`, `gradlew`, `gradlew.bat`. + +Then make `gradlew` executable: + +```bash +chmod +x gradlew +``` + +- [ ] **Step 6: Verify gradle wrapper works** + +```bash +./gradlew --version +``` + +Expected output includes: +``` +Gradle 8.11.1 +Kotlin: 2.0.x +``` + +(Don't worry about the specific Kotlin version in the gradle output — that's gradle's bundled Kotlin, separate from our project Kotlin 2.1.0.) + +- [ ] **Step 7: Commit** + +```bash +git add settings.gradle.kts build.gradle.kts gradle.properties gradle/ +chmod +x gradlew && git add gradlew gradlew.bat +git commit -m "chore: add gradle wrapper + version catalog + root build" +``` + +--- + +## Task 5: App Module Gradle Setup + +**Files:** +- Create: `app/.gitignore` +- Create: `app/build.gradle.kts` +- Create: `app/proguard-rules.pro` + +- [ ] **Step 1: Create `app/.gitignore`** + +``` +/build +``` + +- [ ] **Step 2: Create `app/proguard-rules.pro`** + +``` +# Keep Hilt-generated classes +-keep class dagger.hilt.** { *; } +-keep class * extends dagger.hilt.android.HiltAndroidApp + +# Compose Compiler may keep its own; defaults are fine +-dontwarn org.jetbrains.annotations.** +``` + +- [ ] **Step 3: Create `app/build.gradle.kts`** + +```kotlin +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) +} + +android { + namespace = "de.jeanlucmakiola.calendula" + compileSdk = 36 + + defaultConfig { + applicationId = "de.jeanlucmakiola.calendula" + minSdk = 29 + targetSdk = 36 + versionCode = 1 + versionName = "0.1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { useSupportLibrary = true } + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + debug { + applicationIdSuffix = ".debug" + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + + testOptions { + unitTests.all { test -> + test.useJUnitPlatform() + } + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + + implementation(libs.androidx.datastore.preferences) + + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + testImplementation(libs.junit.jupiter.api) + testRuntimeOnly(libs.junit.jupiter.engine) + testRuntimeOnly(libs.junit.platform.launcher) + testImplementation(libs.truth) + + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) +} +``` + +- [ ] **Step 4: Sync gradle** + +```bash +./gradlew help +``` + +Expected: Build succeeds. No project setup errors. Some dependency download output is normal on first run. + +- [ ] **Step 5: Commit** + +```bash +git add app/.gitignore app/build.gradle.kts app/proguard-rules.pro +git commit -m "chore: configure :app module gradle build" +``` + +--- + +## Task 6: Android Manifest + Base Resources + +**Files:** +- Create: `app/src/main/AndroidManifest.xml` +- Create: `app/src/main/res/values/strings.xml` +- Create: `app/src/main/res/values-de/strings.xml` +- Create: `app/src/main/res/values/colors.xml` +- Create: `app/src/main/res/values/themes.xml` + +- [ ] **Step 1: Create `app/src/main/AndroidManifest.xml`** + +```xml + + + + + + + + + + + + + + + +``` + +- [ ] **Step 2: Create backup-rules placeholder XML files** + +Create `app/src/main/res/xml/backup_rules.xml`: + +```xml + + + + +``` + +Create `app/src/main/res/xml/data_extraction_rules.xml`: + +```xml + + + + + + + + +``` + +- [ ] **Step 3: Create `app/src/main/res/values/strings.xml` (English master)** + +```xml + + Calendula + A modern calendar. + + + Loading… + Retry + Something went wrong. + Calendar access is required. + Grant access + No calendars configured. + Open system calendar settings + Could not read the calendar. + +``` + +- [ ] **Step 4: Create `app/src/main/res/values-de/strings.xml`** + +```xml + + Calendula + Ein moderner Kalender. + + Lädt… + Erneut versuchen + Etwas ist schiefgelaufen. + Zugriff auf den Kalender wird benötigt. + Zugriff erlauben + Keine Kalender eingerichtet. + System-Kalender-Einstellungen öffnen + Kalender konnte nicht gelesen werden. + +``` + +- [ ] **Step 5: Create `app/src/main/res/values/colors.xml`** + +```xml + + + #FF5C6B7A + + #FF5C6B7A + +``` + +- [ ] **Step 6: Create `app/src/main/res/values/themes.xml`** (minimal stub — Compose handles real theming) + +```xml + + + +``` + +- [ ] **Step 7: Commit** + +```bash +git add app/src/main/AndroidManifest.xml app/src/main/res/ +git commit -m "feat: add android manifest, strings (DE+EN), colors, base theme" +``` + +--- + +## Task 7: Adaptive Launcher Icon — Static "1" on Slate Squircle + +**Files:** +- Create: `app/src/main/res/drawable/ic_launcher_background.xml` +- Create: `app/src/main/res/drawable/ic_launcher_foreground.xml` +- Create: `app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml` +- Create: `app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml` + +The adaptive icon is 108dp × 108dp, with the inner 72dp safe zone visible (mask applied by launcher). Foreground is centered on a 432dp viewport when designing in vector terms (factor 4× over 108dp). + +- [ ] **Step 1: Create `app/src/main/res/drawable/ic_launcher_background.xml`** + +A solid slate color background: + +```xml + + + + +``` + +- [ ] **Step 2: Create `app/src/main/res/drawable/ic_launcher_foreground.xml`** + +A bold "1" centered in the 108dp × 108dp viewport, sized to stay inside the 66dp inner safe zone (launcher masks may crop further). The "1" is drawn as a single-color path with a slight serif foot. + +```xml + + + + + +``` + +- [ ] **Step 3: Create `app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml`** + +```xml + + + + + + +``` + +- [ ] **Step 4: Create `app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml`** + +```xml + + + + + + +``` + +- [ ] **Step 5: Verify build still works** + +```bash +./gradlew assembleDebug +``` + +Expected: BUILD SUCCESSFUL. The APK is in `app/build/outputs/apk/debug/`. + +> Note: The "1" path above is a simple approximation. The icon will be visually refined during the later UI-design iteration; this is the "ships" version that establishes shape, color, and meaning correctly. + +- [ ] **Step 6: Commit** + +```bash +git add app/src/main/res/drawable/ic_launcher_background.xml \ + app/src/main/res/drawable/ic_launcher_foreground.xml \ + app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml \ + app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +git commit -m "feat: adaptive launcher icon — '1' on slate squircle (kalendae)" +``` + +--- + +## Task 8: Compose Theme (Color, Theme, Typography) + Unit Test + +**Files:** +- Create: `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Color.kt` +- Create: `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt` +- Create: `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Type.kt` +- Create: `app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/ColorSchemeTest.kt` + +- [ ] **Step 1: Write the failing color scheme test** + +Create `app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/ColorSchemeTest.kt`: + +```kotlin +package de.jeanlucmakiola.calendula.ui.theme + +import androidx.compose.ui.graphics.Color +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +class ColorSchemeTest { + + @Test + fun `seed color matches design spec slate`() { + // The seed color must remain stable - the design is anchored to it. + // Change this only if the spec is updated. + assertThat(CalendulaSeed).isEqualTo(Color(0xFF5C6B7A)) + } + + @Test + fun `light fallback scheme uses seed as primary derivation source`() { + val scheme = CalendulaLightFallback + // Primary should be a recognizable derivative of the seed (not neutral gray) + assertThat(scheme.primary).isNotEqualTo(Color.Black) + assertThat(scheme.primary).isNotEqualTo(Color.White) + } + + @Test + fun `dark fallback scheme differs from light`() { + assertThat(CalendulaDarkFallback.background).isNotEqualTo(CalendulaLightFallback.background) + } +} +``` + +- [ ] **Step 2: Run the failing test** + +```bash +./gradlew :app:testDebugUnitTest --tests "de.jeanlucmakiola.calendula.ui.theme.ColorSchemeTest" +``` + +Expected: FAIL with `Unresolved reference: CalendulaSeed` (and similar). Good — the test drives the API. + +- [ ] **Step 3: Create `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Color.kt`** + +```kotlin +package de.jeanlucmakiola.calendula.ui.theme + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +/** + * Seed color anchoring the entire palette. See spec section 12. + * Desaturated slate blue-gray; distinct from HouseHoldKeaper's sage. + */ +val CalendulaSeed: Color = Color(0xFF5C6B7A) + +/** + * Fallback light scheme used on devices that don't support dynamic color + * (API < 31) or when the user disables it. The real palette is derived by + * Material's tonal-palette generator from CalendulaSeed at runtime; these + * constants are a hand-picked subset that approximates that result. + */ +val CalendulaLightFallback = lightColorScheme( + primary = Color(0xFF3B5364), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFCBE6FA), + onPrimaryContainer = Color(0xFF001E2E), + secondary = Color(0xFF526070), + onSecondary = Color(0xFFFFFFFF), + background = Color(0xFFFBFCFE), + onBackground = Color(0xFF191C1F), + surface = Color(0xFFFBFCFE), + onSurface = Color(0xFF191C1F), +) + +val CalendulaDarkFallback = darkColorScheme( + primary = Color(0xFFA3CBE2), + onPrimary = Color(0xFF003348), + primaryContainer = Color(0xFF21495F), + onPrimaryContainer = Color(0xFFCBE6FA), + secondary = Color(0xFFB9C8DA), + onSecondary = Color(0xFF243240), + background = Color(0xFF101316), + onBackground = Color(0xFFE1E3E6), + surface = Color(0xFF101316), + onSurface = Color(0xFFE1E3E6), +) +``` + +- [ ] **Step 4: Run the test to verify it passes** + +```bash +./gradlew :app:testDebugUnitTest --tests "de.jeanlucmakiola.calendula.ui.theme.ColorSchemeTest" +``` + +Expected: 3 tests PASS. + +- [ ] **Step 5: Create `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Type.kt`** + +For V1 we use Compose Material 3 defaults. Refinement (custom font, expressive type scale) happens later in the UI-design iteration. + +```kotlin +package de.jeanlucmakiola.calendula.ui.theme + +import androidx.compose.material3.Typography + +/** + * Default Material 3 Expressive typography. Custom font + tuned scale will + * land in a later UI-design iteration; the defaults are intentional for V1 + * scaffolding to keep the foundation lean. + */ +val CalendulaTypography = Typography() +``` + +- [ ] **Step 6: Create `app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt`** + +```kotlin +package de.jeanlucmakiola.calendula.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +/** + * App theme. Honors: + * - System light/dark. + * - Dynamic Color on API 31+, else falls back to the hand-tuned scheme + * derived from [CalendulaSeed]. + * + * The Settings screen (later) can override useDynamicColor and themePreference, + * but the V1 foundation just follows the system. + */ +@Composable +fun CalendulaTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit, +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val ctx = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(ctx) else dynamicLightColorScheme(ctx) + } + darkTheme -> CalendulaDarkFallback + else -> CalendulaLightFallback + } + + MaterialTheme( + colorScheme = colorScheme, + typography = CalendulaTypography, + content = content, + ) +} +``` + +- [ ] **Step 7: Verify everything builds** + +```bash +./gradlew assembleDebug +``` + +Expected: BUILD SUCCESSFUL. + +- [ ] **Step 8: Commit** + +```bash +git add app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/ \ + app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/ +git commit -m "feat: M3 Expressive theme with dynamic color + fallback scheme from slate seed" +``` + +--- + +## Task 9: Application Class (Hilt entry point) + MainActivity + +**Files:** +- Create: `app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt` +- Create: `app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt` + +- [ ] **Step 1: Create `app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt`** + +```kotlin +package de.jeanlucmakiola.calendula + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +/** + * Application entry point. Registered as android:name=".CalendulaApp" + * in AndroidManifest.xml. Hilt initializes its component graph here. + */ +@HiltAndroidApp +class CalendulaApp : Application() +``` + +- [ ] **Step 2: Create `app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt`** + +```kotlin +package de.jeanlucmakiola.calendula + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import dagger.hilt.android.AndroidEntryPoint +import de.jeanlucmakiola.calendula.ui.theme.CalendulaTheme + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + CalendulaTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + PlaceholderScreen(modifier = Modifier.padding(innerPadding)) + } + } + } + } +} + +@Composable +private fun PlaceholderScreen(modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.displayMedium, + ) + Text( + text = stringResource(R.string.app_tagline), + style = MaterialTheme.typography.bodyLarge, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PlaceholderPreview() { + CalendulaTheme { PlaceholderScreen() } +} +``` + +- [ ] **Step 3: Build & install on a device or emulator (manual sanity check)** + +```bash +./gradlew installDebug +``` + +Expected: APK installs as "Calendula" with the slate "1" icon. Launching it shows "Calendula" + "A modern calendar." centered on themed background. + +> If you don't have a device handy, just `./gradlew assembleDebug` and move on; the UI test in Task 10 covers behavior. + +- [ ] **Step 4: Commit** + +```bash +git add app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt \ + app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt +git commit -m "feat: scaffold CalendulaApp + MainActivity with themed placeholder" +``` + +--- + +## Task 10: Smoke UI Test + +**Files:** +- Create: `app/src/androidTest/java/de/jeanlucmakiola/calendula/MainActivitySmokeTest.kt` + +- [ ] **Step 1: Write the failing UI test** + +```kotlin +package de.jeanlucmakiola.calendula + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MainActivitySmokeTest { + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @Test + fun appName_isDisplayed_onLaunch() { + composeTestRule.onNodeWithText("Calendula").assertIsDisplayed() + } + + @Test + fun tagline_isDisplayed_onLaunch() { + composeTestRule.onNodeWithText("A modern calendar.").assertIsDisplayed() + } +} +``` + +- [ ] **Step 2: Verify the test runs (requires emulator/device)** + +```bash +./gradlew :app:connectedDebugAndroidTest +``` + +Expected: 2 tests PASS. If no emulator is running, this step is informational only — the CI emulator runs it instead. + +> If running locally without an emulator: skip `connectedDebugAndroidTest` for now; CI will catch failures. + +- [ ] **Step 3: Commit** + +```bash +git add app/src/androidTest/java/de/jeanlucmakiola/calendula/MainActivitySmokeTest.kt +git commit -m "test: add UI smoke test for MainActivity placeholder" +``` + +--- + +## Task 11: CI Workflow (`.gitea/workflows/ci.yaml`) + +**Files:** +- Create: `.gitea/workflows/ci.yaml` + +- [ ] **Step 1: Create `.gitea/workflows/ci.yaml`** + +```yaml +name: CI + +on: + push: + branches: + - '**' + tags-ignore: + - '**' + pull_request: + +jobs: + ci: + runs-on: docker + env: + ANDROID_HOME: /opt/android-sdk + ANDROID_SDK_ROOT: /opt/android-sdk + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android SDK packages + run: | + sdkmanager --licenses >/dev/null <<'EOF' + y + y + y + y + y + y + y + y + y + y + EOF + sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0" + + - name: Install jq + run: | + set -e + SUDO="" + if command -v sudo >/dev/null 2>&1; then + SUDO="sudo" + fi + if command -v apt-get >/dev/null 2>&1; then + $SUDO apt-get update + $SUDO apt-get install -y jq + elif command -v apk >/dev/null 2>&1; then + $SUDO apk add --no-cache jq + fi + + - name: Setup Gradle cache + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle/libs.versions.toml') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Lint + run: ./gradlew lint --no-daemon + + - name: Unit tests + run: ./gradlew test --no-daemon + + - name: Assemble debug APK + run: ./gradlew assembleDebug --no-daemon + + - name: Trivy filesystem scan + run: | + set -e + SUDO="" + if command -v sudo >/dev/null 2>&1; then + SUDO="sudo" + fi + if command -v apt-get >/dev/null 2>&1; then + $SUDO apt-get update + $SUDO apt-get install -y wget apt-transport-https gnupg lsb-release + wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | $SUDO tee /usr/share/keyrings/trivy.gpg > /dev/null + echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | $SUDO tee /etc/apt/sources.list.d/trivy.list + $SUDO apt-get update + $SUDO apt-get install -y trivy + fi + trivy filesystem --severity HIGH,CRITICAL --exit-code 0 . + continue-on-error: true +``` + +- [ ] **Step 2: Commit** + +```bash +git add .gitea/workflows/ci.yaml +git commit -m "ci: add gitea CI workflow (lint, test, assemble, trivy)" +``` + +--- + +## Task 12: Release Workflow (`.gitea/workflows/release.yaml`) + +**Files:** +- Create: `.gitea/workflows/release.yaml` + +This adapts the HouseHoldKeaper release workflow for Gradle/Android. Keystore secrets (`KEYSTORE_BASE64`, `KEY_PASSWORD`, `KEY_ALIAS`, `HETZNER_HOST`, `HETZNER_USER`, `HETZNER_PASS`) must be configured in Gitea repo settings before the first tag is pushed. + +- [ ] **Step 1: Create `.gitea/workflows/release.yaml`** + +```yaml +name: Build and Release to F-Droid + +on: + push: + tags: + - '*' + workflow_dispatch: + +jobs: + ci: + runs-on: docker + env: + ANDROID_HOME: /opt/android-sdk + ANDROID_SDK_ROOT: /opt/android-sdk + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android SDK packages + run: | + sdkmanager --licenses >/dev/null <<'EOF' + y + y + y + y + y + y + y + y + y + y + EOF + sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0" + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Lint + tests + debug build (sanity) + run: ./gradlew lint test assembleDebug --no-daemon + + build-and-deploy: + needs: ci + runs-on: docker + env: + ANDROID_HOME: /opt/android-sdk + ANDROID_SDK_ROOT: /opt/android-sdk + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android SDK packages + run: | + sdkmanager --licenses >/dev/null <<'EOF' + y + y + y + y + y + y + y + y + y + y + EOF + sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0" + + - name: Install jq + run: | + set -e + SUDO="" + if command -v sudo >/dev/null 2>&1; then SUDO="sudo"; fi + if command -v apt-get >/dev/null 2>&1; then + $SUDO apt-get update + $SUDO apt-get install -y jq + elif command -v apk >/dev/null 2>&1; then + $SUDO apk add --no-cache jq + fi + + - name: Set version from git tag + run: | + set -e + RAW_TAG="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}" + VERSION="${RAW_TAG#v}" + MAJOR=$(echo "$VERSION" | cut -d. -f1) + MINOR=$(echo "$VERSION" | cut -d. -f2) + PATCH=$(echo "$VERSION" | cut -d. -f3) + MAJOR=${MAJOR:-0}; MINOR=${MINOR:-0}; PATCH=${PATCH:-0} + VERSION_CODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH )) + echo "Version: $VERSION, VersionCode: $VERSION_CODE" + sed -i "s/versionName = \".*\"/versionName = \"$VERSION\"/" app/build.gradle.kts + sed -i "s/versionCode = .*/versionCode = $VERSION_CODE/" app/build.gradle.kts + grep -E 'versionName|versionCode' app/build.gradle.kts + + - name: Setup Android Keystore + env: + KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + run: | + mkdir -p app + echo "$KEYSTORE_BASE64" | base64 --decode > app/upload-keystore.jks + cat > key.properties </dev/null 2>&1; then SUDO="sudo"; fi + $SUDO apt-get update + $SUDO apt-get install -y sshpass python3-pip + pip3 install --break-system-packages --upgrade fdroidserver + + - name: Initialize or fetch F-Droid Repository + env: + HOST: ${{ secrets.HETZNER_HOST }} + USER: ${{ secrets.HETZNER_USER }} + PASS: ${{ secrets.HETZNER_PASS }} + run: | + mkdir -p fdroid + sshpass -p "$PASS" sftp -o StrictHostKeyChecking=no "$USER@$HOST" <<'SFTP' + -mkdir dev + -mkdir dev/fdroid + -mkdir dev/fdroid/repo + SFTP + sshpass -p "$PASS" scp -o StrictHostKeyChecking=no -r "$USER@$HOST:dev/fdroid/." fdroid/ || (cd fdroid && fdroid init) + + - name: Ensure F-Droid repo signing key and icon + run: | + cd fdroid + mkdir -p repo/icons + if [ ! -f repo/icons/icon.png ]; then + # Fallback: copy a placeholder; until a PNG icon ships, + # F-Droid uses a generic icon. Real icon comes when we have a PNG export. + true + fi + if [ ! -f keystore.p12 ]; then + fdroid update --create-key + fi + + - name: Copy new APK to repo + run: | + set -e + mkdir -p fdroid/repo + REF_NAME="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}" + SAFE_REF_NAME="$(echo "$REF_NAME" | tr '/ ' '__' | tr -cd '[:alnum:]_.-')" + if [ -z "$SAFE_REF_NAME" ]; then + SAFE_REF_NAME="${GITHUB_SHA:-manual}" + fi + cp app/build/outputs/apk/release/app-release.apk "fdroid/repo/calendula_${SAFE_REF_NAME}.apk" + + - name: Copy metadata to F-Droid repo + run: | + mkdir -p fdroid/metadata + cp -r fdroid-metadata/* fdroid/metadata/ + + - name: Generate F-Droid Index + run: | + cd fdroid + fdroid update -c + + - name: Upload Repo to Hetzner + env: + HOST: ${{ secrets.HETZNER_HOST }} + USER: ${{ secrets.HETZNER_USER }} + PASS: ${{ secrets.HETZNER_PASS }} + run: | + set -euo pipefail + SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=20" + sshpass -p "$PASS" sftp $SSH_OPTS "$USER@$HOST" <<'SFTP' + -mkdir dev + -mkdir dev/fdroid + -mkdir dev/fdroid/repo + SFTP + sshpass -p "$PASS" scp $SSH_OPTS -r fdroid/. "$USER@$HOST:dev/fdroid/" +``` + +> Note: The release workflow assumes `app/build.gradle.kts` will pick up signing config from a `key.properties` file at the project root. We add that wiring in Task 13. + +- [ ] **Step 2: Commit** + +```bash +git add .gitea/workflows/release.yaml +git commit -m "ci: add gitea release workflow with F-Droid pipeline" +``` + +--- + +## Task 13: Wire Signing Config into App Gradle + +**Files:** +- Modify: `app/build.gradle.kts` + +The release workflow drops a `key.properties` at project root and a `upload-keystore.jks` in `app/`. Make Gradle read these. + +- [ ] **Step 1: Replace the contents of `app/build.gradle.kts`** + +```kotlin +import java.util.Properties +import java.io.FileInputStream + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) +} + +val keystorePropertiesFile = rootProject.file("key.properties") +val keystoreProperties = Properties().apply { + if (keystorePropertiesFile.exists()) { + load(FileInputStream(keystorePropertiesFile)) + } +} + +android { + namespace = "de.jeanlucmakiola.calendula" + compileSdk = 36 + + defaultConfig { + applicationId = "de.jeanlucmakiola.calendula" + minSdk = 29 + targetSdk = 36 + versionCode = 1 + versionName = "0.1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { useSupportLibrary = true } + } + + signingConfigs { + if (keystorePropertiesFile.exists()) { + create("release") { + keyAlias = keystoreProperties["keyAlias"] as String + keyPassword = keystoreProperties["keyPassword"] as String + storeFile = file(keystoreProperties["storeFile"] as String) + storePassword = keystoreProperties["storePassword"] as String + } + } + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + if (keystorePropertiesFile.exists()) { + signingConfig = signingConfigs.getByName("release") + } + } + debug { + applicationIdSuffix = ".debug" + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + + testOptions { + unitTests.all { test -> + test.useJUnitPlatform() + } + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + + implementation(libs.androidx.datastore.preferences) + + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + testImplementation(libs.junit.jupiter.api) + testRuntimeOnly(libs.junit.jupiter.engine) + testRuntimeOnly(libs.junit.platform.launcher) + testImplementation(libs.truth) + + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) +} +``` + +- [ ] **Step 2: Verify debug build still works (without keystore)** + +```bash +./gradlew assembleDebug +``` + +Expected: BUILD SUCCESSFUL. The release block silently skips signing config when no `key.properties` is present. + +- [ ] **Step 3: Verify release build skips signing locally** + +```bash +./gradlew assembleRelease +``` + +Expected: BUILD SUCCESSFUL with a warning that the release APK is unsigned (or signed with debug key) — this is fine; only CI signs releases. + +- [ ] **Step 4: Commit** + +```bash +git add app/build.gradle.kts +git commit -m "build: wire signing config from key.properties when present" +``` + +--- + +## Task 14: Final Verification & CHANGELOG Update + +- [ ] **Step 1: Run the full local verification** + +```bash +./gradlew lint test assembleDebug +``` + +Expected: All three steps complete with BUILD SUCCESSFUL. + +- [ ] **Step 2: Update `CHANGELOG.md`** + +Replace the existing `[Unreleased]` block with explicit shipped items for v0.1.0 and a new (empty) `[Unreleased]` placeholder. Final file content: + +```markdown +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] — 2026-06-08 + +### Added +- Initial project scaffold (Gradle Kotlin DSL, Version Catalog, Hilt, DataStore) +- Material 3 Expressive theme with Dynamic Color (API 31+) and slate-derived fallback +- Adaptive launcher icon — stylized "1" on slate squircle (references *kalendae*) +- German + English localization infrastructure +- Permission declaration for `READ_CALENDAR` (no UI flow yet — that's Plan 02) +- Gitea CI workflow: lint, unit tests, debug build, Trivy scan +- Gitea release workflow: signed release APK + F-Droid metadata sync to Hetzner +- F-Droid metadata stubs (DE + EN short/full descriptions) +- `.planning/` project-tracking documents +``` + +- [ ] **Step 3: Update `.planning/STATE.md`** + +```markdown +# Calendula — Current State + +*Last updated: 2026-06-08* + +## Status + +**Milestone:** v0.1 — Foundation & CI +**Phase:** Plan 01 complete; ready to start Plan 02 + +## Progress + +- [x] Design spec written and committed (`docs/superpowers/specs/2026-06-08-calendar-app-design.md`) +- [x] V1 design decisions resolved (App name "Calendula", icon, seed color) +- [x] Plan 01 written and executed (`docs/superpowers/plans/2026-06-08-01-foundation.md`) +- [x] Foundation lands: theme, icon, i18n, Hilt, DataStore, CI green +- [ ] Plan 02 written (Data Layer & Permission Flow) + +## Next + +1. Write Plan 02: Data Layer & Permission Flow +2. Execute Plan 02 +3. Iterate on UI design (mockups) before screens are built +``` + +- [ ] **Step 4: Commit** + +```bash +git add CHANGELOG.md .planning/STATE.md +git commit -m "docs: record v0.1.0 foundation in CHANGELOG, update STATE" +``` + +- [ ] **Step 5: Tag v0.1.0 — Optional, only after CI passes on Gitea** + +This step happens only once the repo is pushed to Gitea, CI is configured, and CI runs green on the foundation commit. Skip locally. + +```bash +# After CI is green on Gitea: +git tag -a v0.1.0 -m "v0.1.0 — foundation" +git push origin v0.1.0 +``` + +The release workflow will then build, sign, and publish to F-Droid. + +--- + +## Verification Checklist (post-execution) + +After all 14 tasks are completed, verify: + +- [ ] `./gradlew lint` exits 0 +- [ ] `./gradlew test` exits 0; all unit tests pass (ColorSchemeTest x3) +- [ ] `./gradlew assembleDebug` produces a working APK at `app/build/outputs/apk/debug/app-debug.apk` +- [ ] APK installs and shows "Calendula" + "A modern calendar." in the system language (DE if locale=de_DE, else EN) +- [ ] Launcher icon shows the "1" on slate squircle +- [ ] Theme respects system light/dark +- [ ] On API 31+ with a colored wallpaper, theme picks up wallpaper colors (Dynamic Color) +- [ ] CI workflow file is at `.gitea/workflows/ci.yaml` +- [ ] Release workflow file is at `.gitea/workflows/release.yaml` +- [ ] F-Droid metadata is in `fdroid-metadata/de.jeanlucmakiola.calendula/` +- [ ] `.planning/STATE.md` reflects "Plan 01 complete" + +When pushing to Gitea for the first time: + +- [ ] Configure repo secrets: `KEYSTORE_BASE64`, `KEY_PASSWORD`, `KEY_ALIAS`, `HETZNER_HOST`, `HETZNER_USER`, `HETZNER_PASS` +- [ ] First CI run is green on `main` +- [ ] Tag `v0.1.0` triggers the release workflow successfully +- [ ] F-Droid repo at `https:///dev/fdroid/` shows Calendula + +--- + +## What Plan 01 Does NOT Do (deferred to subsequent plans) + +- No `CalendarRepository`, no `ContentResolver` queries → Plan 02 +- No permission UI flow → Plan 02 +- No calendar event reading → Plan 02 +- No Month / Week / Day views → Plans 03 / 04 / 05 +- No Event-Detail-Sheet → Plan 06 +- No Filter / Settings → Plan 07 +- The "1" icon path is geometrically simple; visual refinement happens during UI-design iteration before V1 release +- Custom typography / fonts → UI-design iteration + +The foundation deliberately ships small. Every subsequent plan layers one focused feature on top of a known-good base.