feat: Improve Updates settings screen

UI:

- Use prereleases toggle now triggers an immediate refetch when changed
- Card now shows manager version and release date below

Backend:

- Move getting app updates into ManagerUpdateRepository with caching
- Store fetched data inside ManagerUpdateRepository regardless of update status
This commit is contained in:
PalmDevs
2026-03-24 03:00:24 +07:00
parent 43d975ea9d
commit 4ce823c8c0
8 changed files with 89 additions and 42 deletions

View File

@@ -1,22 +1,43 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.BuildConfig
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.utils.getOrThrow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.datetime.LocalDateTime
class ManagerUpdateRepository(
private val reVancedAPI: ReVancedAPI
) {
private val _availableVersion = MutableStateFlow<String?>(null)
val availableVersion = _availableVersion.asStateFlow()
private val asset: ReVancedAsset? = null
private val _releasedAt = MutableStateFlow<LocalDateTime?>(null)
private val _version = MutableStateFlow<String?>(null)
private val _hasUpdate = MutableStateFlow(false)
suspend fun refreshAvailableVersion(): String? {
val version = reVancedAPI.getAppUpdate()?.version
_availableVersion.value = version
return version
val releasedAt = _releasedAt.asStateFlow()
val hasUpdate = _hasUpdate.asStateFlow()
val version = _version.asStateFlow()
suspend fun refresh(): ReVancedAsset {
val update = reVancedAPI.getLatestAppInfo().getOrThrow()
_releasedAt.value = update.createdAt
_version.value = update.version
_hasUpdate.value = update.version.removePrefix("v") != BuildConfig.VERSION_NAME
return update
}
fun clearAvailableVersion() {
_availableVersion.value = null
suspend fun getUpdateOrNull(refetch: Boolean = false): ReVancedAsset? {
val asset = if (refetch || asset == null) refresh() else null
return asset.takeIf { _hasUpdate.value }
}
fun clearState() {
_releasedAt.value = null
_version.value = null
_hasUpdate.value = false
}
}

View File

@@ -43,9 +43,6 @@ class ReVancedAPI(
suspend fun getAnnouncements() = request<List<ReVancedAnnouncement>>("announcements")
suspend fun getAppUpdate() =
getLatestAppInfo().getOrThrow().takeIf { it.version.removePrefix("v") != BuildConfig.VERSION_NAME }
suspend fun getLatestAppInfo() =
request<ReVancedAsset>("manager${prefs.useManagerPrereleases.prereleaseString()}")

View File

@@ -47,7 +47,6 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
@@ -141,7 +140,8 @@ fun DashboardScreen(
!suggestedVersionSafeguard
}
}
val availableUpdate by vm.availableManagerUpdate.collectAsStateWithLifecycle()
val hasUpdate by vm.hasUpdate.collectAsStateWithLifecycle()
val updateVersion by vm.updateVersion.collectAsStateWithLifecycle()
val androidContext = LocalContext.current
val resources = LocalResources.current
val logoPainter = rememberDrawablePainter(drawable = remember(resources) {
@@ -207,12 +207,12 @@ fun DashboardScreen(
}
var showUpdateDialog by rememberSaveable { mutableStateOf(true) }
if (managerAutoUpdates && showUpdateDialog && showManagerUpdateDialogOnLaunch && availableUpdate != null) {
if (managerAutoUpdates && showUpdateDialog && showManagerUpdateDialogOnLaunch && hasUpdate) {
AvailableUpdateDialog(
onDismiss = { showUpdateDialog = false },
setShowManagerUpdateDialogOnLaunch = vm::setShowManagerUpdateDialogOnLaunch,
onConfirm = onUpdateClick,
newVersion = availableUpdate!!
newVersion = updateVersion!!
)
}
@@ -337,7 +337,7 @@ fun DashboardScreen(
}
},
actions = {
if (availableUpdate != null) {
if (updateVersion != null) {
TooltipIconButton(
onClick = onUpdateClick,
tooltip = stringResource(R.string.update),

View File

@@ -3,6 +3,7 @@ package app.revanced.manager.ui.screen.settings.update
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -18,7 +19,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Update
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.WorkOutline
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -55,7 +55,7 @@ import app.revanced.manager.ui.component.TooltipIconButton
import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.SafeguardBooleanItem
import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel
import app.revanced.manager.util.toast
import app.revanced.manager.util.relativeTime
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
@@ -69,10 +69,11 @@ fun UpdatesSettingsScreen(
vm: UpdatesSettingsViewModel = koinViewModel(),
) {
val context = LocalContext.current
val resources = LocalResources.current
val coroutineScope = rememberCoroutineScope()
var checkingForUpdate by remember { mutableStateOf(false) }
val availableUpdate by vm.availableManagerUpdate.collectAsStateWithLifecycle()
val managerVersion by vm.managerVersion.collectAsStateWithLifecycle()
val hasUpdate by vm.hasUpdate.collectAsStateWithLifecycle()
val updateReleasedAt by vm.updateReleasedAt.collectAsStateWithLifecycle()
val scrollState = androidx.compose.foundation.rememberScrollState()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
canScroll = {
@@ -112,15 +113,14 @@ fun UpdatesSettingsScreen(
enabled = !checkingForUpdate,
onClick = {
coroutineScope.launch {
if (availableUpdate != null) {
if (hasUpdate) {
onUpdateClick()
return@launch
}
checkingForUpdate = true
try {
val version = vm.checkForUpdates()
if (!version.isNullOrEmpty()) onUpdateClick()
if (vm.checkUpdates()) onUpdateClick()
} finally {
checkingForUpdate = false
}
@@ -144,7 +144,7 @@ fun UpdatesSettingsScreen(
text = stringResource(
when {
checkingForUpdate -> R.string.update_check
availableUpdate != null -> R.string.view_update
hasUpdate -> R.string.view_update
else -> R.string.manual_update_check
}
)
@@ -164,7 +164,13 @@ fun UpdatesSettingsScreen(
) {
ListSection(
title = stringResource(R.string.manager),
leadingContent = { Icon(Icons.Outlined.WorkOutline, contentDescription = null, modifier = Modifier.size(18.dp)) }
leadingContent = {
Icon(
Icons.Outlined.WorkOutline,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
}
) {
Surface(
shape = RoundedCornerShape(4.dp),
@@ -190,11 +196,27 @@ fun UpdatesSettingsScreen(
.padding(start = 4.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
managerVersion?.let { version ->
updateReleasedAt?.let {
val releasedAt = it.relativeTime(LocalContext.current)
Text(
text = "$version\u2002\u2022\u2002$releasedAt",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
Spacer(modifier = Modifier.height(4.dp))
Button(
@@ -242,6 +264,7 @@ fun UpdatesSettingsScreen(
coroutineScope.launch {
vm.useManagerPrereleases.update(value)
vm.clearAvailableManagerUpdate()
vm.checkUpdates(false)
}
}
)

View File

@@ -40,7 +40,8 @@ class DashboardViewModel(
val bundleDownloadError = patchBundleRepository.apiOutageError
private val contentResolver: ContentResolver = app.contentResolver
val availableManagerUpdate = managerUpdateRepository.availableVersion
val hasUpdate = managerUpdateRepository.hasUpdate
val updateVersion = managerUpdateRepository.version
val sourcesNotDownloaded = patchBundleRepository.bundleInfoFlow.map { it.isEmpty() }
val sourceUpdatesAvailable = combine(
@@ -76,7 +77,7 @@ class DashboardViewModel(
if (!prefs.managerAutoUpdates.get()) return
uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") {
managerUpdateRepository.refreshAvailableVersion()
managerUpdateRepository.refresh()
}
}

View File

@@ -19,6 +19,7 @@ import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.domain.repository.ChangelogSource
import app.revanced.manager.domain.repository.ChangelogsRepository
import app.revanced.manager.domain.repository.ManagerUpdateRepository
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.dto.ReVancedAssetHistory
@@ -47,10 +48,10 @@ class UpdateViewModel(
private val downloadOnScreenEntry: Boolean
) : ViewModel(), KoinComponent {
private val app: Application by inject()
private val reVancedAPI: ReVancedAPI by inject()
private val http: HttpService by inject()
private val networkInfo: NetworkInfo by inject()
private val fs: Filesystem by inject()
private val managerUpdateRepository: ManagerUpdateRepository = get()
private val ackpineInstaller: PackageInstaller = get()
// TODO: save state to handle process death.
@@ -85,7 +86,7 @@ class UpdateViewModel(
init {
viewModelScope.launch {
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
releaseInfo = reVancedAPI.getAppUpdate()
releaseInfo = managerUpdateRepository.getUpdateOrNull()
?: throw Exception("No update available")
if (downloadOnScreenEntry) {

View File

@@ -18,22 +18,25 @@ class UpdatesSettingsViewModel(
val managerAutoUpdates = prefs.managerAutoUpdates
val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch
val useManagerPrereleases = prefs.useManagerPrereleases
val availableManagerUpdate = managerUpdateRepository.availableVersion
val managerVersion = managerUpdateRepository.version
val updateReleasedAt = managerUpdateRepository.releasedAt
val hasUpdate = managerUpdateRepository.hasUpdate
suspend fun checkForUpdates(): String? {
var availableVersion: String? = null
suspend fun checkUpdates(showToast: Boolean = true): Boolean {
var hasUpdate = false
uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") {
availableVersion = managerUpdateRepository.refreshAvailableVersion()
if (availableVersion == null)
if (managerUpdateRepository.getUpdateOrNull(true) != null) {
hasUpdate = true
} else if (showToast) {
app.toast(app.getString(R.string.no_update_available))
}
}
return availableVersion
return hasUpdate
}
fun clearAvailableManagerUpdate() {
managerUpdateRepository.clearAvailableVersion()
managerUpdateRepository.clearState()
}
}

View File

@@ -491,6 +491,7 @@ Its only compatible with these versions: %2$s</string>
<string name="prerelease_title">Use pre-releases?</string>
<string name="prereleases_warning">Pre-release versions may be unstable and contain bugs. You may experience crashes, data loss, or other unexpected issues.</string>
<string name="changelog">View changelog</string>
<string name="updated_ago">Updated %s</string>
<string name="changelog_loading">Loading changelog…</string>
<string name="changelog_download_fail">Couldnt download changelog: %s</string>
<string name="battery_optimization_notification">Battery optimizations must be turned off in order for ReVanced Manager to work correctly in the background</string>