From 81bbb31098b82bbf3f8ef7d1eaea7d01ea544a21 Mon Sep 17 00:00:00 2001 From: weishu Date: Mon, 11 Sep 2023 00:03:21 +0800 Subject: [PATCH] manager: show changelog before update module --- manager/app/build.gradle.kts | 2 + .../me/weishu/kernelsu/ui/component/Dialog.kt | 98 ++++++++++++++--- .../me/weishu/kernelsu/ui/screen/Module.kt | 101 +++++++++++++----- .../kernelsu/ui/viewmodel/ModuleViewModel.kt | 6 +- manager/app/src/main/res/values/strings.xml | 1 + manager/gradle/libs.versions.toml | 3 + 6 files changed, 171 insertions(+), 40 deletions(-) diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index d0056182..4463e4f0 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -111,4 +111,6 @@ dependencies { implementation(libs.sheet.compose.dialogs.core) implementation(libs.sheet.compose.dialogs.list) implementation(libs.sheet.compose.dialogs.input) + + implementation(libs.markdown) } 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 64446862..c572290a 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,26 +1,46 @@ package me.weishu.kernelsu.ui.component +import android.graphics.text.LineBreaker +import android.text.Layout +import android.text.method.LinkMovementMethod +import android.view.ViewGroup +import android.widget.TextView import androidx.compose.foundation.layout.Box +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.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import com.maxkeppeker.sheets.core.CoreDialog +import com.maxkeppeker.sheets.core.models.CoreSelection +import com.maxkeppeker.sheets.core.models.base.Header +import com.maxkeppeker.sheets.core.models.base.SelectionButton +import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState +import io.noties.markwon.Markwon +import io.noties.markwon.utils.NoCopySpannableFactory import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import me.weishu.kernelsu.ui.util.LocalDialogHost import kotlin.coroutines.resume @@ -36,6 +56,7 @@ interface PromptDialogVisuals : DialogVisuals { interface ConfirmDialogVisuals : PromptDialogVisuals { val confirm: String? val dismiss: String? + val isMarkdown: Boolean } @@ -68,15 +89,15 @@ class DialogHostState { private object LoadingDialogVisualsImpl : LoadingDialogVisuals private data class PromptDialogVisualsImpl( - override val title: String, - override val content: String + 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 dismiss: String?, + override val isMarkdown: Boolean, ) : ConfirmDialogVisuals private data class LoadingDialogDataImpl( @@ -121,8 +142,7 @@ class DialogHostState { mutex.withLock { suspendCancellableCoroutine { continuation -> currentDialogData = LoadingDialogDataImpl( - visuals = LoadingDialogVisualsImpl, - continuation = continuation + visuals = LoadingDialogVisualsImpl, continuation = continuation ) } } @@ -159,15 +179,12 @@ class DialogHostState { } suspend fun showConfirm( - title: String, - content: String, - confirm: String? = null, - dismiss: String? = null + 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), + visuals = ConfirmDialogVisualsImpl(title, content, confirm, dismiss, markdown), continuation = continuation ) } @@ -201,9 +218,7 @@ fun LoadingDialog( } Dialog(onDismissRequest = {}, properties = dialogProperties) { Surface( - modifier = Modifier - .size(100.dp), - shape = RoundedCornerShape(8.dp) + modifier = Modifier.size(100.dp), shape = RoundedCornerShape(8.dp) ) { Box( contentAlignment = Alignment.Center, @@ -240,11 +255,44 @@ fun PromptDialog( ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ConfirmDialog(state: DialogHostState = LocalDialogHost.current) { val confirmDialogData = state.currentDialogData.tryInto() ?: return val visuals = confirmDialogData.visuals + + if (visuals.isMarkdown) { + CoreDialog( + state = rememberUseCaseState(visible = true, onCloseRequest = { + confirmDialogData.dismiss() + }), + header = Header.Default( + title = visuals.title + ), + selection = CoreSelection( + withButtonView = true, + negativeButton = SelectionButton( + visuals.dismiss ?: stringResource(id = android.R.string.cancel), + ), + positiveButton = SelectionButton( + visuals.confirm ?: stringResource(id = android.R.string.ok), + ), + onPositiveClick = { + confirmDialogData.confirm() + }, + onNegativeClick = { + confirmDialogData.dismiss() + }, + ), + onPositiveValid = true, + body = { + MarkdownContent(visuals.content) + }, + ) + return + } + AlertDialog( onDismissRequest = { confirmDialogData.dismiss() @@ -266,4 +314,28 @@ fun ConfirmDialog(state: DialogHostState = LocalDialogHost.current) { } }, ) +} +@Composable +private fun MarkdownContent(content: String) { + val contentColor = LocalContentColor.current + + AndroidView( + factory = { context -> + TextView(context).apply { + movementMethod = LinkMovementMethod.getInstance() + setSpannableFactory(NoCopySpannableFactory.getInstance()) + breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE + hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + 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/screen/Module.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt index 6e6da318..5e6b4e12 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 @@ -45,6 +45,7 @@ import me.weishu.kernelsu.ui.component.LoadingDialog import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination import me.weishu.kernelsu.ui.util.* import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel +import okhttp3.OkHttpClient @Destination @Composable @@ -145,9 +146,68 @@ private fun ModuleList( val uninstall = stringResource(id = R.string.uninstall) val cancel = stringResource(id = android.R.string.cancel) val moduleUninstallConfirm = stringResource(id = R.string.module_uninstall_confirm) + val updateText = stringResource(R.string.module_update) + val changelogText = stringResource(R.string.module_changelog) + val downloadingText = stringResource(R.string.module_downloading) + val startDownloadingText = stringResource(R.string.module_start_downloading) val dialogHost = LocalDialogHost.current val snackBarHost = LocalSnackbarHost.current + val context = LocalContext.current + + suspend fun onModuleUpdate( + module: ModuleViewModel.ModuleInfo, + changelogUrl: String, + downloadUrl: String, + fileName: String + ) { + val changelog = dialogHost.withLoading { + withContext(Dispatchers.IO) { + val str = OkHttpClient().newCall( + okhttp3.Request.Builder().url(changelogUrl).build() + ).execute().body!!.string() + if (str.length > 1000) str.substring(0, 1000) else str + } + } + + if (changelog.isNotEmpty()) { + // changelog is not empty, show it and wait for confirm + val confirmResult = dialogHost.showConfirm( + changelogText, + content = changelog, + markdown = true, + confirm = updateText, + ) + + if (confirmResult != ConfirmResult.Confirmed) { + return + } + } + + withContext(Dispatchers.Main) { + Toast.makeText( + context, + startDownloadingText.format(module.name), + Toast.LENGTH_SHORT + ).show() + } + + val downloading = downloadingText.format(module.name) + withContext(Dispatchers.IO) { + download( + context, + downloadUrl, + fileName, + downloading, + onDownloaded = onInstallModule, + onDownloading = { + launch(Dispatchers.Main) { + Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show() + } + } + ) + } + } suspend fun onModuleUninstall(module: ModuleViewModel.ModuleInfo) { val confirmResult = dialogHost.showConfirm( @@ -209,33 +269,38 @@ private fun ModuleList( modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center ) { - Text(stringResource(R.string.module_overlay_fs_not_available), textAlign = TextAlign.Center) + Text( + stringResource(R.string.module_overlay_fs_not_available), + textAlign = TextAlign.Center + ) } } } + viewModel.moduleList.isEmpty() -> { item { Box( modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center ) { - Text(stringResource(R.string.module_empty), textAlign = TextAlign.Center) + Text( + stringResource(R.string.module_empty), + textAlign = TextAlign.Center + ) } } } + else -> { items(viewModel.moduleList) { module -> var isChecked by rememberSaveable(module) { mutableStateOf(module.enabled) } val scope = rememberCoroutineScope() - val updatedModule by produceState(initialValue = "" to "") { + val updatedModule by produceState(initialValue = Triple("", "", "")) { scope.launch(Dispatchers.IO) { value = viewModel.checkUpdate(module) } } - val downloadingText = stringResource(R.string.module_downloading) - val startDownloadingText = stringResource(R.string.module_start_downloading) - ModuleItem(module, isChecked, updatedModule.first, onUninstall = { scope.launch { onModuleUninstall(module) } }, onCheckChanged = { @@ -261,26 +326,14 @@ private fun ModuleList( } } }, onUpdate = { - scope.launch { - Toast.makeText( - context, - startDownloadingText.format(module.name), - Toast.LENGTH_SHORT - ).show() + onModuleUpdate( + module, + updatedModule.third, + updatedModule.first, + "${module.name}-${updatedModule.second}.zip" + ) } - - val downloading = downloadingText.format(module.name) - download( - context, - updatedModule.first, - "${module.name}-${updatedModule.second}.zip", - downloading, - onDownloaded = onInstallModule, - onDownloading = { - Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show() - } - ) }) // fix last item shadow incomplete in LazyColumn diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt index 82582f85..1d3bee0c 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt @@ -115,8 +115,8 @@ class ModuleViewModel : ViewModel() { } } - fun checkUpdate(m: ModuleInfo): Pair { - val empty = "" to "" + fun checkUpdate(m: ModuleInfo): Triple { + val empty = Triple("", "", "") if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) { return empty } @@ -155,6 +155,6 @@ class ModuleViewModel : ViewModel() { return empty } - return zipUrl to version + return Triple(zipUrl, version, changelog) } } diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 031b92aa..d174ff60 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -84,4 +84,5 @@ Force Stop Restart Failed to update SELinux rules for: %s + Changelog diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index 160c13d8..6bc49f3c 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -9,6 +9,7 @@ navigation = "2.5.3" compose-destination = "1.9.42-beta" libsu = "5.0.5" sheets-compose-dialogs = "1.2.0" +markdown = "4.6.2" [plugins] agp-app = { id = "com.android.application", version.ref = "agp" } @@ -56,3 +57,5 @@ compose-destinations-ksp = { group = "io.github.raamcosta.compose-destinations", sheet-compose-dialogs-core = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "core", version.ref = "sheets-compose-dialogs"} sheet-compose-dialogs-list = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "list", version.ref = "sheets-compose-dialogs"} sheet-compose-dialogs-input = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "input", version.ref = "sheets-compose-dialogs"} + +markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" } \ No newline at end of file