You've already forked KernelSU-Next
mirror of
https://github.com/KernelSU-Next/KernelSU-Next.git
synced 2025-08-27 23:46:34 +00:00
manager: added allowlist backup and restore (#9)
manager: create separate module & restore screen
This commit is contained in:
@@ -0,0 +1,275 @@
|
|||||||
|
package com.rifsxd.ksunext.ui.screen
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.LineHeightStyle
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import com.maxkeppeker.sheets.core.models.base.Header
|
||||||
|
import com.maxkeppeker.sheets.core.models.base.IconSource
|
||||||
|
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
||||||
|
import com.maxkeppeler.sheets.list.ListDialog
|
||||||
|
import com.maxkeppeler.sheets.list.models.ListOption
|
||||||
|
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import com.rifsxd.ksunext.BuildConfig
|
||||||
|
import com.rifsxd.ksunext.Natives
|
||||||
|
import com.rifsxd.ksunext.ksuApp
|
||||||
|
import com.rifsxd.ksunext.R
|
||||||
|
import com.rifsxd.ksunext.ui.component.AboutDialog
|
||||||
|
import com.rifsxd.ksunext.ui.component.ConfirmResult
|
||||||
|
import com.rifsxd.ksunext.ui.component.DialogHandle
|
||||||
|
import com.rifsxd.ksunext.ui.component.SwitchItem
|
||||||
|
import com.rifsxd.ksunext.ui.component.rememberConfirmDialog
|
||||||
|
import com.rifsxd.ksunext.ui.component.rememberCustomDialog
|
||||||
|
import com.rifsxd.ksunext.ui.component.rememberLoadingDialog
|
||||||
|
import com.rifsxd.ksunext.ui.util.LocalSnackbarHost
|
||||||
|
import com.rifsxd.ksunext.ui.util.getBugreportFile
|
||||||
|
import com.rifsxd.ksunext.ui.util.*
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author rifsxd
|
||||||
|
* @date 2025/1/14.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun BackupRestoreScreen(navigator: DestinationsNavigator) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
|
|
||||||
|
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||||
|
val ksuVersion = if (isManager) Natives.version else null
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopBar(
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackBarHost) },
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||||
|
) { paddingValues ->
|
||||||
|
val loadingDialog = rememberLoadingDialog()
|
||||||
|
val restoreDialog = rememberConfirmDialog()
|
||||||
|
val backupDialog = rememberConfirmDialog()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var showRebootDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showRebootDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showRebootDialog = false },
|
||||||
|
title = { Text(stringResource(R.string.reboot_required)) },
|
||||||
|
text = { Text(stringResource(R.string.reboot_message)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showRebootDialog = false
|
||||||
|
reboot()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.reboot))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showRebootDialog = false }) {
|
||||||
|
Text(stringResource(R.string.later))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val moduleBackup = stringResource(id = R.string.module_backup)
|
||||||
|
val backupMessage = stringResource(id = R.string.module_backup_message)
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Backup,
|
||||||
|
moduleBackup
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(moduleBackup) },
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
scope.launch {
|
||||||
|
val result = backupDialog.awaitConfirm(title = moduleBackup, content = backupMessage)
|
||||||
|
if (result == ConfirmResult.Confirmed) {
|
||||||
|
loadingDialog.withLoading {
|
||||||
|
moduleBackup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (showRebootDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showRebootDialog = false },
|
||||||
|
title = { Text(stringResource(R.string.reboot_required)) },
|
||||||
|
text = { Text(stringResource(R.string.reboot_message)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showRebootDialog = false
|
||||||
|
reboot()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.reboot))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showRebootDialog = false }) {
|
||||||
|
Text(stringResource(R.string.later))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val moduleRestore = stringResource(id = R.string.module_restore)
|
||||||
|
val restoreMessage = stringResource(id = R.string.module_restore_message)
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Restore,
|
||||||
|
moduleRestore
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(moduleRestore) },
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
scope.launch {
|
||||||
|
val result = restoreDialog.awaitConfirm(title = moduleRestore, content = restoreMessage)
|
||||||
|
if (result == ConfirmResult.Confirmed) {
|
||||||
|
loadingDialog.withLoading {
|
||||||
|
moduleRestore()
|
||||||
|
showRebootDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val allowlistBackup = stringResource(id = R.string.allowlist_backup)
|
||||||
|
val allowlistbackupMessage = stringResource(id = R.string.allowlist_backup_message)
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Backup,
|
||||||
|
allowlistBackup
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(allowlistBackup) },
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
scope.launch {
|
||||||
|
val result = backupDialog.awaitConfirm(title = allowlistBackup, content = allowlistbackupMessage)
|
||||||
|
if (result == ConfirmResult.Confirmed) {
|
||||||
|
loadingDialog.withLoading {
|
||||||
|
allowlistBackup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val allowlistRestore = stringResource(id = R.string.allowlist_restore)
|
||||||
|
val allowlistrestoreMessage = stringResource(id = R.string.allowlist_restore_message)
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Restore,
|
||||||
|
allowlistRestore
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(allowlistRestore) },
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
scope.launch {
|
||||||
|
val result = restoreDialog.awaitConfirm(title = allowlistRestore, content = allowlistrestoreMessage)
|
||||||
|
if (result == ConfirmResult.Confirmed) {
|
||||||
|
loadingDialog.withLoading {
|
||||||
|
allowlistRestore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TopBar(
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.backup_restore)) },
|
||||||
|
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun BackupPreview() {
|
||||||
|
BackupRestoreScreen(EmptyDestinationsNavigator)
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ import com.ramcosta.composedestinations.annotation.Destination
|
|||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.BackupRestoreScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -113,8 +114,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
val loadingDialog = rememberLoadingDialog()
|
val loadingDialog = rememberLoadingDialog()
|
||||||
val shrinkDialog = rememberConfirmDialog()
|
val shrinkDialog = rememberConfirmDialog()
|
||||||
val restoreDialog = rememberConfirmDialog()
|
|
||||||
val backupDialog = rememberConfirmDialog()
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -163,6 +162,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
title = stringResource(id = R.string.settings_umount_modules_default),
|
||||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
||||||
checked = umountChecked
|
checked = umountChecked
|
||||||
|
|
||||||
) {
|
) {
|
||||||
if (Natives.setDefaultUmountModules(it)) {
|
if (Natives.setDefaultUmountModules(it)) {
|
||||||
umountChecked = it
|
umountChecked = it
|
||||||
@@ -436,50 +436,17 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ksuVersion != null) {
|
if (ksuVersion != null) {
|
||||||
val moduleBackup = stringResource(id = R.string.module_backup)
|
val backupRestore = stringResource(id = R.string.backup_restore)
|
||||||
val backupMessage = stringResource(id = R.string.module_backup_message)
|
|
||||||
ListItem(
|
ListItem(
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Backup,
|
Icons.Filled.Backup,
|
||||||
moduleBackup
|
backupRestore
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
headlineContent = { Text(moduleBackup) },
|
headlineContent = { Text(backupRestore) },
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
scope.launch {
|
navigator.navigate(BackupRestoreScreenDestination)
|
||||||
val result = backupDialog.awaitConfirm(title = moduleBackup, content = backupMessage)
|
|
||||||
if (result == ConfirmResult.Confirmed) {
|
|
||||||
loadingDialog.withLoading {
|
|
||||||
moduleBackup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ksuVersion != null) {
|
|
||||||
val moduleRestore = stringResource(id = R.string.module_restore)
|
|
||||||
val restoreMessage = stringResource(id = R.string.module_restore_message)
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Restore,
|
|
||||||
moduleRestore
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(moduleRestore) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
scope.launch {
|
|
||||||
val result = restoreDialog.awaitConfirm(title = moduleRestore, content = restoreMessage)
|
|
||||||
if (result == ConfirmResult.Confirmed) {
|
|
||||||
loadingDialog.withLoading {
|
|
||||||
moduleRestore()
|
|
||||||
showRebootDialog = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -503,6 +503,59 @@ fun moduleRestore(): Boolean {
|
|||||||
return result.isEmpty()
|
return result.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun allowlistBackupDir(): String? {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val baseBackupDir = "/data/adb/allowlist_bak"
|
||||||
|
val resultBase = ShellUtils.fastCmd(shell, "mkdir -p $baseBackupDir").trim()
|
||||||
|
if (resultBase.isNotEmpty()) return null
|
||||||
|
|
||||||
|
val timestamp = ShellUtils.fastCmd(shell, "date +%Y%m%d_%H%M%S").trim()
|
||||||
|
if (timestamp.isEmpty()) return null
|
||||||
|
|
||||||
|
val newBackupDir = "$baseBackupDir/$timestamp"
|
||||||
|
val resultNewDir = ShellUtils.fastCmd(shell, "mkdir -p $newBackupDir").trim()
|
||||||
|
|
||||||
|
if (resultNewDir.isEmpty()) return newBackupDir
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun allowlistBackup(): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
|
||||||
|
val checkEmptyCommand = "if [ -z \"$(ls -A /data/adb/ksu/.allowlist)\" ]; then echo 'empty'; fi"
|
||||||
|
val resultCheckEmpty = ShellUtils.fastCmd(shell, checkEmptyCommand).trim()
|
||||||
|
|
||||||
|
if (resultCheckEmpty == "empty") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val backupDir = allowlistBackupDir() ?: return false
|
||||||
|
val command = "cp -rp /data/adb/ksu/.allowlist $backupDir"
|
||||||
|
val result = ShellUtils.fastCmd(shell, command).trim()
|
||||||
|
|
||||||
|
return result.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun allowlistRestore(): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
|
||||||
|
val command = "ls -t /data/adb/allowlist_bak | head -n 1"
|
||||||
|
val latestBackupDir = ShellUtils.fastCmd(shell, command).trim()
|
||||||
|
|
||||||
|
if (latestBackupDir.isEmpty()) return false
|
||||||
|
|
||||||
|
val sourceDir = "/data/adb/allowlist_bak/$latestBackupDir"
|
||||||
|
val destinationDir = "/data/adb/ksu/"
|
||||||
|
|
||||||
|
val createDestDirCommand = "mkdir -p $destinationDir"
|
||||||
|
ShellUtils.fastCmd(shell, createDestDirCommand)
|
||||||
|
|
||||||
|
val moveCommand = "cp -rp $sourceDir/.allowlist $destinationDir"
|
||||||
|
val result = ShellUtils.fastCmd(shell, moveCommand).trim()
|
||||||
|
|
||||||
|
return result.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getSuSFSDaemonPath(): String {
|
private fun getSuSFSDaemonPath(): String {
|
||||||
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libsusfsd.so"
|
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libsusfsd.so"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,8 +81,13 @@
|
|||||||
<string name="reboot_message">The changes will take effect after the system restart. Do you want to reboot now?</string>
|
<string name="reboot_message">The changes will take effect after the system restart. Do you want to reboot now?</string>
|
||||||
<string name="module_restore">Restore module</string>
|
<string name="module_restore">Restore module</string>
|
||||||
<string name="module_restore_message">Restore modules from recent backup.</string>
|
<string name="module_restore_message">Restore modules from recent backup.</string>
|
||||||
|
<string name="backup_restore">Backup & Restore</string>
|
||||||
<string name="module_backup">Backup module</string>
|
<string name="module_backup">Backup module</string>
|
||||||
<string name="module_backup_message">Backup currently installed modules.</string>
|
<string name="module_backup_message">Backup currently installed modules.</string>
|
||||||
|
<string name="allowlist_restore">Restore allowlist</string>
|
||||||
|
<string name="allowlist_restore_message">Restore allowlist from recent backup.</string>
|
||||||
|
<string name="allowlist_backup">Backup allowlist</string>
|
||||||
|
<string name="allowlist_backup_message">Backup currently configured allowlist.</string>
|
||||||
<string name="warning">Warning</string>
|
<string name="warning">Warning</string>
|
||||||
<string name="warning_message">This feature is still in beta and under development. Please ensure you backup your modules before proceeding. Only use this feature if you understand the potential risks. Proceed with caution.</string>
|
<string name="warning_message">This feature is still in beta and under development. Please ensure you backup your modules before proceeding. Only use this feature if you understand the potential risks. Proceed with caution.</string>
|
||||||
<string name="proceed">Proceed</string>
|
<string name="proceed">Proceed</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user