manager: template import/export

This commit is contained in:
weishu
2023-10-24 15:24:32 +08:00
parent fae1fd9826
commit 7371eae382
5 changed files with 201 additions and 40 deletions

View File

@@ -1,5 +1,6 @@
package me.weishu.kernelsu.ui.screen
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -11,13 +12,17 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Create
import androidx.compose.material.icons.filled.ImportExport
import androidx.compose.material.icons.filled.Sync
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
@@ -27,10 +32,17 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.lifecycle.viewmodel.compose.viewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -71,18 +83,53 @@ fun AppProfileTemplateScreen(
Scaffold(
topBar = {
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
val showToast = fun(msg: String) {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
}
TopBar(onBack = { navigator.popBackStack() },
onSync = {
scope.launch { viewModel.fetchTemplates(true) }
},
onClickCreate = {
onImport = {
clipboardManager.getText()?.text?.let {
scope.launch {
viewModel.importTemplates(it, {
showToast(context.getString(R.string.app_profile_template_import_success))
scope.launch {
viewModel.fetchTemplates(false)
}
}, showToast)
}
}
},
onExport = {
scope.launch {
viewModel.exportTemplates(
{
showToast(context.getString(R.string.app_profile_template_export_empty))
}
) {
clipboardManager.setText(AnnotatedString(it))
}
}
}
)
},
floatingActionButton = {
ExtendedFloatingActionButton(
onClick = {
navigator.navigate(
TemplateEditorScreenDestination(
TemplateViewModel.TemplateInfo(),
false
)
)
})
},
icon = { Icon(Icons.Filled.Add, null) },
text = { Text(stringResource(id = R.string.app_profile_template_create)) },
)
},
) { innerPadding ->
val refreshState = rememberPullRefreshState(
@@ -146,7 +193,12 @@ private fun TemplateItem(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(onBack: () -> Unit, onSync: () -> Unit, onClickCreate: () -> Unit) {
private fun TopBar(
onBack: () -> Unit,
onSync: () -> Unit = {},
onImport: () -> Unit = {},
onExport: () -> Unit = {}
) {
TopAppBar(
title = {
Text(stringResource(R.string.settings_profile_template))
@@ -158,10 +210,37 @@ private fun TopBar(onBack: () -> Unit, onSync: () -> Unit, onClickCreate: () ->
},
actions = {
IconButton(onClick = onSync) {
Icon(Icons.Filled.Sync, contentDescription = null)
Icon(
Icons.Filled.Sync,
contentDescription = stringResource(id = R.string.app_profile_template_sync)
)
}
IconButton(onClick = onClickCreate) {
Icon(Icons.Filled.Create, contentDescription = null)
var showDropdown by remember { mutableStateOf(false) }
IconButton(onClick = {
showDropdown = true
}) {
Icon(
imageVector = Icons.Filled.ImportExport,
contentDescription = stringResource(id = R.string.app_profile_import_export)
)
DropdownMenu(expanded = showDropdown, onDismissRequest = {
showDropdown = false
}) {
DropdownMenuItem(text = {
Text(stringResource(id = R.string.app_profile_import_from_clipboard))
}, onClick = {
onImport()
showDropdown = false
})
DropdownMenuItem(text = {
Text(stringResource(id = R.string.app_profile_export_to_clipboard))
}, onClick = {
onExport()
showDropdown = false
})
}
}
}
)

View File

@@ -1,5 +1,6 @@
package me.weishu.kernelsu.ui.screen
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@@ -30,6 +31,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@@ -45,6 +47,7 @@ import me.weishu.kernelsu.ui.util.deleteAppProfileTemplate
import me.weishu.kernelsu.ui.util.getAppProfileTemplate
import me.weishu.kernelsu.ui.util.setAppProfileTemplate
import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel
import me.weishu.kernelsu.ui.viewmodel.toJSON
import org.json.JSONArray
import org.json.JSONObject
@@ -82,6 +85,8 @@ fun TemplateEditorScreen(
""
}
val titleSummary = "${initialTemplate.id}$author$readOnlyHint"
val saveTemplateFailed = stringResource(id = R.string.app_profile_template_save_failed)
val context = LocalContext.current
TopBar(
title = if (isCreation) {
@@ -102,6 +107,8 @@ fun TemplateEditorScreen(
onSave = {
if (saveTemplate(template, isCreation)) {
navigator.navigateBack(result = true)
} else {
Toast.makeText(context, saveTemplateFailed, Toast.LENGTH_SHORT).show()
}
})
},
@@ -224,39 +231,9 @@ fun saveTemplate(template: TemplateViewModel.TemplateInfo, isCreation: Boolean =
return false
}
template.apply {
val json = JSONObject().apply {
put("id", id)
put("name", name.ifBlank { id })
put("description", description.ifBlank { id })
put("local", true)
put("uid", uid)
put("gid", gid)
put("groups", JSONArray().apply {
Groups.values().forEach { group ->
if (group.gid in groups) {
put(group.name)
}
}
})
put("capabilities", JSONArray().apply {
Capabilities.values().forEach { capability ->
if (capability.cap in capabilities) {
put(capability.name)
}
}
})
put("context", context)
put("namespace", Natives.Profile.Namespace.values()[namespace].name)
put("rules", JSONArray().apply {
rules.forEach { rule ->
put(rule)
}
})
}
return setAppProfileTemplate(id, json.toString())
}
val json = template.toJSON()
json.put("local", true)
return setAppProfileTemplate(template.id, json.toString())
}
@OptIn(ExperimentalMaterial3Api::class)

View File

@@ -85,6 +85,53 @@ class TemplateViewModel : ViewModel() {
isRefreshing = false
}
}
suspend fun importTemplates(
templates: String,
onSuccess: () -> Unit,
onFailure: (String) -> Unit
) {
withContext(Dispatchers.IO) {
runCatching {
JSONArray(templates)
}.getOrElse {
runCatching {
val json = JSONObject(templates)
JSONArray().apply { put(json) }
}.getOrElse {
onFailure("invalid templates: $templates")
return@withContext
}
}.let {
0.until(it.length()).forEach { i ->
runCatching {
val template = it.getJSONObject(i)
val id = template.getString("id")
template.put("local", true)
setAppProfileTemplate(id, template.toString())
}.onFailure { e ->
Log.e(TAG, "ignore invalid template: $it", e)
}
}
onSuccess()
}
}
}
suspend fun exportTemplates(onTemplateEmpty: () -> Unit, callback: (String) -> Unit) {
withContext(Dispatchers.IO) {
val templates = listAppProfileTemplates().mapNotNull(::getTemplateInfoById).filter {
it.local
}
templates.ifEmpty {
onTemplateEmpty()
return@withContext
}
JSONArray(templates.map {
it.toJSON()
}).toString().let(callback)
}
}
}
private fun fetchRemoteTemplates() {
@@ -205,6 +252,50 @@ private fun fromJSON(templateJson: JSONObject): TemplateViewModel.TemplateInfo?
}.getOrNull()
}
fun TemplateViewModel.TemplateInfo.toJSON(): JSONObject {
val template = this
return JSONObject().apply {
put("id", template.id)
put("name", template.name.ifBlank { template.id })
put("description", template.description.ifBlank { template.id })
if (template.author.isNotEmpty()) {
put("author", template.author)
}
put("namespace", Natives.Profile.Namespace.values()[template.namespace].name)
put("uid", template.uid)
put("gid", template.gid)
if (template.groups.isNotEmpty()) {
put("groups", JSONArray(
Groups.values().filter {
template.groups.contains(it.gid)
}.map {
it.name
}
))
}
if (template.capabilities.isNotEmpty()) {
put("capabilities", JSONArray(
Capabilities.values().filter {
template.capabilities.contains(it.cap)
}.map {
it.name
}
))
}
if (template.context.isNotEmpty()) {
put("context", template.context)
}
if (template.rules.isNotEmpty()) {
put("rules", JSONArray(template.rules))
}
}
}
@Suppress("unused")
fun generateTemplates() {
val templateJson = JSONObject()