From bd9778a3d16ff7e97c65660045711a8e00923c04 Mon Sep 17 00:00:00 2001 From: Ushie Date: Sun, 19 Nov 2023 23:28:28 +0300 Subject: [PATCH] feat(Update Screen): changelogs & handle states (#1464) Co-authored-by: Ax333l --- app/build.gradle.kts | 3 + .../java/app/revanced/manager/MainActivity.kt | 6 +- .../revanced/manager/di/ViewModelModule.kt | 2 +- .../manager/service/InstallService.kt | 1 + .../manager/service/UninstallService.kt | 2 +- .../ui/component/settings/Changelog.kt | 95 +++++++ .../ui/destination/SettingsDestination.kt | 3 +- .../manager/ui/screen/SettingsScreen.kt | 16 +- .../settings/update/ChangelogsScreen.kt | 86 +------ .../settings/update/UpdateProgressScreen.kt | 98 ------- .../ui/screen/settings/update/UpdateScreen.kt | 239 ++++++++++++++++++ .../ui/viewmodel/UpdateProgressViewModel.kt | 75 ------ .../manager/ui/viewmodel/UpdateViewModel.kt | 170 +++++++++++++ app/src/main/res/values/strings.xml | 18 +- gradle/libs.versions.toml | 4 + 15 files changed, 545 insertions(+), 273 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt delete mode 100644 app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdateProgressScreen.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdateScreen.kt delete mode 100644 app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateProgressViewModel.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a0404b32..4c28bbc5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -158,4 +158,7 @@ dependencies { // Markdown implementation(libs.markdown.renderer) + + // Fading Edges + implementation(libs.fading.edges) } diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 6aa1eee8..5c714a93 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -73,7 +73,7 @@ class MainActivity : ComponentActivity() { TextButton( onClick = { vm.dismissUpdateDialog() - navController.navigate(Destination.Settings(SettingsDestination.UpdateProgress)) + navController.navigate(Destination.Settings(SettingsDestination.Update(false))) } ) { Text(stringResource(R.string.update)) @@ -85,8 +85,8 @@ class MainActivity : ComponentActivity() { } }, icon = { Icon(Icons.Outlined.Update, null) }, - title = { Text(stringResource(R.string.update_available)) }, - text = { Text(stringResource(R.string.update_available_description, it)) } + title = { Text(stringResource(R.string.update_available_dialog_title)) }, + text = { Text(stringResource(R.string.update_available_dialog_description, it)) } ) } diff --git a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt index baf66330..dbcba6a0 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -14,7 +14,7 @@ val viewModelModule = module { viewModelOf(::AppSelectorViewModel) viewModelOf(::VersionSelectorViewModel) viewModelOf(::InstallerViewModel) - viewModelOf(::UpdateProgressViewModel) + viewModelOf(::UpdateViewModel) viewModelOf(::ChangelogsViewModel) viewModelOf(::ImportExportViewModel) viewModelOf(::ContributorViewModel) diff --git a/app/src/main/java/app/revanced/manager/service/InstallService.kt b/app/src/main/java/app/revanced/manager/service/InstallService.kt index 420a5dc0..7bf2d213 100644 --- a/app/src/main/java/app/revanced/manager/service/InstallService.kt +++ b/app/src/main/java/app/revanced/manager/service/InstallService.kt @@ -29,6 +29,7 @@ class InstallService : Service() { else -> { sendBroadcast(Intent().apply { action = APP_INSTALL_ACTION + `package` = packageName putExtra(EXTRA_INSTALL_STATUS, extraStatus) putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage) putExtra(EXTRA_PACKAGE_NAME, extraPackageName) diff --git a/app/src/main/java/app/revanced/manager/service/UninstallService.kt b/app/src/main/java/app/revanced/manager/service/UninstallService.kt index cefd3528..6bb4d4fd 100644 --- a/app/src/main/java/app/revanced/manager/service/UninstallService.kt +++ b/app/src/main/java/app/revanced/manager/service/UninstallService.kt @@ -31,7 +31,7 @@ class UninstallService : Service() { else -> { sendBroadcast(Intent().apply { action = APP_UNINSTALL_ACTION - + `package` = packageName putExtra(EXTRA_UNINSTALL_STATUS, extraStatus) putExtra(EXTRA_UNINSTALL_STATUS_MESSAGE, extraStatusMessage) }) diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt new file mode 100644 index 00000000..0a609e78 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/Changelog.kt @@ -0,0 +1,95 @@ +package app.revanced.manager.ui.component.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CalendarToday +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.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import app.revanced.manager.ui.component.Markdown + +@Composable +fun Changelog( + markdown: String, + version: String, + downloadCount: String, + publishDate: String +) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 0.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.Campaign, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier + .size(32.dp) + ) + Text( + version.removePrefix("v"), + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)), + color = MaterialTheme.colorScheme.primary, + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + ) { + Tag( + Icons.Outlined.Sell, + version + ) + Tag( + Icons.Outlined.FileDownload, + downloadCount + ) + Tag( + Icons.Outlined.CalendarToday, + publishDate + ) + } + } + Markdown( + markdown, + ) +} + +@Composable +private fun Tag(icon: ImageVector, text: String) { + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.outline, + ) + Text( + text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/destination/SettingsDestination.kt b/app/src/main/java/app/revanced/manager/ui/destination/SettingsDestination.kt index ac3374c8..5b6e59ee 100644 --- a/app/src/main/java/app/revanced/manager/ui/destination/SettingsDestination.kt +++ b/app/src/main/java/app/revanced/manager/ui/destination/SettingsDestination.kt @@ -27,7 +27,7 @@ sealed interface SettingsDestination : Parcelable { object About : SettingsDestination @Parcelize - object UpdateProgress : SettingsDestination + data class Update(val downloadOnScreenEntry: Boolean) : SettingsDestination @Parcelize object Changelogs : SettingsDestination @@ -37,5 +37,4 @@ sealed interface SettingsDestination : Parcelable { @Parcelize object Licenses: SettingsDestination - } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt index 76bff712..e26602f4 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt @@ -28,15 +28,17 @@ import androidx.compose.ui.res.stringResource import app.revanced.manager.R import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.NotificationCard +import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.destination.SettingsDestination import app.revanced.manager.ui.screen.settings.* import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen -import app.revanced.manager.ui.screen.settings.update.UpdateProgressScreen +import app.revanced.manager.ui.screen.settings.update.UpdateScreen import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen import app.revanced.manager.ui.viewmodel.SettingsViewModel import dev.olshevski.navigation.reimagined.* import org.koin.androidx.compose.getViewModel -import app.revanced.manager.ui.component.settings.SettingsListItem +import org.koin.core.parameter.parametersOf +import org.koin.androidx.compose.getViewModel as getComposeViewModel @SuppressLint("BatteryLife") @OptIn(ExperimentalMaterial3Api::class) @@ -96,7 +98,6 @@ fun SettingsScreen( controller = navController ) { destination -> when (destination) { - is SettingsDestination.General -> GeneralSettingsScreen( onBackClick = backClick, viewModel = viewModel @@ -109,7 +110,7 @@ fun SettingsScreen( is SettingsDestination.Updates -> UpdatesSettingsScreen( onBackClick = backClick, onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) }, - onUpdateClick = { navController.navigate(SettingsDestination.UpdateProgress) } + onUpdateClick = { navController.navigate(SettingsDestination.Update(false)) } ) is SettingsDestination.Downloads -> DownloadsSettingsScreen( @@ -126,8 +127,13 @@ fun SettingsScreen( onLicensesClick = { navController.navigate(SettingsDestination.Licenses) } ) - is SettingsDestination.UpdateProgress -> UpdateProgressScreen( + is SettingsDestination.Update -> UpdateScreen( onBackClick = backClick, + vm = getComposeViewModel { + parametersOf( + destination.downloadOnScreenEntry + ) + } ) is SettingsDestination.Changelogs -> ChangelogsScreen( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt index 16a9f802..a1ecd92b 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsScreen.kt @@ -1,37 +1,27 @@ package app.revanced.manager.ui.screen.settings.update + import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.CalendarToday -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.Divider import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.revanced.manager.R import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.LoadingIndicator -import app.revanced.manager.ui.component.Markdown +import app.revanced.manager.ui.component.settings.Changelog import app.revanced.manager.ui.viewmodel.ChangelogsViewModel import app.revanced.manager.util.formatNumber import app.revanced.manager.util.relativeTime @@ -103,76 +93,4 @@ fun ChangelogItem( ) } } -} - -@Composable -private fun Changelog( - markdown: String, - version: String, - downloadCount: String, - publishDate: String -) { - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 0.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Outlined.Campaign, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier - .size(32.dp) - ) - Text( - version.removePrefix("v"), - style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)), - color = MaterialTheme.colorScheme.primary, - ) - } - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .fillMaxWidth() - ) { - Tag( - Icons.Outlined.Sell, - version - ) - Tag( - Icons.Outlined.FileDownload, - downloadCount - ) - Tag( - Icons.Outlined.CalendarToday, - publishDate - ) - } - } - Markdown( - markdown, - ) -} - -@Composable -private fun Tag(icon: ImageVector, text: String) { - Row( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.outline, - ) - Text( - text, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.outline, - ) - } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdateProgressScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdateProgressScreen.kt deleted file mode 100644 index b1a5f152..00000000 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdateProgressScreen.kt +++ /dev/null @@ -1,98 +0,0 @@ -package app.revanced.manager.ui.screen.settings.update - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import app.revanced.manager.R -import app.revanced.manager.ui.component.AppTopBar -import app.revanced.manager.ui.viewmodel.UpdateProgressViewModel -import org.koin.androidx.compose.getViewModel - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -@Stable -fun UpdateProgressScreen( - onBackClick: () -> Unit, - vm: UpdateProgressViewModel = getViewModel() -) { - Scaffold( - topBar = { - AppTopBar( - title = stringResource(R.string.updates), - onBackClick = onBackClick - ) - } - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .padding(vertical = 16.dp, horizontal = 24.dp) - .verticalScroll(rememberScrollState()), - ) { - Text( - text = if (vm.isInstalling) stringResource(R.string.installing_manager_update) else stringResource( - R.string.downloading_manager_update - ), style = MaterialTheme.typography.headlineMedium - ) - LinearProgressIndicator( - progress = vm.downloadProgress, - modifier = Modifier - .padding(vertical = 16.dp) - .fillMaxWidth() - ) - Text( - text = if (!vm.isInstalling) "${vm.downloadedSize.div(1000000)} MB / ${ - vm.totalSize.div( - 1000000 - ) - } MB (${vm.downloadProgress.times(100).toInt()}%)" else stringResource(R.string.installing_message), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.outline, - modifier = Modifier.align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center - ) - Text( - text = "This update adds many functionality and fixes many issues in Manager. New experiment toggles are also added, they can be found in Settings > Advanced. Please submit some feedback in Settings > About > Submit issues or feedback. Thank you, everyone!", - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(vertical = 32.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.weight(1f)) - Row( - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - TextButton( - onClick = onBackClick, - ) { - Text(text = stringResource(R.string.cancel)) - } - Button(onClick = vm::installUpdate, enabled = vm.finished) { - Text(text = stringResource(R.string.update)) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdateScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdateScreen.kt new file mode 100644 index 00000000..29ca28fd --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdateScreen.kt @@ -0,0 +1,239 @@ +package app.revanced.manager.ui.screen.settings.update + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.spring +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Update +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.BuildConfig +import app.revanced.manager.R +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.settings.Changelog +import app.revanced.manager.ui.viewmodel.UpdateViewModel +import app.revanced.manager.ui.viewmodel.UpdateViewModel.Changelog +import app.revanced.manager.ui.viewmodel.UpdateViewModel.State +import app.revanced.manager.util.formatNumber +import app.revanced.manager.util.relativeTime +import com.gigamole.composefadingedges.content.FadingEdgesContentType +import com.gigamole.composefadingedges.content.scrollconfig.FadingEdgesScrollConfig +import com.gigamole.composefadingedges.fill.FadingEdgesFillType +import com.gigamole.composefadingedges.verticalFadingEdges +import org.koin.androidx.compose.getViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@Stable +fun UpdateScreen( + onBackClick: () -> Unit, + vm: UpdateViewModel = getViewModel() +) { + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.updates), + onBackClick = onBackClick + ) + } + ) { paddingValues -> + AnimatedVisibility(visible = vm.showInternetCheckDialog) { + MeteredDownloadConfirmationDialog( + onDismiss = { vm.showInternetCheckDialog = false }, + onDownloadAnyways = { vm.downloadUpdate(true) } + ) + } + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(vertical = 16.dp, horizontal = 24.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(32.dp) + ) { + Header( + vm.state, + vm.changelog, + DownloadData(vm.downloadProgress, vm.downloadedSize, vm.totalSize) + ) + vm.changelog?.let { changelog -> + Divider() + Changelog(changelog) + } ?: Spacer(modifier = Modifier.weight(1f)) + Buttons(vm.state, vm::downloadUpdate, vm::installUpdate, onBackClick) + } + } +} + +@Composable +private fun MeteredDownloadConfirmationDialog( + onDismiss: () -> Unit, + onDownloadAnyways: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + dismissButton = { + TextButton(onDismiss) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + onDismiss() + onDownloadAnyways() + } + ) { + Text(stringResource(R.string.download)) + } + }, + title = { Text(stringResource(R.string.download_update_confirmation)) }, + icon = { Icon(Icons.Outlined.Update, null) }, + text = { Text(stringResource(R.string.download_confirmation_metered)) } + ) +} + +@Composable +private fun Header(state: State, changelog: Changelog?, 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 + ) + changelog?.let { changelog -> + Text( + text = stringResource( + id = R.string.new_version, + changelog.version.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 +private fun ColumnScope.Changelog(changelog: Changelog) { + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(scrollState) + .verticalFadingEdges( + fillType = FadingEdgesFillType.FadeColor( + color = MaterialTheme.colorScheme.background, + fillStops = Triple(0F, 0.55F, 1F), + secondStopAlpha = 1F + ), + contentType = FadingEdgesContentType.Dynamic.Scroll( + state = scrollState, + scrollConfig = FadingEdgesScrollConfig.Dynamic( + animationSpec = spring(), + isLerpByDifferenceForPartialContent = true, + scrollFactor = 1.25F + ) + ), + length = 350.dp + ) + ) { + Changelog( + markdown = changelog.body.replace("`", ""), + version = changelog.version, + downloadCount = changelog.downloadCount.formatNumber(), + publishDate = changelog.publishDate.relativeTime(LocalContext.current) + ) + } +} + +@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 +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateProgressViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateProgressViewModel.kt deleted file mode 100644 index a7d7969d..00000000 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateProgressViewModel.kt +++ /dev/null @@ -1,75 +0,0 @@ -package app.revanced.manager.ui.viewmodel - -import android.app.Application -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.revanced.manager.R -import app.revanced.manager.network.api.ReVancedAPI -import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType -import app.revanced.manager.network.service.HttpService -import app.revanced.manager.network.utils.getOrThrow -import app.revanced.manager.util.APK_MIMETYPE -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import app.revanced.manager.util.PM -import app.revanced.manager.util.uiSafe -import io.ktor.client.plugins.onDownload -import io.ktor.client.request.url -import kotlinx.coroutines.withContext -import java.io.File - -class UpdateProgressViewModel( - app: Application, - private val reVancedAPI: ReVancedAPI, - private val http: HttpService, - private val pm: PM -) : ViewModel() { - var downloadedSize by mutableStateOf(0L) - private set - var totalSize by mutableStateOf(0L) - private set - val downloadProgress by derivedStateOf { - if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f - - downloadedSize.toFloat() / totalSize.toFloat() - } - val isInstalling by derivedStateOf { downloadProgress >= 1 } - var finished by mutableStateOf(false) - private set - - private val location = File.createTempFile("updater", ".apk", app.cacheDir) - private val job = viewModelScope.launch { - uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") { - withContext(Dispatchers.IO) { - val asset = reVancedAPI - .getLatestRelease("revanced-manager") - .getOrThrow() - .findAssetByType(APK_MIMETYPE) - - http.download(location) { - url(asset.downloadUrl) - onDownload { bytesSentTotal, contentLength -> - downloadedSize = bytesSentTotal - totalSize = contentLength - } - } - } - finished = true - } - } - - fun installUpdate() = viewModelScope.launch { - pm.installApp(listOf(location)) - } - - override fun onCleared() { - super.onCleared() - - job.cancel() - location.delete() - } -} diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt new file mode 100644 index 00000000..d8b26b22 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt @@ -0,0 +1,170 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageInstaller +import android.util.Log +import androidx.annotation.StringRes +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.data.platform.NetworkInfo +import app.revanced.manager.network.api.ReVancedAPI +import app.revanced.manager.network.api.ReVancedAPI.Extensions.findAssetByType +import app.revanced.manager.network.dto.ReVancedRelease +import app.revanced.manager.network.service.HttpService +import app.revanced.manager.network.utils.getOrThrow +import app.revanced.manager.service.InstallService +import app.revanced.manager.service.UninstallService +import app.revanced.manager.util.APK_MIMETYPE +import app.revanced.manager.util.PM +import app.revanced.manager.util.simpleMessage +import app.revanced.manager.util.tag +import app.revanced.manager.util.toast +import app.revanced.manager.util.uiSafe +import io.ktor.client.plugins.onDownload +import io.ktor.client.request.url +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.io.File + +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 pm: PM by inject() + private val networkInfo: NetworkInfo by inject() + + var downloadedSize by mutableStateOf(0L) + private set + var totalSize by mutableStateOf(0L) + private set + val downloadProgress by derivedStateOf { + if (downloadedSize == 0L || totalSize == 0L) return@derivedStateOf 0f + + downloadedSize.toFloat() / totalSize.toFloat() + } + var showInternetCheckDialog by mutableStateOf(false) + var state by mutableStateOf(State.CAN_DOWNLOAD) + private set + + var installError by mutableStateOf("") + + var changelog: Changelog? by mutableStateOf(null) + + private val location = File.createTempFile("updater", ".apk", app.cacheDir) + private var release: ReVancedRelease? = null + private val job = viewModelScope.launch { + uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") { + withContext(Dispatchers.IO) { + val response = reVancedAPI + .getLatestRelease("revanced-manager") + .getOrThrow() + release = response + changelog = Changelog( + response.metadata.tag, + response.findAssetByType(APK_MIMETYPE).downloadCount, + response.metadata.publishedAt, + response.metadata.body + ) + } + if (downloadOnScreenEntry) { + downloadUpdate() + } else { + state = State.CAN_DOWNLOAD + } + } + } + + fun downloadUpdate(ignoreInternetCheck: Boolean = false) = viewModelScope.launch { + uiSafe(app, R.string.failed_to_download_update, "Failed to download update") { + withContext(Dispatchers.IO) { + if (!networkInfo.isSafe() && !ignoreInternetCheck) { + showInternetCheckDialog = true + } else { + state = State.DOWNLOADING + val asset = release?.findAssetByType(APK_MIMETYPE) + ?: throw Exception("couldn't find asset to download") + + http.download(location) { + url(asset.downloadUrl) + onDownload { bytesSentTotal, contentLength -> + downloadedSize = bytesSentTotal + totalSize = contentLength + } + } + state = State.CAN_INSTALL + } + } + } + } + + fun installUpdate() = viewModelScope.launch { + state = State.INSTALLING + + pm.installApp(listOf(location)) + } + + private val installBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + intent?.let { + val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999) + val extra = + intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!! + + if (pmStatus == PackageInstaller.STATUS_SUCCESS) { + app.toast(app.getString(R.string.install_app_success)) + state = State.SUCCESS + } else { + state = State.FAILED + // TODO: handle install fail with a popup + installError = extra + app.toast(app.getString(R.string.install_app_fail, extra)) + } + } + } + } + + init { + ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply { + addAction(InstallService.APP_INSTALL_ACTION) + }, ContextCompat.RECEIVER_NOT_EXPORTED) + } + + override fun onCleared() { + super.onCleared() + app.unregisterReceiver(installBroadcastReceiver) + + job.cancel() + location.delete() + } + + data class Changelog( + val version: String, + val downloadCount: Int, + val publishDate: String, + val body: String, + ) + + enum class State(@StringRes val title: Int, val showCancel: Boolean = false) { + CAN_DOWNLOAD(R.string.update_available), + DOWNLOADING(R.string.downloading_manager_update, true), + CAN_INSTALL(R.string.ready_to_install_update, true), + INSTALLING(R.string.installing_manager_update), + FAILED(R.string.install_update_manager_failed), + SUCCESS(R.string.update_completed) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31e7be4d..37f33002 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,10 +32,10 @@ Patch selection and options %d patches selected No patches selected - + Change version %s selected - + Could not import legacy settings Select updates to receive @@ -273,6 +273,12 @@ Choose the type of bundle you want About ReVanced Manager ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance. + An update is available + Current version: %s + New version: %s + Ready to install update + Update installed + Failed to install update A minor update for ReVanced Manager is available. Click here to update and get the latest features and fixes! Update channel Stable @@ -300,7 +306,11 @@ Failed to check for updates Not now - New update available - A new version (%s) is available for download. + New update available + A new version (%s) is available for download. + Failed to download update: %s + Download + You are currently on a metered connection, and data charges from your service provider may apply.\n\nDo you still want to continue? + Download update? No contributors found \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d8e6ed55..3baad062 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ koin-version-compose = "3.4.6" reimagined-navigation = "1.5.0" ktor = "2.3.3" markdown-renderer = "0.8.0" +fading-edges = "1.0.4" androidGradlePlugin = "8.1.2" kotlinGradlePlugin = "1.9.10" devToolsGradlePlugin = "1.9.10-1.0.13" @@ -95,6 +96,9 @@ skrapeit-parser = { group = "it.skrape", name = "skrapeit-html-parser", version. # Markdown markdown-renderer = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-android", version.ref = "markdown-renderer" } +# Fading Edges +fading-edges = { group = "com.github.GIGAMOLE", name = "ComposeFadingEdges", version.ref = "fading-edges"} + # LibSU libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" }