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:
+203
-81
@@ -10,6 +10,7 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||||||
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.automirrored.filled.ArrowBack
|
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.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
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.OnboardingSettings
|
||||||
import at.mocode.desktop.screens.onboarding.OnboardingValidator
|
import at.mocode.desktop.screens.onboarding.OnboardingValidator
|
||||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
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.print.PrintServiceLookup
|
||||||
import javax.swing.JFileChooser
|
import javax.swing.JFileChooser
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun OnboardingScreen(
|
fun OnboardingScreen(
|
||||||
settings: OnboardingSettings,
|
settings: OnboardingSettings,
|
||||||
onSettingsChange: (OnboardingSettings) -> Unit,
|
onSettingsChange: (OnboardingSettings) -> Unit,
|
||||||
onContinue: (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 {
|
DesktopThemeV2 {
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
Column(
|
Column(
|
||||||
@@ -50,27 +63,136 @@ fun OnboardingScreen(
|
|||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"Bitte konfiguriere deine lokale Instanz (Geburtsurkunde).",
|
if (currentStep == 0) "Schritt 1: Netzwerk-Rolle festlegen" else "Schritt 2: Rollenspezifische Konfiguration",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
"Wähle aus, ob dieses Gerät als Master (zentrale Datenbank) oder als Client fungiert.",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
|
||||||
|
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) }
|
var showPw by remember { mutableStateOf(false) }
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
// 2.1 / 2.2 IDENTITÄT & SICHERHEIT
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Text("🛡️ Identität & Sicherheit", style = MaterialTheme.typography.titleMedium)
|
Text("🛡️ Identität & Sicherheit", style = MaterialTheme.typography.titleMedium)
|
||||||
|
|
||||||
|
if (settings.networkRole == NetworkRole.MASTER) {
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = settings.geraetName,
|
value = settings.geraetName,
|
||||||
onValueChange = { onSettingsChange(settings.copy(geraetName = it)) },
|
onValueChange = { onSettingsChange(settings.copy(geraetName = it)) },
|
||||||
label = "Gerätename (Pflicht)",
|
label = "Gerätename (Pflicht)",
|
||||||
placeholder = "z. B. Meldestelle-PC-1",
|
placeholder = "z. B. Haupt-PC",
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
imeAction = ImeAction.Next,
|
imeAction = ImeAction.Next,
|
||||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.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 = settings.geraetName,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = "Gerätename (Vom Master freigegeben)",
|
||||||
|
trailingIcon = Icons.Default.ArrowDropDown,
|
||||||
|
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
|
||||||
|
)
|
||||||
|
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(slot) },
|
||||||
|
onClick = {
|
||||||
|
onSettingsChange(settings.copy(geraetName = slot))
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = settings.sharedKey,
|
value = settings.sharedKey,
|
||||||
@@ -87,65 +209,11 @@ fun OnboardingScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3.1 ERWARTETE GERÄTE (NUR MASTER)
|
||||||
|
if (settings.networkRole == NetworkRole.MASTER) {
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Text("⚙️ Lokale Einstellungen", style = MaterialTheme.typography.titleMedium)
|
Text("📋 Erwartete Geräte (Clients)", 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)
|
|
||||||
Text(
|
Text(
|
||||||
"Definiere hier, welche Geräte sich in diesem Netzwerk anmelden dürfen.",
|
"Definiere hier, welche Geräte sich in diesem Netzwerk anmelden dürfen.",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
@@ -195,7 +263,11 @@ fun OnboardingScreen(
|
|||||||
newList.removeAt(index)
|
newList.removeAt(index)
|
||||||
onSettingsChange(settings.copy(expectedClients = newList))
|
onSettingsChange(settings.copy(expectedClients = newList))
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Default.Delete, contentDescription = "Entfernen", tint = MaterialTheme.colorScheme.error)
|
Icon(
|
||||||
|
Icons.Default.Delete,
|
||||||
|
contentDescription = "Entfernen",
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,6 +285,59 @@ fun OnboardingScreen(
|
|||||||
Text("Gerät hinzufügen")
|
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) }
|
var showPrinterDialog by remember { mutableStateOf(false) }
|
||||||
val availablePrinters = remember {
|
val availablePrinters = remember {
|
||||||
@@ -222,7 +347,7 @@ fun OnboardingScreen(
|
|||||||
MsTextField(
|
MsTextField(
|
||||||
value = settings.defaultPrinter,
|
value = settings.defaultPrinter,
|
||||||
onValueChange = { onSettingsChange(settings.copy(defaultPrinter = it)) },
|
onValueChange = { onSettingsChange(settings.copy(defaultPrinter = it)) },
|
||||||
label = "🖨️ Standard-Drucker",
|
label = "Standard-Drucker",
|
||||||
placeholder = "Name des Standard-Druckers",
|
placeholder = "Name des Standard-Druckers",
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
trailingIcon = Icons.Default.Print,
|
trailingIcon = Icons.Default.Print,
|
||||||
@@ -253,10 +378,7 @@ fun OnboardingScreen(
|
|||||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
RadioButton(
|
RadioButton(selected = settings.defaultPrinter == printer, onClick = null)
|
||||||
selected = settings.defaultPrinter == printer,
|
|
||||||
onClick = null
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(printer)
|
Text(printer)
|
||||||
}
|
}
|
||||||
@@ -264,31 +386,31 @@ fun OnboardingScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = { TextButton(onClick = { showPrinterDialog = false }) { Text("Schließen") } }
|
||||||
TextButton(onClick = { showPrinterDialog = false }) {
|
|
||||||
Text("Schließen")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val canContinue = OnboardingValidator.canContinue(settings)
|
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(
|
Button(
|
||||||
onClick = { onContinue(settings) },
|
onClick = { onContinue(settings) },
|
||||||
enabled = canContinue,
|
enabled = OnboardingValidator.canContinue(settings)
|
||||||
modifier = Modifier.align(Alignment.End)
|
|
||||||
) {
|
) {
|
||||||
Text("Konfiguration speichern & starten")
|
Text("Konfiguration abschließen")
|
||||||
|
Icon(Icons.Default.Check, null, Modifier.padding(start = 8.dp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user