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 package com.bricks.test.generic
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.*
import androidx.compose.foundation.layout.height import androidx.compose.runtime.*
import androidx.compose.foundation.layout.padding import androidx.compose.ui.Alignment
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.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
@ -53,34 +37,91 @@ fun main() = application {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val windowState = remember { WindowState(width = 1280.dp, height = 800.dp) } val windowState = remember { WindowState(width = 1280.dp, height = 800.dp) }
LaunchedEffect(Unit) { var serverUrl by remember { mutableStateOf(System.getProperty("bricks.baseUrl", "")) }
context.baseUrl = System.getProperty("bricks.baseUrl", "http://localhost:8080") var entryPath by remember { mutableStateOf(System.getProperty("bricks.entry", "/")) }
DevLogStore.log( var isConnected by remember { mutableStateOf(false) }
level = DevLogLevel.INFO, var isLoading by remember { mutableStateOf(false) }
message = "bricks-mp starting", var connectionError by remember { mutableStateOf<String?>(null) }
details = "baseUrl=${context.baseUrl}",
source = DevLogSource.APP 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", "/")) } OutlinedTextField(
.onSuccess { value = entryPath,
context.setCurrentWidget(it) onValueChange = { entryPath = it },
DevLogStore.log( label = { Text("Entry Path (default: /)") },
level = DevLogLevel.INFO, modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)
message = "Entry UI loaded successfully",
source = DevLogSource.APP
) )
}
.onFailure { connectionError?.let { err ->
DevLogStore.log( Text(err, color = MaterialTheme.colorScheme.error, modifier = Modifier.padding(bottom = 8.dp))
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}")
}
} }
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( Window(
onCloseRequest = { onCloseRequest = {
http.close() http.close()
@ -147,7 +188,7 @@ fun main() = application {
message = "User triggered reload", message = "User triggered reload",
source = DevLogSource.APP source = DevLogSource.APP
) )
runCatching { http.fetchUi(System.getProperty("bricks.entry", "/")) } runCatching { http.fetchUi(entryPath) }
.onSuccess { context.setCurrentWidget(it) } .onSuccess { context.setCurrentWidget(it) }
.onFailure { .onFailure {
message = Triple("Error", it.message ?: "Load failed", true) 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 // DevPanel - only rendered when dev mode is enabled
@ -193,6 +239,7 @@ fun main() = application {
} }
} }
} }
}
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class) @OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
@Composable @Composable
@ -201,12 +248,14 @@ private fun BricksHostScreen(
actionDispatcher: ActionDispatcher, actionDispatcher: ActionDispatcher,
devModeEnabled: Boolean, devModeEnabled: Boolean,
onDevModeToggle: () -> Unit, onDevModeToggle: () -> Unit,
onReload: () -> Unit onReload: () -> Unit,
onDisconnect: () -> Unit,
serverUrl: String
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text("bricks-mp") }, title = { Text("bricks-mp ($serverUrl)") },
actions = { actions = {
Button( Button(
onClick = onDevModeToggle, onClick = onDevModeToggle,
@ -214,9 +263,12 @@ private fun BricksHostScreen(
) { ) {
Text(if (devModeEnabled) "Dev ON" else "Dev") 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") Text("Reload")
} }
Button(onClick = onDisconnect) {
Text("Disconnect")
}
} }
) )
} }