manager: show confirm dialog before uninstall module (#257)
manager: Add Dialog component, show confirm dialog before uninstall module, fix a bug in listModules
This commit is contained in:
@@ -0,0 +1,167 @@
|
|||||||
|
package me.weishu.kernelsu.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import me.weishu.kernelsu.ui.util.LocalDialogHost
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
sealed interface DialogResult {
|
||||||
|
object Confirmed : DialogResult
|
||||||
|
object Dismissed : DialogResult
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DialogVisuals {
|
||||||
|
val title: String
|
||||||
|
val content: String
|
||||||
|
val confirm: String?
|
||||||
|
val dismiss: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DialogData {
|
||||||
|
val visuals: DialogVisuals
|
||||||
|
|
||||||
|
fun confirm()
|
||||||
|
|
||||||
|
fun dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
class DialogHostState {
|
||||||
|
|
||||||
|
private data class DialogVisualsImpl(
|
||||||
|
override val title: String,
|
||||||
|
override val content: String,
|
||||||
|
override val confirm: String?,
|
||||||
|
override val dismiss: String?
|
||||||
|
) : DialogVisuals
|
||||||
|
|
||||||
|
private data class DialogDataImpl(
|
||||||
|
override val visuals: DialogVisuals,
|
||||||
|
val continuation: CancellableContinuation<DialogResult>
|
||||||
|
) : DialogData {
|
||||||
|
|
||||||
|
override fun confirm() {
|
||||||
|
if (continuation.isActive) continuation.resume(DialogResult.Confirmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dismiss() {
|
||||||
|
if (continuation.isActive) continuation.resume(DialogResult.Dismissed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
var currentDialogData by mutableStateOf<DialogData?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
suspend fun showDialog(
|
||||||
|
title: String,
|
||||||
|
content: String,
|
||||||
|
confirm: String? = null,
|
||||||
|
dismiss: String? = null
|
||||||
|
): DialogResult = mutex.withLock {
|
||||||
|
try {
|
||||||
|
return@withLock suspendCancellableCoroutine { continuation ->
|
||||||
|
currentDialogData = DialogDataImpl(
|
||||||
|
visuals = DialogVisualsImpl(title, content, confirm, dismiss),
|
||||||
|
continuation = continuation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
currentDialogData = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberDialogHostState(): DialogHostState {
|
||||||
|
return remember {
|
||||||
|
DialogHostState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BaseDialog(
|
||||||
|
state: DialogHostState = LocalDialogHost.current,
|
||||||
|
title: @Composable (String) -> Unit,
|
||||||
|
confirmButton: @Composable (String?, () -> Unit) -> Unit,
|
||||||
|
dismissButton: @Composable (String?, () -> Unit) -> Unit,
|
||||||
|
content: @Composable (String) -> Unit = { Text(text = it) },
|
||||||
|
) {
|
||||||
|
val currentDialogData = state.currentDialogData ?: return
|
||||||
|
val visuals = currentDialogData.visuals
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
currentDialogData.dismiss()
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
title(visuals.title)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
content(visuals.content)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
confirmButton(visuals.confirm, currentDialogData::confirm)
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
dismissButton(visuals.dismiss, currentDialogData::dismiss)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SimpleDialog(
|
||||||
|
state: DialogHostState = LocalDialogHost.current,
|
||||||
|
content: @Composable (String) -> Unit
|
||||||
|
) {
|
||||||
|
BaseDialog(
|
||||||
|
state = state,
|
||||||
|
title = {
|
||||||
|
Text(text = it)
|
||||||
|
},
|
||||||
|
confirmButton = { text, confirm ->
|
||||||
|
text?.let {
|
||||||
|
TextButton(onClick = confirm) {
|
||||||
|
Text(text = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = { text, dismiss ->
|
||||||
|
text?.let {
|
||||||
|
TextButton(onClick = dismiss) {
|
||||||
|
Text(text = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfirmDialog(state: DialogHostState = LocalDialogHost.current) {
|
||||||
|
BaseDialog(
|
||||||
|
state = state,
|
||||||
|
title = {
|
||||||
|
Text(text = it)
|
||||||
|
},
|
||||||
|
confirmButton = { text, confirm ->
|
||||||
|
text?.let {
|
||||||
|
TextButton(onClick = confirm) {
|
||||||
|
Text(text = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = { text, dismiss ->
|
||||||
|
text?.let {
|
||||||
|
TextButton(onClick = dismiss) {
|
||||||
|
Text(text = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -30,6 +30,9 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.weishu.kernelsu.Natives
|
import me.weishu.kernelsu.Natives
|
||||||
import me.weishu.kernelsu.R
|
import me.weishu.kernelsu.R
|
||||||
|
import me.weishu.kernelsu.ui.component.ConfirmDialog
|
||||||
|
import me.weishu.kernelsu.ui.component.DialogResult
|
||||||
|
import me.weishu.kernelsu.ui.component.rememberDialogHostState
|
||||||
import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination
|
import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination
|
||||||
import me.weishu.kernelsu.ui.util.*
|
import me.weishu.kernelsu.ui.util.*
|
||||||
import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel
|
import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel
|
||||||
@@ -84,6 +87,10 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}) { innerPadding ->
|
}) { innerPadding ->
|
||||||
|
|
||||||
|
val dialogState = rememberDialogHostState()
|
||||||
|
ConfirmDialog(dialogState)
|
||||||
|
|
||||||
val failedEnable = stringResource(R.string.module_failed_to_enable)
|
val failedEnable = stringResource(R.string.module_failed_to_enable)
|
||||||
val failedDisable = stringResource(R.string.module_failed_to_disable)
|
val failedDisable = stringResource(R.string.module_failed_to_disable)
|
||||||
val failedUninstall = stringResource(R.string.module_uninstall_failed)
|
val failedUninstall = stringResource(R.string.module_uninstall_failed)
|
||||||
@@ -125,8 +132,23 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
var isChecked by rememberSaveable(module) { mutableStateOf(module.enabled) }
|
var isChecked by rememberSaveable(module) { mutableStateOf(module.enabled) }
|
||||||
val reboot = stringResource(id = R.string.reboot)
|
val reboot = stringResource(id = R.string.reboot)
|
||||||
val rebootToApply = stringResource(id = R.string.reboot_to_apply)
|
val rebootToApply = stringResource(id = R.string.reboot_to_apply)
|
||||||
|
val moduleStr = stringResource(id = R.string.module)
|
||||||
|
val uninstall = stringResource(id = R.string.uninstall)
|
||||||
|
val cancel = stringResource(id = android.R.string.cancel)
|
||||||
|
val moduleUninstallConfirm =
|
||||||
|
stringResource(id = R.string.module_uninstall_confirm)
|
||||||
ModuleItem(module, isChecked, onUninstall = {
|
ModuleItem(module, isChecked, onUninstall = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
val dialogResult = dialogState.showDialog(
|
||||||
|
moduleStr,
|
||||||
|
content = moduleUninstallConfirm.format(module.name),
|
||||||
|
confirm = uninstall,
|
||||||
|
dismiss = cancel
|
||||||
|
)
|
||||||
|
if (dialogResult != DialogResult.Confirmed) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
val success = uninstallModule(module.id)
|
val success = uninstallModule(module.id)
|
||||||
if (success) {
|
if (success) {
|
||||||
viewModel.fetchModuleList()
|
viewModel.fetchModuleList()
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ import androidx.core.content.FileProvider
|
|||||||
import com.alorma.compose.settings.ui.*
|
import com.alorma.compose.settings.ui.*
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import me.weishu.kernelsu.ui.util.getBugreportFile
|
import me.weishu.kernelsu.ui.util.getBugreportFile
|
||||||
import me.weishu.kernelsu.BuildConfig
|
import me.weishu.kernelsu.BuildConfig
|
||||||
import me.weishu.kernelsu.R
|
import me.weishu.kernelsu.R
|
||||||
|
import me.weishu.kernelsu.ui.component.SimpleDialog
|
||||||
|
import me.weishu.kernelsu.ui.component.rememberDialogHostState
|
||||||
import me.weishu.kernelsu.ui.util.LinkifyText
|
import me.weishu.kernelsu.ui.util.LinkifyText
|
||||||
|
|
||||||
|
|
||||||
@@ -39,29 +42,10 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|
||||||
var openDialog by remember { mutableStateOf(false) }
|
val dialogState = rememberDialogHostState()
|
||||||
|
|
||||||
if (openDialog) {
|
SimpleDialog(dialogState) {
|
||||||
AlertDialog(
|
SupportCard()
|
||||||
onDismissRequest = {
|
|
||||||
openDialog = false
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(text = stringResource(id = R.string.about))
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
SupportCard()
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
openDialog = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(id = android.R.string.ok))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.padding(paddingValues)) {
|
Column(modifier = Modifier.padding(paddingValues)) {
|
||||||
@@ -98,11 +82,17 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val about = stringResource(id = R.string.about)
|
||||||
|
val ok = stringResource(id = android.R.string.ok)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
SettingsMenuLink(title = {
|
SettingsMenuLink(title = {
|
||||||
Text(stringResource(id = R.string.about))
|
Text(about)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
openDialog = true
|
scope.launch {
|
||||||
|
dialogState.showDialog(about, content = "unused", confirm = ok)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ package me.weishu.kernelsu.ui.util
|
|||||||
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import me.weishu.kernelsu.ui.component.DialogHostState
|
||||||
|
|
||||||
val LocalSnackbarHost = compositionLocalOf<SnackbarHostState> {
|
val LocalSnackbarHost = compositionLocalOf<SnackbarHostState> {
|
||||||
error("CompositionLocal LocalSnackbarController not present")
|
error("CompositionLocal LocalSnackbarController not present")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val LocalDialogHost = compositionLocalOf<DialogHostState> {
|
||||||
|
error("CompositionLocal LocalDialogController not present")
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package me.weishu.kernelsu.ui.viewmodel
|
package me.weishu.kernelsu.ui.viewmodel
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
@@ -74,6 +73,7 @@ class ModuleViewModel : ViewModel() {
|
|||||||
}.toList()
|
}.toList()
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
Log.e(TAG, "fetchModuleList: ", e)
|
Log.e(TAG, "fetchModuleList: ", e)
|
||||||
|
isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
<string name="settings_system_rw_summary">使用 overlayfs 使系统分区可写, 重启生效</string>
|
<string name="settings_system_rw_summary">使用 overlayfs 使系统分区可写, 重启生效</string>
|
||||||
<string name="about">关于</string>
|
<string name="about">关于</string>
|
||||||
<string name="require_kernel_version_8">需要 KernelSU 版本 8+</string>
|
<string name="require_kernel_version_8">需要 KernelSU 版本 8+</string>
|
||||||
|
<string name="module_uninstall_confirm">确定要卸载模块 %s 吗?</string>
|
||||||
<string name="module_uninstall_success">%s 已卸载</string>
|
<string name="module_uninstall_success">%s 已卸载</string>
|
||||||
<string name="module_uninstall_failed">卸载失败: %s</string>
|
<string name="module_uninstall_failed">卸载失败: %s</string>
|
||||||
<string name="module_version">版本</string>
|
<string name="module_version">版本</string>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
<string name="settings_system_rw_summary">Use overlayfs to make system partition writable, reboot to take effect.</string>
|
<string name="settings_system_rw_summary">Use overlayfs to make system partition writable, reboot to take effect.</string>
|
||||||
<string name="about">About</string>
|
<string name="about">About</string>
|
||||||
<string name="require_kernel_version_8">Require KernelSU version 8+</string>
|
<string name="require_kernel_version_8">Require KernelSU version 8+</string>
|
||||||
|
<string name="module_uninstall_confirm">Are you sure you want to uninstall module %s?</string>
|
||||||
<string name="module_uninstall_success">%s uninstalled</string>
|
<string name="module_uninstall_success">%s uninstalled</string>
|
||||||
<string name="module_uninstall_failed">Failed to uninstall: %s</string>
|
<string name="module_uninstall_failed">Failed to uninstall: %s</string>
|
||||||
<string name="module_version">Version</string>
|
<string name="module_version">Version</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user