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 @@ + + + + +