manager: refine dialog component & add an animation to UpdateCard (#1429)

This commit is contained in:
TinyHai
2024-03-08 10:31:14 +08:00
committed by GitHub
parent 7611accc33
commit 425713fad3
10 changed files with 384 additions and 281 deletions

View File

@@ -5,13 +5,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon import androidx.compose.material3.*
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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -27,11 +21,9 @@ import com.ramcosta.composedestinations.navigation.popBackStack
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
import me.weishu.kernelsu.Natives import me.weishu.kernelsu.Natives
import me.weishu.kernelsu.ksuApp 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.BottomBarDestination
import me.weishu.kernelsu.ui.screen.NavGraphs import me.weishu.kernelsu.ui.screen.NavGraphs
import me.weishu.kernelsu.ui.theme.KernelSUTheme 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.LocalSnackbarHost
import me.weishu.kernelsu.ui.util.rootAvailable import me.weishu.kernelsu.ui.util.rootAvailable
@@ -54,7 +46,6 @@ class MainActivity : ComponentActivity() {
) { innerPadding -> ) { innerPadding ->
CompositionLocalProvider( CompositionLocalProvider(
LocalSnackbarHost provides snackbarHostState, LocalSnackbarHost provides snackbarHostState,
LocalDialogHost provides rememberDialogHostState(),
) { ) {
DestinationsNavHost( DestinationsNavHost(
modifier = Modifier.padding(innerPadding), modifier = Modifier.padding(innerPadding),

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -52,12 +53,10 @@ fun AboutCard() {
} }
@Composable @Composable
fun AboutDialog(showAboutDialog: MutableState<Boolean>) { fun AboutDialog(dismiss: () -> Unit) {
if (showAboutDialog.value) { Dialog(onDismissRequest = { dismiss() }) {
Dialog(onDismissRequest = { showAboutDialog.value = false }) {
AboutCard() AboutCard()
} }
}
} }
@Composable @Composable

View File

@@ -1,8 +1,10 @@
package me.weishu.kernelsu.ui.component package me.weishu.kernelsu.ui.component
import android.graphics.text.LineBreaker import android.graphics.text.LineBreaker
import android.os.Parcelable
import android.text.Layout import android.text.Layout
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.compose.foundation.layout.Box 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.size
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog import androidx.compose.material3.*
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.runtime.* 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
@@ -28,48 +26,48 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.utils.NoCopySpannableFactory import io.noties.markwon.utils.NoCopySpannableFactory
import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.*
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.flow.onEach
import me.weishu.kernelsu.ui.util.LocalDialogHost import kotlinx.parcelize.Parcelize
import kotlin.coroutines.resume import kotlin.coroutines.resume
interface DialogVisuals private const val TAG = "DialogComponent"
interface LoadingDialogVisuals : DialogVisuals interface ConfirmDialogVisuals : Parcelable {
interface PromptDialogVisuals : DialogVisuals {
val title: String val title: String
val content: String val content: String
} val isMarkdown: Boolean
interface ConfirmDialogVisuals : PromptDialogVisuals {
val confirm: String? val confirm: String?
val dismiss: String? val dismiss: String?
val isMarkdown: Boolean
} }
@Parcelize
sealed interface DialogData { private data class ConfirmDialogVisualsImpl(
val visuals: DialogVisuals 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 { interface DialogHandle {
override val visuals: LoadingDialogVisuals val isShown: Boolean
fun dismiss() val dialogType: String
fun show()
fun hide()
} }
interface PromptDialogData : DialogData { interface LoadingDialogHandle : DialogHandle {
override val visuals: PromptDialogVisuals suspend fun <R> withLoading(block: suspend () -> R): R
fun dismiss() fun showLoading()
}
interface ConfirmDialogData : PromptDialogData {
override val visuals: ConfirmDialogVisuals
fun confirm()
} }
sealed interface ConfirmResult { sealed interface ConfirmResult {
@@ -77,143 +75,313 @@ sealed interface ConfirmResult {
object Canceled : ConfirmResult object Canceled : ConfirmResult
} }
class DialogHostState { interface ConfirmDialogHandle : DialogHandle {
val visuals: ConfirmDialogVisuals
private object LoadingDialogVisualsImpl : LoadingDialogVisuals fun showConfirm(
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<Unit>,
) : LoadingDialogData {
override fun dismiss() {
if (continuation.isActive) continuation.resume(Unit)
}
}
private data class PromptDialogDataImpl(
override val visuals: PromptDialogVisuals,
private val continuation: CancellableContinuation<Unit>,
) : PromptDialogData {
override fun dismiss() {
if (continuation.isActive) continuation.resume(Unit)
}
}
private data class ConfirmDialogDataImpl(
override val visuals: ConfirmDialogVisuals,
private val continuation: CancellableContinuation<ConfirmResult>
) : 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<DialogData?>(null)
private set
suspend fun showLoading() {
try {
mutex.withLock {
suspendCancellableCoroutine { continuation ->
currentDialogData = LoadingDialogDataImpl(
visuals = LoadingDialogVisualsImpl, continuation = continuation
)
}
}
} finally {
currentDialogData = null
}
}
suspend fun <R> 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(
title: String, title: String,
content: String, content: String,
markdown: Boolean = false, markdown: Boolean = false,
confirm: String? = null, confirm: String? = null,
dismiss: String? = null dismiss: String? = null
): ConfirmResult = mutex.withLock { )
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<Boolean>,
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<Boolean>,
coroutineScope: CoroutineScope
) : LoadingDialogHandle, DialogHandleBase(visible, coroutineScope) {
override suspend fun <R> withLoading(block: suspend () -> R): R {
return coroutineScope.async {
try { try {
return@withLock suspendCancellableCoroutine { continuation -> visible.value = true
currentDialogData = ConfirmDialogDataImpl( block()
visuals = ConfirmDialogVisualsImpl(title, content, confirm, dismiss, markdown), } finally {
continuation = continuation 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()
}
}
}
}
private class ConfirmDialogHandleImpl(
visible: MutableState<Boolean>,
coroutineScope: CoroutineScope,
callback: ConfirmCallback,
override var visuals: ConfirmDialogVisuals = ConfirmDialogVisualsImpl.Empty,
private val resultFlow: ReceiveChannel<ConfirmResult>
) : ConfirmDialogHandle, DialogHandleBase(visible, coroutineScope) {
private class ResultCollector(
private val callback: ConfirmCallback
) : FlowCollector<ConfirmResult> {
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<ConfirmResult>? = 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<Boolean>,
coroutineScope: CoroutineScope,
callback: ConfirmCallback,
resultChannel: ReceiveChannel<ConfirmResult>
) = Saver<ConfirmDialogHandle, ConfirmDialogVisuals>(
save = {
it.visuals
},
restore = {
Log.d(TAG, "ConfirmDialog restore, visuals: $it")
ConfirmDialogHandleImpl(visible, coroutineScope, callback, it, resultChannel)
}
) )
} }
} finally { }
currentDialogData = null
} private class CustomDialogHandleImpl(
} visible: MutableState<Boolean>,
coroutineScope: CoroutineScope
) : DialogHandleBase(visible, coroutineScope) {
override val dialogType: String get() = "CustomDialog"
} }
@Composable @Composable
fun rememberDialogHostState(): DialogHostState { fun rememberLoadingDialog(): LoadingDialogHandle {
val visible = remember {
mutableStateOf(false)
}
val coroutineScope = rememberCoroutineScope()
if (visible.value) {
LoadingDialog()
}
return remember { return remember {
DialogHostState() LoadingDialogHandleImpl(visible, coroutineScope)
}
}
private inline fun <reified T : DialogData> DialogData?.tryInto(): T? {
return when (this) {
is T -> this
else -> null
} }
} }
@Composable @Composable
fun LoadingDialog( private fun rememberConfirmDialog(visuals: ConfirmDialogVisuals, callback: ConfirmCallback): ConfirmDialogHandle {
state: DialogHostState = LocalDialogHost.current, val visible = rememberSaveable {
) { mutableStateOf(false)
state.currentDialogData.tryInto<LoadingDialogData>() ?: return
val dialogProperties = remember {
DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = false)
} }
Dialog(onDismissRequest = {}, properties = dialogProperties) { val coroutineScope = rememberCoroutineScope()
val resultChannel = remember {
Channel<ConfirmResult>()
}
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( Surface(
modifier = Modifier.size(100.dp), shape = RoundedCornerShape(8.dp) modifier = Modifier.size(100.dp), shape = RoundedCornerShape(8.dp)
) { ) {
@@ -227,41 +395,10 @@ fun LoadingDialog(
} }
@Composable @Composable
fun PromptDialog( private fun ConfirmDialog(visuals: ConfirmDialogVisuals, confirm: () -> Unit, dismiss: () -> Unit) {
state: DialogHostState = LocalDialogHost.current,
) {
val promptDialogData = state.currentDialogData.tryInto<PromptDialogData>() ?: return
val visuals = promptDialogData.visuals
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest = {
promptDialogData.dismiss() 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<ConfirmDialogData>() ?: return
val visuals = confirmDialogData.visuals
AlertDialog(
onDismissRequest = {
confirmDialogData.dismiss()
}, },
title = { title = {
Text(text = visuals.title) Text(text = visuals.title)
@@ -274,17 +411,18 @@ fun ConfirmDialog(state: DialogHostState = LocalDialogHost.current) {
} }
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { confirmDialogData.confirm() }) { TextButton(onClick = confirm) {
Text(text = visuals.confirm ?: stringResource(id = android.R.string.ok)) Text(text = visuals.confirm ?: stringResource(id = android.R.string.ok))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { confirmDialogData.dismiss() }) { TextButton(onClick = dismiss) {
Text(text = visuals.dismiss ?: stringResource(id = android.R.string.cancel)) Text(text = visuals.dismiss ?: stringResource(id = android.R.string.cancel))
} }
}, },
) )
} }
@Composable @Composable
private fun MarkdownContent(content: String) { private fun MarkdownContent(content: String) {
val contentColor = LocalContentColor.current val contentColor = LocalContentColor.current
@@ -307,5 +445,6 @@ private fun MarkdownContent(content: String) {
update = { update = {
Markwon.create(it.context).setMarkdown(it, content) Markwon.create(it.context).setMarkdown(it, content)
it.setTextColor(contentColor.toArgb()) it.setTextColor(contentColor.toArgb())
}) }
)
} }

View File

@@ -28,6 +28,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -53,6 +54,7 @@ import me.weishu.kernelsu.Natives
import me.weishu.kernelsu.R import me.weishu.kernelsu.R
import me.weishu.kernelsu.profile.Capabilities import me.weishu.kernelsu.profile.Capabilities
import me.weishu.kernelsu.profile.Groups import me.weishu.kernelsu.profile.Groups
import me.weishu.kernelsu.ui.component.rememberCustomDialog
import me.weishu.kernelsu.ui.util.isSepolicyValid import me.weishu.kernelsu.ui.util.isSepolicyValid
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -187,10 +189,7 @@ fun RootProfileConfig(
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>) -> Unit) { fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>) -> Unit) {
val selectGroupsDialog = rememberCustomDialog { dismiss: () -> Unit ->
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
val groups = Groups.values().sortedWith( val groups = Groups.values().sortedWith(
compareBy<Groups> { if (selected.contains(it)) 0 else 1 } compareBy<Groups> { if (selected.contains(it)) 0 else 1 }
.then(compareBy { .then(compareBy {
@@ -217,7 +216,7 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
state = rememberUseCaseState(visible = true, onFinishedRequest = { state = rememberUseCaseState(visible = true, onFinishedRequest = {
closeSelection(selection) closeSelection(selection)
}, onCloseRequest = { }, onCloseRequest = {
showDialog = false dismiss()
}), }),
header = Header.Default( header = Header.Default(
title = stringResource(R.string.profile_groups), title = stringResource(R.string.profile_groups),
@@ -241,7 +240,7 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(16.dp)
.clickable { .clickable {
showDialog = true selectGroupsDialog.show()
}) { }) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
@@ -265,10 +264,7 @@ fun CapsPanel(
selected: Collection<Capabilities>, selected: Collection<Capabilities>,
closeSelection: (selection: Set<Capabilities>) -> Unit closeSelection: (selection: Set<Capabilities>) -> Unit
) { ) {
val selectCapabilitiesDialog = rememberCustomDialog { dismiss ->
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
val caps = Capabilities.values().sortedWith( val caps = Capabilities.values().sortedWith(
compareBy<Capabilities> { if (selected.contains(it)) 0 else 1 } compareBy<Capabilities> { if (selected.contains(it)) 0 else 1 }
.then(compareBy { it.name }) .then(compareBy { it.name })
@@ -286,7 +282,7 @@ fun CapsPanel(
state = rememberUseCaseState(visible = true, onFinishedRequest = { state = rememberUseCaseState(visible = true, onFinishedRequest = {
closeSelection(selection) closeSelection(selection)
}, onCloseRequest = { }, onCloseRequest = {
showDialog = false dismiss()
}), }),
header = Header.Default( header = Header.Default(
title = stringResource(R.string.profile_capabilities), title = stringResource(R.string.profile_capabilities),
@@ -309,7 +305,7 @@ fun CapsPanel(
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(16.dp)
.clickable { .clickable {
showDialog = true selectCapabilitiesDialog.show()
}) { }) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
@@ -377,8 +373,7 @@ private fun SELinuxPanel(
profile: Natives.Profile, profile: Natives.Profile,
onSELinuxChange: (domain: String, rules: String) -> Unit onSELinuxChange: (domain: String, rules: String) -> Unit
) { ) {
var showDialog by remember { mutableStateOf(false) } val editSELinuxDialog = rememberCustomDialog { dismiss ->
if (showDialog) {
var domain by remember { mutableStateOf(profile.context) } var domain by remember { mutableStateOf(profile.context) }
var rules by remember { mutableStateOf(profile.rules) } var rules by remember { mutableStateOf(profile.rules) }
@@ -430,7 +425,7 @@ private fun SELinuxPanel(
onSELinuxChange(domain, rules) onSELinuxChange(domain, rules)
}, },
onCloseRequest = { onCloseRequest = {
showDialog = false dismiss()
}), }),
header = Header.Default( header = Header.Default(
title = stringResource(R.string.profile_selinux_context), title = stringResource(R.string.profile_selinux_context),
@@ -449,7 +444,7 @@ private fun SELinuxPanel(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
showDialog = true editSELinuxDialog.show()
}, },
enabled = false, enabled = false,
colors = TextFieldDefaults.outlinedTextFieldColors( colors = TextFieldDefaults.outlinedTextFieldColors(

View File

@@ -183,7 +183,7 @@ private fun AppProfileInner(
} else { } else {
Mode.Custom Mode.Custom
} }
var mode by remember { var mode by rememberSaveable {
mutableStateOf(initialMode) mutableStateOf(initialMode)
} }
ProfileBox(mode, true) { ProfileBox(mode, true) {

View File

@@ -5,6 +5,7 @@ import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import android.system.Os import android.system.Os
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.animation.*
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState 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.annotation.RootNavGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.weishu.kernelsu.* import me.weishu.kernelsu.*
import me.weishu.kernelsu.R import me.weishu.kernelsu.R
import me.weishu.kernelsu.ui.component.ConfirmDialog import me.weishu.kernelsu.ui.component.rememberConfirmDialog
import me.weishu.kernelsu.ui.component.ConfirmResult
import me.weishu.kernelsu.ui.screen.destinations.SettingScreenDestination import me.weishu.kernelsu.ui.screen.destinations.SettingScreenDestination
import me.weishu.kernelsu.ui.util.* import me.weishu.kernelsu.ui.util.*
@@ -84,7 +83,6 @@ fun HomeScreen(navigator: DestinationsNavigator) {
DonateCard() DonateCard()
LearnMoreCard() LearnMoreCard()
Spacer(Modifier) Spacer(Modifier)
ConfirmDialog()
} }
} }
} }
@@ -99,28 +97,28 @@ fun UpdateCard() {
val newVersionCode = newVersion.first val newVersionCode = newVersion.first
val newVersionUrl = newVersion.second val newVersionUrl = newVersion.second
val changelog = newVersion.third val changelog = newVersion.third
if (newVersionCode <= currentVersionCode) {
return
}
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val dialogHost = LocalDialogHost.current
val title = stringResource(id = R.string.module_changelog) val title = stringResource(id = R.string.module_changelog)
val updateText = stringResource(id = R.string.module_update) val updateText = stringResource(id = R.string.module_update)
val scope = rememberCoroutineScope()
AnimatedVisibility(
visible = newVersionCode >= currentVersionCode,
enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut()
) {
val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) })
WarningCard( WarningCard(
message = stringResource(id = R.string.new_version_available).format(newVersionCode), message = stringResource(id = R.string.new_version_available).format(newVersionCode),
MaterialTheme.colorScheme.outlineVariant MaterialTheme.colorScheme.outlineVariant
) { ) {
scope.launch { if (changelog.isNotEmpty()) {
if (changelog.isEmpty() || dialogHost.showConfirm( updateDialog.showConfirm(
title = title, title = title,
content = changelog, content = changelog,
markdown = true, markdown = true,
confirm = updateText, confirm = updateText
) == ConfirmResult.Confirmed )
) {
uriHandler.openUri(newVersionUrl)
} }
} }
} }

View File

@@ -40,9 +40,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
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.ConfirmResult 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.InstallScreenDestination
import me.weishu.kernelsu.ui.screen.destinations.WebScreenDestination import me.weishu.kernelsu.ui.screen.destinations.WebScreenDestination
import me.weishu.kernelsu.ui.util.* import me.weishu.kernelsu.ui.util.*
@@ -101,10 +101,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
} }
}) { innerPadding -> }) { innerPadding ->
ConfirmDialog()
LoadingDialog()
when { when {
hasMagisk -> { hasMagisk -> {
Box( Box(
@@ -162,17 +158,19 @@ private fun ModuleList(
val startDownloadingText = stringResource(R.string.module_start_downloading) val startDownloadingText = stringResource(R.string.module_start_downloading)
val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed) val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed)
val dialogHost = LocalDialogHost.current
val snackBarHost = LocalSnackbarHost.current val snackBarHost = LocalSnackbarHost.current
val context = LocalContext.current val context = LocalContext.current
val loadingDialog = rememberLoadingDialog()
val confirmDialog = rememberConfirmDialog()
suspend fun onModuleUpdate( suspend fun onModuleUpdate(
module: ModuleViewModel.ModuleInfo, module: ModuleViewModel.ModuleInfo,
changelogUrl: String, changelogUrl: String,
downloadUrl: String, downloadUrl: String,
fileName: String fileName: String
) { ) {
val changelogResult = dialogHost.withLoading { val changelogResult = loadingDialog.withLoading {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
runCatching { runCatching {
OkHttpClient().newCall( OkHttpClient().newCall(
@@ -201,7 +199,7 @@ private fun ModuleList(
} }
// changelog is not empty, show it and wait for confirm // changelog is not empty, show it and wait for confirm
val confirmResult = dialogHost.showConfirm( val confirmResult = confirmDialog.awaitConfirm(
changelogText, changelogText,
content = changelog, content = changelog,
markdown = true, markdown = true,
@@ -232,7 +230,7 @@ private fun ModuleList(
} }
suspend fun onModuleUninstall(module: ModuleViewModel.ModuleInfo) { suspend fun onModuleUninstall(module: ModuleViewModel.ModuleInfo) {
val confirmResult = dialogHost.showConfirm( val confirmResult = confirmDialog.awaitConfirm(
moduleStr, moduleStr,
content = moduleUninstallConfirm.format(module.name), content = moduleUninstallConfirm.format(module.name),
confirm = uninstall, confirm = uninstall,
@@ -242,7 +240,7 @@ private fun ModuleList(
return return
} }
val success = dialogHost.withLoading { val success = loadingDialog.withLoading {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
uninstallModule(module.id) uninstallModule(module.id)
} }
@@ -327,7 +325,7 @@ private fun ModuleList(
scope.launch { onModuleUninstall(module) } scope.launch { onModuleUninstall(module) }
}, onCheckChanged = { }, onCheckChanged = {
scope.launch { scope.launch {
val success = dialogHost.withLoading { val success = loadingDialog.withLoading {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
toggleModule(module.id, !isChecked) toggleModule(module.id, !isChecked)
} }

View File

@@ -4,16 +4,10 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.clickable 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.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.*
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.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@@ -30,10 +24,10 @@ import me.weishu.kernelsu.BuildConfig
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.AboutDialog 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.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.screen.destinations.AppProfileTemplateScreenDestination
import me.weishu.kernelsu.ui.util.LocalDialogHost
import me.weishu.kernelsu.ui.util.getBugreportFile import me.weishu.kernelsu.ui.util.getBugreportFile
/** /**
@@ -43,7 +37,6 @@ import me.weishu.kernelsu.ui.util.getBugreportFile
@Destination @Destination
@Composable @Composable
fun SettingScreen(navigator: DestinationsNavigator) { fun SettingScreen(navigator: DestinationsNavigator) {
Scaffold( Scaffold(
topBar = { topBar = {
TopBar(onBack = { TopBar(onBack = {
@@ -51,16 +44,15 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}) })
} }
) { paddingValues -> ) { paddingValues ->
LoadingDialog() val aboutDialog = rememberCustomDialog {
AboutDialog(it)
val showAboutDialog = remember { mutableStateOf(false) } }
AboutDialog(showAboutDialog) val loadingDialog = rememberLoadingDialog()
Column(modifier = Modifier.padding(paddingValues)) { Column(modifier = Modifier.padding(paddingValues)) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val dialogHost = LocalDialogHost.current
val profileTemplate = stringResource(id = R.string.settings_profile_template) val profileTemplate = stringResource(id = R.string.settings_profile_template)
ListItem( ListItem(
@@ -128,7 +120,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
headlineContent = { Text(stringResource(id = R.string.send_log)) }, headlineContent = { Text(stringResource(id = R.string.send_log)) },
modifier = Modifier.clickable { modifier = Modifier.clickable {
scope.launch { scope.launch {
val bugreport = dialogHost.withLoading { val bugreport = loadingDialog.withLoading {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
getBugreportFile(context) getBugreportFile(context)
} }
@@ -166,7 +158,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}, },
headlineContent = { Text(about) }, headlineContent = { Text(about) },
modifier = Modifier.clickable { modifier = Modifier.clickable {
showAboutDialog.value = true aboutDialog.show()
} }
) )
} }

View File

@@ -30,7 +30,6 @@ 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.SearchAppBar import me.weishu.kernelsu.ui.component.SearchAppBar
import me.weishu.kernelsu.ui.screen.destinations.AppProfileScreenDestination import me.weishu.kernelsu.ui.screen.destinations.AppProfileScreenDestination
import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel
@@ -95,9 +94,6 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
) )
} }
) { innerPadding -> ) { innerPadding ->
ConfirmDialog()
val refreshState = rememberPullRefreshState( val refreshState = rememberPullRefreshState(
refreshing = viewModel.isRefreshing, refreshing = viewModel.isRefreshing,
onRefresh = { scope.launch { viewModel.fetchAppList() } }, onRefresh = { scope.launch { viewModel.fetchAppList() } },

View File

@@ -2,12 +2,7 @@ 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")
}