mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2025-05-20 07:56:47 +08:00
Compare commits
10 Commits
settings-r
...
compose/fi
Author | SHA1 | Date | |
---|---|---|---|
c7b86178c8 | |||
c4ef7d3b07 | |||
1231bc106c | |||
1982ac27e2 | |||
be4066bbf6 | |||
536a24169c | |||
2f1426408a | |||
00c61b6adc | |||
625abd72b0 | |||
0930b9fda7 |
8
.github/workflows/release-build.yml
vendored
8
.github/workflows/release-build.yml
vendored
@ -9,9 +9,6 @@ jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set env
|
||||
@ -44,11 +41,6 @@ jobs:
|
||||
- name: Add version to APK
|
||||
run: mv ${{ steps.sign_apk.outputs.signedFile }} revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-path: revanced-manager-${{ env.RELEASE_VERSION }}.apk
|
||||
|
||||
- name: Publish release APK
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,4 +9,3 @@
|
||||
.cxx
|
||||
local.properties
|
||||
|
||||
.kotlin/
|
||||
|
15
SECURITY.md
15
SECURITY.md
@ -13,8 +13,8 @@
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo-round.svg" />
|
||||
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
@ -70,8 +70,9 @@ If a vulnerability is confirmed and accepted, you can join our [Discord](https:/
|
||||
|
||||
### ⏳ Supported Versions
|
||||
|
||||
| Version | Branch | Supported |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------ |
|
||||
|  | main | :white_check_mark: |
|
||||
|  | dev | :white_check_mark: |
|
||||
|  | compose-dev | :white_check_mark: |
|
||||
| Version | Branch | Supported |
|
||||
| ------- | ------------|------------------- |
|
||||
| v1.18.0 | main | :white_check_mark: |
|
||||
| latest | dev | :white_check_mark: |
|
||||
| latest | compose-dev | :white_check_mark: |
|
||||
|
||||
|
@ -13,7 +13,7 @@ plugins {
|
||||
android {
|
||||
namespace = "app.revanced.manager"
|
||||
compileSdk = 35
|
||||
buildToolsVersion = "35.0.1"
|
||||
buildToolsVersion = "35.0.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "app.revanced.manager"
|
||||
@ -28,7 +28,6 @@ android {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager (dev)")
|
||||
isPseudoLocalesEnabled = true
|
||||
|
||||
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||
}
|
||||
@ -44,8 +43,6 @@ android {
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "ReVanced Manager Debug")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
|
||||
isPseudoLocalesEnabled = true
|
||||
}
|
||||
|
||||
buildConfigField("long", "BUILD_ID", "0L")
|
||||
@ -129,7 +126,6 @@ dependencies {
|
||||
implementation(libs.compose.livedata)
|
||||
implementation(libs.compose.material.icons.extended)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.navigation.compose)
|
||||
|
||||
// Accompanist
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
@ -177,9 +173,11 @@ dependencies {
|
||||
// Koin
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.navigation)
|
||||
implementation(libs.koin.workmanager)
|
||||
|
||||
// Compose Navigation
|
||||
implementation(libs.reimagined.navigation)
|
||||
|
||||
// Licenses
|
||||
implementation(libs.about.libraries)
|
||||
|
||||
|
@ -1,66 +1,36 @@
|
||||
package app.revanced.manager
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.toRoute
|
||||
import app.revanced.manager.ui.model.navigation.AppSelector
|
||||
import app.revanced.manager.ui.model.navigation.ComplexParameter
|
||||
import app.revanced.manager.ui.model.navigation.Dashboard
|
||||
import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.ui.model.navigation.Settings
|
||||
import app.revanced.manager.ui.model.navigation.Update
|
||||
import app.revanced.manager.ui.destination.Destination
|
||||
import app.revanced.manager.ui.destination.SettingsDestination
|
||||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||
import app.revanced.manager.ui.screen.DashboardScreen
|
||||
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
|
||||
import app.revanced.manager.ui.screen.PatcherScreen
|
||||
import app.revanced.manager.ui.screen.PatchesSelectorScreen
|
||||
import app.revanced.manager.ui.screen.RequiredOptionsScreen
|
||||
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
||||
import app.revanced.manager.ui.screen.SettingsScreen
|
||||
import app.revanced.manager.ui.screen.UpdateScreen
|
||||
import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.BackupRestoreSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.ContributorScreen
|
||||
import app.revanced.manager.ui.screen.settings.DeveloperOptionsScreen
|
||||
import app.revanced.manager.ui.screen.settings.DownloadsSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.GeneralSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.LicensesScreen
|
||||
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
|
||||
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.ui.viewmodel.MainViewModel
|
||||
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||
import app.revanced.manager.util.EventEffect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.androidx.compose.navigation.koinNavViewModel
|
||||
import dev.olshevski.navigation.reimagined.AnimatedNavHost
|
||||
import dev.olshevski.navigation.reimagined.NavBackHandler
|
||||
import dev.olshevski.navigation.reimagined.navigate
|
||||
import dev.olshevski.navigation.reimagined.pop
|
||||
import dev.olshevski.navigation.reimagined.popUpTo
|
||||
import dev.olshevski.navigation.reimagined.rememberNavController
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||
import org.koin.androidx.compose.koinViewModel as getComposeViewModel
|
||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
@ExperimentalAnimationApi
|
||||
@ -71,260 +41,90 @@ class MainActivity : ComponentActivity() {
|
||||
enableEdgeToEdge()
|
||||
installSplashScreen()
|
||||
|
||||
val vm: MainViewModel = getActivityViewModel()
|
||||
val vm: MainViewModel = getAndroidViewModel()
|
||||
vm.importLegacySettings(this)
|
||||
|
||||
setContent {
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = vm::applyLegacySettings
|
||||
)
|
||||
val theme by vm.prefs.theme.getAsState()
|
||||
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||
|
||||
EventEffect(vm.legacyImportActivityFlow) {
|
||||
try {
|
||||
launcher.launch(it)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
}
|
||||
}
|
||||
|
||||
ReVancedManagerTheme(
|
||||
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
||||
dynamicColor = dynamicColor
|
||||
) {
|
||||
ReVancedManager(vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val navController =
|
||||
rememberNavController<Destination>(startDestination = Destination.Dashboard)
|
||||
NavBackHandler(navController)
|
||||
|
||||
@Composable
|
||||
private fun ReVancedManager(vm: MainViewModel) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
EventEffect(vm.appSelectFlow) { app ->
|
||||
navController.navigateComplex(
|
||||
SelectedApplicationInfo,
|
||||
SelectedApplicationInfo.ViewModelParams(app)
|
||||
)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Dashboard,
|
||||
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
|
||||
exitTransition = { slideOutHorizontally(targetOffsetX = { -it / 3 }) },
|
||||
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it / 3 }) },
|
||||
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) },
|
||||
) {
|
||||
composable<Dashboard> {
|
||||
DashboardScreen(
|
||||
onSettingsClick = { navController.navigate(Settings) },
|
||||
onAppSelectorClick = {
|
||||
navController.navigate(AppSelector)
|
||||
},
|
||||
onUpdateClick = {
|
||||
navController.navigate(Update())
|
||||
},
|
||||
onDownloaderPluginClick = {
|
||||
navController.navigate(Settings.Downloads)
|
||||
},
|
||||
onAppClick = { packageName ->
|
||||
navController.navigate(InstalledApplicationInfo(packageName))
|
||||
EventEffect(vm.appSelectFlow) { app ->
|
||||
navController.navigate(Destination.SelectedApplicationInfo(app))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable<InstalledApplicationInfo> {
|
||||
val data = it.toRoute<InstalledApplicationInfo>()
|
||||
|
||||
InstalledAppInfoScreen(
|
||||
onPatchClick = vm::selectApp,
|
||||
onBackClick = navController::popBackStack,
|
||||
viewModel = koinViewModel { parametersOf(data.packageName) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<AppSelector> {
|
||||
AppSelectorScreen(
|
||||
onSelect = vm::selectApp,
|
||||
onStorageSelect = vm::selectApp,
|
||||
onBackClick = navController::popBackStack
|
||||
)
|
||||
}
|
||||
|
||||
composable<Patcher> {
|
||||
PatcherScreen(
|
||||
onBackClick = {
|
||||
navController.navigate(route = Dashboard) {
|
||||
launchSingleTop = true
|
||||
popUpTo<Dashboard> {
|
||||
inclusive = false
|
||||
}
|
||||
}
|
||||
},
|
||||
vm = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<Update> {
|
||||
val data = it.toRoute<Update>()
|
||||
|
||||
UpdateScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
vm = koinViewModel { parametersOf(data.downloadOnScreenEntry) }
|
||||
)
|
||||
}
|
||||
|
||||
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
|
||||
composable<SelectedApplicationInfo.Main> {
|
||||
val parentBackStackEntry = navController.navGraphEntry(it)
|
||||
val data =
|
||||
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
||||
val viewModel =
|
||||
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||
parametersOf(data)
|
||||
}
|
||||
|
||||
SelectedAppInfoScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onPatchClick = {
|
||||
it.lifecycleScope.launch {
|
||||
navController.navigateComplex(
|
||||
Patcher,
|
||||
viewModel.getPatcherParams()
|
||||
)
|
||||
}
|
||||
},
|
||||
onPatchSelectorClick = { app, patches, options ->
|
||||
navController.navigateComplex(
|
||||
SelectedApplicationInfo.PatchesSelector,
|
||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||
app,
|
||||
patches,
|
||||
options
|
||||
)
|
||||
AnimatedNavHost(
|
||||
controller = navController
|
||||
) { destination ->
|
||||
when (destination) {
|
||||
is Destination.Dashboard -> DashboardScreen(
|
||||
onSettingsClick = { navController.navigate(Destination.Settings()) },
|
||||
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
|
||||
onUpdateClick = {
|
||||
navController.navigate(Destination.Settings(SettingsDestination.Update()))
|
||||
},
|
||||
onDownloaderPluginClick = {
|
||||
navController.navigate(Destination.Settings(SettingsDestination.Downloads))
|
||||
},
|
||||
onAppClick = { installedApp ->
|
||||
navController.navigate(
|
||||
Destination.InstalledApplicationInfo(
|
||||
installedApp
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
onRequiredOptions = { app, patches, options ->
|
||||
navController.navigateComplex(
|
||||
SelectedApplicationInfo.RequiredOptions,
|
||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||
app,
|
||||
patches,
|
||||
options
|
||||
)
|
||||
|
||||
is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen(
|
||||
onPatchClick = vm::selectApp,
|
||||
onBackClick = { navController.pop() },
|
||||
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
|
||||
)
|
||||
},
|
||||
vm = viewModel
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedApplicationInfo.PatchesSelector> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
is Destination.Settings -> SettingsScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
startDestination = destination.startDestination
|
||||
)
|
||||
|
||||
PatchesSelectorScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onSave = { patches, options ->
|
||||
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||
navController.popBackStack()
|
||||
},
|
||||
vm = koinViewModel { parametersOf(data) }
|
||||
)
|
||||
}
|
||||
is Destination.AppSelector -> AppSelectorScreen(
|
||||
onSelect = vm::selectApp,
|
||||
onStorageSelect = vm::selectApp,
|
||||
onBackClick = { navController.pop() }
|
||||
)
|
||||
|
||||
composable<SelectedApplicationInfo.RequiredOptions> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
|
||||
onPatchClick = { app, patches, options ->
|
||||
navController.navigate(
|
||||
Destination.Patcher(
|
||||
app, patches, options
|
||||
)
|
||||
)
|
||||
},
|
||||
onBackClick = navController::pop,
|
||||
vm = getComposeViewModel {
|
||||
parametersOf(
|
||||
SelectedAppInfoViewModel.Params(
|
||||
destination.selectedApp,
|
||||
destination.patchSelection
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
RequiredOptionsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onContinue = { patches, options ->
|
||||
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||
it.lifecycleScope.launch {
|
||||
navController.navigateComplex(
|
||||
Patcher,
|
||||
selectedAppInfoVm.getPatcherParams()
|
||||
)
|
||||
}
|
||||
},
|
||||
vm = koinViewModel { parametersOf(data) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
navigation<Settings>(startDestination = Settings.Main) {
|
||||
composable<Settings.Main> {
|
||||
SettingsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
navigate = navController::navigate
|
||||
)
|
||||
}
|
||||
|
||||
composable<Settings.General> {
|
||||
GeneralSettingsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onUpdateClick = { navController.navigate(Update()) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<Settings.Advanced> {
|
||||
AdvancedSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Downloads> {
|
||||
DownloadsSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.ImportExport> {
|
||||
BackupRestoreSettingsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.About> {
|
||||
AboutSettingsScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onChangelogClick = { navController.navigate(Settings.Changelogs) },
|
||||
navigate = navController::navigate
|
||||
)
|
||||
}
|
||||
|
||||
composable<Settings.Changelogs> {
|
||||
ChangelogsScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Contributors> {
|
||||
ContributorScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.Licenses> {
|
||||
LicensesScreen(onBackClick = navController::popBackStack)
|
||||
}
|
||||
|
||||
composable<Settings.DeveloperOptions> {
|
||||
DeveloperOptionsScreen(onBackClick = navController::popBackStack)
|
||||
is Destination.Patcher -> PatcherScreen(
|
||||
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
|
||||
vm = getComposeViewModel { parametersOf(destination) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NavController.navGraphEntry(entry: NavBackStackEntry) =
|
||||
remember(entry) { getBackStackEntry(entry.destination.parent!!.id) }
|
||||
|
||||
// Androidx Navigation does not support storing complex types in route objects, so we have to store them inside the saved state handle of the back stack entry instead.
|
||||
private fun <T : Parcelable, R : ComplexParameter<T>> NavController.navigateComplex(
|
||||
route: R,
|
||||
data: T
|
||||
) {
|
||||
navigate(route)
|
||||
getBackStackEntry(route).savedStateHandle["args"] = data
|
||||
}
|
||||
|
||||
private fun <T : Parcelable> NavBackStackEntry.getComplexArg() = savedStateHandle.get<T>("args")!!
|
@ -1,15 +1,18 @@
|
||||
package app.revanced.manager.data.room.apps.installed
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import app.revanced.manager.R
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
enum class InstallType(val stringResource: Int) {
|
||||
DEFAULT(R.string.default_install),
|
||||
MOUNT(R.string.mount_install)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
@Entity(tableName = "installed_app")
|
||||
data class InstalledApp(
|
||||
@PrimaryKey
|
||||
@ -17,4 +20,4 @@ data class InstalledApp(
|
||||
@ColumnInfo(name = "original_package_name") val originalPackageName: String,
|
||||
@ColumnInfo(name = "version") val version: String,
|
||||
@ColumnInfo(name = "install_type") val installType: InstallType
|
||||
)
|
||||
) : Parcelable
|
@ -9,7 +9,7 @@ val viewModelModule = module {
|
||||
viewModelOf(::DashboardViewModel)
|
||||
viewModelOf(::SelectedAppInfoViewModel)
|
||||
viewModelOf(::PatchesSelectorViewModel)
|
||||
viewModelOf(::GeneralSettingsViewModel)
|
||||
viewModelOf(::SettingsViewModel)
|
||||
viewModelOf(::AdvancedSettingsViewModel)
|
||||
viewModelOf(::AppSelectorViewModel)
|
||||
viewModelOf(::PatcherViewModel)
|
||||
|
@ -4,20 +4,9 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -55,14 +44,9 @@ fun AppTopBar(
|
||||
)
|
||||
},
|
||||
actions: @Composable (RowScope.() -> Unit) = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
applyContainerColor: Boolean = false
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val containerColor = if (applyContainerColor) {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||
} else {
|
||||
Color.Unspecified
|
||||
}
|
||||
val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||
|
||||
TopAppBar(
|
||||
title = { Text(title) },
|
||||
|
@ -12,12 +12,11 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||
import app.revanced.manager.util.transparentListItemColors
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AvailableUpdateDialog(
|
||||
onDismiss: () -> Unit,
|
||||
@ -71,11 +70,10 @@ fun AvailableUpdateDialog(
|
||||
Text(stringResource(R.string.never_show_again))
|
||||
},
|
||||
leadingContent = {
|
||||
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
|
||||
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
||||
HapticCheckbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it })
|
||||
}
|
||||
},
|
||||
colors = transparentListItemColors
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@ -1,61 +0,0 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandIn
|
||||
import androidx.compose.animation.shrinkOut
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SelectableChipColors
|
||||
import androidx.compose.material3.SelectableChipElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
|
||||
@Composable
|
||||
fun CheckedFilterChip(
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
label: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
shape: Shape = FilterChipDefaults.shape,
|
||||
colors: SelectableChipColors = FilterChipDefaults.filterChipColors(),
|
||||
elevation: SelectableChipElevation? = FilterChipDefaults.filterChipElevation(),
|
||||
border: BorderStroke? = FilterChipDefaults.filterChipBorder(enabled, selected),
|
||||
interactionSource: MutableInteractionSource? = null
|
||||
) {
|
||||
FilterChip(
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
label = label,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
leadingIcon = {
|
||||
AnimatedVisibility(
|
||||
visible = selected,
|
||||
enter = expandIn(expandFrom = Alignment.CenterStart),
|
||||
exit = shrinkOut(shrinkTowards = Alignment.CenterStart)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(FilterChipDefaults.IconSize),
|
||||
imageVector = Icons.Filled.Done,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingIcon = trailingIcon,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
elevation = elevation,
|
||||
border = border,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SearchBar
|
||||
import androidx.compose.material3.SearchBarColors
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
query: String,
|
||||
onQueryChange: (String) -> Unit,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
placeholder: (@Composable () -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
val colors = SearchBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
dividerColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
SearchBar(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
inputField = {
|
||||
SearchBarDefaults.InputField(
|
||||
modifier = Modifier.sizeIn(minWidth = 380.dp),
|
||||
query = query,
|
||||
onQueryChange = onQueryChange,
|
||||
onSearch = {
|
||||
keyboardController?.hide()
|
||||
},
|
||||
expanded = expanded,
|
||||
onExpandedChange = onExpandedChange,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon
|
||||
)
|
||||
},
|
||||
expanded = expanded,
|
||||
onExpandedChange = onExpandedChange,
|
||||
colors = colors,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
@ -16,7 +16,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||
@ -219,7 +218,7 @@ fun ImportBundleStep(
|
||||
),
|
||||
headlineContent = { Text(stringResource(R.string.auto_update)) },
|
||||
leadingContent = {
|
||||
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
|
||||
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
||||
HapticCheckbox(
|
||||
checked = autoUpdate,
|
||||
onCheckedChange = {
|
||||
|
@ -8,7 +8,6 @@ import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@ -142,19 +141,13 @@ private inline fun <T : Any> WithOptionEditor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T : Any> OptionItem(
|
||||
option: Option<T>,
|
||||
value: T?,
|
||||
setValue: (T?) -> Unit,
|
||||
) {
|
||||
fun <T : Any> OptionItem(option: Option<T>, value: T?, setValue: (T?) -> Unit) {
|
||||
val editor = remember(option.type, option.presets) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val baseOptionEditor =
|
||||
optionEditors.getOrDefault(option.type, UnknownTypeEditor) as OptionEditor<T>
|
||||
|
||||
if (option.type != typeOf<Boolean>() && option.presets != null) PresetOptionEditor(
|
||||
baseOptionEditor
|
||||
)
|
||||
if (option.type != typeOf<Boolean>() && option.presets != null) PresetOptionEditor(baseOptionEditor)
|
||||
else baseOptionEditor
|
||||
}
|
||||
|
||||
@ -162,15 +155,7 @@ fun <T : Any> OptionItem(
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(onClick = ::clickAction),
|
||||
headlineContent = { Text(option.title) },
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(option.description)
|
||||
if (option.required && value == null) Text(
|
||||
stringResource(R.string.option_required),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
},
|
||||
supportingContent = { Text(option.description) },
|
||||
trailingContent = { ListItemTrailingContent() }
|
||||
)
|
||||
}
|
||||
|
@ -1,54 +0,0 @@
|
||||
package app.revanced.manager.ui.component.settings
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.domain.manager.base.Preference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SafeguardBooleanItem(
|
||||
modifier: Modifier = Modifier,
|
||||
preference: Preference<Boolean>,
|
||||
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
||||
@StringRes headline: Int,
|
||||
@StringRes description: Int,
|
||||
@StringRes confirmationText: Int
|
||||
) {
|
||||
val value by preference.getAsState()
|
||||
var showSafeguardWarning by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (showSafeguardWarning) {
|
||||
SafeguardConfirmationDialog(
|
||||
onDismiss = { showSafeguardWarning = false },
|
||||
onConfirm = {
|
||||
coroutineScope.launch { preference.update(!value) }
|
||||
showSafeguardWarning = false
|
||||
},
|
||||
body = stringResource(confirmationText)
|
||||
)
|
||||
}
|
||||
|
||||
BooleanItem(
|
||||
modifier = modifier,
|
||||
value = value,
|
||||
onValueChange = {
|
||||
if (it != preference.default) {
|
||||
showSafeguardWarning = true
|
||||
} else {
|
||||
coroutineScope.launch { preference.update(it) }
|
||||
}
|
||||
},
|
||||
headline = headline,
|
||||
description = description
|
||||
)
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package app.revanced.manager.ui.component.settings
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.AlertDialog
|
||||
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.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import app.revanced.manager.R
|
||||
|
||||
@Composable
|
||||
fun SafeguardConfirmationDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
body: String,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(stringResource(R.string.yes))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.no))
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(Icons.Outlined.WarningAmber, null)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.warning),
|
||||
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(body)
|
||||
}
|
||||
)
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package app.revanced.manager.ui.destination
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.RawValue
|
||||
|
||||
sealed interface Destination : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data object Dashboard : Destination
|
||||
|
||||
@Parcelize
|
||||
data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination
|
||||
|
||||
@Parcelize
|
||||
data object AppSelector : Destination
|
||||
|
||||
@Parcelize
|
||||
data class Settings(val startDestination: SettingsDestination = SettingsDestination.Settings) : Destination
|
||||
|
||||
@Parcelize
|
||||
data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchSelection: PatchSelection? = null) : Destination
|
||||
|
||||
@Parcelize
|
||||
data class Patcher(val selectedApp: SelectedApp, val selectedPatches: PatchSelection, val options: @RawValue Options) : Destination
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package app.revanced.manager.ui.destination
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.RawValue
|
||||
|
||||
sealed interface SelectedAppInfoDestination : Parcelable {
|
||||
@Parcelize
|
||||
data object Main : SelectedAppInfoDestination
|
||||
|
||||
@Parcelize
|
||||
data class PatchesSelector(val app: SelectedApp, val currentSelection: PatchSelection?, val options: @RawValue Options) : SelectedAppInfoDestination
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package app.revanced.manager.ui.destination
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
sealed interface SettingsDestination : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data object Settings : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
data object General : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
data object Advanced : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
data object Updates : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
data object Downloads : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
data object ImportExport : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
data object About : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
data class Update(val downloadOnScreenEntry: Boolean = false) : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
data object Changelogs : SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
data object Contributors: SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
data object Licenses: SettingsDestination
|
||||
|
||||
@Parcelize
|
||||
data object DeveloperOptions: SettingsDestination
|
||||
}
|
@ -34,23 +34,20 @@ data class BundleInfo(
|
||||
}
|
||||
|
||||
companion object Extensions {
|
||||
inline fun Iterable<BundleInfo>.toPatchSelection(
|
||||
allowUnsupported: Boolean,
|
||||
condition: (Int, PatchInfo) -> Boolean
|
||||
): PatchSelection = this.associate { bundle ->
|
||||
val patches =
|
||||
bundle.patchSequence(allowUnsupported)
|
||||
.mapNotNullTo(mutableSetOf()) { patch ->
|
||||
patch.name.takeIf {
|
||||
condition(
|
||||
bundle.uid,
|
||||
patch
|
||||
)
|
||||
inline fun Iterable<BundleInfo>.toPatchSelection(allowUnsupported: Boolean, condition: (Int, PatchInfo) -> Boolean): PatchSelection = this.associate { bundle ->
|
||||
val patches =
|
||||
bundle.patchSequence(allowUnsupported)
|
||||
.mapNotNullTo(mutableSetOf()) { patch ->
|
||||
patch.name.takeIf {
|
||||
condition(
|
||||
bundle.uid,
|
||||
patch
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bundle.uid to patches
|
||||
}
|
||||
bundle.uid to patches
|
||||
}
|
||||
|
||||
fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String?) =
|
||||
sources.flatMapLatestAndCombine(
|
||||
@ -81,28 +78,6 @@ data class BundleInfo(
|
||||
BundleInfo(source.getName(), source.uid, supported, unsupported, universal)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm for determining whether all required options have been set.
|
||||
*/
|
||||
inline fun Iterable<BundleInfo>.requiredOptionsSet(
|
||||
crossinline isSelected: (BundleInfo, PatchInfo) -> Boolean,
|
||||
crossinline optionsForPatch: (BundleInfo, PatchInfo) -> Map<String, Any?>?
|
||||
) = all bundle@{ bundle ->
|
||||
bundle
|
||||
.all
|
||||
.filter { isSelected(bundle, it) }
|
||||
.all patch@{
|
||||
if (it.options.isNullOrEmpty()) return@patch true
|
||||
val opts by lazy { optionsForPatch(bundle, it).orEmpty() }
|
||||
|
||||
it.options.all option@{ option ->
|
||||
if (!option.required || option.default != null) return@option true
|
||||
|
||||
option.key in opts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,93 +0,0 @@
|
||||
package app.revanced.manager.ui.model.navigation
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.RawValue
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
interface ComplexParameter<T : Parcelable>
|
||||
|
||||
@Serializable
|
||||
object Dashboard
|
||||
|
||||
@Serializable
|
||||
object AppSelector
|
||||
|
||||
@Serializable
|
||||
data class InstalledApplicationInfo(val packageName: String)
|
||||
|
||||
@Serializable
|
||||
data class Update(val downloadOnScreenEntry: Boolean = false)
|
||||
|
||||
@Serializable
|
||||
data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val app: SelectedApp,
|
||||
val patches: PatchSelection? = null
|
||||
) : Parcelable
|
||||
|
||||
@Serializable
|
||||
object Main
|
||||
|
||||
@Serializable
|
||||
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val app: SelectedApp,
|
||||
val currentSelection: PatchSelection?,
|
||||
val options: @RawValue Options,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams>
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val selectedApp: SelectedApp,
|
||||
val selectedPatches: PatchSelection,
|
||||
val options: @RawValue Options
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object Settings {
|
||||
sealed interface Destination
|
||||
|
||||
@Serializable
|
||||
data object Main : Destination
|
||||
|
||||
@Serializable
|
||||
data object General : Destination
|
||||
|
||||
@Serializable
|
||||
data object Advanced : Destination
|
||||
|
||||
@Serializable
|
||||
data object Downloads : Destination
|
||||
|
||||
@Serializable
|
||||
data object ImportExport : Destination
|
||||
|
||||
@Serializable
|
||||
data object About : Destination
|
||||
|
||||
@Serializable
|
||||
data object Changelogs : Destination
|
||||
|
||||
@Serializable
|
||||
data object Contributors : Destination
|
||||
|
||||
@Serializable
|
||||
data object Licenses : Destination
|
||||
|
||||
@Serializable
|
||||
data object DeveloperOptions : Destination
|
||||
}
|
@ -3,26 +3,12 @@ package app.revanced.manager.ui.screen
|
||||
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.layout.*
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -31,7 +17,6 @@ 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.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -153,13 +138,10 @@ fun AppSelectorScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.select_app),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick,
|
||||
actions = {
|
||||
IconButton(onClick = { search = true }) {
|
||||
@ -167,8 +149,7 @@ fun AppSelectorScreen(
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
|
@ -7,46 +7,17 @@ import android.provider.Settings
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.layout.*
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.BatteryAlert
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Apps
|
||||
import androidx.compose.material.icons.outlined.BugReport
|
||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.Source
|
||||
import androidx.compose.material.icons.outlined.Update
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@ -54,6 +25,7 @@ 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.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||
@ -62,9 +34,9 @@ import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||
import app.revanced.manager.ui.component.AvailableUpdateDialog
|
||||
import app.revanced.manager.ui.component.NotificationCard
|
||||
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
||||
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
|
||||
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
|
||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
|
||||
import app.revanced.manager.ui.viewmodel.DashboardViewModel
|
||||
import app.revanced.manager.util.RequestInstallAppsContract
|
||||
import app.revanced.manager.util.toast
|
||||
@ -88,7 +60,7 @@ fun DashboardScreen(
|
||||
onSettingsClick: () -> Unit,
|
||||
onUpdateClick: () -> Unit,
|
||||
onDownloaderPluginClick: () -> Unit,
|
||||
onAppClick: (String) -> Unit
|
||||
onAppClick: (InstalledApp) -> Unit
|
||||
) {
|
||||
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
|
||||
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
||||
@ -138,6 +110,7 @@ fun DashboardScreen(
|
||||
)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
var showAndroid11Dialog by rememberSaveable { mutableStateOf(false) }
|
||||
val installAppsPermissionLauncher =
|
||||
rememberLauncherForActivityResult(RequestInstallAppsContract) { granted ->
|
||||
@ -149,7 +122,7 @@ fun DashboardScreen(
|
||||
showAndroid11Dialog = false
|
||||
},
|
||||
onContinue = {
|
||||
installAppsPermissionLauncher.launch(androidContext.packageName)
|
||||
installAppsPermissionLauncher.launch(context.packageName)
|
||||
}
|
||||
)
|
||||
|
||||
@ -200,7 +173,11 @@ fun DashboardScreen(
|
||||
) {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
Badge(modifier = Modifier.size(6.dp))
|
||||
Badge(
|
||||
// A size value above 6.dp forces the Badge icon to be closer to the center, fixing a clipping issue
|
||||
modifier = Modifier.size(7.dp),
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Outlined.Update, stringResource(R.string.update))
|
||||
@ -210,8 +187,7 @@ fun DashboardScreen(
|
||||
IconButton(onClick = onSettingsClick) {
|
||||
Icon(Icons.Outlined.Settings, stringResource(R.string.settings))
|
||||
}
|
||||
},
|
||||
applyContainerColor = true
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
@ -264,9 +240,6 @@ fun DashboardScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val showBatteryOptimizationsWarning by vm.showBatteryOptimizationsWarningFlow.collectAsStateWithLifecycle(
|
||||
false
|
||||
)
|
||||
Notifications(
|
||||
if (!Aapt.supportsDevice()) {
|
||||
{
|
||||
@ -278,7 +251,7 @@ fun DashboardScreen(
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
if (showBatteryOptimizationsWarning) {
|
||||
if (vm.showBatteryOptimizationsWarning) {
|
||||
{
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
@ -316,7 +289,7 @@ fun DashboardScreen(
|
||||
when (DashboardPage.entries[index]) {
|
||||
DashboardPage.DASHBOARD -> {
|
||||
InstalledAppsScreen(
|
||||
onAppClick = { onAppClick(it.currentPackageName) }
|
||||
onAppClick = onAppClick
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -21,8 +21,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -31,7 +29,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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
|
||||
@ -67,29 +64,23 @@ fun InstalledAppInfoScreen(
|
||||
onConfirm = { viewModel.uninstall() }
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.app_info),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
}
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
val installedApp = viewModel.installedApp ?: return@ColumnWithScrollbar
|
||||
|
||||
AppInfo(viewModel.appInfo) {
|
||||
Text(installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
|
||||
|
||||
if (installedApp.installType == InstallType.MOUNT) {
|
||||
if (viewModel.installedApp.installType == InstallType.MOUNT) {
|
||||
Text(
|
||||
text = if (viewModel.isMounted) {
|
||||
stringResource(R.string.mounted)
|
||||
@ -113,7 +104,7 @@ fun InstalledAppInfoScreen(
|
||||
onClick = viewModel::launch
|
||||
)
|
||||
|
||||
when (installedApp.installType) {
|
||||
when (viewModel.installedApp.installType) {
|
||||
InstallType.DEFAULT -> SegmentedButton(
|
||||
icon = Icons.Outlined.Delete,
|
||||
text = stringResource(R.string.uninstall),
|
||||
@ -142,9 +133,9 @@ fun InstalledAppInfoScreen(
|
||||
icon = Icons.Outlined.Update,
|
||||
text = stringResource(R.string.repatch),
|
||||
onClick = {
|
||||
onPatchClick(installedApp.originalPackageName)
|
||||
onPatchClick(viewModel.installedApp.originalPackageName)
|
||||
},
|
||||
enabled = installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess()
|
||||
enabled = viewModel.installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess()
|
||||
)
|
||||
}
|
||||
|
||||
@ -167,19 +158,19 @@ fun InstalledAppInfoScreen(
|
||||
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.package_name),
|
||||
supportingContent = installedApp.currentPackageName
|
||||
supportingContent = viewModel.installedApp.currentPackageName
|
||||
)
|
||||
|
||||
if (installedApp.originalPackageName != installedApp.currentPackageName) {
|
||||
if (viewModel.installedApp.originalPackageName != viewModel.installedApp.currentPackageName) {
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.original_package_name),
|
||||
supportingContent = installedApp.originalPackageName
|
||||
supportingContent = viewModel.installedApp.originalPackageName
|
||||
)
|
||||
}
|
||||
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.install_type),
|
||||
supportingContent = stringResource(installedApp.installType.stringResource)
|
||||
supportingContent = stringResource(viewModel.installedApp.installType.stringResource)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -121,10 +121,9 @@ fun PatcherScreen(
|
||||
}
|
||||
|
||||
AppScaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.patcher),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = ::leaveScreen
|
||||
)
|
||||
},
|
||||
|
@ -1,50 +1,16 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.EaseInOut
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material.icons.outlined.Restore
|
||||
import androidx.compose.material.icons.outlined.Save
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -53,10 +19,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.draw.alpha
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -67,15 +31,15 @@ import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.patch.Option
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.CheckedFilterChip
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.SafeguardDialog
|
||||
import app.revanced.manager.ui.component.SearchBar
|
||||
import app.revanced.manager.ui.component.SearchView
|
||||
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||
import app.revanced.manager.ui.component.patches.OptionItem
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
||||
import app.revanced.manager.util.Options
|
||||
@ -84,7 +48,7 @@ import app.revanced.manager.util.isScrollingUp
|
||||
import app.revanced.manager.util.transparentListItemColors
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PatchesSelectorScreen(
|
||||
onSave: (PatchSelection?, Options) -> Unit,
|
||||
@ -99,17 +63,20 @@ fun PatchesSelectorScreen(
|
||||
bundles.size
|
||||
}
|
||||
val composableScope = rememberCoroutineScope()
|
||||
val (query, setQuery) = rememberSaveable {
|
||||
mutableStateOf("")
|
||||
}
|
||||
val (searchExpanded, setSearchExpanded) = rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
var search: String? by rememberSaveable {
|
||||
mutableStateOf(null)
|
||||
}
|
||||
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
|
||||
val showSaveButton by remember {
|
||||
val showPatchButton by remember {
|
||||
derivedStateOf { vm.selectionIsValid(bundles) }
|
||||
}
|
||||
|
||||
val availablePatchCount by remember {
|
||||
derivedStateOf {
|
||||
bundles.sumOf { it.patchCount }
|
||||
}
|
||||
}
|
||||
|
||||
val defaultPatchSelectionCount by vm.defaultSelectionCount
|
||||
.collectAsStateWithLifecycle(initialValue = 0)
|
||||
|
||||
@ -141,22 +108,27 @@ fun PatchesSelectorScreen(
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
FlowRow(
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
||||
) {
|
||||
CheckedFilterChip(
|
||||
selected = vm.filter and SHOW_UNSUPPORTED == 0,
|
||||
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
|
||||
FilterChip(
|
||||
selected = vm.filter and SHOW_SUPPORTED != 0,
|
||||
onClick = { vm.toggleFlag(SHOW_SUPPORTED) },
|
||||
label = { Text(stringResource(R.string.supported)) }
|
||||
)
|
||||
|
||||
CheckedFilterChip(
|
||||
FilterChip(
|
||||
selected = vm.filter and SHOW_UNIVERSAL != 0,
|
||||
onClick = { vm.toggleFlag(SHOW_UNIVERSAL) },
|
||||
label = { Text(stringResource(R.string.universal)) },
|
||||
)
|
||||
|
||||
FilterChip(
|
||||
selected = vm.filter and SHOW_UNSUPPORTED != 0,
|
||||
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
|
||||
label = { Text(stringResource(R.string.unsupported)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -203,21 +175,20 @@ fun PatchesSelectorScreen(
|
||||
fun LazyListScope.patchList(
|
||||
uid: Int,
|
||||
patches: List<PatchInfo>,
|
||||
visible: Boolean,
|
||||
filterFlag: Int,
|
||||
supported: Boolean,
|
||||
header: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
if (patches.isNotEmpty() && visible) {
|
||||
if (patches.isNotEmpty() && (vm.filter and filterFlag) != 0 || vm.filter == 0) {
|
||||
header?.let {
|
||||
item(contentType = 0) {
|
||||
item {
|
||||
it()
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
items = patches,
|
||||
key = { it.name },
|
||||
contentType = { 1 }
|
||||
key = { it.name }
|
||||
) { patch ->
|
||||
PatchItem(
|
||||
patch = patch,
|
||||
@ -251,142 +222,103 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchBar(
|
||||
query = query,
|
||||
onQueryChange = setQuery,
|
||||
expanded = searchExpanded,
|
||||
onExpandedChange = setSearchExpanded,
|
||||
placeholder = {
|
||||
Text(stringResource(R.string.search_patches))
|
||||
},
|
||||
leadingIcon = {
|
||||
val rotation by animateFloatAsState(
|
||||
targetValue = if (searchExpanded) 360f else 0f,
|
||||
animationSpec = tween(durationMillis = 400, easing = EaseInOut),
|
||||
label = "SearchBar back button"
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (searchExpanded) {
|
||||
setSearchExpanded(false)
|
||||
} else {
|
||||
onBackClick()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.rotate(rotation),
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
AnimatedContent(
|
||||
targetState = searchExpanded,
|
||||
label = "Filter/Clear",
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() }
|
||||
) { searchExpanded ->
|
||||
if (searchExpanded) {
|
||||
IconButton(
|
||||
onClick = { setQuery("") },
|
||||
enabled = query.isNotEmpty()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = stringResource(R.string.clear)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton(onClick = { showBottomSheet = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterList,
|
||||
contentDescription = stringResource(R.string.more)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
search?.let { query ->
|
||||
SearchView(
|
||||
query = query,
|
||||
onQueryChange = { search = it },
|
||||
onActiveChange = { if (!it) search = null },
|
||||
placeholder = { Text(stringResource(R.string.search_patches)) }
|
||||
) {
|
||||
val bundle = bundles[pagerState.currentPage]
|
||||
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
val bundle = bundles[pagerState.currentPage]
|
||||
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
fun List<PatchInfo>.searched() = filter {
|
||||
it.name.contains(query, true)
|
||||
}
|
||||
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.supported.searched(),
|
||||
visible = true,
|
||||
supported = true
|
||||
)
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.universal.searched(),
|
||||
visible = vm.filter and SHOW_UNIVERSAL != 0,
|
||||
supported = true
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.universal_patches),
|
||||
)
|
||||
}
|
||||
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.unsupported.searched(),
|
||||
visible = vm.filter and SHOW_UNSUPPORTED != 0,
|
||||
supported = vm.allowIncompatiblePatches
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.unsupported_patches),
|
||||
onHelpClick = { showUnsupportedPatchesDialog = true }
|
||||
)
|
||||
}
|
||||
fun List<PatchInfo>.searched() = filter {
|
||||
it.name.contains(query, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (!showSaveButton) return@Scaffold
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = !searchExpanded,
|
||||
enter = slideInHorizontally { it } + fadeIn(),
|
||||
exit = slideOutHorizontally { it } + fadeOut()
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.supported.searched(),
|
||||
filterFlag = SHOW_SUPPORTED,
|
||||
supported = true
|
||||
)
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.universal.searched(),
|
||||
filterFlag = SHOW_UNIVERSAL,
|
||||
supported = true
|
||||
) {
|
||||
SmallFloatingActionButton(
|
||||
onClick = vm::reset,
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||
) {
|
||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||
}
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.save_with_count, selectedPatchCount)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Save,
|
||||
contentDescription = stringResource(R.string.save)
|
||||
)
|
||||
},
|
||||
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp ?: true,
|
||||
onClick = {
|
||||
onSave(vm.getCustomSelection(), vm.getOptions())
|
||||
}
|
||||
ListHeader(
|
||||
title = stringResource(R.string.universal_patches),
|
||||
)
|
||||
}
|
||||
|
||||
if (!vm.allowIncompatiblePatches) return@LazyColumnWithScrollbar
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.unsupported.searched(),
|
||||
filterFlag = SHOW_UNSUPPORTED,
|
||||
supported = true
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.unsupported_patches),
|
||||
onHelpClick = { showUnsupportedPatchesDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(
|
||||
R.string.patches_selected,
|
||||
selectedPatchCount,
|
||||
availablePatchCount
|
||||
),
|
||||
onBackClick = onBackClick,
|
||||
actions = {
|
||||
IconButton(onClick = vm::reset) {
|
||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||
}
|
||||
IconButton(onClick = { showBottomSheet = true }) {
|
||||
Icon(Icons.Outlined.FilterList, stringResource(R.string.more))
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
search = ""
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Outlined.Search, stringResource(R.string.search))
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (!showPatchButton) return@Scaffold
|
||||
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.save)) },
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Outlined.Save,
|
||||
stringResource(R.string.save)
|
||||
)
|
||||
},
|
||||
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
|
||||
?: true,
|
||||
onClick = {
|
||||
// TODO: only allow this if all required options have been set.
|
||||
onSave(vm.getCustomSelection(), vm.getOptions())
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
@ -428,13 +360,13 @@ fun PatchesSelectorScreen(
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.supported,
|
||||
visible = true,
|
||||
filterFlag = SHOW_SUPPORTED,
|
||||
supported = true
|
||||
)
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.universal,
|
||||
visible = vm.filter and SHOW_UNIVERSAL != 0,
|
||||
filterFlag = SHOW_UNIVERSAL,
|
||||
supported = true
|
||||
) {
|
||||
ListHeader(
|
||||
@ -444,7 +376,7 @@ fun PatchesSelectorScreen(
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.unsupported,
|
||||
visible = vm.filter and SHOW_UNSUPPORTED != 0,
|
||||
filterFlag = SHOW_UNSUPPORTED,
|
||||
supported = vm.allowIncompatiblePatches
|
||||
) {
|
||||
ListHeader(
|
||||
@ -532,7 +464,7 @@ private fun PatchItem(
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ListHeader(
|
||||
private fun ListHeader(
|
||||
title: String,
|
||||
onHelpClick: (() -> Unit)? = null
|
||||
) {
|
||||
|
@ -1,165 +0,0 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.patch.Option
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||
import app.revanced.manager.ui.component.patches.OptionItem
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.isScrollingUp
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RequiredOptionsScreen(
|
||||
onContinue: (PatchSelection?, Options) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
vm: PatchesSelectorViewModel
|
||||
) {
|
||||
val list by vm.requiredOptsPatches.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = 0,
|
||||
initialPageOffsetFraction = 0f
|
||||
) {
|
||||
list.size
|
||||
}
|
||||
val patchLazyListStates = remember(list) { List(list.size, ::LazyListState) }
|
||||
val bundles by vm.bundlesFlow.collectAsStateWithLifecycle(emptyList())
|
||||
val showContinueButton by remember {
|
||||
derivedStateOf {
|
||||
bundles.requiredOptionsSet(
|
||||
isSelected = { bundle, patch -> vm.isSelected(bundle.uid, patch) },
|
||||
optionsForPatch = { bundle, patch -> vm.getOptions(bundle.uid, patch) }
|
||||
)
|
||||
}
|
||||
}
|
||||
val composableScope = rememberCoroutineScope()
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.required_options_screen),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (!showContinueButton) return@Scaffold
|
||||
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.patch)) },
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Default.AutoFixHigh,
|
||||
stringResource(R.string.patch)
|
||||
)
|
||||
},
|
||||
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
|
||||
?: true,
|
||||
onClick = {
|
||||
onContinue(vm.getCustomSelection(), vm.getOptions())
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
if (list.isEmpty()) return@Column
|
||||
else if (list.size > 1) ScrollableTabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||
) {
|
||||
list.forEachIndexed { index, (bundle, _) ->
|
||||
HapticTab(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = {
|
||||
composableScope.launch {
|
||||
pagerState.animateScrollToPage(
|
||||
index
|
||||
)
|
||||
}
|
||||
},
|
||||
text = { Text(bundle.name) },
|
||||
selectedContentColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
userScrollEnabled = true,
|
||||
pageContent = { index ->
|
||||
// Avoid crashing if the lists have not been fully initialized yet.
|
||||
if (index > list.lastIndex || list.size != patchLazyListStates.size) return@HorizontalPager
|
||||
val (bundle, patches) = list[index]
|
||||
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = patchLazyListStates[index]
|
||||
) {
|
||||
items(patches, key = { it.name }) {
|
||||
ListHeader(it.name)
|
||||
|
||||
val values = vm.getOptions(bundle.uid, it)
|
||||
it.options?.forEach { option ->
|
||||
val key = option.key
|
||||
val value =
|
||||
if (values == null || key !in values) option.default else values[key]
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
OptionItem(
|
||||
option = option as Option<Any>,
|
||||
value = value,
|
||||
setValue = { new ->
|
||||
vm.setOption(bundle.uid, it, key, new)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -12,21 +12,12 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
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.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -41,7 +32,10 @@ import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.destination.SelectedAppInfoDestination
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||
import app.revanced.manager.util.EventEffect
|
||||
import app.revanced.manager.util.Options
|
||||
@ -49,14 +43,14 @@ import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.enabled
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.transparentListItemColors
|
||||
import kotlinx.coroutines.launch
|
||||
import dev.olshevski.navigation.reimagined.*
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SelectedAppInfoScreen(
|
||||
onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit,
|
||||
onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit,
|
||||
onPatchClick: () -> Unit,
|
||||
onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
vm: SelectedAppInfoViewModel
|
||||
) {
|
||||
@ -64,14 +58,20 @@ fun SelectedAppInfoScreen(
|
||||
|
||||
val packageName = vm.selectedApp.packageName
|
||||
val version = vm.selectedApp.version
|
||||
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
|
||||
val bundles by remember(packageName, version) {
|
||||
vm.bundlesRepo.bundleInfoFlow(packageName, version)
|
||||
}.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
||||
val patches = remember(bundles, allowIncompatiblePatches) {
|
||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
||||
val patches by remember {
|
||||
derivedStateOf {
|
||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
||||
}
|
||||
}
|
||||
val selectedPatchCount = remember(patches) {
|
||||
patches.values.sumOf { it.size }
|
||||
val selectedPatchCount by remember {
|
||||
derivedStateOf {
|
||||
patches.values.sumOf { it.size }
|
||||
}
|
||||
}
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
@ -81,133 +81,147 @@ fun SelectedAppInfoScreen(
|
||||
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
||||
launcher.launch(intent)
|
||||
}
|
||||
val composableScope = rememberCoroutineScope()
|
||||
|
||||
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
|
||||
val navController =
|
||||
rememberNavController<SelectedAppInfoDestination>(startDestination = SelectedAppInfoDestination.Main)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
NavBackHandler(controller = navController)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.app_info),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (error != null) return@Scaffold
|
||||
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.patch)) },
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Default.AutoFixHigh,
|
||||
stringResource(R.string.patch)
|
||||
AnimatedNavHost(controller = navController) { destination ->
|
||||
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
|
||||
when (destination) {
|
||||
is SelectedAppInfoDestination.Main -> Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.app_info),
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
onClick = patchClick@{
|
||||
if (selectedPatchCount == 0) {
|
||||
context.toast(context.getString(R.string.no_patches_selected))
|
||||
floatingActionButton = {
|
||||
if (error != null) return@Scaffold
|
||||
|
||||
return@patchClick
|
||||
}
|
||||
|
||||
composableScope.launch {
|
||||
if (!vm.hasSetRequiredOptions(patches)) {
|
||||
onRequiredOptions(
|
||||
vm.selectedApp,
|
||||
vm.getCustomPatches(bundles, allowIncompatiblePatches),
|
||||
vm.options
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.patch)) },
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Default.AutoFixHigh,
|
||||
stringResource(R.string.patch)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
},
|
||||
onClick = patchClick@{
|
||||
if (selectedPatchCount == 0) {
|
||||
context.toast(context.getString(R.string.no_patches_selected))
|
||||
|
||||
onPatchClick()
|
||||
return@patchClick
|
||||
}
|
||||
onPatchClick(
|
||||
vm.selectedApp,
|
||||
patches,
|
||||
vm.getOptionsFiltered(bundles)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
if (vm.showSourceSelector) {
|
||||
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
|
||||
|
||||
AppSourceSelectorDialog(
|
||||
plugins = plugins,
|
||||
installedApp = vm.installedAppData,
|
||||
searchApp = SelectedApp.Search(
|
||||
vm.packageName,
|
||||
vm.desiredVersion
|
||||
),
|
||||
activeSearchJob = vm.activePluginAction,
|
||||
hasRoot = vm.hasRoot,
|
||||
onDismissRequest = vm::dismissSourceSelector,
|
||||
onSelectPlugin = vm::searchUsingPlugin,
|
||||
requiredVersion = requiredVersion,
|
||||
onSelect = {
|
||||
vm.selectedApp = it
|
||||
vm.dismissSourceSelector()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
|
||||
Text(
|
||||
version ?: stringResource(R.string.selected_app_meta_any_version),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
PageItem(
|
||||
R.string.patch_selector_item,
|
||||
stringResource(
|
||||
R.string.patch_selector_item_description,
|
||||
selectedPatchCount
|
||||
),
|
||||
onClick = {
|
||||
navController.navigate(
|
||||
SelectedAppInfoDestination.PatchesSelector(
|
||||
vm.selectedApp,
|
||||
vm.getCustomPatches(
|
||||
bundles,
|
||||
allowIncompatiblePatches
|
||||
),
|
||||
vm.options
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
PageItem(
|
||||
R.string.apk_source_selector_item,
|
||||
when (val app = vm.selectedApp) {
|
||||
is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
|
||||
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
|
||||
is SelectedApp.Download -> stringResource(
|
||||
R.string.apk_source_downloader,
|
||||
plugins.find { it.packageName == app.data.pluginPackageName }?.name
|
||||
?: app.data.pluginPackageName
|
||||
)
|
||||
|
||||
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
|
||||
},
|
||||
onClick = {
|
||||
vm.showSourceSelector()
|
||||
}
|
||||
)
|
||||
error?.let {
|
||||
Text(
|
||||
stringResource(it.resourceId),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(horizontal = 24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
if (vm.showSourceSelector) {
|
||||
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
|
||||
|
||||
AppSourceSelectorDialog(
|
||||
plugins = plugins,
|
||||
installedApp = vm.installedAppData,
|
||||
searchApp = SelectedApp.Search(
|
||||
vm.packageName,
|
||||
vm.desiredVersion
|
||||
),
|
||||
activeSearchJob = vm.activePluginAction,
|
||||
hasRoot = vm.hasRoot,
|
||||
onDismissRequest = vm::dismissSourceSelector,
|
||||
onSelectPlugin = vm::searchUsingPlugin,
|
||||
requiredVersion = requiredVersion,
|
||||
onSelect = {
|
||||
vm.selectedApp = it
|
||||
vm.dismissSourceSelector()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
|
||||
Text(
|
||||
version ?: stringResource(R.string.selected_app_meta_any_version),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
PageItem(
|
||||
R.string.patch_selector_item,
|
||||
stringResource(
|
||||
R.string.patch_selector_item_description,
|
||||
selectedPatchCount
|
||||
),
|
||||
onClick = {
|
||||
onPatchSelectorClick(
|
||||
vm.selectedApp,
|
||||
vm.getCustomPatches(
|
||||
bundles,
|
||||
allowIncompatiblePatches
|
||||
),
|
||||
vm.options
|
||||
)
|
||||
}
|
||||
)
|
||||
PageItem(
|
||||
R.string.apk_source_selector_item,
|
||||
when (val app = vm.selectedApp) {
|
||||
is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
|
||||
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
|
||||
is SelectedApp.Download -> stringResource(
|
||||
R.string.apk_source_downloader,
|
||||
plugins.find { it.packageName == app.data.pluginPackageName }?.name
|
||||
?: app.data.pluginPackageName
|
||||
)
|
||||
|
||||
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
|
||||
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
|
||||
onSave = { patches, options ->
|
||||
vm.updateConfiguration(patches, options, bundles)
|
||||
navController.pop()
|
||||
},
|
||||
onClick = {
|
||||
vm.showSourceSelector()
|
||||
onBackClick = navController::pop,
|
||||
vm = koinViewModel {
|
||||
parametersOf(
|
||||
PatchesSelectorViewModel.Params(
|
||||
destination.app,
|
||||
destination.currentSelection,
|
||||
destination.options,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
error?.let {
|
||||
Text(
|
||||
stringResource(it.resourceId),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(horizontal = 24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,8 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.SwapVert
|
||||
import androidx.compose.material.icons.outlined.Tune
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -19,59 +13,146 @@ import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.model.navigation.Settings
|
||||
|
||||
private val settingsSections = listOf(
|
||||
Triple(
|
||||
R.string.general,
|
||||
R.string.general_description,
|
||||
Icons.Outlined.Settings
|
||||
) to Settings.General,
|
||||
Triple(
|
||||
R.string.extensions,
|
||||
R.string.extensions_description,
|
||||
Icons.Outlined.Download
|
||||
) to Settings.Downloads,
|
||||
Triple(
|
||||
R.string.backup_restore,
|
||||
R.string.backup_restore_description,
|
||||
Icons.Outlined.SwapVert
|
||||
) to Settings.ImportExport,
|
||||
Triple(
|
||||
R.string.advanced,
|
||||
R.string.advanced_description,
|
||||
Icons.Outlined.Tune
|
||||
) to Settings.Advanced,
|
||||
Triple(
|
||||
R.string.about,
|
||||
R.string.app_name,
|
||||
Icons.Outlined.Info
|
||||
) to Settings.About,
|
||||
)
|
||||
import app.revanced.manager.ui.destination.SettingsDestination
|
||||
import app.revanced.manager.ui.screen.settings.*
|
||||
import app.revanced.manager.ui.screen.settings.update.ChangelogsScreen
|
||||
import app.revanced.manager.ui.screen.settings.update.UpdateScreen
|
||||
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
|
||||
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
||||
import dev.olshevski.navigation.reimagined.*
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> Unit) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.settings),
|
||||
onBackClick = onBackClick,
|
||||
fun SettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
startDestination: SettingsDestination,
|
||||
viewModel: SettingsViewModel = koinViewModel()
|
||||
) {
|
||||
val navController = rememberNavController(startDestination)
|
||||
|
||||
val backClick: () -> Unit = {
|
||||
if (navController.backstack.entries.size == 1)
|
||||
onBackClick()
|
||||
else navController.pop()
|
||||
}
|
||||
|
||||
val settingsSections = listOf(
|
||||
Triple(
|
||||
R.string.general,
|
||||
R.string.general_description,
|
||||
Icons.Outlined.Settings
|
||||
) to SettingsDestination.General,
|
||||
Triple(
|
||||
R.string.updates,
|
||||
R.string.updates_description,
|
||||
Icons.Outlined.Update
|
||||
) to SettingsDestination.Updates,
|
||||
Triple(
|
||||
R.string.downloads,
|
||||
R.string.downloads_description,
|
||||
Icons.Outlined.Download
|
||||
) to SettingsDestination.Downloads,
|
||||
Triple(
|
||||
R.string.import_export,
|
||||
R.string.import_export_description,
|
||||
Icons.Outlined.SwapVert
|
||||
) to SettingsDestination.ImportExport,
|
||||
Triple(
|
||||
R.string.advanced,
|
||||
R.string.advanced_description,
|
||||
Icons.Outlined.Tune
|
||||
) to SettingsDestination.Advanced,
|
||||
Triple(
|
||||
R.string.about,
|
||||
R.string.app_name,
|
||||
Icons.Outlined.Info
|
||||
) to SettingsDestination.About,
|
||||
)
|
||||
NavBackHandler(navController)
|
||||
|
||||
AnimatedNavHost(
|
||||
controller = navController
|
||||
) { destination ->
|
||||
when (destination) {
|
||||
is SettingsDestination.General -> GeneralSettingsScreen(
|
||||
onBackClick = backClick,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
settingsSections.forEach { (titleDescIcon, destination) ->
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { navigate(destination) },
|
||||
headlineContent = stringResource(titleDescIcon.first),
|
||||
supportingContent = stringResource(titleDescIcon.second),
|
||||
leadingContent = { Icon(titleDescIcon.third, null) }
|
||||
)
|
||||
|
||||
is SettingsDestination.Advanced -> AdvancedSettingsScreen(
|
||||
onBackClick = backClick
|
||||
)
|
||||
|
||||
is SettingsDestination.Updates -> UpdatesSettingsScreen(
|
||||
onBackClick = backClick,
|
||||
onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) },
|
||||
onUpdateClick = { navController.navigate(SettingsDestination.Update(false)) }
|
||||
)
|
||||
|
||||
is SettingsDestination.Downloads -> DownloadsSettingsScreen(
|
||||
onBackClick = backClick
|
||||
)
|
||||
|
||||
is SettingsDestination.ImportExport -> ImportExportSettingsScreen(
|
||||
onBackClick = backClick
|
||||
)
|
||||
|
||||
is SettingsDestination.About -> AboutSettingsScreen(
|
||||
onBackClick = backClick,
|
||||
onContributorsClick = { navController.navigate(SettingsDestination.Contributors) },
|
||||
onDeveloperOptionsClick = { navController.navigate(SettingsDestination.DeveloperOptions) },
|
||||
onLicensesClick = { navController.navigate(SettingsDestination.Licenses) },
|
||||
)
|
||||
|
||||
is SettingsDestination.Update -> UpdateScreen(
|
||||
onBackClick = backClick,
|
||||
vm = koinViewModel {
|
||||
parametersOf(
|
||||
destination.downloadOnScreenEntry
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
is SettingsDestination.Changelogs -> ChangelogsScreen(
|
||||
onBackClick = backClick,
|
||||
)
|
||||
|
||||
is SettingsDestination.Contributors -> ContributorScreen(
|
||||
onBackClick = backClick,
|
||||
)
|
||||
|
||||
is SettingsDestination.Licenses -> LicensesScreen(
|
||||
onBackClick = backClick,
|
||||
)
|
||||
|
||||
is SettingsDestination.DeveloperOptions -> DeveloperOptionsScreen(onBackClick = backClick)
|
||||
|
||||
is SettingsDestination.Settings -> {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.settings),
|
||||
onBackClick = backClick,
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
settingsSections.forEach { (titleDescIcon, destination) ->
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { navController.navigate(destination) },
|
||||
headlineContent = stringResource(titleDescIcon.first),
|
||||
supportingContent = stringResource(titleDescIcon.second),
|
||||
leadingContent = { Icon(titleDescIcon.third, null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,13 +24,10 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
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.remember
|
||||
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.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -40,7 +37,6 @@ import app.revanced.manager.network.dto.ReVancedSocial
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.model.navigation.Settings
|
||||
import app.revanced.manager.ui.viewmodel.AboutViewModel
|
||||
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
|
||||
import app.revanced.manager.util.openUrl
|
||||
@ -51,8 +47,9 @@ import org.koin.androidx.compose.koinViewModel
|
||||
@Composable
|
||||
fun AboutSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onChangelogClick: () -> Unit,
|
||||
navigate: (Settings.Destination) -> Unit,
|
||||
onContributorsClick: () -> Unit,
|
||||
onLicensesClick: () -> Unit,
|
||||
onDeveloperOptionsClick: () -> Unit,
|
||||
viewModel: AboutViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@ -109,11 +106,6 @@ fun AboutSettingsScreen(
|
||||
}
|
||||
|
||||
val listItems = listOfNotNull(
|
||||
Triple(
|
||||
stringResource(R.string.changelog),
|
||||
stringResource(R.string.changelog_description),
|
||||
third = { onChangelogClick }
|
||||
),
|
||||
Triple(stringResource(R.string.submit_feedback),
|
||||
stringResource(R.string.submit_feedback_description),
|
||||
third = {
|
||||
@ -122,31 +114,27 @@ fun AboutSettingsScreen(
|
||||
Triple(
|
||||
stringResource(R.string.contributors),
|
||||
stringResource(R.string.contributors_description),
|
||||
third = { navigate(Settings.Contributors) }
|
||||
third = onContributorsClick
|
||||
),
|
||||
Triple(
|
||||
stringResource(R.string.developer_options),
|
||||
stringResource(R.string.developer_options_description),
|
||||
third = { navigate(Settings.DeveloperOptions) }
|
||||
third = onDeveloperOptionsClick
|
||||
),
|
||||
Triple(
|
||||
stringResource(R.string.opensource_licenses),
|
||||
stringResource(R.string.opensource_licenses_description),
|
||||
third = { navigate(Settings.Licenses) }
|
||||
third = onLicensesClick
|
||||
)
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.about),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
}
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
|
@ -10,34 +10,14 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Api
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Restore
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@ -51,7 +31,6 @@ import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.settings.BooleanItem
|
||||
import app.revanced.manager.ui.component.settings.IntegerItem
|
||||
import app.revanced.manager.ui.component.settings.SafeguardBooleanItem
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
||||
import app.revanced.manager.util.toast
|
||||
@ -73,17 +52,14 @@ fun AdvancedSettingsScreen(
|
||||
activityManager.largeMemoryClass
|
||||
)
|
||||
}
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.advanced),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
}
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
@ -101,10 +77,17 @@ fun AdvancedSettingsScreen(
|
||||
defaultUrl = vm.prefs.api.default,
|
||||
onSubmit = {
|
||||
showApiUrlDialog = false
|
||||
it?.let(vm::setApiSource)
|
||||
it?.let(vm::setApiUrl)
|
||||
}
|
||||
)
|
||||
}
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.api_url),
|
||||
supportingContent = stringResource(R.string.api_url_description),
|
||||
modifier = Modifier.clickable {
|
||||
showApiUrlDialog = true
|
||||
}
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.patcher))
|
||||
BooleanItem(
|
||||
@ -120,57 +103,30 @@ fun AdvancedSettingsScreen(
|
||||
description = R.string.process_runtime_memory_limit_description,
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.manager))
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.api_url),
|
||||
supportingContent = apiUrl,
|
||||
modifier = Modifier.clickable {
|
||||
showApiUrlDialog = true
|
||||
},
|
||||
trailingContent = {
|
||||
IconButton(onClick = { showApiUrlDialog = true }) {
|
||||
Icon(
|
||||
Icons.Outlined.Edit,
|
||||
contentDescription = stringResource(R.string.edit)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
SafeguardBooleanItem(
|
||||
GroupHeader(stringResource(R.string.safeguards))
|
||||
BooleanItem(
|
||||
preference = vm.prefs.disablePatchVersionCompatCheck,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.allow_compatibility_mixing,
|
||||
description = R.string.allow_compatibility_mixing_description,
|
||||
confirmationText = R.string.allow_compatibility_mixing_confirmation
|
||||
headline = R.string.patch_compat_check,
|
||||
description = R.string.patch_compat_check_description
|
||||
)
|
||||
SafeguardBooleanItem(
|
||||
BooleanItem(
|
||||
preference = vm.prefs.disableUniversalPatchWarning,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.universal_patches_safeguard,
|
||||
description = R.string.universal_patches_safeguard_description,
|
||||
confirmationText = R.string.universal_patches_safeguard_confirmation
|
||||
description = R.string.universal_patches_safeguard_description
|
||||
)
|
||||
SafeguardBooleanItem(
|
||||
BooleanItem(
|
||||
preference = vm.prefs.suggestedVersionSafeguard,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.suggested_version_safeguard,
|
||||
description = R.string.suggested_version_safeguard_description
|
||||
)
|
||||
BooleanItem(
|
||||
preference = vm.prefs.disableSelectionWarning,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.patch_selection_safeguard,
|
||||
description = R.string.patch_selection_safeguard_description,
|
||||
confirmationText = R.string.patch_selection_safeguard_confirmation
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.update))
|
||||
BooleanItem(
|
||||
preference = vm.prefs.showManagerUpdateDialogOnLaunch,
|
||||
headline = R.string.show_manager_update_dialog_on_launch,
|
||||
description = R.string.check_for_update_auto_description
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.experimental_features))
|
||||
BooleanItem(
|
||||
preference = vm.prefs.useProcessRuntime,
|
||||
coroutineScope = vm.viewModelScope,
|
||||
headline = R.string.process_runtime,
|
||||
description = R.string.process_runtime_description,
|
||||
description = R.string.patch_selection_safeguard_description
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.debugging))
|
||||
|
@ -1,5 +1,6 @@
|
||||
package app.revanced.manager.ui.screen.settings
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@ -25,14 +26,11 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
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.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@ -56,17 +54,13 @@ fun ContributorScreen(
|
||||
viewModel: ContributorViewModel = koinViewModel()
|
||||
) {
|
||||
val repositories = viewModel.repositories
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.contributors),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
@ -102,7 +96,7 @@ fun ContributorScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ContributorsCard(
|
||||
title: String,
|
||||
@ -137,7 +131,7 @@ fun ContributorsCard(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
text = processHeadlineText(title),
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Medium)
|
||||
)
|
||||
Text(
|
||||
@ -205,4 +199,11 @@ fun ContributorsCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun processHeadlineText(repositoryName: String): String {
|
||||
return "ReVanced " + repositoryName.replace("revanced/revanced-", "")
|
||||
.replace("-", " ")
|
||||
.split(" ").joinToString(" ") { if (it.length > 3) it else it.uppercase() }
|
||||
.replaceFirstChar { it.uppercase() }
|
||||
}
|
@ -5,11 +5,8 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
@ -24,17 +21,13 @@ fun DeveloperOptionsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: DeveloperOptionsViewModel = koinViewModel()
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.developer_options),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(modifier = Modifier.padding(paddingValues)) {
|
||||
GroupHeader(stringResource(R.string.patch_bundles_section))
|
||||
@ -44,13 +37,7 @@ fun DeveloperOptionsScreen(
|
||||
)
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.patch_bundles_reset),
|
||||
modifier = Modifier.clickable(onClick = vm::resetBundles)
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.testing))
|
||||
SettingsListItem(
|
||||
headlineContent = stringResource(R.string.disable_safeguard),
|
||||
modifier = Modifier.clickable(onClick = vm::disableSafeguard)
|
||||
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -17,11 +17,9 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.pullToRefresh
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -30,7 +28,6 @@ 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.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -58,13 +55,11 @@ fun DownloadsSettingsScreen(
|
||||
val pullRefreshState = rememberPullToRefreshState()
|
||||
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
||||
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.extensions),
|
||||
scrollBehavior = scrollBehavior,
|
||||
title = stringResource(R.string.downloads),
|
||||
onBackClick = onBackClick,
|
||||
actions = {
|
||||
if (viewModel.appSelection.isNotEmpty()) {
|
||||
@ -74,8 +69,7 @@ fun DownloadsSettingsScreen(
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
|
@ -8,14 +8,12 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -23,7 +21,6 @@ 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.res.stringResource
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
@ -35,16 +32,14 @@ import app.revanced.manager.ui.component.haptics.HapticRadioButton
|
||||
import app.revanced.manager.ui.component.settings.BooleanItem
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun GeneralSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onUpdateClick: () -> Unit,
|
||||
viewModel: GeneralSettingsViewModel = koinViewModel()
|
||||
viewModel: SettingsViewModel
|
||||
) {
|
||||
val prefs = viewModel.prefs
|
||||
val coroutineScope = viewModel.viewModelScope
|
||||
@ -56,17 +51,14 @@ fun GeneralSettingsScreen(
|
||||
onConfirm = { viewModel.setTheme(it) }
|
||||
)
|
||||
}
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.general),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
}
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
@ -78,10 +70,10 @@ fun GeneralSettingsScreen(
|
||||
val theme by prefs.theme.getAsState()
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { showThemePicker = true },
|
||||
headlineContent = stringResource(R.string.theme_mode),
|
||||
supportingContent = stringResource(R.string.theme_mode_description),
|
||||
headlineContent = stringResource(R.string.theme),
|
||||
supportingContent = stringResource(R.string.theme_description),
|
||||
trailingContent = {
|
||||
Button(
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
showThemePicker = true
|
||||
}
|
||||
@ -94,24 +86,11 @@ fun GeneralSettingsScreen(
|
||||
BooleanItem(
|
||||
preference = prefs.dynamicColor,
|
||||
coroutineScope = coroutineScope,
|
||||
headline = R.string.personalized_color,
|
||||
description = R.string.personalized_color_description
|
||||
headline = R.string.dynamic_color,
|
||||
description = R.string.dynamic_color_description
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
GroupHeader(stringResource(R.string.update))
|
||||
BooleanItem(
|
||||
preference = prefs.managerAutoUpdates,
|
||||
headline = R.string.check_for_update,
|
||||
description = R.string.check_for_update_auto_description
|
||||
)
|
||||
FilledTonalButton (
|
||||
modifier = Modifier.padding(top = paddingValues.calculateTopPadding()),
|
||||
onClick = onUpdateClick
|
||||
) {
|
||||
Text(stringResource(R.string.check_for_update))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,7 +104,7 @@ private fun ThemePicker(
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.theme_mode)) },
|
||||
title = { Text(stringResource(R.string.theme)) },
|
||||
text = {
|
||||
Column {
|
||||
Theme.entries.forEach {
|
||||
|
@ -13,17 +13,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Key
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -32,7 +22,6 @@ 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.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@ -48,82 +37,118 @@ import app.revanced.manager.ui.component.bundle.BundleSelector
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.viewmodel.ImportExportViewModel
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BackupRestoreSettingsScreen(
|
||||
fun ImportExportSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: ImportExportViewModel = koinViewModel()
|
||||
vm: ImportExportViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val importKeystoreLauncher =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
|
||||
it?.let { uri -> viewModel.startKeystoreImport(uri) }
|
||||
it?.let { uri -> vm.startKeystoreImport(uri) }
|
||||
}
|
||||
val exportKeystoreLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) {
|
||||
it?.let(viewModel::exportKeystore)
|
||||
it?.let(vm::exportKeystore)
|
||||
}
|
||||
|
||||
val patchBundles by viewModel.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val packagesWithOptions by viewModel.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
|
||||
val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
|
||||
|
||||
viewModel.selectionAction?.let { action ->
|
||||
vm.selectionAction?.let { action ->
|
||||
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
|
||||
if (uri == null) {
|
||||
viewModel.clearSelectionAction()
|
||||
vm.clearSelectionAction()
|
||||
} else {
|
||||
viewModel.executeSelectionAction(uri)
|
||||
vm.executeSelectionAction(uri)
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.selectedBundle == null) {
|
||||
if (vm.selectedBundle == null) {
|
||||
BundleSelector(patchBundles) {
|
||||
if (it == null) {
|
||||
viewModel.clearSelectionAction()
|
||||
vm.clearSelectionAction()
|
||||
} else {
|
||||
viewModel.selectBundle(it)
|
||||
vm.selectBundle(it)
|
||||
launcher.launch(action.activityArg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.showCredentialsDialog) {
|
||||
if (vm.showCredentialsDialog) {
|
||||
KeystoreCredentialsDialog(
|
||||
onDismissRequest = viewModel::cancelKeystoreImport,
|
||||
onDismissRequest = vm::cancelKeystoreImport,
|
||||
onSubmit = { cn, pass ->
|
||||
viewModel.viewModelScope.launch {
|
||||
uiSafe(context, R.string.failed_to_import_keystore, "Failed to import keystore") {
|
||||
val result = viewModel.tryKeystoreImport(cn, pass)
|
||||
if (!result) context.toast(context.getString(R.string.restore_keystore_wrong_credentials))
|
||||
}
|
||||
vm.viewModelScope.launch {
|
||||
val result = vm.tryKeystoreImport(cn, pass)
|
||||
if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.backup_restore),
|
||||
scrollBehavior = scrollBehavior,
|
||||
title = stringResource(R.string.import_export),
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
}
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
GroupHeader(stringResource(R.string.signing))
|
||||
GroupItem(
|
||||
onClick = {
|
||||
importKeystoreLauncher.launch("*/*")
|
||||
},
|
||||
headline = R.string.import_keystore,
|
||||
description = R.string.import_keystore_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = {
|
||||
if (!vm.canExport()) {
|
||||
context.toast(context.getString(R.string.export_keystore_unavailable))
|
||||
return@GroupItem
|
||||
}
|
||||
exportKeystoreLauncher.launch("Manager.keystore")
|
||||
},
|
||||
headline = R.string.export_keystore,
|
||||
description = R.string.export_keystore_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = vm::regenerateKeystore,
|
||||
headline = R.string.regenerate_keystore,
|
||||
description = R.string.regenerate_keystore_description
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.patches))
|
||||
GroupItem(
|
||||
onClick = vm::importSelection,
|
||||
headline = R.string.import_patch_selection,
|
||||
description = R.string.import_patch_selection_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = vm::exportSelection,
|
||||
headline = R.string.export_patch_selection,
|
||||
description = R.string.export_patch_selection_description
|
||||
)
|
||||
// TODO: allow resetting selection for specific bundle or package name.
|
||||
GroupItem(
|
||||
onClick = vm::resetSelection,
|
||||
headline = R.string.reset_patch_selection,
|
||||
description = R.string.reset_patch_selection_description
|
||||
)
|
||||
|
||||
var showPackageSelector by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
@ -131,108 +156,37 @@ fun BackupRestoreSettingsScreen(
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (showPackageSelector) {
|
||||
if (showPackageSelector)
|
||||
PackageSelector(packages = packagesWithOptions) { selected ->
|
||||
selected?.let(viewModel::resetOptionsForPackage)
|
||||
selected?.let(vm::resetOptionsForPackage)
|
||||
|
||||
showPackageSelector = false
|
||||
}
|
||||
}
|
||||
|
||||
if (showBundleSelector) {
|
||||
if (showBundleSelector)
|
||||
BundleSelector(bundles = patchBundles) { bundle ->
|
||||
bundle?.let(viewModel::clearOptionsForBundle)
|
||||
bundle?.let(vm::clearOptionsForBundle)
|
||||
|
||||
showBundleSelector = false
|
||||
}
|
||||
}
|
||||
|
||||
GroupHeader(stringResource(R.string.keystore))
|
||||
GroupItem(
|
||||
onClick = {
|
||||
if (!viewModel.canExport()) {
|
||||
context.toast(context.getString(R.string.backup_keystore_unavailable))
|
||||
return@GroupItem
|
||||
}
|
||||
exportKeystoreLauncher.launch("Manager.keystore")
|
||||
},
|
||||
headline = R.string.backup,
|
||||
description = R.string.backup_keystore_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = {
|
||||
importKeystoreLauncher.launch("*/*")
|
||||
},
|
||||
headline = R.string.restore,
|
||||
description = R.string.restore_keystore_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = viewModel::regenerateKeystore,
|
||||
headline = {
|
||||
Text(
|
||||
stringResource(R.string.regenerate_keystore),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
description = R.string.regenerate_keystore_description
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.patch_selection))
|
||||
GroupItem(
|
||||
onClick = viewModel::exportSelection,
|
||||
headline = R.string.backup,
|
||||
description = R.string.restore_patch_selection_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = viewModel::importSelection,
|
||||
headline = R.string.restore,
|
||||
description = R.string.backup_patch_selection_description
|
||||
)
|
||||
GroupItem(
|
||||
onClick = viewModel::resetSelection, // TODO: allow resetting selection for specific bundle or package name.
|
||||
headline = {
|
||||
Text(
|
||||
stringResource(R.string.reset),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
description = R.string.reset_patch_selection_description
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.patch_options))
|
||||
// TODO: patch options import/export.
|
||||
GroupItem(
|
||||
onClick = viewModel::resetOptions,
|
||||
headline = {
|
||||
Text(
|
||||
stringResource(R.string.reset),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
onClick = vm::resetOptions,
|
||||
headline = R.string.patch_options_reset_all,
|
||||
description = R.string.patch_options_reset_all_description,
|
||||
)
|
||||
GroupItem(
|
||||
onClick = { showPackageSelector = true },
|
||||
headline = {
|
||||
Text(
|
||||
stringResource(R.string.patch_options_reset_package),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
headline = R.string.patch_options_reset_package,
|
||||
description = R.string.patch_options_reset_package_description
|
||||
)
|
||||
if (patchBundles.size > 1) {
|
||||
if (patchBundles.size > 1)
|
||||
GroupItem(
|
||||
onClick = { showBundleSelector = true },
|
||||
headline = {
|
||||
Text(
|
||||
stringResource(R.string.patch_options_reset_bundle),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
headline = R.string.patch_options_reset_bundle,
|
||||
description = R.string.patch_options_reset_bundle_description,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -308,19 +262,6 @@ private fun GroupItem(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupItem(
|
||||
onClick: () -> Unit,
|
||||
headline: @Composable () -> Unit,
|
||||
@StringRes description: Int? = null
|
||||
) {
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable { onClick() },
|
||||
headlineContent = headline,
|
||||
supportingContent = description?.let { stringResource(it) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeystoreCredentialsDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
@ -337,7 +278,7 @@ fun KeystoreCredentialsDialog(
|
||||
onSubmit(cn, pass)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.restore_keystore_dialog_button))
|
||||
Text(stringResource(R.string.import_keystore_dialog_button))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
@ -350,7 +291,7 @@ fun KeystoreCredentialsDialog(
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.restore_keystore_dialog_title),
|
||||
text = stringResource(R.string.import_keystore_dialog_title),
|
||||
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
@ -361,19 +302,19 @@ fun KeystoreCredentialsDialog(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.restore_keystore_dialog_description),
|
||||
text = stringResource(R.string.import_keystore_dialog_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = cn,
|
||||
onValueChange = { cn = it },
|
||||
label = { Text(stringResource(R.string.restore_keystore_dialog_alias_field)) }
|
||||
label = { Text(stringResource(R.string.import_keystore_dialog_alias_field)) }
|
||||
)
|
||||
PasswordField(
|
||||
value = pass,
|
||||
onValueChange = { pass = it },
|
||||
label = { Text(stringResource(R.string.restore_keystore_dialog_password_field)) }
|
||||
label = { Text(stringResource(R.string.import_keystore_dialog_password_field)) }
|
||||
)
|
||||
}
|
||||
}
|
@ -7,12 +7,9 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
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.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -31,17 +28,13 @@ fun ChangelogsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: ChangelogsViewModel = koinViewModel()
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.changelog),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
}
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
package app.revanced.manager.ui.screen.settings.update
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.spring
|
||||
@ -24,13 +24,10 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
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.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -55,17 +52,13 @@ fun UpdateScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: UpdateViewModel = koinViewModel()
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.update),
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
}
|
||||
) { paddingValues ->
|
||||
AnimatedVisibility(visible = vm.showInternetCheckDialog) {
|
||||
MeteredDownloadConfirmationDialog(
|
@ -0,0 +1,75 @@
|
||||
package app.revanced.manager.ui.screen.settings.update
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.settings.BooleanItem
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.viewmodel.UpdatesSettingsViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun UpdatesSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onChangelogClick: () -> Unit,
|
||||
onUpdateClick: () -> Unit,
|
||||
vm: UpdatesSettingsViewModel = koinViewModel(),
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.updates),
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable {
|
||||
coroutineScope.launch {
|
||||
if (vm.checkForUpdates()) onUpdateClick()
|
||||
}
|
||||
},
|
||||
headlineContent = stringResource(R.string.manual_update_check),
|
||||
supportingContent = stringResource(R.string.manual_update_check_description)
|
||||
)
|
||||
|
||||
SettingsListItem(
|
||||
modifier = Modifier.clickable(onClick = onChangelogClick),
|
||||
headlineContent = stringResource(R.string.changelog),
|
||||
supportingContent = stringResource(
|
||||
R.string.changelog_description
|
||||
)
|
||||
)
|
||||
|
||||
BooleanItem(
|
||||
preference = vm.managerAutoUpdates,
|
||||
headline = R.string.update_checking_manager,
|
||||
description = R.string.update_checking_manager_description
|
||||
)
|
||||
|
||||
BooleanItem(
|
||||
preference = vm.showManagerUpdateDialogOnLaunch,
|
||||
headline = R.string.show_manager_update_dialog_on_launch,
|
||||
description = R.string.update_checking_manager_description
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ class AdvancedSettingsViewModel(
|
||||
return "revanced-manager_logcat_$time"
|
||||
}
|
||||
|
||||
fun setApiSource(value: String) = viewModelScope.launch(Dispatchers.Default) {
|
||||
fun setApiUrl(value: String) = viewModelScope.launch(Dispatchers.Default) {
|
||||
if (value == prefs.api.get()) return@launch
|
||||
|
||||
prefs.api.update(value)
|
||||
|
@ -24,9 +24,7 @@ import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ -58,19 +56,14 @@ class DashboardViewModel(
|
||||
|
||||
var updatedManagerVersion: String? by mutableStateOf(null)
|
||||
private set
|
||||
val showBatteryOptimizationsWarningFlow = flow {
|
||||
while (true) {
|
||||
// There is no callback for this, so we have to poll it.
|
||||
val result = !powerManager.isIgnoringBatteryOptimizations(app.packageName)
|
||||
emit(result)
|
||||
if (!result) return@flow
|
||||
delay(500L)
|
||||
}
|
||||
}
|
||||
var showBatteryOptimizationsWarning by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
checkForManagerUpdates()
|
||||
showBatteryOptimizationsWarning =
|
||||
!powerManager.isIgnoringBatteryOptimizations(app.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,10 +24,4 @@ class DeveloperOptionsViewModel(
|
||||
fun resetBundles() = viewModelScope.launch {
|
||||
patchBundleRepository.reset()
|
||||
}
|
||||
|
||||
fun disableSafeguard() = viewModelScope.launch {
|
||||
prefs.disablePatchVersionCompatCheck.update(true)
|
||||
prefs.disableSelectionWarning.update(true)
|
||||
prefs.disableUniversalPatchWarning.update(true)
|
||||
}
|
||||
}
|
@ -69,27 +69,25 @@ class ImportExportViewModel(
|
||||
}
|
||||
|
||||
fun startKeystoreImport(content: Uri) = viewModelScope.launch {
|
||||
uiSafe(app, R.string.failed_to_import_keystore, "Failed to import keystore") {
|
||||
val path = withContext(Dispatchers.IO) {
|
||||
File.createTempFile("signing", "ks", app.cacheDir).toPath().also {
|
||||
Files.copy(
|
||||
contentResolver.openInputStream(content)!!,
|
||||
it,
|
||||
StandardCopyOption.REPLACE_EXISTING
|
||||
)
|
||||
}
|
||||
val path = withContext(Dispatchers.IO) {
|
||||
File.createTempFile("signing", "ks", app.cacheDir).toPath().also {
|
||||
Files.copy(
|
||||
contentResolver.openInputStream(content)!!,
|
||||
it,
|
||||
StandardCopyOption.REPLACE_EXISTING
|
||||
)
|
||||
}
|
||||
|
||||
aliases.forEach { alias ->
|
||||
knownPasswords.forEach { pass ->
|
||||
if (tryKeystoreImport(alias, pass, path)) {
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keystoreImportPath = path
|
||||
}
|
||||
|
||||
aliases.forEach { alias ->
|
||||
knownPasswords.forEach { pass ->
|
||||
if (tryKeystoreImport(alias, pass, path)) {
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keystoreImportPath = path
|
||||
}
|
||||
|
||||
fun cancelKeystoreImport() {
|
||||
@ -103,7 +101,7 @@ class ImportExportViewModel(
|
||||
private suspend fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean {
|
||||
path.inputStream().use { stream ->
|
||||
if (keystoreManager.import(cn, pass, stream)) {
|
||||
app.toast(app.getString(R.string.restore_keystore_success))
|
||||
app.toast(app.getString(R.string.import_keystore_success))
|
||||
cancelKeystoreImport()
|
||||
return true
|
||||
}
|
||||
@ -122,7 +120,7 @@ class ImportExportViewModel(
|
||||
|
||||
fun exportKeystore(target: Uri) = viewModelScope.launch {
|
||||
keystoreManager.export(contentResolver.openOutputStream(target)!!)
|
||||
app.toast(app.getString(R.string.backup_keystore_success))
|
||||
app.toast(app.getString(R.string.export_keystore_success))
|
||||
}
|
||||
|
||||
fun regenerateKeystore() = viewModelScope.launch {
|
||||
@ -171,7 +169,7 @@ class ImportExportViewModel(
|
||||
override val activityArg = JSON_MIMETYPE
|
||||
override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe(
|
||||
app,
|
||||
R.string.restore_patch_selection_fail,
|
||||
R.string.import_patch_selection_fail,
|
||||
"Failed to restore patch selection"
|
||||
) {
|
||||
val selection = withContext(Dispatchers.IO) {
|
||||
@ -181,7 +179,7 @@ class ImportExportViewModel(
|
||||
}
|
||||
|
||||
selectionRepository.import(bundleUid, selection)
|
||||
app.toast(app.getString(R.string.restore_patch_selection_success))
|
||||
app.toast(app.getString(R.string.import_patch_selection_success))
|
||||
}
|
||||
}
|
||||
|
||||
@ -190,7 +188,7 @@ class ImportExportViewModel(
|
||||
override val activityArg = "selection.json"
|
||||
override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe(
|
||||
app,
|
||||
R.string.backup_patch_selection_fail,
|
||||
R.string.export_patch_selection_fail,
|
||||
"Failed to backup patch selection"
|
||||
) {
|
||||
val selection = selectionRepository.export(bundleUid)
|
||||
@ -200,7 +198,7 @@ class ImportExportViewModel(
|
||||
Json.Default.encodeToStream(selection, it)
|
||||
}
|
||||
}
|
||||
app.toast(app.getString(R.string.backup_patch_selection_success))
|
||||
app.toast(app.getString(R.string.export_patch_selection_success))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,17 +32,15 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class InstalledAppInfoViewModel(
|
||||
packageName: String
|
||||
val installedApp: InstalledApp
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val context: Application by inject()
|
||||
private val app: Application by inject()
|
||||
private val pm: PM by inject()
|
||||
private val installedAppRepository: InstalledAppRepository by inject()
|
||||
val rootInstaller: RootInstaller by inject()
|
||||
|
||||
lateinit var onBackClick: () -> Unit
|
||||
|
||||
var installedApp: InstalledApp? by mutableStateOf(null)
|
||||
private set
|
||||
var appInfo: PackageInfo? by mutableStateOf(null)
|
||||
private set
|
||||
var appliedPatches: PatchSelection? by mutableStateOf(null)
|
||||
@ -51,48 +49,38 @@ class InstalledAppInfoViewModel(
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
installedApp = installedAppRepository.get(packageName)?.also {
|
||||
isMounted = rootInstaller.isAppMounted(it.currentPackageName)
|
||||
appInfo = withContext(Dispatchers.IO) {
|
||||
pm.getPackageInfo(it.currentPackageName)
|
||||
}
|
||||
appliedPatches = withContext(Dispatchers.IO) {
|
||||
installedAppRepository.getAppliedPatches(it.currentPackageName)
|
||||
}
|
||||
}
|
||||
isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName)
|
||||
}
|
||||
}
|
||||
|
||||
fun launch() = installedApp?.currentPackageName?.let(pm::launch)
|
||||
fun launch() = pm.launch(installedApp.currentPackageName)
|
||||
|
||||
fun mountOrUnmount() = viewModelScope.launch {
|
||||
val pkgName = installedApp?.currentPackageName ?: return@launch
|
||||
try {
|
||||
if (isMounted)
|
||||
rootInstaller.unmount(pkgName)
|
||||
rootInstaller.unmount(installedApp.currentPackageName)
|
||||
else
|
||||
rootInstaller.mount(pkgName)
|
||||
rootInstaller.mount(installedApp.currentPackageName)
|
||||
} catch (e: Exception) {
|
||||
if (isMounted) {
|
||||
context.toast(context.getString(R.string.failed_to_unmount, e.simpleMessage()))
|
||||
app.toast(app.getString(R.string.failed_to_unmount, e.simpleMessage()))
|
||||
Log.e(tag, "Failed to unmount", e)
|
||||
} else {
|
||||
context.toast(context.getString(R.string.failed_to_mount, e.simpleMessage()))
|
||||
app.toast(app.getString(R.string.failed_to_mount, e.simpleMessage()))
|
||||
Log.e(tag, "Failed to mount", e)
|
||||
}
|
||||
} finally {
|
||||
isMounted = rootInstaller.isAppMounted(pkgName)
|
||||
isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName)
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstall() {
|
||||
val app = installedApp ?: return
|
||||
when (app.installType) {
|
||||
InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
|
||||
when (installedApp.installType) {
|
||||
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName)
|
||||
|
||||
InstallType.MOUNT -> viewModelScope.launch {
|
||||
rootInstaller.uninstall(app.currentPackageName)
|
||||
installedAppRepository.delete(app)
|
||||
rootInstaller.uninstall(installedApp.currentPackageName)
|
||||
installedAppRepository.delete(installedApp)
|
||||
onBackClick()
|
||||
}
|
||||
}
|
||||
@ -109,22 +97,34 @@ class InstalledAppInfoViewModel(
|
||||
|
||||
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||
viewModelScope.launch {
|
||||
installedApp?.let {
|
||||
installedAppRepository.delete(it)
|
||||
}
|
||||
installedAppRepository.delete(installedApp)
|
||||
onBackClick()
|
||||
}
|
||||
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
||||
this@InstalledAppInfoViewModel.context.toast(this@InstalledAppInfoViewModel.context.getString(R.string.uninstall_app_fail, extraStatusMessage))
|
||||
app.toast(app.getString(R.string.uninstall_app_fail, extraStatusMessage))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
appInfo = withContext(Dispatchers.IO) {
|
||||
pm.getPackageInfo(installedApp.currentPackageName)
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
appliedPatches = withContext(Dispatchers.IO) {
|
||||
installedAppRepository.getAppliedPatches(installedApp.currentPackageName)
|
||||
}
|
||||
}
|
||||
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
it,
|
||||
app,
|
||||
uninstallBroadcastReceiver,
|
||||
IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
@ -132,6 +132,6 @@ class InstalledAppInfoViewModel(
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
context.unregisterReceiver(uninstallBroadcastReceiver)
|
||||
app.unregisterReceiver(uninstallBroadcastReceiver)
|
||||
}
|
||||
}
|
@ -2,10 +2,13 @@ package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
@ -25,7 +28,6 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class MainViewModel(
|
||||
@ -34,13 +36,10 @@ class MainViewModel(
|
||||
private val downloadedAppRepository: DownloadedAppRepository,
|
||||
private val keystoreManager: KeystoreManager,
|
||||
private val app: Application,
|
||||
val prefs: PreferencesManager,
|
||||
private val json: Json
|
||||
val prefs: PreferencesManager
|
||||
) : ViewModel() {
|
||||
private val appSelectChannel = Channel<SelectedApp>()
|
||||
val appSelectFlow = appSelectChannel.receiveAsFlow()
|
||||
private val legacyImportActivityChannel = Channel<Intent>()
|
||||
val legacyImportActivityFlow = legacyImportActivityChannel.receiveAsFlow()
|
||||
|
||||
private suspend fun suggestedVersion(packageName: String) =
|
||||
patchBundleRepository.suggestedVersions.first()[packageName]
|
||||
@ -51,8 +50,7 @@ class MainViewModel(
|
||||
val suggestedVersion = suggestedVersion(app.packageName) ?: return null
|
||||
|
||||
val downloadedApp =
|
||||
downloadedAppRepository.get(app.packageName, suggestedVersion, markUsed = true)
|
||||
?: return null
|
||||
downloadedAppRepository.get(app.packageName, suggestedVersion, markUsed = true) ?: return null
|
||||
return SelectedApp.Local(
|
||||
downloadedApp.packageName,
|
||||
downloadedApp.version,
|
||||
@ -69,46 +67,42 @@ class MainViewModel(
|
||||
selectApp(SelectedApp.Search(packageName, suggestedVersion(packageName)))
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
if (!prefs.firstLaunch.get()) return@launch
|
||||
legacyImportActivityChannel.send(Intent().apply {
|
||||
fun importLegacySettings(componentActivity: ComponentActivity) {
|
||||
if (!prefs.firstLaunch.getBlocking()) return
|
||||
|
||||
try {
|
||||
val launcher = componentActivity.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
result.data?.getStringExtra("data")?.let {
|
||||
applyLegacySettings(it)
|
||||
} ?: app.toast(app.getString(R.string.legacy_import_failed))
|
||||
} else {
|
||||
app.toast(app.getString(R.string.legacy_import_failed))
|
||||
}
|
||||
}
|
||||
|
||||
val intent = Intent().apply {
|
||||
setClassName(
|
||||
"app.revanced.manager.flutter",
|
||||
"app.revanced.manager.flutter.ExportSettingsActivity"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
launcher.launch(intent)
|
||||
} catch (e: Exception) {
|
||||
if (e !is ActivityNotFoundException) {
|
||||
app.toast(app.getString(R.string.legacy_import_failed))
|
||||
Log.e(tag, "Failed to launch legacy import activity: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyLegacySettings(result: ActivityResult) {
|
||||
if (result.resultCode != Activity.RESULT_OK) {
|
||||
app.toast(app.getString(R.string.legacy_import_failed))
|
||||
Log.e(
|
||||
tag,
|
||||
"Got unknown result code while importing legacy settings: ${result.resultCode}"
|
||||
)
|
||||
return
|
||||
}
|
||||
private fun applyLegacySettings(data: String) = viewModelScope.launch {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val settings = json.decodeFromString<LegacySettings>(data)
|
||||
|
||||
val jsonStr = result.data?.getStringExtra("data")
|
||||
if (jsonStr == null) {
|
||||
app.toast(app.getString(R.string.legacy_import_failed))
|
||||
Log.e(tag, "Legacy settings data is null")
|
||||
return
|
||||
}
|
||||
val settings = try {
|
||||
json.decodeFromString<LegacySettings>(jsonStr)
|
||||
} catch (e: SerializationException) {
|
||||
app.toast(app.getString(R.string.legacy_import_failed))
|
||||
Log.e(tag, "Legacy settings data could not be deserialized", e)
|
||||
return
|
||||
}
|
||||
|
||||
applyLegacySettings(settings)
|
||||
}
|
||||
|
||||
private fun applyLegacySettings(settings: LegacySettings) = viewModelScope.launch {
|
||||
settings.themeMode?.let { theme ->
|
||||
val themeMap = mapOf(
|
||||
0 to Theme.SYSTEM,
|
||||
@ -137,8 +131,8 @@ class MainViewModel(
|
||||
updateCheck()
|
||||
}
|
||||
}
|
||||
settings.patchesChangeEnabled?.let { allowChangingPatchSelection ->
|
||||
prefs.allowChangingPatchSelection.update(allowChangingPatchSelection)
|
||||
settings.patchesChangeEnabled?.let { disableSelectionWarning ->
|
||||
prefs.disableSelectionWarning.update(disableSelectionWarning)
|
||||
}
|
||||
settings.keystore?.let { keystore ->
|
||||
val keystoreBytes = Base64.decode(keystore, Base64.DEFAULT)
|
||||
@ -151,7 +145,6 @@ class MainViewModel(
|
||||
settings.patches?.let { selection ->
|
||||
patchSelectionRepository.import(0, selection)
|
||||
}
|
||||
Log.d(tag, "Imported legacy settings")
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
@ -39,6 +39,7 @@ import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import app.revanced.manager.ui.destination.Destination
|
||||
import app.revanced.manager.ui.model.InstallerModel
|
||||
import app.revanced.manager.ui.model.ProgressKey
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
@ -46,7 +47,6 @@ import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.StepProgressProvider
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.saveableVar
|
||||
import app.revanced.manager.util.saver.snapshotStateListSaver
|
||||
@ -72,7 +72,7 @@ import java.time.Duration
|
||||
|
||||
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||
class PatcherViewModel(
|
||||
private val input: Patcher.ViewModelParams
|
||||
private val input: Destination.Patcher
|
||||
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
|
||||
private val app: Application by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
@ -21,7 +22,7 @@ import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.ui.model.BundleInfo
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.saver.Nullable
|
||||
@ -35,14 +36,11 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import kotlinx.collections.immutable.*
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@Stable
|
||||
@OptIn(SavedStateHandleSaveableApi::class)
|
||||
class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) :
|
||||
ViewModel(), KoinComponent {
|
||||
class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||
private val app: Application = get()
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
private val prefs: PreferencesManager = get()
|
||||
@ -66,7 +64,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
viewModelScope.launch {
|
||||
universalPatchWarningEnabled = !prefs.disableUniversalPatchWarning.get()
|
||||
|
||||
if (prefs.allowChangingPatchSelection.get()) {
|
||||
if (prefs.disableSelectionWarning.get()) {
|
||||
selectionWarningEnabled = false
|
||||
return@launch
|
||||
}
|
||||
@ -103,7 +101,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
|
||||
val compatibleVersions = mutableStateListOf<String>()
|
||||
|
||||
var filter by mutableIntStateOf(SHOW_UNIVERSAL)
|
||||
var filter by mutableIntStateOf(0)
|
||||
private set
|
||||
|
||||
private val defaultPatchSelection = bundlesFlow.map { bundles ->
|
||||
@ -115,22 +113,6 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
selection.values.sumOf { it.size }
|
||||
}
|
||||
|
||||
// This is for the required options screen.
|
||||
private val requiredOptsPatchesDeferred = viewModelScope.async(start = CoroutineStart.LAZY) {
|
||||
bundlesFlow.first().map { bundle ->
|
||||
bundle to bundle.all.filter { patch ->
|
||||
val opts by lazy {
|
||||
getOptions(bundle.uid, patch).orEmpty()
|
||||
}
|
||||
isSelected(
|
||||
bundle.uid,
|
||||
patch
|
||||
) && patch.options?.any { it.required && it.default == null && it.key !in opts } ?: false
|
||||
}.toList()
|
||||
}.filter { (_, patches) -> patches.isNotEmpty() }
|
||||
}
|
||||
val requiredOptsPatches = flow { emit(requiredOptsPatchesDeferred.await()) }
|
||||
|
||||
fun selectionIsValid(bundles: List<BundleInfo>) = bundles.any { bundle ->
|
||||
bundle.patchSequence(allowIncompatiblePatches).any { patch ->
|
||||
isSelected(bundle.uid, patch)
|
||||
@ -217,8 +199,9 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SHOW_UNSUPPORTED = 1 // 2^0
|
||||
const val SHOW_SUPPORTED = 1 // 2^0
|
||||
const val SHOW_UNIVERSAL = 2 // 2^1
|
||||
const val SHOW_UNSUPPORTED = 4 // 2^2
|
||||
|
||||
private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver(
|
||||
// Patch name -> Options
|
||||
@ -231,6 +214,12 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
private val selectionSaver: Saver<PersistentPatchSelection?, Nullable<PatchSelection>> =
|
||||
nullableSaver(persistentMapSaver(valueSaver = persistentSetSaver()))
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val app: SelectedApp,
|
||||
val currentSelection: PatchSelection?,
|
||||
val options: Options,
|
||||
)
|
||||
}
|
||||
|
||||
// Versions of other types, but utilizing persistent/observable collection types.
|
||||
|
@ -9,7 +9,6 @@ import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
@ -30,17 +29,12 @@ import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.plugin.downloader.GetScope
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.BundleInfo
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.requiredOptionsSet
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
@ -63,10 +57,9 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||
class SelectedAppInfoViewModel(
|
||||
input: SelectedApplicationInfo.ViewModelParams
|
||||
) : ViewModel(), KoinComponent {
|
||||
class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||
private val app: Application = get()
|
||||
val bundlesRepo: PatchBundleRepository = get()
|
||||
private val bundleRepository: PatchBundleRepository = get()
|
||||
private val selectionRepository: PatchSelectionRepository = get()
|
||||
private val optionsRepository: PatchOptionsRepository = get()
|
||||
@ -117,10 +110,7 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val requiredVersion = combine(
|
||||
prefs.suggestedVersionSafeguard.flow,
|
||||
bundleRepository.suggestedVersions
|
||||
) { suggestedVersionSafeguard, suggestedVersions ->
|
||||
val requiredVersion = combine(prefs.suggestedVersionSafeguard.flow, bundleRepository.suggestedVersions) { suggestedVersionSafeguard, suggestedVersions ->
|
||||
if (!suggestedVersionSafeguard) return@combine null
|
||||
|
||||
suggestedVersions[input.app.packageName]
|
||||
@ -152,7 +142,7 @@ class SelectedAppInfoViewModel(
|
||||
|
||||
// Try to get the previous selection if customization is enabled.
|
||||
viewModelScope.launch {
|
||||
if (!prefs.allowChangingPatchSelection.get()) return@launch
|
||||
if (!prefs.disableSelectionWarning.get()) return@launch
|
||||
|
||||
val previous = selectionRepository.getSelection(packageName)
|
||||
if (previous.values.sumOf { it.size } == 0) return@launch
|
||||
@ -177,10 +167,6 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val bundleInfoFlow by derivedStateOf {
|
||||
bundleRepository.bundleInfoFlow(packageName, selectedApp.version)
|
||||
}
|
||||
|
||||
fun showSourceSelector() {
|
||||
dismissSourceSelector()
|
||||
showSourceSelector = true
|
||||
@ -267,23 +253,6 @@ class SelectedAppInfoViewModel(
|
||||
selectedAppInfo = info
|
||||
}
|
||||
|
||||
suspend fun hasSetRequiredOptions(patchSelection: PatchSelection) = bundleInfoFlow
|
||||
.first()
|
||||
.requiredOptionsSet(
|
||||
isSelected = { bundle, patch -> patch.name in patchSelection[bundle.uid]!! },
|
||||
optionsForPatch = { bundle, patch -> options[bundle.uid]?.get(patch.name) },
|
||||
)
|
||||
|
||||
suspend fun getPatcherParams(): Patcher.ViewModelParams {
|
||||
val allowUnsupported = prefs.disablePatchVersionCompatCheck.get()
|
||||
val bundles = bundleInfoFlow.first()
|
||||
return Patcher.ViewModelParams(
|
||||
selectedApp,
|
||||
getPatches(bundles, allowUnsupported),
|
||||
getOptionsFiltered(bundles)
|
||||
)
|
||||
}
|
||||
|
||||
fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles)
|
||||
|
||||
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||
@ -295,15 +264,17 @@ class SelectedAppInfoViewModel(
|
||||
): PatchSelection? =
|
||||
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
|
||||
|
||||
fun updateConfiguration(selection: PatchSelection?, options: Options) = viewModelScope.launch {
|
||||
val bundles = bundleInfoFlow.first()
|
||||
|
||||
fun updateConfiguration(
|
||||
selection: PatchSelection?,
|
||||
options: Options,
|
||||
bundles: List<BundleInfo>
|
||||
) {
|
||||
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
||||
|
||||
val filteredOptions = options.filtered(bundles)
|
||||
this@SelectedAppInfoViewModel.options = filteredOptions
|
||||
this.options = filteredOptions
|
||||
|
||||
if (!persistConfiguration) return@launch
|
||||
if (!persistConfiguration) return
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
||||
?: selectionRepository.clearSelection(packageName)
|
||||
@ -312,6 +283,11 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val app: SelectedApp,
|
||||
val patches: PatchSelection?,
|
||||
)
|
||||
|
||||
enum class Error(@StringRes val resourceId: Int) {
|
||||
NoPlugins(R.string.downloader_no_plugins_available)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class GeneralSettingsViewModel(
|
||||
class SettingsViewModel(
|
||||
val prefs: PreferencesManager
|
||||
) : ViewModel() {
|
||||
fun setTheme(theme: Theme) = viewModelScope.launch {
|
@ -108,19 +108,14 @@ class UpdateViewModel(
|
||||
val extra =
|
||||
intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
|
||||
|
||||
when(pmStatus) {
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
state = State.SUCCESS
|
||||
}
|
||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
||||
state = State.CAN_INSTALL
|
||||
}
|
||||
else -> {
|
||||
app.toast(app.getString(R.string.install_app_fail, extra))
|
||||
installError = extra
|
||||
state = State.FAILED
|
||||
}
|
||||
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
state = State.SUCCESS
|
||||
} else {
|
||||
state = State.FAILED
|
||||
// TODO: handle install fail with a popup
|
||||
installError = extra
|
||||
app.toast(app.getString(R.string.install_app_fail, extra))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,11 @@ package app.revanced.manager.util
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.icu.number.Notation
|
||||
import android.icu.number.NumberFormatter
|
||||
import android.icu.number.Precision
|
||||
import android.icu.text.CompactDecimalFormat
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
|
@ -5,11 +5,8 @@
|
||||
<string name="cli">CLI</string>
|
||||
<string name="manager">Manager</string>
|
||||
|
||||
<string name="revanced_patcher">ReVanced Patcher</string>
|
||||
|
||||
<string name="plugin_host_permission_label">ReVanced Manager plugin host</string>
|
||||
<string name="plugin_host_permission_description">Used to control access to ReVanced Manager
|
||||
plugins. Only ReVanced Manager has this.</string>
|
||||
<string name="plugin_host_permission_description">Used to control access to ReVanced Manager plugins. Only ReVanced Manager has this.</string>
|
||||
|
||||
<string name="toast_copied_to_clipboard">Copied!</string>
|
||||
<string name="copy_to_clipboard">Copy to clipboard</string>
|
||||
@ -19,10 +16,8 @@
|
||||
<string name="select_app">Select an app</string>
|
||||
<string name="patches_selected">%1$d/%2$d selected</string>
|
||||
|
||||
<string name="new_downloader_plugins_notification">New downloader plugins available. Click here
|
||||
to configure them.</string>
|
||||
<string name="unsupported_architecture_warning">Patching on this device architecture is
|
||||
unsupported and will most likely fail.</string>
|
||||
<string name="new_downloader_plugins_notification">New downloader plugins available. Click here to configure them.</string>
|
||||
<string name="unsupported_architecture_warning">Patching on this device architecture is unsupported and will most likely fail.</string>
|
||||
|
||||
<string name="import_">Import</string>
|
||||
<string name="import_bundle">Import patch bundle</string>
|
||||
@ -41,20 +36,15 @@
|
||||
<string name="bundle_name_fallback">Unnamed</string>
|
||||
|
||||
<string name="android_11_bug_dialog_title">Android 11 bug</string>
|
||||
<string name="android_11_bug_dialog_description">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.</string>
|
||||
<string name="android_11_bug_dialog_description">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.</string>
|
||||
|
||||
<string name="selected_app_meta_any_version">Any available version</string>
|
||||
<string name="app_source_dialog_title">Select source</string>
|
||||
<string name="app_source_dialog_option_auto">Auto</string>
|
||||
<string name="app_source_dialog_option_auto_description">Use all installed downloaders to find a
|
||||
suitable APK file</string>
|
||||
<string name="app_source_dialog_option_auto_description">Use all installed downloaders to find a suitable APK file</string>
|
||||
<string name="app_source_dialog_option_auto_unavailable">No plugins available</string>
|
||||
<string name="app_source_dialog_option_installed_no_root">Mounted apps cannot be patched again
|
||||
without root access</string>
|
||||
<string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not
|
||||
match the suggested version</string>
|
||||
<string name="app_source_dialog_option_installed_no_root">Mounted apps cannot be patched again without root access</string>
|
||||
<string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not match the suggested version</string>
|
||||
|
||||
<string name="patch_item_description">Start patching the application</string>
|
||||
<string name="patch_selector_item">Patch selection and options</string>
|
||||
@ -70,91 +60,73 @@
|
||||
<string name="legacy_import_failed">Could not import legacy settings</string>
|
||||
|
||||
<string name="auto_updates_dialog_title">Configure updates</string>
|
||||
<string name="auto_updates_dialog_description">Do you want ReVanced Manager to periodically
|
||||
check for updates for the following components?</string>
|
||||
<string name="auto_updates_dialog_description">Do you want ReVanced Manager to periodically check for updates for the following components?</string>
|
||||
<string name="auto_updates_dialog_manager">ReVanced Manager</string>
|
||||
<string name="auto_updates_dialog_patches">ReVanced Patches</string>
|
||||
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
||||
|
||||
<string name="general">General</string>
|
||||
<string name="general_description">Appearances, Updates</string>
|
||||
|
||||
<string name="general_description">Theme, dynamic color</string>
|
||||
<string name="updates">Updates</string>
|
||||
<string name="updates_description">Check for updates and view changelogs</string>
|
||||
|
||||
<string name="extensions">Extensions</string>
|
||||
<string name="extensions_description">Downloader plugins, downloaded apps</string>
|
||||
<string name="backup_restore">Backup & Restore</string>
|
||||
<string name="backup_restore_description">Keystore, Patch selections, Patch options</string>
|
||||
<string name="downloads">Downloads</string>
|
||||
<string name="downloads_description">Downloader plugins and downloaded apps</string>
|
||||
<string name="import_export">Import & export</string>
|
||||
<string name="import_export_description">Keystore, patch options and selection</string>
|
||||
<string name="advanced">Advanced</string>
|
||||
<string name="advanced_description">API Source, memory limits, debug logs</string>
|
||||
<string name="experimental_features">Experimental features</string>
|
||||
|
||||
|
||||
<string name="advanced_description">API URL, memory limit, debugging</string>
|
||||
<string name="about">About</string>
|
||||
<string name="opensource_licenses">Open source licenses</string>
|
||||
<string name="opensource_licenses_description">View all the libraries used to make this
|
||||
application</string>
|
||||
<string name="opensource_licenses_description">View all the libraries used to make this application</string>
|
||||
|
||||
<string name="contributors">Contributors</string>
|
||||
<string name="contributors_description">View the contributors of ReVanced</string>
|
||||
<string name="personalized_color">Personalized color</string>
|
||||
<string name="personalized_color_description">Use color provided by your device\'s palette</string>
|
||||
<string name="theme_mode">Theme mode</string>
|
||||
<string name="theme_mode_description">Choose between light, dark, and system provided mode</string>
|
||||
<string name="dynamic_color">Dynamic color</string>
|
||||
<string name="dynamic_color_description">Adapt colors to the wallpaper</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="theme_description">Choose between light or dark theme</string>
|
||||
<string name="safeguards">Safeguards</string>
|
||||
<string name="allow_compatibility_mixing">Allow unsupported compatibility</string>
|
||||
<string name="allow_compatibility_mixing_description">Permit apps and patches to be mixed in
|
||||
unsupported state</string>
|
||||
<string name="allow_compatibility_mixing_confirmation">Selecting incompatible patches can result
|
||||
in a broken app.\n\nAllow anyways?</string>
|
||||
<string name="patch_compat_check">Disable version compatibility check</string>
|
||||
<string name="patch_compat_check_description">The check restricts patches to supported app versions</string>
|
||||
<string name="suggested_version_safeguard">Require suggested app version</string>
|
||||
<string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string>
|
||||
<string name="patch_selection_safeguard">Allow changing patch selection</string>
|
||||
<string name="patch_selection_safeguard_description">Permit selecting or deselecting patches
|
||||
from default</string>
|
||||
<string name="patch_selection_safeguard_confirmation">Changing the selection of patches may
|
||||
cause unexpected issues.\n\nAllow anyways?</string>
|
||||
<string name="universal_patches_safeguard">Allow universal patch</string>
|
||||
<string name="universal_patches_safeguard_description">Permit selecting app\'s generic patch</string>
|
||||
<string name="universal_patches_safeguard_confirmation">Universal patch are not as well tested
|
||||
as those that target specific apps.\n\nAllow anyways?</string>
|
||||
<string name="backup">Backup</string>
|
||||
<string name="restore">Restore</string>
|
||||
<string name="keystore">Keystore</string>
|
||||
<string name="patch_selection">Patch selection</string>
|
||||
<string name="patch_options">Patch options</string>
|
||||
<string name="restore_keystore_description">Restore keystore from external source</string>
|
||||
<string name="restore_keystore_dialog_title">Enter keystore credentials</string>
|
||||
<string name="restore_keystore_dialog_description">You\'ll need enter the keystore’s credentials
|
||||
to restore it.</string>
|
||||
<string name="restore_keystore_dialog_alias_field">Username (Alias)</string>
|
||||
<string name="restore_keystore_dialog_password_field">Password</string>
|
||||
<string name="restore_keystore_dialog_button">Import</string>
|
||||
<string name="restore_keystore_wrong_credentials">Wrong keystore credentials</string>
|
||||
<string name="restore_keystore_success">Keystore successfully restored</string>
|
||||
<string name="backup_keystore_description">Export app’s keystore into usable file</string>
|
||||
<string name="backup_keystore_unavailable">No keystore to backup</string>
|
||||
<string name="backup_keystore_success">Keystore successfully backed up</string>
|
||||
<string name="regenerate_keystore">Regeneration</string>
|
||||
<string name="regenerate_keystore_description">Replace current keystore with new one</string>
|
||||
<string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches</string>
|
||||
<string name="universal_patches_safeguard">Disable universal patch warning</string>
|
||||
<string name="universal_patches_safeguard_description">Disables the warning that appears when you try to select universal patches</string>
|
||||
<string name="import_keystore">Import keystore</string>
|
||||
<string name="import_keystore_description">Import a custom keystore</string>
|
||||
<string name="import_keystore_dialog_title">Enter keystore credentials</string>
|
||||
<string name="import_keystore_dialog_description">You\'ll need enter the keystore’s credentials to import it.</string>
|
||||
<string name="import_keystore_dialog_alias_field">Username (Alias)</string>
|
||||
<string name="import_keystore_dialog_password_field">Password</string>
|
||||
<string name="import_keystore_dialog_button">Import</string>
|
||||
<string name="import_keystore_wrong_credentials">Wrong keystore credentials</string>
|
||||
<string name="import_keystore_success">Imported keystore</string>
|
||||
<string name="export_keystore">Export keystore</string>
|
||||
<string name="export_keystore_description">Export the current keystore</string>
|
||||
<string name="export_keystore_unavailable">No keystore to export</string>
|
||||
<string name="export_keystore_success">Exported keystore</string>
|
||||
<string name="regenerate_keystore">Regenerate keystore</string>
|
||||
<string name="regenerate_keystore_description">Generate a new keystore</string>
|
||||
<string name="regenerate_keystore_success">The keystore has been successfully replaced</string>
|
||||
<string name="restore_patch_selection_description">Export app’s patch selections into usable
|
||||
file</string>
|
||||
<string name="restore_patch_selection_fail">Could not import patch selection: %s</string>
|
||||
<string name="restore_patch_selection_success">Patch selection successfully restored</string>
|
||||
<string name="backup_patch_selection_description">Import app’s patch selections from external
|
||||
source</string>
|
||||
<string name="backup_patch_selection_fail">Could not backup patch selection: %s</string>
|
||||
<string name="backup_patch_selection_success">Patch selection successfully restored</string>
|
||||
<string name="import_patch_selection">Import patch selection</string>
|
||||
<string name="import_patch_selection_description">Import patch selection from a JSON file</string>
|
||||
<string name="import_patch_selection_fail">Could not import patch selection: %s</string>
|
||||
<string name="import_patch_selection_success">Imported patch selection</string>
|
||||
<string name="export_patch_selection">Export patch selection</string>
|
||||
<string name="export_patch_selection_description">Export patch selection to a JSON file</string>
|
||||
<string name="export_patch_selection_fail">Could not export patch selection: %s</string>
|
||||
<string name="export_patch_selection_success">Exported patch selection</string>
|
||||
<string name="reset_patch_selection">Reset patch selection</string>
|
||||
<string name="reset_patch_selection_description">Reset the stored patch selection</string>
|
||||
<string name="reset_patch_selection_success">Patch selection successfully reset</string>
|
||||
<string name="patch_options_reset_package">Reset for app</string>
|
||||
<string name="patch_options_reset_package_description">Use the default patch options
|
||||
configuration for a single app</string>
|
||||
<string name="patch_options_reset_bundle">Reset for bundle</string>
|
||||
<string name="patch_options_reset_bundle_description">Use the default patch options
|
||||
configuration for all patches in a bundle</string>
|
||||
<string name="patch_options_reset_all_description">Use the default patch options configuration</string>
|
||||
<string name="reset_patch_selection_success">Patch selection has been reset</string>
|
||||
<string name="patch_options_reset_package">Reset patch options for app</string>
|
||||
<string name="patch_options_reset_package_description">Resets patch options for a single app</string>
|
||||
<string name="patch_options_reset_bundle">Resets patch options for bundle</string>
|
||||
<string name="patch_options_reset_bundle_description">Resets patch options for all patches in a bundle</string>
|
||||
<string name="patch_options_reset_all">Reset patch options</string>
|
||||
<string name="patch_options_reset_all_description">Resets all patch options</string>
|
||||
<string name="downloader_plugins">Plugins</string>
|
||||
<string name="downloader_plugin_state_trusted">Trusted</string>
|
||||
<string name="downloader_plugin_state_failed">Failed to load. Click for more details</string>
|
||||
@ -170,8 +142,6 @@
|
||||
|
||||
<string name="options">Options</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="dialog_input_placeholder">Value</string>
|
||||
<string name="reset">Reset</string>
|
||||
@ -188,31 +158,26 @@
|
||||
<string name="warning">Warning</string>
|
||||
<string name="add">Add</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="clear">Clear</string>
|
||||
<string name="system">System</string>
|
||||
<string name="light">Light</string>
|
||||
<string name="dark">Dark</string>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="downloaded_apps">Downloaded apps</string>
|
||||
<string name="process_runtime">Run patcher in another process</string>
|
||||
<string name="process_runtime_description">Faster and allows patcher to use more memory</string>
|
||||
<string name="process_runtime">Run Patcher in another process (experimental)</string>
|
||||
<string name="process_runtime_description">This is faster and allows Patcher to use more memory.</string>
|
||||
<string name="process_runtime_memory_limit">Patcher process memory limit</string>
|
||||
<string name="process_runtime_memory_limit_description">The max amount of memory that the
|
||||
patcher process can use (in megabytes)</string>
|
||||
<string name="process_runtime_memory_limit_description">The max amount of memory that the Patcher process can use (in megabytes)</string>
|
||||
<string name="debug_logs_export">Export debug logs</string>
|
||||
<string name="debug_logs_export_read_failed">Failed to read logs (exit code %d)</string>
|
||||
<string name="debug_logs_export_failed">Failed to export logs</string>
|
||||
<string name="debug_logs_export_success">Exported logs</string>
|
||||
<string name="api_url">API Source</string>
|
||||
<string name="api_url_dialog_title">Set custom API Source</string>
|
||||
<string name="api_url_dialog_description">Set the API Source of ReVanced Manager. ReVanced
|
||||
Manager uses the API to download patches and updates.</string>
|
||||
<string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches
|
||||
and updates. Make sure that you trust it.</string>
|
||||
<string name="api_url">API URL</string>
|
||||
<string name="api_url_description">The API used to download necessary files.</string>
|
||||
<string name="api_url_dialog_title">Set custom API URL</string>
|
||||
<string name="api_url_dialog_description">Set the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates.</string>
|
||||
<string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it.</string>
|
||||
<string name="api_url_dialog_save">Set</string>
|
||||
<string name="api_url_dialog_reset">Reset API Source</string>
|
||||
<string name="testing">Testing</string>
|
||||
<string name="disable_safeguard">Disable all safeguards</string>
|
||||
<string name="api_url_dialog_reset">Reset API URL</string>
|
||||
<string name="device">Device</string>
|
||||
<string name="device_android_version">Android version</string>
|
||||
<string name="device_model">Model</string>
|
||||
@ -239,34 +204,24 @@
|
||||
<string name="no_patched_apps_found">No patched apps found</string>
|
||||
<string name="tap_on_patches">Tap on the patches to get more information about them</string>
|
||||
<string name="bundles_selected">%s selected</string>
|
||||
<string name="unsupported_patches">Incompatible patches</string>
|
||||
<string name="unsupported_patches">Unsupported patches</string>
|
||||
<string name="universal_patches">Universal patches</string>
|
||||
<string name="patch_selection_reset_toast">Patch selection and options has been reset to
|
||||
recommended defaults</string>
|
||||
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
|
||||
<string name="patch_options_reset_toast">Patch options have been reset</string>
|
||||
<string name="non_suggested_version_warning_title">Non suggested version</string>
|
||||
<string name="non_suggested_version_warning_description">The version of the app you have
|
||||
selected does not match the suggested version.\nPlease use the suggested version: %s\n\nTo
|
||||
continue anyway, disable \"Require suggested app version\" in the advanced settings.</string>
|
||||
<string name="non_suggested_version_warning_description">The version of the app you have selected does not match the suggested version.\nPlease use the suggested version: %s\n\nTo continue anyway, disable \"Require suggested app version\" in the advanced settings.</string>
|
||||
<string name="selection_warning_title">Stop using defaults?</string>
|
||||
<string name="selection_warning_description">It is recommended to use the default patch
|
||||
selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on
|
||||
\"Allow changing patch selection\" in the advanced settings before toggling patches.</string>
|
||||
<string name="universal_patch_warning_description">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.\n\nThis warning can be disabled in the advanced settings.</string>
|
||||
<string name="supported">This version</string>
|
||||
<string name="universal">Any app</string>
|
||||
<string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on \"Allow changing patch selection\" in the advanced settings before toggling patches.</string>
|
||||
<string name="universal_patch_warning_description">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.\n\nThis warning can be disabled in the advanced settings.</string>
|
||||
<string name="supported">Supported</string>
|
||||
<string name="universal">Universal</string>
|
||||
<string name="unsupported">Unsupported</string>
|
||||
<string name="search_patches">Search patches</string>
|
||||
<string name="app_not_supported">This patch is not compatible with the selected app version
|
||||
(%1$s).\n\nIt only supports the following version(s): %2$s.</string>
|
||||
<string name="search_patches">Patch name</string>
|
||||
<string name="app_not_supported">This patch is not compatible with the selected app version (%1$s).\n\nIt only supports the following version(s): %2$s.</string>
|
||||
<string name="continue_with_version">Continue with this version?</string>
|
||||
<string name="version_not_supported">Not all patches support this version (%s). Do you want to
|
||||
continue anyway?</string>
|
||||
<string name="version_not_supported">Not all patches support this version (%s). Do you want to continue anyway?</string>
|
||||
<string name="download_application">Download application?</string>
|
||||
<string name="app_not_installed">The app you selected isn\'t installed. Do you want to download
|
||||
it?</string>
|
||||
<string name="app_not_installed">The app you selected isn\'t installed. Do you want to download it?</string>
|
||||
<string name="failed_to_load_apk">Failed to load APK</string>
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="not_installed">Not installed</string>
|
||||
@ -296,8 +251,7 @@
|
||||
<string name="downloader_app_not_found">Downloader did not find the app</string>
|
||||
<string name="downloader_error">Downloader error: %s</string>
|
||||
<string name="downloader_no_plugins_installed">No plugins installed.</string>
|
||||
<string name="downloader_no_plugins_available">No trusted plugins available for use. Check your
|
||||
settings.</string>
|
||||
<string name="downloader_no_plugins_available">No trusted plugins available for use. Check your settings.</string>
|
||||
<string name="already_patched">Already patched</string>
|
||||
|
||||
<string name="patch_selector_sheet_filter_title">Filter</string>
|
||||
@ -325,8 +279,7 @@
|
||||
<string name="save_apk_success">APK Saved</string>
|
||||
<string name="sign_fail">Failed to sign APK: %s</string>
|
||||
<string name="save_logs">Save logs</string>
|
||||
<string name="plugin_activity_dialog_body">User interaction is required in order to proceed with
|
||||
this plugin.</string>
|
||||
<string name="plugin_activity_dialog_body">User interaction is required in order to proceed with this plugin.</string>
|
||||
<string name="select_install_type">Select installation type</string>
|
||||
|
||||
<string name="patcher_step_group_preparing">Preparing</string>
|
||||
@ -368,43 +321,36 @@
|
||||
<string name="bundle_update_success">Successfully updated %s</string>
|
||||
<string name="bundle_update_unavailable">No update available for %s</string>
|
||||
<string name="bundle_auto_update">Auto update</string>
|
||||
<string name="bundle_auto_update_description">Automatically update this bundle when ReVanced
|
||||
starts</string>
|
||||
<string name="bundle_auto_update_description">Automatically update this bundle when ReVanced starts</string>
|
||||
<string name="bundle_view_patches">View patches</string>
|
||||
<string name="bundle_view_patches_any_version">Any version</string>
|
||||
<string name="bundle_view_patches_any_package">Any package</string>
|
||||
|
||||
<string name="about_revanced_manager">About ReVanced Manager</string>
|
||||
<string name="revanced_manager_description">ReVanced Manager is an application designed to work
|
||||
with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps.
|
||||
The patching system is designed to automatically work with new versions of apps with minimal
|
||||
maintenance.</string>
|
||||
<string name="revanced_manager_description">ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.</string>
|
||||
<string name="update_available">An update is available</string>
|
||||
<string name="current_version">Current version: %s</string>
|
||||
<string name="new_version">New version: %s</string>
|
||||
<string name="ready_to_install_update">Ready to install update</string>
|
||||
<string name="update_completed">Update installed</string>
|
||||
<string name="install_update_manager_failed">Failed to install update</string>
|
||||
<string name="check_for_update">Check for update</string>
|
||||
<string name="check_for_update_auto_description">Automatically check for new version when the
|
||||
app launched</string>
|
||||
<string name="manual_update_check">Check for updates</string>
|
||||
<string name="manual_update_check_description">Manually check for updates</string>
|
||||
<string name="update_checking_manager">Auto check for updates</string>
|
||||
<string name="update_checking_manager_description">Check for new versions of ReVanced Manager when the application starts</string>
|
||||
<string name="changelog">View changelogs</string>
|
||||
<string name="changelog_loading">Loading changelog</string>
|
||||
<string name="changelog_download_fail">Failed to download changelog: %s</string>
|
||||
<string name="changelog_description">Check out the latest changes in this update</string>
|
||||
<string name="battery_optimization_notification">Battery optimizations must be turned off in
|
||||
order for ReVanced Manager to work correctly in the background. Click here to turn off
|
||||
optimizations.</string>
|
||||
<string name="battery_optimization_notification">Battery optimizations must be turned off in order for ReVanced Manager to work correctly in the background. Click here to turn off optimizations.</string>
|
||||
<string name="installing_manager_update">Installing update…</string>
|
||||
<string name="downloading_manager_update">Downloading update…</string>
|
||||
<string name="download_manager_failed">Failed to download update: %s</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="save_with_count">Save (%1$s)</string>
|
||||
<string name="update">Update</string>
|
||||
<string name="empty">Empty</string>
|
||||
<string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will
|
||||
close when updating.</string>
|
||||
<string name="installing_message">Tap on <b>Update</b> when prompted. \n ReVanced Manager will close when updating.</string>
|
||||
<string name="no_changelogs_found">No changelogs found</string>
|
||||
<string name="just_now">Just now</string>
|
||||
<string name="minutes_ago">%sm ago</string>
|
||||
@ -413,29 +359,23 @@
|
||||
<string name="invalid_date">Invalid date</string>
|
||||
<string name="disable_battery_optimization">Disable battery optimization</string>
|
||||
<string name="input_dialog_value_invalid">Invalid value</string>
|
||||
<string name="option_required">This option is required</string>
|
||||
<string name="required_options_screen">Required options</string>
|
||||
|
||||
<string name="failed_to_check_updates">Failed to check for updates: %s</string>
|
||||
<string name="no_update_available">No update available</string>
|
||||
<string name="update_check">Checking for updates…</string>
|
||||
<string name="dismiss_temporary">Not now</string>
|
||||
<string name="update_available_dialog_description">A new version of ReVanced Manager (%s) is
|
||||
available.</string>
|
||||
<string name="update_available_dialog_description">A new version of ReVanced Manager (%s) is available.</string>
|
||||
<string name="failed_to_download_update">Failed to download update: %s</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="download_confirmation_metered">You are currently on a metered connection, and data
|
||||
charges from your service provider may apply.\n\nDo you still want to continue?</string>
|
||||
<string name="download_confirmation_metered">You are currently on a metered connection, and data charges from your service provider may apply.\n\nDo you still want to continue?</string>
|
||||
<string name="download_update_confirmation">Download update?</string>
|
||||
<string name="no_contributors_found">No contributors found</string>
|
||||
<string name="select">Select</string>
|
||||
<string name="select_deselect_all">Select or deselect all</string>
|
||||
<string name="select_bundle_type_dialog_title">Add new bundle</string>
|
||||
<string name="select_bundle_type_dialog_description">Add a new bundle from a URL or storage</string>
|
||||
<string name="local_bundle_description">Import local files from your storage, does not
|
||||
automatically update</string>
|
||||
<string name="remote_bundle_description">Import remote files from a URL, can automatically
|
||||
update</string>
|
||||
<string name="local_bundle_description">Import local files from your storage, does not automatically update</string>
|
||||
<string name="remote_bundle_description">Import remote files from a URL, can automatically update</string>
|
||||
<string name="recommended">Recommended</string>
|
||||
|
||||
<string name="installation_failed_dialog_title">Installation failed</string>
|
||||
@ -446,20 +386,13 @@
|
||||
<string name="installation_invalid_dialog_title">Installation invalid</string>
|
||||
<string name="installation_storage_issue_dialog_title">Not enough storage</string>
|
||||
<string name="installation_timeout_dialog_title">Installation timed out</string>
|
||||
<string name="installation_failed_description">The installation failed due to an unknown reason.
|
||||
Try again?</string>
|
||||
<string name="installation_aborted_description">The installation was cancelled manually. Try
|
||||
again?</string>
|
||||
<string name="installation_blocked_description">The installation was blocked. Review your device
|
||||
security settings and try again.</string>
|
||||
<string name="installation_conflict_description">The installation was prevented by an existing
|
||||
installation of the app. Uninstall the installed app and try again?</string>
|
||||
<string name="installation_incompatible_description">The app is incompatible with this device.
|
||||
Use an APK that is supported by this device and try again.</string>
|
||||
<string name="installation_invalid_description">The app is invalid. Uninstall the app and try
|
||||
again?</string>
|
||||
<string name="installation_storage_issue_description">The app could not be installed due to
|
||||
insufficient storage. Free up some space and try again.</string>
|
||||
<string name="installation_failed_description">The installation failed due to an unknown reason. Try again?</string>
|
||||
<string name="installation_aborted_description">The installation was cancelled manually. Try again?</string>
|
||||
<string name="installation_blocked_description">The installation was blocked. Review your device security settings and try again.</string>
|
||||
<string name="installation_conflict_description">The installation was prevented by an existing installation of the app. Uninstall the installed app and try again?</string>
|
||||
<string name="installation_incompatible_description">The app is incompatible with this device. Use an APK that is supported by this device and try again.</string>
|
||||
<string name="installation_invalid_description">The app is invalid. Uninstall the app and try again?</string>
|
||||
<string name="installation_storage_issue_description">The app could not be installed due to insufficient storage. Free up some space and try again.</string>
|
||||
<string name="installation_timeout_description">The installation took too long. Try again?</string>
|
||||
<string name="reinstall">Reinstall</string>
|
||||
<string name="show">Show</string>
|
||||
@ -470,14 +403,10 @@
|
||||
<string name="add_patch_bundle">Add patch bundle</string>
|
||||
<string name="bundle_url">Bundle URL</string>
|
||||
<string name="auto_update">Auto update</string>
|
||||
<string name="unsupported_patches_dialog">These patches are not compatible with the selected app
|
||||
version (%1$s).\n\nClick on the patches to see more details.</string>
|
||||
<string name="unsupported_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string>
|
||||
<string name="unsupported_patch">Unsupported patch</string>
|
||||
<string name="any_version">Any</string>
|
||||
<string name="never_show_again">Never show again</string>
|
||||
<string name="show_manager_update_dialog_on_launch">Show update message on launch</string>
|
||||
<string name="show_manager_update_dialog_on_launch_description">Shows a popup notification
|
||||
whenever there is a new update available on launch.</string>
|
||||
<string name="failed_to_import_keystore">Failed to import keystore</string>
|
||||
<string name="export">Export</string>
|
||||
</resources>
|
||||
<string name="show_manager_update_dialog_on_launch_description">Shows a popup notification whenever there is a new update available on launch.</string>
|
||||
</resources>
|
||||
|
@ -1,41 +1,42 @@
|
||||
[versions]
|
||||
ktx = "1.15.0"
|
||||
material3 = "1.3.1"
|
||||
ui-tooling = "1.7.7"
|
||||
ui-tooling = "1.7.6"
|
||||
viewmodel-lifecycle = "2.8.7"
|
||||
splash-screen = "1.0.1"
|
||||
activity = "1.10.0"
|
||||
activity = "1.9.3"
|
||||
appcompat = "1.7.0"
|
||||
preferences-datastore = "1.1.2"
|
||||
preferences-datastore = "1.1.1"
|
||||
work-runtime = "2.10.0"
|
||||
compose-bom = "2025.01.01"
|
||||
navigation = "2.8.6"
|
||||
accompanist = "0.37.0"
|
||||
compose-bom = "2024.12.01"
|
||||
accompanist = "0.34.0"
|
||||
placeholder = "1.1.2"
|
||||
reorderable = "2.4.3"
|
||||
serialization = "1.8.0"
|
||||
reorderable = "1.5.2"
|
||||
serialization = "1.7.3"
|
||||
collection = "0.3.8"
|
||||
datetime = "0.6.1"
|
||||
datetime = "0.6.0"
|
||||
room-version = "2.6.1"
|
||||
revanced-patcher = "21.0.0"
|
||||
revanced-library = "3.0.2"
|
||||
koin = "3.5.3"
|
||||
koin-version = "3.5.3"
|
||||
koin-version-compose = "3.5.3"
|
||||
reimagined-navigation = "1.5.0"
|
||||
ktor = "2.3.9"
|
||||
markdown-renderer = "0.30.0"
|
||||
markdown-renderer = "0.22.0"
|
||||
fading-edges = "1.0.4"
|
||||
kotlin = "2.1.10"
|
||||
android-gradle-plugin = "8.8.0"
|
||||
dev-tools-gradle-plugin = "2.1.10-1.0.29"
|
||||
about-libraries-gradle-plugin = "11.5.0"
|
||||
kotlin = "2.1.0"
|
||||
android-gradle-plugin = "8.7.3"
|
||||
dev-tools-gradle-plugin = "2.1.0-1.0.29"
|
||||
about-libraries-gradle-plugin = "11.1.1"
|
||||
binary-compatibility-validator = "0.17.0"
|
||||
coil = "2.7.0"
|
||||
coil = "2.6.0"
|
||||
app-icon-loader-coil = "1.5.0"
|
||||
skrapeit = "1.2.2"
|
||||
libsu = "6.0.0"
|
||||
libsu = "5.2.2"
|
||||
scrollbars = "1.0.4"
|
||||
enumutil = "1.1.1"
|
||||
enumutil = "1.1.0"
|
||||
compose-icons = "1.2.4"
|
||||
kotlin-process = "1.5.1"
|
||||
kotlin-process = "1.4.1"
|
||||
hidden-api-stub = "4.3.3"
|
||||
|
||||
[libraries]
|
||||
@ -56,9 +57,8 @@ compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "ui-tooling" }
|
||||
compose-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
|
||||
compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
|
||||
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" }
|
||||
|
||||
# Coil
|
||||
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
||||
@ -85,10 +85,12 @@ revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.
|
||||
revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" }
|
||||
|
||||
# Koin
|
||||
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
|
||||
koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }
|
||||
koin-compose-navigation = { group = "io.insert-koin", name = "koin-androidx-compose-navigation", version.ref = "koin" }
|
||||
koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin" }
|
||||
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin-version" }
|
||||
koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin-version-compose" }
|
||||
koin-workmanager = { group = "io.insert-koin", name = "koin-androidx-workmanager", version.ref = "koin-version" }
|
||||
|
||||
# Compose Navigation
|
||||
reimagined-navigation = { group = "dev.olshevski.navigation", name = "reimagined", version.ref = "reimagined-navigation" }
|
||||
|
||||
# About Libraries
|
||||
about-libraries = { group = "com.mikepenz", name = "aboutlibraries-compose", version.ref = "about-libraries-gradle-plugin" }
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,7 +1,7 @@
|
||||
#Tue Nov 12 21:36:50 CET 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
6
gradlew
vendored
6
gradlew
vendored
@ -15,8 +15,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@ -57,7 +55,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@ -86,7 +84,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
22
gradlew.bat
vendored
22
gradlew.bat
vendored
@ -13,8 +13,6 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
@ -59,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
|
Reference in New Issue
Block a user