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()

View File

@@ -93,4 +93,11 @@
<string name="app_profile_template_view">查看模版</string>
<string name="app_profile_template_readonly">只读</string>
<string name="app_profile_template_id_exist">模版 id 已存在!</string>
<string name="app_profile_import_export">导入/导出</string>
<string name="app_profile_import_from_clipboard">从剪切板导入</string>
<string name="app_profile_export_to_clipboard">导出到剪切板</string>
<string name="app_profile_template_export_empty">没有本地模版可以导出!</string>
<string name="app_profile_template_import_success">导入成功!</string>
<string name="app_profile_template_sync">同步在线规则</string>
<string name="app_profile_template_save_failed">模版保存失败!</string>
</resources>

View File

@@ -95,4 +95,11 @@
<string name="app_profile_template_view">View Template</string>
<string name="app_profile_template_readonly">readonly</string>
<string name="app_profile_template_id_exist">template id already exists!</string>
<string name="app_profile_import_export">Import/Export</string>
<string name="app_profile_import_from_clipboard">Import from clipboard</string>
<string name="app_profile_export_to_clipboard">Export to clipboard</string>
<string name="app_profile_template_export_empty">Can not find local template to export!</string>
<string name="app_profile_template_import_success">Imported successfully</string>
<string name="app_profile_template_sync">Sync online templates</string>
<string name="app_profile_template_save_failed">Failed to save template</string>
</resources>