diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 944656e3..4cc8e1c6 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -19,16 +19,17 @@ import androidx.compose.runtime.setValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.destination.Destination -import app.revanced.manager.ui.screen.AppInfoScreen +import app.revanced.manager.ui.screen.InstalledAppInfoScreen import app.revanced.manager.ui.screen.AppSelectorScreen import app.revanced.manager.ui.screen.DashboardScreen import app.revanced.manager.ui.screen.InstallerScreen -import app.revanced.manager.ui.screen.PatchesSelectorScreen +import app.revanced.manager.ui.screen.SelectedAppInfoScreen import app.revanced.manager.ui.screen.SettingsScreen import app.revanced.manager.ui.screen.VersionSelectorScreen import app.revanced.manager.ui.theme.ReVancedManagerTheme import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.viewmodel.MainViewModel +import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel import app.revanced.manager.util.tag import app.revanced.manager.util.toast import dev.olshevski.navigation.reimagined.AnimatedNavHost @@ -37,9 +38,9 @@ import dev.olshevski.navigation.reimagined.navigate import dev.olshevski.navigation.reimagined.pop import dev.olshevski.navigation.reimagined.popUpTo import dev.olshevski.navigation.reimagined.rememberNavController -import org.koin.androidx.compose.getViewModel +import org.koin.androidx.compose.getViewModel as getComposeViewModel +import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel import org.koin.core.parameter.parametersOf -import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel class MainActivity : ComponentActivity() { @ExperimentalAnimationApi @@ -48,7 +49,7 @@ class MainActivity : ComponentActivity() { installSplashScreen() - val vm: MainViewModel = getActivityViewModel() + val vm: MainViewModel = getAndroidViewModel() setContent { val theme by vm.prefs.theme.getAsState() @@ -102,7 +103,7 @@ class MainActivity : ComponentActivity() { } legacyActivityState = LegacyActivity.LAUNCHED - } else if (legacyActivityState == LegacyActivity.FAILED){ + } else if (legacyActivityState == LegacyActivity.FAILED) { AutoUpdatesDialog(vm::applyAutoUpdatePrefs) } } @@ -114,15 +115,26 @@ class MainActivity : ComponentActivity() { is Destination.Dashboard -> DashboardScreen( onSettingsClick = { navController.navigate(Destination.Settings) }, onAppSelectorClick = { navController.navigate(Destination.AppSelector) }, - onAppClick = { installedApp -> navController.navigate(Destination.ApplicationInfo(installedApp)) } + onAppClick = { installedApp -> + navController.navigate( + Destination.InstalledApplicationInfo( + installedApp + ) + ) + } ) - is Destination.ApplicationInfo -> AppInfoScreen( + is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen( onPatchClick = { packageName, patchesSelection -> - navController.navigate(Destination.VersionSelector(packageName, patchesSelection)) + navController.navigate( + Destination.VersionSelector( + packageName, + patchesSelection + ) + ) }, onBackClick = { navController.pop() }, - viewModel = getViewModel { parametersOf(destination.installedApp) } + viewModel = getComposeViewModel { parametersOf(destination.installedApp) } ) is Destination.Settings -> SettingsScreen( @@ -131,7 +143,13 @@ class MainActivity : ComponentActivity() { is Destination.AppSelector -> AppSelectorScreen( onAppClick = { navController.navigate(Destination.VersionSelector(it)) }, - onStorageClick = { navController.navigate(Destination.PatchesSelector(it)) }, + onStorageClick = { + navController.navigate( + Destination.SelectedApplicationInfo( + it + ) + ) + }, onBackClick = { navController.pop() } ) @@ -139,32 +157,42 @@ class MainActivity : ComponentActivity() { onBackClick = { navController.pop() }, onAppClick = { selectedApp -> navController.navigate( - Destination.PatchesSelector( + Destination.SelectedApplicationInfo( selectedApp, + destination.patchesSelection, + ) + ) + }, + viewModel = getComposeViewModel { + parametersOf( + destination.packageName, + destination.patchesSelection + ) + } + ) + + is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen( + onPatchClick = { app, patches, options -> + navController.navigate( + Destination.Installer( + app, patches, options + ) + ) + }, + onBackClick = navController::pop, + vm = getComposeViewModel { + parametersOf( + SelectedAppInfoViewModel.Params( + destination.selectedApp, destination.patchesSelection ) ) - }, - viewModel = getViewModel { parametersOf(destination.packageName, destination.patchesSelection) } - ) - - is Destination.PatchesSelector -> PatchesSelectorScreen( - onBackClick = { navController.pop() }, - onPatchClick = { patches, options -> - navController.navigate( - Destination.Installer( - destination.selectedApp, - patches, - options - ) - ) - }, - vm = getViewModel { parametersOf(destination) } + } ) is Destination.Installer -> InstallerScreen( onBackClick = { navController.popUpTo { it is Destination.Dashboard } }, - vm = getViewModel { parametersOf(destination) } + vm = getComposeViewModel { parametersOf(destination) } ) } } diff --git a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt index 5f12a2ad..1729f546 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -7,6 +7,7 @@ import org.koin.dsl.module val viewModelModule = module { viewModelOf(::MainViewModel) viewModelOf(::DashboardViewModel) + viewModelOf(::SelectedAppInfoViewModel) viewModelOf(::PatchesSelectorViewModel) viewModelOf(::SettingsViewModel) viewModelOf(::AdvancedSettingsViewModel) @@ -19,5 +20,5 @@ val viewModelModule = module { viewModelOf(::ContributorViewModel) viewModelOf(::DownloadsViewModel) viewModelOf(::InstalledAppsViewModel) - viewModelOf(::AppInfoViewModel) + viewModelOf(::InstalledAppInfoViewModel) } diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppInfo.kt b/app/src/main/java/app/revanced/manager/ui/component/AppInfo.kt new file mode 100644 index 00000000..6d45b20b --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AppInfo.kt @@ -0,0 +1,39 @@ +package app.revanced.manager.ui.component + +import android.content.pm.PackageInfo +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun AppInfo(appInfo: PackageInfo?, placeholderLabel: String? = null, extraContent: @Composable () -> Unit = {}) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AppIcon( + appInfo, + contentDescription = null, + modifier = Modifier + .size(100.dp) + .padding(bottom = 5.dp) + ) + + AppLabel( + appInfo, + modifier = Modifier.padding(top = 16.dp), + style = MaterialTheme.typography.titleLarge, + defaultText = placeholderLabel + ) + + extraContent() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt b/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt index e737ed8c..6c01e783 100644 --- a/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt +++ b/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt @@ -14,7 +14,7 @@ sealed interface Destination : Parcelable { object Dashboard : Destination @Parcelize - data class ApplicationInfo(val installedApp: InstalledApp) : Destination + data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination @Parcelize object AppSelector : Destination @@ -26,7 +26,7 @@ sealed interface Destination : Parcelable { data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination @Parcelize - data class PatchesSelector(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination + data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination @Parcelize data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination diff --git a/app/src/main/java/app/revanced/manager/ui/destination/SelectedAppInfoDestination.kt b/app/src/main/java/app/revanced/manager/ui/destination/SelectedAppInfoDestination.kt new file mode 100644 index 00000000..32036c2a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/destination/SelectedAppInfoDestination.kt @@ -0,0 +1,19 @@ +package app.revanced.manager.ui.destination + +import android.os.Parcelable +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.util.Options +import app.revanced.manager.util.PatchesSelection +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue + +sealed interface SelectedAppInfoDestination : Parcelable { + @Parcelize + data object Main : SelectedAppInfoDestination + + @Parcelize + data class PatchesSelector(val app: SelectedApp, val currentSelection: PatchesSelection?, val options: @RawValue Options) : SelectedAppInfoDestination + + @Parcelize + data object VersionSelector: SelectedAppInfoDestination +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt new file mode 100644 index 00000000..fd048e4b --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt @@ -0,0 +1,82 @@ +package app.revanced.manager.ui.model + +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.patcher.patch.PatchInfo +import app.revanced.manager.util.PatchesSelection +import app.revanced.manager.util.flatMapLatestAndCombine +import kotlinx.coroutines.flow.map + +/** + * A data class that contains patch bundle metadata for use by UI code. + */ +data class BundleInfo( + val name: String, + val uid: Int, + val supported: List, + val unsupported: List, + val universal: List +) { + val all = sequence { + yieldAll(supported) + yieldAll(unsupported) + yieldAll(universal) + } + + val patchCount get() = supported.size + unsupported.size + universal.size + + fun patchSequence(allowUnsupported: Boolean) = if (allowUnsupported) { + all + } else { + sequence { + yieldAll(supported) + yieldAll(universal) + } + } + + companion object Extensions { + inline fun Iterable.toPatchSelection(allowUnsupported: Boolean, condition: (Int, PatchInfo) -> Boolean): PatchesSelection = this.associate { bundle -> + val patches = + bundle.patchSequence(allowUnsupported) + .mapNotNullTo(mutableSetOf()) { patch -> + patch.name.takeIf { + condition( + bundle.uid, + patch + ) + } + } + + bundle.uid to patches + } + + fun PatchBundleRepository.bundleInfoFlow(packageName: String, version: String) = + sources.flatMapLatestAndCombine( + combiner = { it.filterNotNull() } + ) { source -> + // Regenerate bundle information whenever this source updates. + source.state.map { state -> + val bundle = state.patchBundleOrNull() ?: return@map null + + val supported = mutableListOf() + val unsupported = mutableListOf() + val universal = mutableListOf() + + bundle.patches.filter { it.compatibleWith(packageName) }.forEach { + val targetList = when { + it.compatiblePackages == null -> universal + it.supportsVersion( + packageName, + version + ) -> supported + + else -> unsupported + } + + targetList.add(it) + } + + BundleInfo(source.name, source.uid, supported, unsupported, universal) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt similarity index 90% rename from app/src/main/java/app/revanced/manager/ui/screen/AppInfoScreen.kt rename to app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt index 1cd3762b..4abea5ec 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/AppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt @@ -41,18 +41,19 @@ import androidx.compose.ui.unit.dp import app.revanced.manager.R import app.revanced.manager.data.room.apps.installed.InstallType import app.revanced.manager.ui.component.AppIcon +import app.revanced.manager.ui.component.AppInfo import app.revanced.manager.ui.component.AppLabel import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.SegmentedButton -import app.revanced.manager.ui.viewmodel.AppInfoViewModel +import app.revanced.manager.ui.viewmodel.InstalledAppInfoViewModel import app.revanced.manager.util.PatchesSelection @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AppInfoScreen( +fun InstalledAppInfoScreen( onPatchClick: (packageName: String, patchesSelection: PatchesSelection) -> Unit, onBackClick: () -> Unit, - viewModel: AppInfoViewModel + viewModel: InstalledAppInfoViewModel ) { SideEffect { viewModel.onBackClick = onBackClick @@ -80,27 +81,8 @@ fun AppInfoScreen( .padding(paddingValues) .verticalScroll(rememberScrollState()) ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - AppIcon( - viewModel.appInfo, - contentDescription = null, - modifier = Modifier - .size(100.dp) - .padding(bottom = 5.dp) - ) - - AppLabel( - viewModel.appInfo, - style = MaterialTheme.typography.titleLarge, - defaultText = null - ) - - Text(viewModel.installedApp.version, style = MaterialTheme.typography.bodySmall) + AppInfo(viewModel.appInfo) { + Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium) if (viewModel.installedApp.installType == InstallType.ROOT) { Text( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt index 4ac41b06..d33c28a5 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt @@ -15,16 +15,15 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.Restore +import androidx.compose.material.icons.outlined.Save import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.WarningAmber import androidx.compose.material3.AlertDialog import androidx.compose.material3.Checkbox -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FilterChip @@ -42,6 +41,7 @@ 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 @@ -57,7 +57,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewModelScope import app.revanced.manager.R import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.patcher.patch.PatchInfo @@ -65,7 +64,6 @@ import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.Countdown import app.revanced.manager.ui.component.patches.OptionItem import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel -import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.BaseSelectionMode 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 @@ -77,7 +75,7 @@ import org.koin.compose.rememberKoinInject @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun PatchesSelectorScreen( - onPatchClick: (PatchesSelection, Options) -> Unit, + onSave: (PatchesSelection?, Options) -> Unit, onBackClick: () -> Unit, vm: PatchesSelectorViewModel ) { @@ -93,10 +91,10 @@ fun PatchesSelectorScreen( mutableStateOf(null) } var showBottomSheet by rememberSaveable { mutableStateOf(false) } - var showPatchButton by remember { mutableStateOf(true) } - LaunchedEffect(Unit) { - showPatchButton = vm.isSelectionNotEmpty() + val showPatchButton by remember { + derivedStateOf { vm.selectionIsValid(bundles) } } + if (showBottomSheet) { ModalBottomSheet( onDismissRequest = { @@ -140,39 +138,12 @@ fun PatchesSelectorScreen( ) } } - - Divider() - - ListItem( - modifier = Modifier - .fillMaxWidth() - .clickable( - enabled = vm.hasPreviousSelection, - onClick = vm::switchBaseSelectionMode - ), - leadingContent = { - Checkbox( - checked = vm.baseSelectionMode == BaseSelectionMode.PREVIOUS, - onCheckedChange = { - vm.switchBaseSelectionMode() - }, - enabled = vm.hasPreviousSelection - ) - }, - headlineContent = { - Text( - "Use previous selection", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface - ) - } - ) } } if (vm.compatibleVersions.isNotEmpty()) UnsupportedDialog( - appVersion = vm.input.selectedApp.version, + appVersion = vm.appVersion, supportedVersions = vm.compatibleVersions, onDismissRequest = vm::dismissDialogs ) @@ -194,8 +165,6 @@ fun PatchesSelectorScreen( ) } - val allowExperimental by vm.allowExperimental.getAsState() - fun LazyListScope.patchList( uid: Int, patches: List, @@ -227,17 +196,10 @@ fun PatchesSelectorScreen( if (vm.selectionWarningEnabled) { vm.pendingSelectionAction = { vm.togglePatch(uid, patch) - vm.viewModelScope.launch { - showPatchButton = vm.isSelectionNotEmpty() - } } } else { vm.togglePatch(uid, patch) - vm.viewModelScope.launch { - showPatchButton = vm.isSelectionNotEmpty() - } } - }, supported = supported ) @@ -292,7 +254,7 @@ fun PatchesSelectorScreen( ) } - if (!allowExperimental) return@LazyColumn + if (!vm.allowExperimental) return@LazyColumn patchList( uid = bundle.uid, patches = bundle.unsupported.searched(), @@ -332,22 +294,19 @@ fun PatchesSelectorScreen( ) }, floatingActionButton = { - if(showPatchButton) { - ExtendedFloatingActionButton( - text = { - Text(stringResource(R.string.patch)) - }, - icon = { Icon(Icons.Default.Build, null) }, - onClick = { - // TODO: only allow this if all required options have been set. - composableScope.launch { - val selection = vm.getSelection() - vm.saveSelection(selection).join() - onPatchClick(selection, vm.getOptions()) - } + if (!showPatchButton) return@Scaffold + + ExtendedFloatingActionButton( + text = { Text(stringResource(R.string.save)) }, + icon = { Icon(Icons.Outlined.Save, null) }, + onClick = { + // TODO: only allow this if all required options have been set. + composableScope.launch { + vm.saveSelection() + onSave(vm.getCustomSelection(), vm.getOptions()) } - ) - } + } + ) } ) { paddingValues -> Column( @@ -407,7 +366,7 @@ fun PatchesSelectorScreen( uid = bundle.uid, patches = bundle.unsupported, filterFlag = SHOW_UNSUPPORTED, - supported = allowExperimental + supported = vm.allowExperimental ) { ListHeader( title = stringResource(R.string.unsupported_patches), diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt new file mode 100644 index 00000000..2d50e62c --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt @@ -0,0 +1,218 @@ +package app.revanced.manager.ui.screen + +import android.content.pm.PackageInfo +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowRight +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.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.ui.component.AppInfo +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.destination.SelectedAppInfoDestination +import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel +import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel +import app.revanced.manager.util.Options +import app.revanced.manager.util.PatchesSelection +import dev.olshevski.navigation.reimagined.AnimatedNavHost +import dev.olshevski.navigation.reimagined.NavBackHandler +import dev.olshevski.navigation.reimagined.navigate +import dev.olshevski.navigation.reimagined.pop +import dev.olshevski.navigation.reimagined.rememberNavController +import org.koin.androidx.compose.getViewModel +import org.koin.core.parameter.parametersOf + +@Composable +fun SelectedAppInfoScreen( + onPatchClick: (SelectedApp, PatchesSelection, Options) -> Unit, + onBackClick: () -> Unit, + vm: SelectedAppInfoViewModel +) { + val bundles by remember(vm.selectedApp.packageName, vm.selectedApp.version) { + vm.bundlesRepo.bundleInfoFlow(vm.selectedApp.packageName, vm.selectedApp.version) + }.collectAsStateWithLifecycle(initialValue = emptyList()) + val allowExperimental by vm.prefs.allowExperimental.getAsState() + val patches by remember { + derivedStateOf { + vm.getPatches(bundles, allowExperimental) + } + } + val selectedPatchCount by remember { + derivedStateOf { + patches.values.sumOf { it.size } + } + } + val availablePatchCount by remember { + derivedStateOf { + bundles.sumOf { it.patchCount } + } + } + + val navController = + rememberNavController(startDestination = SelectedAppInfoDestination.Main) + + NavBackHandler(controller = navController) + + AnimatedNavHost(controller = navController) { destination -> + when (destination) { + is SelectedAppInfoDestination.Main -> SelectedAppInfoScreen( + onPatchClick = { + onPatchClick( + vm.selectedApp, + patches, + vm.patchOptions + ) + }, + onPatchSelectorClick = { + navController.navigate( + SelectedAppInfoDestination.PatchesSelector( + vm.selectedApp, + vm.getCustomPatches( + bundles, + allowExperimental + ), + vm.patchOptions + ) + ) + }, + onVersionSelectorClick = { + navController.navigate(SelectedAppInfoDestination.VersionSelector) + }, + onBackClick = onBackClick, + availablePatchCount = availablePatchCount, + selectedPatchCount = selectedPatchCount, + packageName = vm.selectedApp.packageName, + version = vm.selectedApp.version, + packageInfo = vm.selectedAppInfo, + ) + + is SelectedAppInfoDestination.VersionSelector -> VersionSelectorScreen( + onBackClick = navController::pop, + onAppClick = { + vm.setSelectedApp(it) + navController.pop() + }, + viewModel = getViewModel { parametersOf(vm.selectedApp.packageName) } + ) + + is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen( + onSave = { patches, options -> + vm.setCustomPatches(patches) + vm.patchOptions = options + navController.pop() + }, + onBackClick = navController::pop, + vm = getViewModel { + parametersOf( + PatchesSelectorViewModel.Params( + destination.app, + destination.currentSelection, + destination.options + ) + ) + } + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SelectedAppInfoScreen( + onPatchClick: () -> Unit, + onPatchSelectorClick: () -> Unit, + onVersionSelectorClick: () -> Unit, + onBackClick: () -> Unit, + availablePatchCount: Int, + selectedPatchCount: Int, + packageName: String, + version: String, + packageInfo: PackageInfo?, +) { + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.app_info), + onBackClick = onBackClick + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + AppInfo(packageInfo, placeholderLabel = packageName) { + Text( + stringResource(R.string.selected_app_meta, version, availablePatchCount), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + } + + PageItem(R.string.patch, stringResource(R.string.patch_item_description), onPatchClick) + + Text( + stringResource(R.string.advanced), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp) + ) + + PageItem( + R.string.patch_selector_item, + stringResource(R.string.patch_selector_item_description, selectedPatchCount), + onPatchSelectorClick + ) + PageItem( + R.string.version_selector_item, + stringResource(R.string.version_selector_item_description, version), + onVersionSelectorClick + ) + } + } +} + +@Composable +private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) { + ListItem( + modifier = Modifier + .clickable(onClick = onClick) + .padding(start = 8.dp), + headlineContent = { + Text( + stringResource(title), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge + ) + }, + supportingContent = { + Text( + description, + color = MaterialTheme.colorScheme.outline, + style = MaterialTheme.typography.bodyMedium + ) + }, + trailingContent = { + Icon(Icons.Outlined.ArrowRight, null) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt similarity index 99% rename from app/src/main/java/app/revanced/manager/ui/viewmodel/AppInfoViewModel.kt rename to app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt index 7cf31727..3055794d 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject -class AppInfoViewModel( +class InstalledAppInfoViewModel( val installedApp: InstalledApp ) : ViewModel(), KoinComponent { private val app: Application by inject() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt index 8e4f7246..69449f80 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt @@ -2,8 +2,8 @@ package app.revanced.manager.ui.viewmodel import android.app.Application import androidx.compose.runtime.Stable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf @@ -20,66 +20,46 @@ import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.patcher.patch.PatchInfo -import app.revanced.manager.ui.destination.Destination +import app.revanced.manager.ui.model.BundleInfo +import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow +import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection +import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.Options import app.revanced.manager.util.PatchesSelection -import app.revanced.manager.util.flatMapLatestAndCombine +import app.revanced.manager.util.saver.nullableSaver +import app.revanced.manager.util.saver.persistentMapSaver +import app.revanced.manager.util.saver.persistentSetSaver import app.revanced.manager.util.saver.snapshotStateMapSaver import app.revanced.manager.util.toast import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.get +import kotlinx.collections.immutable.* +import kotlinx.coroutines.withContext +import java.util.Optional @Stable @OptIn(SavedStateHandleSaveableApi::class) -class PatchesSelectorViewModel( - val input: Destination.PatchesSelector -) : ViewModel(), KoinComponent { +class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent { private val app: Application = get() private val selectionRepository: PatchSelectionRepository = get() private val savedStateHandle: SavedStateHandle = get() private val prefs: PreferencesManager = get() - private val packageName = input.selectedApp.packageName + private val packageName = input.app.packageName + val appVersion = input.app.version var pendingSelectionAction by mutableStateOf<(() -> Unit)?>(null) + // TODO: this should be hoisted to the parent screen var selectionWarningEnabled by mutableStateOf(true) private set - val allowExperimental = get().allowExperimental - val bundlesFlow = get().sources.flatMapLatestAndCombine( - combiner = { it.filterNotNull() } - ) { source -> - // Regenerate bundle information whenever this source updates. - source.state.map { state -> - val bundle = state.patchBundleOrNull() ?: return@map null - - val supported = mutableListOf() - val unsupported = mutableListOf() - val universal = mutableListOf() - - bundle.patches.filter { it.compatibleWith(packageName) }.forEach { - val targetList = when { - it.compatiblePackages == null -> universal - it.supportsVersion( - input.selectedApp.packageName, - input.selectedApp.version - ) -> supported - - else -> unsupported - } - - targetList.add(it) - } - - BundleInfo(source.name, source.uid, bundle.patches, supported, unsupported, universal) - } - } + val allowExperimental = get().allowExperimental.getBlocking() + val bundlesFlow = + get().bundleInfoFlow(packageName, input.app.version) init { viewModelScope.launch { @@ -88,63 +68,28 @@ class PatchesSelectorViewModel( return@launch } - val experimental = allowExperimental.get() - fun BundleInfo.hasDefaultPatches(): Boolean { - return if (experimental) { - all.asSequence() - } else { - sequence { - yieldAll(supported) - yieldAll(universal) - } - }.any { it.include } - } + fun BundleInfo.hasDefaultPatches() = patchSequence(allowExperimental).any { it.include } // Don't show the warning if there are no default patches. selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches) } } - var baseSelectionMode by mutableStateOf(BaseSelectionMode.DEFAULT) - private set - - private val previousPatchesSelection: SnapshotStateMap> = mutableStateMapOf() - - init { - viewModelScope.launch(Dispatchers.Default) { loadPreviousSelection() } - } - - val hasPreviousSelection by derivedStateOf { - previousPatchesSelection.filterValues(Set::isNotEmpty).isNotEmpty() - } - private var hasModifiedSelection = false + private var customPatchesSelection: PersistentPatchesSelection? by savedStateHandle.saveable( + key = "selection", + stateSaver = patchesSaver, + ) { + mutableStateOf(input.currentSelection?.toPersistentPatchesSelection()) + } - private val explicitPatchesSelection: SnapshotExplicitPatchesSelection by savedStateHandle.saveable( - saver = explicitPatchesSelectionSaver, - init = ::mutableStateMapOf - ) - - private val patchOptions: SnapshotOptions by savedStateHandle.saveable( + private val patchOptions: PersistentOptions by savedStateHandle.saveable( saver = optionsSaver, - init = ::mutableStateMapOf - ) - - private val selectors by derivedStateOf> { - arrayOf( - // Patches that were explicitly selected - { bundle, patch -> - explicitPatchesSelection[bundle]?.get(patch.name) - }, - // The fallback selection. - when (baseSelectionMode) { - BaseSelectionMode.DEFAULT -> ({ _, patch -> patch.include }) - - BaseSelectionMode.PREVIOUS -> ({ bundle, patch -> - previousPatchesSelection[bundle]?.contains(patch.name) ?: false - }) - } - ) + ) { + // Convert Options to PersistentOptions + input.options.mapValuesTo(mutableStateMapOf()) { (_, allPatches) -> + allPatches.mapValues { (_, options) -> options.toPersistentMap() }.toPersistentMap() + } } /** @@ -154,52 +99,39 @@ class PatchesSelectorViewModel( val compatibleVersions = mutableStateListOf() - var filter by mutableStateOf(SHOW_SUPPORTED or SHOW_UNIVERSAL or SHOW_UNSUPPORTED) + var filter by mutableIntStateOf(SHOW_SUPPORTED or SHOW_UNIVERSAL or SHOW_UNSUPPORTED) private set - private suspend fun loadPreviousSelection() { - val selection = (input.patchesSelection ?: selectionRepository.getSelection( - packageName - )).mapValues { (_, value) -> value.toSet() } + private suspend fun generateDefaultSelection(): PersistentPatchesSelection { + val bundles = bundlesFlow.first() + val generatedSelection = + bundles.toPatchSelection(allowExperimental) { _, patch -> patch.include } - withContext(Dispatchers.Main) { - previousPatchesSelection.putAll(selection) + return generatedSelection.toPersistentPatchesSelection() + } + + fun selectionIsValid(bundles: List) = bundles.any { bundle -> + bundle.patchSequence(allowExperimental).any { patch -> + isSelected(bundle.uid, patch) } } - fun switchBaseSelectionMode() = viewModelScope.launch { - baseSelectionMode = if (baseSelectionMode == BaseSelectionMode.DEFAULT) { - BaseSelectionMode.PREVIOUS - } else { - BaseSelectionMode.DEFAULT - } - } - - private suspend fun patchesAvailable(bundle: BundleInfo): List { - val patches = (bundle.supported + bundle.universal).toMutableList() - val removeUnsupported = !allowExperimental.get() - if (!removeUnsupported) patches += bundle.unsupported - return patches - } - - suspend fun isSelectionNotEmpty() = - bundlesFlow.first().any { bundle -> - patchesAvailable(bundle).any { patch -> - isSelected(bundle.uid, patch) - } - } - - private fun getOrCreateSelection(bundle: Int) = - explicitPatchesSelection.getOrPut(bundle, ::mutableStateMapOf) - - fun isSelected(bundle: Int, patch: PatchInfo) = - selectors.firstNotNullOf { fn -> fn(bundle, patch) } - - fun togglePatch(bundle: Int, patch: PatchInfo) { - val patches = getOrCreateSelection(bundle) + fun isSelected(bundle: Int, patch: PatchInfo) = customPatchesSelection?.let { selection -> + selection[bundle]?.contains(patch.name) ?: false + } ?: patch.include + fun togglePatch(bundle: Int, patch: PatchInfo) = viewModelScope.launch { hasModifiedSelection = true - patches[patch.name] = !isSelected(bundle, patch) + + val selection = customPatchesSelection ?: generateDefaultSelection() + val newPatches = selection[bundle]?.let { patches -> + if (patch.name in patches) + patches.remove(patch.name) + else + patches.add(patch.name) + } ?: persistentSetOf(patch.name) + + customPatchesSelection = selection.put(bundle, newPatches) } fun confirmSelectionWarning(dismissPermanently: Boolean) { @@ -221,46 +153,39 @@ class PatchesSelectorViewModel( fun reset() { patchOptions.clear() - baseSelectionMode = BaseSelectionMode.DEFAULT - explicitPatchesSelection.clear() + customPatchesSelection = null hasModifiedSelection = false app.toast(app.getString(R.string.patch_selection_reset_toast)) } - suspend fun getSelection(): PatchesSelection { - val bundles = bundlesFlow.first() - val removeUnsupported = !allowExperimental.get() + fun getCustomSelection(): PatchesSelection? { + // Convert persistent collections to standard hash collections because persistent collections are not parcelable. - return bundles.associate { bundle -> - val included = - bundle.all.filter { isSelected(bundle.uid, it) }.map { it.name }.toMutableSet() - - if (removeUnsupported) { - val unsupported = bundle.unsupported.map { it.name }.toSet() - included.removeAll(unsupported) - } - - bundle.uid to included - } + return customPatchesSelection?.mapValues { (_, v) -> v.toSet() } } - suspend fun saveSelection(selection: PatchesSelection) = - viewModelScope.launch(Dispatchers.Default) { - when { - hasModifiedSelection -> selectionRepository.updateSelection(packageName, selection) - baseSelectionMode == BaseSelectionMode.DEFAULT -> selectionRepository.clearSelection( - packageName - ) + fun getOptions(): Options { + // Convert the collection for the same reasons as in getCustomSelection() - else -> {} - } - } + return patchOptions.mapValues { (_, allPatches) -> allPatches.mapValues { (_, options) -> options.toMap() } } + } + + suspend fun saveSelection() = withContext(Dispatchers.Default) { + customPatchesSelection?.let { selectionRepository.updateSelection(packageName, it) } + ?: selectionRepository.clearSelection(packageName) + } - fun getOptions(): Options = patchOptions fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name) fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) { - patchOptions.getOrCreate(bundle).getOrCreate(patch.name)[key] = value + // All patches + val patchesToOpts = patchOptions.getOrElse(bundle, ::persistentMapOf) + // The key-value options of an individual patch + val patchToOpts = patchesToOpts + .getOrElse(patch.name, ::persistentMapOf) + .put(key, value) + + patchOptions[bundle] = patchesToOpts.put(patch.name, patchToOpts) } fun resetOptions(bundle: Int, patch: PatchInfo) { @@ -274,7 +199,7 @@ class PatchesSelectorViewModel( fun openUnsupportedDialog(unsupportedPatches: List) { compatibleVersions.addAll(unsupportedPatches.flatMap { patch -> - patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName }?.versions.orEmpty() + patch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty() }) } @@ -287,50 +212,28 @@ class PatchesSelectorViewModel( const val SHOW_UNIVERSAL = 2 // 2^1 const val SHOW_UNSUPPORTED = 4 // 2^2 - private fun SnapshotStateMap>.getOrCreate(key: K) = - getOrPut(key, ::mutableStateMapOf) - - private val optionsSaver: Saver = snapshotStateMapSaver( + private val optionsSaver: Saver = snapshotStateMapSaver( // Patch name -> Options - valueSaver = snapshotStateMapSaver( + valueSaver = persistentMapSaver( // Option key -> Option value - valueSaver = snapshotStateMapSaver() + valueSaver = persistentMapSaver() ) ) - private val explicitPatchesSelectionSaver: Saver = - snapshotStateMapSaver(valueSaver = snapshotStateMapSaver()) + private val patchesSaver: Saver> = + nullableSaver(persistentMapSaver(valueSaver = persistentSetSaver())) } - /** - * An enum for controlling the behavior of the selector. - */ - enum class BaseSelectionMode { - /** - * Selection is determined by the [PatchInfo.include] field. - */ - DEFAULT, - - /** - * Selection is determined by what the user selected previously. - * Any patch that is not part of the previous selection will be deselected. - */ - PREVIOUS - } - - data class BundleInfo( - val name: String, - val uid: Int, - val all: List, - val supported: List, - val unsupported: List, - val universal: List + data class Params( + val app: SelectedApp, + val currentSelection: PatchesSelection?, + val options: Options, ) } -private typealias Selector = (Int, PatchInfo) -> Boolean? -private typealias ExplicitPatchesSelection = Map> +// Versions of other types, but utilizing persistent/observable collection types. +private typealias PersistentOptions = SnapshotStateMap>> +private typealias PersistentPatchesSelection = PersistentMap> -// Versions of other types, but utilizing observable collection types instead. -private typealias SnapshotOptions = SnapshotStateMap>> -private typealias SnapshotExplicitPatchesSelection = SnapshotStateMap> \ No newline at end of file +private fun PatchesSelection.toPersistentPatchesSelection(): PersistentPatchesSelection = + mapValues { (_, v) -> v.toPersistentSet() }.toPersistentMap() \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt new file mode 100644 index 00000000..e66ec074 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -0,0 +1,128 @@ +package app.revanced.manager.ui.viewmodel + +import android.content.pm.PackageInfo +import android.os.Parcelable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi +import androidx.lifecycle.viewmodel.compose.saveable +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.domain.repository.PatchSelectionRepository +import app.revanced.manager.ui.model.BundleInfo +import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.util.Options +import app.revanced.manager.util.PM +import app.revanced.manager.util.PatchesSelection +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +@OptIn(SavedStateHandleSaveableApi::class) +class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { + val bundlesRepo: PatchBundleRepository = get() + private val selectionRepository: PatchSelectionRepository = get() + private val pm: PM = get() + private val savedStateHandle: SavedStateHandle = get() + val prefs: PreferencesManager = get() + + var selectedApp by savedStateHandle.saveable { + mutableStateOf(input.app) + } + private set + + var selectedAppInfo: PackageInfo? by mutableStateOf(null) + private set + + init { + invalidateSelectedAppInfo() + } + + var patchOptions: Options by savedStateHandle.saveable { + mutableStateOf(emptyMap()) + } + + private var selectionState by savedStateHandle.saveable { + if (input.patches != null) { + return@saveable mutableStateOf(SelectionState.Customized(input.patches)) + } + + val selection: MutableState = mutableStateOf(SelectionState.Default) + + // Get previous selection (if present). + viewModelScope.launch { + val previous = selectionRepository.getSelection(selectedApp.packageName) + + if (previous.values.sumOf { it.size } == 0) { + return@launch + } + + selection.value = SelectionState.Customized(previous) + } + + selection + } + + fun setSelectedApp(new: SelectedApp) { + selectedApp = new + invalidateSelectedAppInfo() + } + + private fun invalidateSelectedAppInfo() = viewModelScope.launch { + val info = when (val app = selectedApp) { + is SelectedApp.Download -> null + is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) } + is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) } + } + + selectedAppInfo = info + } + + fun getPatches(bundles: List, allowUnsupported: Boolean) = + selectionState.patches(bundles, allowUnsupported) + + fun getCustomPatches( + bundles: List, + allowUnsupported: Boolean + ): PatchesSelection? = + (selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported) + + fun setCustomPatches(selection: PatchesSelection?) { + selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default + } + + data class Params( + val app: SelectedApp, + val patches: PatchesSelection?, + ) +} + +private sealed interface SelectionState : Parcelable { + fun patches(bundles: List, allowUnsupported: Boolean): PatchesSelection + + @Parcelize + data class Customized(val patchesSelection: PatchesSelection) : SelectionState { + override fun patches(bundles: List, allowUnsupported: Boolean) = + bundles.toPatchSelection( + allowUnsupported + ) { uid, patch -> + patchesSelection[uid]?.contains(patch.name) ?: false + } + } + + @Parcelize + data object Default : SelectionState { + override fun patches(bundles: List, allowUnsupported: Boolean) = + bundles.toPatchSelection(allowUnsupported) { _, patch -> patch.include } + } +} + diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt index 45f21377..12a91a6c 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -98,7 +98,18 @@ class PM( null } - fun getPackageInfo(file: File): PackageInfo? = app.packageManager.getPackageArchiveInfo(file.absolutePath, 0) + fun getPackageInfo(file: File): PackageInfo? { + val path = file.absolutePath + val pkgInfo = app.packageManager.getPackageArchiveInfo(path, 0) ?: return null + + // This is needed in order to load label and icon. + pkgInfo.applicationInfo.apply { + sourceDir = path + publicSourceDir = path + } + + return pkgInfo + } fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString() diff --git a/app/src/main/java/app/revanced/manager/util/saver/NullableSaver.kt b/app/src/main/java/app/revanced/manager/util/saver/NullableSaver.kt new file mode 100644 index 00000000..6116722a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/saver/NullableSaver.kt @@ -0,0 +1,22 @@ +package app.revanced.manager.util.saver + +import androidx.compose.runtime.saveable.Saver +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** + * Creates a saver that can save nullable versions of types that have custom savers. + */ +fun nullableSaver(baseSaver: Saver): Saver> = + Saver( + save = { value -> + with(baseSaver) { + save(value ?: return@Saver Optional.empty()) + }?.let { + Optional.of(it) + } + }, + restore = { + it.getOrNull()?.let(baseSaver::restore) + } + ) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/saver/PersistentCollectionSavers.kt b/app/src/main/java/app/revanced/manager/util/saver/PersistentCollectionSavers.kt new file mode 100644 index 00000000..2a1418cd --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/saver/PersistentCollectionSavers.kt @@ -0,0 +1,69 @@ +package app.revanced.manager.util.saver + +import androidx.compose.runtime.saveable.Saver +import kotlinx.collections.immutable.* + +/** + * Create a [Saver] for [PersistentList]s. + */ +fun persistentListSaver() = Saver, List>( + save = { + it.toList() + }, + restore = { + it.toPersistentList() + } +) + +/** + * Create a [Saver] for [PersistentSet]s. + */ +fun persistentSetSaver() = Saver, Set>( + save = { + it.toSet() + }, + restore = { + it.toPersistentSet() + } +) + +/** + * Create a [Saver] for [PersistentMap]s. + */ +fun persistentMapSaver() = Saver, Map>( + save = { + it.toMap() + }, + restore = { + it.toPersistentMap() + } +) + +/** + * Create a saver for [PersistentMap]s with a custom [Saver] used for the values. + * Null values will not be saved by this [Saver]. + * + * @param valueSaver The [Saver] used for the values of the [Map]. + */ +fun persistentMapSaver( + valueSaver: Saver +) = Saver, Map>( + save = { + buildMap { + it.forEach { (key, value) -> + with(valueSaver) { + save(value)?.let { + this@buildMap[key] = it + } + } + } + } + }, + restore = { + buildMap { + it.forEach { (key, value) -> + this[key] = valueSaver.restore(value) ?: return@forEach + } + }.toPersistentMap() + } +) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 18b5bb75..5d49308d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,15 @@ Missing Error + + %1s • %2d available patches + + Start patching the application + Patch selection and options + %d patches selected + + Change version + %s selected Could not import legacy settings