feat(onboarding): Netzwerkrollen und automatisches Discovery im Onboarding hinzugefügt
- Unterstützung für Master- und Client-Rollen mit angepasster Konfiguration. - Automatische Dienstsuche (Discovery) für Clients implementiert. - Erweiterte UI für Drucker-, Backup- und Rollenspezifische Einstellungen. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+335
-213
@@ -10,6 +10,7 @@ import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -29,15 +30,27 @@ import at.mocode.desktop.screens.onboarding.NetworkRole
|
||||
import at.mocode.desktop.screens.onboarding.OnboardingSettings
|
||||
import at.mocode.desktop.screens.onboarding.OnboardingValidator
|
||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||
import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService
|
||||
import org.koin.compose.koinInject
|
||||
import javax.print.PrintServiceLookup
|
||||
import javax.swing.JFileChooser
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OnboardingScreen(
|
||||
settings: OnboardingSettings,
|
||||
onSettingsChange: (OnboardingSettings) -> Unit,
|
||||
onContinue: (OnboardingSettings) -> Unit,
|
||||
) {
|
||||
var currentStep by remember { mutableStateOf(0) }
|
||||
val discoveryService: NetworkDiscoveryService = koinInject()
|
||||
val discoveredServices by remember { mutableStateOf(discoveryService.getDiscoveredServices()) }
|
||||
|
||||
// Automatische Discovery starten, wenn wir auf Schritt 0 sind
|
||||
LaunchedEffect(currentStep) {
|
||||
if (currentStep == 0) discoveryService.startDiscovery()
|
||||
}
|
||||
|
||||
DesktopThemeV2 {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Column(
|
||||
@@ -50,245 +63,354 @@ fun OnboardingScreen(
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
"Bitte konfiguriere deine lokale Instanz (Geburtsurkunde).",
|
||||
if (currentStep == 0) "Schritt 1: Netzwerk-Rolle festlegen" else "Schritt 2: Rollenspezifische Konfiguration",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
var showPw by remember { mutableStateOf(false) }
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("🛡️ Identität & Sicherheit", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
MsTextField(
|
||||
value = settings.geraetName,
|
||||
onValueChange = { onSettingsChange(settings.copy(geraetName = it)) },
|
||||
label = "Gerätename (Pflicht)",
|
||||
placeholder = "z. B. Meldestelle-PC-1",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||
)
|
||||
|
||||
MsTextField(
|
||||
value = settings.sharedKey,
|
||||
onValueChange = { onSettingsChange(settings.copy(sharedKey = it)) },
|
||||
label = "Sicherheitsschlüssel (Pflicht)",
|
||||
placeholder = "Shared Secret für Netzwerk-Sync",
|
||||
trailingIcon = if (showPw) Icons.Default.VisibilityOff else Icons.Default.Visibility,
|
||||
onTrailingIconClick = { showPw = !showPw },
|
||||
visualTransformation = if (showPw) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("⚙️ Lokale Einstellungen", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
MsTextField(
|
||||
value = settings.backupPath,
|
||||
onValueChange = { onSettingsChange(settings.copy(backupPath = it)) },
|
||||
label = "💾 Datenbank-Sicherungspfad (Backup)",
|
||||
placeholder = "Pfad zum Backup-Verzeichnis",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
trailingIcon = Icons.Default.FolderOpen,
|
||||
onTrailingIconClick = {
|
||||
val chooser = JFileChooser()
|
||||
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||
chooser.dialogTitle = "Backup-Verzeichnis auswählen"
|
||||
if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
|
||||
onSettingsChange(settings.copy(backupPath = chooser.selectedFile.absolutePath))
|
||||
}
|
||||
},
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||
)
|
||||
|
||||
Text("🌐 Netzwerk-Rolle", style = MaterialTheme.typography.labelLarge)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = settings.networkRole == NetworkRole.MASTER,
|
||||
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.MASTER)) }
|
||||
)
|
||||
Text(
|
||||
"Master (Hostet lokale DB)",
|
||||
modifier = Modifier.clickable { onSettingsChange(settings.copy(networkRole = NetworkRole.MASTER)) })
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = settings.networkRole == NetworkRole.CLIENT,
|
||||
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.CLIENT)) }
|
||||
)
|
||||
Text(
|
||||
"Client",
|
||||
modifier = Modifier.clickable { onSettingsChange(settings.copy(networkRole = NetworkRole.CLIENT)) })
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Text("📡 Sync-Intervall: ${settings.syncInterval} Minuten", style = MaterialTheme.typography.labelLarge)
|
||||
Slider(
|
||||
value = settings.syncInterval.toFloat(),
|
||||
onValueChange = { onSettingsChange(settings.copy(syncInterval = it.toInt())) },
|
||||
valueRange = 1f..60f,
|
||||
steps = 59,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
Text("📋 Erwartete Geräte (Clients)", style = MaterialTheme.typography.titleSmall)
|
||||
if (currentStep == 0) {
|
||||
// PHASE 1: NETZWERK-ROLLE
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("🌐 Netzwerk-Rolle wählen", style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
"Definiere hier, welche Geräte sich in diesem Netzwerk anmelden dürfen.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
"Wähle aus, ob dieses Gerät als Master (zentrale Datenbank) oder als Client fungiert.",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
|
||||
settings.expectedClients.forEachIndexed { index, client ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Surface(
|
||||
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.MASTER)) },
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = if (settings.networkRole == NetworkRole.MASTER) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = settings.networkRole == NetworkRole.MASTER,
|
||||
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.MASTER)) }
|
||||
)
|
||||
Column {
|
||||
Text("Master (Host)", style = MaterialTheme.typography.labelLarge)
|
||||
Text(
|
||||
"Verwaltet die zentrale Datenbank und koordiniert den Sync.",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.CLIENT)) },
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = if (settings.networkRole == NetworkRole.CLIENT) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = settings.networkRole == NetworkRole.CLIENT,
|
||||
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.CLIENT)) }
|
||||
)
|
||||
Column {
|
||||
Text("Client", style = MaterialTheme.typography.labelLarge)
|
||||
Text(
|
||||
"Verbindet sich mit einem Master und synchronisiert Daten.",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { currentStep = 1 },
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
) {
|
||||
Text("Weiter")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowForward, null, Modifier.padding(start = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// PHASE 2: ROLLENSPEZIFISCH
|
||||
var showPw by remember { mutableStateOf(false) }
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
// 2.1 / 2.2 IDENTITÄT & SICHERHEIT
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("🛡️ Identität & Sicherheit", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
MsTextField(
|
||||
value = settings.geraetName,
|
||||
onValueChange = { onSettingsChange(settings.copy(geraetName = it)) },
|
||||
label = "Gerätename (Pflicht)",
|
||||
placeholder = "z. B. Haupt-PC",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||
)
|
||||
} else {
|
||||
// Client: Auswahlbox
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val availableSlots =
|
||||
discoveredServices.flatMap { it.metadata["availableSlots"]?.split(",") ?: emptyList() }
|
||||
.filter { it.isNotBlank() }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded }
|
||||
) {
|
||||
MsTextField(
|
||||
value = client.name,
|
||||
onValueChange = { newName ->
|
||||
val newList = settings.expectedClients.toMutableList()
|
||||
newList[index] = client.copy(name = newName)
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
},
|
||||
label = "Name",
|
||||
modifier = Modifier.weight(1f)
|
||||
value = settings.geraetName,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = "Gerätename (Vom Master freigegeben)",
|
||||
trailingIcon = Icons.Default.ArrowDropDown,
|
||||
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
|
||||
)
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
OutlinedButton(onClick = { expanded = true }) {
|
||||
Text(client.role.name)
|
||||
Icon(Icons.Default.ArrowDropDown, null)
|
||||
}
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
NetworkRole.entries.filter { it != NetworkRole.MASTER }.forEach { role ->
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
if (availableSlots.isEmpty()) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Suche nach verfügbaren Slots...") },
|
||||
onClick = { expanded = false }
|
||||
)
|
||||
} else {
|
||||
availableSlots.forEach { slot ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(role.name) },
|
||||
text = { Text(slot) },
|
||||
onClick = {
|
||||
val newList = settings.expectedClients.toMutableList()
|
||||
newList[index] = client.copy(role = role)
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
onSettingsChange(settings.copy(geraetName = slot))
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
val newList = settings.expectedClients.toMutableList()
|
||||
newList.removeAt(index)
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
}) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Entfernen", tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
val newList = settings.expectedClients.toMutableList()
|
||||
newList.add(ExpectedClient("Neues Gerät", NetworkRole.CLIENT))
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
},
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Add, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Gerät hinzufügen")
|
||||
}
|
||||
}
|
||||
|
||||
var showPrinterDialog by remember { mutableStateOf(false) }
|
||||
val availablePrinters = remember {
|
||||
PrintServiceLookup.lookupPrintServices(null, null).map { it.name }
|
||||
}
|
||||
|
||||
MsTextField(
|
||||
value = settings.defaultPrinter,
|
||||
onValueChange = { onSettingsChange(settings.copy(defaultPrinter = it)) },
|
||||
label = "🖨️ Standard-Drucker",
|
||||
placeholder = "Name des Standard-Druckers",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
trailingIcon = Icons.Default.Print,
|
||||
onTrailingIconClick = { showPrinterDialog = true },
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
if (OnboardingValidator.canContinue(settings)) onContinue(settings)
|
||||
})
|
||||
)
|
||||
|
||||
if (showPrinterDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showPrinterDialog = false },
|
||||
title = { Text("Drucker auswählen") },
|
||||
text = {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
if (availablePrinters.isEmpty()) {
|
||||
Text("Keine Drucker gefunden", style = MaterialTheme.typography.bodyMedium)
|
||||
} else {
|
||||
availablePrinters.forEach { printer ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onSettingsChange(settings.copy(defaultPrinter = printer))
|
||||
showPrinterDialog = false
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = settings.defaultPrinter == printer,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(printer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showPrinterDialog = false }) {
|
||||
Text("Schließen")
|
||||
}
|
||||
}
|
||||
MsTextField(
|
||||
value = settings.sharedKey,
|
||||
onValueChange = { onSettingsChange(settings.copy(sharedKey = it)) },
|
||||
label = "Sicherheitsschlüssel (Pflicht)",
|
||||
placeholder = "Shared Secret für Netzwerk-Sync",
|
||||
trailingIcon = if (showPw) Icons.Default.VisibilityOff else Icons.Default.Visibility,
|
||||
onTrailingIconClick = { showPw = !showPw },
|
||||
visualTransformation = if (showPw) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val canContinue = OnboardingValidator.canContinue(settings)
|
||||
Button(
|
||||
onClick = { onContinue(settings) },
|
||||
enabled = canContinue,
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
) {
|
||||
Text("Konfiguration speichern & starten")
|
||||
}
|
||||
// 3.1 ERWARTETE GERÄTE (NUR MASTER)
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("📋 Erwartete Geräte (Clients)", style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
"Definiere hier, welche Geräte sich in diesem Netzwerk anmelden dürfen.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
if (!canContinue) {
|
||||
Text(
|
||||
"Bitte alle Pflichtfelder korrekt ausfüllen (Name min. 3, Key min. 8, Backup-Pfad gesetzt).",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
settings.expectedClients.forEachIndexed { index, client ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
MsTextField(
|
||||
value = client.name,
|
||||
onValueChange = { newName ->
|
||||
val newList = settings.expectedClients.toMutableList()
|
||||
newList[index] = client.copy(name = newName)
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
},
|
||||
label = "Name",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
OutlinedButton(onClick = { expanded = true }) {
|
||||
Text(client.role.name)
|
||||
Icon(Icons.Default.ArrowDropDown, null)
|
||||
}
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
NetworkRole.entries.filter { it != NetworkRole.MASTER }.forEach { role ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(role.name) },
|
||||
onClick = {
|
||||
val newList = settings.expectedClients.toMutableList()
|
||||
newList[index] = client.copy(role = role)
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
val newList = settings.expectedClients.toMutableList()
|
||||
newList.removeAt(index)
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Entfernen",
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
val newList = settings.expectedClients.toMutableList()
|
||||
newList.add(ExpectedClient("Neues Gerät", NetworkRole.CLIENT))
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
},
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Add, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Gerät hinzufügen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4.1 / 3.2 DATENBANK-SICHERHEITSPFAD
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
if (settings.networkRole == NetworkRole.MASTER) "💾 Datenbank-Sicherheitspfad" else "💾 Lokaler Cache-Sicherungspfad",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
MsTextField(
|
||||
value = settings.backupPath,
|
||||
onValueChange = { onSettingsChange(settings.copy(backupPath = it)) },
|
||||
label = "Pfad auswählen",
|
||||
placeholder = "Verzeichnis für Backups/Cache",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
trailingIcon = Icons.Default.FolderOpen,
|
||||
onTrailingIconClick = {
|
||||
val chooser = JFileChooser()
|
||||
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||
chooser.dialogTitle = "Verzeichnis auswählen"
|
||||
if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
|
||||
onSettingsChange(settings.copy(backupPath = chooser.selectedFile.absolutePath))
|
||||
}
|
||||
},
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 5.1 SYNC-INTERVALL (NUR MASTER)
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("🔄 Sync-Intervall", style = MaterialTheme.typography.titleMedium)
|
||||
Text("📡 Intervall: ${settings.syncInterval} Minuten", style = MaterialTheme.typography.labelLarge)
|
||||
Slider(
|
||||
value = settings.syncInterval.toFloat(),
|
||||
onValueChange = { onSettingsChange(settings.copy(syncInterval = it.toInt())) },
|
||||
valueRange = 1f..60f,
|
||||
steps = 59,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6.1 / 4.2 DRUCKER
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("🖨️ Drucker", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
var showPrinterDialog by remember { mutableStateOf(false) }
|
||||
val availablePrinters = remember {
|
||||
PrintServiceLookup.lookupPrintServices(null, null).map { it.name }
|
||||
}
|
||||
|
||||
MsTextField(
|
||||
value = settings.defaultPrinter,
|
||||
onValueChange = { onSettingsChange(settings.copy(defaultPrinter = it)) },
|
||||
label = "Standard-Drucker",
|
||||
placeholder = "Name des Standard-Druckers",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
trailingIcon = Icons.Default.Print,
|
||||
onTrailingIconClick = { showPrinterDialog = true },
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
if (OnboardingValidator.canContinue(settings)) onContinue(settings)
|
||||
})
|
||||
)
|
||||
|
||||
if (showPrinterDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showPrinterDialog = false },
|
||||
title = { Text("Drucker auswählen") },
|
||||
text = {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
if (availablePrinters.isEmpty()) {
|
||||
Text("Keine Drucker gefunden", style = MaterialTheme.typography.bodyMedium)
|
||||
} else {
|
||||
availablePrinters.forEach { printer ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onSettingsChange(settings.copy(defaultPrinter = printer))
|
||||
showPrinterDialog = false
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(selected = settings.defaultPrinter == printer, onClick = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(printer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = { TextButton(onClick = { showPrinterDialog = false }) { Text("Schließen") } }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextButton(onClick = { currentStep = 0 }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Zurück zur Rollenauswahl")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { onContinue(settings) },
|
||||
enabled = OnboardingValidator.canContinue(settings)
|
||||
) {
|
||||
Text("Konfiguration abschließen")
|
||||
Icon(Icons.Default.Check, null, Modifier.padding(start = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user