Merge pull request #1077 from Hy4ri/fix/update-dialog-download-indicator

fix(update): bind live download state to dialog and display real-time progress
This commit is contained in:
M57
2026-09-06 21:42:31 +03:00
committed by GitHub
4 changed files with 118 additions and 34 deletions
@@ -600,6 +600,23 @@ fun ChatScreen(
?: error("Application not available") ?: error("Application not available")
AppUpdateViewModel(app) 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) { if (UpdateNoticeManager.enabled && !AppUpdateCache.dismissed) {
val noticeTag = val noticeTag =
@@ -616,8 +633,14 @@ fun ChatScreen(
if (AppUpdateCache.isDialogVisible) { if (AppUpdateCache.isDialogVisible) {
val context = androidx.compose.ui.platform.LocalContext.current val context = androidx.compose.ui.platform.LocalContext.current
val dialogState =
if (appUpdateState !is AppUpdateState.Idle) {
appUpdateState
} else {
updateNotice
}
AppUpdateDialog( AppUpdateDialog(
state = updateNotice, state = dialogState,
onDismiss = { AppUpdateCache.hideDialog() }, onDismiss = { AppUpdateCache.hideDialog() },
onStartUpdate = { appUpdateViewModel.startUpdate() }, onStartUpdate = { appUpdateViewModel.startUpdate() },
onCancelDownload = { appUpdateViewModel.cancelDownload() }, onCancelDownload = { appUpdateViewModel.cancelDownload() },
@@ -29,6 +29,10 @@ 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.Composable 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -49,14 +53,19 @@ fun AppUpdateDialog(
onNeverAskAgain: () -> Unit, onNeverAskAgain: () -> Unit,
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
) { ) {
val tag = state.releaseTag().orEmpty()
val available = state as? AppUpdateState.UpdateAvailable 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 = val sizeMb =
available?.let { currentAvailable?.let {
val mb = it.sizeBytes / (1024 * 1024.0) val mb = it.sizeBytes / (1024 * 1024.0)
"%.1f MB".format(mb) "%.1f MB".format(mb)
} }
val releaseNotes = available?.releaseNotes?.trim().orEmpty() val releaseNotes = currentAvailable?.releaseNotes?.trim().orEmpty()
Dialog(onDismissRequest = onDismiss) { Dialog(onDismissRequest = onDismiss) {
Surface( Surface(
@@ -156,6 +165,7 @@ fun AppUpdateDialog(
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) { ) {
val progressPercent = (state.progress * 100).toInt() val progressPercent = (state.progress * 100).toInt()
Text( Text(
@@ -167,16 +177,35 @@ fun AppUpdateDialog(
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, 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)) Spacer(modifier = Modifier.height(12.dp))
OutlinedButton( OutlinedButton(
onClick = onCancelDownload, onClick = onCancelDownload,
@@ -188,11 +217,21 @@ fun AppUpdateDialog(
} }
is AppUpdateState.Installing -> { is AppUpdateState.Installing -> {
Text( Column(modifier = Modifier.fillMaxWidth()) {
text = stringResource(R.string.settings_about_update_installing, tag), Text(
style = MaterialTheme.typography.bodyMedium, text = stringResource(R.string.settings_about_update_installing, tag),
color = MaterialTheme.colorScheme.primary, 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 -> { is AppUpdateState.NeedsUnknownSourcesPermission -> {
@@ -43,10 +43,14 @@ class AppUpdateViewModel(
val state: StateFlow<AppUpdateState> = _state.asStateFlow() val state: StateFlow<AppUpdateState> = _state.asStateFlow()
private var downloadJob: kotlinx.coroutines.Job? = null private var downloadJob: kotlinx.coroutines.Job? = null
private var lastAvailable: AppUpdateState.UpdateAvailable? = null
init { init {
val cached = AppUpdateCache.state.value 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 _state.value = cached
} else { } else {
checkForUpdate() checkForUpdate()
@@ -83,12 +87,13 @@ class AppUpdateViewModel(
} }
_state.value = _state.value =
if (isNewerVersion(info.tagName, currentVersion)) { if (isNewerVersion(info.tagName, currentVersion)) {
AppUpdateState.UpdateAvailable( AppUpdateState
latestTag = info.tagName, .UpdateAvailable(
apkUrl = apk.browserDownloadUrl, latestTag = info.tagName,
sizeBytes = apk.size, apkUrl = apk.browserDownloadUrl,
releaseNotes = info.body, sizeBytes = apk.size,
) releaseNotes = info.body,
).also { lastAvailable = it }
} else { } else {
AppUpdateState.UpToDate(latestTag = info.tagName) AppUpdateState.UpToDate(latestTag = info.tagName)
} }
@@ -100,7 +105,12 @@ class AppUpdateViewModel(
/** Download the release APK and launch the system installer. */ /** Download the release APK and launch the system installer. */
fun startUpdate() { 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()) { if (!canRequestInstalls()) {
_state.value = AppUpdateState.NeedsUnknownSourcesPermission _state.value = AppUpdateState.NeedsUnknownSourcesPermission
return return
@@ -129,7 +139,9 @@ class AppUpdateViewModel(
fun cancelDownload() { fun cancelDownload() {
downloadJob?.cancel() downloadJob?.cancel()
downloadJob = null downloadJob = null
val available = AppUpdateCache.state.value as? AppUpdateState.UpdateAvailable val available =
lastAvailable
?: (AppUpdateCache.state.value as? AppUpdateState.UpdateAvailable)
if (available != null) { if (available != null) {
_state.value = available _state.value = available
} else { } else {
@@ -139,7 +151,10 @@ class AppUpdateViewModel(
/** Dismiss the current update tag so it stops nagging. */ /** Dismiss the current update tag so it stops nagging. */
fun dismissCurrentUpdate() { fun dismissCurrentUpdate() {
val tag = _state.value.releaseTag() val tag =
_state.value.releaseTag()
?: lastAvailable?.latestTag
?: AppUpdateCache.state.value.releaseTag()
if (tag != null) { if (tag != null) {
AuthManager.setDismissedUpdateTag(tag) AuthManager.setDismissedUpdateTag(tag)
} }
@@ -152,8 +167,9 @@ class AppUpdateViewModel(
if (canRequestInstalls()) { if (canRequestInstalls()) {
val dest = File(getApplication<Application>().cacheDir, APK_FILE_NAME) val dest = File(getApplication<Application>().cacheDir, APK_FILE_NAME)
val available = val available =
_state.value as? AppUpdateState.UpdateAvailable (_state.value as? AppUpdateState.UpdateAvailable)
?: AppUpdateCache.state.value as? AppUpdateState.UpdateAvailable ?: lastAvailable
?: (AppUpdateCache.state.value as? AppUpdateState.UpdateAvailable)
if (dest.exists() && dest.length() > 0 && available != null) { if (dest.exists() && dest.length() > 0 && available != null) {
_state.value = AppUpdateState.Installing(available.latestTag) _state.value = AppUpdateState.Installing(available.latestTag)
launchInstaller(dest) launchInstaller(dest)
@@ -178,10 +178,16 @@ private fun UpdateRow(
} }
is AppUpdateState.Downloading -> { is AppUpdateState.Downloading -> {
LinearProgressIndicator( if (state.progress > 0f) {
progress = { state.progress }, LinearProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(top = 8.dp), progress = { state.progress },
) modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
)
} else {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
)
}
} }
is AppUpdateState.NeedsUnknownSourcesPermission -> { is AppUpdateState.NeedsUnknownSourcesPermission -> {