|
@@ -0,0 +1,275 @@
|
|
|
+package com.hzliuzhi.applet.browser.ui
|
|
|
+
|
|
|
+import android.annotation.SuppressLint
|
|
|
+import android.util.Log
|
|
|
+import android.widget.Toast
|
|
|
+import androidx.compose.foundation.background
|
|
|
+import androidx.compose.foundation.layout.Arrangement
|
|
|
+import androidx.compose.foundation.layout.Box
|
|
|
+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.lazy.LazyColumn
|
|
|
+import androidx.compose.foundation.lazy.rememberLazyListState
|
|
|
+import androidx.compose.foundation.selection.selectable
|
|
|
+import androidx.compose.material3.Button
|
|
|
+import androidx.compose.material3.CircularProgressIndicator
|
|
|
+import androidx.compose.material3.MaterialTheme
|
|
|
+import androidx.compose.material3.OutlinedButton
|
|
|
+import androidx.compose.material3.OutlinedTextField
|
|
|
+import androidx.compose.material3.RadioButton
|
|
|
+import androidx.compose.material3.RadioButtonDefaults
|
|
|
+import androidx.compose.material3.Text
|
|
|
+import androidx.compose.runtime.Composable
|
|
|
+import androidx.compose.runtime.LaunchedEffect
|
|
|
+import androidx.compose.runtime.collectAsState
|
|
|
+import androidx.compose.runtime.getValue
|
|
|
+import androidx.compose.runtime.mutableStateOf
|
|
|
+import androidx.compose.runtime.remember
|
|
|
+import androidx.compose.runtime.rememberCoroutineScope
|
|
|
+import androidx.compose.runtime.setValue
|
|
|
+import androidx.compose.ui.Alignment
|
|
|
+import androidx.compose.ui.Modifier
|
|
|
+import androidx.compose.ui.graphics.Color
|
|
|
+import androidx.compose.ui.platform.LocalContext
|
|
|
+import androidx.compose.ui.text.font.FontWeight
|
|
|
+import androidx.compose.ui.unit.dp
|
|
|
+import androidx.compose.ui.unit.sp
|
|
|
+import com.hzliuzhi.applet.browser.update.UpdatePackage
|
|
|
+import com.hzliuzhi.applet.browser.update.UpdateUtil
|
|
|
+import com.hzliuzhi.applet.browser.update.WebViewUpdate
|
|
|
+import com.hzliuzhi.applet.browser.update.rememberWebViewUpdateController
|
|
|
+import kotlinx.coroutines.Dispatchers
|
|
|
+import kotlinx.coroutines.launch
|
|
|
+import kotlinx.coroutines.withContext
|
|
|
+
|
|
|
+@Composable
|
|
|
+fun UpdateScreen(
|
|
|
+ modifier: Modifier = Modifier,
|
|
|
+) {
|
|
|
+ val context = LocalContext.current
|
|
|
+ val controller = rememberWebViewUpdateController()
|
|
|
+
|
|
|
+ val system by remember { mutableStateOf(controller.system) }
|
|
|
+ var current by remember { mutableStateOf<UpdatePackage?>(null) }
|
|
|
+ var throwable by remember { mutableStateOf<Throwable?>(null) }
|
|
|
+
|
|
|
+ var packages by remember { mutableStateOf<List<UpdatePackage>>(emptyList()) }
|
|
|
+ var selected by remember { mutableStateOf<UpdatePackage?>(null) }
|
|
|
+
|
|
|
+ val updating by controller.updating.collectAsState()
|
|
|
+ val process by controller.process.collectAsState()
|
|
|
+
|
|
|
+ LaunchedEffect(Unit) {
|
|
|
+ packages = withContext(Dispatchers.IO) {
|
|
|
+ controller.packages.toList()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ fun onTest() {
|
|
|
+ current = UpdateUtil.getSystemPackage().takeIf { it != system }?.also {
|
|
|
+ Toast.makeText(context, "更新成功 ${it.packageName}", Toast.LENGTH_SHORT).show()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ val scope = rememberCoroutineScope()
|
|
|
+ fun onUpdate() {
|
|
|
+ packages.firstOrNull { it == selected }?.also {
|
|
|
+ throwable = null
|
|
|
+ scope.launch {
|
|
|
+ withContext(Dispatchers.IO) {
|
|
|
+ controller.update(it) { error ->
|
|
|
+ // 回到主线程处理回调
|
|
|
+ if (error == null) onTest() else throwable = error
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } ?: Toast.makeText(context, "请选择要更新的包", Toast.LENGTH_SHORT).show()
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ Column(
|
|
|
+ modifier = modifier.padding(16.dp),
|
|
|
+ verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
|
+ ) {
|
|
|
+ PackageDescription("系统 WebView", system)
|
|
|
+ UpdatePackages(
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth()
|
|
|
+ .weight(1f),
|
|
|
+ packages = packages,
|
|
|
+ selected = selected,
|
|
|
+ onSelected = { selected = it }
|
|
|
+ )
|
|
|
+
|
|
|
+ if (throwable != null) OutlinedTextField(
|
|
|
+ value = Log.getStackTraceString(throwable),
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth()
|
|
|
+ .weight(0.4f),
|
|
|
+ label = { Text(throwable!!.message.toString()) },
|
|
|
+ onValueChange = {},
|
|
|
+ readOnly = true,
|
|
|
+ )
|
|
|
+
|
|
|
+ PackageDescription("应用 WebView", current)
|
|
|
+
|
|
|
+ Row(
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth()
|
|
|
+ .padding(horizontal = 16.dp),
|
|
|
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
|
+ verticalAlignment = Alignment.Bottom
|
|
|
+ ) {
|
|
|
+ Button(
|
|
|
+ modifier = Modifier
|
|
|
+ .weight(1f)
|
|
|
+ .height(48.dp),
|
|
|
+ enabled = true,
|
|
|
+ onClick = { if (!updating) onUpdate() }
|
|
|
+ ) {
|
|
|
+ Row(
|
|
|
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
|
+ verticalAlignment = Alignment.CenterVertically
|
|
|
+ ) {
|
|
|
+ if (updating) {
|
|
|
+ CircularProgressIndicator(
|
|
|
+ modifier = Modifier.size(24.dp),
|
|
|
+ color = MaterialTheme.colorScheme.onPrimary,
|
|
|
+ strokeWidth = 2.dp
|
|
|
+ )
|
|
|
+ }
|
|
|
+ Text(
|
|
|
+ text = if (updating) process.format() else "加载",
|
|
|
+ style = MaterialTheme.typography.labelLarge.copy(
|
|
|
+ fontSize = 16.sp
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ OutlinedButton(
|
|
|
+ modifier = Modifier
|
|
|
+ .weight(1f)
|
|
|
+ .height(48.dp),
|
|
|
+ enabled = !updating,
|
|
|
+ onClick = { onTest() }
|
|
|
+ ) {
|
|
|
+ Text(
|
|
|
+ text = "测试",
|
|
|
+ style = MaterialTheme.typography.labelLarge.copy(
|
|
|
+ fontSize = 16.sp
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@SuppressLint("DefaultLocale")
|
|
|
+private fun Float.format(): String {
|
|
|
+ return String.format("%05.2f%%", this * 100)
|
|
|
+}
|
|
|
+
|
|
|
+@Composable
|
|
|
+fun PackageDescription(label: String, info: UpdatePackage?) {
|
|
|
+ if (info != null) OutlinedTextField(
|
|
|
+ value = info.toString(),
|
|
|
+ modifier = Modifier.fillMaxWidth(),
|
|
|
+ label = { Text(label) },
|
|
|
+ onValueChange = {},
|
|
|
+ readOnly = true,
|
|
|
+ enabled = info.packageName.isNotEmpty() && info.packageName != "unknown",
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+@Composable
|
|
|
+fun UpdatePackages(
|
|
|
+ packages: List<UpdatePackage>,
|
|
|
+ selected: UpdatePackage?,
|
|
|
+ modifier: Modifier = Modifier,
|
|
|
+ onSelected: (UpdatePackage) -> Unit = {},
|
|
|
+) {
|
|
|
+ val grouped = packages.groupBy { it.type.label }
|
|
|
+ val listState = rememberLazyListState()
|
|
|
+
|
|
|
+ // 默认滚动到选中项
|
|
|
+ LaunchedEffect(selected) {
|
|
|
+ val index = packages.indexOfFirst { it == selected }
|
|
|
+ if (index != -1) {
|
|
|
+ listState.animateScrollToItem(index)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Box(modifier = modifier) {
|
|
|
+ LazyColumn(state = listState) {
|
|
|
+ grouped.forEach { (group, packages) ->
|
|
|
+ val isGroupSelected = group == selected?.type?.label
|
|
|
+ stickyHeader {
|
|
|
+ Box(
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth()
|
|
|
+ .background(
|
|
|
+ if (isGroupSelected) MaterialTheme.colorScheme.primaryContainer
|
|
|
+ else MaterialTheme.colorScheme.surface
|
|
|
+ )
|
|
|
+ .padding(vertical = 8.dp, horizontal = 16.dp)
|
|
|
+ ) {
|
|
|
+ Text(
|
|
|
+ text = group,
|
|
|
+ style = MaterialTheme.typography.titleMedium,
|
|
|
+ color = if (isGroupSelected) MaterialTheme.colorScheme.onPrimaryContainer
|
|
|
+ else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
|
|
+ fontWeight = if (isGroupSelected) FontWeight.Bold else FontWeight.Normal
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ items(packages.size) { index ->
|
|
|
+ val item = packages[index]
|
|
|
+ val isSelected = item == selected
|
|
|
+
|
|
|
+ Row(
|
|
|
+ modifier = modifier
|
|
|
+ .background(
|
|
|
+ if (isSelected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.1f)
|
|
|
+ else Color.Transparent
|
|
|
+ )
|
|
|
+ .padding(horizontal = 16.dp, vertical = 8.dp)
|
|
|
+ .selectable(isSelected, onClick = { onSelected(item) }),
|
|
|
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
|
+ verticalAlignment = Alignment.CenterVertically,
|
|
|
+ ) {
|
|
|
+ RadioButton(
|
|
|
+ selected = isSelected,
|
|
|
+ onClick = { onSelected(item) },
|
|
|
+ colors = RadioButtonDefaults.colors(
|
|
|
+ selectedColor = MaterialTheme.colorScheme.primary,
|
|
|
+ unselectedColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
|
|
+ )
|
|
|
+ )
|
|
|
+ Column(modifier = Modifier.weight(1f)) {
|
|
|
+ Text(
|
|
|
+ text = item.packageName,
|
|
|
+ style = MaterialTheme.typography.bodyLarge,
|
|
|
+ color = if (isSelected) MaterialTheme.colorScheme.primary
|
|
|
+ else MaterialTheme.colorScheme.onSurface
|
|
|
+ )
|
|
|
+ Spacer(modifier = Modifier.height(2.dp))
|
|
|
+ item.versionName?.let {
|
|
|
+ Text(
|
|
|
+ text = it,
|
|
|
+ style = MaterialTheme.typography.bodyMedium,
|
|
|
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|