# 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.