From 425713fad371416dc3eab08be9c8af0c3321800e Mon Sep 17 00:00:00 2001 From: TinyHai <34483077+TinyHai@users.noreply.github.com> Date: Fri, 8 Mar 2024 10:31:14 +0800 Subject: [PATCH] manager: refine dialog component & add an animation to UpdateCard (#1429) --- .../me/weishu/kernelsu/ui/MainActivity.kt | 11 +- .../weishu/kernelsu/ui/component/AboutCard.kt | 9 +- .../me/weishu/kernelsu/ui/component/Dialog.kt | 521 +++++++++++------- .../ui/component/profile/RootProfileConfig.kt | 27 +- .../weishu/kernelsu/ui/screen/AppProfile.kt | 2 +- .../java/me/weishu/kernelsu/ui/screen/Home.kt | 34 +- .../me/weishu/kernelsu/ui/screen/Module.kt | 22 +- .../me/weishu/kernelsu/ui/screen/Settings.kt | 30 +- .../me/weishu/kernelsu/ui/screen/SuperUser.kt | 4 - .../kernelsu/ui/util/CompositionProvider.kt | 5 - 10 files changed, 384 insertions(+), 281 deletions(-) diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt index 5d1efdb9..b90d83b1 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt @@ -5,13 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -27,11 +21,9 @@ import com.ramcosta.composedestinations.navigation.popBackStack import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ksuApp -import me.weishu.kernelsu.ui.component.rememberDialogHostState import me.weishu.kernelsu.ui.screen.BottomBarDestination import me.weishu.kernelsu.ui.screen.NavGraphs import me.weishu.kernelsu.ui.theme.KernelSUTheme -import me.weishu.kernelsu.ui.util.LocalDialogHost import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.rootAvailable @@ -54,7 +46,6 @@ class MainActivity : ComponentActivity() { ) { innerPadding -> CompositionLocalProvider( LocalSnackbarHost provides snackbarHostState, - LocalDialogHost provides rememberDialogHostState(), ) { DestinationsNavHost( modifier = Modifier.padding(innerPadding), diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AboutCard.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AboutCard.kt index 973de2db..269f48b2 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AboutCard.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AboutCard.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog import androidx.compose.material3.ElevatedCard import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme @@ -52,11 +53,9 @@ fun AboutCard() { } @Composable -fun AboutDialog(showAboutDialog: MutableState) { - if (showAboutDialog.value) { - Dialog(onDismissRequest = { showAboutDialog.value = false }) { - AboutCard() - } +fun AboutDialog(dismiss: () -> Unit) { + Dialog(onDismissRequest = { dismiss() }) { + AboutCard() } } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt index 62b02a36..e2c3fa45 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt @@ -1,8 +1,10 @@ package me.weishu.kernelsu.ui.component import android.graphics.text.LineBreaker +import android.os.Parcelable import android.text.Layout import android.text.method.LinkMovementMethod +import android.util.Log import android.view.ViewGroup import android.widget.TextView import androidx.compose.foundation.layout.Box @@ -10,14 +12,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb @@ -28,48 +26,48 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import io.noties.markwon.Markwon import io.noties.markwon.utils.NoCopySpannableFactory -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import me.weishu.kernelsu.ui.util.LocalDialogHost +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.onEach +import kotlinx.parcelize.Parcelize import kotlin.coroutines.resume -interface DialogVisuals +private const val TAG = "DialogComponent" -interface LoadingDialogVisuals : DialogVisuals - -interface PromptDialogVisuals : DialogVisuals { +interface ConfirmDialogVisuals : Parcelable { val title: String val content: String -} - -interface ConfirmDialogVisuals : PromptDialogVisuals { + val isMarkdown: Boolean val confirm: String? val dismiss: String? - val isMarkdown: Boolean } - -sealed interface DialogData { - val visuals: DialogVisuals +@Parcelize +private data class ConfirmDialogVisualsImpl( + override val title: String, + override val content: String, + override val isMarkdown: Boolean, + override val confirm: String?, + override val dismiss: String?, +) : ConfirmDialogVisuals { + companion object { + val Empty: ConfirmDialogVisuals = ConfirmDialogVisualsImpl("", "", false, null, null) + } } -interface LoadingDialogData : DialogData { - override val visuals: LoadingDialogVisuals - fun dismiss() +interface DialogHandle { + val isShown: Boolean + val dialogType: String + fun show() + fun hide() } -interface PromptDialogData : DialogData { - override val visuals: PromptDialogVisuals - fun dismiss() -} - -interface ConfirmDialogData : PromptDialogData { - override val visuals: ConfirmDialogVisuals - fun confirm() +interface LoadingDialogHandle : DialogHandle { + suspend fun withLoading(block: suspend () -> R): R + fun showLoading() } sealed interface ConfirmResult { @@ -77,143 +75,313 @@ sealed interface ConfirmResult { object Canceled : ConfirmResult } -class DialogHostState { +interface ConfirmDialogHandle : DialogHandle { + val visuals: ConfirmDialogVisuals - private object LoadingDialogVisualsImpl : LoadingDialogVisuals - - private data class PromptDialogVisualsImpl( - override val title: String, override val content: String - ) : PromptDialogVisuals - - private data class ConfirmDialogVisualsImpl( - override val title: String, - override val content: String, - override val confirm: String?, - override val dismiss: String?, - override val isMarkdown: Boolean, - ) : ConfirmDialogVisuals - - private data class LoadingDialogDataImpl( - override val visuals: LoadingDialogVisuals, - private val continuation: CancellableContinuation, - ) : LoadingDialogData { - override fun dismiss() { - if (continuation.isActive) continuation.resume(Unit) - } - } - - private data class PromptDialogDataImpl( - override val visuals: PromptDialogVisuals, - private val continuation: CancellableContinuation, - ) : PromptDialogData { - override fun dismiss() { - if (continuation.isActive) continuation.resume(Unit) - } - } - - private data class ConfirmDialogDataImpl( - override val visuals: ConfirmDialogVisuals, - private val continuation: CancellableContinuation - ) : ConfirmDialogData { - - override fun confirm() { - if (continuation.isActive) continuation.resume(ConfirmResult.Confirmed) - } - - override fun dismiss() { - if (continuation.isActive) continuation.resume(ConfirmResult.Canceled) - } - } - - private val mutex = Mutex() - - var currentDialogData by mutableStateOf(null) - private set - - suspend fun showLoading() { - try { - mutex.withLock { - suspendCancellableCoroutine { continuation -> - currentDialogData = LoadingDialogDataImpl( - visuals = LoadingDialogVisualsImpl, continuation = continuation - ) - } - } - } finally { - currentDialogData = null - } - } - - suspend fun withLoading(block: suspend () -> R) = coroutineScope { - val showLoading = launch { - showLoading() - } - - val result = block() - - showLoading.cancel() - - result - } - - suspend fun showPrompt(title: String, content: String) { - try { - mutex.withLock { - suspendCancellableCoroutine { continuation -> - currentDialogData = PromptDialogDataImpl( - visuals = PromptDialogVisualsImpl(title, content), - continuation = continuation - ) - } - } - } finally { - currentDialogData = null - } - } - - suspend fun showConfirm( + fun showConfirm( title: String, content: String, markdown: Boolean = false, confirm: String? = null, dismiss: String? = null - ): ConfirmResult = mutex.withLock { - try { - return@withLock suspendCancellableCoroutine { continuation -> - currentDialogData = ConfirmDialogDataImpl( - visuals = ConfirmDialogVisualsImpl(title, content, confirm, dismiss, markdown), - continuation = continuation - ) + ) + + suspend fun awaitConfirm( + title: String, + content: String, + markdown: Boolean = false, + confirm: String? = null, + dismiss: String? = null + ): ConfirmResult +} + +private abstract class DialogHandleBase( + protected val visible: MutableState, + protected val coroutineScope: CoroutineScope +) : DialogHandle { + override val isShown: Boolean + get() = visible.value + + override fun show() { + coroutineScope.launch { + visible.value = true + } + } + + final override fun hide() { + coroutineScope.launch { + visible.value = false + } + } + + override fun toString(): String { + return dialogType + } +} + +private class LoadingDialogHandleImpl( + visible: MutableState, + coroutineScope: CoroutineScope +) : LoadingDialogHandle, DialogHandleBase(visible, coroutineScope) { + override suspend fun withLoading(block: suspend () -> R): R { + return coroutineScope.async { + try { + visible.value = true + block() + } finally { + visible.value = false + } + }.await() + } + + override fun showLoading() { + show() + } + + override val dialogType: String get() = "LoadingDialog" +} + +typealias NullableCallback = (() -> Unit)? + +interface ConfirmCallback { + + val onConfirm: NullableCallback + + val onDismiss: NullableCallback + + val isEmpty: Boolean get() = onConfirm == null && onDismiss == null + + companion object { + operator fun invoke(onConfirmProvider: () -> NullableCallback, onDismissProvider: () -> NullableCallback): ConfirmCallback { + return object : ConfirmCallback { + override val onConfirm: NullableCallback + get() = onConfirmProvider() + override val onDismiss: NullableCallback + get() = onDismissProvider() } - } finally { - currentDialogData = null } } } +private class ConfirmDialogHandleImpl( + visible: MutableState, + coroutineScope: CoroutineScope, + callback: ConfirmCallback, + override var visuals: ConfirmDialogVisuals = ConfirmDialogVisualsImpl.Empty, + private val resultFlow: ReceiveChannel +) : ConfirmDialogHandle, DialogHandleBase(visible, coroutineScope) { + private class ResultCollector( + private val callback: ConfirmCallback + ) : FlowCollector { + fun handleResult(result: ConfirmResult) { + Log.d(TAG, "handleResult: ${result.javaClass.simpleName}") + when (result) { + ConfirmResult.Confirmed -> onConfirm() + ConfirmResult.Canceled -> onDismiss() + } + } + + fun onConfirm() { + callback.onConfirm?.invoke() + } + + fun onDismiss() { + callback.onDismiss?.invoke() + } + + override suspend fun emit(value: ConfirmResult) { + handleResult(value) + } + } + + private val resultCollector = ResultCollector(callback) + + private var awaitContinuation: CancellableContinuation? = null + + private val isCallbackEmpty = callback.isEmpty + + init { + coroutineScope.launch { + resultFlow + .consumeAsFlow() + .onEach { result -> + awaitContinuation?.let { + awaitContinuation = null + if (it.isActive) { + it.resume(result) + } + } + } + .onEach { hide() } + .collect(resultCollector) + } + } + + private suspend fun awaitResult(): ConfirmResult { + return suspendCancellableCoroutine { + awaitContinuation = it.apply { + if (isCallbackEmpty) { + invokeOnCancellation { + visible.value = false + } + } + } + } + } + + fun updateVisuals(visuals: ConfirmDialogVisuals) { + this.visuals = visuals + } + + override fun show() { + if (visuals !== ConfirmDialogVisualsImpl.Empty) { + super.show() + } else { + throw UnsupportedOperationException("can't show confirm dialog with the Empty visuals") + } + } + + override fun showConfirm( + title: String, + content: String, + markdown: Boolean, + confirm: String?, + dismiss: String? + ) { + coroutineScope.launch { + updateVisuals(ConfirmDialogVisualsImpl(title, content, markdown, confirm, dismiss)) + show() + } + } + + override suspend fun awaitConfirm( + title: String, + content: String, + markdown: Boolean, + confirm: String?, + dismiss: String? + ): ConfirmResult { + coroutineScope.launch { + updateVisuals(ConfirmDialogVisualsImpl(title, content, markdown, confirm, dismiss)) + show() + } + return awaitResult() + } + + override val dialogType: String get() = "ConfirmDialog" + + override fun toString(): String { + return "${super.toString()}(visuals: $visuals)" + } + + companion object { + fun Saver( + visible: MutableState, + coroutineScope: CoroutineScope, + callback: ConfirmCallback, + resultChannel: ReceiveChannel + ) = Saver( + save = { + it.visuals + }, + restore = { + Log.d(TAG, "ConfirmDialog restore, visuals: $it") + ConfirmDialogHandleImpl(visible, coroutineScope, callback, it, resultChannel) + } + ) + } +} + +private class CustomDialogHandleImpl( + visible: MutableState, + coroutineScope: CoroutineScope +) : DialogHandleBase(visible, coroutineScope) { + override val dialogType: String get() = "CustomDialog" +} + @Composable -fun rememberDialogHostState(): DialogHostState { +fun rememberLoadingDialog(): LoadingDialogHandle { + val visible = remember { + mutableStateOf(false) + } + val coroutineScope = rememberCoroutineScope() + + if (visible.value) { + LoadingDialog() + } + return remember { - DialogHostState() - } -} - -private inline fun DialogData?.tryInto(): T? { - return when (this) { - is T -> this - else -> null + LoadingDialogHandleImpl(visible, coroutineScope) } } @Composable -fun LoadingDialog( - state: DialogHostState = LocalDialogHost.current, -) { - state.currentDialogData.tryInto() ?: return - val dialogProperties = remember { - DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = false) +private fun rememberConfirmDialog(visuals: ConfirmDialogVisuals, callback: ConfirmCallback): ConfirmDialogHandle { + val visible = rememberSaveable { + mutableStateOf(false) } - Dialog(onDismissRequest = {}, properties = dialogProperties) { + val coroutineScope = rememberCoroutineScope() + val resultChannel = remember { + Channel() + } + + val handle = rememberSaveable( + saver = ConfirmDialogHandleImpl.Saver(visible, coroutineScope, callback, resultChannel), + init = { + ConfirmDialogHandleImpl(visible, coroutineScope, callback, visuals, resultChannel) + } + ) + + if (visible.value) { + ConfirmDialog( + handle.visuals, + confirm = { coroutineScope.launch { resultChannel.send(ConfirmResult.Confirmed) } }, + dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } } + ) + } + + return handle +} + +@Composable +fun rememberConfirmCallback(onConfirm: NullableCallback, onDismiss: NullableCallback): ConfirmCallback { + val currentOnConfirm by rememberUpdatedState(newValue = onConfirm) + val currentOnDismiss by rememberUpdatedState(newValue = onDismiss) + return remember { + ConfirmCallback({ currentOnConfirm }, { currentOnDismiss }) + } +} + +@Composable +fun rememberConfirmDialog(onConfirm: NullableCallback = null, onDismiss: NullableCallback = null): ConfirmDialogHandle { + return rememberConfirmDialog(rememberConfirmCallback(onConfirm, onDismiss)) +} + +@Composable +fun rememberConfirmDialog(callback: ConfirmCallback): ConfirmDialogHandle { + return rememberConfirmDialog(ConfirmDialogVisualsImpl.Empty, callback) +} + +@Composable +fun rememberCustomDialog(composable: @Composable (dismiss: () -> Unit) -> Unit): DialogHandle { + val visible = rememberSaveable { + mutableStateOf(false) + } + val coroutineScope = rememberCoroutineScope() + if (visible.value) { + composable { visible.value = false } + } + return remember { + CustomDialogHandleImpl(visible, coroutineScope) + } +} + +@Composable +private fun LoadingDialog() { + Dialog( + onDismissRequest = {}, + properties = DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = false) + ) { Surface( modifier = Modifier.size(100.dp), shape = RoundedCornerShape(8.dp) ) { @@ -227,41 +395,10 @@ fun LoadingDialog( } @Composable -fun PromptDialog( - state: DialogHostState = LocalDialogHost.current, -) { - val promptDialogData = state.currentDialogData.tryInto() ?: return - - val visuals = promptDialogData.visuals +private fun ConfirmDialog(visuals: ConfirmDialogVisuals, confirm: () -> Unit, dismiss: () -> Unit) { AlertDialog( onDismissRequest = { - promptDialogData.dismiss() - }, - title = { - Text(text = visuals.title) - }, - text = { - Text(text = visuals.content) - }, - confirmButton = { - TextButton(onClick = { promptDialogData.dismiss() }) { - Text(text = stringResource(id = android.R.string.ok)) - } - }, - dismissButton = null, - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ConfirmDialog(state: DialogHostState = LocalDialogHost.current) { - val confirmDialogData = state.currentDialogData.tryInto() ?: return - - val visuals = confirmDialogData.visuals - - AlertDialog( - onDismissRequest = { - confirmDialogData.dismiss() + dismiss() }, title = { Text(text = visuals.title) @@ -274,17 +411,18 @@ fun ConfirmDialog(state: DialogHostState = LocalDialogHost.current) { } }, confirmButton = { - TextButton(onClick = { confirmDialogData.confirm() }) { + TextButton(onClick = confirm) { Text(text = visuals.confirm ?: stringResource(id = android.R.string.ok)) } }, dismissButton = { - TextButton(onClick = { confirmDialogData.dismiss() }) { + TextButton(onClick = dismiss) { Text(text = visuals.dismiss ?: stringResource(id = android.R.string.cancel)) } }, ) } + @Composable private fun MarkdownContent(content: String) { val contentColor = LocalContentColor.current @@ -307,5 +445,6 @@ private fun MarkdownContent(content: String) { update = { Markwon.create(it.context).setMarkdown(it, content) it.setTextColor(contentColor.toArgb()) - }) + } + ) } \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt index 682ff9e9..500ae786 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -53,6 +54,7 @@ import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.profile.Capabilities import me.weishu.kernelsu.profile.Groups +import me.weishu.kernelsu.ui.component.rememberCustomDialog import me.weishu.kernelsu.ui.util.isSepolicyValid @OptIn(ExperimentalMaterial3Api::class) @@ -187,10 +189,7 @@ fun RootProfileConfig( @OptIn(ExperimentalLayoutApi::class) @Composable fun GroupsPanel(selected: List, closeSelection: (selection: Set) -> Unit) { - - var showDialog by remember { mutableStateOf(false) } - - if (showDialog) { + val selectGroupsDialog = rememberCustomDialog { dismiss: () -> Unit -> val groups = Groups.values().sortedWith( compareBy { if (selected.contains(it)) 0 else 1 } .then(compareBy { @@ -217,7 +216,7 @@ fun GroupsPanel(selected: List, closeSelection: (selection: Set) state = rememberUseCaseState(visible = true, onFinishedRequest = { closeSelection(selection) }, onCloseRequest = { - showDialog = false + dismiss() }), header = Header.Default( title = stringResource(R.string.profile_groups), @@ -241,7 +240,7 @@ fun GroupsPanel(selected: List, closeSelection: (selection: Set) .fillMaxWidth() .padding(16.dp) .clickable { - showDialog = true + selectGroupsDialog.show() }) { Column(modifier = Modifier.padding(16.dp)) { @@ -265,10 +264,7 @@ fun CapsPanel( selected: Collection, closeSelection: (selection: Set) -> Unit ) { - - var showDialog by remember { mutableStateOf(false) } - - if (showDialog) { + val selectCapabilitiesDialog = rememberCustomDialog { dismiss -> val caps = Capabilities.values().sortedWith( compareBy { if (selected.contains(it)) 0 else 1 } .then(compareBy { it.name }) @@ -286,7 +282,7 @@ fun CapsPanel( state = rememberUseCaseState(visible = true, onFinishedRequest = { closeSelection(selection) }, onCloseRequest = { - showDialog = false + dismiss() }), header = Header.Default( title = stringResource(R.string.profile_capabilities), @@ -309,7 +305,7 @@ fun CapsPanel( .fillMaxWidth() .padding(16.dp) .clickable { - showDialog = true + selectCapabilitiesDialog.show() }) { Column(modifier = Modifier.padding(16.dp)) { @@ -377,8 +373,7 @@ private fun SELinuxPanel( profile: Natives.Profile, onSELinuxChange: (domain: String, rules: String) -> Unit ) { - var showDialog by remember { mutableStateOf(false) } - if (showDialog) { + val editSELinuxDialog = rememberCustomDialog { dismiss -> var domain by remember { mutableStateOf(profile.context) } var rules by remember { mutableStateOf(profile.rules) } @@ -430,7 +425,7 @@ private fun SELinuxPanel( onSELinuxChange(domain, rules) }, onCloseRequest = { - showDialog = false + dismiss() }), header = Header.Default( title = stringResource(R.string.profile_selinux_context), @@ -449,7 +444,7 @@ private fun SELinuxPanel( modifier = Modifier .fillMaxWidth() .clickable { - showDialog = true + editSELinuxDialog.show() }, enabled = false, colors = TextFieldDefaults.outlinedTextFieldColors( diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt index cd98731f..b68e45a2 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt @@ -183,7 +183,7 @@ private fun AppProfileInner( } else { Mode.Custom } - var mode by remember { + var mode by rememberSaveable { mutableStateOf(initialMode) } ProfileBox(mode, true) { diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt index d7affe1d..2bbe80c3 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt @@ -5,6 +5,7 @@ import android.os.Build import android.os.PowerManager import android.system.Os import androidx.annotation.StringRes +import androidx.compose.animation.* import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -29,12 +30,10 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.* import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.ConfirmDialog -import me.weishu.kernelsu.ui.component.ConfirmResult +import me.weishu.kernelsu.ui.component.rememberConfirmDialog import me.weishu.kernelsu.ui.screen.destinations.SettingScreenDestination import me.weishu.kernelsu.ui.util.* @@ -84,7 +83,6 @@ fun HomeScreen(navigator: DestinationsNavigator) { DonateCard() LearnMoreCard() Spacer(Modifier) - ConfirmDialog() } } } @@ -99,28 +97,28 @@ fun UpdateCard() { val newVersionCode = newVersion.first val newVersionUrl = newVersion.second val changelog = newVersion.third - if (newVersionCode <= currentVersionCode) { - return - } val uriHandler = LocalUriHandler.current - val dialogHost = LocalDialogHost.current val title = stringResource(id = R.string.module_changelog) val updateText = stringResource(id = R.string.module_update) - val scope = rememberCoroutineScope() - WarningCard( - message = stringResource(id = R.string.new_version_available).format(newVersionCode), - MaterialTheme.colorScheme.outlineVariant + + AnimatedVisibility( + visible = newVersionCode >= currentVersionCode, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut() ) { - scope.launch { - if (changelog.isEmpty() || dialogHost.showConfirm( + val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) }) + WarningCard( + message = stringResource(id = R.string.new_version_available).format(newVersionCode), + MaterialTheme.colorScheme.outlineVariant + ) { + if (changelog.isNotEmpty()) { + updateDialog.showConfirm( title = title, content = changelog, markdown = true, - confirm = updateText, - ) == ConfirmResult.Confirmed - ) { - uriHandler.openUri(newVersionUrl) + confirm = updateText + ) } } } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt index 97f16315..08f8ffbb 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt @@ -40,9 +40,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.ConfirmDialog import me.weishu.kernelsu.ui.component.ConfirmResult -import me.weishu.kernelsu.ui.component.LoadingDialog +import me.weishu.kernelsu.ui.component.rememberConfirmDialog +import me.weishu.kernelsu.ui.component.rememberLoadingDialog import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination import me.weishu.kernelsu.ui.screen.destinations.WebScreenDestination import me.weishu.kernelsu.ui.util.* @@ -101,10 +101,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) { } }) { innerPadding -> - ConfirmDialog() - - LoadingDialog() - when { hasMagisk -> { Box( @@ -162,17 +158,19 @@ private fun ModuleList( val startDownloadingText = stringResource(R.string.module_start_downloading) val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed) - val dialogHost = LocalDialogHost.current val snackBarHost = LocalSnackbarHost.current val context = LocalContext.current + val loadingDialog = rememberLoadingDialog() + val confirmDialog = rememberConfirmDialog() + suspend fun onModuleUpdate( module: ModuleViewModel.ModuleInfo, changelogUrl: String, downloadUrl: String, fileName: String ) { - val changelogResult = dialogHost.withLoading { + val changelogResult = loadingDialog.withLoading { withContext(Dispatchers.IO) { runCatching { OkHttpClient().newCall( @@ -201,7 +199,7 @@ private fun ModuleList( } // changelog is not empty, show it and wait for confirm - val confirmResult = dialogHost.showConfirm( + val confirmResult = confirmDialog.awaitConfirm( changelogText, content = changelog, markdown = true, @@ -232,7 +230,7 @@ private fun ModuleList( } suspend fun onModuleUninstall(module: ModuleViewModel.ModuleInfo) { - val confirmResult = dialogHost.showConfirm( + val confirmResult = confirmDialog.awaitConfirm( moduleStr, content = moduleUninstallConfirm.format(module.name), confirm = uninstall, @@ -242,7 +240,7 @@ private fun ModuleList( return } - val success = dialogHost.withLoading { + val success = loadingDialog.withLoading { withContext(Dispatchers.IO) { uninstallModule(module.id) } @@ -327,7 +325,7 @@ private fun ModuleList( scope.launch { onModuleUninstall(module) } }, onCheckChanged = { scope.launch { - val success = dialogHost.withLoading { + val success = loadingDialog.withLoading { withContext(Dispatchers.IO) { toggleModule(module.id, !isChecked) } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt index 74516d6c..71502e98 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt @@ -4,16 +4,10 @@ import android.content.Context import android.content.Intent import android.net.Uri import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.BugReport -import androidx.compose.material.icons.filled.ContactPage -import androidx.compose.material.icons.filled.DeveloperMode -import androidx.compose.material.icons.filled.Fence -import androidx.compose.material.icons.filled.RemoveModerator -import androidx.compose.material.icons.filled.Update -import androidx.compose.material.icons.filled.Upgrade +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -30,10 +24,10 @@ import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.AboutDialog -import me.weishu.kernelsu.ui.component.LoadingDialog import me.weishu.kernelsu.ui.component.SwitchItem +import me.weishu.kernelsu.ui.component.rememberCustomDialog +import me.weishu.kernelsu.ui.component.rememberLoadingDialog import me.weishu.kernelsu.ui.screen.destinations.AppProfileTemplateScreenDestination -import me.weishu.kernelsu.ui.util.LocalDialogHost import me.weishu.kernelsu.ui.util.getBugreportFile /** @@ -43,7 +37,6 @@ import me.weishu.kernelsu.ui.util.getBugreportFile @Destination @Composable fun SettingScreen(navigator: DestinationsNavigator) { - Scaffold( topBar = { TopBar(onBack = { @@ -51,16 +44,15 @@ fun SettingScreen(navigator: DestinationsNavigator) { }) } ) { paddingValues -> - LoadingDialog() - - val showAboutDialog = remember { mutableStateOf(false) } - AboutDialog(showAboutDialog) + val aboutDialog = rememberCustomDialog { + AboutDialog(it) + } + val loadingDialog = rememberLoadingDialog() Column(modifier = Modifier.padding(paddingValues)) { val context = LocalContext.current val scope = rememberCoroutineScope() - val dialogHost = LocalDialogHost.current val profileTemplate = stringResource(id = R.string.settings_profile_template) ListItem( @@ -128,7 +120,7 @@ fun SettingScreen(navigator: DestinationsNavigator) { headlineContent = { Text(stringResource(id = R.string.send_log)) }, modifier = Modifier.clickable { scope.launch { - val bugreport = dialogHost.withLoading { + val bugreport = loadingDialog.withLoading { withContext(Dispatchers.IO) { getBugreportFile(context) } @@ -166,7 +158,7 @@ fun SettingScreen(navigator: DestinationsNavigator) { }, headlineContent = { Text(about) }, modifier = Modifier.clickable { - showAboutDialog.value = true + aboutDialog.show() } ) } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt index 643c4741..2eb2e770 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt @@ -30,7 +30,6 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.ConfirmDialog import me.weishu.kernelsu.ui.component.SearchAppBar import me.weishu.kernelsu.ui.screen.destinations.AppProfileScreenDestination import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel @@ -95,9 +94,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { ) } ) { innerPadding -> - - ConfirmDialog() - val refreshState = rememberPullRefreshState( refreshing = viewModel.isRefreshing, onRefresh = { scope.launch { viewModel.fetchAppList() } }, diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt index 63b4b520..c1b57483 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt @@ -2,12 +2,7 @@ package me.weishu.kernelsu.ui.util import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.compositionLocalOf -import me.weishu.kernelsu.ui.component.DialogHostState val LocalSnackbarHost = compositionLocalOf { error("CompositionLocal LocalSnackbarController not present") -} - -val LocalDialogHost = compositionLocalOf { - error("CompositionLocal LocalDialogController not present") } \ No newline at end of file