diff --git a/test/generic-client/src/jvmMain/kotlin/com/bricks/test/generic/Main.kt b/test/generic-client/src/jvmMain/kotlin/com/bricks/test/generic/Main.kt index 6d89fb0..e5b6492 100644 --- a/test/generic-client/src/jvmMain/kotlin/com/bricks/test/generic/Main.kt +++ b/test/generic-client/src/jvmMain/kotlin/com/bricks/test/generic/Main.kt @@ -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(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(null) } - var message by remember { mutableStateOf?>(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(null) } + var message by remember { mutableStateOf?>(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") + } } ) }