Skip to content

Commit

Permalink
manager: show changelog before update module
Browse files Browse the repository at this point in the history
  • Loading branch information
tiann committed Sep 10, 2023
1 parent 52234d0 commit 81bbb31
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 40 deletions.
2 changes: 2 additions & 0 deletions manager/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
98 changes: 85 additions & 13 deletions manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -36,6 +56,7 @@ interface PromptDialogVisuals : DialogVisuals {
interface ConfirmDialogVisuals : PromptDialogVisuals {
val confirm: String?
val dismiss: String?
val isMarkdown: Boolean
}


Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -121,8 +142,7 @@ class DialogHostState {
mutex.withLock {
suspendCancellableCoroutine { continuation ->
currentDialogData = LoadingDialogDataImpl(
visuals = LoadingDialogVisualsImpl,
continuation = continuation
visuals = LoadingDialogVisualsImpl, continuation = continuation
)
}
}
Expand Down Expand Up @@ -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
)
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -240,11 +255,44 @@ fun PromptDialog(
)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConfirmDialog(state: DialogHostState = LocalDialogHost.current) {
val confirmDialogData = state.currentDialogData.tryInto<ConfirmDialogData>() ?: 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()
Expand All @@ -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())
})
}
101 changes: 77 additions & 24 deletions manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 = {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ class ModuleViewModel : ViewModel() {
}
}

fun checkUpdate(m: ModuleInfo): Pair<String, String> {
val empty = "" to ""
fun checkUpdate(m: ModuleInfo): Triple<String, String, String> {
val empty = Triple("", "", "")
if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) {
return empty
}
Expand Down Expand Up @@ -155,6 +155,6 @@ class ModuleViewModel : ViewModel() {
return empty
}

return zipUrl to version
return Triple(zipUrl, version, changelog)
}
}
1 change: 1 addition & 0 deletions manager/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,5 @@
<string name="force_stop_app">Force Stop</string>
<string name="restart_app">Restart</string>
<string name="failed_to_update_sepolicy">Failed to update SELinux rules for: %s</string>
<string name="module_changelog">Changelog</string>
</resources>
3 changes: 3 additions & 0 deletions manager/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -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" }

0 comments on commit 81bbb31

Please sign in to comment.