fix(update): bind live download state to dialog and show real-time progress

This commit is contained in:
M57 committed 2026-09-06 20:59:39 +03:00
1 parent fb509c3cae
commit 850d01ca89
5 files changed
+167 -34

No files matched your search

@@ -600,6 +600,23 @@ fun ChatScreen(
?: error("Application not available")
AppUpdateViewModel(app)
}
val appUpdateState by appUpdateViewModel.state.collectAsStateWithLifecycle()
val updateLifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
androidx.compose.runtime.DisposableEffect(updateLifecycleOwner) {
val observer =
androidx.lifecycle.LifecycleEventObserver { _, event ->
if (event == androidx.lifecycle.Lifecycle.Event.ON_RESUME) {
if (appUpdateState is AppUpdateState.NeedsUnknownSourcesPermission) {
appUpdateViewModel.resumeInstallAfterPermission()
}
}
}
updateLifecycleOwner.lifecycle.addObserver(observer)
onDispose {
updateLifecycleOwner.lifecycle.removeObserver(observer)
}
}
if (UpdateNoticeManager.enabled && !AppUpdateCache.dismissed) {
val noticeTag =
@@ -616,8 +633,14 @@ fun ChatScreen(
if (AppUpdateCache.isDialogVisible) {
val context = androidx.compose.ui.platform.LocalContext.current
val dialogState =
if (appUpdateState !is AppUpdateState.Idle) {
appUpdateState
} else {
updateNotice
}
AppUpdateDialog(
state = updateNotice,
state = dialogState,
onDismiss = { AppUpdateCache.hideDialog() },
onStartUpdate = { appUpdateViewModel.startUpdate() },
onCancelDownload = { appUpdateViewModel.cancelDownload() },
@@ -29,6 +29,10 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -49,14 +53,19 @@ fun AppUpdateDialog(
onNeverAskAgain: () -> Unit,
onOpenSettings: () -> Unit,
) {
val tag = state.releaseTag().orEmpty()
val available = state as? AppUpdateState.UpdateAvailable
var cachedAvailable by remember { mutableStateOf(available) }
if (available != null) {
cachedAvailable = available
}
val currentAvailable = available ?: cachedAvailable
val tag = state.releaseTag() ?: currentAvailable?.latestTag.orEmpty()
val sizeMb =
available?.let {
currentAvailable?.let {
val mb = it.sizeBytes / (1024 * 1024.0)
"%.1f MB".format(mb)
}
val releaseNotes = available?.releaseNotes?.trim().orEmpty()
val releaseNotes = currentAvailable?.releaseNotes?.trim().orEmpty()
Dialog(onDismissRequest = onDismiss) {
Surface(
@@ -156,6 +165,7 @@ fun AppUpdateDialog(
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
val progressPercent = (state.progress * 100).toInt()
Text(
@@ -167,16 +177,35 @@ fun AppUpdateDialog(
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (currentAvailable != null && currentAvailable.sizeBytes > 0) {
val downloadedMb = (state.progress * currentAvailable.sizeBytes) / (1024 * 1024.0)
val totalMb = currentAvailable.sizeBytes / (1024 * 1024.0)
Text(
text = "%.1f / %.1f MB".format(downloadedMb, totalMb),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
Spacer(modifier = Modifier.height(8.dp))
if (state.progress > 0f) {
LinearProgressIndicator(
progress = { state.progress },
modifier =
Modifier
.fillMaxWidth()
.height(8.dp)
.clip(RoundedCornerShape(4.dp)),
)
} else {
LinearProgressIndicator(
modifier =
Modifier
.fillMaxWidth()
.height(8.dp)
.clip(RoundedCornerShape(4.dp)),
)
}
Spacer(modifier = Modifier.height(6.dp))
LinearProgressIndicator(
progress = { state.progress },
modifier =
Modifier
.fillMaxWidth()
.height(8.dp)
.clip(RoundedCornerShape(4.dp)),
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedButton(
onClick = onCancelDownload,
@@ -188,11 +217,21 @@ fun AppUpdateDialog(
}
is AppUpdateState.Installing -> {
Text(
text = stringResource(R.string.settings_about_update_installing, tag),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
)
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = stringResource(R.string.settings_about_update_installing, tag),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.height(8.dp))
LinearProgressIndicator(
modifier =
Modifier
.fillMaxWidth()
.height(8.dp)
.clip(RoundedCornerShape(4.dp)),
)
}
}
is AppUpdateState.NeedsUnknownSourcesPermission -> {
@@ -43,10 +43,14 @@ class AppUpdateViewModel(
val state: StateFlow<AppUpdateState> = _state.asStateFlow()
private var downloadJob: kotlinx.coroutines.Job? = null
private var lastAvailable: AppUpdateState.UpdateAvailable? = null
init {
val cached = AppUpdateCache.state.value
if (cached is AppUpdateState.UpdateAvailable || cached is AppUpdateState.UpToDate) {
if (cached is AppUpdateState.UpdateAvailable) {
lastAvailable = cached
_state.value = cached
} else if (cached is AppUpdateState.UpToDate) {
_state.value = cached
} else {
checkForUpdate()
@@ -83,12 +87,13 @@ class AppUpdateViewModel(
}
_state.value =
if (isNewerVersion(info.tagName, currentVersion)) {
AppUpdateState.UpdateAvailable(
latestTag = info.tagName,
apkUrl = apk.browserDownloadUrl,
sizeBytes = apk.size,
releaseNotes = info.body,
)
AppUpdateState
.UpdateAvailable(
latestTag = info.tagName,
apkUrl = apk.browserDownloadUrl,
sizeBytes = apk.size,
releaseNotes = info.body,
).also { lastAvailable = it }
} else {
AppUpdateState.UpToDate(latestTag = info.tagName)
}
@@ -100,7 +105,12 @@ class AppUpdateViewModel(
/** Download the release APK and launch the system installer. */
fun startUpdate() {
val available = _state.value as? AppUpdateState.UpdateAvailable ?: return
val available =
(_state.value as? AppUpdateState.UpdateAvailable)
?: lastAvailable
?: (AppUpdateCache.state.value as? AppUpdateState.UpdateAvailable)
?: return
lastAvailable = available
if (!canRequestInstalls()) {
_state.value = AppUpdateState.NeedsUnknownSourcesPermission
return
@@ -129,7 +139,9 @@ class AppUpdateViewModel(
fun cancelDownload() {
downloadJob?.cancel()
downloadJob = null
val available = AppUpdateCache.state.value as? AppUpdateState.UpdateAvailable
val available =
lastAvailable
?: (AppUpdateCache.state.value as? AppUpdateState.UpdateAvailable)
if (available != null) {
_state.value = available
} else {
@@ -139,7 +151,10 @@ class AppUpdateViewModel(
/** Dismiss the current update tag so it stops nagging. */
fun dismissCurrentUpdate() {
val tag = _state.value.releaseTag()
val tag =
_state.value.releaseTag()
?: lastAvailable?.latestTag
?: AppUpdateCache.state.value.releaseTag()
if (tag != null) {
AuthManager.setDismissedUpdateTag(tag)
}
@@ -152,8 +167,9 @@ class AppUpdateViewModel(
if (canRequestInstalls()) {
val dest = File(getApplication<Application>().cacheDir, APK_FILE_NAME)
val available =
_state.value as? AppUpdateState.UpdateAvailable
?: AppUpdateCache.state.value as? AppUpdateState.UpdateAvailable
(_state.value as? AppUpdateState.UpdateAvailable)
?: lastAvailable
?: (AppUpdateCache.state.value as? AppUpdateState.UpdateAvailable)
if (dest.exists() && dest.length() > 0 && available != null) {
_state.value = AppUpdateState.Installing(available.latestTag)
launchInstaller(dest)
@@ -178,10 +178,16 @@ private fun UpdateRow(
}
is AppUpdateState.Downloading -> {
LinearProgressIndicator(
progress = { state.progress },
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
)
if (state.progress > 0f) {
LinearProgressIndicator(
progress = { state.progress },
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
)
} else {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
)
}
}
is AppUpdateState.NeedsUnknownSourcesPermission -> {