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:
parent
d4f7e39834
commit
b7cffab3f9
@ -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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user