refactor: migrate Gson serialization to kotlinx.serialization (#475)
* refactor: migrate Gson serialization to kotlinx.serialization * fix(dto): resolve serializing Any? and Map<String, Any?> with JsonElement * test(refactor): fix unit test issues under kotlinx.serialization and update nullable DTO defaults * fix(chat): support flexible numeric and string decoding for message timestamps * fix(chat): sequentialize cache loading to avoid overwriting network state
This commit is contained in:
64 files changed
+863
-577
No files matched your search
@@ -82,6 +82,7 @@ import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.Attachment
|
||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||
import com.m57.hermescontrol.theme.AssistantBubble
|
||||
import com.m57.hermescontrol.theme.AssistantBubbleLight
|
||||
import com.m57.hermescontrol.theme.StatusGreen
|
||||
@@ -92,6 +93,14 @@ import com.m57.hermescontrol.theme.ToolChipColorLight
|
||||
import com.m57.hermescontrol.theme.UserBubble
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.double
|
||||
import kotlinx.serialization.json.int
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -537,9 +546,7 @@ fun parseToolOutput(
|
||||
val trimmed = content.trim()
|
||||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null
|
||||
return try {
|
||||
val element =
|
||||
com.google.gson.JsonParser
|
||||
.parseString(trimmed)
|
||||
val element = OkHttpProvider.json.parseToJsonElement(trimmed)
|
||||
if (!element.isJsonObject) return null
|
||||
val obj = element.asJsonObject
|
||||
|
||||
@@ -2301,3 +2308,23 @@ private fun InlineAttachment(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extensions for backward compatibility with Gson API under kotlinx.serialization
|
||||
private val JsonElement.isJsonObject: Boolean get() = this is JsonObject
|
||||
private val JsonElement.asJsonObject: JsonObject get() = this as JsonObject
|
||||
private val JsonElement.isJsonArray: Boolean get() = this is JsonArray
|
||||
private val JsonElement.asJsonArray: JsonArray get() = this as JsonArray
|
||||
private val JsonElement.isJsonNull: Boolean get() = this is JsonNull
|
||||
private val JsonElement.isJsonPrimitive: Boolean get() = this is JsonPrimitive
|
||||
private val JsonElement.asString: String get() = (this as JsonPrimitive).content
|
||||
private val JsonElement.asDouble: Double get() = (this as JsonPrimitive).double
|
||||
private val JsonElement.asInt: Int get() = (this as JsonPrimitive).int
|
||||
private val JsonElement.asBoolean: Boolean get() = (this as JsonPrimitive).boolean
|
||||
|
||||
private fun JsonElement.has(key: String): Boolean = (this as? JsonObject)?.containsKey(key) == true
|
||||
|
||||
private fun JsonObject.entrySet(): Set<Map.Entry<String, JsonElement>> = entries
|
||||
|
||||
private fun JsonObject.keySet(): Set<String> = keys
|
||||
|
||||
private fun JsonArray.size(): Int = size
|
||||
@@ -18,6 +18,7 @@ import com.m57.hermescontrol.data.ws.ConnectionStatus
|
||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||
import com.m57.hermescontrol.data.ws.WsEvent
|
||||
import com.m57.hermescontrol.data.ws.WsMethods
|
||||
import com.m57.hermescontrol.data.ws.toJsonElement
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -32,6 +33,7 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
private const val TAG = "ChatViewModel"
|
||||
@@ -845,7 +847,9 @@ class ChatViewModel(
|
||||
val body = result.data
|
||||
val platformsStr =
|
||||
body.gateway_platforms?.entries?.joinToString("\n") { (k, v) ->
|
||||
" • **$k**: ${v.state ?: "Unknown"}${if (v.error_code != null) " (Error: ${v.error_code})" else ""}"
|
||||
val status = v?.state ?: "Unknown"
|
||||
val err = v?.error_code?.let { " (Error: $it)" } ?: ""
|
||||
" • **$k**: $status$err"
|
||||
} ?: "No active platforms"
|
||||
addAssistantMessage(
|
||||
"""
|
||||
@@ -983,13 +987,15 @@ class ChatViewModel(
|
||||
|
||||
fun refreshCurrentSession() {
|
||||
val sessionId = _uiState.value.currentSessionId ?: return
|
||||
loadCachedMessages(sessionId)
|
||||
// Skip the REST call if we already have messages — the WebSocket keeps
|
||||
// the UI current. The REST endpoint can 404 on tab switch-back when the
|
||||
// WS session ID doesn't round-trip through the resolver (issue #366),
|
||||
// showing a misleading error banner while the connection is fine.
|
||||
if (_uiState.value.messages.isNotEmpty()) return
|
||||
loadSessionMessages(sessionId)
|
||||
viewModelScope.launch {
|
||||
loadCachedMessages(sessionId).join()
|
||||
// Skip the REST call if we already have messages — the WebSocket keeps
|
||||
// the UI current. The REST endpoint can 404 on tab switch-back when the
|
||||
// WS session ID doesn't round-trip through the resolver (issue #366),
|
||||
// showing a misleading error banner while the connection is fine.
|
||||
if (_uiState.value.messages.isNotEmpty()) return@launch
|
||||
loadSessionMessages(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshSettings() {
|
||||
@@ -1004,8 +1010,8 @@ class ChatViewModel(
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun parseCommandCatalog(map: Map<*, *>): CommandCatalog? =
|
||||
try {
|
||||
val json = OkHttpProvider.gson.toJson(map)
|
||||
OkHttpProvider.gson.fromJson(json, CommandCatalog::class.java)
|
||||
val jsonElement = map.toJsonElement()
|
||||
OkHttpProvider.json.decodeFromJsonElement<CommandCatalog>(jsonElement)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to parse command catalog", e)
|
||||
null
|
||||
@@ -1030,23 +1036,25 @@ class ChatViewModel(
|
||||
)
|
||||
}
|
||||
_streamingState.update { StreamingState() }
|
||||
// Step 1: Load cached messages first — instant display
|
||||
loadCachedMessages(sessionId)
|
||||
// Step 2: Resume session on server
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
wsClient.send(
|
||||
WsMethods.SESSION_RESUME,
|
||||
mapOf("session_id" to sessionId),
|
||||
onSent = { id -> trackRequest(id, WsMethods.SESSION_RESUME) },
|
||||
)
|
||||
viewModelScope.launch {
|
||||
// Step 1: Load cached messages first — instant display
|
||||
loadCachedMessages(sessionId).join()
|
||||
// Step 2: Resume session on server
|
||||
launch(Dispatchers.IO) {
|
||||
wsClient.send(
|
||||
WsMethods.SESSION_RESUME,
|
||||
mapOf("session_id" to sessionId),
|
||||
onSent = { id -> trackRequest(id, WsMethods.SESSION_RESUME) },
|
||||
)
|
||||
}
|
||||
// Step 3: Load fresh messages from REST and merge
|
||||
loadSessionMessages(sessionId)
|
||||
// Step 4: Refresh session list to get latest titles
|
||||
loadSessions()
|
||||
}
|
||||
// Step 3: Load fresh messages from REST and merge
|
||||
loadSessionMessages(sessionId)
|
||||
// Step 4: Refresh session list to get latest titles
|
||||
loadSessions()
|
||||
}
|
||||
|
||||
private fun loadCachedMessages(sessionId: String) {
|
||||
private fun loadCachedMessages(sessionId: String): Job =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val cachedMessages = repo.loadMessages(sessionId)
|
||||
_uiState.update { state ->
|
||||
@@ -1058,7 +1066,6 @@ class ChatViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSessionMessages(sessionId: String) {
|
||||
viewModelScope.launch {
|
||||
@@ -1082,7 +1089,7 @@ class ChatViewModel(
|
||||
// Room duplicates when re-loading the same session.
|
||||
val stableId = "rest-$sessionId-$index"
|
||||
// Preserve original timestamp from the API when available
|
||||
val ts = msg.timestamp?.toLongOrNull() ?: System.currentTimeMillis()
|
||||
val ts = msg.timestampText?.toLongOrNull() ?: System.currentTimeMillis()
|
||||
ChatMessage(
|
||||
id = stableId,
|
||||
role = role,
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||
import com.m57.hermescontrol.data.ws.WsEvent
|
||||
import com.m57.hermescontrol.data.ws.toJsonElement
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
/**
|
||||
* Pure state reducer for WebSocket events.
|
||||
@@ -225,7 +227,7 @@ object ChatWsEventReducer {
|
||||
): ReducerResult {
|
||||
val contentJson =
|
||||
event.data?.let {
|
||||
OkHttpProvider.gson.toJson(it)
|
||||
OkHttpProvider.json.encodeToString(it.toJsonElement())
|
||||
} ?: ""
|
||||
val toolMessage =
|
||||
ChatMessage(
|
||||
@@ -271,7 +273,7 @@ object ChatWsEventReducer {
|
||||
): ReducerResult {
|
||||
val contentJson =
|
||||
event.data?.let {
|
||||
OkHttpProvider.gson.toJson(it)
|
||||
OkHttpProvider.json.encodeToString(it.toJsonElement())
|
||||
} ?: ""
|
||||
val messages = state.messages.toMutableList()
|
||||
val toolIdx =
|
||||
|
||||
@@ -53,8 +53,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.SchemaField
|
||||
import com.m57.hermescontrol.ui.common.ErrorState
|
||||
@@ -63,6 +61,11 @@ import com.m57.hermescontrol.ui.common.LoadingState
|
||||
import com.m57.hermescontrol.ui.common.NavIcon
|
||||
import com.m57.hermescontrol.ui.common.SearchBar
|
||||
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -580,7 +583,7 @@ private fun ConfigField(
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Switch(
|
||||
checked = currentValue?.asBoolean ?: false,
|
||||
checked = (currentValue as? JsonPrimitive)?.booleanOrNull ?: false,
|
||||
onCheckedChange = { onChange(JsonPrimitive(it)) },
|
||||
)
|
||||
}
|
||||
@@ -590,7 +593,7 @@ private fun ConfigField(
|
||||
DropdownField(
|
||||
label = label,
|
||||
options = field.options ?: emptyList(),
|
||||
selectedValue = currentValue?.asString ?: "",
|
||||
selectedValue = (currentValue as? JsonPrimitive)?.content ?: "",
|
||||
onOptionSelected = { onChange(JsonPrimitive(it)) },
|
||||
)
|
||||
if (description != label) {
|
||||
@@ -607,7 +610,7 @@ private fun ConfigField(
|
||||
OutlinedTextField(
|
||||
value =
|
||||
currentValue?.let {
|
||||
if (it.isJsonPrimitive) it.asString else ""
|
||||
if (it is JsonPrimitive) it.content else ""
|
||||
} ?: "",
|
||||
onValueChange = { text ->
|
||||
text.toDoubleOrNull()?.let { doubleVal ->
|
||||
@@ -630,10 +633,10 @@ private fun ConfigField(
|
||||
// String or other type
|
||||
val textValue =
|
||||
currentValue?.let {
|
||||
when {
|
||||
it.isJsonPrimitive -> it.asString
|
||||
it.isJsonObject -> it.toString()
|
||||
it.isJsonArray -> it.toString()
|
||||
when (it) {
|
||||
is JsonPrimitive -> it.content
|
||||
is JsonObject -> it.toString()
|
||||
is JsonArray -> it.toString()
|
||||
else -> ""
|
||||
}
|
||||
} ?: ""
|
||||
@@ -708,20 +711,17 @@ private fun collectUncoveredPaths(
|
||||
val dotPath = if (prefix.isEmpty()) key else "$prefix.$key"
|
||||
// If this exact path is in the schema, it's covered — skip entirely
|
||||
if (dotPath in schemaPaths) continue
|
||||
if (value is com.google.gson.JsonObject) {
|
||||
// Recurse into nested objects to find uncovered leaves
|
||||
val subKeys = value.keySet()
|
||||
if (value is JsonObject) {
|
||||
// Before recursing deeper, check if this whole sub-tree has any schema coverage
|
||||
val hasSchemaCoverage = schemaPaths.any { it == dotPath || it.startsWith("$dotPath.") }
|
||||
if (hasSchemaCoverage) {
|
||||
// Schema covers some sub-paths — recurse to find what's NOT covered
|
||||
val subMap = subKeys.associateWith { value.get(it) }
|
||||
result.addAll(collectUncoveredPaths(subMap, schemaPaths, dotPath))
|
||||
result.addAll(collectUncoveredPaths(value, schemaPaths, dotPath))
|
||||
} else {
|
||||
// No schema coverage at all for this subtree — add the whole thing
|
||||
result.add(dotPath)
|
||||
}
|
||||
} else if (value is com.google.gson.JsonArray) {
|
||||
} else if (value is JsonArray) {
|
||||
// Arrays: check if the path is in schema; if not, add it
|
||||
if (dotPath !in schemaPaths) {
|
||||
result.add(dotPath)
|
||||
@@ -746,7 +746,7 @@ private fun getJsonValue(
|
||||
if (i == 0) {
|
||||
current = config[key]
|
||||
} else {
|
||||
current = current?.asJsonObject?.get(key)
|
||||
current = (current as? JsonObject)?.get(key)
|
||||
}
|
||||
if (current == null) return null
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package com.m57.hermescontrol.ui.config
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.m57.hermescontrol.data.model.ConfigSchemaResponse
|
||||
import com.m57.hermescontrol.data.model.ConfigUpdateRequest
|
||||
import com.m57.hermescontrol.data.model.UpdateRawConfigRequest
|
||||
@@ -19,6 +17,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
data class ConfigUiState(
|
||||
val isLoading: Boolean = false,
|
||||
@@ -296,15 +296,14 @@ class ConfigViewModel :
|
||||
val topKey = parts.first()
|
||||
val rest = parts.drop(1).joinToString(".")
|
||||
val topValue = config[topKey]
|
||||
val nestedJson = topValue?.asJsonObject ?: JsonObject()
|
||||
val nestedJson = (topValue as? JsonObject) ?: JsonObject(emptyMap())
|
||||
val updatedNested =
|
||||
applyNestedValue(
|
||||
nestedJson.entrySet().associate { it.key to it.value },
|
||||
nestedJson,
|
||||
rest,
|
||||
value,
|
||||
)
|
||||
val nestedObj = JsonObject()
|
||||
updatedNested.forEach { (k, v) -> nestedObj.add(k, v) }
|
||||
val nestedObj = JsonObject(updatedNested)
|
||||
val mutable = config.toMutableMap()
|
||||
mutable[topKey] = nestedObj
|
||||
return mutable
|
||||
@@ -312,23 +311,38 @@ class ConfigViewModel :
|
||||
|
||||
/** Build a nested JSON changeset from dot-path pending changes. */
|
||||
private fun buildChangeset(changes: Map<String, JsonElement>): Map<String, JsonElement> {
|
||||
val root = JsonObject()
|
||||
val root = mutableMapOf<String, Any>()
|
||||
for ((dotPath, value) in changes) {
|
||||
val parts = dotPath.split(".")
|
||||
var current = root
|
||||
for (i in 0 until parts.size - 1) {
|
||||
val key = parts[i]
|
||||
val existing = current.get(key)
|
||||
if (existing == null || !existing.isJsonObject) {
|
||||
val newObj = JsonObject()
|
||||
current.add(key, newObj)
|
||||
current = newObj
|
||||
val existing = current[key]
|
||||
if (existing !is MutableMap<*, *>) {
|
||||
val newMap = mutableMapOf<String, Any>()
|
||||
current[key] = newMap
|
||||
current = newMap
|
||||
} else {
|
||||
current = existing.asJsonObject
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
current = existing as MutableMap<String, Any>
|
||||
}
|
||||
}
|
||||
current.add(parts.last(), value)
|
||||
current[parts.last()] = value
|
||||
}
|
||||
return root.entrySet().associate { it.key to it.value as JsonElement }
|
||||
|
||||
fun toJsonObject(map: Map<String, Any>): JsonObject {
|
||||
val content =
|
||||
map.mapValues { (_, v) ->
|
||||
if (v is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
toJsonObject(v as Map<String, Any>)
|
||||
} else {
|
||||
v as JsonElement
|
||||
}
|
||||
}
|
||||
return JsonObject(content)
|
||||
}
|
||||
|
||||
return toJsonObject(root)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.NetworkError
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -17,6 +18,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
data class ConnectUiState(
|
||||
val token: String = "",
|
||||
@@ -317,13 +320,10 @@ class ConnectViewModel(
|
||||
val decodedBytes = android.util.Base64.decode(trimmed, android.util.Base64.DEFAULT)
|
||||
val decodedString = String(decodedBytes, Charsets.UTF_8)
|
||||
if (decodedString.startsWith("{") && decodedString.endsWith("}")) {
|
||||
val json =
|
||||
com.google.gson.JsonParser
|
||||
.parseString(decodedString)
|
||||
.asJsonObject
|
||||
val host = json.get("host")?.asString
|
||||
val port = json.get("port")?.asInt?.toString() ?: json.get("port")?.asString
|
||||
val token = json.get("token")?.asString
|
||||
val json = OkHttpProvider.json.parseToJsonElement(decodedString).jsonObject
|
||||
val host = json["host"]?.jsonPrimitive?.content
|
||||
val port = json["port"]?.jsonPrimitive?.content
|
||||
val token = json["token"]?.jsonPrimitive?.content
|
||||
if (host != null && port != null && token != null) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.m57.hermescontrol.data.model.UpdateCronJobRequest
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import com.m57.hermescontrol.data.ws.toJsonElement
|
||||
import com.m57.hermescontrol.ui.common.ToastHost
|
||||
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -18,6 +19,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
data class CronJobsUiState(
|
||||
val isLoading: Boolean = false,
|
||||
@@ -292,7 +295,7 @@ class CronJobsViewModel :
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.updateCronJob(
|
||||
editor.jobId ?: "",
|
||||
UpdateCronJobRequest(updates = updates),
|
||||
UpdateCronJobRequest(updates = updates.mapValues { it.value.toJsonElement() }),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -336,8 +339,8 @@ class CronJobsViewModel :
|
||||
private fun extractScheduleString(job: CronJob): String {
|
||||
val s = job.schedule
|
||||
return when (s) {
|
||||
is String -> s
|
||||
is Map<*, *> -> (s["value"] as? String) ?: job.scheduleText
|
||||
is JsonPrimitive -> s.content
|
||||
is JsonObject -> (s["value"] as? JsonPrimitive)?.content ?: job.scheduleText
|
||||
else -> job.scheduleText
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ fun GatewayScreen(
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
val stateText = pStatus.state?.uppercase() ?: "UNKNOWN"
|
||||
val stateText = pStatus?.state?.uppercase() ?: "UNKNOWN"
|
||||
val badgeColor =
|
||||
when (stateText) {
|
||||
"RUNNING" -> Color(0xFF4CAF50)
|
||||
|
||||
@@ -6,12 +6,12 @@ import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.JsonParser
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.NetworkError
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -20,6 +20,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
/**
|
||||
* UI state for the pairing code entry screen.
|
||||
@@ -88,12 +90,10 @@ class PairingCodeEntryViewModel(
|
||||
val decodedBytes = Base64.decode(trimmed, Base64.DEFAULT)
|
||||
val decodedString = String(decodedBytes, Charsets.UTF_8)
|
||||
if (decodedString.startsWith("{") && decodedString.endsWith("}")) {
|
||||
val json = JsonParser.parseString(decodedString).asJsonObject
|
||||
val host = json.get("host")?.asString
|
||||
val port =
|
||||
json.get("port")?.asInt?.toString()
|
||||
?: json.get("port")?.asString
|
||||
val token = json.get("token")?.asString
|
||||
val json = OkHttpProvider.json.parseToJsonElement(decodedString).jsonObject
|
||||
val host = json["host"]?.jsonPrimitive?.content
|
||||
val port = json["port"]?.jsonPrimitive?.content
|
||||
val token = json["token"]?.jsonPrimitive?.content
|
||||
// Accept raw "host" + "port" + "token", or a composite "pairing" payload
|
||||
if (host != null && port != null && token != null) {
|
||||
connectToDashboard(host, port.toIntOrNull() ?: 9119, token)
|
||||
|
||||
@@ -786,7 +786,7 @@ private fun LazyListScope.gatewaySection(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
platform.state?.let { platformState ->
|
||||
platform?.state?.let { platformState ->
|
||||
StatusBadge(
|
||||
text = platformState,
|
||||
status =
|
||||
|
||||
Reference in new issue
Block a user