feat(desktop, device-initialization): Tools-Menü mit Backup-Option und Reset-Funktion ergänzt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+63
-33
@@ -8,9 +8,12 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.filechooser.FileNameExtensionFilter
|
||||
|
||||
@Composable
|
||||
actual fun MsFilePicker(
|
||||
@@ -23,17 +26,35 @@ actual fun MsFilePicker(
|
||||
enabled: Boolean,
|
||||
modifier: Modifier
|
||||
) {
|
||||
val currentValue = selectedPath ?: ""
|
||||
val (isError, errorMessage) = run {
|
||||
if (!enabled) false to null
|
||||
else if (currentValue.isBlank()) false to null
|
||||
else {
|
||||
val f = File(currentValue)
|
||||
if (directoryOnly) {
|
||||
val ok = f.exists() && f.isDirectory && f.canWrite()
|
||||
(!ok) to if (!ok) "Ordner existiert nicht oder ist nicht beschreibbar" else null
|
||||
} else {
|
||||
val ok = (f.exists() && f.isFile && f.canWrite()) || (f.parentFile?.canWrite() == true)
|
||||
(!ok) to if (!ok) "Datei/Ordner nicht beschreibbar" else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MsTextField(
|
||||
value = selectedPath ?: "",
|
||||
onValueChange = { },
|
||||
readOnly = true,
|
||||
value = currentValue,
|
||||
onValueChange = { newValue -> onFileSelected(newValue) },
|
||||
readOnly = false,
|
||||
label = label,
|
||||
helpDescription = helpDescription,
|
||||
placeholder = if (directoryOnly) "Verzeichnis wählen..." else "Datei wählen...",
|
||||
isError = isError,
|
||||
errorMessage = errorMessage,
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = enabled,
|
||||
compact = true
|
||||
@@ -43,40 +64,49 @@ actual fun MsFilePicker(
|
||||
|
||||
MsButton(
|
||||
onClick = {
|
||||
if (directoryOnly) {
|
||||
// AWT FileDialog für nativen Look auch bei Verzeichnissen (Windows/Linux/macOS)
|
||||
// unter macOS erzwingt dies die Verzeichnisauswahl. Unter Windows/Linux ist es der Standard-Dialog.
|
||||
System.setProperty("apple.awt.fileDialogForDirectories", "true")
|
||||
val dialog = FileDialog(null as Frame?, label, FileDialog.LOAD).apply {
|
||||
selectedPath?.let {
|
||||
val currentDir = File(it)
|
||||
if (currentDir.exists()) {
|
||||
directory = currentDir.absolutePath
|
||||
// Einheitlich plattformübergreifend: Swing JFileChooser verwenden
|
||||
SwingUtilities.invokeLater {
|
||||
val chooser = JFileChooser().apply {
|
||||
isMultiSelectionEnabled = false
|
||||
// Initiales Verzeichnis/Pfad
|
||||
selectedPath?.let { p ->
|
||||
val f = File(p)
|
||||
currentDirectory = when {
|
||||
f.isDirectory -> f
|
||||
f.parentFile?.isDirectory == true -> f.parentFile
|
||||
else -> currentDirectory
|
||||
}
|
||||
if (!directoryOnly && f.isFile) selectedFile = f
|
||||
}
|
||||
|
||||
if (directoryOnly) {
|
||||
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||
dialogType = JFileChooser.SAVE_DIALOG // ermöglicht "Neuer Ordner"
|
||||
} else {
|
||||
fileSelectionMode = JFileChooser.FILES_ONLY
|
||||
if (fileExtensions.isNotEmpty()) {
|
||||
fileFilter = FileNameExtensionFilter(
|
||||
"Erlaubte Dateien",
|
||||
*fileExtensions.map { it.trimStart('.') }.toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog.isVisible = true
|
||||
if (dialog.directory != null && dialog.file != null) {
|
||||
// Bei FileDialog.LOAD unter Windows/Linux wählt man oft eine Datei im Ordner,
|
||||
// aber wir wollen den Ordner. Wir nehmen also das Verzeichnis.
|
||||
onFileSelected(File(dialog.directory, dialog.file).parentFile.absolutePath)
|
||||
} else if (dialog.directory != null) {
|
||||
onFileSelected(dialog.directory)
|
||||
}
|
||||
System.setProperty("apple.awt.fileDialogForDirectories", "false")
|
||||
} else {
|
||||
// AWT FileDialog für nativen Look bei Dateiauswahl (wie vom User gewünscht)
|
||||
val dialog = FileDialog(null as Frame?, label, FileDialog.LOAD).apply {
|
||||
if (fileExtensions.isNotEmpty()) {
|
||||
setFilenameFilter { _, name ->
|
||||
fileExtensions.any { name.lowercase().endsWith(it.lowercase()) }
|
||||
|
||||
val result = chooser.showDialog(null, "Auswählen")
|
||||
if (result == JFileChooser.APPROVE_OPTION) {
|
||||
val chosen = chooser.selectedFile
|
||||
if (directoryOnly) {
|
||||
if (!chosen.exists()) {
|
||||
try {
|
||||
Files.createDirectories(Path.of(chosen.absolutePath))
|
||||
} catch (_: Exception) { /* ignorieren, Validierung zeigt Fehler */ }
|
||||
}
|
||||
onFileSelected(chosen.absolutePath)
|
||||
} else {
|
||||
onFileSelected(chosen.absolutePath)
|
||||
}
|
||||
}
|
||||
dialog.isVisible = true
|
||||
if (dialog.file != null) {
|
||||
onFileSelected(File(dialog.directory, dialog.file).absolutePath)
|
||||
}
|
||||
}
|
||||
},
|
||||
text = "Durchsuchen",
|
||||
|
||||
+38
-6
@@ -13,6 +13,11 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Überwacht die Konnektivität zum API-Gateway.
|
||||
*
|
||||
* Robustere Strategie:
|
||||
* 1) /actuator/health/readiness
|
||||
* 2) /actuator/health (Fallback)
|
||||
* 3) /api/ping/simple (Fallback)
|
||||
*/
|
||||
class ConnectivityTracker : KoinComponent {
|
||||
private val client: HttpClient by inject(named("baseHttpClient"))
|
||||
@@ -24,20 +29,47 @@ class ConnectivityTracker : KoinComponent {
|
||||
fun startTracking() {
|
||||
if (scope.isActive && _isOnline.value) return // Bereits aktiv (Dummy-Check)
|
||||
scope.launch {
|
||||
// Sofort prüfen
|
||||
_isOnline.value = checkConnection()
|
||||
// Zweiter Check nach kurzer Wartezeit, um Start-Races zu glätten
|
||||
delay(3_000.milliseconds)
|
||||
_isOnline.value = checkConnection()
|
||||
// Danach im Intervall prüfen
|
||||
while (isActive) {
|
||||
_isOnline.value = checkConnection()
|
||||
delay(10_000.milliseconds) // Alle 10 Sekunden prüfen
|
||||
_isOnline.value = checkConnection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkConnection(): Boolean {
|
||||
return try {
|
||||
val response = client.get(NetworkConfig.baseUrl.trimEnd('/') + "/actuator/health/readiness")
|
||||
response.status.value in 200..299
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
val base = NetworkConfig.baseUrl.trimEnd('/')
|
||||
// 1) readiness
|
||||
try {
|
||||
val r1 = client.get("$base/actuator/health/readiness")
|
||||
if (r1.status.value in 200..299) return true
|
||||
} catch (e: Exception) {
|
||||
// Debug-Log schlank halten
|
||||
println("[Connectivity] readiness failed: ${e.message}")
|
||||
}
|
||||
|
||||
// 2) health
|
||||
try {
|
||||
val r2 = client.get("$base/actuator/health")
|
||||
if (r2.status.value in 200..299) return true
|
||||
} catch (e: Exception) {
|
||||
println("[Connectivity] health failed: ${e.message}")
|
||||
}
|
||||
|
||||
// 3) public ping via gateway routing
|
||||
try {
|
||||
val r3 = client.get("$base/api/ping/simple")
|
||||
if (r3.status.value in 200..299) return true
|
||||
} catch (e: Exception) {
|
||||
println("[Connectivity] ping/simple failed: ${e.message}")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun stopTracking() {
|
||||
|
||||
+1
-1
@@ -100,6 +100,6 @@ class KtorWebSocketServerService(
|
||||
fun getPort(): Int = port
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_PORT: Int = 8081
|
||||
const val DEFAULT_PORT: Int = 8090
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user