From 09d3f9b9348570ae8e55d8fbf94a0a3a086138b0 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 28 Jun 2026 15:24:17 +0200 Subject: [PATCH] components: add AboutCard + LanguagePickerRow settings recipes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AboutCard — logo slot + app name/author, a row of outlined link buttons and an optional tonal highlight link (each opens its URL). LanguagePickerRow — a GroupedRow that opens the full-screen language picker, backed by core-locale's AppLanguage. Both parameterized so each app keeps its own strings, logo and row icon. Adds the components -> core-locale dependency. Co-Authored-By: Claude Opus 4.8 (1M context) --- components/build.gradle.kts | 2 + .../floret/components/SettingsRecipes.kt | 147 ++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 components/src/main/kotlin/de/jeanlucmakiola/floret/components/SettingsRecipes.kt diff --git a/components/build.gradle.kts b/components/build.gradle.kts index cfcab7d..111df3d 100644 --- a/components/build.gradle.kts +++ b/components/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { // identity supplies the shared motion (expandEnter/collapseExit) that the // revealable components animate with. implementation(project(":identity")) + // core-locale backs the language-picker recipe (AppLanguage). + implementation(project(":core-locale")) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) diff --git a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/SettingsRecipes.kt b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/SettingsRecipes.kt new file mode 100644 index 0000000..9f647da --- /dev/null +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/SettingsRecipes.kt @@ -0,0 +1,147 @@ +package de.jeanlucmakiola.floret.components + +import android.content.Intent +import android.net.Uri +import androidx.annotation.XmlRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +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.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import de.jeanlucmakiola.floret.locale.AppLanguage + +/** A labelled, icon-led external link rendered as a button in [AboutCard]. */ +data class AboutLink(val icon: ImageVector, val label: String, val url: String) + +/** + * The "about this app" card shared across the family: the app's [logo] beside + * its [appName] and [author], then [primaryLinks] as a row of equal-width + * outlined buttons (e.g. source, licence) and an optional full-width + * [highlightLink] tonal button (e.g. donate). Each link opens its URL in the + * browser. The app supplies its own logo, strings and links. + */ +@Composable +fun AboutCard( + logo: @Composable () -> Unit, + appName: String, + author: String, + primaryLinks: List, + highlightLink: AboutLink? = null, +) { + val context = LocalContext.current + val open = { url: String -> + runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } + Unit + } + Surface( + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = RoundedCornerShape(24.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Column(Modifier.fillMaxWidth().padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + logo() + Spacer(Modifier.width(16.dp)) + Column(Modifier.weight(1f)) { + Text(appName, style = MaterialTheme.typography.titleLarge) + Text( + text = author, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + if (primaryLinks.isNotEmpty()) { + Spacer(Modifier.height(12.dp)) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { + primaryLinks.forEach { link -> + OutlinedButton( + onClick = { open(link.url) }, + contentPadding = PaddingValues(horizontal = 12.dp), + modifier = Modifier.weight(1f), + ) { + Icon(link.icon, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(8.dp)) + Text(link.label) + } + } + } + } + if (highlightLink != null) { + Spacer(Modifier.height(8.dp)) + FilledTonalButton(onClick = { open(highlightLink.url) }, modifier = Modifier.fillMaxWidth()) { + Icon(highlightLink.icon, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(8.dp)) + Text(highlightLink.label) + } + } + } + } +} + +/** + * A settings row that opens a full-screen language picker, backed by + * [AppLanguage]. The choice is mirrored locally so the row updates instantly + * before the activity recreation lands. [autoLabel] names the follow-the-system + * option (the `null` tag); [localesConfig] is the app's `res/xml/locales_config`; + * [leading] is the row's icon. + */ +@Composable +fun LanguagePickerRow( + position: Position, + title: String, + autoLabel: String, + @XmlRes localesConfig: Int, + leading: @Composable () -> Unit, +) { + val context = LocalContext.current + var current by remember { mutableStateOf(AppLanguage.currentTag()) } + var showDialog by remember { mutableStateOf(false) } + // null = follow the system; the rest are BCP-47 tags from locales_config.xml. + val options = remember { listOf(null) + AppLanguage.supportedTags(context, localesConfig) } + val label: (String?) -> String = { tag -> tag?.let { AppLanguage.displayName(it) } ?: autoLabel } + + GroupedRow( + title = title, + summary = label(current), + position = position, + leading = leading, + onClick = { showDialog = true }, + ) + + if (showDialog) { + OptionPicker( + title = title, + options = options, + selected = current, + label = { label(it) }, + onSelect = { + current = it + AppLanguage.apply(it) + }, + onDismiss = { showDialog = false }, + ) + } +}