feat: Improve update screen design (#2487)

This commit is contained in:
Ushie 2025-04-23 20:09:05 +03:00 committed by GitHub
parent d5c63ead26
commit fc05f95837
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 118 additions and 139 deletions

View File

@ -81,3 +81,41 @@ fun AppTopBar(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppTopBar(
title: @Composable () -> Unit,
onBackClick: (() -> Unit)? = null,
backIcon: @Composable (() -> Unit) = @Composable {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(
R.string.back
)
)
},
actions: @Composable (RowScope.() -> Unit) = {},
scrollBehavior: TopAppBarScrollBehavior? = null,
applyContainerColor: Boolean = false
) {
val containerColor = if (applyContainerColor) {
MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
} else {
Color.Unspecified
}
TopAppBar(
title = title,
scrollBehavior = scrollBehavior,
navigationIcon = {
if (onBackClick != null) {
IconButton(onClick = onBackClick) {
backIcon()
}
}
},
actions = actions,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = containerColor
)
)
}

View File

@ -7,10 +7,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.CalendarToday import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.outlined.Campaign
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.Sell
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -37,28 +34,18 @@ fun Changelog(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.Campaign, imageVector = Icons.Outlined.NewReleases,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier modifier = Modifier
.size(32.dp) .size(32.dp)
) )
Text( Text(
version.removePrefix("v"), "${version.removePrefix("v")} ($publishDate)",
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)), style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)),
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
) )
} }
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxWidth()
) {
Tag(
Icons.Outlined.CalendarToday,
publishDate
)
}
} }
Markdown( Markdown(
markdown, markdown,

View File

@ -5,19 +5,17 @@ import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.InstallMobile
import androidx.compose.material.icons.outlined.Update import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -28,16 +26,15 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.BuildConfig
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.network.dto.ReVancedAsset import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
import app.revanced.manager.ui.component.settings.Changelog import app.revanced.manager.ui.component.settings.Changelog
import app.revanced.manager.ui.viewmodel.UpdateViewModel import app.revanced.manager.ui.viewmodel.UpdateViewModel
import app.revanced.manager.ui.viewmodel.UpdateViewModel.State import app.revanced.manager.ui.viewmodel.UpdateViewModel.State
@ -60,37 +57,81 @@ fun UpdateScreen(
Scaffold( Scaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.update), title = {
Column {
Text(stringResource(vm.state.title))
if (vm.state == State.DOWNLOADING) {
Text(
text = "${vm.downloadedSize.div(1000000)} MB / ${
vm.totalSize.div(1000000)
} MB (${vm.downloadProgress.times(100).toInt()}%)",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline
)
}
}
},
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
onBackClick = onBackClick onBackClick = onBackClick
) )
}, },
floatingActionButton = {
val buttonConfig = when (vm.state) {
State.CAN_DOWNLOAD -> Triple(
{ vm.downloadUpdate() },
R.string.download,
Icons.Outlined.InstallMobile
)
State.DOWNLOADING -> Triple(onBackClick, R.string.cancel, Icons.Outlined.Cancel)
State.CAN_INSTALL -> Triple(
{ vm.installUpdate() },
R.string.install_app,
Icons.Outlined.InstallMobile
)
else -> null
}
buttonConfig?.let { (onClick, textRes, icon) ->
HapticExtendedFloatingActionButton(
onClick = onClick::invoke,
icon = { Icon(icon, null) },
text = { Text(stringResource(textRes)) }
)
}
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues -> ) { paddingValues ->
AnimatedVisibility(visible = vm.showInternetCheckDialog) {
MeteredDownloadConfirmationDialog(
onDismiss = { vm.showInternetCheckDialog = false },
onDownloadAnyways = { vm.downloadUpdate(true) }
)
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .padding(paddingValues),
.padding(paddingValues)
.padding(vertical = 16.dp, horizontal = 24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(32.dp)
) { ) {
Header( if (vm.state == State.DOWNLOADING)
vm.state, LinearProgressIndicator(
vm.releaseInfo, progress = { vm.downloadProgress },
DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize) modifier = Modifier.fillMaxWidth(),
) )
vm.releaseInfo?.let { changelog ->
HorizontalDivider() AnimatedVisibility(visible = vm.showInternetCheckDialog) {
Changelog(changelog) MeteredDownloadConfirmationDialog(
} ?: Spacer(modifier = Modifier.weight(1f)) onDismiss = { vm.showInternetCheckDialog = false },
Buttons(vm.state, vm::downloadUpdate, vm::installUpdate, onBackClick) onDownloadAnyways = { vm.downloadUpdate(true) }
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(32.dp)
) {
vm.releaseInfo?.let { changelog ->
Changelog(changelog)
}
}
} }
} }
} }
@ -123,58 +164,6 @@ private fun MeteredDownloadConfirmationDialog(
) )
} }
@Composable
private fun Header(state: State, releaseInfo: ReVancedAsset?, downloadData: DownloadData) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
text = stringResource(state.title),
style = MaterialTheme.typography.headlineMedium
)
if (state == State.CAN_DOWNLOAD) {
Column {
Text(
text = stringResource(
id = R.string.current_version,
BuildConfig.VERSION_NAME
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
releaseInfo?.version?.let {
Text(
text = stringResource(
R.string.new_version,
it.replace("v", "")
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else if (state == State.DOWNLOADING) {
LinearProgressIndicator(
progress = { downloadData.downloadProgress },
modifier = Modifier.fillMaxWidth(),
)
Text(
text =
"${downloadData.downloadedSize.div(1000000)} MB / ${
downloadData.totalSize.div(
1000000
)
} MB (${
downloadData.downloadProgress.times(
100
).toInt()
}%)",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}
}
@Composable @Composable
private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) { private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@ -206,39 +195,3 @@ private fun ColumnScope.Changelog(releaseInfo: ReVancedAsset) {
) )
} }
} }
@Composable
private fun Buttons(
state: State,
onDownloadClick: () -> Unit,
onInstallClick: () -> Unit,
onBackClick: () -> Unit
) {
Row(modifier = Modifier.fillMaxWidth()) {
if (state.showCancel) {
TextButton(
onClick = onBackClick,
) {
Text(text = stringResource(R.string.cancel))
}
}
Spacer(modifier = Modifier.weight(1f))
if (state == State.CAN_DOWNLOAD) {
Button(onClick = onDownloadClick) {
Text(text = stringResource(R.string.update))
}
} else if (state == State.CAN_INSTALL) {
Button(
onClick = onInstallClick
) {
Text(text = stringResource(R.string.install_app))
}
}
}
}
data class DownloadData(
val downloadProgress: Float,
val downloadedSize: Long,
val totalSize: Long
)

View File

@ -9,6 +9,7 @@ import android.content.pm.PackageInstaller
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -42,9 +43,9 @@ class UpdateViewModel(
private val networkInfo: NetworkInfo by inject() private val networkInfo: NetworkInfo by inject()
private val fs: Filesystem by inject() private val fs: Filesystem by inject()
var downloadedSize by mutableStateOf(0L) var downloadedSize by mutableLongStateOf(0L)
private set private set
var totalSize by mutableStateOf(0L) var totalSize by mutableLongStateOf(0L)
private set private set
val downloadProgress by derivedStateOf { val downloadProgress by derivedStateOf {
if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f
@ -89,7 +90,7 @@ class UpdateViewModel(
totalSize = contentLength totalSize = contentLength
} }
} }
state = State.CAN_INSTALL installUpdate()
} }
} }
} }
@ -140,10 +141,10 @@ class UpdateViewModel(
location.delete() location.delete()
} }
enum class State(@StringRes val title: Int, val showCancel: Boolean = false) { enum class State(@StringRes val title: Int) {
CAN_DOWNLOAD(R.string.update_available), CAN_DOWNLOAD(R.string.update_available),
DOWNLOADING(R.string.downloading_manager_update, true), DOWNLOADING(R.string.downloading_manager_update),
CAN_INSTALL(R.string.ready_to_install_update, true), CAN_INSTALL(R.string.ready_to_install_update),
INSTALLING(R.string.installing_manager_update), INSTALLING(R.string.installing_manager_update),
FAILED(R.string.install_update_manager_failed), FAILED(R.string.install_update_manager_failed),
SUCCESS(R.string.update_completed) SUCCESS(R.string.update_completed)