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:
parent
fdc016c786
commit
9ff21cbc4b
1 changed files with 175 additions and 78 deletions
|
|
@ -22,12 +22,15 @@ 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.Refresh
|
||||||
|
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
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
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.IconButton
|
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
|
||||||
|
|
@ -47,16 +50,21 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import de.bollwerk.app.BuildConfig
|
||||||
import de.bollwerk.app.data.sync.ConnectionState
|
import de.bollwerk.app.data.sync.ConnectionState
|
||||||
import de.bollwerk.app.domain.model.AgeGroup
|
import de.bollwerk.app.domain.model.AgeGroup
|
||||||
import de.bollwerk.app.domain.model.totalDailyKcal
|
import de.bollwerk.app.domain.model.totalDailyKcal
|
||||||
|
import de.bollwerk.app.ui.update.UpdateStatus
|
||||||
|
import de.bollwerk.app.ui.update.UpdateViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SettingsScreen(
|
internal fun SettingsScreen(
|
||||||
viewModel: SettingsViewModel = hiltViewModel()
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
|
updateViewModel: UpdateViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
val updateState by updateViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
|
@ -363,48 +371,12 @@ internal fun SettingsScreen(
|
||||||
uiState.serverUrl.isNotBlank() &&
|
uiState.serverUrl.isNotBlank() &&
|
||||||
!uiState.isSyncing
|
!uiState.isSyncing
|
||||||
|
|
||||||
ConnectionStatusIndicator(uiState = uiState)
|
SyncStatusCard(
|
||||||
|
uiState = uiState,
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
isSyncEnabled = isSyncEnabled,
|
||||||
|
onSyncClick = viewModel::pullSync
|
||||||
Text(
|
|
||||||
text = "Synchronisierung erfolgt automatisch.",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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) {
|
if (de.bollwerk.app.BuildConfig.FEATURE_CAMERA_ENABLED) {
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
|
@ -436,6 +408,90 @@ internal fun SettingsScreen(
|
||||||
color = MaterialTheme.colorScheme.secondary
|
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,46 +544,87 @@ internal fun SettingsScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ConnectionStatusIndicator(uiState: SettingsUiState) {
|
private fun SyncStatusCard(
|
||||||
Column {
|
uiState: SettingsUiState,
|
||||||
Row(
|
isSyncEnabled: Boolean,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
onSyncClick: () -> Unit
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
) {
|
||||||
) {
|
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||||
val (dotColor, statusText) = when (val state = uiState.connectionState) {
|
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
|
||||||
is ConnectionState.Connected -> Color(0xFF4CAF50) to "Verbunden"
|
Row(
|
||||||
is ConnectionState.Connecting -> Color(0xFFFFC107) to "Verbinde…"
|
modifier = Modifier.fillMaxWidth(),
|
||||||
is ConnectionState.Disconnected -> Color(0xFFF44336) to
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
"Keine Verbindung – nächster Versuch in ${state.reconnectInSeconds} Sek."
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
is ConnectionState.NotConfigured -> Color.Gray to "Nicht angemeldet"
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
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"
|
||||||
|
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(
|
||||||
Text(
|
visible = uiState.syncActivity != null,
|
||||||
text = "●",
|
enter = fadeIn(),
|
||||||
color = dotColor,
|
exit = fadeOut()
|
||||||
style = MaterialTheme.typography.bodyLarge
|
) {
|
||||||
)
|
uiState.syncActivity?.let { activity ->
|
||||||
Text(
|
Text(
|
||||||
text = statusText,
|
text = activity.text,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
color = if (activity.isError) MaterialTheme.colorScheme.error
|
||||||
}
|
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(start = 26.dp, top = 4.dp)
|
||||||
AnimatedVisibility(
|
)
|
||||||
visible = uiState.syncActivity != null,
|
}
|
||||||
enter = fadeIn(),
|
}
|
||||||
exit = fadeOut()
|
if (uiState.lastSyncTime != null) {
|
||||||
) {
|
HorizontalDivider(modifier = Modifier.padding(top = 12.dp, bottom = 8.dp))
|
||||||
uiState.syncActivity?.let { activity ->
|
|
||||||
Text(
|
Text(
|
||||||
text = activity.text,
|
text = "Letzte Synchronisierung: ${uiState.lastSyncTime}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = if (activity.isError) {
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
MaterialTheme.colorScheme.error
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(start = 20.dp, top = 4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue