manager: refine dialog component and make a small fix in AboutCard (#422)

Co-authored-by: weishu <twsxtd@gmail.com>
This commit is contained in:
TinyHai
2023-04-22 18:40:11 +08:00
committed by GitHub
parent 91c80279bd
commit 057330c68f
5 changed files with 238 additions and 159 deletions

View File

@@ -3,16 +3,23 @@ package me.weishu.kernelsu.ui.component
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.widget.TextView import android.widget.TextView
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -59,46 +66,43 @@ private fun AboutCardContent() {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
) { ) {
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyMedium) { val drawable = ResourcesCompat.getDrawable(
LocalContext.current.resources,
R.mipmap.ic_launcher,
LocalContext.current.theme
)
val drawable = ResourcesCompat.getDrawable( Row {
LocalContext.current.resources, Image(
R.mipmap.ic_launcher, painter = rememberDrawablePainter(drawable),
LocalContext.current.theme contentDescription = "icon",
modifier = Modifier.size(40.dp)
) )
Row { Spacer(modifier = Modifier.width(12.dp))
Image(
painter = rememberDrawablePainter(drawable), Column {
contentDescription = "icon",
modifier = Modifier.size(40.dp) Text(
stringResource(id = R.string.app_name),
style = MaterialTheme.typography.titleSmall,
fontSize = 18.sp
)
Text(
BuildConfig.VERSION_NAME,
style = MaterialTheme.typography.bodySmall,
fontSize = 14.sp
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.height(8.dp))
Column { HtmlText(
html = stringResource(
Text( id = R.string.about_source_code,
stringResource(id = R.string.app_name), "<b><a href=\"https://github.com/tiann/KernelSU\">GitHub</a></b>",
style = MaterialTheme.typography.titleSmall, "<b><a href=\"https://t.me/KernelSU\">Telegram</a></b>"
fontSize = 18.sp
) )
Text( )
BuildConfig.VERSION_NAME,
style = MaterialTheme.typography.bodySmall,
fontSize = 14.sp
)
Spacer(modifier = Modifier.height(8.dp))
HtmlText(
html = stringResource(
id = R.string.about_source_code,
"<b><a href=\"https://github.com/tiann/KernelSU\">GitHub</a></b>",
"<b><a href=\"https://t.me/KernelSU\">Telegram</a></b>"
)
)
}
} }
} }
} }
@@ -106,6 +110,7 @@ private fun AboutCardContent() {
@Composable @Composable
fun HtmlText(html: String, modifier: Modifier = Modifier) { fun HtmlText(html: String, modifier: Modifier = Modifier) {
val contentColor = LocalContentColor.current
AndroidView( AndroidView(
modifier = modifier, modifier = modifier,
factory = { context -> factory = { context ->
@@ -113,6 +118,9 @@ fun HtmlText(html: String, modifier: Modifier = Modifier) {
it.movementMethod = LinkMovementMethod.getInstance() it.movementMethod = LinkMovementMethod.getInstance()
} }
}, },
update = { it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) } update = {
it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
it.setTextColor(contentColor.toArgb())
}
) )
} }

View File

