feat(settings): server-sync UI aufräumen (#108)

- Login-Status + Logout auf eigene ElevatedCard
- Logout-Button als OutlinedButton (Material-Button statt TextLink)
- Letzte Sync direkt unter Verbindungsstatus ohne Divider
- Refresh-IconButton neben Serverstatus entfernt
- Server-URL Reset nur sichtbar wenn nicht-default Adresse
- Manuelle Sync-Buttons entfernt (vollautomatisch)
This commit is contained in:
Jens Reinemann 2026-05-18 10:09:58 +02:00
parent bdd8cb4b11
commit 887cdbd3f7

View file

@ -21,7 +21,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.SystemUpdate import androidx.compose.material.icons.filled.SystemUpdate
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
@ -31,7 +30,6 @@ import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
@ -290,31 +288,32 @@ internal fun SettingsScreen(
singleLine = true, singleLine = true,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Spacer(modifier = Modifier.width(8.dp)) val isNonDefaultUrl = uiState.serverUrl != de.bollwerk.app.domain.model.SettingsKey.DEFAULT_SERVER_URL
IconButton(onClick = viewModel::resetServerUrl) { if (isNonDefaultUrl) {
Icon( Spacer(modifier = Modifier.width(8.dp))
imageVector = Icons.Default.Refresh, TextButton(onClick = viewModel::resetServerUrl) {
contentDescription = "Standard-Server wiederherstellen" Text("Reset")
) }
} }
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
if (uiState.isLoggedIn) { if (uiState.isLoggedIn) {
Row( ElevatedCard(modifier = Modifier.fillMaxWidth()) {
modifier = Modifier.fillMaxWidth(), Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
verticalAlignment = Alignment.CenterVertically, Text(
horizontalArrangement = Arrangement.SpaceBetween text = "Angemeldet als: ${uiState.loggedInUsername}",
) { style = MaterialTheme.typography.bodyMedium,
Text( color = MaterialTheme.colorScheme.primary
text = "Angemeldet als: ${uiState.loggedInUsername}", )
style = MaterialTheme.typography.bodyMedium, Spacer(modifier = Modifier.height(8.dp))
color = MaterialTheme.colorScheme.primary, OutlinedButton(
modifier = Modifier.weight(1f) onClick = viewModel::logout,
) modifier = Modifier.fillMaxWidth()
TextButton(onClick = viewModel::logout) { ) {
Text("Abmelden") Text("Abmelden")
}
} }
} }
} else { } else {
@ -367,15 +366,7 @@ internal fun SettingsScreen(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
val isSyncEnabled = uiState.isLoggedIn && SyncStatusCard(uiState = uiState)
uiState.serverUrl.isNotBlank() &&
!uiState.isSyncing
SyncStatusCard(
uiState = uiState,
isSyncEnabled = isSyncEnabled,
onSyncClick = viewModel::pullSync
)
if (de.bollwerk.app.BuildConfig.FEATURE_CAMERA_ENABLED) { if (de.bollwerk.app.BuildConfig.FEATURE_CAMERA_ENABLED) {
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
@ -549,65 +540,56 @@ internal fun SettingsScreen(
@Composable @Composable
private fun SyncStatusCard( private fun SyncStatusCard(
uiState: SettingsUiState, uiState: SettingsUiState
isSyncEnabled: Boolean,
onSyncClick: () -> Unit
) { ) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) { ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Row( val (dotColor, statusText) = when (uiState.connectionState) {
verticalAlignment = Alignment.CenterVertically, is ConnectionState.Connected -> Color(0xFF4CAF50) to "Verbunden"
horizontalArrangement = Arrangement.spacedBy(10.dp) is ConnectionState.Connecting -> Color(0xFFFFC107) to "Verbinde…"
) { is ConnectionState.Disconnected -> Color(0xFFF44336) to "Keine Verbindung"
val (dotColor, statusText) = when (uiState.connectionState) { is ConnectionState.NotConfigured -> Color.Gray to "Nicht angemeldet"
is ConnectionState.Connected -> Color(0xFF4CAF50) to "Verbunden" }
is ConnectionState.Connecting -> Color(0xFFFFC107) to "Verbinde…" val disconnectedSeconds =
is ConnectionState.Disconnected -> Color(0xFFF44336) to "Keine Verbindung" (uiState.connectionState as? ConnectionState.Disconnected)?.reconnectInSeconds
is ConnectionState.NotConfigured -> Color.Gray to "Nicht angemeldet" Text(
} text = "",
val disconnectedSeconds = color = dotColor,
(uiState.connectionState as? ConnectionState.Disconnected)?.reconnectInSeconds style = MaterialTheme.typography.bodyLarge
)
Column {
Text( Text(
text = "", text = statusText,
color = dotColor, style = MaterialTheme.typography.bodyMedium
style = MaterialTheme.typography.bodyLarge
) )
Column { if (disconnectedSeconds != null) {
Text( Text(
text = statusText, text = "Nächster Versuch in $disconnectedSeconds Sek.",
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
if (disconnectedSeconds != null) {
Text(
text = "Nächster Versuch in $disconnectedSeconds Sek.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
} if (uiState.lastSyncTime != null) {
if (uiState.isSyncing) { Text(
CircularProgressIndicator( text = "Letzte Sync: ${uiState.lastSyncTime}",
modifier = Modifier.size(24.dp), style = MaterialTheme.typography.bodySmall,
strokeWidth = 2.dp color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
IconButton(
onClick = onSyncClick,
enabled = isSyncEnabled
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = "Jetzt synchronisieren"
) )
} }
} }
} }
if (uiState.isSyncing) {
Spacer(modifier = Modifier.height(8.dp))
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp
)
}
AnimatedVisibility( AnimatedVisibility(
visible = uiState.syncActivity != null, visible = uiState.syncActivity != null,
enter = fadeIn(), enter = fadeIn(),
@ -623,14 +605,6 @@ private fun SyncStatusCard(
) )
} }
} }
if (uiState.lastSyncTime != null) {
HorizontalDivider(modifier = Modifier.padding(top = 12.dp, bottom = 8.dp))
Text(
text = "Letzte Synchronisierung: ${uiState.lastSyncTime}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
} }
} }