chore: entferne nicht genutzte NennungsMaske-Komponente, extrahiere AktionsButtonLeiste in separaten Komponentenordner

This commit is contained in:
2026-04-19 00:52:12 +02:00
parent 1b20e480f4
commit 64d749be3a
31 changed files with 2704 additions and 2970 deletions
@@ -8,25 +8,37 @@ 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.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.frontend.features.deviceinitialization.domain.DeviceInitializationValidator
import at.mocode.frontend.features.deviceinitialization.domain.NetworkRole
@Composable
fun DeviceInitializationScreen(
viewModel: DeviceInitializationViewModel
) {
val uiState by viewModel.uiState.collectAsState()
val focusManager = LocalFocusManager.current
val (roleSelectorFocus, nextButtonFocus) = remember { FocusRequester.createRefs() }
// Automatische Discovery starten, wenn wir auf Schritt 0 sind
LaunchedEffect(uiState.currentStep) {
if (uiState.currentStep == 0) viewModel.startDiscovery()
if (uiState.currentStep == 0) {
viewModel.startDiscovery()
roleSelectorFocus.requestFocus()
}
}
Surface(color = MaterialTheme.colorScheme.background) {
@@ -57,12 +69,24 @@ fun DeviceInitializationScreen(
NetworkRoleSelector(
selectedRole = uiState.settings.networkRole,
onRoleSelected = { viewModel.setNetworkRole(it) }
onRoleSelected = {
viewModel.setNetworkRole(it)
focusManager.moveFocus(FocusDirection.Next)
},
modifier = Modifier.focusRequester(roleSelectorFocus)
)
Button(
onClick = { viewModel.nextStep() },
modifier = Modifier.align(Alignment.End)
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)
@@ -32,28 +32,37 @@ class DeviceInitializationViewModel(
}
fun nextStep() {
println("[DeviceInit] Übergang zu Schritt ${uiState.value.currentStep + 1}")
_uiState.update { it.copy(currentStep = it.currentStep + 1) }
}
fun previousStep() {
println("[DeviceInit] Zurück zu Schritt ${(uiState.value.currentStep - 1).coerceAtLeast(0)}")
_uiState.update { it.copy(currentStep = (it.currentStep - 1).coerceAtLeast(0)) }
}
fun updateSettings(update: (DeviceInitializationSettings) -> DeviceInitializationSettings) {
_uiState.update { it.copy(settings = update(it.settings)) }
_uiState.update {
val newSettings = update(it.settings)
it.copy(settings = newSettings)
}
}
fun setNetworkRole(role: NetworkRole) {
println("[DeviceInit] Netzwerk-Rolle gesetzt: $role")
updateSettings { it.copy(networkRole = role) }
}
fun addExpectedClient(name: String, role: NetworkRole) {
println("[DeviceInit] Erwarteter Client hinzugefügt: $name ($role)")
updateSettings {
it.copy(expectedClients = it.expectedClients + ExpectedClient(name, role))
}
}
fun removeExpectedClient(index: Int) {
val client = _uiState.value.settings.expectedClients.getOrNull(index)
println("[DeviceInit] Erwarteter Client entfernt: ${client?.name}")
updateSettings {
val newList = it.expectedClients.toMutableList().apply { removeAt(index) }
it.copy(expectedClients = newList)
@@ -61,6 +70,7 @@ class DeviceInitializationViewModel(
}
fun completeInitialization() {
println("[DeviceInit] Konfiguration abgeschlossen. Speichere Einstellungen...")
onInitializationComplete(_uiState.value.settings)
}
}
@@ -8,27 +8,41 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.*
import androidx.compose.ui.unit.dp
import at.mocode.frontend.features.deviceinitialization.domain.NetworkRole
@Composable
fun NetworkRoleSelector(
selectedRole: NetworkRole,
onRoleSelected: (NetworkRole) -> Unit
onRoleSelected: (NetworkRole) -> Unit,
modifier: Modifier = Modifier
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
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 = { onRoleSelected(NetworkRole.MASTER) },
modifier = Modifier.onKeyEvent {
if ((it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) {
onRoleSelected(NetworkRole.MASTER)
true
} else false
}
)
NetworkRoleCard(
title = "Client",
description = "Verbindet sich mit einem Master-Gerät im lokalen Netzwerk.",
isSelected = selectedRole == NetworkRole.CLIENT,
onClick = { onRoleSelected(NetworkRole.CLIENT) }
onClick = { onRoleSelected(NetworkRole.CLIENT) },
modifier = Modifier.onKeyEvent {
if ((it.key == Key.Enter || it.key == Key.Spacebar) && it.type == KeyEventType.KeyUp) {
onRoleSelected(NetworkRole.CLIENT)
true
} else false
}
)
}
}
@@ -38,18 +52,19 @@ private fun NetworkRoleCard(
title: String,
description: String,
isSelected: Boolean,
onClick: () -> Unit
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Surface(
onClick = onClick,
shape = MaterialTheme.shapes.medium,
color = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.fillMaxWidth()
modifier = modifier.fillMaxWidth()
) {
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = isSelected,
onClick = onClick
onClick = null
)
Column {
Text(title, style = MaterialTheme.typography.labelLarge)
@@ -1,6 +1,8 @@
package at.mocode.frontend.features.deviceinitialization.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
@@ -13,7 +15,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component3
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component4
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component5
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
@@ -26,8 +34,6 @@ import at.mocode.frontend.features.deviceinitialization.domain.NetworkRole
import java.io.File
import javax.swing.JFileChooser
import javax.swing.UIManager
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@Composable
actual fun DeviceInitializationConfig(
@@ -36,7 +42,11 @@ actual fun DeviceInitializationConfig(
) {
val settings = uiState.settings
val focusManager = LocalFocusManager.current
val (deviceNameFocus, sharedKeyFocus, backupPathFocus) = remember { FocusRequester.createRefs() }
val (deviceNameFocus, sharedKeyFocus, backupPathFocus, clientNameFocus, clientRoleFocus) = remember { FocusRequester.createRefs() }
LaunchedEffect(Unit) {
deviceNameFocus.requestFocus()
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
@@ -50,8 +60,15 @@ actual fun DeviceInitializationConfig(
isError = settings.deviceName.isNotEmpty() && !DeviceInitializationValidator.isNameValid(settings.deviceName),
errorText = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.",
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { sharedKeyFocus.requestFocus() }),
modifier = Modifier.focusRequester(deviceNameFocus)
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
modifier = Modifier.focusRequester(deviceNameFocus).onKeyEvent {
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
focusManager.moveFocus(FocusDirection.Next)
true
} else {
false
}
}
)
var passwordVisible by remember { mutableStateOf(false) }
@@ -63,15 +80,31 @@ actual fun DeviceInitializationConfig(
isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey),
errorText = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.",
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = {
if (settings.networkRole == NetworkRole.MASTER) {
backupPathFocus.requestFocus()
} else {
focusManager.moveFocus(FocusDirection.Next)
keyboardOptions = KeyboardOptions(
imeAction = if (settings.networkRole == NetworkRole.MASTER) ImeAction.Next else ImeAction.Done
),
keyboardActions = KeyboardActions(
onNext = { focusManager.moveFocus(FocusDirection.Next) },
onDone = {
if (DeviceInitializationValidator.canContinue(settings)) {
viewModel.completeInitialization()
} else {
focusManager.clearFocus()
}
}
}),
modifier = Modifier.focusRequester(sharedKeyFocus),
),
modifier = Modifier.focusRequester(sharedKeyFocus).onKeyEvent {
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
if (settings.networkRole == NetworkRole.MASTER) {
focusManager.moveFocus(FocusDirection.Next)
} else if (DeviceInitializationValidator.canContinue(settings)) {
viewModel.completeInitialization()
}
true
} else {
false
}
},
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
@@ -88,31 +121,24 @@ actual fun DeviceInitializationConfig(
onValueChange = { viewModel.updateSettings { s -> s.copy(backupPath = it) } },
label = { Text("Backup-Verzeichnis (Pfad)") },
placeholder = { Text("/pfad/zu/den/backups") },
modifier = Modifier.fillMaxWidth().focusRequester(backupPathFocus),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(
onNext = { focusManager.moveFocus(FocusDirection.Next) }
),
modifier = Modifier.fillMaxWidth().focusRequester(backupPathFocus).onKeyEvent {
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
selectBackupPath(settings.backupPath) { selectedPath ->
viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) }
}
true
} else {
false
}
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(
onNext = { focusManager.moveFocus(FocusDirection.Next) }
),
trailingIcon = {
IconButton(onClick = {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
val chooser = JFileChooser().apply {
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
dialogTitle = "Backup-Verzeichnis wählen"
if (settings.backupPath.isNotEmpty()) {
val currentDir = File(settings.backupPath)
if (currentDir.exists()) currentDirectory = currentDir
}
}
val result = chooser.showOpenDialog(null)
if (result == JFileChooser.APPROVE_OPTION) {
val selectedPath = chooser.selectedFile.absolutePath
viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) }
println("[DeviceInit] Backup-Verzeichnis gewählt: $selectedPath")
}
} catch (e: Exception) {
println("[DeviceInit] [Error] Fehler beim Öffnen des Verzeichnis-Wählers: ${e.message}")
selectBackupPath(settings.backupPath) { selectedPath ->
viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) }
}
}) {
Icon(Icons.Outlined.FolderOpen, contentDescription = "Verzeichnis wählen")
@@ -185,33 +211,27 @@ actual fun DeviceInitializationConfig(
var newClientName by remember { mutableStateOf("") }
var newClientRole by remember { mutableStateOf(NetworkRole.RICHTER) }
var showAddClient by remember { mutableStateOf(false) }
val addClientNameFocus = remember { FocusRequester() }
if (showAddClient) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
LaunchedEffect(Unit) { addClientNameFocus.requestFocus() }
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = newClientName,
onValueChange = { newClientName = it },
label = { Text("Gerätename des Clients") },
modifier = Modifier.weight(1f).focusRequester(addClientNameFocus),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
)
MsEnumDropdown(
label = "Rolle",
options = NetworkRole.entries.filter { it != NetworkRole.MASTER }.toTypedArray(),
selectedOption = newClientRole,
onOptionSelected = { newClientRole = it },
modifier = Modifier.weight(0.5f)
)
}
LaunchedEffect(Unit) { clientNameFocus.requestFocus() }
ClientEntryRow(
name = newClientName,
onNameChange = { newClientName = it },
role = newClientRole,
onRoleChange = { newClientRole = it },
focusManager = focusManager,
clientNameFocus = clientNameFocus,
clientRoleFocus = clientRoleFocus,
onEnter = {
if (newClientName.isNotBlank()) {
viewModel.addExpectedClient(newClientName, newClientRole)
println("[DeviceInit] Client hinzugefügt: $newClientName ($newClientRole)")
newClientName = ""
showAddClient = false
}
}
)
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
@@ -282,6 +302,48 @@ actual fun DeviceInitializationConfig(
}
}
@Composable
private fun ClientEntryRow(
name: String,
onNameChange: (String) -> Unit,
role: NetworkRole,
onRoleChange: (NetworkRole) -> Unit,
focusManager: androidx.compose.ui.focus.FocusManager,
clientNameFocus: FocusRequester,
clientRoleFocus: FocusRequester,
onEnter: () -> Unit
) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Gerätename des Clients") },
modifier = Modifier.weight(1f).focusRequester(clientNameFocus),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
)
MsEnumDropdown(
label = "Rolle",
options = NetworkRole.entries.filter { it != NetworkRole.MASTER }.toTypedArray(),
selectedOption = role,
onOptionSelected = onRoleChange,
modifier = Modifier.weight(0.5f).focusRequester(clientRoleFocus).onKeyEvent {
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
onEnter()
true
} else {
false
}
}
)
}
}
@Composable
private fun MsSettingsField(
value: String,
@@ -314,3 +376,25 @@ private fun MsSettingsField(
}
)
}
private fun selectBackupPath(currentPath: String, onPathSelected: (String) -> Unit) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
val chooser = JFileChooser().apply {
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
dialogTitle = "Backup-Verzeichnis wählen"
if (currentPath.isNotEmpty()) {
val currentDir = File(currentPath)
if (currentDir.exists()) currentDirectory = currentDir
}
}
val result = chooser.showOpenDialog(null)
if (result == JFileChooser.APPROVE_OPTION) {
val selectedPath = chooser.selectedFile.absolutePath
onPathSelected(selectedPath)
println("[DeviceInit] Backup-Verzeichnis gewählt: $selectedPath")
}
} catch (e: Exception) {
println("[DeviceInit] [Error] Fehler beim Öffnen des Verzeichnis-Wählers: ${e.message}")
}
}