diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md index 6faa9fed2..ac24b8445 100644 --- a/app/CHANGELOG.md +++ b/app/CHANGELOG.md @@ -1,3 +1,59 @@ +# app [2.4.0-dev.2](https://github.com/ReVanced/revanced-manager/compare/v2.4.0-dev.1...v2.4.0-dev.2) (2026-03-23) + + +### Bug Fixes + +* Load app info from installed apps by default ([e7cff33](https://github.com/ReVanced/revanced-manager/commit/e7cff333182acb140883ee763b8be70730ab798f)) + +# app [2.4.0-dev.1](https://github.com/ReVanced/revanced-manager/compare/v2.3.1-dev.6...v2.4.0-dev.1) (2026-03-22) + + +### Features + +* Speed up loading app list ([f0727b2](https://github.com/ReVanced/revanced-manager/commit/f0727b27debef4cebc3c8f9339330a39c4e92d9a)) + +## app [2.3.1-dev.6](https://github.com/ReVanced/revanced-manager/compare/v2.3.1-dev.5...v2.3.1-dev.6) (2026-03-22) + + +### Bug Fixes + +* Hide apps with no explicit compatibility when safeguard is on ([#3174](https://github.com/ReVanced/revanced-manager/issues/3174)) ([be91aa1](https://github.com/ReVanced/revanced-manager/commit/be91aa1c67a7896bfb3c789961ae4411140f0b0e)) + +## app [2.3.1-dev.5](https://github.com/ReVanced/revanced-manager/compare/v2.3.1-dev.4...v2.3.1-dev.5) (2026-03-22) + + +### Bug Fixes + +* Show patchable section header only when it's not the only section ([#3173](https://github.com/ReVanced/revanced-manager/issues/3173)) ([65b9e95](https://github.com/ReVanced/revanced-manager/commit/65b9e95c60abe9e974a6e4c67b827dcf4f429ee8)) + +## app [2.3.1-dev.4](https://github.com/ReVanced/revanced-manager/compare/v2.3.1-dev.3...v2.3.1-dev.4) (2026-03-22) + + +### Bug Fixes + +* Improve dialogs ([#3165](https://github.com/ReVanced/revanced-manager/issues/3165)) ([2fb13cd](https://github.com/ReVanced/revanced-manager/commit/2fb13cdcc73ec5741a3dca94d93a951f88012769)) + +## app [2.3.1-dev.3](https://github.com/ReVanced/revanced-manager/compare/v2.3.1-dev.2...v2.3.1-dev.3) (2026-03-22) + + +### Bug Fixes + +* Improve notification cards ([653c14e](https://github.com/ReVanced/revanced-manager/commit/653c14ea5d1eeaf986a0ce2a4ab2e0c7dcf1d53b)) + +## app [2.3.1-dev.2](https://github.com/ReVanced/revanced-manager/compare/v2.3.1-dev.1...v2.3.1-dev.2) (2026-03-22) + + +### Bug Fixes + +* Handle prerelease status when fetching changelogs ([742e6e8](https://github.com/ReVanced/revanced-manager/commit/742e6e8e781a320ea711ef5dd4c66b8144b0e3c8)) + +## app [2.3.1-dev.1](https://github.com/ReVanced/revanced-manager/compare/v2.3.0...v2.3.1-dev.1) (2026-03-21) + + +### Bug Fixes + +* Apply M3 style guide to UI strings ([#3114](https://github.com/ReVanced/revanced-manager/issues/3114)) ([8f773bc](https://github.com/ReVanced/revanced-manager/commit/8f773bc7ab61e3e475de2fd2e3df9aab7d9984f3)) + # app [2.3.0](https://github.com/ReVanced/revanced-manager/compare/v2.2.3...v2.3.0) (2026-03-20) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 953ce8450..bbf881e49 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(libs.compose.material.icons.extended) implementation(libs.compose.material3) implementation(libs.navigation.compose) + implementation(libs.paging3) // Accompanist implementation(libs.accompanist.drawablepainter) diff --git a/app/gradle.properties b/app/gradle.properties index b4a784968..2d99f0b08 100644 --- a/app/gradle.properties +++ b/app/gradle.properties @@ -1 +1 @@ -version = 2.3.0 +version = 2.4.0-dev.2 diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 48db9c40f..5f5a7dc10 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -30,7 +30,6 @@ import androidx.navigation.toRoute import app.revanced.manager.domain.repository.ChangelogSource import app.revanced.manager.ui.model.navigation.Announcement import app.revanced.manager.ui.model.navigation.Announcements -import app.revanced.manager.ui.model.navigation.AppSelector import app.revanced.manager.ui.model.navigation.BundleInformation import app.revanced.manager.ui.model.navigation.ComplexParameter import app.revanced.manager.ui.model.navigation.Dashboard @@ -42,7 +41,6 @@ import app.revanced.manager.ui.model.navigation.Settings import app.revanced.manager.ui.model.navigation.Update import app.revanced.manager.ui.screen.AnnouncementScreen import app.revanced.manager.ui.screen.AnnouncementsScreen -import app.revanced.manager.ui.screen.AppSelectorScreen import app.revanced.manager.ui.screen.BundleInformationScreen import app.revanced.manager.ui.screen.DashboardScreen import app.revanced.manager.ui.screen.InstalledAppInfoScreen @@ -166,9 +164,6 @@ private fun ReVancedManager(vm: MainViewModel) { composable { DashboardScreen( onSettingsClick = { navController.navigateSafe(Settings) }, - onAppSelectorClick = { - navController.navigateSafe(AppSelector) - }, onUpdateClick = { navController.navigateSafe(Update()) }, @@ -215,14 +210,6 @@ private fun ReVancedManager(vm: MainViewModel) { ) } - composable { - AppSelectorScreen( - onSelect = vm::selectApp, - onStorageSelect = vm::selectApp, - onBackClick = navController::popBackStackSafe - ) - } - composable { PatcherScreen( onBackClick = { 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 7029dc28a..150c290af 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -12,7 +12,6 @@ val viewModelModule = module { viewModelOf(::PatchesSelectorViewModel) viewModelOf(::GeneralSettingsViewModel) viewModelOf(::AdvancedSettingsViewModel) - viewModelOf(::AppSelectorViewModel) viewModelOf(::PatcherViewModel) viewModelOf(::UpdateViewModel) viewModelOf(::AnnouncementsViewModel) @@ -22,7 +21,7 @@ val viewModelModule = module { viewModelOf(::DeveloperOptionsViewModel) viewModelOf(::ContributorViewModel) viewModelOf(::DownloadsViewModel) - viewModelOf(::InstalledAppsViewModel) + viewModelOf(::AppsViewModel) viewModelOf(::InstalledAppInfoViewModel) viewModelOf(::UpdatesSettingsViewModel) viewModelOf(::BundleListViewModel) diff --git a/app/src/main/java/app/revanced/manager/domain/repository/ChangelogsRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/ChangelogsRepository.kt index aed2c04b7..77d59e665 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/ChangelogsRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/ChangelogsRepository.kt @@ -2,6 +2,8 @@ package app.revanced.manager.domain.repository import android.os.Parcelable import androidx.core.net.toUri +import androidx.paging.PagingSource +import androidx.paging.PagingState import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.dto.ReVancedAssetHistory import app.revanced.manager.network.utils.getOrThrow @@ -11,58 +13,36 @@ import kotlinx.serialization.Serializable @Parcelize @Serializable sealed interface ChangelogSource : Parcelable { - data object Manager : ChangelogSource { - } - data class Patches(val url: String) : ChangelogSource { - val baseUrl get() = url.toUri().let { "${it.scheme}://${it.host}" } + data object Manager : ChangelogSource + data class Patches(val url: String, val prerelease: Boolean) : ChangelogSource { + val baseUrl by lazy { url.toUri().let { "${it.scheme}://${it.host}" } } } } class ChangelogsRepository( - private val api: ReVancedAPI -) { - private var all: List = emptyList() - private var page = 0 + private val api: ReVancedAPI, + private val source: ChangelogSource, +) : PagingSource() { - suspend fun loadInitial( - source: ChangelogSource, - pageSize: Int - ): PageResult { - all = when (source) { - is ChangelogSource.Manager -> - api.getAppHistory().getOrThrow() + override suspend fun load(params: LoadParams): LoadResult { + return try { + val items = when (source) { + is ChangelogSource.Manager -> + api.getAppHistory().getOrThrow() - is ChangelogSource.Patches -> - api.getPatchesHistory(source.baseUrl).getOrThrow() + is ChangelogSource.Patches -> + api.getPatchesHistory(source.baseUrl, source.prerelease).getOrThrow() + } + + LoadResult.Page( + data = items, + prevKey = null, + nextKey = null + ) + } catch (e: Exception) { + LoadResult.Error(e) } - - page = 1 - - val items = all.take(pageSize) - return PageResult( - items = items, - hasMore = hasMore(pageSize) - ) } - fun loadNext(pageSize: Int): PageResult { - val items = all - .drop(page * pageSize) - .take(pageSize) - - page++ - - return PageResult( - items = items, - hasMore = hasMore(pageSize) - ) - } - - private fun hasMore(pageSize: Int) = - page * pageSize < all.size -} - -data class PageResult( - val items: List, - val hasMore: Boolean -) \ No newline at end of file + override fun getRefreshKey(state: PagingState): Int? = null +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt b/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt index d4e61b0ba..67591012b 100644 --- a/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt +++ b/app/src/main/java/app/revanced/manager/network/api/ReVancedAPI.kt @@ -49,12 +49,12 @@ class ReVancedAPI( suspend fun getLatestAppInfo() = request("manager${prefs.useManagerPrereleases.prereleaseString()}") - suspend fun getAppHistory() = request>("manager/history") + suspend fun getAppHistory() = request>("manager/history${prefs.useManagerPrereleases.prereleaseString()}") suspend fun getPatchesUpdate() = request("patches${prefs.usePatchesPrereleases.prereleaseString()}") - suspend fun getPatchesHistory(apiUrl: String) = - request>(apiUrl, defaultApiVersion, "patches/history") + suspend fun getPatchesHistory(apiUrl: String, prerelease: Boolean) = + request>(apiUrl, defaultApiVersion, "patches/history${prerelease.prereleaseString()}") suspend fun getDownloaderUpdate() = request("manager/downloaders${prefs.useDownloaderPrerelease.prereleaseString()}") @@ -64,5 +64,6 @@ class ReVancedAPI( private companion object { suspend fun Preference.prereleaseString() = if (get()) "/prerelease" else "" + fun Boolean.prereleaseString() = if (this) "/prerelease" else "" } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/ChangelogList.kt b/app/src/main/java/app/revanced/manager/ui/component/ChangelogList.kt index 7f93620e2..1b9a71909 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/ChangelogList.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/ChangelogList.kt @@ -31,85 +31,65 @@ 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 androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems import app.revanced.manager.R import app.revanced.manager.network.dto.ReVancedAssetHistory import app.revanced.manager.util.relativeTime -sealed interface ChangelogUiState { - data object Loading : ChangelogUiState - data class Error(val error: String) : ChangelogUiState - data class Success( - val changelogs: List, - val hasMore: Boolean = false, - val isLoadingMore: Boolean = false, - ) : ChangelogUiState -} - @Composable fun ChangelogList( - state: ChangelogUiState, - onLoadMore: () -> Unit, + changelogs: LazyPagingItems, modifier: Modifier = Modifier, ) { - val listState = rememberLazyListState() - - val shouldLoadMore by remember { - derivedStateOf { - val lastVisible = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 - val total = listState.layoutInfo.totalItemsCount - val canScroll = listState.canScrollForward || listState.canScrollBackward - - (lastVisible >= total - 2 || !canScroll) && total > 0 - } - } - - LaunchedEffect(shouldLoadMore, state) { - if (shouldLoadMore) onLoadMore() - } - Box( - modifier = modifier.then(Modifier.fillMaxSize()), + modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - when (state) { - is ChangelogUiState.Loading -> LoadingIndicator() + when { + changelogs.loadState.refresh is LoadState.Loading -> LoadingIndicator() - is ChangelogUiState.Error -> Text( - text = state.error, + changelogs.loadState.refresh is LoadState.Error -> { + val error = changelogs.loadState.refresh as LoadState.Error + Text( + text = error.error.message ?: stringResource(R.string.changelog_download_fail), + style = MaterialTheme.typography.titleLarge + ) + } + + changelogs.itemCount == 0 -> Text( + text = stringResource(R.string.no_changelogs_found), style = MaterialTheme.typography.titleLarge ) - is ChangelogUiState.Success -> { - if (state.changelogs.isEmpty()) { - Text( - text = stringResource(R.string.no_changelogs_found), - style = MaterialTheme.typography.titleLarge - ) - } else { - LazyColumnWithScrollbar( - modifier = Modifier.fillMaxSize(), - state = listState - ) { - items( - items = state.changelogs, - key = { it.version } - ) { changelog -> + else -> { + val listState = rememberLazyListState() + + LazyColumnWithScrollbar( + modifier = Modifier.fillMaxSize(), + state = listState + ) { + items( + count = changelogs.itemCount, + key = { changelogs.peek(it)?.version ?: it } + ) { index -> + changelogs[index]?.let { changelog -> ChangelogItem( changelog = changelog, - showDivider = changelog != state.changelogs.last() + showDivider = index < changelogs.itemCount - 1 ) } + } - if (state.isLoadingMore) { - item(key = "loading_more") { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } + if (changelogs.loadState.append is LoadState.Loading) { + item(key = "loading_more") { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() } } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt index 4de782e28..82fddad0a 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt @@ -2,26 +2,17 @@ package app.revanced.manager.ui.component import android.annotation.SuppressLint import android.content.pm.PackageInstaller -import androidx.annotation.RequiresApi import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.graphics.vector.ImageVector 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.model.InstallerModel import com.github.materiiapps.enumutil.FromValue @@ -43,9 +34,6 @@ fun InstallerStatusDialog(installerStatus: Int, model: InstallerModel, onDismiss dismissButton = { dialogKind.dismissButton?.invoke(model, onDismiss) }, - icon = { - Icon(dialogKind.icon, null) - }, title = { Text( text = stringResource(dialogKind.title), @@ -54,12 +42,7 @@ fun InstallerStatusDialog(installerStatus: Int, model: InstallerModel, onDismiss ) }, text = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Text(stringResource(dialogKind.contentStringResId)) - } + Text(stringResource(dialogKind.contentStringResId)) } ) } @@ -85,7 +68,6 @@ enum class DialogKind( val flag: Int, val title: Int, @param:StringRes val contentStringResId: Int, - val icon: ImageVector = Icons.Outlined.ErrorOutline, val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok), val dismissButton: InstallerStatusDialogButton? = null, ) { @@ -93,58 +75,47 @@ enum class DialogKind( flag = PackageInstaller.STATUS_FAILURE, title = R.string.installation_failed_dialog_title, contentStringResId = R.string.installation_failed_description, - confirmButton = installerStatusDialogButton(R.string.install_app) { model -> - model.install() - } - ), - FAILURE_ABORTED( - flag = PackageInstaller.STATUS_FAILURE_ABORTED, - title = R.string.installation_cancelled_dialog_title, - contentStringResId = R.string.installation_aborted_description, - confirmButton = installerStatusDialogButton(R.string.install_app) { model -> - model.install() - } + confirmButton = installerStatusDialogButton(R.string.try_again) { it.install() }, + dismissButton = installerStatusDialogButton(R.string.cancel), ), FAILURE_BLOCKED( flag = PackageInstaller.STATUS_FAILURE_BLOCKED, title = R.string.installation_blocked_dialog_title, contentStringResId = R.string.installation_blocked_description, + dismissButton = installerStatusDialogButton(R.string.cancel), ), FAILURE_CONFLICT( flag = PackageInstaller.STATUS_FAILURE_CONFLICT, title = R.string.installation_conflict_dialog_title, contentStringResId = R.string.installation_conflict_description, - confirmButton = installerStatusDialogButton(R.string.reinstall) { model -> - model.reinstall() - }, + confirmButton = installerStatusDialogButton(R.string.reinstall) { it.reinstall() }, dismissButton = installerStatusDialogButton(R.string.cancel), ), FAILURE_INCOMPATIBLE( flag = PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, title = R.string.installation_incompatible_dialog_title, contentStringResId = R.string.installation_incompatible_description, + dismissButton = installerStatusDialogButton(R.string.ok), ), FAILURE_INVALID( flag = PackageInstaller.STATUS_FAILURE_INVALID, title = R.string.installation_invalid_dialog_title, contentStringResId = R.string.installation_invalid_description, - confirmButton = installerStatusDialogButton(R.string.reinstall) { model -> - model.reinstall() - }, + confirmButton = installerStatusDialogButton(R.string.reinstall) { it.reinstall() }, dismissButton = installerStatusDialogButton(R.string.cancel), ), FAILURE_STORAGE( flag = PackageInstaller.STATUS_FAILURE_STORAGE, title = R.string.installation_storage_issue_dialog_title, contentStringResId = R.string.installation_storage_issue_description, + dismissButton = installerStatusDialogButton(R.string.ok), ), FAILURE_TIMEOUT( flag = @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT, title = R.string.installation_timeout_dialog_title, contentStringResId = R.string.installation_timeout_description, - confirmButton = installerStatusDialogButton(R.string.install_app) { model -> - model.install() - }, + confirmButton = installerStatusDialogButton(R.string.try_again) { it.install() }, + dismissButton = installerStatusDialogButton(R.string.cancel), ); // Needed due to the @FromValue annotation. diff --git a/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt b/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt index 6204ee142..5e9f36f1f 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.revanced.manager.R @@ -57,7 +58,7 @@ fun NotificationCard( modifier = Modifier .fillMaxWidth() .padding(16.dp), - verticalAlignment = Alignment.Top, + verticalAlignment = if (actions != null) Alignment.Top else Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Box( @@ -74,18 +75,21 @@ fun NotificationCard( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - title?.let { + Column { + title?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMediumEmphasized.merge(fontWeight = FontWeight.SemiBold), + color = LocalContentColor.current, + ) + } Text( - text = it, - style = MaterialTheme.typography.bodyLarge, + text = text, + style = MaterialTheme.typography.bodyMedium, color = LocalContentColor.current, ) } - Text( - text = text, - style = MaterialTheme.typography.bodyLarge, - color = LocalContentColor.current, - ) + actions?.let { Row( modifier = Modifier.fillMaxWidth(), @@ -95,11 +99,14 @@ fun NotificationCard( } } if (onDismiss != null) { + // @TODO: Use xsmall icon buttons when that is available in Compose TooltipIconButton( + modifier = Modifier.size(32.dp), onClick = onDismiss, tooltip = stringResource(R.string.close), ) { Icon( + modifier = Modifier.size(20.dp), imageVector = Icons.Outlined.Close, contentDescription = stringResource(R.string.close), ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt index 3fe65c028..9dac923a4 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/sources/ImportSourceDialog.kt @@ -37,7 +37,6 @@ private enum class SourceType { enum class ImportSourceDialogStrings( val title: Int, - val type_description: Int, val type_remote_description: Int, val type_local_description: Int, val import_local: Int, @@ -45,7 +44,6 @@ enum class ImportSourceDialogStrings( ) { PATCHES( R.string.add_patches, - R.string.select_patches_type_dialog_description, R.string.remote_patches_description, R.string.local_patches_description, R.string.patches, @@ -53,7 +51,6 @@ enum class ImportSourceDialogStrings( ), DOWNLOADERS( R.string.downloader_add, - R.string.select_downloader_type_dialog_description, R.string.remote_downloaders_description, R.string.local_downloaders_description, R.string.downloaders, @@ -117,7 +114,7 @@ fun ImportSourceDialog( AlertDialogExtended( onDismissRequest = onDismiss, title = { - Text(stringResource(if (currentStep == 0) R.string.select else strings.title)) + Text(stringResource(strings.title)) }, text = { steps[currentStep]() @@ -167,10 +164,6 @@ private fun SelectSourceTypeStep( modifier = Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(24.dp) ) { - Text( - modifier = Modifier.padding(horizontal = 24.dp), - text = stringResource(strings.type_description) - ) Column { ListItem( modifier = Modifier.clickable( diff --git a/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt b/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt index c19dd677c..9b43ad703 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/navigation/Nav.kt @@ -18,9 +18,6 @@ object Onboarding @Serializable object Dashboard -@Serializable -object AppSelector - @Serializable data class InstalledApplicationInfo(val packageName: String) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt deleted file mode 100644 index 0978ac577..000000000 --- a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt +++ /dev/null @@ -1,262 +0,0 @@ -package app.revanced.manager.ui.screen - -import android.content.ActivityNotFoundException -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Storage -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import app.revanced.manager.R -import app.revanced.manager.ui.component.AppIcon -import app.revanced.manager.ui.component.AppLabel -import app.revanced.manager.ui.component.AppTopBar -import app.revanced.manager.ui.component.LazyColumnWithScrollbar -import app.revanced.manager.ui.component.LoadingIndicator -import app.revanced.manager.ui.component.NonSuggestedVersionDialog -import app.revanced.manager.ui.component.SearchView -import app.revanced.manager.ui.component.TooltipIconButton -import app.revanced.manager.ui.model.SelectedApp -import app.revanced.manager.ui.viewmodel.AppSelectorViewModel -import app.revanced.manager.util.APK_MIMETYPE -import app.revanced.manager.util.EventEffect -import app.revanced.manager.util.toast -import app.revanced.manager.util.transparentListItemColors -import org.koin.androidx.compose.koinViewModel - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun AppSelectorScreen( - onSelect: (String) -> Unit, - onStorageSelect: (SelectedApp.Local) -> Unit, - onBackClick: () -> Unit, - vm: AppSelectorViewModel = koinViewModel() -) { - val context = LocalContext.current - EventEffect(flow = vm.storageSelectionFlow) { - onStorageSelect(it) - } - - val pickApkLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> - uri?.let(vm::handleStorageResult) - } - - val suggestedVersions by vm.suggestedAppVersions.collectAsStateWithLifecycle() - - var search by rememberSaveable { mutableStateOf(false) } - val appList by vm.apps.collectAsStateWithLifecycle() - val appsListFiltered by vm.filteredApps.collectAsStateWithLifecycle() - - vm.nonSuggestedVersionDialogSubject?.let { - NonSuggestedVersionDialog( - suggestedVersion = suggestedVersions[it.packageName].orEmpty(), - onDismiss = vm::dismissNonSuggestedVersionDialog - ) - } - - if (search) { - val filterText by vm.filterText.collectAsState() - - SearchView( - query = filterText, - onQueryChange = vm::setFilterText, - onActiveChange = { search = it }, - placeholder = { Text(stringResource(R.string.search_apps)) } - ) { - val appsFiltered = appsListFiltered - if (!appsFiltered.isNullOrEmpty() && filterText.isNotEmpty()) { - LazyColumnWithScrollbar(modifier = Modifier.fillMaxSize()) { - items( - items = appsFiltered, - key = { it.packageName } - ) { app -> - ListItem( - modifier = Modifier.clickable { - onSelect(app.packageName) - }, - leadingContent = { - AppIcon( - packageInfo = app.packageInfo, - contentDescription = null, - modifier = Modifier.size(36.dp) - ) - }, - headlineContent = { AppLabel(app.packageInfo) }, - supportingContent = { Text(app.packageName) }, - trailingContent = app.patches?.let { - { - Text( - pluralStringResource( - R.plurals.patch_count, - it, - it - ) - ) - } - }, - colors = transparentListItemColors - ) - } - } - } else { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - imageVector = Icons.Filled.Search, - contentDescription = stringResource(R.string.search), - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - text = stringResource(R.string.type_anything), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - - Scaffold( - topBar = { - AppTopBar( - title = stringResource(R.string.select_app), - scrollBehavior = scrollBehavior, - onBackClick = onBackClick, - actions = { - TooltipIconButton( - onClick = { search = true }, - tooltip = stringResource(R.string.search) - ) { - Icon(Icons.Filled.Search, stringResource(R.string.search)) - } - } - ) - }, - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - ) { paddingValues -> - LazyColumnWithScrollbar( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - horizontalAlignment = Alignment.CenterHorizontally - ) { - item { - ListItem( - modifier = Modifier.clickable { - try { - pickApkLauncher.launch(APK_MIMETYPE) - } catch (_: ActivityNotFoundException) { - context.toast(R.string.no_file_picker_found) - } - }, - leadingContent = { - Box(Modifier.size(36.dp), Alignment.Center) { - Icon( - Icons.Default.Storage, - null, - modifier = Modifier.size(24.dp) - ) - } - }, - headlineContent = { Text(stringResource(R.string.select_from_storage)) }, - supportingContent = { - Text(stringResource(R.string.select_from_storage_description)) - } - ) - HorizontalDivider() - } - - val apps = appList - if (apps == null) { - item(key = "LOADING") { - Box( - modifier = Modifier.fillParentMaxSize(), - contentAlignment = Alignment.Center - ) { - LoadingIndicator() - } - } - } else if (apps.isNotEmpty()) { - items( - items = apps, - key = { "APP-" + it.packageName }, - contentType = { "APP" }, - ) { app -> - ListItem( - modifier = Modifier.clickable { - onSelect(app.packageName) - }, - leadingContent = { - AppIcon( - packageInfo = app.packageInfo, - contentDescription = null, - modifier = Modifier.size(36.dp) - ) - }, - headlineContent = { - AppLabel( - app.packageInfo, - defaultText = app.packageName - ) - }, - supportingContent = { - suggestedVersions[app.packageName]?.let { - Text(stringResource(R.string.suggested_version_info, it)) - } - }, - trailingContent = app.patches?.let { - { - Text( - pluralStringResource( - R.plurals.patch_count, - it, - it - ) - ) - } - } - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AppsScreen.kt similarity index 92% rename from app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt rename to app/src/main/java/app/revanced/manager/ui/screen/AppsScreen.kt index 4405cd836..5d8d914fd 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/AppsScreen.kt @@ -47,8 +47,7 @@ import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.component.SearchBar import app.revanced.manager.ui.component.TooltipIconButton import app.revanced.manager.ui.model.SelectedApp -import app.revanced.manager.ui.viewmodel.AppSelectorViewModel -import app.revanced.manager.ui.viewmodel.InstalledAppsViewModel +import app.revanced.manager.ui.viewmodel.AppsViewModel import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.EventEffect import app.revanced.manager.util.toast @@ -57,25 +56,24 @@ import org.koin.androidx.compose.koinViewModel @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun InstalledAppsScreen( +fun AppsScreen( onAppClick: (InstalledApp) -> Unit, onPatchableAppClick: (String) -> Unit, onStorageSelect: (SelectedApp.Local) -> Unit, - viewModel: InstalledAppsViewModel = koinViewModel(), - selectorVm: AppSelectorViewModel = koinViewModel() + viewModel: AppsViewModel = koinViewModel() ) { val context = LocalContext.current - EventEffect(flow = selectorVm.storageSelectionFlow) { + EventEffect(flow = viewModel.storageSelectionFlow) { onStorageSelect(it) } val pickApkLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> - uri?.let(selectorVm::handleStorageResult) + uri?.let(viewModel::handleStorageResult) } - val installedApps by viewModel.apps.collectAsStateWithLifecycle() - val patchableApps by selectorVm.apps.collectAsStateWithLifecycle() + val installedApps by viewModel.installedApps.collectAsStateWithLifecycle() + val patchableApps by viewModel.patchableApps.collectAsStateWithLifecycle() fun patchedPackageNames(apps: List?): Set = apps @@ -89,7 +87,7 @@ fun InstalledAppsScreen( val packageInfo = viewModel.packageInfoMap[currentPackageName] return currentPackageName.contains(query, ignoreCase = true) || originalPackageName.contains(query, ignoreCase = true) || - selectorVm.loadLabel(packageInfo).contains(query, ignoreCase = true) + viewModel.loadLabel(packageInfo).contains(query, ignoreCase = true) } fun patchableMatchesQuery(packageName: String, label: String?, query: String): Boolean { @@ -100,7 +98,7 @@ fun InstalledAppsScreen( } var searchExpanded by rememberSaveable { mutableStateOf(false) } - val filterText by selectorVm.filterText.collectAsStateWithLifecycle() + val filterText by viewModel.filterText.collectAsStateWithLifecycle() val TITLE_HORIZONTAL = 16.dp val TITLE_VERTICAL = 8.dp @@ -110,7 +108,7 @@ fun InstalledAppsScreen( Box(modifier = Modifier.padding(horizontal = if (searchExpanded) 0.dp else 16.dp)) { SearchBar( query = filterText, - onQueryChange = selectorVm::setFilterText, + onQueryChange = viewModel::setFilterText, expanded = searchExpanded, onExpandedChange = { searchExpanded = it }, placeholder = { Text(stringResource(R.string.search_apps)) }, @@ -120,7 +118,7 @@ fun InstalledAppsScreen( onClick = { if (searchExpanded) { searchExpanded = false - selectorVm.setFilterText("") + viewModel.setFilterText("") } }, tooltip = if (searchExpanded) stringResource(R.string.back) else stringResource(R.string.search), @@ -139,7 +137,7 @@ fun InstalledAppsScreen( trailingIcon = { if (searchExpanded && filterText.isNotEmpty()) { TooltipIconButton( - onClick = { selectorVm.setFilterText("") }, + onClick = { viewModel.setFilterText("") }, tooltip = stringResource(R.string.clear), ) { contentDescription -> Icon( @@ -162,7 +160,7 @@ fun InstalledAppsScreen( app.packageName !in patchedPkgNames && patchableMatchesQuery( packageName = app.packageName, - label = selectorVm.loadLabel(app.packageInfo), + label = viewModel.loadLabel(app.packageInfo), query = query ) } @@ -211,7 +209,7 @@ fun InstalledAppsScreen( ListItem( modifier = Modifier.clickable { searchExpanded = false - selectorVm.setFilterText("") + viewModel.setFilterText("") onAppClick(installedApp) }, leadingContent = { @@ -252,7 +250,7 @@ fun InstalledAppsScreen( ListItem( modifier = Modifier.clickable { searchExpanded = false - selectorVm.setFilterText("") + viewModel.setFilterText("") onPatchableAppClick(app.packageName) }, leadingContent = { @@ -357,15 +355,17 @@ fun InstalledAppsScreen( } } - item(key = "HEADER_PATCHABLE") { - Text( - text = stringResource(R.string.patchable_apps_section_title), - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.labelLarge, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TITLE_HORIZONTAL, vertical = TITLE_VERTICAL) - ) + if (patched.isNotEmpty()) { + item(key = "HEADER_PATCHABLE") { + Text( + text = stringResource(R.string.patchable_apps_section_title), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TITLE_HORIZONTAL, vertical = TITLE_VERTICAL) + ) + } } item(key = "PATCHABLE_STORAGE") { @@ -431,4 +431,4 @@ fun InstalledAppsScreen( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt index 53247858d..278429444 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/BundleInformationScreen.kt @@ -315,7 +315,18 @@ fun BundleInformationScreen( SettingsListItem( headlineContent = stringResource(R.string.changelog), onClick = { - onChangelogClick(ChangelogSource.Patches(if (src.isDefault) viewModel.prefs.api.getBlocking() else endpoint)) + val source = if (src.isDefault) { + ChangelogSource.Patches( + url = viewModel.prefs.api.getBlocking(), + prerelease = viewModel.prefs.usePatchesPrereleases.getBlocking() + ) + } else { + ChangelogSource.Patches( + url = endpoint, + prerelease = false + ) + } + onChangelogClick(source) }, ) } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt index 18dea7dc3..ef5f80ae4 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -1,14 +1,16 @@ package app.revanced.manager.ui.screen import android.annotation.SuppressLint +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.animation.togetherWith -import androidx.activity.compose.BackHandler -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.appcompat.content.res.AppCompatResources import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -36,15 +38,16 @@ import androidx.compose.material.icons.outlined.Apps import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Source import androidx.compose.material.icons.outlined.WarningAmber import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox -import androidx.compose.material3.ButtonColors 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 @@ -70,10 +73,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.core.tween -import androidx.compose.material.icons.outlined.Refresh -import androidx.compose.material3.LocalContentColor import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.network.dto.ReVancedAnnouncement @@ -85,9 +84,9 @@ import app.revanced.manager.ui.component.NotificationCardType import app.revanced.manager.ui.component.PillTab import app.revanced.manager.ui.component.PillTabBar import app.revanced.manager.ui.component.TooltipIconButton +import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.component.sources.ImportSourceDialog import app.revanced.manager.ui.component.sources.ImportSourceDialogStrings -import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo import app.revanced.manager.ui.viewmodel.DashboardViewModel @@ -97,7 +96,6 @@ import com.google.accompanist.drawablepainter.rememberDrawablePainter import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf -import kotlin.collections.emptyList enum class DashboardPage( val titleResId: Int, @@ -112,7 +110,6 @@ enum class DashboardPage( @Composable fun DashboardScreen( vm: DashboardViewModel = koinViewModel(), - onAppSelectorClick: () -> Unit, onSettingsClick: () -> Unit, onUpdateClick: () -> Unit, onAnnouncementsClick: () -> Unit, @@ -122,7 +119,6 @@ fun DashboardScreen( onStorageSelect: (SelectedApp.Local) -> Unit, onBundleClick: (Int) -> Unit ) { - val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0) val bundleDownloadError by vm.bundleDownloadError.collectAsStateWithLifecycle(null) val sourcesNotDownloaded by vm.sourcesNotDownloaded.collectAsStateWithLifecycle(false) val sourceUpdatesAvailable by vm.sourceUpdatesAvailable.collectAsStateWithLifecycle(false) @@ -220,23 +216,15 @@ fun DashboardScreen( ) } - var pendingAppSelectorLaunch by rememberSaveable { mutableStateOf(false) } var pendingPatchablePackage by rememberSaveable { mutableStateOf(null) } var pendingStorageSelection by rememberSaveable { mutableStateOf(null) } fun clearPendingSelection() { - pendingAppSelectorLaunch = false pendingPatchablePackage = null pendingStorageSelection = null } fun resumePendingSelection() { - if (pendingAppSelectorLaunch) { - clearPendingSelection() - onAppSelectorClick() - return - } - pendingPatchablePackage?.let { clearPendingSelection() onPatchableAppClick(it) @@ -462,36 +450,15 @@ fun DashboardScreen( vm.unreadAnnouncement?.let { announcement -> { NotificationCard( - text = stringResource(R.string.new_announcement, announcement.title), + title = stringResource(R.string.new_announcement), + text = announcement.title, icon = Icons.Filled.Notifications, - actions = { - val colors = ButtonDefaults.textButtonColors( - contentColor = LocalContentColor.current - ) - - TextButton( - onClick = vm::markUnreadAnnouncementRead, - shapes = ButtonDefaults.shapes(), - colors = colors - ) { - Text(stringResource(R.string.dismiss)) - } - TextButton( - onClick = { - vm.markUnreadAnnouncementRead() - onAnnouncementClick(announcement) - }, - shapes = ButtonDefaults.shapes(), - colors = colors - ) { - Text(stringResource(R.string.view_announcement)) - } - }, type = if (announcement.level > 0) NotificationCardType.ERROR else NotificationCardType.NORMAL, onClick = { vm.markUnreadAnnouncementRead() onAnnouncementClick(announcement) - } + }, + onDismiss = vm::markUnreadAnnouncementRead ) } } @@ -504,7 +471,7 @@ fun DashboardScreen( ) { index -> when (DashboardPage.entries[index]) { DashboardPage.DASHBOARD -> { - InstalledAppsScreen( + AppsScreen( onAppClick = { onAppClick(it.currentPackageName) }, onPatchableAppClick = ::onPatchableSelection, onStorageSelect = { selectedApp -> onStorageSelection(selectedApp) } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 98afabc52..1bfda9c25 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -1,7 +1,7 @@ package app.revanced.manager.ui.screen import android.app.Activity -import android.os.Build +import android.content.pm.PackageInstaller import android.view.WindowManager import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult @@ -150,6 +150,8 @@ fun PatcherScreen( } viewModel.packageInstallerStatus?.let { + // Don't show when the user cancels the installation (they can click Install again anyway) + if (it == PackageInstaller.STATUS_FAILURE_ABORTED) return@let InstallerStatusDialog(it, viewModel, viewModel::dismissPackageInstallerDialog) } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt index 7bc3a5044..fab0ecf6e 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/UpdateScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.paging.compose.collectAsLazyPagingItems import app.revanced.manager.R import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.BottomContentBar @@ -48,6 +49,7 @@ fun UpdateScreen( vm: UpdateViewModel = koinViewModel() ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val changelogs = vm.changelogs.collectAsLazyPagingItems() val buttonConfig = when (vm.state) { State.CAN_DOWNLOAD -> Triple( @@ -123,10 +125,7 @@ fun UpdateScreen( ) } - ChangelogList( - state = vm.changelogsState, - onLoadMore = vm::loadNextPage, - ) + ChangelogList(changelogs = changelogs, modifier = Modifier.padding(paddingValues)) } } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt index 29361b0b7..54a57b412 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt @@ -193,11 +193,6 @@ private fun APIUrlDialog(currentUrl: String, defaultUrl: String, onSubmit: (Stri modifier = Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Text( - text = stringResource(R.string.api_url_dialog_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) Text( text = stringResource(R.string.api_url_dialog_warning), style = MaterialTheme.typography.bodyMedium, diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsSettingsScreen.kt index cee726cdb..506b20edd 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/ChangelogsSettingsScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.paging.compose.collectAsLazyPagingItems import app.revanced.manager.domain.repository.ChangelogSource import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.ChangelogList @@ -23,6 +24,7 @@ fun ChangelogsSettingsScreen( vm: ChangelogsViewModel = koinViewModel { parametersOf(source) } ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val changelogs = vm.changelogs.collectAsLazyPagingItems() Scaffold( topBar = { @@ -34,6 +36,6 @@ fun ChangelogsSettingsScreen( }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), ) { paddingValues -> - ChangelogList(modifier = Modifier.padding(paddingValues), state = vm.state, onLoadMore =vm::loadNextPage) + ChangelogList(changelogs = changelogs, modifier = Modifier.padding(paddingValues)) } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt deleted file mode 100644 index ae5598639..000000000 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt +++ /dev/null @@ -1,129 +0,0 @@ -package app.revanced.manager.ui.viewmodel - -import android.app.Application -import android.content.pm.PackageInfo -import android.net.Uri -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi -import androidx.lifecycle.viewmodel.compose.saveable -import app.revanced.manager.R -import app.revanced.manager.data.platform.Filesystem -import app.revanced.manager.domain.repository.PatchBundleRepository -import app.revanced.manager.ui.model.SelectedApp -import app.revanced.manager.util.PM -import app.revanced.manager.util.toast -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File -import java.nio.file.Files - - -// TODO: delete this viewmodel and the screen. -@OptIn(SavedStateHandleSaveableApi::class) -class AppSelectorViewModel( - private val app: Application, - private val pm: PM, - fs: Filesystem, - patchBundleRepository: PatchBundleRepository, - savedStateHandle: SavedStateHandle, -) : ViewModel() { - private val inputFile = savedStateHandle.saveable(key = "inputFile") { - File( - fs.uiTempDir, - "input.apk" - ).also(File::delete) - } - - val filterTextFlow = MutableStateFlow("") - val filterText: StateFlow = filterTextFlow - - val apps = pm.appList.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = null, - ) - - val filteredApps = filterText - .combine(apps) { filter, apps -> - if (apps == null || filter.isBlank()) { - apps - } else { - apps.filter { app -> - app.packageName.contains(filter, true) || - loadLabel(app.packageInfo).contains(filter) - } - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed( - stopTimeoutMillis = 0, - replayExpirationMillis = 10_000, - ), - initialValue = null, - ) - - private val storageSelectionChannel = Channel() - val storageSelectionFlow = storageSelectionChannel.receiveAsFlow() - - val suggestedAppVersions = patchBundleRepository.suggestedVersions.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = emptyMap(), - ) - - var nonSuggestedVersionDialogSubject by mutableStateOf(null) - private set - - fun setFilterText(filter: String) { - filterTextFlow.value = filter - } - - fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" } - - fun dismissNonSuggestedVersionDialog() { - nonSuggestedVersionDialogSubject = null - } - - fun handleStorageResult(uri: Uri) = viewModelScope.launch { - val selectedApp = withContext(Dispatchers.IO) { - loadSelectedFile(uri) - } - - if (selectedApp == null) { - app.toast(app.getString(R.string.failed_to_load_apk)) - return@launch - } - - storageSelectionChannel.send(selectedApp) - } - - private fun loadSelectedFile(uri: Uri) = - app.contentResolver.openInputStream(uri)?.use { stream -> - with(inputFile) { - delete() - Files.copy(stream, toPath()) - - pm.getPackageInfo(this)?.let { packageInfo -> - SelectedApp.Local( - packageName = packageInfo.packageName, - version = packageInfo.versionName!!, - file = this, - temporary = true - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt new file mode 100644 index 000000000..7d0d4f0d9 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt @@ -0,0 +1,131 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import android.content.pm.PackageInfo +import android.net.Uri +import androidx.compose.runtime.mutableStateMapOf +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi +import androidx.lifecycle.viewmodel.compose.saveable +import app.revanced.manager.R +import app.revanced.manager.data.platform.Filesystem +import app.revanced.manager.data.room.apps.installed.InstallType +import app.revanced.manager.data.room.apps.installed.InstalledApp +import app.revanced.manager.domain.installer.RootInstaller +import app.revanced.manager.domain.installer.RootServiceException +import app.revanced.manager.domain.repository.InstalledAppRepository +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.util.PM +import app.revanced.manager.util.toast +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.nio.file.Files + +@OptIn(SavedStateHandleSaveableApi::class) +class AppsViewModel( + private val app: Application, + private val installedAppsRepository: InstalledAppRepository, + private val pm: PM, + private val rootInstaller: RootInstaller, + fs: Filesystem, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + + private val inputFile = savedStateHandle.saveable(key = "inputFile") { + File(fs.uiTempDir, "input.apk").also(File::delete) + } + + val filterTextFlow = MutableStateFlow("") + val filterText: StateFlow = filterTextFlow + + val patchableApps = pm.appList.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null, + ) + + val installedApps = installedAppsRepository.getAll().stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null, + ) + + val packageInfoMap = mutableStateMapOf() + + private val storageSelectionChannel = Channel() + val storageSelectionFlow = storageSelectionChannel.receiveAsFlow() + + init { + viewModelScope.launch { + installedApps.filterNotNull().collectLatest(::fetchPackageInfos) + } + } + + fun setFilterText(filter: String) { + filterTextFlow.value = filter + } + + fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" } + + fun handleStorageResult(uri: Uri) = viewModelScope.launch { + val selectedApp = withContext(Dispatchers.IO) { loadSelectedFile(uri) } + + if (selectedApp == null) { + app.toast(app.getString(R.string.failed_to_load_apk)) + return@launch + } + + storageSelectionChannel.send(selectedApp) + } + + private suspend fun fetchPackageInfos(apps: List) { + for (app in apps) { + packageInfoMap[app.currentPackageName] = withContext(Dispatchers.IO) { + if (app.installType == InstallType.MOUNT) { + try { + if (!rootInstaller.isAppInstalled(app.currentPackageName)) { + installedAppsRepository.delete(app) + return@withContext null + } + } catch (_: RootServiceException) { } + } + + val packageInfo = pm.getPackageInfo(app.currentPackageName) + + if (packageInfo == null && app.installType != InstallType.MOUNT) { + installedAppsRepository.delete(app) + return@withContext null + } + + packageInfo + } + } + } + + private fun loadSelectedFile(uri: Uri): SelectedApp.Local? = + app.contentResolver.openInputStream(uri)?.use { stream -> + inputFile.delete() + Files.copy(stream, inputFile.toPath()) + + pm.getPackageInfo(inputFile)?.let { packageInfo -> + SelectedApp.Local( + packageName = packageInfo.packageName, + version = packageInfo.versionName!!, + file = inputFile, + temporary = true, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt index 9825ee874..064650464 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/ChangelogsViewModel.kt @@ -1,60 +1,26 @@ package app.revanced.manager.ui.viewmodel -import android.app.Application -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 androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn import app.revanced.manager.domain.repository.ChangelogSource import app.revanced.manager.domain.repository.ChangelogsRepository -import app.revanced.manager.ui.component.ChangelogUiState -import app.revanced.manager.util.uiSafe -import kotlinx.coroutines.launch +import app.revanced.manager.network.api.ReVancedAPI +import app.revanced.manager.network.dto.ReVancedAssetHistory +import kotlinx.coroutines.flow.Flow class ChangelogsViewModel( - private val repository: ChangelogsRepository, - private val app: Application, + private val api: ReVancedAPI, private val source: ChangelogSource, ) : ViewModel() { - - var state: ChangelogUiState by mutableStateOf(ChangelogUiState.Loading) - private set - - private val pageSize = 2 - - init { - viewModelScope.launch { - uiSafe(app, R.string.changelog_download_fail, "Failed to download changelog") { - val result = repository.loadInitial(source, pageSize) - - state = ChangelogUiState.Success( - changelogs = result.items, - hasMore = result.hasMore - ) - } - - if (state is ChangelogUiState.Loading) { - state = ChangelogUiState.Error( - app.getString(R.string.changelog_download_fail) - ) - } - } - } - - fun loadNextPage() { - val current = state as? ChangelogUiState.Success ?: return - if (current.isLoadingMore || !current.hasMore) return - - state = current.copy(isLoadingMore = true) - - val result = repository.loadNext(pageSize) - - state = current.copy( - changelogs = current.changelogs + result.items, - isLoadingMore = false, - hasMore = result.hasMore - ) - } + val changelogs: Flow> = Pager( + config = PagingConfig( + pageSize = 10, + enablePlaceholders = false + ), + pagingSourceFactory = { ChangelogsRepository(api, source) } + ).flow.cachedIn(viewModelScope) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt deleted file mode 100644 index 7d2945cbd..000000000 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppsViewModel.kt +++ /dev/null @@ -1,65 +0,0 @@ -package app.revanced.manager.ui.viewmodel - -import android.content.pm.PackageInfo -import androidx.compose.runtime.mutableStateMapOf -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.revanced.manager.data.room.apps.installed.InstallType -import app.revanced.manager.data.room.apps.installed.InstalledApp -import app.revanced.manager.domain.installer.RootInstaller -import app.revanced.manager.domain.installer.RootServiceException -import app.revanced.manager.domain.repository.InstalledAppRepository -import app.revanced.manager.util.PM -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class InstalledAppsViewModel( - private val installedAppsRepository: InstalledAppRepository, - private val pm: PM, - private val rootInstaller: RootInstaller -) : ViewModel() { - val apps = installedAppsRepository.getAll() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = null, - ) - - val packageInfoMap = mutableStateMapOf() - - init { - viewModelScope.launch { - apps.filterNotNull().collectLatest(::fetchPackageInfos) - } - } - - private suspend fun fetchPackageInfos(apps: List) { - for (app in apps) { - packageInfoMap[app.currentPackageName] = withContext(Dispatchers.IO) { - try { - if (app.installType == InstallType.MOUNT && - !rootInstaller.isAppInstalled(app.currentPackageName) - ) { - installedAppsRepository.delete(app) - return@withContext null - } - } catch (_: RootServiceException) { - } - - val packageInfo = pm.getPackageInfo(app.currentPackageName) - - if (packageInfo == null && app.installType != InstallType.MOUNT) { - installedAppsRepository.delete(app) - return@withContext null - } - - packageInfo - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index 167de4b05..981143c30 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -369,8 +369,7 @@ class SelectedAppInfoViewModel( private fun invalidateSelectedAppInfo() = viewModelScope.launch { val info = when (val app = selectedApp) { is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) } - is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) } - else -> null + else -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) } } selectedAppInfo = info 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 index fb61e05f9..78f97cca1 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt @@ -10,6 +10,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn import app.revanced.manager.R import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.platform.NetworkInfo @@ -17,13 +21,14 @@ import app.revanced.manager.domain.repository.ChangelogSource import app.revanced.manager.domain.repository.ChangelogsRepository import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.dto.ReVancedAsset +import app.revanced.manager.network.dto.ReVancedAssetHistory import app.revanced.manager.network.service.HttpService -import app.revanced.manager.ui.component.ChangelogUiState 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.flow.Flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent @@ -37,7 +42,8 @@ import ru.solrudev.ackpine.session.await import ru.solrudev.ackpine.session.parameters.Confirmation class UpdateViewModel( - private val changelogsRepository: ChangelogsRepository, + private val api: ReVancedAPI, + private val source: ChangelogSource, private val downloadOnScreenEntry: Boolean ) : ViewModel(), KoinComponent { private val app: Application by inject() @@ -66,8 +72,13 @@ class UpdateViewModel( var releaseInfo: ReVancedAsset? by mutableStateOf(null) private set - var changelogsState: ChangelogUiState by mutableStateOf(ChangelogUiState.Loading) - private val changelogsPageSize = 2 + val changelogs: Flow> = Pager( + config = PagingConfig( + pageSize = 10, + enablePlaceholders = false + ), + pagingSourceFactory = { ChangelogsRepository(api, source) } + ).flow.cachedIn(viewModelScope) private val location = fs.tempDir.resolve("updater.apk") @@ -77,44 +88,14 @@ class UpdateViewModel( releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available") - val result = changelogsRepository.loadInitial( - ChangelogSource.Manager, - changelogsPageSize - ) - - changelogsState = ChangelogUiState.Success( - changelogs = result.items, - hasMore = result.hasMore - ) - if (downloadOnScreenEntry) { downloadUpdate() } else { state = State.CAN_DOWNLOAD } } - - if (changelogsState is ChangelogUiState.Loading) { - changelogsState = ChangelogUiState.Error( - app.getString(R.string.changelog_download_fail) - ) - } } } - fun loadNextPage() { - val current = changelogsState as? ChangelogUiState.Success ?: return - if (current.isLoadingMore || !current.hasMore) return - - changelogsState = current.copy(isLoadingMore = true) - - val result = changelogsRepository.loadNext(changelogsPageSize) - - changelogsState = current.copy( - changelogs = current.changelogs + result.items, - isLoadingMore = false, - hasMore = result.hasMore - ) - } fun downloadUpdate(ignoreInternetCheck: Boolean = false) = viewModelScope.launch { uiSafe(app, R.string.failed_to_download_update, "Failed to download update") { diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt index 6d3b7852d..e4e370313 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -4,18 +4,19 @@ import android.annotation.SuppressLint import android.app.Application import android.content.Intent import android.content.pm.PackageInfo -import android.content.pm.PackageManager.PackageInfoFlags import android.content.pm.PackageManager.NameNotFoundException -import androidx.core.content.pm.PackageInfoCompat +import android.content.pm.PackageManager.PackageInfoFlags import android.os.Build import android.os.Parcelable import androidx.compose.runtime.Immutable +import androidx.core.content.pm.PackageInfoCompat +import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import ru.solrudev.ackpine.session.await @@ -37,11 +38,15 @@ data class AppInfo( class PM( private val app: Application, patchBundleRepository: PatchBundleRepository, + prefs: PreferencesManager, private val uninstaller: PackageUninstaller ) { private val scope = CoroutineScope(Dispatchers.IO) - val appList = patchBundleRepository.bundleInfoFlow.map { bundles -> + val appList = combine( + patchBundleRepository.bundleInfoFlow, + prefs.disableUniversalPatchCheck.flow + ) { bundles, showInstalled -> val compatibleApps = scope.async { val compatiblePackages = bundles .flatMap { (_, bundle) -> bundle.patches } @@ -64,28 +69,32 @@ class PM( } } - val installedApps = scope.async { - getInstalledPackages().map { packageInfo -> - AppInfo( - packageInfo.packageName, - 0, - packageInfo - ) + val installedApps = if (showInstalled) { + scope.async { + getInstalledPackages().map { packageInfo -> + AppInfo(packageInfo.packageName, 0, packageInfo) + } } - } + } else null - if (compatibleApps.await().isNotEmpty()) { - (compatibleApps.await() + installedApps.await()) - .distinctBy { it.packageName } - .sortedWith( - compareByDescending { - it.packageInfo != null && (it.patches ?: 0) > 0 - }.thenByDescending { - it.patches - }.thenBy { - it.packageInfo?.label() - }.thenBy { it.packageName } - ) + val compatible = compatibleApps.await() + + if (compatible.isNotEmpty()) { + val base = if (installedApps != null) { + (compatible + installedApps.await()).distinctBy { it.packageName } + } else { + compatible + } + + base.sortedWith( + compareByDescending { + it.packageInfo != null && (it.patches ?: 0) > 0 + }.thenByDescending { + it.patches + }.thenBy { + it.packageInfo?.label() + }.thenBy { it.packageName } + ) } else { emptyList() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 47d57eb3b..553548513 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,636 +1,592 @@ - - - ReVanced Manager - Patcher - Patches - CLI - Manager - - Welcome to - Hi! It\'s the new - You can select an app to patch now or do it later - Could not load patches because the network is unavailable or unstable. Connect to a stable network and try again. - Configure automatic updates to keep ReVanced Manager and patches up to date - - Patches and downloaders could not be downloaded during setup. Tap update to download them. - Patches or downloaders have updates, but were not downloaded because the network is metered. Tap update to download them. - ReVanced Manager will connect to %s in order to download initial versions if your device is connected to the internet. - - Retry - Skip for now - Recommended version: %s - - Skip permissions? - Without the required permissions, some features may not work correctly. You can grant them later in app settings. - Skip anyway - - ReVanced Manager needs a few permissions to work properly - Install unknown apps - Required to install patched applications - Notifications - Allows for uninterrupted patching in the background - Battery optimization - Prevents patching from being interrupted in the background - Grant - - ReVanced Manager downloader host - Used to control access to ReVanced Manager downloaders. Only ReVanced Manager has this. - - Copied! - Copy to clipboard - - Dashboard - Settings - Select an app - %1$d/%2$d selected - - Patch an app - Add patches - - New downloader(s) available. Click here to configure them. - Patching on this device architecture is unsupported and will most likely fail. - Device not supported - Your device\'s architecture is not supported by ReVanced Manager. Patching will not work on this device. - - Import - Import patches - Selected - Not selected - - Not set - - Missing - Error - Patches could not be loaded. Click to view the error - Patches has not been downloaded. - Patches - Unnamed - - Android 11 bug - The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that will negatively affect the user experience. - - Any available version - Select source - Auto - Use installed app, then downloaded APK, then available downloaders - No compatible found app or downloaders available - Mounted apps can\'t be patched again without root access - Version %s does not match the suggested version - - Start patching the application - Select patches - %d patches selected - Selection of patches has been changed - No patches selected - - You are currently on a metered connection. Data charges from your service provider may apply. - - Select APK source - Auto - Auto · Using installed APK - Auto · Using downloaded APK - Auto · Using available downloaders - Auto · Select from storage - Using %s - Using installed APK - Using a local APK file - Already downloaded - - Could not import legacy settings - - - Configure updates - Do you want ReVanced Manager to periodically check for updates for the following components? - ReVanced Manager - ReVanced Patches - APK Downloaders - These settings can be changed later. - - ReVanced Manager will connect to %s in order to download initial versions if your device is connected to the internet. - - Filter by tag - Show archived - - General - Language, theme, dynamic color - Updates - Check for updates and view changelog - Downloads - Downloaders and downloaded apps - Import & export - Keystore, patch options and selection - Advanced - API URL, memory limit, debugging - Safeguards have been toggled - About - About %1$s - Open source licenses - View all the libraries used to make this application - - Contributors - View the contributors of ReVanced - Dynamic color - Adapt colors to the wallpaper - Pure black theme - Use pure black backgrounds for dark theme - Theme - Choose between light or dark theme - Language - Choose the app display language - System default - Search languages… - - Safeguards - Disable version compatibility check - Do not restrict patches to compatible app versions - Disable version compatibility check? - Selecting incompatible patches can result in a broken app. - -Do you want to proceed anyways? - Require suggested app version - Enforce selection of the suggested app version - Disable requiring suggested app version? - Selecting an app that is not the suggested version may cause unexpected issues. - -Do you want to proceed anyways? - Allow changing patch selection and options - Do not prevent selecting or deselecting patches and customization of options - Changing patch selection and options? - Changing the selection of patches may cause unexpected issues. - -Enable anyways? - Allow using universal patches - Do not prevent using universal patches - Use universal patches? - Universal patches are not as well tested as those that target specific apps. - -Enable anyways? - Keystore - Patches selections - Import keystore - Import a custom keystore - Enter keystore credentials - You\'ll need enter the keystore’s credentials to import it. - Username (Alias) - Password - Import - Wrong keystore credentials - Imported keystore - Export keystore - Export the current keystore - No keystore to export - Exported keystore - Regenerate keystore - Generate a new keystore - You are about to regenerate your keystore the manager will use during the patching process. - -You will not be able to update the previously installed apps from this source. - The keystore has been successfully replaced - Import patch selection - Import patch selection from a JSON file - Could not import patch selection: %s - Imported patch selection - Select bundle to import into - Export patch selection - Export patch selection to a JSON file - Could not export patch selection: %s - Exported patch selection - Select bundle to export from - Apps with saved selections - Total selected patches - Reset configuration - Patch selections - Reset the stored patch selection - Patch options - Reset the stored patch options - Patch selection has been reset - Reset patch selection globally - You are about to reset all patch selections. You will need to manually select each patch again. - Resets all patch selections - Reset patch selection for app - You are about to reset the patch selection for the app \"%s\". You will have to manually select each patch again. - Resets patch selection for a single app - Reset patch selection (single) - You are about to reset the patch selection for \"%s\". You will have to manually select each patch again. - Resets the patch selection for a specific collection of patches - Reset patch options for app - You are about to reset the patch options for the app \"%s\". You will have to reapply each option again. - Resets patch options for a single app - Reset patch options (single) - You are about to reset the patch options for \"%s\". You will have to reapply each option again. - Resets the patch options for a specific collection of patches - Reset patch options globally - You are about to reset all patch options. You will have to reapply each option again. - Resets all patch options - Downloaders - Use pre-releases - Use pre-release versions of the main downloader - Loaded - Failed to load. Click for more details - Missing - Delete selected apps - Are you sure you want to delete the selected apps? - Are you sure you want to delete \"%s\"? - No downloaded apps found. - Apps downloaded through ReVanced Manager will appear here. - Downloader URL - Add downloader - Add new downloaders from URL or local files - Add downloaders from local storage. - Add downloaders from URL. Can be automatically updated. - Install downloaders to get apps directly within ReVanced Manager. - Install ReVanced Downloaders - Cancel downloaders install? - A downloaders install is in progress. If you leave now, the download will be cancelled. - These downloaders are missing - Click on the update button to fix it. - - Failed to update downloader: %s - Failed to import downloader: %s - - Search apps… - Loading… - Downloading patches… - - Options - OK - Yes - No - Edit - Value - Reset - Share - Patch - Select from storage - Select an APK file from storage using file picker - Please use alternative method!, we couldn\'t find your file picker app! - Suggested version: %s - Type anything to continue - Search patches… - Apply - Help - Back - Warning - Add - Enable - Disable - Close - Clear - System - Light - Dark - Appearance - Networking - Allow metered networks - Permits automatic updates on metered networks. - The application might still warn about metered networks for manual operations. - Downloaded apps - Run Patcher in another process (experimental) - This is faster and allows Patcher to use more memory - Patcher process memory limit - The max amount of memory that the Patcher process can use - Export debug logs - Failed to read logs (exit code %d) - Failed to export logs - Exported logs - API URL - The API used to download necessary files - Change API URL - Change the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates. - ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it. - Set - Reset API URL - Device - Android version - Model - CPU Architectures - Memory limits - %1$dMB (Normal) - %2$dMB (Large) - Force download all patches - Reset patches - Reset downloaders - Reset onboarding - Show the onboarding screen on next app launch - Reset announcement read - Forget that announcements have been read - Patching - Signing - Storage - No patch can be found. Check your patches - Apps - Patches - Delete - Refresh - Continue anyways - Download another version - Download app - Download APK file - Failed to download patches: %s - API service is currently down - Some features might be impacted. Check your connection or API URL in settings. - Failed to import patches: %s - No patched apps found - You currently don\'t have any patched apps that we know of. Change that by patching your first app! - Patched apps - Apps that can be patched - No patches found - You don\'t have any patches yet. Add patches by tapping the button below! - Tap on the patches to get more information about them - %s selected - Incompatible patches - Universal patches - Patch selection and options has been reset to recommended defaults - Patch options have been reset - Non suggested version - The version of the app you have selected does not match the suggested version. -Please use the suggested version: %s - -To continue anyway, disable \"Require suggested app version\" in the advanced settings. - Stop using defaults? - It is recommended to use the default patch selection and options. Changing them may result in unexpected issues. - -You need to turn on \"Allow changing patch selection and options\" in the advanced settings before toggling patches. - Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them. - -You need to turn on \"Allow using universal patches\" in the advanced settings before using universal patches. - This version - Any app - Search patches - This patch is not compatible with the selected app version (%1$s) - -It is only compatible with the following version(s): %2$s - Continue with this version? - Not all patches are compatible with this version (%s). Do you want to continue anyway? - Download application? - The app you selected isn\'t installed. Do you want to download it? - The app you selected has the wrong package name! - Failed to load APK - Split APKs cannot be patched - Loading… - Not installed - Installed - - App info - Uninstall - Unpatch - Repatch - Installation type - Package name - Original package name - Applied patches - View applied patches - - Default - Mount - Mounted - Not mounted - Mount - Unmount - Failed to mount: %s - Failed to unmount: %s - Unpatch app? - Are you sure you want to unpatch this app? - - Downloader did not fetch the correct version - Downloader did not find the app - Downloader error: %s - No app downloaders are installed! - No app downloaders have been trusted! Please check your settings. - Already patched - - Filter - Compatibility - Packages - - Actions - Restore default selection - Deselect all - Invert selection - Deselect all except %s - Apply to - All patches - %s only - - More options - Custom value - - Select from storage - Previous directory - Directories - Files - - Show password - Hide password - - Installer - Install - App installed - Failed to install app: %s - Failed to reinstall app: %s - Failed to uninstall app: %s - Open - Save APK - APK Saved - Failed to sign APK: %s - Save logs - Save to files - Export patcher logs - Logs saved - User interaction is required in order to proceed with this downloader. - Select installation type - - Preparing - Load patches - Prepare patcher - Patching - Saving - Write patched APK file - Sign patched APK file - Patching in progress… - Tap to return to the patcher - Stop patcher - Are you sure you want to stop the patching process? - Installation is in progress. Please wait - Execute patches - Execute %s - Failed to execute %s - - completed - failed - running - waiting - - expand - collapse - reorder - - More - Less - Continue - Dismiss - View announcement - Do not show this again - Donate - Website - GitHub - Contact - License - Source - Repository - By %1$s - Version - Selected version may be incompatible with selected patches - Submit issue or feedback - Help us improve this application - Developer options - Options for debugging issues - Update successful - No update available - View patches - Any version - Any package - Are you sure you want to delete \"%s\"? - Are you sure you want to delete the selected patches? - - Announcements - Archive - About ReVanced Manager - ReVanced Manager is an Android application that uses ReVanced Patcher to patch Android apps. It allows you to download and patch apps with custom patches, and manage the patching process. - %d taps remaining - Developer options enabled - Developer options are already enabled - An update is available - Current version: %s - New version: %s - Ready to install update - Update installed - Failed to install update - Check for updates - View update - Manually check for updates - Check for updates on launch - Check for new versions of ReVanced Manager when the application starts - Check for ReVanced Downloaders updates on launch - Check for updates to installed ReVanced Downloaders when the application starts - Use pre-releases - Use pre-release versions of ReVanced Manager - Use pre-releases? - Pre-release versions may be unstable and contain bugs. You may experience crashes, data loss, or other unexpected issues. Only enable this if you are comfortable with these risks. - View changelog - Loading changelog - Failed to download changelog: %s - Check out the latest changes in this update - Battery optimizations must be turned off in order for ReVanced Manager to work correctly in the background. Click here to turn off optimizations. - Installing update… - Downloading update… - Failed to download update: %s - Cancel - Save - Save (%1$s) - Update - Empty - Tap on Update when prompted. -ReVanced Manager will close when updating. - No changelogs found - Just now - %sm ago - %sh ago - %sd ago - Invalid date - Disable battery optimization - Invalid value - This option is required - Required options - - Failed to check for updates: %s - No update available - No announcements found - Checking for updates… - Not now - A new version of ReVanced Manager (%s) is available. - Failed to download update: %s - Download - You are currently on a metered connection, and data charges from your service provider may apply. - -Do you still want to continue? - Download update? - No contributors found - Select - Select or deselect all - Add new patches from URL or local files - Add patches from local storage. - Add patches from URL. Patches can automatically update. - Recommended - - Installation failed - Installation cancelled - Installation blocked - Installation conflict - Installation incompatible - Installation invalid - Not enough storage - Installation timed out - The installation failed due to an unknown reason. Try again? - The installation was cancelled manually. Try again? - The installation was blocked. Review your device security settings and try again. - The installation was prevented by an existing installation of the app. Uninstall the installed app and try again? - The app is incompatible with this device. Use an APK that is compatible by this device and try again. - The app is invalid. Uninstall the app and try again? - The app could not be installed due to insufficient storage. Free up some space and try again. - The installation took too long. Try again? - Reinstall - Show - Debugging - About device - Enter URL - Next - Auto update - Add patches - Automatically update when a new version is available - Use pre-releases - Use pre-release versions of %s - Patches URL - These patches are not compatible with the selected app version (%1$s). - -Click on the patches to see more details. - Incompatible patch - Any - Never show again - Show update message on launch - Show a popup notification whenever a new update is available on launch - Failed to import keystore - Export - Confirm - New announcement:\n%s - - Required - Optional - - Restart to see changes - Network is unstable or unavailable! - - - %d patch - %d patches - - - Execute %d patch - Execute %d patches - - - %d selected - - + + + ReVanced Manager + Patcher + Patches + CLI + Manager + + Welcome to + Hi! It’s the new + You can select an app to patch now or do it later + Patches can’t be downloaded. Check your internet connection and try again. + Configure automatic updates to keep ReVanced Manager and patches up to date + + Patches and downloaders couldn’t be downloaded during setup. Tap Update to download them. + Patches and downloaders can’t be updated on a metered network. Tap Update to download them. + ReVanced Manager will connect to %s in order to download initial versions if your device is connected to the internet + + Retry + Try again + Skip for now + Recommended version: %s + + Skip permissions? + Without the required permissions, some features may not work correctly. You can grant them later in settings. + Skip anyway + + ReVanced Manager needs a few permissions to work properly + Install unknown apps + Required to install patched applications + Notifications + Allows for uninterrupted patching in the background + Battery optimization + Prevents patching from being interrupted in the background + Grant + + ReVanced Manager downloader host + Used to control access to ReVanced Manager downloaders. Only ReVanced Manager has this permission. + + Copied + Copy to clipboard + + Dashboard + Settings + Select an app + %1$d/%2$d selected + + Patch an app + Add patches + + New downloaders available, tap to configure them + Device not supported + ReVanced Manager doesn’t support this device. You won’t be able to patch apps. + + Import + Import patches + Selected + Not selected + + Not set + + Missing + Error + Couldn’t load patches, tap for details + Couldn’t download patches + Patches + Unnamed + + Android 11 bug + The app installation permission must be granted ahead of time to avoid a bug in the Android 11 system that would negatively affect your experience + + Any available version + Select source + Auto + Use installed app, then downloaded APK, then available downloaders + No compatible app or downloaders available + Mounted apps can’t be patched again without root access + Version %s doesn’t match the suggested version + + Select patches + %d patches selected + Patch selection has been changed + No patches selected + + You are currently on a metered connection. Data charges from your service provider may apply. + + Select APK source + Auto + Auto • Using installed APK + Auto • Using downloaded APK + Auto • Using available downloaders + Auto • Select from storage + Using %s + Using installed APK + Using an APK file + Already downloaded + + Couldn’t import legacy settings + + + Configure updates + Do you want ReVanced Manager to periodically check for updates for the following components? + ReVanced Manager + ReVanced Patches + APK Downloaders + These settings can be changed later. + + ReVanced Manager will connect to %s in order to download initial versions if you are connected to the internet. + + Filter by tag + Archived + + General + Language, theme, dynamic color + Updates + Check for updates and view changelog + Downloads + Downloaders and downloaded apps + Import & export + Keystore, patch options and selection + Advanced + API URL, memory limit, debugging + Safeguards have been toggled + About + About %1$s + Open source licenses + View all the libraries used to make this application + + Contributors + View ReVanced contributors + Dynamic color + Use colors provided by your device + Pure black theme + Use pure black backgrounds for dark theme + Theme + Choose between light or dark theme + Language + Choose the app display language + System default + Search languages + + Safeguards + Turn off version compatibility check + Disables checking patch/app version compatibility + Turn off version compatibility check? + Selecting incompatible patches may cause unexpected issues + Require suggested app version + Enforces selection of the suggested app version + Stop requiring suggested app version? + Selecting an app that isn’t the suggested version may cause unexpected issues + Allow changing patch selection and options + Allows selecting or deselecting patches, and changing patch options + Allow changing patch selection and options? + Changing selection of patches and options may cause unexpected issues + Allow using universal patches + Allows the use of general-purpose patches not made for specific apps + Use universal patches? + Universal patches aren’t as well tested as app-specific patches. You may face issues using them. + Keystore + Patch selections + Import keystore + Import a custom keystore + Enter keystore credentials + You must enter credentials before importing the keystore. + Username (Alias) + Password + Import + Incorrect credentials + Keystore imported + Export keystore + Export the current keystore + No keystore to export + Keystore exported + Regenerate keystore + Generate a new keystore + You’re about to regenerate your keystore, used during the patching process. + +You won’t be able to update apps that were signed with the previous keystore. + Keystore regenerated + Couldn’t import patch selection: %s + Imported patch selection + Select bundle to import into + Couldn’t export patch selection: %s + Exported patch selection + Select bundle to export from + Apps with saved selections + Total selected patches + Reset configuration + Patch selections + Patch options + Patch selection reset + Downloaders + Use pre-releases + Use pre-release versions of the main downloader + Loaded + Failed to load, tap for details + Missing + Delete selected apps + The apps you selected will be deleted + %s will be deleted + No downloaded apps found + Apps downloaded through ReVanced Manager will appear here + Downloader URL + Add downloader + Add new downloaders from a URL or local files + Add downloader from local storage + Downloader can receive updates + These downloaders are missing + Tap Update to fix this issue + + Failed to update downloader: %s + Failed to import downloader: %s + + Search apps + Loading… + Downloading patches… + + Options + OK + Yes + No + Edit + Value + Reset + Share + Patch + Select from storage + Select an APK file using file picker + Couldn’t open file picker, please use an alternative method + Suggested version: %s + Type anything to continue + Search patches + Apply + Help + Back + Warning + Add + Enable + Disable + Close + Clear + System + Light + Dark + Appearance + Networking + Allow metered networks + Allow automatic updates on metered networks. + You may still be warned before performing manual operations. + Downloaded apps + Run Patcher in another process (experimental) + Faster, and allows Patcher to use more memory + Patcher process memory limit + The max amount of memory that the Patcher process can use + Export debug logs + Failed to read logs (exit code %d) + Failed to export logs + Exported logs + API URL + The API used to download necessary files + Change API URL + ReVanced Manager uses the API to download patches and updates + ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it. + Set + Reset API URL + Device + Android version + Model + CPU architectures + Memory limits + %1$dMB (Normal) – %2$dMB (Large) + Force download all patches + Reset patches + Reset downloaders + Reset onboarding + Show the onboarding screen on next app launch + Reset announcement read + Forget that announcements have been read + Patching + Signing + Storage + Couldn’t find any patches + Apps + Patches + Delete + Refresh + Continue anyway + Download another version + Download app + Download APK file + Failed to download patches: %s + API service is currently down + Some features may be unavailable. Check your internet connection or API URL in settings. + Couldn’t import patches: %s + No patched apps found + You don’t have any patched apps yet. Start by patching your first app! + Patched apps + Apps that can be patched + No patches found + You don’t have any patches yet. Add patches by tapping the button below! + Tap on the patches to get more information about them + %s selected + Incompatible patches + Universal patches + Patch selection and options have been reset to recommended defaults + Patch options have been reset + Non suggested version + The version of the app you selected doesn’t match the suggested version: %s + +To continue anyway, turn off “Require suggested app version” in Advanced settings. + Stop using defaults? + We recommend using the default patch selection and options, as changing them may cause unexpected issues. + +To continue anyway, turn on “Allow changing patch selection and options” in Advanced settings. + Universal patches are more general-purpose, and may not be as reliable as app-specific patches. You may face issues using them. + +To continue anyway, turn on “Allow using universal patches” in Advanced settings. + This version + Any app + Search patches + This patch isn’t compatible with the version of the app you selected: %1$s + +It’s only compatible with these versions: %2$s + Continue with this version? + Not all patches are compatible with this version: %s + Download app? + The app you selected isn’t installed. Do you want to download it? + Selected app has the wrong package name + Couldn’t load the APK file + Split APKs aren’t supported + Loading… + Not installed + Installed + + App info + Uninstall + Unpatch + Repatch + Install mode + Package name + Original package name + Applied patches + View applied patches + + Default + Mount + Mounted + Not mounted + Mount + Unmount + Failed to mount: %s + Failed to unmount: %s + Unpatch app? + All patches will be removed, and the original app will be restored + + Downloader couldn’t get the correct version + Downloader couldn’t find the app + Downloader error: %s + No downloaders are installed + No downloaders have been trusted. Check your settings. + + Filter + Compatibility + Packages + + Actions + Restore default selection + Deselect all + Invert selection + Deselect all except %s + Apply to + All patches + %s only + + More options + Custom value + + Select from storage + Previous directory + Directories + Files + + Show password + Hide password + + Installer + Install + App installed + Failed to install app: %s + Failed to reinstall app: %s + Failed to uninstall app: %s + Open + Save APK + Saved APK + Failed to sign APK: %s + Save logs + Save to files + Export patcher logs + Logs saved + You need to interact with this downloader in order to continue + Install mode + + Preparing + Load patches + Prepare patcher + Patching + Saving + Write patched APK file + Sign patched APK file + Patching in progress + Tap to return to the patcher + Stop patcher? + The patching process will be stopped + Installing, please wait… + Execute patches + Execute %s + Couldn’t execute %s + + completed + failed + running + waiting + + expand + collapse + reorder + + More + Less + Continue + Dismiss + View + Don’t show this again + Donate + Website + GitHub + Contact + License + Source + Repository + By %1$s + Version + Selected version may be incompatible with selected patches + Submit issue or feedback + Help us improve this application + Developer options + Options for debugging issues + Patches updated + No updates available + View patches + Any version + Any package + “%s” will be deleted + Selected patches will be deleted + + Announcements + Archive + About ReVanced Manager + ReVanced Manager is an Android application that uses ReVanced Patcher, allowing you to download and apply patches to your favorite apps + %d more taps + Enabled developer options + Developer options are already enabled + Update available + Current version: %s + New version: %s + Ready to install update + Update installed + Couldn’t install update + Check for updates + View update + Manually check for updates + Check for updates on launch + Automatically check for new versions of ReVanced Manager when you open the app + Check for ReVanced downloaders updates on launch + Automatically check for ReVanced downloaders updates when you open the app + Use pre-releases + Use pre-release versions of ReVanced Manager + Use pre-releases? + Pre-release versions may be unstable and contain bugs. You may experience crashes, data loss, or other unexpected issues. + View changelog + Loading changelog… + Couldn’t download changelog: %s + Battery optimizations must be turned off in order for ReVanced Manager to work correctly in the background + Installing update… + Downloading update… + Couldn’t download update: %s + Cancel + Save + Save (%1$s) + Update + Empty + ReVanced Manager will close when updating + No changelogs found + Just now + %sm ago + %sh ago + %sd ago + Invalid date + Invalid value + This option is required + Required options + + Couldn’t check for updates: %s + No updates available + No announcements found + Checking for updates… + Not now + Install ReVanced Manager %s for the latest features and bug fixes + Failed to download update: %s + Download + You are currently on a metered connection. Data charges from your service provider may apply. + Download update? + No contributors found + Select + Select or deselect all + Add new patches from a URL or local files + Add patches from local storage + Patches can receive updates + Recommended + + Install failed + Install canceled + Install blocked + Install conflict + Incompatible app + Install invalid + Not enough storage + Timed out + There was a problem installing the app + Installation was canceled + Installation was blocked. Try adjusting the security settings of your device, and try again. + An existing app is preventing your installation. Uninstall the app, and try again. + This app isn’t compatible with your device. Use an APK that is compatible, and try again. + There was a problem installing the app. Uninstall the app, and try again. + There’s not enough storage space to install this app. Free up some space, and try again. + The installation took too long + Reinstall + Show + Debugging + About device + Enter URL + Next + Auto update + Add patches + Automatically update when a new version is available + Use pre-releases + Use pre-release versions of %s + Patches URL + These patches aren’t compatible with the selected app version: %1$s + +Tap them for more details. + Incompatible patch + Any + Don’t show this again + Show update message on launch + Get notified when you open the app and a new update is available + Couldn’t import keystore + Export + Confirm + New announcement + + Required + Optional + + Restart the app to see changes + You’re offline. Check your internet connection. + + + %d patch + %d patches + + + Execute %d patch + Execute %d patches + + + %d selected + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b562c45f5..2737b623b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,6 +40,7 @@ binary-compatibility-validator = "0.18.1" semver-parser = "3.0.0" ackpine = "0.20.6" foundation-layout = "1.10.5" +paging3 = "3.4.2" [libraries] # AndroidX Core @@ -62,6 +63,7 @@ compose-livedata = { group = "androidx.compose.runtime", name = "runtime-livedat compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } +paging3 = { group = "androidx.paging", name = "paging-compose", version.ref = "paging3" } # Coil coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }