Add KPM module support, update related strings and view models
This commit is contained in:
@@ -9,6 +9,7 @@ import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestina
|
|||||||
import com.ramcosta.composedestinations.generated.destinations.ModuleScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.ModuleScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination
|
||||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||||
import shirkneko.zako.sukisu.R
|
import shirkneko.zako.sukisu.R
|
||||||
|
|
||||||
@@ -22,5 +23,6 @@ enum class BottomBarDestination(
|
|||||||
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
|
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
|
||||||
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
|
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
|
||||||
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
|
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
|
||||||
|
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Build, Icons.Outlined.Build, true),
|
||||||
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
|
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
|
||||||
}
|
}
|
||||||
|
|||||||
297
manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/kpm.kt
Normal file
297
manager/app/src/main/java/shirkneko/zako/sukisu/ui/screen/kpm.kt
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
package shirkneko.zako.sukisu.ui.screen
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import shirkneko.zako.sukisu.R
|
||||||
|
import shirkneko.zako.sukisu.ui.component.ConfirmResult
|
||||||
|
import shirkneko.zako.sukisu.ui.component.SearchAppBar
|
||||||
|
import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog
|
||||||
|
import shirkneko.zako.sukisu.ui.component.rememberLoadingDialog
|
||||||
|
import shirkneko.zako.sukisu.ui.theme.getCardColors
|
||||||
|
import shirkneko.zako.sukisu.ui.theme.getCardElevation
|
||||||
|
import shirkneko.zako.sukisu.ui.viewmodel.KpmViewModel
|
||||||
|
import shirkneko.zako.sukisu.ui.util.loadKpmModule
|
||||||
|
import shirkneko.zako.sukisu.ui.util.unloadKpmModule
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun KpmScreen(
|
||||||
|
navigator: DestinationsNavigator,
|
||||||
|
viewModel: KpmViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val snackBarHost = remember { SnackbarHostState() }
|
||||||
|
val confirmDialog = rememberConfirmDialog()
|
||||||
|
val loadingDialog = rememberLoadingDialog()
|
||||||
|
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
|
val kpmInstall = stringResource(R.string.kpm_install)
|
||||||
|
val kpmInstallConfirm = stringResource(R.string.kpm_install_confirm)
|
||||||
|
val kpmInstallSuccess = stringResource(R.string.kpm_install_success)
|
||||||
|
val kpmInstallFailed = stringResource(R.string.kpm_install_failed)
|
||||||
|
val install = stringResource(R.string.install)
|
||||||
|
val cancel = stringResource(R.string.cancel)
|
||||||
|
val kpmUninstall = stringResource(R.string.kpm_uninstall)
|
||||||
|
val kpmUninstallConfirmTemplate = stringResource(R.string.kpm_uninstall_confirm)
|
||||||
|
val uninstall = stringResource(R.string.uninstall)
|
||||||
|
val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success)
|
||||||
|
val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed)
|
||||||
|
|
||||||
|
val selectPatchLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult
|
||||||
|
|
||||||
|
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
// 复制文件到临时目录
|
||||||
|
val tempFile = File(context.cacheDir, "temp_patch.kpm")
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
tempFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val confirmResult = confirmDialog.awaitConfirm(
|
||||||
|
title = kpmInstall,
|
||||||
|
content = kpmInstallConfirm,
|
||||||
|
confirm = install,
|
||||||
|
dismiss = cancel
|
||||||
|
)
|
||||||
|
|
||||||
|
if (confirmResult == ConfirmResult.Confirmed) {
|
||||||
|
val success = loadingDialog.withLoading {
|
||||||
|
loadKpmModule(tempFile.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("KsuCli", "loadKpmModule result: $success")
|
||||||
|
|
||||||
|
if (success == "success") {
|
||||||
|
viewModel.fetchModuleList()
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmInstallSuccess,
|
||||||
|
duration = SnackbarDuration.Long
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 修正为显示安装失败的消息
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmInstallFailed,
|
||||||
|
duration = SnackbarDuration.Long
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (viewModel.moduleList.isEmpty()) {
|
||||||
|
viewModel.fetchModuleList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
SearchAppBar(
|
||||||
|
title = { Text(stringResource(R.string.kpm_title)) },
|
||||||
|
searchText = viewModel.search,
|
||||||
|
onSearchTextChange = { viewModel.search = it },
|
||||||
|
onClearClick = { viewModel.search = "" },
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
dropdownContent = {
|
||||||
|
IconButton(onClick = { viewModel.fetchModuleList() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Refresh,
|
||||||
|
contentDescription = stringResource(R.string.refresh)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
selectPatchLauncher.launch(
|
||||||
|
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
type = "application/*"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Add,
|
||||||
|
contentDescription = stringResource(R.string.kpm_install)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = { Text(stringResource(R.string.kpm_install)) },
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||||
|
) { padding ->
|
||||||
|
PullToRefreshBox(
|
||||||
|
onRefresh = { viewModel.fetchModuleList() },
|
||||||
|
isRefreshing = viewModel.isRefreshing,
|
||||||
|
modifier = Modifier.padding(padding)
|
||||||
|
) {
|
||||||
|
if (viewModel.moduleList.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.kpm_empty),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(viewModel.moduleList) { module ->
|
||||||
|
val kpmUninstallConfirm = String.format(kpmUninstallConfirmTemplate, module.name)
|
||||||
|
KpmModuleItem(
|
||||||
|
module = module,
|
||||||
|
onUninstall = {
|
||||||
|
scope.launch {
|
||||||
|
val confirmResult = confirmDialog.awaitConfirm(
|
||||||
|
title = kpmUninstall,
|
||||||
|
content = kpmUninstallConfirm,
|
||||||
|
confirm = uninstall,
|
||||||
|
dismiss = cancel
|
||||||
|
)
|
||||||
|
if (confirmResult == ConfirmResult.Confirmed) {
|
||||||
|
val success = loadingDialog.withLoading {
|
||||||
|
unloadKpmModule(module.id)
|
||||||
|
}
|
||||||
|
Log.d("KsuCli", "unloadKpmModule result: $success")
|
||||||
|
if (success == "success") {
|
||||||
|
viewModel.fetchModuleList()
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmUninstallSuccess,
|
||||||
|
duration = SnackbarDuration.Long
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmUninstallFailed,
|
||||||
|
duration = SnackbarDuration.Long
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onControl = {
|
||||||
|
viewModel.loadModuleDetail(module.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KpmModuleItem(
|
||||||
|
module: KpmViewModel.ModuleInfo,
|
||||||
|
onUninstall: () -> Unit,
|
||||||
|
onControl: () -> Unit
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = module.name,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = module.description,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = onControl
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Settings,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(stringResource(R.string.kpm_control))
|
||||||
|
}
|
||||||
|
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = onUninstall
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Delete,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(stringResource(R.string.kpm_uninstall))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -480,3 +480,57 @@ fun susfsSUS_SU_Mode(): String {
|
|||||||
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su mode")
|
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su mode")
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getKpmmgrPath(): String {
|
||||||
|
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libkpmmgr.so"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun loadKpmModule(path: String, args: String? = null): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}"
|
||||||
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unloadKpmModule(name: String): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKpmmgrPath()} unload $name"
|
||||||
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getKpmModuleCount(): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKpmmgrPath()} num"
|
||||||
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listKpmModules(): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKpmmgrPath()} list"
|
||||||
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getKpmModuleInfo(name: String): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKpmmgrPath()} info $name"
|
||||||
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun controlKpmModule(name: String, args: String? = null): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKpmmgrPath()} control $name ${args ?: ""}"
|
||||||
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun printKpmModules(): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKpmmgrPath()} print"
|
||||||
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package shirkneko.zako.sukisu.ui.viewmodel
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import shirkneko.zako.sukisu.ui.util.*
|
||||||
|
|
||||||
|
class KpmViewModel : ViewModel() {
|
||||||
|
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
||||||
|
private set
|
||||||
|
|
||||||
|
|
||||||
|
var search by mutableStateOf("")
|
||||||
|
internal set
|
||||||
|
|
||||||
|
var isRefreshing by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var currentModuleDetail by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun loadModuleDetail(moduleId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
currentModuleDetail = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
getKpmModuleInfo(moduleId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"无法获取模块详细信息: ${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d("KsuCli", "Module detail: $currentModuleDetail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetchModuleList() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
isRefreshing = true
|
||||||
|
try {
|
||||||
|
val moduleCount = getKpmModuleCount()
|
||||||
|
Log.d("KsuCli", "Module count: $moduleCount")
|
||||||
|
|
||||||
|
val moduleInfo = listKpmModules()
|
||||||
|
Log.d("KsuCli", "Module info: $moduleInfo")
|
||||||
|
|
||||||
|
val modules = parseModuleList(moduleInfo)
|
||||||
|
moduleList = modules
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInstalledKernelPatches(): List<ModuleInfo> {
|
||||||
|
return try {
|
||||||
|
val output = printKpmModules()
|
||||||
|
parseModuleList(output)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseModuleList(output: String): List<ModuleInfo> {
|
||||||
|
return output.split("\n").mapNotNull { line ->
|
||||||
|
if (line.isBlank()) return@mapNotNull null
|
||||||
|
val parts = line.split("|")
|
||||||
|
if (parts.size < 7) return@mapNotNull null
|
||||||
|
|
||||||
|
ModuleInfo(
|
||||||
|
id = parts[0].trim(),
|
||||||
|
name = parts[1].trim(),
|
||||||
|
version = parts[2].trim(),
|
||||||
|
author = parts[3].trim(),
|
||||||
|
description = parts[4].trim(),
|
||||||
|
args = parts[6].trim(),
|
||||||
|
enabled = true,
|
||||||
|
hasAction = controlKpmModule(parts[0].trim()).isNotBlank()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class ModuleInfo(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val version: String,
|
||||||
|
val author: String,
|
||||||
|
val description: String,
|
||||||
|
val args: String,
|
||||||
|
val enabled: Boolean,
|
||||||
|
val hasAction: Boolean
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -236,4 +236,6 @@
|
|||||||
<string name="kpm_install_confirm">Confirm installation?</string>
|
<string name="kpm_install_confirm">Confirm installation?</string>
|
||||||
<string name="kpm_install_success">Installation of kpm module successful</string>
|
<string name="kpm_install_success">Installation of kpm module successful</string>
|
||||||
<string name="kpm_install_failed">Installation of kpm module failed</string>
|
<string name="kpm_install_failed">Installation of kpm module failed</string>
|
||||||
|
<string name="kpm_args">kpm 参数</string>
|
||||||
|
<string name="kpm_control">kpm 控制</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user