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
|
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,142 +37,205 @@ 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
|
|
||||||
)
|
|
||||||
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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Window(
|
if (!isConnected) {
|
||||||
onCloseRequest = {
|
Window(
|
||||||
http.close()
|
onCloseRequest = { exitApplication() },
|
||||||
exitApplication()
|
title = "bricks-mp Connect",
|
||||||
},
|
state = WindowState(width = 500.dp, height = 300.dp)
|
||||||
title = "bricks-mp",
|
) {
|
||||||
state = windowState
|
MaterialTheme {
|
||||||
) {
|
Column(
|
||||||
MaterialTheme {
|
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||||
val density = LocalDensity.current
|
verticalArrangement = Arrangement.Center,
|
||||||
val lang = remember { Locale.getDefault().toLanguageTag() }
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
val fallbackWidthPx = with(density) { windowState.size.width.roundToPx() }
|
) {
|
||||||
val fallbackHeightPx = with(density) { windowState.size.height.roundToPx() }
|
Text("Connect to Bricks Server", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(bottom = 16.dp))
|
||||||
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) }
|
|
||||||
|
|
||||||
DisposableEffect(window) {
|
OutlinedTextField(
|
||||||
fun updateWindowSize() {
|
value = serverUrl,
|
||||||
val size = window.size
|
onValueChange = { serverUrl = it },
|
||||||
if (size.width > 0 && size.height > 0) {
|
label = { Text("Server URL (e.g., http://127.0.0.1:8080)") },
|
||||||
windowSizePx = IntSize(size.width, size.height)
|
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() {
|
Button(
|
||||||
override fun componentResized(e: ComponentEvent) = updateWindowSize()
|
onClick = {
|
||||||
override fun componentShown(e: ComponentEvent) = updateWindowSize()
|
isLoading = true
|
||||||
}
|
connectionError = null
|
||||||
|
scope.launch {
|
||||||
updateWindowSize()
|
try {
|
||||||
window.addComponentListener(listener)
|
context.baseUrl = serverUrl
|
||||||
onDispose { window.removeComponentListener(listener) }
|
DevLogStore.log(
|
||||||
}
|
level = DevLogLevel.INFO,
|
||||||
|
message = "Connecting to server",
|
||||||
LaunchedEffect(windowSizePx, lang) {
|
details = "baseUrl=$serverUrl",
|
||||||
http.updateRequestContext(
|
source = DevLogSource.APP
|
||||||
width = windowSizePx.width.takeIf { it > 0 } ?: fallbackWidthPx,
|
)
|
||||||
height = windowSizePx.height.takeIf { it > 0 } ?: fallbackHeightPx,
|
val widget = http.fetchUi(entryPath)
|
||||||
isMobile = false,
|
context.setCurrentWidget(widget)
|
||||||
lang = lang
|
isConnected = true
|
||||||
)
|
DevLogStore.log(
|
||||||
}
|
level = DevLogLevel.INFO,
|
||||||
|
message = "Connected and UI loaded successfully",
|
||||||
val actionDispatcher = remember(context, http, scope) {
|
source = DevLogSource.APP
|
||||||
ActionDispatcher(context = context, http = http, scope = scope).apply {
|
)
|
||||||
onWidgetLoaded = { widget -> context.setCurrentWidget(widget) }
|
} catch (e: Exception) {
|
||||||
onDialog = { widget, show -> dialogWidget = if (show) widget else null }
|
connectionError = "Connection failed: ${e.message}"
|
||||||
onMessage = { title, body, isError -> message = Triple(title, body, isError) }
|
DevLogStore.log(
|
||||||
}
|
level = DevLogLevel.ERROR,
|
||||||
}
|
message = "Connection failed: ${e.message}",
|
||||||
|
source = DevLogSource.APP,
|
||||||
val currentWidget by context.currentWidget.collectAsState()
|
stackTrace = e.stackTraceToString()
|
||||||
var devModeEnabled by remember { mutableStateOf(false) }
|
)
|
||||||
BricksHostScreen(
|
} finally {
|
||||||
widget = currentWidget,
|
isLoading = false
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
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 ->
|
DisposableEffect(window) {
|
||||||
AlertDialog(
|
fun updateWindowSize() {
|
||||||
onDismissRequest = { dialogWidget = null },
|
val size = window.size
|
||||||
title = { Text("Login") },
|
if (size.width > 0 && size.height > 0) {
|
||||||
text = { RenderWidget(widget, actionDispatcher) },
|
windowSizePx = IntSize(size.width, size.height)
|
||||||
confirmButton = {
|
}
|
||||||
TextButton(onClick = { dialogWidget = null }) { Text("Close") }
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
message?.let { (title, body, _) ->
|
val listener = object : ComponentAdapter() {
|
||||||
AlertDialog(
|
override fun componentResized(e: ComponentEvent) = updateWindowSize()
|
||||||
onDismissRequest = { message = null },
|
override fun componentShown(e: ComponentEvent) = updateWindowSize()
|
||||||
title = { Text(title) },
|
|
||||||
text = { Text(body) },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = { message = null }) { Text("OK") }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user