Compare commits

..

20 Commits

Author SHA1 Message Date
1851a35e10 Revert api_source to api_url 2025-02-05 19:04:36 +03:00
f4a2b06276 Remove unnecessary changes and deprecated code 2025-02-05 19:03:25 +03:00
bbfd625ac1 fix: Compilation 2025-02-02 13:03:11 +07:00
c012a3e9c0 feat: Settings v2
Signed-off-by: validcube <pun.butrach@gmail.com>
2025-02-02 00:41:37 +07:00
e10e5e4e3f build(deps): bump the gradle-compose group with 16 updates (#2407) 2025-02-01 10:45:14 +07:00
ede1ab5ed4 feat: Reorder Import & Export settings (#2403) 2025-02-01 04:05:28 +03:00
1092188ab0 feat: TopAppBar scroll behavior (#2397) 2025-01-31 15:03:50 +03:00
f348eba115 ci: Generate release artifact provenance (#2324)
Signed-off-by: validcube <pun.butrach@gmail.com>
2025-01-29 20:43:16 +01:00
3d234820a3 fix: improve keystore import error handling and show toast 2025-01-29 19:58:38 +01:00
cd06d36f68 build: Enable pseudo locale for debug variant 2025-01-29 23:22:24 +07:00
242c4570ce chore: Update project's dependencies to latest 2025-01-29 23:17:34 +07:00
71b73a3b42 fix: show install button when installation has been cancelled 2025-01-29 15:17:41 +01:00
067020f38f feat: Screen slide transition (#2396) 2025-01-27 19:28:55 +07:00
2aef67872d fix: Offset badge 2025-01-27 03:56:15 +03:00
818dc09aa4 build: Bump AGP to 8.8.0
build: Bump AGP to 8.8.0
2025-01-19 17:47:56 +07:00
a762969966 docs: Merge documentation from Flutter to Compose 2025-01-19 17:08:07 +07:00
74338931b8 feat: Redesign the patches screen (#2381) 2025-01-18 17:03:38 +01:00
0ab424bfdb fix: available updates dialog list item color 2025-01-05 00:12:00 +01:00
fff1a41fee refactor: use EventEffect for legacy import 2025-01-05 00:08:29 +01:00
7644a74648 feat: add required options screen (#2378) 2025-01-03 22:26:40 +01:00
41 changed files with 1085 additions and 574 deletions

View File

@ -9,6 +9,9 @@ jobs:
build:
name: Build
runs-on: ubuntu-latest
permissions:
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- name: Set env
@ -41,6 +44,11 @@ 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:

View File

@ -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-round.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
<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" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
@ -70,9 +70,8 @@ If a vulnerability is confirmed and accepted, you can join our [Discord](https:/
### ⏳ Supported Versions
| Version | Branch | Supported |
| ------- | ------------|------------------- |
| v1.18.0 | main | :white_check_mark: |
| latest | dev | :white_check_mark: |
| latest | compose-dev | :white_check_mark: |
| Version | Branch | Supported |
| --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------ |
| ![Latest stable release](https://img.shields.io/github/v/release/ReVanced/revanced-manager?style=for-the-badge "Latest stable release") | main | :white_check_mark: |
| ![Latest version](https://img.shields.io/badge/version-latest-brightgreen?style=for-the-badge "Latest version") | dev | :white_check_mark: |
| ![Latest version](https://img.shields.io/badge/version-latest-brightgreen?style=for-the-badge "Latest version") | compose-dev | :white_check_mark: |

View File

@ -13,7 +13,7 @@ plugins {
android {
namespace = "app.revanced.manager"
compileSdk = 35
buildToolsVersion = "35.0.0"
buildToolsVersion = "35.0.1"
defaultConfig {
applicationId = "app.revanced.manager"
@ -28,6 +28,7 @@ android {
debug {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager (dev)")
isPseudoLocalesEnabled = true
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
}
@ -43,6 +44,8 @@ android {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager Debug")
signingConfig = signingConfigs.getByName("debug")
isPseudoLocalesEnabled = true
}
buildConfigField("long", "BUILD_ID", "0L")

View File

@ -1,11 +1,16 @@
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
@ -20,11 +25,32 @@ 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.*
import app.revanced.manager.ui.screen.*
import app.revanced.manager.ui.screen.settings.*
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.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.screen.settings.update.UpdatesSettingsScreen
import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel
@ -46,12 +72,22 @@ class MainActivity : ComponentActivity() {
installSplashScreen()
val vm: MainViewModel = getActivityViewModel()
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
@ -76,6 +112,10 @@ private fun ReVancedManager(vm: MainViewModel) {
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(
@ -229,32 +269,28 @@ private fun ReVancedManager(vm: MainViewModel) {
}
composable<Settings.General> {
GeneralSettingsScreen(onBackClick = navController::popBackStack)
GeneralSettingsScreen(
onBackClick = navController::popBackStack,
onUpdateClick = { navController.navigate(Update()) }
)
}
composable<Settings.Advanced> {
AdvancedSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.Updates> {
UpdatesSettingsScreen(
onBackClick = navController::popBackStack,
onChangelogClick = { navController.navigate(Settings.Changelogs) },
onUpdateClick = { navController.navigate(Update()) }
)
}
composable<Settings.Downloads> {
DownloadsSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.ImportExport> {
ImportExportSettingsScreen(onBackClick = navController::popBackStack)
BackupRestoreSettingsScreen(onBackClick = navController::popBackStack)
}
composable<Settings.About> {
AboutSettingsScreen(
onBackClick = navController::popBackStack,
onChangelogClick = { navController.navigate(Settings.Changelogs) },
navigate = navController::navigate
)
}

View File

@ -4,9 +4,20 @@ 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.*
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.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
@ -44,9 +55,14 @@ fun AppTopBar(
)
},
actions: @Composable (RowScope.() -> Unit) = {},
scrollBehavior: TopAppBarScrollBehavior? = null
scrollBehavior: TopAppBarScrollBehavior? = null,
applyContainerColor: Boolean = false
) {
val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
val containerColor = if (applyContainerColor) {
MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
} else {
Color.Unspecified
}
TopAppBar(
title = { Text(title) },

View File

@ -12,11 +12,12 @@ 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,
@ -70,10 +71,11 @@ fun AvailableUpdateDialog(
Text(stringResource(R.string.never_show_again))
},
leadingContent = {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
HapticCheckbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it })
}
}
},
colors = transparentListItemColors
)
}
},

View File

@ -0,0 +1,61 @@
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
)
}

View File

@ -0,0 +1,60 @@
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
)
}
}

View File

@ -16,6 +16,7 @@ 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
@ -218,7 +219,7 @@ fun ImportBundleStep(
),
headlineContent = { Text(stringResource(R.string.auto_update)) },
leadingContent = {
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
HapticCheckbox(
checked = autoUpdate,
onCheckedChange = {

View File

@ -70,9 +70,6 @@ object Settings {
@Serializable
data object Advanced : Destination
@Serializable
data object Updates : Destination
@Serializable
data object Downloads : Destination

View File

@ -3,12 +3,26 @@ 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.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.*
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.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -17,6 +31,7 @@ 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
@ -138,10 +153,13 @@ 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 }) {
@ -149,7 +167,8 @@ fun AppSelectorScreen(
}
}
)
}
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues ->
LazyColumnWithScrollbar(
modifier = Modifier

View File

@ -7,17 +7,46 @@ import android.provider.Settings
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
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.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.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.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
@ -33,9 +62,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
@ -171,11 +200,7 @@ fun DashboardScreen(
) {
BadgedBox(
badge = {
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,
)
Badge(modifier = Modifier.size(6.dp))
}
) {
Icon(Icons.Outlined.Update, stringResource(R.string.update))
@ -185,7 +210,8 @@ fun DashboardScreen(
IconButton(onClick = onSettingsClick) {
Icon(Icons.Outlined.Settings, stringResource(R.string.settings))
}
}
},
applyContainerColor = true
)
}
},
@ -238,7 +264,9 @@ fun DashboardScreen(
}
}
val showBatteryOptimizationsWarning by vm.showBatteryOptimizationsWarningFlow.collectAsStateWithLifecycle(false)
val showBatteryOptimizationsWarning by vm.showBatteryOptimizationsWarningFlow.collectAsStateWithLifecycle(
false
)
Notifications(
if (!Aapt.supportsDevice()) {
{

View File

@ -21,6 +21,8 @@ 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
@ -29,6 +31,7 @@ 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
@ -64,13 +67,17 @@ 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

View File

@ -121,9 +121,10 @@ fun PatcherScreen(
}
AppScaffold(
topBar = {
topBar = { scrollBehavior ->
AppTopBar(
title = stringResource(R.string.patcher),
scrollBehavior = scrollBehavior,
onBackClick = ::leaveScreen
)
},

View File

@ -1,16 +1,50 @@
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.*
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.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.outlined.*
import androidx.compose.material3.*
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.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@ -19,8 +53,10 @@ 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
@ -31,15 +67,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.SearchView
import app.revanced.manager.ui.component.SearchBar
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
@ -48,7 +84,7 @@ import app.revanced.manager.util.isScrollingUp
import app.revanced.manager.util.transparentListItemColors
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun PatchesSelectorScreen(
onSave: (PatchSelection?, Options) -> Unit,
@ -63,20 +99,17 @@ fun PatchesSelectorScreen(
bundles.size
}
val composableScope = rememberCoroutineScope()
var search: String? by rememberSaveable {
mutableStateOf(null)
val (query, setQuery) = rememberSaveable {
mutableStateOf("")
}
val (searchExpanded, setSearchExpanded) = rememberSaveable {
mutableStateOf(false)
}
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
val showSaveButton by remember {
derivedStateOf { vm.selectionIsValid(bundles) }
}
val availablePatchCount by remember {
derivedStateOf {
bundles.sumOf { it.patchCount }
}
}
val defaultPatchSelectionCount by vm.defaultSelectionCount
.collectAsStateWithLifecycle(initialValue = 0)
@ -108,27 +141,22 @@ fun PatchesSelectorScreen(
style = MaterialTheme.typography.titleMedium
)
Row(
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp)
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
FilterChip(
selected = vm.filter and SHOW_SUPPORTED != 0,
onClick = { vm.toggleFlag(SHOW_SUPPORTED) },
CheckedFilterChip(
selected = vm.filter and SHOW_UNSUPPORTED == 0,
onClick = { vm.toggleFlag(SHOW_UNSUPPORTED) },
label = { Text(stringResource(R.string.supported)) }
)
FilterChip(
CheckedFilterChip(
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)) },
)
}
}
}
@ -175,20 +203,21 @@ fun PatchesSelectorScreen(
fun LazyListScope.patchList(
uid: Int,
patches: List<PatchInfo>,
filterFlag: Int,
visible: Boolean,
supported: Boolean,
header: (@Composable () -> Unit)? = null
) {
if (patches.isNotEmpty() && (vm.filter and filterFlag) != 0 || vm.filter == 0) {
if (patches.isNotEmpty() && visible) {
header?.let {
item {
item(contentType = 0) {
it()
}
}
items(
items = patches,
key = { it.name }
key = { it.name },
contentType = { 1 }
) { patch ->
PatchItem(
patch = patch,
@ -222,102 +251,142 @@ fun PatchesSelectorScreen(
}
}
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()
) {
fun List<PatchInfo>.searched() = filter {
it.name.contains(query, true)
}
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
) {
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))
}
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 = {
search = ""
if (searchExpanded) {
setSearchExpanded(false)
} else {
onBackClick()
}
}
) {
Icon(Icons.Outlined.Search, stringResource(R.string.search))
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)
)
}
}
}
}
)
) {
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 }
)
}
}
}
},
floatingActionButton = {
if (!showSaveButton) return@Scaffold
HapticExtendedFloatingActionButton(
text = { Text(stringResource(R.string.save)) },
icon = {
Icon(
Icons.Outlined.Save,
stringResource(R.string.save)
AnimatedVisibility(
visible = !searchExpanded,
enter = slideInHorizontally { it } + fadeIn(),
exit = slideOutHorizontally { it } + fadeOut()
) {
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
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())
}
)
},
expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp
?: true,
onClick = {
onSave(vm.getCustomSelection(), vm.getOptions())
}
)
}
}
) { paddingValues ->
Column(
Modifier
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
@ -359,13 +428,13 @@ fun PatchesSelectorScreen(
patchList(
uid = bundle.uid,
patches = bundle.supported,
filterFlag = SHOW_SUPPORTED,
visible = true,
supported = true
)
patchList(
uid = bundle.uid,
patches = bundle.universal,
filterFlag = SHOW_UNIVERSAL,
visible = vm.filter and SHOW_UNIVERSAL != 0,
supported = true
) {
ListHeader(
@ -375,7 +444,7 @@ fun PatchesSelectorScreen(
patchList(
uid = bundle.uid,
patches = bundle.unsupported,
filterFlag = SHOW_UNSUPPORTED,
visible = vm.filter and SHOW_UNSUPPORTED != 0,
supported = vm.allowIncompatiblePatches
) {
ListHeader(

View File

@ -15,6 +15,8 @@ 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
@ -22,6 +24,7 @@ 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
@ -66,10 +69,13 @@ fun RequiredOptionsScreen(
}
val composableScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.required_options_screen),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick
)
},
@ -90,7 +96,8 @@ fun RequiredOptionsScreen(
onContinue(vm.getCustomSelection(), vm.getOptions())
}
)
}
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues ->
Column(
Modifier

View File

@ -12,12 +12,21 @@ 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.*
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.runtime.Composable
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
@ -75,10 +84,14 @@ fun SelectedAppInfoScreen(
val composableScope = rememberCoroutineScope()
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.app_info),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick
)
},
@ -114,7 +127,8 @@ fun SelectedAppInfoScreen(
}
}
)
}
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues ->
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())

