mirror of
https://github.com/ReVanced/revanced-manager
synced 2026-04-25 17:15:36 +02:00
chore: Merge branch dev to main (#3161)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 2.3.0
|
||||
version = 2.4.0-dev.2
|
||||
|
||||
@@ -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<Dashboard> {
|
||||
DashboardScreen(
|
||||
onSettingsClick = { navController.navigateSafe(Settings) },
|
||||
onAppSelectorClick = {
|
||||
navController.navigateSafe(AppSelector)
|
||||
},
|
||||
onUpdateClick = {
|
||||
navController.navigateSafe(Update())
|
||||
},
|
||||
@@ -215,14 +210,6 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
)
|
||||
}
|
||||
|
||||
composable<AppSelector> {
|
||||
AppSelectorScreen(
|
||||
onSelect = vm::selectApp,
|
||||
onStorageSelect = vm::selectApp,
|
||||
onBackClick = navController::popBackStackSafe
|
||||
)
|
||||
}
|
||||
|
||||
composable<Patcher> {
|
||||
PatcherScreen(
|
||||
onBackClick = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<ReVancedAssetHistory> = emptyList()
|
||||
private var page = 0
|
||||
private val api: ReVancedAPI,
|
||||
private val source: ChangelogSource,
|
||||
) : PagingSource<Int, ReVancedAssetHistory>() {
|
||||
|
||||
suspend fun loadInitial(
|
||||
source: ChangelogSource,
|
||||
pageSize: Int
|
||||
): PageResult<ReVancedAssetHistory> {
|
||||
all = when (source) {
|
||||
is ChangelogSource.Manager ->
|
||||
api.getAppHistory().getOrThrow()
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ReVancedAssetHistory> {
|
||||
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<ReVancedAssetHistory> {
|
||||
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<T>(
|
||||
val items: List<T>,
|
||||
val hasMore: Boolean
|
||||
)
|
||||
override fun getRefreshKey(state: PagingState<Int, ReVancedAssetHistory>): Int? = null
|
||||
}
|
||||
@@ -49,12 +49,12 @@ class ReVancedAPI(
|
||||
suspend fun getLatestAppInfo() =
|
||||
request<ReVancedAsset>("manager${prefs.useManagerPrereleases.prereleaseString()}")
|
||||
|
||||
suspend fun getAppHistory() = request<List<ReVancedAssetHistory>>("manager/history")
|
||||
suspend fun getAppHistory() = request<List<ReVancedAssetHistory>>("manager/history${prefs.useManagerPrereleases.prereleaseString()}")
|
||||
|
||||
suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches${prefs.usePatchesPrereleases.prereleaseString()}")
|
||||
|
||||
suspend fun getPatchesHistory(apiUrl: String) =
|
||||
request<List<ReVancedAssetHistory>>(apiUrl, defaultApiVersion, "patches/history")
|
||||
suspend fun getPatchesHistory(apiUrl: String, prerelease: Boolean) =
|
||||
request<List<ReVancedAssetHistory>>(apiUrl, defaultApiVersion, "patches/history${prerelease.prereleaseString()}")
|
||||
|
||||
suspend fun getDownloaderUpdate() = request<ReVancedAsset>("manager/downloaders${prefs.useDownloaderPrerelease.prereleaseString()}")
|
||||
|
||||
@@ -64,5 +64,6 @@ class ReVancedAPI(
|
||||
|
||||
private companion object {
|
||||
suspend fun Preference<Boolean>.prereleaseString() = if (get()) "/prerelease" else ""
|
||||
fun Boolean.prereleaseString() = if (this) "/prerelease" else ""
|
||||
}
|
||||
}
|
||||
@@ -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<ReVancedAssetHistory>,
|
||||
val hasMore: Boolean = false,
|
||||
val isLoadingMore: Boolean = false,
|
||||
) : ChangelogUiState
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangelogList(
|
||||
state: ChangelogUiState,
|
||||
onLoadMore: () -> Unit,
|
||||
changelogs: LazyPagingItems<ReVancedAssetHistory>,
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -18,9 +18,6 @@ object Onboarding
|
||||
@Serializable
|
||||
object Dashboard
|
||||
|
||||
@Serializable
|
||||
object AppSelector
|
||||
|
||||
@Serializable
|
||||
data class InstalledApplicationInfo(val packageName: String)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<InstalledApp>?): Set<String> =
|
||||
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(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<String?>(null) }
|
||||
var pendingStorageSelection by rememberSaveable { mutableStateOf<SelectedApp.Local?>(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) }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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<String> = 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<SelectedApp.Local>()
|
||||
val storageSelectionFlow = storageSelectionChannel.receiveAsFlow()
|
||||
|
||||
val suggestedAppVersions = patchBundleRepository.suggestedVersions.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String> = 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<String, PackageInfo?>()
|
||||
|
||||
private val storageSelectionChannel = Channel<SelectedApp.Local>()
|
||||
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<InstalledApp>) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<PagingData<ReVancedAssetHistory>> = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 10,
|
||||
enablePlaceholders = false
|
||||
),
|
||||
pagingSourceFactory = { ChangelogsRepository(api, source) }
|
||||
).flow.cachedIn(viewModelScope)
|
||||
}
|
||||
@@ -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<String, PackageInfo?>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
apps.filterNotNull().collectLatest(::fetchPackageInfos)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchPackageInfos(apps: List<InstalledApp>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<PagingData<ReVancedAssetHistory>> = 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") {
|
||||
|
||||
@@ -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<AppInfo> {
|
||||
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<AppInfo> {
|
||||
it.packageInfo != null && (it.patches ?: 0) > 0
|
||||
}.thenByDescending {
|
||||
it.patches
|
||||
}.thenBy {
|
||||
it.packageInfo?.label()
|
||||
}.thenBy { it.packageName }
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user