feat: verbessere Device-Setup-UX durch präzise Fokus-Navigation, Plug-and-Play-Optimierungen und Logging-Standardisierung
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 1m0s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 1m0s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
This commit is contained in:
+128
-80
@@ -11,7 +11,12 @@ import androidx.compose.material3.*
|
||||
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.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -21,6 +26,8 @@ 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(
|
||||
@@ -28,6 +35,8 @@ actual fun DeviceInitializationConfig(
|
||||
viewModel: DeviceInitializationViewModel
|
||||
) {
|
||||
val settings = uiState.settings
|
||||
val focusManager = LocalFocusManager.current
|
||||
val (deviceNameFocus, sharedKeyFocus, backupPathFocus) = remember { FocusRequester.createRefs() }
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
@@ -39,7 +48,10 @@ actual fun DeviceInitializationConfig(
|
||||
label = "Gerätename",
|
||||
placeholder = "z.B. Meldestelle-PC-1",
|
||||
isError = settings.deviceName.isNotEmpty() && !DeviceInitializationValidator.isNameValid(settings.deviceName),
|
||||
errorText = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich."
|
||||
errorText = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.",
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
keyboardActions = KeyboardActions(onNext = { sharedKeyFocus.requestFocus() }),
|
||||
modifier = Modifier.focusRequester(deviceNameFocus)
|
||||
)
|
||||
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
@@ -51,6 +63,15 @@ 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)
|
||||
}
|
||||
}),
|
||||
modifier = Modifier.focusRequester(sharedKeyFocus),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
@@ -62,14 +83,54 @@ actual fun DeviceInitializationConfig(
|
||||
)
|
||||
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||
Text("👥 Erwartete Clients (Richter, Zeitnehmer, etc.)", style = MaterialTheme.typography.titleSmall)
|
||||
Text(
|
||||
"Definiere, welche Geräte sich mit diesem Master synchronisieren dürfen.",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
OutlinedTextField(
|
||||
value = settings.backupPath,
|
||||
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) }
|
||||
),
|
||||
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}")
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Outlined.FolderOpen, contentDescription = "Verzeichnis wählen")
|
||||
}
|
||||
},
|
||||
isError = settings.backupPath.isNotEmpty() && !DeviceInitializationValidator.isBackupPathValid(settings.backupPath)
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Sync-Intervall: ${settings.syncInterval} Min.", style = MaterialTheme.typography.labelMedium)
|
||||
Slider(
|
||||
value = settings.syncInterval.toFloat(),
|
||||
onValueChange = { viewModel.updateSettings { s -> s.copy(syncInterval = it.toInt()) } },
|
||||
valueRange = 1f..60f,
|
||||
steps = 59
|
||||
)
|
||||
|
||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||
Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall)
|
||||
|
||||
settings.expectedClients.forEachIndexed { index, client ->
|
||||
ListItem(
|
||||
@@ -101,7 +162,11 @@ actual fun DeviceInitializationConfig(
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
IconButton(onClick = { viewModel.removeExpectedClient(index) }) {
|
||||
IconButton(onClick = {
|
||||
val clientName = settings.expectedClients[index].name
|
||||
viewModel.removeExpectedClient(index)
|
||||
println("[DeviceInit] Client entfernt: $clientName")
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Löschen",
|
||||
@@ -120,39 +185,58 @@ 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) {
|
||||
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)
|
||||
)
|
||||
|
||||
MsEnumDropdown(
|
||||
label = "Rolle",
|
||||
options = NetworkRole.entries.filter { it != NetworkRole.MASTER }.toTypedArray(),
|
||||
selectedOption = newClientRole,
|
||||
onOptionSelected = { newClientRole = it },
|
||||
modifier = Modifier.weight(0.5f)
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (newClientName.isNotBlank()) {
|
||||
viewModel.addExpectedClient(newClientName, newClientRole)
|
||||
newClientName = ""
|
||||
showAddClient = false
|
||||
}
|
||||
},
|
||||
enabled = newClientName.isNotBlank()
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
LaunchedEffect(Unit) { addClientNameFocus.requestFocus() }
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Add, null)
|
||||
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)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
showAddClient = false
|
||||
newClientName = ""
|
||||
}) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
if (newClientName.isNotBlank()) {
|
||||
viewModel.addExpectedClient(newClientName, newClientRole)
|
||||
println("[DeviceInit] Client hinzugefügt: $newClientName ($newClientRole)")
|
||||
newClientName = ""
|
||||
showAddClient = false
|
||||
}
|
||||
},
|
||||
enabled = newClientName.isNotBlank()
|
||||
) {
|
||||
Text("Client speichern")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -162,47 +246,6 @@ actual fun DeviceInitializationConfig(
|
||||
Text("Client hinzufügen")
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||
OutlinedTextField(
|
||||
value = settings.backupPath,
|
||||
onValueChange = { viewModel.updateSettings { s -> s.copy(backupPath = it) } },
|
||||
label = { Text("Backup-Verzeichnis (Pfad)") },
|
||||
placeholder = { Text("/pfad/zu/den/backups") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
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) {
|
||||
viewModel.updateSettings { s -> s.copy(backupPath = chooser.selectedFile.absolutePath) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("[Error] Fehler beim Öffnen des Verzeichnis-Wählers: ${e.message}")
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Outlined.FolderOpen, contentDescription = "Verzeichnis wählen")
|
||||
}
|
||||
},
|
||||
isError = settings.backupPath.isNotEmpty() && !DeviceInitializationValidator.isBackupPathValid(settings.backupPath)
|
||||
)
|
||||
|
||||
Text("Sync-Intervall: ${settings.syncInterval} Min.", style = MaterialTheme.typography.labelMedium)
|
||||
Slider(
|
||||
value = settings.syncInterval.toFloat(),
|
||||
onValueChange = { viewModel.updateSettings { s -> s.copy(syncInterval = it.toInt()) } },
|
||||
valueRange = 1f..60f,
|
||||
steps = 59
|
||||
)
|
||||
} else {
|
||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||
Text("🔍 Verfügbare Master im Netzwerk", style = MaterialTheme.typography.titleSmall)
|
||||
@@ -247,7 +290,10 @@ private fun MsSettingsField(
|
||||
placeholder: String,
|
||||
isError: Boolean,
|
||||
errorText: String,
|
||||
modifier: Modifier = Modifier,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
trailingIcon: @Composable (() -> Unit)? = null
|
||||
) {
|
||||
OutlinedTextField(
|
||||
@@ -255,9 +301,11 @@ private fun MsSettingsField(
|
||||
onValueChange = onValueChange,
|
||||
label = { Text(label) },
|
||||
placeholder = { Text(placeholder) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
isError = isError,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
trailingIcon = trailingIcon,
|
||||
supportingText = {
|
||||
if (isError) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"deviceName": "Meldestelle",
|
||||
"sharedKey": "Meldestelle",
|
||||
"sharedKey": "Password",
|
||||
"backupPath": "/mocode/meldestelle/docs/temp",
|
||||
"networkRole": "MASTER"
|
||||
}
|
||||
Reference in New Issue
Block a user