integration supabas

This commit is contained in:
2025-12-28 10:14:19 +01:00
parent 3a37eac107
commit 36d5fc5ce9
29 changed files with 1944 additions and 363 deletions

View File

@@ -0,0 +1,237 @@
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())
}
}

View File

@@ -0,0 +1,83 @@
package net.tinsae.clocked.settings
import androidx.activity.result.launch
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.tinsae.clocked.data.Locale
import net.tinsae.clocked.data.LogRepository
import net.tinsae.clocked.data.Theme
import net.tinsae.clocked.util.CsvExporter
data class SettingsUiState(
val defaultDuration: String = "8h",
val timeRounding: String = "15 min",
val weekStartsOn: String = "Monday",
val theme: Theme = Theme.SYSTEM,
val locale: Locale = Locale.SYSTEM,
val appVersion: String = "1.0.0",
val showThemeDialog: Boolean = false,
val showLocaleDialog: Boolean = false,
// Hold the content of the CSV to be saved. Null means no save operation is pending.
val pendingCsvContent: String? = null
)
class SettingsViewModel : ViewModel() {
private val _uiState = MutableStateFlow(SettingsUiState())
val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()
fun onThemeClicked() {
_uiState.update { it.copy(showThemeDialog = true) }
}
fun onThemeSelected(theme: Theme) {
_uiState.update { it.copy(theme = theme, showThemeDialog = false) }
}
fun onDismissThemeDialog() {
_uiState.update { it.copy(showThemeDialog = false) }
}
fun onLocaleClicked() {
_uiState.update { it.copy(showLocaleDialog = true) }
}
fun onLocaleSelected(locale: Locale) {
_uiState.update { it.copy(locale = locale, showLocaleDialog = false) }
}
fun onDismissLocaleDialog() {
_uiState.update { it.copy(showLocaleDialog = false) }
}
fun onExportClicked() {
viewModelScope.launch {
try {
// 1. Fetch data from the service (suspend call)
val logs = LogRepository.logs
// 2. Pass the fetched data to the exporter to generate the CSV string
val csvContent = CsvExporter.exportLogsToCsv(logs.value)
// 3. Update the UI state with the generated content.
// MainActivity is already observing this and will trigger the file save.
_uiState.update { it.copy(pendingCsvContent = csvContent) }
} catch (e: Exception) {
// Handle any errors during fetching or exporting
android.util.Log.e("SettingsViewModel", "Failed to export CSV", e)
// Optionally show an error to the user
// _uiState.update { it.copy(error = "Export failed.") }
}
}
}
fun onExportHandled() {
// Clear the pending content after it has been saved or the user has cancelled.
_uiState.update { it.copy(pendingCsvContent = null) }
}
}