From 3c49b60924e6639e85fa11ab3dea6a78d8ed5f33 Mon Sep 17 00:00:00 2001 From: lisonge Date: Wed, 6 Dec 2023 16:28:52 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E8=A7=84=E5=88=99=E4=BD=BF=E7=94=A8JSO?= =?UTF-8?q?N5=E7=BC=96=E8=BE=91=E5=A4=8D=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/li/songe/gkd/ui/AppItemPage.kt | 29 ++++--- .../main/kotlin/li/songe/gkd/ui/SubsPage.kt | 7 +- .../main/kotlin/li/songe/gkd/util/Json5.kt | 84 +++++++++++++++++++ 3 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 app/src/main/kotlin/li/songe/gkd/util/Json5.kt diff --git a/app/src/main/kotlin/li/songe/gkd/ui/AppItemPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/AppItemPage.kt index 15e77d799..243ca5c07 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/AppItemPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/AppItemPage.kt @@ -1,6 +1,5 @@ package li.songe.gkd.ui -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -63,6 +62,7 @@ import li.songe.gkd.ui.destinations.GroupItemPageDestination import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.appInfoCacheFlow +import li.songe.gkd.util.encodeToJson5String import li.songe.gkd.util.json import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchTry @@ -257,7 +257,7 @@ fun AppItemPage( } } TextButton(onClick = { - val groupAppText = json.encodeToString( + val groupAppText = json.encodeToJson5String( appRaw?.copy( groups = listOf(showGroupItemVal) ) @@ -291,14 +291,15 @@ fun AppItemPage( .clickable { vm.viewModelScope.launchTry(Dispatchers.IO) { val subsRaw = subsIdToRawFlow.value[subsItemId] ?: return@launchTry - val newSubsRaw = subsRaw.copy(apps = subsRaw.apps - .toMutableList() - .apply { - set( - indexOfFirst { a -> a.id == appRawVal.id }, - appRawVal.copy(groups = appRawVal.groups.filter { g -> g.key != menuGroupRaw.key }) - ) - }) + val newSubsRaw = subsRaw.copy( + apps = subsRaw.apps + .toMutableList() + .apply { + set( + indexOfFirst { a -> a.id == appRawVal.id }, + appRawVal.copy(groups = appRawVal.groups.filter { g -> g.key != menuGroupRaw.key }) + ) + }) subsItemVal.subsFile.writeText( json.encodeToString( newSubsRaw @@ -321,7 +322,7 @@ fun AppItemPage( if (editGroupRaw != null && appRawVal != null && subsItemVal != null) { var source by remember { - mutableStateOf(json.encodeToString(editGroupRaw)) + mutableStateOf(json.encodeToJson5String(editGroupRaw)) } val oldSource = remember { source } AlertDialog( @@ -365,7 +366,8 @@ fun AppItemPage( setEditGroupRaw(null) val subsRaw = subsIdToRawFlow.value[subsItemId] ?: return@TextButton val newSubsRaw = subsRaw.copy(apps = subsRaw.apps.toMutableList().apply { - set(indexOfFirst { a -> a.id == appRawVal.id }, + set( + indexOfFirst { a -> a.id == appRawVal.id }, appRawVal.copy(groups = appRawVal.groups.toMutableList().apply { set( indexOfFirst { g -> g.key == newGroupRaw.key }, newGroupRaw @@ -441,7 +443,8 @@ fun AppItemPage( val newKey = appRawVal.groups.maxBy { g -> g.key }.key + 1 val subsRaw = subsIdToRawFlow.value[subsItemId] ?: return@TextButton val newSubsRaw = subsRaw.copy(apps = subsRaw.apps.toMutableList().apply { - set(indexOfFirst { a -> a.id == appRawVal.id }, + set( + indexOfFirst { a -> a.id == appRawVal.id }, appRawVal.copy(groups = appRawVal.groups + tempGroups.mapIndexed { i, g -> g.copy( key = newKey + i diff --git a/app/src/main/kotlin/li/songe/gkd/ui/SubsPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/SubsPage.kt index 18f89d10d..9de5705a6 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/SubsPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/SubsPage.kt @@ -61,6 +61,7 @@ import li.songe.gkd.ui.destinations.AppItemPageDestination import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.appInfoCacheFlow +import li.songe.gkd.util.encodeToJson5String import li.songe.gkd.util.json import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchTry @@ -306,7 +307,7 @@ fun SubsPage( val editAppRawVal = editAppRaw if (editAppRawVal != null && subsItemVal != null && subsRaw != null) { var source by remember { - mutableStateOf(json.encodeToString(editAppRawVal)) + mutableStateOf(json.encodeToJson5String(editAppRawVal)) } AlertDialog(title = { Text(text = "编辑本地APP规则") }, text = { OutlinedTextField( @@ -367,9 +368,7 @@ fun SubsPage( Text(text = "复制", modifier = Modifier .clickable { ClipboardUtils.copyText( - json.encodeToString( - menuAppRawVal - ) + json.encodeToJson5String(menuAppRawVal) ) ToastUtils.showShort("复制成功") menuAppRaw = null diff --git a/app/src/main/kotlin/li/songe/gkd/util/Json5.kt b/app/src/main/kotlin/li/songe/gkd/util/Json5.kt new file mode 100644 index 000000000..68162d76c --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/util/Json5.kt @@ -0,0 +1,84 @@ +package li.songe.gkd.util + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.serializer + +private val json5IdentifierReg = Regex("[a-zA-Z_][a-zA-Z0-9_]*") + +/** + * https://spec.json5.org/#strings + */ +private fun escapeString(value: String): String { + val wrapChar = '\'' + val sb = StringBuilder() + sb.append(wrapChar) + value.forEach { c -> + val escapeChar = when (c) { + wrapChar -> wrapChar + '\n' -> 'n' + '\r' -> 'r' + '\t' -> 't' + '\b' -> 'b' + '\\' -> '\\' + else -> null + } + if (escapeChar != null) { + sb.append("\\" + escapeChar) + } else { + when (c.code) { + in 0..0xf -> { + sb.append("\\x0" + c.code.toString(16)) + } + + in 0..0x1f -> { + sb.append("\\x" + c.code.toString(16)) + } + + else -> { + sb.append(c) + } + } + } + } + sb.append(wrapChar) + return sb.toString() +} + +fun convertJsonElementToJson5(element: JsonElement): String { + return when (element) { + is JsonPrimitive -> { + val content = element.content + if (element.isString) { + escapeString(content) + } else { + content + } + } + + is JsonObject -> { + // Handle JSON objects + val entries = element.entries.joinToString(",") { (key, value) -> + // If key is a valid identifier, no quotes are needed + if (key.matches(json5IdentifierReg)) { + "$key:${convertJsonElementToJson5(value)}" + } else { + "${escapeString(key)}:${convertJsonElementToJson5(value)}" + } + } + "{$entries}" + } + + is JsonArray -> { + val elements = element.joinToString(",") { convertJsonElementToJson5(it) } + "[$elements]" + } + } +} + +inline fun Json.encodeToJson5String(value: T): String { + return convertJsonElementToJson5(encodeToJsonElement(serializersModule.serializer(), value)) +}