package cc.unitmesh.devins.ui.nano import cc.unitmesh.xuiper.action.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.awt.Desktop import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import javax.swing.JOptionPane /** * Compose/Desktop implementation of NanoActionHandler * * Handles NanoUI actions in a Compose Desktop environment. * Provides platform-specific implementations for navigation, toast, and fetch. * * Example: * ```kotlin * val handler = ComposeActionHandler( * scope = rememberCoroutineScope(), * onNavigate = { route -> navController.navigate(route) }, * onToast = { message -> snackbarHostState.showSnackbar(message) } * ) * * handler.registerCustomAction("AddTask") { payload, context -> * val title = payload["title"] as? String ?: "" * taskRepository.add(Task(title)) * ActionResult.Success * } * ``` */ class ComposeActionHandler( private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO), private val onNavigate: ((String) -> Unit)? = null, private val onToast: ((String) -> Unit)? = null, private val onFetchComplete: ((String, Boolean, String?) -> Unit)? = null ) : BaseNanoActionHandler() { private val httpClient = HttpClient.newBuilder().build() override fun handleNavigate( navigate: NanoAction.Navigate, context: NanoActionContext ): ActionResult { return try { if (onNavigate != null) { onNavigate.invoke(navigate.to) } else { // Default: open in browser if it's a URL if (navigate.to.startsWith("http://") || navigate.to.startsWith("https://")) { Desktop.getDesktop().browse(URI(navigate.to)) } } ActionResult.Success } catch (e: Exception) { ActionResult.Error("Navigation failed: ${e.message}", e) } } override fun handleFetch( fetch: NanoAction.Fetch, context: NanoActionContext ): ActionResult { // Set loading state if specified fetch.loadingState?.let { path -> context.set(path, true) } scope.launch { try { val requestBuilder = HttpRequest.newBuilder() .uri(URI(fetch.url)) // Set method when (fetch.method) { HttpMethod.GET -> requestBuilder.GET() HttpMethod.POST -> { val body = buildRequestBody(fetch.body, context) requestBuilder.POST(HttpRequest.BodyPublishers.ofString(body)) requestBuilder.header("Content-Type", fetch.contentType.mimeType) } HttpMethod.PUT -> { val body = buildRequestBody(fetch.body, context) requestBuilder.PUT(HttpRequest.BodyPublishers.ofString(body)) requestBuilder.header("Content-Type", fetch.contentType.mimeType) } HttpMethod.DELETE -> requestBuilder.DELETE() else -> requestBuilder.GET() } // Add headers fetch.headers?.forEach { (key, value) -> requestBuilder.header(key, value) } val response = httpClient.send( requestBuilder.build(), HttpResponse.BodyHandlers.ofString() ) // Update loading state fetch.loadingState?.let { path -> context.set(path, false) } if (response.statusCode() in 200..299) { // Success fetch.responseBinding?.let { path -> context.set(path, response.body()) } fetch.onSuccess?.let { successAction -> handleAction(successAction, context) } onFetchComplete?.invoke(fetch.url, true, response.body()) } else { // Error val errorMsg = "HTTP ${response.statusCode()}: ${response.body()}" fetch.errorBinding?.let { path -> context.set(path, errorMsg) } fetch.onError?.let { errorAction -> handleAction(errorAction, context) } onFetchComplete?.invoke(fetch.url, false, errorMsg) } } catch (e: Exception) { fetch.loadingState?.let { path -> context.set(path, false) } fetch.errorBinding?.let { path -> context.set(path, e.message) } fetch.onError?.let { errorAction -> handleAction(errorAction, context) } onFetchComplete?.invoke(fetch.url, false, e.message) } } return ActionResult.Pending { /* async operation */ } } override fun handleShowToast( toast: NanoAction.ShowToast, context: NanoActionContext ): ActionResult { return try { if (onToast != null) { onToast.invoke(toast.message) } else { // Default: use Swing dialog (for desktop) javax.swing.SwingUtilities.invokeLater { JOptionPane.showMessageDialog(null, toast.message) } } ActionResult.Success } catch (e: Exception) { ActionResult.Error("Toast failed: ${e.message}", e) } } private fun buildRequestBody( body: Map?, context: NanoActionContext ): String { if (body == null) return "" val resolvedBody = body.mapValues { (_, field) -> when (field) { is BodyField.Literal -> field.value is BodyField.StateBinding -> context.get(field.path)?.toString() ?: "" } } // Simple JSON serialization with proper escaping return buildString { append("{") resolvedBody.entries.forEachIndexed { index, (key, value) -> if (index > 0) append(",") append("\"$key\":") when (value) { is String -> append("\"${value.replace("\\", "\\\\").replace("\"", "\\\"")}\"") is Number, is Boolean -> append(value) null -> append("null") else -> append("\"${value.toString().replace("\\", "\\\\").replace("\"", "\\\"")}\"") } } append("}") } } }