190 lines
7.3 KiB
Markdown
190 lines
7.3 KiB
Markdown
# bricks-mp
|
|
|
|
Kotlin Multiplatform / Compose runtime for rendering WebBricks UI descriptions.
|
|
|
|
The library is intentionally product-neutral. Application-specific clients, login flows and entry UI loading should live in the application project, not in the `com.bricks.mp` library packages.
|
|
|
|
## WebBricks backend request parameters
|
|
|
|
`BricksHttp` automatically appends the WebBricks runtime parameters to `.ui` and `.dspy` requests:
|
|
|
|
- `_webbricks_`
|
|
- `_width`
|
|
- `_height`
|
|
- `_is_mobile`
|
|
- `_lang`
|
|
|
|
Update viewport/language metadata from the host application when the window or device changes:
|
|
|
|
```kotlin
|
|
http.updateRequestContext(
|
|
width = windowWidthPx,
|
|
height = windowHeightPx,
|
|
isMobile = false,
|
|
lang = Locale.getDefault().toLanguageTag()
|
|
)
|
|
```
|
|
|
|
## Generic HTTP handling
|
|
|
|
`BricksHttp` does not silently parse error responses as widgets. It throws `BricksHttpException` for HTTP errors and redirects. `ActionDispatcher` provides generic UI behavior:
|
|
|
|
- `403`: load `/rbac/user/login.ui` and show it through `onDialog`.
|
|
- `401`: show the response body through `onMessage`.
|
|
- `3xx` including `301`: read the `Location` header and load the redirected UI.
|
|
|
|
Applications can override the login UI path:
|
|
|
|
```kotlin
|
|
actionDispatcher.loginUiPath = "/my/login.ui"
|
|
```
|
|
|
|
## 新建 Sage 应用主程序示例
|
|
|
|
下面代码是新建 Sage 桌面应用时可放到应用工程里的主程序示例。它演示如何用通用 `BricksHttp` 完成 Sage 的 cookie/session 登录、加载 `center.ui`,并接入 `ActionDispatcher`。这不是 bricks-mp 库包内容。
|
|
|
|
```kotlin
|
|
package com.example.sage
|
|
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
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.OutlinedTextField
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.TextButton
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.window.Window
|
|
import androidx.compose.ui.window.application
|
|
import com.bricks.mp.actions.ActionDispatcher
|
|
import com.bricks.mp.core.BricksContext
|
|
import com.bricks.mp.core.BricksHttp
|
|
import com.bricks.mp.core.BricksWidget
|
|
import com.bricks.mp.core.RenderWidget
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.serialization.json.jsonPrimitive
|
|
|
|
private const val DEFAULT_SAGE_BASE_URL = "https://ai.atvoe.com"
|
|
|
|
fun main() = application {
|
|
val context = remember { BricksContext().apply { baseUrl = DEFAULT_SAGE_BASE_URL } }
|
|
val http = remember { BricksHttp(context) }
|
|
val scope = rememberCoroutineScope()
|
|
|
|
var loggedIn by remember { mutableStateOf(false) }
|
|
var rootWidget by remember { mutableStateOf<BricksWidget?>(null) }
|
|
var dialogWidget by remember { mutableStateOf<BricksWidget?>(null) }
|
|
var message by remember { mutableStateOf<String?>(null) }
|
|
|
|
Window(
|
|
onCloseRequest = {
|
|
http.close()
|
|
exitApplication()
|
|
},
|
|
title = if (loggedIn) "Sage" else "Sage - Login"
|
|
) {
|
|
MaterialTheme {
|
|
if (!loggedIn) {
|
|
SageLoginScreen(
|
|
baseUrl = context.baseUrl,
|
|
onBaseUrlChange = { context.baseUrl = it },
|
|
onLogin = { username, password ->
|
|
scope.launch {
|
|
runCatching {
|
|
// Sage requires an initial request so the server can issue a session cookie.
|
|
http.getText(context.baseUrl.trimEnd('/') + "/")
|
|
|
|
val loginJson = http.getJson(
|
|
url = context.entireUrl("/rbac/user/up_login.dspy"),
|
|
params = mapOf(
|
|
"username" to username,
|
|
"password" to password
|
|
)
|
|
)
|
|
|
|
val widgetType = loginJson["widgettype"]?.jsonPrimitive?.content
|
|
require(widgetType == "Message" || widgetType == "UiMessage") {
|
|
loginJson["options"]?.toString() ?: "登录失败"
|
|
}
|
|
|
|
http.fetchUi("center.ui")
|
|
}.onSuccess { widget ->
|
|
rootWidget = widget
|
|
context.setCurrentWidget(widget)
|
|
loggedIn = true
|
|
}.onFailure { error ->
|
|
message = error.message ?: "登录失败"
|
|
}
|
|
}
|
|
}
|
|
)
|
|
} else {
|
|
val actionDispatcher = remember {
|
|
ActionDispatcher(context = context, http = http, scope = scope).apply {
|
|
loginUiPath = "/rbac/user/login.ui"
|
|
onWidgetLoaded = { widget -> context.setCurrentWidget(widget) }
|
|
onDialog = { widget, show -> dialogWidget = if (show) widget else null }
|
|
onMessage = { title, body, _ -> message = "$title\n$body" }
|
|
}
|
|
}
|
|
val currentWidget by context.currentWidget.collectAsState()
|
|
RenderWidget(
|
|
widget = currentWidget ?: rootWidget ?: return@MaterialTheme,
|
|
actionDispatcher = actionDispatcher,
|
|
modifier = Modifier.fillMaxSize()
|
|
)
|
|
|
|
dialogWidget?.let { widget ->
|
|
AlertDialog(
|
|
onDismissRequest = { dialogWidget = null },
|
|
text = { RenderWidget(widget, actionDispatcher) },
|
|
confirmButton = {
|
|
TextButton(onClick = { dialogWidget = null }) { Text("Close") }
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
message?.let { text ->
|
|
AlertDialog(
|
|
onDismissRequest = { message = null },
|
|
title = { Text("Sage") },
|
|
text = { Text(text) },
|
|
confirmButton = { TextButton(onClick = { message = null }) { Text("OK") } }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun SageLoginScreen(
|
|
baseUrl: String,
|
|
onBaseUrlChange: (String) -> Unit,
|
|
onLogin: (username: String, password: String) -> Unit
|
|
) {
|
|
var username by remember { mutableStateOf("") }
|
|
var password by remember { mutableStateOf("") }
|
|
|
|
Box(Modifier.fillMaxSize().padding(32.dp)) {
|
|
Column {
|
|
Text("Sage")
|
|
OutlinedTextField(baseUrl, onBaseUrlChange, label = { Text("服务器地址") })
|
|
OutlinedTextField(username, { username = it }, label = { Text("用户名") })
|
|
OutlinedTextField(password, { password = it }, label = { Text("密码") })
|
|
Button(
|
|
onClick = { onLogin(username, password) },
|
|
enabled = username.isNotBlank() && password.isNotBlank()
|
|
) {
|
|
Text("登录")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|