chore: implementiere Lockscreen-Logik für Geräte- und Veranstaltungsinitialisierung, füge Zustandsprüfungen und neue UI-Komponenten hinzu

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-20 23:37:54 +02:00
parent db58c24613
commit 5b8ef5ea2d
9 changed files with 469 additions and 69 deletions
@@ -9,6 +9,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -69,31 +70,45 @@ fun DeviceInitializationScreen(
NetworkRoleSelector(
selectedRole = uiState.settings.networkRole,
onRoleSelected = {
if (uiState.settings.networkRole != it && uiState.settings.deviceName.isNotBlank()) {
// Hier könnte ein Dialog kommen, aber fürs Erste einfach setzen
}
viewModel.setNetworkRole(it)
focusManager.moveFocus(FocusDirection.Next)
},
modifier = Modifier.focusRequester(roleSelectorFocus)
modifier = Modifier.focusRequester(roleSelectorFocus),
enabled = !uiState.isLocked
)
Button(
onClick = { viewModel.nextStep() },
modifier = Modifier
.align(Alignment.End)
.focusRequester(nextButtonFocus)
.onKeyEvent {
if ((it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) {
viewModel.nextStep()
true
} else false
}
) {
Text("Weiter")
Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null)
if (!uiState.isLocked) {
Button(
onClick = { viewModel.nextStep() },
modifier = Modifier
.align(Alignment.End)
.focusRequester(nextButtonFocus)
.onKeyEvent {
if ((it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) {
viewModel.nextStep()
true
} else false
}
) {
Text("Weiter")
Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null)
}
} else {
Button(
onClick = { viewModel.nextStep() },
modifier = Modifier.align(Alignment.End)
) {
Text("Zur Konfiguration")
Icon(Icons.AutoMirrored.Filled.ArrowForward, null)
}
}
}
}
} else {
// PHASE 2: ROLLENSPEZIFISCH (JVM spezifische Implementierung folgt)
// PHASE 2 & Review
DeviceInitializationConfig(
uiState = uiState,
viewModel = viewModel
@@ -110,12 +125,24 @@ fun DeviceInitializationScreen(
Text("Zurück zur Rollenauswahl")
}
Button(
onClick = { viewModel.completeInitialization() },
enabled = DeviceInitializationValidator.canContinue(uiState.settings)
) {
Text("Konfiguration abschließen")
Icon(Icons.Default.Check, null, Modifier.padding(start = 8.dp))
if (uiState.isLocked) {
Button(
onClick = { viewModel.unlockConfiguration() },
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary
)
) {
Text("Konfiguration bearbeiten")
Icon(Icons.Default.Edit, null, Modifier.padding(start = 8.dp))
}
} else {
Button(
onClick = { viewModel.completeInitialization() },
enabled = DeviceInitializationValidator.canContinue(uiState.settings)
) {
Text("Konfiguration finalisieren & Sperren")
Icon(Icons.Default.Check, null, Modifier.padding(start = 8.dp))
}
}
}
}
@@ -10,5 +10,6 @@ data class DeviceInitializationUiState(
val settings: DeviceInitializationSettings = DeviceInitializationSettings(),
val discoveredMasters: List<DiscoveredService> = emptyList(),
val isProcessing: Boolean = false,
val error: String? = null
val error: String? = null,
val isLocked: Boolean = false
)
@@ -1,16 +1,14 @@
@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
package at.mocode.frontend.features.device.initialization.presentation
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
import at.mocode.frontend.features.device.initialization.domain.model.NetworkRole
import at.mocode.frontend.features.device.initialization.domain.model.ExpectedClient
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
import at.mocode.frontend.features.device.initialization.domain.model.ExpectedClient
import at.mocode.frontend.features.device.initialization.domain.model.NetworkRole
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -75,7 +73,13 @@ class DeviceInitializationViewModel(
}
fun completeInitialization() {
println("[DeviceInit] Konfiguration abgeschlossen. Speichere Einstellungen...")
println("[DeviceInit] Konfiguration wird finalisiert...")
_uiState.update { it.copy(isLocked = true) }
onInitializationComplete(_uiState.value.settings)
}
fun unlockConfiguration() {
println("[DeviceInit] Konfiguration entsperrt für Änderungen.")
_uiState.update { it.copy(isLocked = false) }
}
}
@@ -18,16 +18,18 @@ import at.mocode.frontend.features.device.initialization.domain.model.NetworkRol
fun NetworkRoleSelector(
selectedRole: NetworkRole,
onRoleSelected: (NetworkRole) -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
NetworkRoleCard(
title = "Master (Host)",
description = "Verwaltet die zentrale Datenbank und koordiniert den Sync.",
isSelected = selectedRole == NetworkRole.MASTER,
onClick = { onRoleSelected(NetworkRole.MASTER) },
onClick = { if (enabled) onRoleSelected(NetworkRole.MASTER) },
enabled = enabled,
modifier = Modifier.onKeyEvent {
if ((it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) {
if (enabled && (it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) {
onRoleSelected(NetworkRole.MASTER)
true
} else false
@@ -38,9 +40,10 @@ fun NetworkRoleSelector(
title = "Client",
description = "Verbindet sich mit einem Master-Gerät im lokalen Netzwerk.",
isSelected = selectedRole == NetworkRole.CLIENT,
onClick = { onRoleSelected(NetworkRole.CLIENT) },
onClick = { if (enabled) onRoleSelected(NetworkRole.CLIENT) },
enabled = enabled,
modifier = Modifier.onKeyEvent {
if ((it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) {
if (enabled && (it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) {
onRoleSelected(NetworkRole.CLIENT)
true
} else false
@@ -55,24 +58,36 @@ private fun NetworkRoleCard(
description: String,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
Surface(
onClick = onClick,
enabled = enabled,
shape = MaterialTheme.shapes.medium,
color = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
color = when {
isSelected -> MaterialTheme.colorScheme.primaryContainer
!enabled -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
else -> MaterialTheme.colorScheme.surfaceVariant
},
modifier = modifier.fillMaxWidth()
) {
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = isSelected,
onClick = null
onClick = null,
enabled = enabled
)
Column {
Text(title, style = MaterialTheme.typography.labelLarge)
Text(
title,
style = MaterialTheme.typography.labelLarge,
color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
)
Text(
description,
style = MaterialTheme.typography.bodySmall
style = MaterialTheme.typography.bodySmall,
color = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
)
}
}
@@ -60,7 +60,8 @@ actual fun DeviceInitializationConfig(
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.",
imeAction = ImeAction.Next,
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
modifier = Modifier.focusRequester(deviceNameFocus)
modifier = Modifier.focusRequester(deviceNameFocus),
enabled = !uiState.isLocked
)
var passwordVisible by remember { mutableStateOf(false) }
@@ -71,14 +72,15 @@ actual fun DeviceInitializationConfig(
placeholder = "Mindestens 8 Zeichen",
isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey),
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.",
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
visualTransformation = if (passwordVisible || uiState.isLocked) VisualTransformation.None else PasswordVisualTransformation(),
imeAction = ImeAction.Next,
keyboardActions = KeyboardActions(
onNext = { focusManager.moveFocus(FocusDirection.Next) }
),
modifier = Modifier.focusRequester(sharedKeyFocus),
trailingIcon = if (passwordVisible) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
onTrailingIconClick = { passwordVisible = !passwordVisible }
onTrailingIconClick = { passwordVisible = !passwordVisible },
enabled = !uiState.isLocked
)
MsFilePicker(
@@ -88,7 +90,18 @@ actual fun DeviceInitializationConfig(
viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) }
},
directoryOnly = true,
modifier = Modifier.focusRequester(backupPathFocus)
modifier = Modifier.focusRequester(backupPathFocus),
enabled = !uiState.isLocked
)
MsTextField(
value = settings.defaultPrinter,
onValueChange = { viewModel.updateSettings { s -> s.copy(defaultPrinter = it) } },
label = "Standard-Drucker",
placeholder = "z.B. Brother-HL-L2350DW",
enabled = !uiState.isLocked,
imeAction = ImeAction.Done,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
)
if (settings.networkRole == NetworkRole.MASTER) {
@@ -97,21 +110,24 @@ actual fun DeviceInitializationConfig(
value = settings.syncInterval.toFloat(),
onValueChange = { viewModel.updateSettings { s -> s.copy(syncInterval = it.toInt()) } },
valueRange = 1f..60f,
steps = 59
steps = 59,
enabled = !uiState.isLocked
)
} else {
// Button zum Abschließen für Clients, da diese keinen Slider/Clients haben
Spacer(Modifier.height(8.dp))
Button(
onClick = { viewModel.completeInitialization() },
modifier = Modifier.fillMaxWidth(),
enabled = DeviceInitializationValidator.canContinue(settings)
) {
Text("Konfiguration abschließen")
if (!uiState.isLocked) {
Button(
onClick = { viewModel.completeInitialization() },
modifier = Modifier.fillMaxWidth(),
enabled = DeviceInitializationValidator.canContinue(settings)
) {
Text("Konfiguration abschließen")
}
}
}
if (settings.networkRole == NetworkRole.MASTER) {
if (settings.networkRole == NetworkRole.MASTER && !uiState.isLocked) {
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall)
@@ -255,6 +271,18 @@ actual fun DeviceInitializationConfig(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (settings.networkRole == NetworkRole.MASTER && uiState.isLocked) {
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall)
settings.expectedClients.forEach { client ->
ListItem(
headlineContent = { Text(client.name) },
trailingContent = {
SuggestionChip(onClick = {}, label = { Text(client.role.name) })
}
)
}
}
}
}
}