mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-04-30 05:54:26 +02:00
finish UI stuff (i think)
This commit is contained in:
parent
38fe7bf9fd
commit
11a2a140e6
@ -167,7 +167,7 @@ class PatcherWorker(
|
|||||||
|
|
||||||
val inputFile = when (val selectedApp = args.input) {
|
val inputFile = when (val selectedApp = args.input) {
|
||||||
is SelectedApp.Download -> {
|
is SelectedApp.Download -> {
|
||||||
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.app)
|
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
|
||||||
|
|
||||||
download(plugin, data)
|
download(plugin, data)
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ sealed interface SelectedApp : Parcelable {
|
|||||||
data class Download(
|
data class Download(
|
||||||
override val packageName: String,
|
override val packageName: String,
|
||||||
override val version: String,
|
override val version: String,
|
||||||
val app: ParceledDownloaderData
|
val data: ParceledDownloaderData
|
||||||
) : SelectedApp
|
) : SelectedApp
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
import android.content.pm.PackageInfo
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@ -16,34 +17,36 @@ import androidx.compose.material3.ExtendedFloatingActionButton
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
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.InstalledApp
|
||||||
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||||
import app.revanced.manager.ui.component.AppInfo
|
import app.revanced.manager.ui.component.AppInfo
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||||
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
import app.revanced.manager.ui.destination.SelectedAppInfoDestination
|
import app.revanced.manager.ui.destination.SelectedAppInfoDestination
|
||||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||||
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||||
|
import app.revanced.manager.util.EventEffect
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
|
import app.revanced.manager.util.enabled
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.transparentListItemColors
|
import app.revanced.manager.util.transparentListItemColors
|
||||||
import dev.olshevski.navigation.reimagined.AnimatedNavHost
|
import dev.olshevski.navigation.reimagined.AnimatedNavHost
|
||||||
@ -54,6 +57,7 @@ import dev.olshevski.navigation.reimagined.rememberNavController
|
|||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectedAppInfoScreen(
|
fun SelectedAppInfoScreen(
|
||||||
onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit,
|
onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit,
|
||||||
@ -80,8 +84,12 @@ fun SelectedAppInfoScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var showSourceSelectorDialog by rememberSaveable {
|
val launcher = rememberLauncherForActivityResult(
|
||||||
mutableStateOf(false)
|
contract = ActivityResultContracts.StartActivityForResult(),
|
||||||
|
onResult = vm::handlePluginActivityResult
|
||||||
|
)
|
||||||
|
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
||||||
|
launcher.launch(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
val navController =
|
val navController =
|
||||||
@ -90,42 +98,120 @@ fun SelectedAppInfoScreen(
|
|||||||
NavBackHandler(controller = navController)
|
NavBackHandler(controller = navController)
|
||||||
|
|
||||||
AnimatedNavHost(controller = navController) { destination ->
|
AnimatedNavHost(controller = navController) { destination ->
|
||||||
|
val error by vm.error.collectAsStateWithLifecycle(null)
|
||||||
when (destination) {
|
when (destination) {
|
||||||
is SelectedAppInfoDestination.Main -> SelectedAppInfoScreen(
|
is SelectedAppInfoDestination.Main -> Scaffold(
|
||||||
onPatchClick = patchClick@{
|
topBar = {
|
||||||
if (selectedPatchCount == 0) {
|
AppTopBar(
|
||||||
context.toast(context.getString(R.string.no_patches_selected))
|
title = stringResource(R.string.app_info),
|
||||||
|
onBackClick = onBackClick
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
if (error != null) return@Scaffold
|
||||||
|
|
||||||
return@patchClick
|
ExtendedFloatingActionButton(
|
||||||
}
|
text = { Text(stringResource(R.string.patch)) },
|
||||||
onPatchClick(
|
icon = {
|
||||||
vm.selectedApp,
|
Icon(
|
||||||
patches,
|
Icons.Default.AutoFixHigh,
|
||||||
vm.getOptionsFiltered(bundles)
|
stringResource(R.string.patch)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = patchClick@{
|
||||||
|
if (selectedPatchCount == 0) {
|
||||||
|
context.toast(context.getString(R.string.no_patches_selected))
|
||||||
|
|
||||||
|
return@patchClick
|
||||||
|
}
|
||||||
|
onPatchClick(
|
||||||
|
vm.selectedApp,
|
||||||
|
patches,
|
||||||
|
vm.getOptionsFiltered(bundles)
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
onPatchSelectorClick = {
|
) { paddingValues ->
|
||||||
navController.navigate(
|
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
|
||||||
SelectedAppInfoDestination.PatchesSelector(
|
|
||||||
vm.selectedApp,
|
if (vm.showSourceSelector) {
|
||||||
vm.getCustomPatches(
|
AppSourceSelectorDialog(
|
||||||
bundles,
|
plugins = plugins,
|
||||||
allowIncompatiblePatches
|
installedApp = vm.installedAppData,
|
||||||
),
|
searchApp = SelectedApp.Search(
|
||||||
vm.options
|
vm.packageName,
|
||||||
|
vm.desiredVersion
|
||||||
|
),
|
||||||
|
activeSearchJob = vm.activePluginAction,
|
||||||
|
hasRoot = vm.hasRoot,
|
||||||
|
onDismissRequest = vm::dismissSourceSelector,
|
||||||
|
onSelectPlugin = vm::searchInPlugin,
|
||||||
|
onSelect = {
|
||||||
|
vm.selectedApp = it
|
||||||
|
vm.dismissSourceSelector()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnWithScrollbar(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
|
||||||
|
Text(
|
||||||
|
version ?: stringResource(R.string.selected_app_meta_any_version),
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PageItem(
|
||||||
|
R.string.patch_selector_item,
|
||||||
|
stringResource(
|
||||||
|
R.string.patch_selector_item_description,
|
||||||
|
selectedPatchCount
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(
|
||||||
|
SelectedAppInfoDestination.PatchesSelector(
|
||||||
|
vm.selectedApp,
|
||||||
|
vm.getCustomPatches(
|
||||||
|
bundles,
|
||||||
|
allowIncompatiblePatches
|
||||||
|
),
|
||||||
|
vm.options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
PageItem(
|
||||||
onSourceSelectorClick = {
|
R.string.apk_source_selector_item,
|
||||||
showSourceSelectorDialog = true
|
when (val app = vm.selectedApp) {
|
||||||
// navController.navigate(SelectedAppInfoDestination.VersionSelector)
|
is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
|
||||||
},
|
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
|
||||||
onBackClick = onBackClick,
|
is SelectedApp.Download -> stringResource(
|
||||||
selectedPatchCount = selectedPatchCount,
|
R.string.apk_source_downloader,
|
||||||
packageName = packageName,
|
plugins.find { it.packageName == app.data.pluginPackageName }?.name
|
||||||
version = version,
|
?: app.data.pluginPackageName
|
||||||
packageInfo = vm.selectedAppInfo,
|
)
|
||||||
)
|
|
||||||
|
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
vm.showSourceSelector()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
error?.let {
|
||||||
|
Text(
|
||||||
|
stringResource(it.resourceId),
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
|
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
|
||||||
onSave = { patches, options ->
|
onSave = { patches, options ->
|
||||||
@ -147,66 +233,6 @@ fun SelectedAppInfoScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun SelectedAppInfoScreen(
|
|
||||||
onPatchClick: () -> Unit,
|
|
||||||
onPatchSelectorClick: () -> Unit,
|
|
||||||
onSourceSelectorClick: () -> Unit,
|
|
||||||
onBackClick: () -> Unit,
|
|
||||||
selectedPatchCount: Int,
|
|
||||||
packageName: String,
|
|
||||||
version: String?,
|
|
||||||
packageInfo: PackageInfo?,
|
|
||||||
) {
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
AppTopBar(
|
|
||||||
title = stringResource(R.string.app_info),
|
|
||||||
onBackClick = onBackClick
|
|
||||||
)
|
|
||||||
},
|
|
||||||
floatingActionButton = {
|
|
||||||
ExtendedFloatingActionButton(
|
|
||||||
text = { Text(stringResource(R.string.patch)) },
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.AutoFixHigh,
|
|
||||||
stringResource(R.string.patch)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = onPatchClick
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
ColumnWithScrollbar(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues)
|
|
||||||
) {
|
|
||||||
AppInfo(packageInfo, placeholderLabel = packageName) {
|
|
||||||
Text(
|
|
||||||
version ?: stringResource(R.string.selected_app_meta_any_version),
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
PageItem(
|
|
||||||
R.string.patch_selector_item,
|
|
||||||
stringResource(R.string.patch_selector_item_description, selectedPatchCount),
|
|
||||||
onPatchSelectorClick
|
|
||||||
)
|
|
||||||
PageItem(
|
|
||||||
R.string.version_selector_item,
|
|
||||||
version?.let { stringResource(R.string.version_selector_item_description, it) }
|
|
||||||
?: stringResource(R.string.version_selector_item_description_auto),
|
|
||||||
onSourceSelectorClick
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) {
|
private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) {
|
||||||
ListItem(
|
ListItem(
|
||||||
@ -234,19 +260,21 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppSourceSelectorDialog(onDismissRequest: () -> Unit) {
|
private fun AppSourceSelectorDialog(
|
||||||
|
plugins: List<LoadedDownloaderPlugin>,
|
||||||
|
installedApp: Pair<SelectedApp.Installed, InstalledApp?>?,
|
||||||
|
searchApp: SelectedApp.Search,
|
||||||
|
activeSearchJob: String?,
|
||||||
|
hasRoot: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSelectPlugin: (LoadedDownloaderPlugin) -> Unit,
|
||||||
|
onSelect: (SelectedApp) -> Unit,
|
||||||
|
) {
|
||||||
|
val canSelect = activeSearchJob == null
|
||||||
|
|
||||||
AlertDialogExtended(
|
AlertDialogExtended(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text("Select")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismissRequest) {
|
TextButton(onClick = onDismissRequest) {
|
||||||
Text(stringResource(R.string.cancel))
|
Text(stringResource(R.string.cancel))
|
||||||
}
|
}
|
||||||
@ -254,37 +282,49 @@ private fun AppSourceSelectorDialog(onDismissRequest: () -> Unit) {
|
|||||||
title = { Text("Select source") },
|
title = { Text("Select source") },
|
||||||
textHorizontalPadding = PaddingValues(horizontal = 0.dp),
|
textHorizontalPadding = PaddingValues(horizontal = 0.dp),
|
||||||
text = {
|
text = {
|
||||||
/*
|
|
||||||
val presets = remember(scope.option.presets) {
|
|
||||||
scope.option.presets?.entries?.toList().orEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
@Composable
|
item(key = "auto") {
|
||||||
fun Item(title: String, value: Any?, presetKey: String?) {
|
val hasPlugins = plugins.isNotEmpty()
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { selectedPreset = presetKey },
|
modifier = Modifier
|
||||||
headlineContent = { Text(title) },
|
.clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) }
|
||||||
supportingContent = value?.toString()?.let { { Text(it) } },
|
.enabled(hasPlugins),
|
||||||
leadingContent = {
|
headlineContent = { Text("Auto") },
|
||||||
RadioButton(
|
supportingContent = { Text(if (hasPlugins) "Use all installed downloaders to find a suitable app." else "No plugins available") },
|
||||||
selected = selectedPreset == presetKey,
|
|
||||||
onClick = { selectedPreset = presetKey }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
colors = transparentListItemColors
|
colors = transparentListItemColors
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
items(presets, key = { it.key }) {
|
installedApp?.let { (app, meta) ->
|
||||||
Item(it.key, it.value, it.key)
|
item(key = "installed") {
|
||||||
|
val (usable, text) = when {
|
||||||
|
// Mounted apps must be unpatched before patching, which cannot be done without root access.
|
||||||
|
meta?.installType == InstallType.ROOT && !hasRoot -> false to "Mounted apps cannot be patched again without root access"
|
||||||
|
// Patching already patched apps is not allowed because patches expect unpatched apps.
|
||||||
|
meta?.installType == InstallType.DEFAULT -> false to stringResource(R.string.already_patched)
|
||||||
|
else -> true to app.version
|
||||||
|
}
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(enabled = canSelect && usable) { onSelect(app) }
|
||||||
|
.enabled(usable), // TODO: version safeguard
|
||||||
|
headlineContent = { Text(stringResource(R.string.installed)) },
|
||||||
|
supportingContent = { Text(text) },
|
||||||
|
colors = transparentListItemColors
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item(key = null) {
|
items(plugins, key = { "plugin_${it.packageName}" }) { plugin ->
|
||||||
Item(stringResource(R.string.option_preset_custom_value), null, null)
|
ListItem(
|
||||||
|
modifier = Modifier.clickable(enabled = canSelect) { onSelectPlugin(plugin) },
|
||||||
|
headlineContent = { Text(plugin.name) },
|
||||||
|
supportingContent = { Text("Try to find the app using ${plugin.name}") },
|
||||||
|
trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == plugin.packageName },
|
||||||
|
colors = transparentListItemColors
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,46 +1,80 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||||
import androidx.lifecycle.viewmodel.compose.saveable
|
import androidx.lifecycle.viewmodel.compose.saveable
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||||
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||||
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||||
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
|
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||||
|
import app.revanced.manager.plugin.downloader.GetScope
|
||||||
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
|
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||||
import app.revanced.manager.ui.model.BundleInfo
|
import app.revanced.manager.ui.model.BundleInfo
|
||||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
|
import app.revanced.manager.util.toast
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
|
||||||
@OptIn(SavedStateHandleSaveableApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||||
class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||||
|
private val app: Application = get()
|
||||||
val bundlesRepo: PatchBundleRepository = get()
|
val bundlesRepo: PatchBundleRepository = get()
|
||||||
private val bundleRepository: PatchBundleRepository = get()
|
private val bundleRepository: PatchBundleRepository = get()
|
||||||
private val selectionRepository: PatchSelectionRepository = get()
|
private val selectionRepository: PatchSelectionRepository = get()
|
||||||
private val optionsRepository: PatchOptionsRepository = get()
|
private val optionsRepository: PatchOptionsRepository = get()
|
||||||
|
private val pluginsRepository: DownloaderPluginRepository = get()
|
||||||
|
private val installedAppRepository: InstalledAppRepository = get()
|
||||||
|
private val rootInstaller: RootInstaller = get()
|
||||||
private val pm: PM = get()
|
private val pm: PM = get()
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
val prefs: PreferencesManager = get()
|
val prefs: PreferencesManager = get()
|
||||||
|
val plugins = pluginsRepository.loadedPluginsFlow
|
||||||
|
val desiredVersion = input.app.version
|
||||||
|
val packageName = input.app.packageName
|
||||||
|
|
||||||
private val persistConfiguration = input.patches == null
|
private val persistConfiguration = input.patches == null
|
||||||
|
|
||||||
|
val hasRoot = rootInstaller.hasRootAccess()
|
||||||
|
var installedAppData: Pair<SelectedApp.Installed, InstalledApp?>? by mutableStateOf(null)
|
||||||
|
private set
|
||||||
|
|
||||||
private var _selectedApp by savedStateHandle.saveable {
|
private var _selectedApp by savedStateHandle.saveable {
|
||||||
mutableStateOf(input.app)
|
mutableStateOf(input.app)
|
||||||
}
|
}
|
||||||
@ -57,6 +91,19 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
invalidateSelectedAppInfo()
|
invalidateSelectedAppInfo()
|
||||||
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
|
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
|
||||||
|
val installedAppDeferred =
|
||||||
|
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
|
||||||
|
|
||||||
|
installedAppData =
|
||||||
|
packageInfo.await()?.let {
|
||||||
|
SelectedApp.Installed(
|
||||||
|
packageName,
|
||||||
|
it.versionName
|
||||||
|
) to installedAppDeferred.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var options: Options by savedStateHandle.saveable {
|
var options: Options by savedStateHandle.saveable {
|
||||||
@ -65,9 +112,6 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||||
|
|
||||||
// Accessing this from another thread may cause crashes.
|
|
||||||
val packageName = selectedApp.packageName
|
|
||||||
|
|
||||||
state.value = withContext(Dispatchers.Default) {
|
state.value = withContext(Dispatchers.Default) {
|
||||||
val bundlePatches = bundleRepository.bundles.first()
|
val bundlePatches = bundleRepository.bundles.first()
|
||||||
.mapValues { (_, bundle) -> bundle.patches.associateBy { it.name } }
|
.mapValues { (_, bundle) -> bundle.patches.associateBy { it.name } }
|
||||||
@ -90,7 +134,7 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (!prefs.disableSelectionWarning.get()) return@launch
|
if (!prefs.disableSelectionWarning.get()) return@launch
|
||||||
|
|
||||||
val previous = selectionRepository.getSelection(selectedApp.packageName)
|
val previous = selectionRepository.getSelection(packageName)
|
||||||
if (previous.values.sumOf { it.size } == 0) return@launch
|
if (previous.values.sumOf { it.size } == 0) return@launch
|
||||||
selection.value = SelectionState.Customized(previous)
|
selection.value = SelectionState.Customized(previous)
|
||||||
}
|
}
|
||||||
@ -98,6 +142,86 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
|||||||
selection
|
selection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showSourceSelector by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
private var pluginAction: Pair<LoadedDownloaderPlugin, Job>? by mutableStateOf(null)
|
||||||
|
val activePluginAction get() = pluginAction?.first?.packageName
|
||||||
|
private var launchedActivity by mutableStateOf<CompletableDeferred<ActivityResult>?>(null)
|
||||||
|
private val launchActivityChannel = Channel<Intent>()
|
||||||
|
val launchActivityFlow = launchActivityChannel.receiveAsFlow()
|
||||||
|
|
||||||
|
val error = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app ->
|
||||||
|
when {
|
||||||
|
app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showSourceSelector() {
|
||||||
|
dismissSourceSelector()
|
||||||
|
showSourceSelector = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissSourceSelector() {
|
||||||
|
pluginAction?.second?.cancel()
|
||||||
|
pluginAction = null
|
||||||
|
showSourceSelector = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun searchInPlugin(plugin: LoadedDownloaderPlugin) {
|
||||||
|
pluginAction?.second?.cancel()
|
||||||
|
pluginAction = null
|
||||||
|
pluginAction = plugin to viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val scope = object : GetScope {
|
||||||
|
override suspend fun requestStartActivity(intent: Intent) =
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (launchedActivity != null) error("Previous activity has not finished")
|
||||||
|
try {
|
||||||
|
val result = with(CompletableDeferred<ActivityResult>()) {
|
||||||
|
launchedActivity = this
|
||||||
|
launchActivityChannel.send(intent)
|
||||||
|
await()
|
||||||
|
}
|
||||||
|
when (result.resultCode) {
|
||||||
|
Activity.RESULT_OK -> result.data
|
||||||
|
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||||
|
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||||
|
result.resultCode,
|
||||||
|
result.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
launchedActivity = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
plugin.get(scope, packageName, desiredVersion)
|
||||||
|
}?.let { (data, version) ->
|
||||||
|
if (desiredVersion != null && version != desiredVersion) {
|
||||||
|
app.toast("Plugin returned a package with the wrong version")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
selectedApp = SelectedApp.Download(
|
||||||
|
packageName,
|
||||||
|
version
|
||||||
|
?: error("Umm, I guess I need to make the parameter nullable now?"),
|
||||||
|
ParceledDownloaderData(plugin, data)
|
||||||
|
)
|
||||||
|
} ?: app.toast("App was not found")
|
||||||
|
} finally {
|
||||||
|
pluginAction = null
|
||||||
|
dismissSourceSelector()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handlePluginActivityResult(result: ActivityResult) {
|
||||||
|
launchedActivity?.complete(result)
|
||||||
|
}
|
||||||
|
|
||||||
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
|
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
|
||||||
val info = when (val app = selectedApp) {
|
val info = when (val app = selectedApp) {
|
||||||
is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) }
|
is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) }
|
||||||
@ -130,8 +254,6 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
|||||||
this.options = filteredOptions
|
this.options = filteredOptions
|
||||||
|
|
||||||
if (!persistConfiguration) return
|
if (!persistConfiguration) return
|
||||||
|
|
||||||
val packageName = selectedApp.packageName
|
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
||||||
?: selectionRepository.clearSelection(packageName)
|
?: selectionRepository.clearSelection(packageName)
|
||||||
@ -145,6 +267,10 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
|||||||
val patches: PatchSelection?,
|
val patches: PatchSelection?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enum class Error(@StringRes val resourceId: Int) {
|
||||||
|
NoPlugins(R.string.downloader_no_plugins_available)
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
/**
|
/**
|
||||||
* Returns a copy with all nonexistent options removed.
|
* Returns a copy with all nonexistent options removed.
|
||||||
|
@ -24,6 +24,8 @@ import androidx.compose.runtime.mutableIntStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@ -259,3 +261,5 @@ fun ScrollState.isScrollingUp(): State<Boolean> {
|
|||||||
|
|
||||||
val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
|
val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
|
||||||
val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
|
val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value
|
||||||
|
|
||||||
|
fun Modifier.enabled(condition: Boolean) = if (condition) this else alpha(0.5f)
|
@ -41,9 +41,11 @@
|
|||||||
<string name="patch_selector_item_description">%d patches selected</string>
|
<string name="patch_selector_item_description">%d patches selected</string>
|
||||||
<string name="no_patches_selected">No patches selected</string>
|
<string name="no_patches_selected">No patches selected</string>
|
||||||
|
|
||||||
<string name="version_selector_item">Change version</string>
|
<string name="apk_source_selector_item">Change source</string>
|
||||||
<string name="version_selector_item_description">%s selected</string>
|
<string name="apk_source_auto">Current: All downloaders</string>
|
||||||
<string name="version_selector_item_description_auto">Automatically selected</string>
|
<string name="apk_source_downloader">Current: %s</string>
|
||||||
|
<string name="apk_source_installed">Current: Installed</string>
|
||||||
|
<string name="apk_source_local">Current: File</string>
|
||||||
|
|
||||||
<string name="legacy_import_failed">Could not import legacy settings</string>
|
<string name="legacy_import_failed">Could not import legacy settings</string>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user