View File

@ -4,8 +4,14 @@ 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.*
import androidx.compose.material3.*
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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -22,18 +28,13 @@ private val settingsSections = listOf(
Icons.Outlined.Settings
) to Settings.General,
Triple(
R.string.updates,
R.string.updates_description,
Icons.Outlined.Update
) to Settings.Updates,
Triple(
R.string.downloads,
R.string.downloads_description,
R.string.extensions,
R.string.extensions_description,
Icons.Outlined.Download
) to Settings.Downloads,
Triple(
R.string.import_export,
R.string.import_export_description,
R.string.backup_restore,
R.string.backup_restore_description,
Icons.Outlined.SwapVert
) to Settings.ImportExport,
Triple(

View File

@ -24,10 +24,13 @@ 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
@ -52,13 +55,17 @@ 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(

View File

@ -24,10 +24,13 @@ 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
@ -48,6 +51,7 @@ import org.koin.androidx.compose.koinViewModel
@Composable
fun AboutSettingsScreen(
onBackClick: () -> Unit,
onChangelogClick: () -> Unit,
navigate: (Settings.Destination) -> Unit,
viewModel: AboutViewModel = koinViewModel()
) {
@ -105,6 +109,11 @@ 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 = {
@ -127,13 +136,17 @@ fun AboutSettingsScreen(
)
)
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

View File

@ -10,14 +10,34 @@ 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.*
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.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.*
import androidx.compose.runtime.*
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.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
@ -53,14 +73,17 @@ 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
@ -78,17 +101,10 @@ fun AdvancedSettingsScreen(
defaultUrl = vm.prefs.api.default,
onSubmit = {
showApiUrlDialog = false
it?.let(vm::setApiUrl)
it?.let(vm::setApiSource)
}
)
}
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(
@ -104,13 +120,28 @@ fun AdvancedSettingsScreen(
description = R.string.process_runtime_memory_limit_description,
)
GroupHeader(stringResource(R.string.safeguards))
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(
preference = vm.prefs.disablePatchVersionCompatCheck,
coroutineScope = vm.viewModelScope,
headline = R.string.patch_compat_check,
description = R.string.patch_compat_check_description,
confirmationText = R.string.patch_compat_check_confirmation
headline = R.string.allow_compatibility_mixing,
description = R.string.allow_compatibility_mixing_description,
confirmationText = R.string.allow_compatibility_mixing_confirmation
)
SafeguardBooleanItem(
preference = vm.prefs.disableUniversalPatchWarning,
@ -119,13 +150,6 @@ fun AdvancedSettingsScreen(
description = R.string.universal_patches_safeguard_description,
confirmationText = R.string.universal_patches_safeguard_confirmation
)
SafeguardBooleanItem(
preference = vm.prefs.suggestedVersionSafeguard,
coroutineScope = vm.viewModelScope,
headline = R.string.suggested_version_safeguard,
description = R.string.suggested_version_safeguard_description,
confirmationText = R.string.suggested_version_safeguard_confirmation
)
SafeguardBooleanItem(
preference = vm.prefs.disableSelectionWarning,
coroutineScope = vm.viewModelScope,
@ -134,6 +158,21 @@ fun AdvancedSettingsScreen(
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,
)
GroupHeader(stringResource(R.string.debugging))
val exportDebugLogsLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {

View File

@ -13,7 +13,17 @@ 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.*
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -22,6 +32,7 @@ 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
@ -37,118 +48,82 @@ 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 ImportExportSettingsScreen(
fun BackupRestoreSettingsScreen(
onBackClick: () -> Unit,
vm: ImportExportViewModel = koinViewModel()
viewModel: ImportExportViewModel = koinViewModel()
) {
val context = LocalContext.current
val importKeystoreLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
it?.let { uri -> vm.startKeystoreImport(uri) }
it?.let { uri -> viewModel.startKeystoreImport(uri) }
}
val exportKeystoreLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) {
it?.let(vm::exportKeystore)
it?.let(viewModel::exportKeystore)
}
val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
val patchBundles by viewModel.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
val packagesWithOptions by viewModel.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
vm.selectionAction?.let { action ->
viewModel.selectionAction?.let { action ->
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
if (uri == null) {
vm.clearSelectionAction()
viewModel.clearSelectionAction()
} else {
vm.executeSelectionAction(uri)
viewModel.executeSelectionAction(uri)
}
}
if (vm.selectedBundle == null) {
if (viewModel.selectedBundle == null) {
BundleSelector(patchBundles) {
if (it == null) {
vm.clearSelectionAction()
viewModel.clearSelectionAction()
} else {
vm.selectBundle(it)
viewModel.selectBundle(it)
launcher.launch(action.activityArg)
}
}
}
}
if (vm.showCredentialsDialog) {
if (viewModel.showCredentialsDialog) {
KeystoreCredentialsDialog(
onDismissRequest = vm::cancelKeystoreImport,
onDismissRequest = viewModel::cancelKeystoreImport,
onSubmit = { cn, pass ->
vm.viewModelScope.launch {
val result = vm.tryKeystoreImport(cn, pass)
if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
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))
}
}
}
)
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.import_export),
title = stringResource(R.string.backup_restore),
scrollBehavior = scrollBehavior,
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)
}
@ -156,37 +131,108 @@ fun ImportExportSettingsScreen(
mutableStateOf(false)
}
if (showPackageSelector)
if (showPackageSelector) {
PackageSelector(packages = packagesWithOptions) { selected ->
selected?.let(vm::resetOptionsForPackage)
selected?.let(viewModel::resetOptionsForPackage)
showPackageSelector = false
}
}
if (showBundleSelector)
if (showBundleSelector) {
BundleSelector(bundles = patchBundles) { bundle ->
bundle?.let(vm::clearOptionsForBundle)
bundle?.let(viewModel::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 = vm::resetOptions,
headline = R.string.patch_options_reset_all,
onClick = viewModel::resetOptions,
headline = {
Text(
stringResource(R.string.reset),
color = MaterialTheme.colorScheme.error
)
},
description = R.string.patch_options_reset_all_description,
)
GroupItem(
onClick = { showPackageSelector = true },
headline = R.string.patch_options_reset_package,
headline = {
Text(
stringResource(R.string.patch_options_reset_package),
color = MaterialTheme.colorScheme.error
)
},
description = R.string.patch_options_reset_package_description
)
if (patchBundles.size > 1)
if (patchBundles.size > 1) {
GroupItem(
onClick = { showBundleSelector = true },
headline = R.string.patch_options_reset_bundle,
headline = {
Text(
stringResource(R.string.patch_options_reset_bundle),
color = MaterialTheme.colorScheme.error
)
},
description = R.string.patch_options_reset_bundle_description,
)
}
}
}
}
@ -262,6 +308,19 @@ 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,
@ -278,7 +337,7 @@ fun KeystoreCredentialsDialog(
onSubmit(cn, pass)
}
) {
Text(stringResource(R.string.import_keystore_dialog_button))
Text(stringResource(R.string.restore_keystore_dialog_button))
}
},
dismissButton = {
@ -291,7 +350,7 @@ fun KeystoreCredentialsDialog(
},
title = {
Text(
text = stringResource(R.string.import_keystore_dialog_title),
text = stringResource(R.string.restore_keystore_dialog_title),
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
color = MaterialTheme.colorScheme.onSurface,
)
@ -302,19 +361,19 @@ fun KeystoreCredentialsDialog(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = stringResource(R.string.import_keystore_dialog_description),
text = stringResource(R.string.restore_keystore_dialog_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
OutlinedTextField(
value = cn,
onValueChange = { cn = it },
label = { Text(stringResource(R.string.import_keystore_dialog_alias_field)) }
label = { Text(stringResource(R.string.restore_keystore_dialog_alias_field)) }
)
PasswordField(
value = pass,
onValueChange = { pass = it },
label = { Text(stringResource(R.string.import_keystore_dialog_password_field)) }
label = { Text(stringResource(R.string.restore_keystore_dialog_password_field)) }
)
}
}

View File

@ -25,11 +25,14 @@ 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
@ -53,13 +56,17 @@ 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

View File

@ -5,8 +5,11 @@ 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
@ -21,13 +24,17 @@ 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))
@ -37,7 +44,13 @@ fun DeveloperOptionsScreen(
)
SettingsListItem(
headlineContent = stringResource(R.string.patch_bundles_reset),
modifier = Modifier.clickable(onClick = vm::redownloadBundles)
modifier = Modifier.clickable(onClick = vm::resetBundles)
)
GroupHeader(stringResource(R.string.testing))
SettingsListItem(
headlineContent = stringResource(R.string.disable_safeguard),
modifier = Modifier.clickable(onClick = vm::disableSafeguard)
)
}
}

