build: S08 settings and S09 units

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-17 13:52:42 +02:00
co-authored by Claude Opus 5.5
parent 1d8c3f8f5f
commit c0bf0f6042
@@ -0,0 +1,120 @@
package app.wetter.ui.screens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import app.wetter.R
import app.wetter.data.DistanceUnit
import app.wetter.data.Graph
import app.wetter.data.PrecipUnit
import app.wetter.data.PressureUnit
import app.wetter.data.Settings
import app.wetter.data.TempUnit
import app.wetter.data.ThemeMode
import app.wetter.data.WindUnit
import app.wetter.ui.theme.WSpace
import app.wetter.ui.theme.WType
import kotlinx.coroutines.launch
/** S08 + S09: units, look, data sources. */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(settings: Settings, onBack: () -> Unit) {
val scope = rememberCoroutineScope()
val set: ((Settings) -> Settings) -> Unit = { t -> scope.launch { Graph.settings.update(t) } }
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.settings)) },
navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Rounded.ArrowBack, stringResource(R.string.back)) } },
)
},
) { pad ->
Column(Modifier.padding(pad).verticalScroll(rememberScrollState()).padding(horizontal = WSpace.s16)) {
Header(stringResource(R.string.set_units))
Choice(stringResource(R.string.set_temp), TempUnit.entries, settings.temp, { if (it == TempUnit.C) "°C" else "°F" }) { v -> set { it.copy(temp = v) } }
Choice(stringResource(R.string.set_wind), WindUnit.entries, settings.wind, {
when (it) { WindUnit.KMH -> "km/h"; WindUnit.MS -> "m/s"; WindUnit.MPH -> "mph"; WindUnit.KN -> "kn" }
}) { v -> set { it.copy(wind = v) } }
Choice(stringResource(R.string.set_precip), PrecipUnit.entries, settings.precip, { if (it == PrecipUnit.MM) "mm" else "in" }) { v -> set { it.copy(precip = v) } }
Choice(stringResource(R.string.set_pressure), PressureUnit.entries, settings.pressure, { if (it == PressureUnit.HPA) "hPa" else "inHg" }) { v -> set { it.copy(pressure = v) } }
Choice(stringResource(R.string.set_distance), DistanceUnit.entries, settings.distance, { if (it == DistanceUnit.KM) "km" else "mi" }) { v -> set { it.copy(distance = v) } }
Header(stringResource(R.string.set_look))
val themeLabels = mapOf(
ThemeMode.SYSTEM to stringResource(R.string.theme_system),
ThemeMode.LIGHT to stringResource(R.string.theme_light),
ThemeMode.DARK to stringResource(R.string.theme_dark),
)
Choice(stringResource(R.string.set_theme), ThemeMode.entries, settings.theme, { themeLabels.getValue(it) }) { v -> set { it.copy(theme = v) } }
Toggle(stringResource(R.string.set_animations), stringResource(R.string.set_animations_sub), settings.animations) { v -> set { it.copy(animations = v) } }
Toggle(stringResource(R.string.set_brief), null, settings.brief) { v -> set { it.copy(brief = v) } }
Header(stringResource(R.string.set_about))
Text(stringResource(R.string.attribution), style = WType.bodySm, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = WSpace.s32))
}
}
}
@Composable
private fun Header(text: String) = Text(
text, style = WType.title, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = WSpace.s24, bottom = WSpace.s8).semantics { heading() },
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun <T> Choice(label: String, options: List<T>, selected: T, name: (T) -> String, onPick: (T) -> Unit) {
Column(Modifier.padding(vertical = WSpace.s8)) {
Text(label, style = WType.body)
SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth().padding(top = WSpace.s8).selectableGroup()) {
options.forEachIndexed { i, o ->
SegmentedButton(
selected = o == selected,
onClick = { onPick(o) },
shape = SegmentedButtonDefaults.itemShape(i, options.size),
) { Text(name(o)) }
}
}
}
}
@Composable
private fun Toggle(label: String, sub: String?, checked: Boolean, onChange: (Boolean) -> Unit) {
Row(
Modifier.fillMaxWidth().toggleable(checked, role = Role.Switch, onValueChange = onChange).padding(vertical = WSpace.s12),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
Text(label, style = WType.body)
if (sub != null) Text(sub, style = WType.bodySm, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
Switch(checked, onCheckedChange = null)
}
}