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" }