View File

@ -17,9 +17,11 @@ 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
@ -28,6 +30,7 @@ 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
@ -55,11 +58,13 @@ 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.downloads),
title = stringResource(R.string.extensions),
scrollBehavior = scrollBehavior,
onBackClick = onBackClick,
actions = {
if (viewModel.appSelection.isNotEmpty()) {
@ -69,7 +74,8 @@ fun DownloadsSettingsScreen(
}
}
)
}
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { paddingValues ->
Box(
contentAlignment = Alignment.TopCenter,

View File

@ -8,11 +8,14 @@ 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.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
@ -20,6 +23,7 @@ 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
@ -39,6 +43,7 @@ import org.koin.compose.koinInject
@Composable
fun GeneralSettingsScreen(
onBackClick: () -> Unit,
onUpdateClick: () -> Unit,
viewModel: GeneralSettingsViewModel = koinViewModel()
) {
val prefs = viewModel.prefs
@ -51,14 +56,17 @@ 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
@ -70,10 +78,10 @@ fun GeneralSettingsScreen(
val theme by prefs.theme.getAsState()
SettingsListItem(
modifier = Modifier.clickable { showThemePicker = true },
headlineContent = stringResource(R.string.theme),
supportingContent = stringResource(R.string.theme_description),
headlineContent = stringResource(R.string.theme_mode),
supportingContent = stringResource(R.string.theme_mode_description),
trailingContent = {
FilledTonalButton(
Button(
onClick = {
showThemePicker = true
}
@ -86,11 +94,24 @@ fun GeneralSettingsScreen(
BooleanItem(
preference = prefs.dynamicColor,
coroutineScope = coroutineScope,
headline = R.string.dynamic_color,
description = R.string.dynamic_color_description
headline = R.string.personalized_color,
description = R.string.personalized_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))
}
}
}
@ -104,7 +125,7 @@ private fun ThemePicker(
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.theme)) },
title = { Text(stringResource(R.string.theme_mode)) },
text = {
Column {
Theme.entries.forEach {

View File

@ -7,9 +7,12 @@ 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
@ -28,13 +31,17 @@ 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

View File

@ -1,75 +0,0 @@
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
)
}
}
}

View File

@ -34,7 +34,7 @@ class AdvancedSettingsViewModel(
return "revanced-manager_logcat_$time"
}
fun setApiUrl(value: String) = viewModelScope.launch(Dispatchers.Default) {
fun setApiSource(value: String) = viewModelScope.launch(Dispatchers.Default) {
if (value == prefs.api.get()) return@launch
prefs.api.update(value)

View File

@ -24,4 +24,10 @@ class DeveloperOptionsViewModel(
fun resetBundles() = viewModelScope.launch {
patchBundleRepository.reset()
}
fun disableSafeguard() = viewModelScope.launch {
prefs.disablePatchVersionCompatCheck.update(true)
prefs.disableSelectionWarning.update(true)
prefs.disableUniversalPatchWarning.update(true)
}
}

View File

@ -69,25 +69,27 @@ class ImportExportViewModel(
}
fun startKeystoreImport(content: Uri) = viewModelScope.launch {
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
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
)
}
}
}
keystoreImportPath = path
aliases.forEach { alias ->
knownPasswords.forEach { pass ->
if (tryKeystoreImport(alias, pass, path)) {
return@launch
}
}
}
keystoreImportPath = path
}
}
fun cancelKeystoreImport() {
@ -101,7 +103,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.import_keystore_success))
app.toast(app.getString(R.string.restore_keystore_success))
cancelKeystoreImport()
return true
}
@ -120,7 +122,7 @@ class ImportExportViewModel(
fun exportKeystore(target: Uri) = viewModelScope.launch {
keystoreManager.export(contentResolver.openOutputStream(target)!!)
app.toast(app.getString(R.string.export_keystore_success))
app.toast(app.getString(R.string.backup_keystore_success))
}
fun regenerateKeystore() = viewModelScope.launch {
@ -169,7 +171,7 @@ class ImportExportViewModel(
override val activityArg = JSON_MIMETYPE
override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe(
app,
R.string.import_patch_selection_fail,
R.string.restore_patch_selection_fail,
"Failed to restore patch selection"
) {
val selection = withContext(Dispatchers.IO) {
@ -179,7 +181,7 @@ class ImportExportViewModel(
}
selectionRepository.import(bundleUid, selection)
app.toast(app.getString(R.string.import_patch_selection_success))
app.toast(app.getString(R.string.restore_patch_selection_success))
}
}
@ -188,7 +190,7 @@ class ImportExportViewModel(
override val activityArg = "selection.json"
override suspend fun execute(bundleUid: Int, location: Uri) = uiSafe(
app,
R.string.export_patch_selection_fail,
R.string.backup_patch_selection_fail,
"Failed to backup patch selection"
) {
val selection = selectionRepository.export(bundleUid)
@ -198,7 +200,7 @@ class ImportExportViewModel(
Json.Default.encodeToStream(selection, it)
}
}
app.toast(app.getString(R.string.export_patch_selection_success))
app.toast(app.getString(R.string.backup_patch_selection_success))
}
}

View File

@ -2,13 +2,10 @@ 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
@ -28,6 +25,7 @@ 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(
@ -36,10 +34,13 @@ class MainViewModel(
private val downloadedAppRepository: DownloadedAppRepository,
private val keystoreManager: KeystoreManager,
private val app: Application,
val prefs: PreferencesManager
val prefs: PreferencesManager,
private val json: Json
) : 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]
@ -50,7 +51,8 @@ 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,
@ -67,42 +69,46 @@ class MainViewModel(
selectApp(SelectedApp.Search(packageName, suggestedVersion(packageName)))
}
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 {
init {
viewModelScope.launch {
if (!prefs.firstLaunch.get()) return@launch
legacyImportActivityChannel.send(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")
}
})
}
}
private fun applyLegacySettings(data: String) = viewModelScope.launch {
val json = Json { ignoreUnknownKeys = true }
val settings = json.decodeFromString<LegacySettings>(data)
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
}
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,
@ -131,8 +137,8 @@ class MainViewModel(
updateCheck()
}
}
settings.patchesChangeEnabled?.let { disableSelectionWarning ->
prefs.disableSelectionWarning.update(disableSelectionWarning)
settings.patchesChangeEnabled?.let { allowChangingPatchSelection ->
prefs.allowChangingPatchSelection.update(allowChangingPatchSelection)
}
settings.keystore?.let { keystore ->
val keystoreBytes = Base64.decode(keystore, Base64.DEFAULT)
@ -145,6 +151,7 @@ class MainViewModel(
settings.patches?.let { selection ->
patchSelectionRepository.import(0, selection)
}
Log.d(tag, "Imported legacy settings")
}
@Serializable

View File

@ -1,7 +1,6 @@
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
@ -67,7 +66,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
viewModelScope.launch {
universalPatchWarningEnabled = !prefs.disableUniversalPatchWarning.get()
if (prefs.disableSelectionWarning.get()) {
if (prefs.allowChangingPatchSelection.get()) {
selectionWarningEnabled = false
return@launch
}
@ -104,7 +103,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
val compatibleVersions = mutableStateListOf<String>()
var filter by mutableIntStateOf(0)
var filter by mutableIntStateOf(SHOW_UNIVERSAL)
private set
private val defaultPatchSelection = bundlesFlow.map { bundles ->
@ -218,9 +217,8 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
}
companion object {
const val SHOW_SUPPORTED = 1 // 2^0
const val SHOW_UNSUPPORTED = 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

View File

@ -152,7 +152,7 @@ class SelectedAppInfoViewModel(
// Try to get the previous selection if customization is enabled.
viewModelScope.launch {
if (!prefs.disableSelectionWarning.get()) return@launch
if (!prefs.allowChangingPatchSelection.get()) return@launch
val previous = selectionRepository.getSelection(packageName)
if (previous.values.sumOf { it.size } == 0) return@launch

View File

@ -108,14 +108,19 @@ class UpdateViewModel(
val extra =
intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
app.toast(app.getString(R.string.install_app_success))
state = State.SUCCESS
} else {
state = State.FAILED
// TODO: handle install fail with a popup
installError = extra
app.toast(app.getString(R.string.install_app_fail, extra))
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
}
}
}
}

View File

@ -5,8 +5,11 @@
<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>
@ -16,8 +19,10 @@
<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>
@ -36,15 +41,20 @@
<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>
@ -60,77 +70,91 @@
<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">Theme, dynamic color</string>
<string name="general_description">Appearances, Updates</string>
<string name="updates">Updates</string>
<string name="updates_description">Check for updates and view changelogs</string>
<string name="downloads">Downloads</string>
<string name="downloads_description">Downloader plugins and downloaded apps</string>
<string name="import_export">Import &amp; export</string>
<string name="import_export_description">Keystore, patch options and selection</string>
<string name="extensions">Extensions</string>
<string name="extensions_description">Downloader plugins, downloaded apps</string>
<string name="backup_restore">Backup &amp; Restore</string>
<string name="backup_restore_description">Keystore, Patch selections, Patch options</string>
<string name="advanced">Advanced</string>
<string name="advanced_description">API URL, memory limit, debugging</string>
<string name="advanced_description">API Source, memory limits, debug logs</string>
<string name="experimental_features">Experimental features</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="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="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="safeguards">Safeguards</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="patch_compat_check_confirmation">Selecting incompatible patches can result in a broken app.\n\nDo you want to proceed anyways?</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="suggested_version_safeguard_confirmation">Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?</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_selection_safeguard">Allow changing patch selection</string>
<string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches</string>
<string name="patch_selection_safeguard_confirmation">Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?</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="universal_patches_safeguard_confirmation">Universal patches are not as well tested as those that target specific apps.\n\nEnable anyways?</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 keystores 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="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 keystores 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 apps 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="regenerate_keystore_success">The keystore has been successfully replaced</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="restore_patch_selection_description">Export apps 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 apps 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="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 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="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="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>
@ -164,26 +188,31 @@
<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 (experimental)</string>
<string name="process_runtime_description">This is faster and allows Patcher to use more memory.</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_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 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">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_dialog_save">Set</string>
<string name="api_url_dialog_reset">Reset API URL</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="device">Device</string>
<string name="device_android_version">Android version</string>
<string name="device_model">Model</string>
@ -210,24 +239,34 @@
<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">Unsupported patches</string>
<string name="unsupported_patches">Incompatible 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">Supported</string>
<string name="universal">Universal</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="unsupported">Unsupported</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="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="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>
@ -257,7 +296,8 @@
<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>
@ -285,7 +325,8 @@
<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>
@ -327,36 +368,43 @@
<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="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="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="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>
@ -372,18 +420,22 @@
<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>
@ -394,13 +446,20 @@
<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>
@ -411,10 +470,14 @@
<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>
</resources>
<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>

View File

@ -1,41 +1,41 @@
[versions]
ktx = "1.15.0"
material3 = "1.3.1"
ui-tooling = "1.7.6"
ui-tooling = "1.7.7"
viewmodel-lifecycle = "2.8.7"
splash-screen = "1.0.1"
activity = "1.9.3"
activity = "1.10.0"
appcompat = "1.7.0"
preferences-datastore = "1.1.1"
preferences-datastore = "1.1.2"
work-runtime = "2.10.0"
compose-bom = "2024.12.01"
navigation = "2.8.5"
accompanist = "0.34.0"
compose-bom = "2025.01.01"
navigation = "2.8.6"
accompanist = "0.37.0"
placeholder = "1.1.2"
reorderable = "1.5.2"
serialization = "1.7.3"
reorderable = "2.4.3"
serialization = "1.8.0"
collection = "0.3.8"
datetime = "0.6.0"
datetime = "0.6.1"
room-version = "2.6.1"
revanced-patcher = "21.0.0"
revanced-library = "3.0.2"
koin = "3.5.3"
ktor = "2.3.9"
markdown-renderer = "0.22.0"
markdown-renderer = "0.30.0"
fading-edges = "1.0.4"
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"
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"
binary-compatibility-validator = "0.17.0"
coil = "2.6.0"
coil = "2.7.0"
app-icon-loader-coil = "1.5.0"
skrapeit = "1.2.2"
libsu = "5.2.2"
libsu = "6.0.0"
scrollbars = "1.0.4"
enumutil = "1.1.0"
enumutil = "1.1.1"
compose-icons = "1.2.4"
kotlin-process = "1.4.1"
kotlin-process = "1.5.1"
hidden-api-stub = "4.3.3"
[libraries]

Binary file not shown.

View File

@ -1,7 +1,7 @@
#Tue Nov 12 21:36:50 CET 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionSha256Sum=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

6
gradlew vendored
View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

22
gradlew.bat vendored
View File

@ -13,6 +13,8 @@
@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 ##########################################################################
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
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.
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
goto fail
@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
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.
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
goto fail