From 5ddec0f24839e6f093e80bf65182342fa28ddb38 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 11 Sep 2026 11:46:54 +0200 Subject: [PATCH] feat: theme, app entry points, and the sage identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clockula's seed is #6B7A5C, the third step of the family's colour cycle. Calendula's slate (#5C6B7A) and Agendula's mauve (#7A5C6B) are the same three bytes rotated — and that byte rotation is exactly a 120° hue rotation, so the three seeds sit at 210°, 330° and 90° with identical saturation and lightness. The fallback schemes are therefore not eyeballed: they are Agendula's hand-picked scheme with its hue rotated the same 120°, which keeps the three apps structurally one palette turned rather than three colours picked. The cycle closes at three. The launcher icon keeps the family's Calendula bloom badge path-for-path and swaps the task card for a clock dial, drawn as a 308° arc so the badge overhangs into a gap rather than colliding with the stroke — the same device Agendula uses to open its card's corner. MainActivity is M0 scaffolding: theme plus the crash surface, with a placeholder where the four-tab shell and the live pill land in M4. The theme follows stored preferences from M2, once there are any. The manifest declares no permissions yet, deliberately. Clockula's permission set belongs to the alarm engine and is declared in M3 alongside the receivers and the ringing service that need it, so the manifest never claims a capability the code cannot honour. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01L94fydiJC37LtxVusNQBDy --- app/src/main/AndroidManifest.xml | 57 ++++++++++ .../de/jeanlucmakiola/clockula/ClockulaApp.kt | 34 ++++++ .../jeanlucmakiola/clockula/MainActivity.kt | 103 ++++++++++++++++++ .../clockula/ui/crash/CrashReportActivity.kt | 61 +++++++++++ .../jeanlucmakiola/clockula/ui/theme/Color.kt | 51 +++++++++ .../jeanlucmakiola/clockula/ui/theme/Theme.kt | 28 +++++ .../jeanlucmakiola/clockula/ui/theme/Type.kt | 10 ++ .../res/drawable/ic_launcher_background.xml | 5 + .../res/drawable/ic_launcher_foreground.xml | 93 ++++++++++++++++ app/src/main/res/drawable/ic_notification.xml | 15 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/values/colors.xml | 7 ++ app/src/main/res/values/strings.xml | 8 ++ app/src/main/res/values/themes.xml | 7 ++ app/src/main/res/xml/backup_rules.xml | 4 + .../main/res/xml/data_extraction_rules.xml | 8 ++ app/src/main/res/xml/locales_config.xml | 11 ++ 18 files changed, 514 insertions(+) create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/ClockulaApp.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/MainActivity.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/ui/crash/CrashReportActivity.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/ui/theme/Color.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/ui/theme/Theme.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/ui/theme/Type.kt create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_notification.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/main/res/xml/locales_config.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2149134 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ClockulaApp.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ClockulaApp.kt new file mode 100644 index 0000000..202e47c --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ClockulaApp.kt @@ -0,0 +1,34 @@ +package de.jeanlucmakiola.clockula + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp +import de.jeanlucmakiola.floret.crash.CrashConfig +import de.jeanlucmakiola.floret.crash.CrashReporter + +/** + * Application entry point. Registered as android:name=".ClockulaApp". + * + * For now this is Hilt init plus crash capture. From M3 it also owns the launch- + * time alarm re-sync: an alarm's next fire time is resolved against the current + * zone, never stored absolutely (docs/PLAN.md §4), so anything that could have + * moved while the app was closed has to be re-resolved before the user can see + * a stale one. + */ +@HiltAndroidApp +class ClockulaApp : Application() { + + override fun onCreate() { + super.onCreate() + // Install first thing so startup crashes are captured too (privacy- + // respecting, on-device; the user submits the report by hand). + CrashReporter.install( + this, + CrashConfig( + appLabel = getString(R.string.app_name), + newIssueUrl = getString(R.string.report_issue_url), + chooseIssueUrl = getString(R.string.report_issue_url), + issueTitle = getString(R.string.crash_report_issue_title), + ), + ) + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/MainActivity.kt b/app/src/main/java/de/jeanlucmakiola/clockula/MainActivity.kt new file mode 100644 index 0000000..8009bf1 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/MainActivity.kt @@ -0,0 +1,103 @@ +package de.jeanlucmakiola.clockula + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import dagger.hilt.android.AndroidEntryPoint +import de.jeanlucmakiola.clockula.ui.crash.CrashReportActivity +import de.jeanlucmakiola.clockula.ui.theme.ClockulaTheme +import de.jeanlucmakiola.floret.crash.CrashReportDialog +import de.jeanlucmakiola.floret.crash.CrashReporter +import de.jeanlucmakiola.floret.crash.submitCrashReport + +/** + * Single activity. M0 scaffolding: it brings up the theme and the crash surface + * and nothing else. The four-tab shell and the live pill (docs/PLAN.md §9) land + * in M4, and the theme starts following stored preferences once the data layer + * exists in M2 — until then [ClockulaTheme]'s own defaults (follow-system, + * dynamic colour) stand in. + */ +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + + // A captured crash report awaiting the user's decision, surfaced as a dialog + // over the app on the next launch (the single-crash path). A startup + // crash-loop is handled out of band, before setContent — see below. + private var pendingCrashReport by mutableStateOf(null) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // If the app keeps crashing as it starts, the main UI can't be trusted + // to come up. Route to the standalone report screen instead of + // re-entering the crashing graph. + if (CrashReporter.isCrashLoop(this)) { + startActivity(Intent(this, CrashReportActivity::class.java)) + finish() + return + } + + enableEdgeToEdge() + + // Surface a single captured crash as a dialog on the next launch. + if (CrashReporter.shouldPrompt(this)) pendingCrashReport = CrashReporter.pendingReport(this) + + setContent { + ClockulaTheme { + Placeholder(Modifier.fillMaxSize()) + pendingCrashReport?.let { report -> + CrashReportDialog( + report = report, + onSend = { + submitCrashReport(this@MainActivity, report) + CrashReporter.clearReport(this@MainActivity) + pendingCrashReport = null + }, + onDismiss = { + CrashReporter.dismissPrompt(this@MainActivity) + pendingCrashReport = null + }, + ) + } + } + } + } + + override fun onResume() { + super.onResume() + // A successful start breaks any loop; reset the timing trail so a later + // ordinary crash isn't mistaken for a loop. + CrashReporter.markHealthy(this) + } +} + +/** Stands in for the app shell until M4. Proves the theme resolves and renders. */ +@Composable +private fun Placeholder(modifier: Modifier = Modifier) { + Surface(modifier, color = MaterialTheme.colorScheme.background) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = stringResourceAppName(), + style = MaterialTheme.typography.displaySmall, + color = MaterialTheme.colorScheme.primary, + ) + } + } +} + +@Composable +private fun stringResourceAppName(): String = + androidx.compose.ui.res.stringResource(R.string.app_name) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/crash/CrashReportActivity.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/crash/CrashReportActivity.kt new file mode 100644 index 0000000..f43e960 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/crash/CrashReportActivity.kt @@ -0,0 +1,61 @@ +package de.jeanlucmakiola.clockula.ui.crash + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import de.jeanlucmakiola.clockula.ui.theme.ClockulaTheme +import de.jeanlucmakiola.floret.crash.CrashReportDialog +import de.jeanlucmakiola.floret.crash.CrashReporter +import de.jeanlucmakiola.floret.crash.submitCrashReport + +/** + * Standalone surface for a captured crash report. [MainActivity] routes here + * when it detects a startup crash-loop (see [CrashReporter.isCrashLoop]): the + * main UI can't be trusted to start, so this screen stays clear of the app's + * Hilt graph, DataStore-backed theme and Compose content — it only reads the + * report file and shows the kit's report dialog. Plain [ClockulaTheme] defaults + * (follow-system, dynamic colour) avoid touching anything that might be the + * cause of the crash. The reusable machinery lives in floret-kit's core-crash. + */ +class CrashReportActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val report = CrashReporter.pendingReport(this) + if (report == null) { + finish() + return + } + enableEdgeToEdge() + setContent { + ClockulaTheme { + // Opaque backdrop so the dialog doesn't float over a bare task. + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.surface) {} + CrashReportDialog( + report = report, + onSend = { + submitCrashReport(this, report) + CrashReporter.clearReport(this) + finish() + }, + onDismiss = { + CrashReporter.clearReport(this) + finish() + }, + ) + } + } + } + + override fun onResume() { + super.onResume() + // Reaching this screen breaks the loop; reset the timing trail so a + // later ordinary crash isn't mistaken for a loop. + CrashReporter.markHealthy(this) + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/theme/Color.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/theme/Color.kt new file mode 100644 index 0000000..6ebe076 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/theme/Color.kt @@ -0,0 +1,51 @@ +package de.jeanlucmakiola.clockula.ui.theme + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +/** + * Seed color anchoring the entire palette. A muted sage, completing the family's + * colour cycle: Calendula's cool slate ([0xFF5C6B7A]) and Agendula's warm mauve + * ([0xFF7A5C6B]) are the same three bytes rotated, and this is the third + * rotation. + * + * The byte rotation is not a coincidence of notation — it is exactly a 120° hue + * rotation. The three seeds sit at 210°, 330° and 90° at identical saturation + * and lightness, so the family reads as one palette turned, not three colours + * picked. The cycle closes at three; a fourth app needs a different idea. + */ +val ClockulaSeed: Color = Color(0xFF6B7A5C) + +/** + * Fallback light scheme for devices without dynamic color (API < 31) or when the + * user disables it. Dynamic color (API 31+) is the real path; these are + * Agendula's hand-picked approximation with its hue rotated the same 120°, so + * the siblings' schemes stay structurally identical rather than independently + * eyeballed. + */ +val ClockulaLightFallback = lightColorScheme( + primary = Color(0xFF5C6A4E), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFE8F6D9), + onPrimaryContainer = Color(0xFF1D2A10), + secondary = Color(0xFF627055), + onSecondary = Color(0xFFFFFFFF), + background = Color(0xFFFCFEFB), + onBackground = Color(0xFF1C1F1A), + surface = Color(0xFFFCFEFB), + onSurface = Color(0xFF1C1F1A), +) + +val ClockulaDarkFallback = darkColorScheme( + primary = Color(0xFFCEE2BA), + onPrimary = Color(0xFF334025), + primaryContainer = Color(0xFF4A583B), + onPrimaryContainer = Color(0xFFE8F6D9), + secondary = Color(0xFFCAD8BC), + onSecondary = Color(0xFF313A28), + background = Color(0xFF141611), + onBackground = Color(0xFFE4E9E0), + surface = Color(0xFF141611), + onSurface = Color(0xFFE4E9E0), +) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/theme/Theme.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/theme/Theme.kt new file mode 100644 index 0000000..a9dea2e --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/theme/Theme.kt @@ -0,0 +1,28 @@ +package de.jeanlucmakiola.clockula.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import de.jeanlucmakiola.floret.identity.FloretExpressiveTheme + +/** + * Clockula's theme: the family's [FloretExpressiveTheme] machinery (dynamic + * colour on API 31+, system light/dark, the standard motion scheme) fed with + * Clockula's own identity — its seed-derived fallback schemes + * ([ClockulaLightFallback] / [ClockulaDarkFallback]) and [ClockulaTypography]. + * The mechanics live in floret-kit; the look stays here. + */ +@Composable +fun ClockulaTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit, +) { + FloretExpressiveTheme( + lightScheme = ClockulaLightFallback, + darkScheme = ClockulaDarkFallback, + darkTheme = darkTheme, + dynamicColor = dynamicColor, + typography = ClockulaTypography, + content = content, + ) +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/theme/Type.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/theme/Type.kt new file mode 100644 index 0000000..5f89ce9 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/theme/Type.kt @@ -0,0 +1,10 @@ +package de.jeanlucmakiola.clockula.ui.theme + +import androidx.compose.material3.Typography + +/** + * Default Material 3 Expressive typography. A clock app is mostly one enormous + * number, so the readout scale is the real typographic decision here — it gets + * settled against a working stopwatch in M7, not guessed at in the scaffolding. + */ +val ClockulaTypography = Typography() diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..5912048 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..749e2e1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 0000000..8ebb832 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..b3e26b4 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..b3e26b4 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..080f756 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #FF6B7A5C + + #FF6B7A5C + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..bbab5db --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + Clockula + A modern Material 3 Expressive clock app.\nComing into bloom. + + + Crash report + https://codeberg.org/jlmakiola/clockula/issues/new + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..939bd76 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..87d1f20 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..c6ba7b9 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml new file mode 100644 index 0000000..7381289 --- /dev/null +++ b/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,11 @@ + + + + +