fix: generic-client startup window now prompts for server URL instead of hardcoding localhost:8080

- Add connection UI with server URL and entry path inputs
- Show loading/error states during connection attempt
- Add Disconnect button to main window to return to connection screen
- Display connected URL in top bar
This commit is contained in:
yumoqing 2026-05-20 22:14:50 +08:00
parent d4f7e39834
commit b7cffab3f9

View File

@ -1,25 +1,9 @@
package com.bricks.test.generic
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
@ -53,34 +37,91 @@ fun main() = application {
val scope = rememberCoroutineScope()
val windowState = remember { WindowState(width = 1280.dp, height = 800.dp) }
LaunchedEffect(Unit) {
context.baseUrl = System.getProperty("bricks.baseUrl", "http://localhost:8080")
DevLogStore.log(
level = DevLogLevel.INFO,
message = "bricks-mp starting",
details = "baseUrl=${context.baseUrl}",
source = DevLogSource.APP
var serverUrl by remember { mutableStateOf(System.getProperty("bricks.baseUrl", "")) }
var entryPath by remember { mutableStateOf(System.getProperty("bricks.entry", "/")) }
var isConnected by remember { mutableStateOf(false) }
var isLoading by remember { mutableStateOf(false) }
var connectionError by remember { mutableStateOf<String?>(null) }
if (!isConnected) {
Window(
onCloseRequest = { exitApplication() },
title = "bricks-mp Connect",
state = WindowState(width = 500.dp, height = 300.dp)
) {
MaterialTheme {
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Connect to Bricks Server", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(bottom = 16.dp))
OutlinedTextField(
value = serverUrl,
onValueChange = { serverUrl = it },
label = { Text("Server URL (e.g., http://127.0.0.1:8080)") },
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
)
runCatching { http.fetchUi(System.getProperty("bricks.entry", "/")) }
.onSuccess {
context.setCurrentWidget(it)
DevLogStore.log(
level = DevLogLevel.INFO,
message = "Entry UI loaded successfully",
source = DevLogSource.APP
OutlinedTextField(
value = entryPath,
onValueChange = { entryPath = it },
label = { Text("Entry Path (default: /)") },
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)
)
}
.onFailure {
DevLogStore.log(
level = DevLogLevel.ERROR,
message = "Failed to load entry UI: ${it.message}",
source = DevLogSource.APP,
stackTrace = it.stackTraceToString()
)
println("[Bricks] Failed to load entry UI: ${it.message}")
}
connectionError?.let { err ->
Text(err, color = MaterialTheme.colorScheme.error, modifier = Modifier.padding(bottom = 8.dp))
}
Button(
onClick = {
isLoading = true
connectionError = null
scope.launch {
try {
context.baseUrl = serverUrl
DevLogStore.log(
level = DevLogLevel.INFO,
message = "Connecting to server",
details = "baseUrl=$serverUrl",
source = DevLogSource.APP
)
val widget = http.fetchUi(entryPath)
context.setCurrentWidget(widget)
isConnected = true
DevLogStore.log(
level = DevLogLevel.INFO,
message = "Connected and UI loaded successfully",
source = DevLogSource.APP
)
} catch (e: Exception) {
connectionError = "Connection failed: ${e.message}"
DevLogStore.log(
level = DevLogLevel.ERROR,
message = "Connection failed: ${e.message}",
source = DevLogSource.APP,
stackTrace = e.stackTraceToString()
)
} finally {
isLoading = false
}
}
},
enabled = !isLoading && serverUrl.isNotBlank(),
modifier = Modifier.fillMaxWidth()
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(20.dp), color = MaterialTheme.colorScheme.onPrimary)
Spacer(modifier = Modifier.width(8.dp))
}
Text(if (isLoading) "Connecting..." else "Connect")
}
}
}
}
} else {
// Main window after connection
Window(
onCloseRequest = {
http.close()
@ -147,7 +188,7 @@ fun main() = application {
message = "User triggered reload",
source = DevLogSource.APP
)
runCatching { http.fetchUi(System.getProperty("bricks.entry", "/")) }
runCatching { http.fetchUi(entryPath) }
.onSuccess { context.setCurrentWidget(it) }
.onFailure {
message = Triple("Error", it.message ?: "Load failed", true)
@ -158,7 +199,12 @@ fun main() = application {
)
}
}
}
},
onDisconnect = {
isConnected = false
context.setCurrentWidget(null)
},
serverUrl = serverUrl
)
// DevPanel - only rendered when dev mode is enabled
@ -192,6 +238,7 @@ fun main() = application {
}
}
}
}
}
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
@ -201,12 +248,14 @@ private fun BricksHostScreen(
actionDispatcher: ActionDispatcher,
devModeEnabled: Boolean,
onDevModeToggle: () -> Unit,
onReload: () -> Unit
onReload: () -> Unit,
onDisconnect: () -> Unit,
serverUrl: String
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("bricks-mp") },
title = { Text("bricks-mp ($serverUrl)") },
actions = {
Button(
onClick = onDevModeToggle,
@ -214,9 +263,12 @@ private fun BricksHostScreen(
) {
Text(if (devModeEnabled) "Dev ON" else "Dev")
}
Button(onClick = onReload, modifier = Modifier.padding(end = 8.dp)) {
Button(onClick = onReload, modifier = Modifier.padding(end = 4.dp)) {
Text("Reload")
}
Button(onClick = onDisconnect) {
Text("Disconnect")
}
}
)
}