feat(core, device-initialization): Netzwerk-Discovery verbessert, IP-Binding hinzugefügt und UI optimiert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+15
-5
@@ -144,7 +144,7 @@ fun DeviceInitializationScreen(
|
||||
onClick = { viewModel.updateSettings { it.copy(appTheme = theme) } },
|
||||
label = {
|
||||
Text(
|
||||
when(theme) {
|
||||
when (theme) {
|
||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.SYSTEM -> "System"
|
||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.LIGHT -> "Hell"
|
||||
at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting.DARK -> "Dunkel"
|
||||
@@ -179,13 +179,23 @@ fun DeviceInitializationScreen(
|
||||
if (!uiState.isLocked) {
|
||||
val role = uiState.settings.networkRole
|
||||
val hasDiscoveries = uiState.discoveredMasters.isNotEmpty()
|
||||
val selectedInterface = uiState.settings.networkInterface
|
||||
|
||||
LaunchedEffect(selectedInterface, role) {
|
||||
if (selectedInterface.isNotEmpty()) {
|
||||
viewModel.startDiscovery()
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = if (hasDiscoveries) MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.15f)
|
||||
else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.1f),
|
||||
else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.1f),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
border = if (hasDiscoveries) androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.2f))
|
||||
else null
|
||||
border = if (hasDiscoveries) androidx.compose.foundation.BorderStroke(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)
|
||||
)
|
||||
else null
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
@@ -203,7 +213,7 @@ fun DeviceInitializationScreen(
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (hasDiscoveries) MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.7f)
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+24
-7
@@ -22,16 +22,20 @@ class DeviceInitializationViewModel(
|
||||
val uiState: StateFlow<DeviceInitializationUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val _initializationCompleteEvent = MutableSharedFlow<DeviceInitializationSettings>()
|
||||
val initializationCompleteEvent: SharedFlow<DeviceInitializationSettings> = _initializationCompleteEvent.asSharedFlow()
|
||||
val initializationCompleteEvent: SharedFlow<DeviceInitializationSettings> =
|
||||
_initializationCompleteEvent.asSharedFlow()
|
||||
|
||||
init {
|
||||
val existingSettings = at.mocode.frontend.features.device.initialization.data.local.DeviceInitializationSettingsManager.loadSettings()
|
||||
val existingSettings =
|
||||
at.mocode.frontend.features.device.initialization.data.local.DeviceInitializationSettingsManager.loadSettings()
|
||||
if (existingSettings != null) {
|
||||
println("[DeviceInit] Bestehende Einstellungen geladen.")
|
||||
_uiState.update { it.copy(
|
||||
settings = existingSettings,
|
||||
isLocked = existingSettings.isConfigured
|
||||
) }
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
settings = existingSettings,
|
||||
isLocked = existingSettings.isConfigured
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
@@ -43,7 +47,20 @@ class DeviceInitializationViewModel(
|
||||
}
|
||||
|
||||
fun startDiscovery() {
|
||||
discoveryService.startDiscovery()
|
||||
val selectedInterface = uiState.value.settings.networkInterface
|
||||
val ip = if (selectedInterface.contains("(") && selectedInterface.contains(")")) {
|
||||
selectedInterface.substringAfter("(").substringBefore(")")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
println("[DeviceInit] Starte/Restart Discovery für IP: $ip (Interface: $selectedInterface)")
|
||||
discoveryService.stopDiscovery()
|
||||
discoveryService.startDiscovery(ip)
|
||||
|
||||
// Falls wir ein Master sind, registrieren wir uns auch direkt, damit andere uns finden
|
||||
if (uiState.value.settings.networkRole == NetworkRole.MASTER) {
|
||||
discoveryService.registerService(8080, ip)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+111
-37
@@ -2,10 +2,13 @@
|
||||
|
||||
package at.mocode.frontend.features.device.initialization.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Usb
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
@@ -22,6 +25,7 @@ import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -82,32 +86,94 @@ actual fun DeviceInitializationConfig(
|
||||
.filter { it.isUp && !it.isLoopback && it.inetAddresses.hasMoreElements() }
|
||||
.map { ni ->
|
||||
val friendlyName = when {
|
||||
ni.displayName.contains("wlan", ignoreCase = true) || ni.displayName.contains("wi-fi", ignoreCase = true) -> "WLAN"
|
||||
ni.displayName.contains("eth", ignoreCase = true) || ni.displayName.contains("ethernet", ignoreCase = true) -> "Ethernet"
|
||||
else -> ni.displayName
|
||||
ni.displayName.contains("wlan", ignoreCase = true) || ni.displayName.contains(
|
||||
"wi-fi",
|
||||
ignoreCase = true
|
||||
) || ni.name.contains("wlan", ignoreCase = true) -> "🌐 WLAN"
|
||||
|
||||
ni.displayName.contains("eth", ignoreCase = true) || ni.displayName.contains(
|
||||
"ethernet",
|
||||
ignoreCase = true
|
||||
) || ni.name.contains("eth", ignoreCase = true) || ni.name.contains(
|
||||
"en",
|
||||
ignoreCase = true
|
||||
) -> "🔌 Ethernet"
|
||||
|
||||
else -> "💻 " + ni.displayName
|
||||
}
|
||||
val address = ni.inetAddresses.asSequence()
|
||||
.filter { !it.isLinkLocalAddress && it.hostAddress.indexOf(':') == -1 } // Nur IPv4, keine Link-Local
|
||||
.firstOrNull()?.hostAddress ?: ni.inetAddresses.nextElement().hostAddress
|
||||
"$friendlyName ($address)"
|
||||
val address =
|
||||
ni.inetAddresses.asSequence()
|
||||
.firstOrNull { !it.isLinkLocalAddress && it.hostAddress.indexOf(':') == -1 }?.hostAddress
|
||||
?: ni.inetAddresses.nextElement().hostAddress
|
||||
|
||||
val isConnected = !ni.isLoopback && ni.isUp && ni.interfaceAddresses.any {
|
||||
it.address.isSiteLocalAddress || it.address.hostAddress.startsWith("192.168") || it.address.hostAddress.startsWith(
|
||||
"10."
|
||||
)
|
||||
}
|
||||
|
||||
InterfaceInfo(
|
||||
id = "$friendlyName ($address)",
|
||||
name = friendlyName,
|
||||
address = address,
|
||||
hardwareName = ni.name,
|
||||
isConnected = isConnected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(interfaces) {
|
||||
if (settings.networkInterface.isEmpty() && interfaces.isNotEmpty()) {
|
||||
viewModel.updateSettings { s -> s.copy(networkInterface = interfaces.first()) }
|
||||
val bestMatch = interfaces.find { it.isConnected } ?: interfaces.first()
|
||||
viewModel.updateSettings { s -> s.copy(networkInterface = bestMatch.id) }
|
||||
}
|
||||
}
|
||||
|
||||
MsStringDropdown(
|
||||
label = "Netzwerk-Interface",
|
||||
helpDescription = "Wähle das Netzwerk-Interface aus, über das die App kommunizieren soll (z.B. LAN für das Turnier-Netzwerk).",
|
||||
options = interfaces,
|
||||
selectedOption = settings.networkInterface,
|
||||
onOptionSelected = { viewModel.updateSettings { s -> s.copy(networkInterface = it) } },
|
||||
placeholder = "Interface wählen...",
|
||||
enabled = !uiState.isLocked
|
||||
)
|
||||
Text("🌐 Netzwerk-Interface", style = MaterialTheme.typography.titleSmall)
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
interfaces.forEach { info ->
|
||||
val isSelected = settings.networkInterface == info.id
|
||||
Surface(
|
||||
onClick = { if (!uiState.isLocked) viewModel.updateSettings { s -> s.copy(networkInterface = info.id) } },
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant.copy(
|
||||
alpha = 0.3f
|
||||
),
|
||||
border = if (isSelected) androidx.compose.foundation.BorderStroke(
|
||||
2.dp,
|
||||
MaterialTheme.colorScheme.primary
|
||||
) else null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(
|
||||
color = if (info.isConnected) Color(0xFF4CAF50) else Color(0xFFF44336),
|
||||
shape = CircleShape
|
||||
)
|
||||
)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(info.name, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold)
|
||||
Text("IP: ${info.address} (${info.hardwareName})", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
if (isSelected) {
|
||||
Icon(Icons.Default.CheckCircle, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (interfaces.isEmpty()) {
|
||||
Text("⚠️ Kein aktives Netzwerk-Interface gefunden!", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
MsTextField(
|
||||
@@ -143,30 +209,30 @@ actual fun DeviceInitializationConfig(
|
||||
)
|
||||
|
||||
if (!uiState.isLocked && settings.backupPath.isNotBlank() && settings.sharedKey.isNotBlank()) {
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.testUsbBackup() },
|
||||
modifier = Modifier.padding(top = 4.dp).align(Alignment.End),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Usb, null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Plan-USB Test-Export", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.testUsbBackup() },
|
||||
modifier = Modifier.padding(top = 4.dp).align(Alignment.End),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Usb, null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Plan-USB Test-Export", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
}
|
||||
|
||||
val printers = remember {
|
||||
val systemPrinters = PrintServiceLookup.lookupPrintServices(null, null).map { it.name }.toMutableList()
|
||||
if (!systemPrinters.contains("PDF-Export (Lokal)")) {
|
||||
systemPrinters.add(0, "PDF-Export (Lokal)")
|
||||
}
|
||||
systemPrinters.sortedBy { it != "PDF-Export (Lokal)" } // PDF immer oben
|
||||
val printers = remember {
|
||||
val systemPrinters = PrintServiceLookup.lookupPrintServices(null, null).map { it.name }.toMutableList()
|
||||
if (!systemPrinters.contains("PDF-Export (Lokal)")) {
|
||||
systemPrinters.add(0, "PDF-Export (Lokal)")
|
||||
}
|
||||
systemPrinters.sortedBy { it != "PDF-Export (Lokal)" } // PDF immer oben
|
||||
}
|
||||
|
||||
LaunchedEffect(printers) {
|
||||
if (settings.defaultPrinter.isEmpty() && printers.isNotEmpty()) {
|
||||
viewModel.updateSettings { s -> s.copy(defaultPrinter = printers.first()) }
|
||||
}
|
||||
LaunchedEffect(printers) {
|
||||
if (settings.defaultPrinter.isEmpty() && printers.isNotEmpty()) {
|
||||
viewModel.updateSettings { s -> s.copy(defaultPrinter = printers.first()) }
|
||||
}
|
||||
}
|
||||
|
||||
MsStringDropdown(
|
||||
label = "Standard-Drucker",
|
||||
@@ -303,7 +369,7 @@ actual fun DeviceInitializationConfig(
|
||||
Text("Client hinzufügen")
|
||||
}
|
||||
}
|
||||
} else if (settings.networkRole != NetworkRole.MASTER && !uiState.isLocked) {
|
||||
} else if (settings.networkRole != NetworkRole.MASTER && !uiState.isLocked) {
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
Text("🔍 Verfügbare Master im Netzwerk", style = MaterialTheme.typography.titleSmall)
|
||||
|
||||
@@ -366,6 +432,14 @@ actual fun DeviceInitializationConfig(
|
||||
}
|
||||
}
|
||||
|
||||
private data class InterfaceInfo(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val address: String,
|
||||
val hardwareName: String,
|
||||
val isConnected: Boolean
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun ClientEntryRow(
|
||||
name: String,
|
||||
|
||||
Reference in New Issue
Block a user