Files
Clocked/app/src/main/java/net/tinsae/clocked/settings/SettingsScreen.kt
2025-12-28 10:14:19 +01:00

238 lines
8.7 KiB
Kotlin

package net.tinsae.clocked.settings
import androidx.compose.foundation.clickable
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import net.tinsae.clocked.AuthViewModel
import net.tinsae.clocked.R
import net.tinsae.clocked.data.Locale
import net.tinsae.clocked.data.Theme
import net.tinsae.clocked.ui.theme.ClockedTheme
@Composable
fun SettingsScreen(
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = viewModel(),
authViewModel: AuthViewModel
) {
val uiState by viewModel.uiState.collectAsState()
if (uiState.showThemeDialog) {
ThemeSelectionDialog(
currentTheme = uiState.theme,
onThemeSelected = viewModel::onThemeSelected,
onDismiss = viewModel::onDismissThemeDialog
)
}
if (uiState.showLocaleDialog) {
LocaleSelectionDialog(
currentLocale = uiState.locale,
onLocaleSelected = viewModel::onLocaleSelected,
onDismiss = viewModel::onDismissLocaleDialog
)
}
Column(
modifier = modifier
.padding(16.dp)
) {
SettingsSection(title = stringResource(R.string.settings_section_data)) {
SettingsItem(
title = stringResource(R.string.settings_item_export_csv),
onClick = viewModel::onExportClicked,
trailingContent = {
Icon(
Icons.Filled.ChevronRight,
contentDescription = stringResource(R.string.settings_item_navigate)
)
}
)
}
Spacer(modifier = Modifier.height(24.dp))
SettingsSection(title = stringResource(R.string.settings_section_preferences)) {
SettingsItem(title = stringResource(R.string.settings_item_default_duration), value = uiState.defaultDuration, onClick = { /* TODO: Handle duration change */ })
SettingsItem(title = stringResource(R.string.settings_item_time_rounding), value = uiState.timeRounding, onClick = { /* TODO: Handle rounding change */ })
SettingsItem(title = stringResource(R.string.settings_item_week_starts_on), value = uiState.weekStartsOn, onClick = { /* TODO: Handle week start change */ })
}
Spacer(modifier = Modifier.height(24.dp))
SettingsSection(title = stringResource(R.string.settings_section_appearance)) {
SettingsItem(title = stringResource(R.string.theme), value = uiState.theme.title, onClick = viewModel::onThemeClicked)
SettingsItem(title = stringResource(R.string.language), value = uiState.locale.title, onClick = viewModel::onLocaleClicked)
}
Spacer(modifier = Modifier.height(24.dp))
SettingsSection(title = stringResource(R.string.about)) {
SettingsItem(title = stringResource(R.string.settings_item_app_version), value = uiState.appVersion)
}
}
}
@Composable
fun SettingsSection(title: String, content: @Composable () -> Unit) {
Column {
Text(text = title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
content()
}
}
@Composable
fun SettingsItem(
title: String,
modifier: Modifier = Modifier,
value: String? = null,
onClick: (() -> Unit)? = null,
trailingContent: @Composable (() -> Unit)? = null
) {
val itemModifier = if (onClick != null) {
modifier.clickable(onClick = onClick)
} else {
modifier
}
Row(
modifier = itemModifier
.fillMaxWidth()
.padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = title, style = MaterialTheme.typography.bodyLarge)
if (value != null) {
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (trailingContent != null) {
trailingContent()
}
}
}
@Composable
fun ThemeSelectionDialog(
currentTheme: Theme,
onThemeSelected: (Theme) -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(text = stringResource(R.string.theme)) },
text = {
Column(Modifier.selectableGroup()) {
Theme.entries.forEach { theme ->
Row(
Modifier
.fillMaxWidth()
.height(56.dp)
.selectable(
selected = (theme == currentTheme),
onClick = { onThemeSelected(theme) },
role = Role.RadioButton
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (theme == currentTheme),
onClick = null // null recommended for accessibility with screenreaders
)
Text(
text = theme.title,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
}
)
}
@Composable
fun LocaleSelectionDialog(
currentLocale: Locale,
onLocaleSelected: (Locale) -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(text = stringResource(R.string.language)) },
text = {
Column(Modifier.selectableGroup()) {
Locale.entries.forEach { locale ->
Row(
Modifier
.fillMaxWidth()
.height(56.dp)
.selectable(
selected = (locale == currentLocale),
onClick = { onLocaleSelected(locale) },
role = Role.RadioButton
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (locale == currentLocale),
onClick = null // null recommended for accessibility with screenreaders
)
Text(
text = locale.title,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
}
)
}
@Preview(showBackground = true)
@Composable
fun SettingsScreenPreview() {
ClockedTheme {
SettingsScreen(viewModel = SettingsViewModel(), authViewModel = AuthViewModel())
}
}