components: add AboutCard + LanguagePickerRow settings recipes
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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<AboutLink>,
|
||||
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<String?>(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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user