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,142 +37,205 @@ 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
)
runCatching { http.fetchUi(System.getProperty("bricks.entry", "/")) }
.onSuccess {
context.setCurrentWidget(it)
DevLogStore.log(
level = DevLogLevel.INFO,
message = "Entry UI loaded successfully",
source = DevLogSource.APP
)
}
.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}")
}
}
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) }
Window(
onCloseRequest = {
http.close()
exitApplication()
},
title = "bricks-mp",
state = windowState
) {
MaterialTheme {
val density = LocalDensity.current
val lang = remember { Locale.getDefault().toLanguageTag() }
val fallbackWidthPx = with(density) { windowState.size.width.roundToPx() }
val fallbackHeightPx = with(density) { windowState.size.height.roundToPx() }
var windowSizePx by remember { mutableStateOf(IntSize(fallbackWidthPx, fallbackHeightPx)) }
var dialogWidget by remember { mutableStateOf<BricksWidget?>(null) }
var message by remember { mutableStateOf<Triple<String, String, Boolean>?>(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))
DisposableEffect(window) {
fun updateWindowSize() {
val size = window.size
if (size.width > 0 && size.height > 0) {
windowSizePx = IntSize(size.width, size.height)
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)
)
OutlinedTextField(
value = entryPath,
onValueChange = { entryPath = it },
label = { Text("Entry Path (default: /)") },
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)
)
connectionError?.let { err ->
Text(err, color = MaterialTheme.colorScheme.error, modifier = Modifier.padding(bottom = 8.dp))
}
}
val listener = object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) = updateWindowSize()
override fun componentShown(e: ComponentEvent) = updateWindowSize()
}
updateWindowSize()
window.addComponentListener(listener)
onDispose { window.removeComponentListener(listener) }
}
LaunchedEffect(windowSizePx, lang) {
http.updateRequestContext(
width = windowSizePx.width.takeIf { it > 0 } ?: fallbackWidthPx,
height = windowSizePx.height.takeIf { it > 0 } ?: fallbackHeightPx,
isMobile = false,
lang = lang
)
}
val actionDispatcher = remember(context, http, scope) {
ActionDispatcher(context = context, http = http, scope = scope).apply {
onWidgetLoaded = { widget -> context.setCurrentWidget(widget) }
onDialog = { widget, show -> dialogWidget = if (show) widget else null }
onMessage = { title, body, isError -> message = Triple(title, body, isError) }
}
}
val currentWidget by context.currentWidget.collectAsState()
var devModeEnabled by remember { mutableStateOf(false) }
BricksHostScreen(
widget = currentWidget,
actionDispatcher = actionDispatcher,
devModeEnabled = devModeEnabled,
onDevModeToggle = { devModeEnabled = !devModeEnabled },
onReload = {
scope.launch {
DevLogStore.log(
level = DevLogLevel.INFO,
message = "User triggered reload",
source = DevLogSource.APP
)
runCatching { http.fetchUi(System.getProperty("bricks.entry", "/")) }
.onSuccess { context.setCurrentWidget(it) }
.onFailure {
message = Triple("Error", it.message ?: "Load failed", true)
DevLogStore.log(
level = DevLogLevel.ERROR,
message = "Reload failed: ${it.message}",
source = DevLogSource.APP
)
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")
}
}
)
// DevPanel - only rendered when dev mode is enabled
if (devModeEnabled) {
DevPanel(
modifier = Modifier.height(320.dp),
onClose = { devModeEnabled = false }
)
}
}
} else {
// Main window after connection
Window(
onCloseRequest = {
http.close()
exitApplication()
},
title = "bricks-mp",
state = windowState
) {
MaterialTheme {
val density = LocalDensity.current
val lang = remember { Locale.getDefault().toLanguageTag() }
val fallbackWidthPx = with(density) { windowState.size.width.roundToPx() }
val fallbackHeightPx = with(density) { windowState.size.height.roundToPx() }
var windowSizePx by remember { mutableStateOf(IntSize(fallbackWidthPx, fallbackHeightPx)) }
var dialogWidget by remember { mutableStateOf<BricksWidget?>(null) }
var message by remember { mutableStateOf<Triple<String, String, Boolean>?>(null) }
dialogWidget?.let { widget ->
AlertDialog(
onDismissRequest = { dialogWidget = null },
title = { Text("Login") },
text = { RenderWidget(widget, actionDispatcher) },
confirmButton = {
TextButton(onClick = { dialogWidget = null }) { Text("Close") }
DisposableEffect(window) {
fun updateWindowSize() {
val size = window.size
if (size.width > 0 && size.height > 0) {
windowSizePx = IntSize(size.width, size.height)
}
}
)
}
message?.let { (title, body, _) ->
AlertDialog(
onDismissRequest = { message = null },
title = { Text(title) },
text = { Text(body) },
confirmButton = {
TextButton(onClick = { message = null }) { Text("OK") }
val listener = object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) = updateWindowSize()
override fun componentShown(e: ComponentEvent) = updateWindowSize()
}
updateWindowSize()
window.addComponentListener(listener)
onDispose { window.removeComponentListener(listener) }
}
LaunchedEffect(windowSizePx, lang) {
http.updateRequestContext(
width = windowSizePx.width.takeIf { it > 0 } ?: fallbackWidthPx,
height = windowSizePx.height.takeIf { it > 0 } ?: fallbackHeightPx,
isMobile = false,
lang = lang
)
}
val actionDispatcher = remember(context, http, scope) {
ActionDispatcher(context = context, http = http, scope = scope).apply {
onWidgetLoaded = { widget -> context.setCurrentWidget(widget) }
onDialog = { widget, show -> dialogWidget = if (show) widget else null }
onMessage = { title, body, isError -> message = Triple(title, body, isError) }
}
}
val currentWidget by context.currentWidget.collectAsState()
var devModeEnabled by remember { mutableStateOf(false) }
BricksHostScreen(
widget = currentWidget,
actionDispatcher = actionDispatcher,
devModeEnabled = devModeEnabled,
onDevModeToggle = { devModeEnabled = !devModeEnabled },
onReload = {
scope.launch {
DevLogStore.log(
level = DevLogLevel.INFO,
message = "User triggered reload",
source = DevLogSource.APP
)
runCatching { http.fetchUi(entryPath) }
.onSuccess { context.setCurrentWidget(it) }
.onFailure {
message = Triple("Error", it.message ?: "Load failed", true)
DevLogStore.log(
level = DevLogLevel.ERROR,
message = "Reload failed: ${it.message}",
source = DevLogSource.APP
)
}
}
},
onDisconnect = {
isConnected = false
context.setCurrentWidget(null)
},
serverUrl = serverUrl
)
// DevPanel - only rendered when dev mode is enabled
if (devModeEnabled) {
DevPanel(
modifier = Modifier.height(320.dp),
onClose = { devModeEnabled = false }
)
}
dialogWidget?.let { widget ->
AlertDialog(
onDismissRequest = { dialogWidget = null },
title = { Text("Login") },
text = { RenderWidget(widget, actionDispatcher) },
confirmButton = {
TextButton(onClick = { dialogWidget = null }) { Text("Close") }
}
)
}
message?.let { (title, body, _) ->
AlertDialog(
onDismissRequest = { message = null },
title = { Text(title) },
text = { Text(body) },
confirmButton = {
TextButton(onClick = { message = null }) { Text("OK") }
}
)
}
}
}
}
@ -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")
}
}
)
}