@@ -1,56 +1,113 @@
package me.weishu.kernelsu.ui.component package me.weishu.kernelsu.ui.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import me.weishu.kernelsu.ui.util.LocalDialogHost import me.weishu.kernelsu.ui.util.LocalDialogHost
import kotlin.coroutines.resume import kotlin.coroutines.resume
sealed interface DialogResult { interface DialogVisuals
object Confirmed : DialogResult
object Dismissed : DialogResult
}
interface DialogVisuals { interface LoadingDialogVisuals : DialogVisuals
interface PromptDialogVisuals : DialogVisuals {
val title: String val title: String
val content: String val content: String
}
interface ConfirmDialogVisuals : PromptDialogVisuals {
val confirm: String? val confirm: String?
val dismiss: String? val dismiss: String?
} }
interface DialogData {
sealed interface DialogData {
val visuals: DialogVisuals val visuals: DialogVisuals
}
fun confirm() interface LoadingDialogData : DialogData {
override val visuals: LoadingDialogVisuals
fun dismiss() fun dismiss()
} }
interface PromptDialogData : DialogData {
override val visuals: PromptDialogVisuals
fun dismiss()
}
interface ConfirmDialogData : PromptDialogData {
override val visuals: ConfirmDialogVisuals
fun confirm()
}
sealed interface ConfirmResult {
object Confirmed : ConfirmResult
object Canceled : ConfirmResult
}
class DialogHostState { class DialogHostState {
private data class DialogVisualsImpl( 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 title: String,
override val content: String, override val content: String,
override val confirm: String?, override val confirm: String?,
override val dismiss: String? override val dismiss: String?
) : DialogVisuals ) : ConfirmDialogVisuals
private data class DialogDataImpl( private data class LoadingDialogDataImpl(
override val visuals: DialogVisuals, override val visuals: LoadingDialogVisuals,
val continuation: CancellableContinuation<DialogResult> private val continuation: CancellableContinuation<Unit>,
) : DialogData { ) : 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() { override fun confirm() {
if (continuation.isActive) continuation.resume(DialogResult.Confirmed) if (continuation.isActive) continuation.resume(ConfirmResult.Confirmed)
} }
override fun dismiss() { override fun dismiss() {
if (continuation.isActive) continuation.resume(DialogResult.Dismissed) if (continuation.isActive) continuation.resume(ConfirmResult.Canceled)
} }
} }
@@ -59,16 +116,58 @@ class DialogHostState {
var currentDialogData by mutableStateOf<DialogData?>(null) var currentDialogData by mutableStateOf<DialogData?>(null)
private set private set
suspend fun showDialog( 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,
confirm: String? = null, confirm: String? = null,
dismiss: String? = null dismiss: String? = null
): DialogResult = mutex.withLock { ): ConfirmResult = mutex.withLock {
try { try {
return@withLock suspendCancellableCoroutine { continuation -> return@withLock suspendCancellableCoroutine { continuation ->
currentDialogData = DialogDataImpl( currentDialogData = ConfirmDialogDataImpl(
visuals = DialogVisualsImpl(title, content, confirm, dismiss), visuals = ConfirmDialogVisualsImpl(title, content, confirm, dismiss),
continuation = continuation continuation = continuation
) )
} }
@@ -85,82 +184,85 @@ fun rememberDialogHostState(): DialogHostState {
} }
} }
@Composable private inline fun <reified T : DialogData> DialogData?.tryInto(): T? {
fun BaseDialog( return when (this) {
state: DialogHostState = LocalDialogHost.current, is T -> this
title: @Composable (String) -> Unit, else -> null
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 @Composable
fun SimpleDialog( fun LoadingDialog(
state: DialogHostState = LocalDialogHost.current, state: DialogHostState = LocalDialogHost.current,
content: @Composable (String) -> Unit
) { ) {
BaseDialog( state.currentDialogData.tryInto<LoadingDialogData>() ?: return
state = state, val dialogProperties = remember {
DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = false)
}
Dialog(onDismissRequest = {}, properties = dialogProperties) {
Surface(
modifier = Modifier
.size(100.dp),
shape = RoundedCornerShape(8.dp)
) {
Box(
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}
}
}
@Composable
fun PromptDialog(
state: DialogHostState = LocalDialogHost.current,
) {
val promptDialogData = state.currentDialogData.tryInto<PromptDialogData>() ?: return
val visuals = promptDialogData.visuals
AlertDialog(
onDismissRequest = {
promptDialogData.dismiss()
},
title = { title = {
Text(text = it) Text(text = visuals.title)
}, },
confirmButton = { text, confirm -> text = {
text?.let { Text(text = visuals.content)
TextButton(onClick = confirm) { },
Text(text = it) confirmButton = {
} TextButton(onClick = { promptDialogData.dismiss() }) {
Text(text = stringResource(id = android.R.string.ok))
} }
}, },
dismissButton = { text, dismiss -> dismissButton = null,
text?.let {
TextButton(onClick = dismiss) {
Text(text = it)
}
}
},
content = content
) )
} }
@Composable @Composable
fun ConfirmDialog(state: DialogHostState = LocalDialogHost.current) { fun ConfirmDialog(state: DialogHostState = LocalDialogHost.current) {
BaseDialog( val confirmDialogData = state.currentDialogData.tryInto<ConfirmDialogData>() ?: return
state = state,
title = { val visuals = confirmDialogData.visuals
Text(text = it) AlertDialog(
onDismissRequest = {
confirmDialogData.dismiss()
}, },
confirmButton = { text, confirm -> title = {
text?.let { Text(text = visuals.title)
TextButton(onClick = confirm) { },
Text(text = it) text = {
} Text(text = visuals.content)
},
confirmButton = {
TextButton(onClick = { confirmDialogData.dismiss() }) {
Text(text = visuals.confirm ?: stringResource(id = android.R.string.ok))
} }
}, },
dismissButton = { text, dismiss -> dismissButton = {
text?.let { TextButton(onClick = { confirmDialogData.dismiss() }) {
TextButton(onClick = dismiss) { Text(text = visuals.dismiss ?: stringResource(id = android.R.string.cancel))
Text(text = it)
}
} }
}, },
) )

View File

@@ -34,7 +34,7 @@ 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.ConfirmDialog
import me.weishu.kernelsu.ui.component.DialogResult import me.weishu.kernelsu.ui.component.ConfirmResult
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
@@ -143,13 +143,13 @@ private fun ModuleList(viewModel: ModuleViewModel, modifier: Modifier = Modifier
val snackBarHost = LocalSnackbarHost.current val snackBarHost = LocalSnackbarHost.current
suspend fun onModuleUninstall(module: ModuleViewModel.ModuleInfo) { suspend fun onModuleUninstall(module: ModuleViewModel.ModuleInfo) {
val dialogResult = dialogHost.showDialog( val confirmResult = dialogHost.showConfirm(
moduleStr, moduleStr,
content = moduleUninstallConfirm.format(module.name), content = moduleUninstallConfirm.format(module.name),
confirm = uninstall, confirm = uninstall,
dismiss = cancel dismiss = cancel
) )
if (dialogResult != DialogResult.Confirmed) { if (confirmResult != ConfirmResult.Confirmed) {
return return
} }

View File

@@ -2,21 +2,14 @@ package me.weishu.kernelsu.ui.screen
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
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.ArrowBack
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color.Companion.White
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.FileProvider 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
@@ -27,6 +20,7 @@ import kotlinx.coroutines.withContext
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.AboutDialog import me.weishu.kernelsu.ui.component.AboutDialog
import me.weishu.kernelsu.ui.component.LoadingDialog
import me.weishu.kernelsu.ui.util.LocalDialogHost import me.weishu.kernelsu.ui.util.LocalDialogHost
import me.weishu.kernelsu.ui.util.getBugreportFile import me.weishu.kernelsu.ui.util.getBugreportFile
@@ -46,13 +40,11 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}) })
} }
) { paddingValues -> ) { paddingValues ->
LoadingDialog()
val showAboutDialog = remember { mutableStateOf(false) } val showAboutDialog = remember { mutableStateOf(false) }
AboutDialog(showAboutDialog) AboutDialog(showAboutDialog)
var showLoadingDialog by remember { mutableStateOf(false) }
LoadingDialog(showLoadingDialog)
Column(modifier = Modifier.padding(paddingValues)) { Column(modifier = Modifier.padding(paddingValues)) {
val context = LocalContext.current val context = LocalContext.current
@@ -64,14 +56,12 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}, },
onClick = { onClick = {
scope.launch { scope.launch {
showLoadingDialog = true val bugreport = dialogHost.withLoading {
withContext(Dispatchers.IO) {
val bugreport = withContext(Dispatchers.IO) { getBugreportFile(context)
getBugreportFile(context) }
} }
showLoadingDialog = false
val uri: Uri = val uri: Uri =
FileProvider.getUriForFile( FileProvider.getUriForFile(
context, context,
@@ -119,24 +109,3 @@ private fun TopBar(onBack: () -> Unit = {}) {
}, },
) )
} }
@Composable
fun LoadingDialog(showLoadingDialog: Boolean) {
if (!showLoadingDialog) {
return
}
Dialog(
onDismissRequest = { },
DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(100.dp)
.background(White, shape = RoundedCornerShape(8.dp))
) {
CircularProgressIndicator()
}
}
}

View File

@@ -25,7 +25,7 @@ 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.ConfirmDialog
import me.weishu.kernelsu.ui.component.DialogResult import me.weishu.kernelsu.ui.component.ConfirmResult
import me.weishu.kernelsu.ui.component.SearchAppBar import me.weishu.kernelsu.ui.component.SearchAppBar
import me.weishu.kernelsu.ui.util.LocalDialogHost import me.weishu.kernelsu.ui.util.LocalDialogHost
import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.LocalSnackbarHost
@@ -119,13 +119,13 @@ fun SuperUserScreen() {
AppItem(app, isChecked) { checked -> AppItem(app, isChecked) { checked ->
scope.launch { scope.launch {
if (checked) { if (checked) {
val dialogResult = dialogHost.showDialog( val confirmResult = dialogHost.showConfirm(
app.label, app.label,
content = content, content = content,
confirm = confirm, confirm = confirm,
dismiss = cancel dismiss = cancel
) )
if (dialogResult != DialogResult.Confirmed) { if (confirmResult != ConfirmResult.Confirmed) {
return@launch return@launch
} }
} }