ui: Server-Synchronisierung als ElevatedCard neu gestaltet

- Status-Dot + Verbindungstext links, Sync-Button (Refresh-Icon) rechts
  in einer kompakten Zeile statt verstreuter Einzelelemente
- Bei Disconnect: Haupttext 'Keine Verbindung', Countdown als
  zweite Zeile darunter (statt alles in einer langen Zeile)
- Sync-Spinner ersetzt den Button solange Sync läuft
- Aktivitätsmeldung animiert unter der Statuszeile
- Letzte-Sync-Zeit mit Divider am Card-Boden, klar abgetrennt
- 'Synchronisierung erfolgt automatisch.' entfernt (redundant)
This commit is contained in:
Jens Reinemann 2026-05-17 20:35:15 +02:00
parent fdc016c786
commit 9ff21cbc4b

View file

@ -22,12 +22,15 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.SystemUpdate
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
@ -47,16 +50,21 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.bollwerk.app.BuildConfig
import de.bollwerk.app.data.sync.ConnectionState
import de.bollwerk.app.domain.model.AgeGroup
import de.bollwerk.app.domain.model.totalDailyKcal
import de.bollwerk.app.ui.update.UpdateStatus
import de.bollwerk.app.ui.update.UpdateViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel()
viewModel: SettingsViewModel = hiltViewModel(),
updateViewModel: UpdateViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val updateState by updateViewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val filePickerLauncher = rememberLauncherForActivityResult(
@ -363,48 +371,12 @@ internal fun SettingsScreen(
uiState.serverUrl.isNotBlank() &&
!uiState.isSyncing
ConnectionStatusIndicator(uiState = uiState)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Synchronisierung erfolgt automatisch.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
SyncStatusCard(
uiState = uiState,
isSyncEnabled = isSyncEnabled,
onSyncClick = viewModel::pullSync
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedButton(
onClick = viewModel::pullSync,
enabled = isSyncEnabled,
) {
Text("Jetzt synchronisieren")
}
if (uiState.isSyncing) {
Spacer(modifier = Modifier.height(12.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Synchronisierung läuft…",
style = MaterialTheme.typography.bodySmall
)
}
}
if (uiState.lastSyncTime != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Letzte Synchronisierung: ${uiState.lastSyncTime}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (de.bollwerk.app.BuildConfig.FEATURE_CAMERA_ENABLED) {
Spacer(modifier = Modifier.height(32.dp))
@ -436,6 +408,90 @@ internal fun SettingsScreen(
color = MaterialTheme.colorScheme.secondary
)
}
Spacer(modifier = Modifier.height(32.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "App",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Version",
style = MaterialTheme.typography.bodyMedium
)
Text(
text = "v${BuildConfig.VERSION_NAME} (Build ${BuildConfig.VERSION_CODE})",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(8.dp))
when (val s = updateState.status) {
is UpdateStatus.Available -> Text(
text = "Update v${s.versionName} verfügbar",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
is UpdateStatus.Checking -> Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
Text("Suche nach Updates…", style = MaterialTheme.typography.bodySmall)
}
is UpdateStatus.Downloading -> Column {
Text("Lädt herunter…", style = MaterialTheme.typography.bodySmall)
Spacer(modifier = Modifier.height(4.dp))
LinearProgressIndicator(
progress = { s.progress },
modifier = Modifier.fillMaxWidth()
)
}
is UpdateStatus.ReadyToInstall -> Text(
text = "v${s.versionName} bereit zur Installation",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
is UpdateStatus.Error -> Text(
text = s.message,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
is UpdateStatus.Hidden -> {}
}
Spacer(modifier = Modifier.height(8.dp))
OutlinedButton(
onClick = updateViewModel::checkForUpdate,
enabled = updateState.status !is UpdateStatus.Checking &&
updateState.status !is UpdateStatus.Downloading,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.Default.SystemUpdate,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Auf Updates prüfen")
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}
@ -488,31 +544,66 @@ internal fun SettingsScreen(
}
@Composable
private fun ConnectionStatusIndicator(uiState: SettingsUiState) {
Column {
private fun SyncStatusCard(
uiState: SettingsUiState,
isSyncEnabled: Boolean,
onSyncClick: () -> Unit
) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
val (dotColor, statusText) = when (val state = uiState.connectionState) {
val (dotColor, statusText) = when (uiState.connectionState) {
is ConnectionState.Connected -> Color(0xFF4CAF50) to "Verbunden"
is ConnectionState.Connecting -> Color(0xFFFFC107) to "Verbinde…"
is ConnectionState.Disconnected -> Color(0xFFF44336) to
"Keine Verbindung nächster Versuch in ${state.reconnectInSeconds} Sek."
is ConnectionState.Disconnected -> Color(0xFFF44336) to "Keine Verbindung"
is ConnectionState.NotConfigured -> Color.Gray to "Nicht angemeldet"
}
val disconnectedSeconds =
(uiState.connectionState as? ConnectionState.Disconnected)?.reconnectInSeconds
Text(
text = "",
color = dotColor,
style = MaterialTheme.typography.bodyLarge
)
Column {
Text(
text = statusText,
style = MaterialTheme.typography.bodyMedium
)
if (disconnectedSeconds != null) {
Text(
text = "Nächster Versuch in $disconnectedSeconds Sek.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
if (uiState.isSyncing) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp
)
} else {
IconButton(
onClick = onSyncClick,
enabled = isSyncEnabled
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = "Jetzt synchronisieren"
)
}
}
}
AnimatedVisibility(
visible = uiState.syncActivity != null,
enter = fadeIn(),
@ -522,12 +613,18 @@ private fun ConnectionStatusIndicator(uiState: SettingsUiState) {
Text(
text = activity.text,
style = MaterialTheme.typography.bodySmall,
color = if (activity.isError) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
modifier = Modifier.padding(start = 20.dp, top = 4.dp)
color = if (activity.isError) MaterialTheme.colorScheme.error
else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 26.dp, top = 4.dp)
)
}
}
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
)
}
}