mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2024-11-13 02:14:31 +01:00
feat: selected app info page (#1395)
This commit is contained in:
parent
7ba00cafd9
commit
c3af6acb2c
16 changed files with 779 additions and 309 deletions
|
@ -19,16 +19,17 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||||
import app.revanced.manager.ui.destination.Destination
|
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.AppSelectorScreen
|
||||||
import app.revanced.manager.ui.screen.DashboardScreen
|
import app.revanced.manager.ui.screen.DashboardScreen
|
||||||
import app.revanced.manager.ui.screen.InstallerScreen
|
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.SettingsScreen
|
||||||
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
||||||
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
||||||
import app.revanced.manager.ui.theme.Theme
|
import app.revanced.manager.ui.theme.Theme
|
||||||
import app.revanced.manager.ui.viewmodel.MainViewModel
|
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.tag
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import dev.olshevski.navigation.reimagined.AnimatedNavHost
|
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.pop
|
||||||
import dev.olshevski.navigation.reimagined.popUpTo
|
import dev.olshevski.navigation.reimagined.popUpTo
|
||||||
import dev.olshevski.navigation.reimagined.rememberNavController
|
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.core.parameter.parametersOf
|
||||||
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
|
@ -48,7 +49,7 @@ class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
|
|
||||||
val vm: MainViewModel = getActivityViewModel()
|
val vm: MainViewModel = getAndroidViewModel()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val theme by vm.prefs.theme.getAsState()
|
val theme by vm.prefs.theme.getAsState()
|
||||||
|
@ -102,7 +103,7 @@ class MainActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
legacyActivityState = LegacyActivity.LAUNCHED
|
legacyActivityState = LegacyActivity.LAUNCHED
|
||||||
} else if (legacyActivityState == LegacyActivity.FAILED){
|
} else if (legacyActivityState == LegacyActivity.FAILED) {
|
||||||
AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
|
AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,15 +115,26 @@ class MainActivity : ComponentActivity() {
|
||||||
is Destination.Dashboard -> DashboardScreen(
|
is Destination.Dashboard -> DashboardScreen(
|
||||||
onSettingsClick = { navController.navigate(Destination.Settings) },
|
onSettingsClick = { navController.navigate(Destination.Settings) },
|
||||||
onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
|
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 ->
|
onPatchClick = { packageName, patchesSelection ->
|
||||||
navController.navigate(Destination.VersionSelector(packageName, patchesSelection))
|
navController.navigate(
|
||||||
|
Destination.VersionSelector(
|
||||||
|
packageName,
|
||||||
|
patchesSelection
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onBackClick = { navController.pop() },
|
onBackClick = { navController.pop() },
|
||||||
viewModel = getViewModel { parametersOf(destination.installedApp) }
|
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is Destination.Settings -> SettingsScreen(
|
is Destination.Settings -> SettingsScreen(
|
||||||
|
@ -131,7 +143,13 @@ class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
is Destination.AppSelector -> AppSelectorScreen(
|
is Destination.AppSelector -> AppSelectorScreen(
|
||||||
onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
|
onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
|
||||||
onStorageClick = { navController.navigate(Destination.PatchesSelector(it)) },
|
onStorageClick = {
|
||||||
|
navController.navigate(
|
||||||
|
Destination.SelectedApplicationInfo(
|
||||||
|
it
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
onBackClick = { navController.pop() }
|
onBackClick = { navController.pop() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -139,32 +157,42 @@ class MainActivity : ComponentActivity() {
|
||||||
onBackClick = { navController.pop() },
|
onBackClick = { navController.pop() },
|
||||||
onAppClick = { selectedApp ->
|
onAppClick = { selectedApp ->
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
Destination.PatchesSelector(
|
Destination.SelectedApplicationInfo(
|
||||||
selectedApp,
|
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
|
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(
|
is Destination.Installer -> InstallerScreen(
|
||||||
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
|
onBackClick = { navController.popUpTo { it is Destination.Dashboard } },
|
||||||
vm = getViewModel { parametersOf(destination) }
|
vm = getComposeViewModel { parametersOf(destination) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import org.koin.dsl.module
|
||||||
val viewModelModule = module {
|
val viewModelModule = module {
|
||||||
viewModelOf(::MainViewModel)
|
viewModelOf(::MainViewModel)
|
||||||
viewModelOf(::DashboardViewModel)
|
viewModelOf(::DashboardViewModel)
|
||||||
|
viewModelOf(::SelectedAppInfoViewModel)
|
||||||
viewModelOf(::PatchesSelectorViewModel)
|
viewModelOf(::PatchesSelectorViewModel)
|
||||||
viewModelOf(::SettingsViewModel)
|
viewModelOf(::SettingsViewModel)
|
||||||
viewModelOf(::AdvancedSettingsViewModel)
|
viewModelOf(::AdvancedSettingsViewModel)
|
||||||
|
@ -19,5 +20,5 @@ val viewModelModule = module {
|
||||||
viewModelOf(::ContributorViewModel)
|
viewModelOf(::ContributorViewModel)
|
||||||
viewModelOf(::DownloadsViewModel)
|
viewModelOf(::DownloadsViewModel)
|
||||||
viewModelOf(::InstalledAppsViewModel)
|
viewModelOf(::InstalledAppsViewModel)
|
||||||
viewModelOf(::AppInfoViewModel)
|
viewModelOf(::InstalledAppInfoViewModel)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ sealed interface Destination : Parcelable {
|
||||||
object Dashboard : Destination
|
object Dashboard : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ApplicationInfo(val installedApp: InstalledApp) : Destination
|
data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object AppSelector : Destination
|
object AppSelector : Destination
|
||||||
|
@ -26,7 +26,7 @@ sealed interface Destination : Parcelable {
|
||||||
data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination
|
data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class PatchesSelector(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
|
data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination
|
data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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<PatchInfo>,
|
||||||
|
val unsupported: List<PatchInfo>,
|
||||||
|
val universal: List<PatchInfo>
|
||||||
|
) {
|
||||||
|
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<BundleInfo>.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<PatchInfo>()
|
||||||
|
val unsupported = mutableListOf<PatchInfo>()
|
||||||
|
val universal = mutableListOf<PatchInfo>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,18 +41,19 @@ import androidx.compose.ui.unit.dp
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
import app.revanced.manager.ui.component.AppIcon
|
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.AppLabel
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.SegmentedButton
|
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
|
import app.revanced.manager.util.PatchesSelection
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppInfoScreen(
|
fun InstalledAppInfoScreen(
|
||||||
onPatchClick: (packageName: String, patchesSelection: PatchesSelection) -> Unit,
|
onPatchClick: (packageName: String, patchesSelection: PatchesSelection) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
viewModel: AppInfoViewModel
|
viewModel: InstalledAppInfoViewModel
|
||||||
) {
|
) {
|
||||||
SideEffect {
|
SideEffect {
|
||||||
viewModel.onBackClick = onBackClick
|
viewModel.onBackClick = onBackClick
|
||||||
|
@ -80,27 +81,8 @@ fun AppInfoScreen(
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
Column(
|
AppInfo(viewModel.appInfo) {
|
||||||
modifier = Modifier
|
Text(viewModel.installedApp.version, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
|
||||||
.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)
|
|
||||||
|
|
||||||
if (viewModel.installedApp.installType == InstallType.ROOT) {
|
if (viewModel.installedApp.installType == InstallType.ROOT) {
|
||||||
Text(
|
Text(
|
|
@ -15,16 +15,15 @@ import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
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.FilterList
|
||||||
import androidx.compose.material.icons.outlined.HelpOutline
|
import androidx.compose.material.icons.outlined.HelpOutline
|
||||||
import androidx.compose.material.icons.outlined.Restore
|
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.Search
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material.icons.outlined.WarningAmber
|
import androidx.compose.material.icons.outlined.WarningAmber
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
|
@ -42,6 +41,7 @@ import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
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.Countdown
|
||||||
import app.revanced.manager.ui.component.patches.OptionItem
|
import app.revanced.manager.ui.component.patches.OptionItem
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
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_SUPPORTED
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
||||||
|
@ -77,7 +75,7 @@ import org.koin.compose.rememberKoinInject
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PatchesSelectorScreen(
|
fun PatchesSelectorScreen(
|
||||||
onPatchClick: (PatchesSelection, Options) -> Unit,
|
onSave: (PatchesSelection?, Options) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: PatchesSelectorViewModel
|
vm: PatchesSelectorViewModel
|
||||||
) {
|
) {
|
||||||
|
@ -93,10 +91,10 @@ fun PatchesSelectorScreen(
|
||||||
mutableStateOf(null)
|
mutableStateOf(null)
|
||||||
}
|
}
|
||||||
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
|
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
|
||||||
var showPatchButton by remember { mutableStateOf(true) }
|
val showPatchButton by remember {
|
||||||
LaunchedEffect(Unit) {
|
derivedStateOf { vm.selectionIsValid(bundles) }
|
||||||
showPatchButton = vm.isSelectionNotEmpty()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = {
|
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())
|
if (vm.compatibleVersions.isNotEmpty())
|
||||||
UnsupportedDialog(
|
UnsupportedDialog(
|
||||||
appVersion = vm.input.selectedApp.version,
|
appVersion = vm.appVersion,
|
||||||
supportedVersions = vm.compatibleVersions,
|
supportedVersions = vm.compatibleVersions,
|
||||||
onDismissRequest = vm::dismissDialogs
|
onDismissRequest = vm::dismissDialogs
|
||||||
)
|
)
|
||||||
|
@ -194,8 +165,6 @@ fun PatchesSelectorScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowExperimental by vm.allowExperimental.getAsState()
|
|
||||||
|
|
||||||
fun LazyListScope.patchList(
|
fun LazyListScope.patchList(
|
||||||
uid: Int,
|
uid: Int,
|
||||||
patches: List<PatchInfo>,
|
patches: List<PatchInfo>,
|
||||||
|
@ -227,17 +196,10 @@ fun PatchesSelectorScreen(
|
||||||
if (vm.selectionWarningEnabled) {
|
if (vm.selectionWarningEnabled) {
|
||||||
vm.pendingSelectionAction = {
|
vm.pendingSelectionAction = {
|
||||||
vm.togglePatch(uid, patch)
|
vm.togglePatch(uid, patch)
|
||||||
vm.viewModelScope.launch {
|
|
||||||
showPatchButton = vm.isSelectionNotEmpty()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
vm.togglePatch(uid, patch)
|
vm.togglePatch(uid, patch)
|
||||||
vm.viewModelScope.launch {
|
|
||||||
showPatchButton = vm.isSelectionNotEmpty()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
supported = supported
|
supported = supported
|
||||||
)
|
)
|
||||||
|
@ -292,7 +254,7 @@ fun PatchesSelectorScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allowExperimental) return@LazyColumn
|
if (!vm.allowExperimental) return@LazyColumn
|
||||||
patchList(
|
patchList(
|
||||||
uid = bundle.uid,
|
uid = bundle.uid,
|
||||||
patches = bundle.unsupported.searched(),
|
patches = bundle.unsupported.searched(),
|
||||||
|
@ -332,22 +294,19 @@ fun PatchesSelectorScreen(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if(showPatchButton) {
|
if (!showPatchButton) return@Scaffold
|
||||||
ExtendedFloatingActionButton(
|
|
||||||
text = {
|
ExtendedFloatingActionButton(
|
||||||
Text(stringResource(R.string.patch))
|
text = { Text(stringResource(R.string.save)) },
|
||||||
},
|
icon = { Icon(Icons.Outlined.Save, null) },
|
||||||
icon = { Icon(Icons.Default.Build, null) },
|
onClick = {
|
||||||
onClick = {
|
// TODO: only allow this if all required options have been set.
|
||||||
// TODO: only allow this if all required options have been set.
|
composableScope.launch {
|
||||||
composableScope.launch {
|
vm.saveSelection()
|
||||||
val selection = vm.getSelection()
|
onSave(vm.getCustomSelection(), vm.getOptions())
|
||||||
vm.saveSelection(selection).join()
|
|
||||||
onPatchClick(selection, vm.getOptions())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Column(
|
Column(
|
||||||
|
@ -407,7 +366,7 @@ fun PatchesSelectorScreen(
|
||||||
uid = bundle.uid,
|
uid = bundle.uid,
|
||||||
patches = bundle.unsupported,
|
patches = bundle.unsupported,
|
||||||
filterFlag = SHOW_UNSUPPORTED,
|
filterFlag = SHOW_UNSUPPORTED,
|
||||||
supported = allowExperimental
|
supported = vm.allowExperimental
|
||||||
) {
|
) {
|
||||||
ListHeader(
|
ListHeader(
|
||||||
title = stringResource(R.string.unsupported_patches),
|
title = stringResource(R.string.unsupported_patches),
|
||||||
|
|
|
@ -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<SelectedAppInfoDestination>(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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
class AppInfoViewModel(
|
class InstalledAppInfoViewModel(
|
||||||
val installedApp: InstalledApp
|
val installedApp: InstalledApp
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
|
@ -2,8 +2,8 @@ package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
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.PatchSelectionRepository
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
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.Options
|
||||||
import app.revanced.manager.util.PatchesSelection
|
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.saver.snapshotStateMapSaver
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
import kotlinx.collections.immutable.*
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@OptIn(SavedStateHandleSaveableApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class)
|
||||||
class PatchesSelectorViewModel(
|
class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||||
val input: Destination.PatchesSelector
|
|
||||||
) : ViewModel(), KoinComponent {
|
|
||||||
private val app: Application = get()
|
private val app: Application = get()
|
||||||
private val selectionRepository: PatchSelectionRepository = get()
|
private val selectionRepository: PatchSelectionRepository = get()
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
private val prefs: PreferencesManager = 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)
|
var pendingSelectionAction by mutableStateOf<(() -> Unit)?>(null)
|
||||||
|
|
||||||
|
// TODO: this should be hoisted to the parent screen
|
||||||
var selectionWarningEnabled by mutableStateOf(true)
|
var selectionWarningEnabled by mutableStateOf(true)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val allowExperimental = get<PreferencesManager>().allowExperimental
|
val allowExperimental = get<PreferencesManager>().allowExperimental.getBlocking()
|
||||||
val bundlesFlow = get<PatchBundleRepository>().sources.flatMapLatestAndCombine(
|
val bundlesFlow =
|
||||||
combiner = { it.filterNotNull() }
|
get<PatchBundleRepository>().bundleInfoFlow(packageName, input.app.version)
|
||||||
) { source ->
|
|
||||||
// Regenerate bundle information whenever this source updates.
|
|
||||||
source.state.map { state ->
|
|
||||||
val bundle = state.patchBundleOrNull() ?: return@map null
|
|
||||||
|
|
||||||
val supported = mutableListOf<PatchInfo>()
|
|
||||||
val unsupported = mutableListOf<PatchInfo>()
|
|
||||||
val universal = mutableListOf<PatchInfo>()
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
@ -88,63 +68,28 @@ class PatchesSelectorViewModel(
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val experimental = allowExperimental.get()
|
fun BundleInfo.hasDefaultPatches() = patchSequence(allowExperimental).any { it.include }
|
||||||
fun BundleInfo.hasDefaultPatches(): Boolean {
|
|
||||||
return if (experimental) {
|
|
||||||
all.asSequence()
|
|
||||||
} else {
|
|
||||||
sequence {
|
|
||||||
yieldAll(supported)
|
|
||||||
yieldAll(universal)
|
|
||||||
}
|
|
||||||
}.any { it.include }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't show the warning if there are no default patches.
|
// Don't show the warning if there are no default patches.
|
||||||
selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches)
|
selectionWarningEnabled = bundlesFlow.first().any(BundleInfo::hasDefaultPatches)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var baseSelectionMode by mutableStateOf(BaseSelectionMode.DEFAULT)
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val previousPatchesSelection: SnapshotStateMap<Int, Set<String>> = mutableStateMapOf()
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch(Dispatchers.Default) { loadPreviousSelection() }
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasPreviousSelection by derivedStateOf {
|
|
||||||
previousPatchesSelection.filterValues(Set<String>::isNotEmpty).isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var hasModifiedSelection = false
|
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(
|
private val patchOptions: PersistentOptions by savedStateHandle.saveable(
|
||||||
saver = explicitPatchesSelectionSaver,
|
|
||||||
init = ::mutableStateMapOf
|
|
||||||
)
|
|
||||||
|
|
||||||
private val patchOptions: SnapshotOptions by savedStateHandle.saveable(
|
|
||||||
saver = optionsSaver,
|
saver = optionsSaver,
|
||||||
init = ::mutableStateMapOf
|
) {
|
||||||
)
|
// Convert Options to PersistentOptions
|
||||||
|
input.options.mapValuesTo(mutableStateMapOf()) { (_, allPatches) ->
|
||||||
private val selectors by derivedStateOf<Array<Selector>> {
|
allPatches.mapValues { (_, options) -> options.toPersistentMap() }.toPersistentMap()
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -154,52 +99,39 @@ class PatchesSelectorViewModel(
|
||||||
|
|
||||||
val compatibleVersions = mutableStateListOf<String>()
|
val compatibleVersions = mutableStateListOf<String>()
|
||||||
|
|
||||||
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 set
|
||||||
|
|
||||||
private suspend fun loadPreviousSelection() {
|
private suspend fun generateDefaultSelection(): PersistentPatchesSelection {
|
||||||
val selection = (input.patchesSelection ?: selectionRepository.getSelection(
|
val bundles = bundlesFlow.first()
|
||||||
packageName
|
val generatedSelection =
|
||||||
)).mapValues { (_, value) -> value.toSet() }
|
bundles.toPatchSelection(allowExperimental) { _, patch -> patch.include }
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
return generatedSelection.toPersistentPatchesSelection()
|
||||||
previousPatchesSelection.putAll(selection)
|
}
|
||||||
|
|
||||||
|
fun selectionIsValid(bundles: List<BundleInfo>) = bundles.any { bundle ->
|
||||||
|
bundle.patchSequence(allowExperimental).any { patch ->
|
||||||
|
isSelected(bundle.uid, patch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun switchBaseSelectionMode() = viewModelScope.launch {
|
fun isSelected(bundle: Int, patch: PatchInfo) = customPatchesSelection?.let { selection ->
|
||||||
baseSelectionMode = if (baseSelectionMode == BaseSelectionMode.DEFAULT) {
|
selection[bundle]?.contains(patch.name) ?: false
|
||||||
BaseSelectionMode.PREVIOUS
|
} ?: patch.include
|
||||||
} else {
|
|
||||||
BaseSelectionMode.DEFAULT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun patchesAvailable(bundle: BundleInfo): List<PatchInfo> {
|
|
||||||
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 togglePatch(bundle: Int, patch: PatchInfo) = viewModelScope.launch {
|
||||||
hasModifiedSelection = true
|
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) {
|
fun confirmSelectionWarning(dismissPermanently: Boolean) {
|
||||||
|
@ -221,46 +153,39 @@ class PatchesSelectorViewModel(
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
patchOptions.clear()
|
patchOptions.clear()
|
||||||
baseSelectionMode = BaseSelectionMode.DEFAULT
|
customPatchesSelection = null
|
||||||
explicitPatchesSelection.clear()
|
|
||||||
hasModifiedSelection = false
|
hasModifiedSelection = false
|
||||||
app.toast(app.getString(R.string.patch_selection_reset_toast))
|
app.toast(app.getString(R.string.patch_selection_reset_toast))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSelection(): PatchesSelection {
|
fun getCustomSelection(): PatchesSelection? {
|
||||||
val bundles = bundlesFlow.first()
|
// Convert persistent collections to standard hash collections because persistent collections are not parcelable.
|
||||||
val removeUnsupported = !allowExperimental.get()
|
|
||||||
|
|
||||||
return bundles.associate { bundle ->
|
return customPatchesSelection?.mapValues { (_, v) -> v.toSet() }
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveSelection(selection: PatchesSelection) =
|
fun getOptions(): Options {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
// Convert the collection for the same reasons as in getCustomSelection()
|
||||||
when {
|
|
||||||
hasModifiedSelection -> selectionRepository.updateSelection(packageName, selection)
|
|
||||||
baseSelectionMode == BaseSelectionMode.DEFAULT -> selectionRepository.clearSelection(
|
|
||||||
packageName
|
|
||||||
)
|
|
||||||
|
|
||||||
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 getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
|
||||||
|
|
||||||
fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {
|
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) {
|
fun resetOptions(bundle: Int, patch: PatchInfo) {
|
||||||
|
@ -274,7 +199,7 @@ class PatchesSelectorViewModel(
|
||||||
|
|
||||||
fun openUnsupportedDialog(unsupportedPatches: List<PatchInfo>) {
|
fun openUnsupportedDialog(unsupportedPatches: List<PatchInfo>) {
|
||||||
compatibleVersions.addAll(unsupportedPatches.flatMap { patch ->
|
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_UNIVERSAL = 2 // 2^1
|
||||||
const val SHOW_UNSUPPORTED = 4 // 2^2
|
const val SHOW_UNSUPPORTED = 4 // 2^2
|
||||||
|
|
||||||
private fun <K, K2, V> SnapshotStateMap<K, SnapshotStateMap<K2, V>>.getOrCreate(key: K) =
|
private val optionsSaver: Saver<PersistentOptions, Options> = snapshotStateMapSaver(
|
||||||
getOrPut(key, ::mutableStateMapOf)
|
|
||||||
|
|
||||||
private val optionsSaver: Saver<SnapshotOptions, Options> = snapshotStateMapSaver(
|
|
||||||
// Patch name -> Options
|
// Patch name -> Options
|
||||||
valueSaver = snapshotStateMapSaver(
|
valueSaver = persistentMapSaver(
|
||||||
// Option key -> Option value
|
// Option key -> Option value
|
||||||
valueSaver = snapshotStateMapSaver()
|
valueSaver = persistentMapSaver()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
private val explicitPatchesSelectionSaver: Saver<SnapshotExplicitPatchesSelection, ExplicitPatchesSelection> =
|
private val patchesSaver: Saver<PersistentPatchesSelection?, Optional<PatchesSelection>> =
|
||||||
snapshotStateMapSaver(valueSaver = snapshotStateMapSaver())
|
nullableSaver(persistentMapSaver(valueSaver = persistentSetSaver()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
data class Params(
|
||||||
* An enum for controlling the behavior of the selector.
|
val app: SelectedApp,
|
||||||
*/
|
val currentSelection: PatchesSelection?,
|
||||||
enum class BaseSelectionMode {
|
val options: Options,
|
||||||
/**
|
|
||||||
* 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<PatchInfo>,
|
|
||||||
val supported: List<PatchInfo>,
|
|
||||||
val unsupported: List<PatchInfo>,
|
|
||||||
val universal: List<PatchInfo>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private typealias Selector = (Int, PatchInfo) -> Boolean?
|
// Versions of other types, but utilizing persistent/observable collection types.
|
||||||
private typealias ExplicitPatchesSelection = Map<Int, Map<String, Boolean>>
|
private typealias PersistentOptions = SnapshotStateMap<Int, PersistentMap<String, PersistentMap<String, Any?>>>
|
||||||
|
private typealias PersistentPatchesSelection = PersistentMap<Int, PersistentSet<String>>
|
||||||
|
|
||||||
// Versions of other types, but utilizing observable collection types instead.
|
private fun PatchesSelection.toPersistentPatchesSelection(): PersistentPatchesSelection =
|
||||||
private typealias SnapshotOptions = SnapshotStateMap<Int, SnapshotStateMap<String, SnapshotStateMap<String, Any?>>>
|
mapValues { (_, v) -> v.toPersistentSet() }.toPersistentMap()
|
||||||
private typealias SnapshotExplicitPatchesSelection = SnapshotStateMap<Int, SnapshotStateMap<String, Boolean>>
|
|
|
@ -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<SelectionState> = 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<BundleInfo>, allowUnsupported: Boolean) =
|
||||||
|
selectionState.patches(bundles, allowUnsupported)
|
||||||
|
|
||||||
|
fun getCustomPatches(
|
||||||
|
bundles: List<BundleInfo>,
|
||||||
|
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<BundleInfo>, allowUnsupported: Boolean): PatchesSelection
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Customized(val patchesSelection: PatchesSelection) : SelectionState {
|
||||||
|
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||||
|
bundles.toPatchSelection(
|
||||||
|
allowUnsupported
|
||||||
|
) { uid, patch ->
|
||||||
|
patchesSelection[uid]?.contains(patch.name) ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object Default : SelectionState {
|
||||||
|
override fun patches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||||
|
bundles.toPatchSelection(allowUnsupported) { _, patch -> patch.include }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -98,7 +98,18 @@ class PM(
|
||||||
null
|
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()
|
fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString()
|
||||||
|
|
||||||
|
|
|
@ -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 <Original : Any, Saveable : Any> nullableSaver(baseSaver: Saver<Original, Saveable>): Saver<Original?, Optional<Saveable>> =
|
||||||
|
Saver(
|
||||||
|
save = { value ->
|
||||||
|
with(baseSaver) {
|
||||||
|
save(value ?: return@Saver Optional.empty())
|
||||||
|
}?.let {
|
||||||
|
Optional.of(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restore = {
|
||||||
|
it.getOrNull()?.let(baseSaver::restore)
|
||||||
|
}
|
||||||
|
)
|
|
@ -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 <T> persistentListSaver() = Saver<PersistentList<T>, List<T>>(
|
||||||
|
save = {
|
||||||
|
it.toList()
|
||||||
|
},
|
||||||
|
restore = {
|
||||||
|
it.toPersistentList()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [Saver] for [PersistentSet]s.
|
||||||
|
*/
|
||||||
|
fun <T> persistentSetSaver() = Saver<PersistentSet<T>, Set<T>>(
|
||||||
|
save = {
|
||||||
|
it.toSet()
|
||||||
|
},
|
||||||
|
restore = {
|
||||||
|
it.toPersistentSet()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [Saver] for [PersistentMap]s.
|
||||||
|
*/
|
||||||
|
fun <K, V> persistentMapSaver() = Saver<PersistentMap<K, V>, Map<K, V>>(
|
||||||
|
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 <K, Original, Saveable : Any> persistentMapSaver(
|
||||||
|
valueSaver: Saver<Original, Saveable>
|
||||||
|
) = Saver<PersistentMap<K, Original>, Map<K, Saveable>>(
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
)
|
|
@ -25,6 +25,15 @@
|
||||||
|
|
||||||
<string name="bundle_missing">Missing</string>
|
<string name="bundle_missing">Missing</string>
|
||||||
<string name="bundle_error">Error</string>
|
<string name="bundle_error">Error</string>
|
||||||
|
|
||||||
|
<string name="selected_app_meta">%1s • %2d available patches</string>
|
||||||
|
|
||||||
|
<string name="patch_item_description">Start patching the application</string>
|
||||||
|
<string name="patch_selector_item">Patch selection and options</string>
|
||||||
|
<string name="patch_selector_item_description">%d patches selected</string>
|
||||||
|
|
||||||
|
<string name="version_selector_item">Change version</string>
|
||||||
|
<string name="version_selector_item_description">%s selected</string>
|
||||||
|
|
||||||
<string name="legacy_import_failed">Could not import legacy settings</string>
|
<string name="legacy_import_failed">Could not import legacy settings</string>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue