From 11a2a140e685b401d874011a9da59a9aac02b23c Mon Sep 17 00:00:00 2001 From: Ax333l Date: Sat, 19 Oct 2024 23:44:44 +0200 Subject: [PATCH] finish UI stuff (i think) --- .../manager/patcher/worker/PatcherWorker.kt | 2 +- .../revanced/manager/ui/model/SelectedApp.kt | 2 +- .../ui/screen/SelectedAppInfoScreen.kt | 298 ++++++++++-------- .../ui/viewmodel/SelectedAppInfoViewModel.kt | 140 +++++++- .../java/app/revanced/manager/util/Util.kt | 6 +- app/src/main/res/values/strings.xml | 8 +- 6 files changed, 314 insertions(+), 142 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 9d00d43f..56aaaa66 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -167,7 +167,7 @@ class PatcherWorker( val inputFile = when (val selectedApp = args.input) { is SelectedApp.Download -> { - val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.app) + val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data) download(plugin, data) } diff --git a/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt b/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt index 3b5e3f40..95aa7c37 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt @@ -13,7 +13,7 @@ sealed interface SelectedApp : Parcelable { data class Download( override val packageName: String, override val version: String, - val app: ParceledDownloaderData + val data: ParceledDownloaderData ) : SelectedApp @Parcelize diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt index 7685a357..0202e344 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt @@ -1,6 +1,7 @@ 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.compose.foundation.clickable import androidx.compose.foundation.layout.PaddingValues @@ -16,34 +17,36 @@ import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R +import app.revanced.manager.data.room.apps.installed.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.AppInfo import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.ColumnWithScrollbar +import app.revanced.manager.ui.component.LoadingIndicator import app.revanced.manager.ui.destination.SelectedAppInfoDestination import app.revanced.manager.ui.model.BundleInfo.Extensions.bundleInfoFlow import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel +import app.revanced.manager.util.EventEffect import app.revanced.manager.util.Options import app.revanced.manager.util.PatchSelection +import app.revanced.manager.util.enabled import app.revanced.manager.util.toast import app.revanced.manager.util.transparentListItemColors import dev.olshevski.navigation.reimagined.AnimatedNavHost @@ -54,6 +57,7 @@ import dev.olshevski.navigation.reimagined.rememberNavController import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SelectedAppInfoScreen( onPatchClick: (SelectedApp, PatchSelection, Options) -> Unit, @@ -80,8 +84,12 @@ fun SelectedAppInfoScreen( } } - var showSourceSelectorDialog by rememberSaveable { - mutableStateOf(false) + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = vm::handlePluginActivityResult + ) + EventEffect(flow = vm.launchActivityFlow) { intent -> + launcher.launch(intent) } val navController = @@ -90,42 +98,120 @@ fun SelectedAppInfoScreen( NavBackHandler(controller = navController) AnimatedNavHost(controller = navController) { destination -> + val error by vm.error.collectAsStateWithLifecycle(null) when (destination) { - is SelectedAppInfoDestination.Main -> SelectedAppInfoScreen( - onPatchClick = patchClick@{ - if (selectedPatchCount == 0) { - context.toast(context.getString(R.string.no_patches_selected)) + is SelectedAppInfoDestination.Main -> Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.app_info), + onBackClick = onBackClick + ) + }, + floatingActionButton = { + if (error != null) return@Scaffold - return@patchClick - } - onPatchClick( - vm.selectedApp, - patches, - vm.getOptionsFiltered(bundles) + ExtendedFloatingActionButton( + text = { Text(stringResource(R.string.patch)) }, + icon = { + Icon( + Icons.Default.AutoFixHigh, + 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 = { - navController.navigate( - SelectedAppInfoDestination.PatchesSelector( - vm.selectedApp, - vm.getCustomPatches( - bundles, - allowIncompatiblePatches - ), - vm.options + } + ) { paddingValues -> + val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList()) + + if (vm.showSourceSelector) { + AppSourceSelectorDialog( + plugins = plugins, + installedApp = vm.installedAppData, + searchApp = SelectedApp.Search( + 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 + ) + ) + } ) - }, - onSourceSelectorClick = { - showSourceSelectorDialog = true - // navController.navigate(SelectedAppInfoDestination.VersionSelector) - }, - onBackClick = onBackClick, - selectedPatchCount = selectedPatchCount, - packageName = packageName, - version = version, - packageInfo = vm.selectedAppInfo, - ) + PageItem( + R.string.apk_source_selector_item, + when (val app = vm.selectedApp) { + is SelectedApp.Search -> stringResource(R.string.apk_source_auto) + is SelectedApp.Installed -> stringResource(R.string.apk_source_installed) + is SelectedApp.Download -> stringResource( + R.string.apk_source_downloader, + plugins.find { it.packageName == app.data.pluginPackageName }?.name + ?: app.data.pluginPackageName + ) + + is SelectedApp.Local -> stringResource(R.string.apk_source_local) + }, + onClick = { + vm.showSourceSelector() + } + ) + error?.let { + Text( + stringResource(it.resourceId), + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(horizontal = 24.dp) + ) + } + } + } is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen( 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 private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) { ListItem( @@ -234,19 +260,21 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () -> } @Composable -private fun AppSourceSelectorDialog(onDismissRequest: () -> Unit) { +private fun AppSourceSelectorDialog( + plugins: List, + installedApp: Pair?, + searchApp: SelectedApp.Search, + activeSearchJob: String?, + hasRoot: Boolean, + onDismissRequest: () -> Unit, + onSelectPlugin: (LoadedDownloaderPlugin) -> Unit, + onSelect: (SelectedApp) -> Unit, +) { + val canSelect = activeSearchJob == null + AlertDialogExtended( onDismissRequest = onDismissRequest, confirmButton = { - TextButton( - onClick = { - - } - ) { - Text("Select") - } - }, - dismissButton = { TextButton(onClick = onDismissRequest) { Text(stringResource(R.string.cancel)) } @@ -254,37 +282,49 @@ private fun AppSourceSelectorDialog(onDismissRequest: () -> Unit) { title = { Text("Select source") }, textHorizontalPadding = PaddingValues(horizontal = 0.dp), text = { - /* - val presets = remember(scope.option.presets) { - scope.option.presets?.entries?.toList().orEmpty() - } - LazyColumn { - @Composable - fun Item(title: String, value: Any?, presetKey: String?) { + item(key = "auto") { + val hasPlugins = plugins.isNotEmpty() ListItem( - modifier = Modifier.clickable { selectedPreset = presetKey }, - headlineContent = { Text(title) }, - supportingContent = value?.toString()?.let { { Text(it) } }, - leadingContent = { - RadioButton( - selected = selectedPreset == presetKey, - onClick = { selectedPreset = presetKey } - ) - }, + modifier = Modifier + .clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) } + .enabled(hasPlugins), + headlineContent = { Text("Auto") }, + supportingContent = { Text(if (hasPlugins) "Use all installed downloaders to find a suitable app." else "No plugins available") }, colors = transparentListItemColors ) } - items(presets, key = { it.key }) { - Item(it.key, it.value, it.key) + installedApp?.let { (app, meta) -> + 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) { - Item(stringResource(R.string.option_preset_custom_value), null, null) + items(plugins, key = { "plugin_${it.packageName}" }) { plugin -> + 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 + ) } } - */ } ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index 6d20be65..7d5a4c85 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -1,46 +1,80 @@ 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.os.Parcelable +import androidx.activity.result.ActivityResult +import androidx.annotation.StringRes import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow 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.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.repository.DownloaderPluginRepository +import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchOptionsRepository import app.revanced.manager.domain.repository.PatchSelectionRepository +import app.revanced.manager.network.downloader.LoadedDownloaderPlugin +import app.revanced.manager.network.downloader.ParceledDownloaderData +import app.revanced.manager.plugin.downloader.GetScope +import app.revanced.manager.plugin.downloader.PluginHostApi +import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.ui.model.BundleInfo import app.revanced.manager.ui.model.BundleInfo.Extensions.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.PatchSelection +import app.revanced.manager.util.toast +import kotlinx.coroutines.CompletableDeferred 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.receiveAsFlow 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) +@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class) class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { + private val app: Application = get() val bundlesRepo: PatchBundleRepository = get() private val bundleRepository: PatchBundleRepository = get() private val selectionRepository: PatchSelectionRepository = get() private val optionsRepository: PatchOptionsRepository = get() + private val pluginsRepository: DownloaderPluginRepository = get() + private val installedAppRepository: InstalledAppRepository = get() + private val rootInstaller: RootInstaller = get() private val pm: PM = get() private val savedStateHandle: SavedStateHandle = 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 + val hasRoot = rootInstaller.hasRootAccess() + var installedAppData: Pair? by mutableStateOf(null) + private set + private var _selectedApp by savedStateHandle.saveable { mutableStateOf(input.app) } @@ -57,6 +91,19 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { init { 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 { @@ -65,9 +112,6 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { viewModelScope.launch { 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) { val bundlePatches = bundleRepository.bundles.first() .mapValues { (_, bundle) -> bundle.patches.associateBy { it.name } } @@ -90,7 +134,7 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { viewModelScope.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 selection.value = SelectionState.Customized(previous) } @@ -98,6 +142,86 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { selection } + var showSourceSelector by mutableStateOf(false) + private set + private var pluginAction: Pair? by mutableStateOf(null) + val activePluginAction get() = pluginAction?.first?.packageName + private var launchedActivity by mutableStateOf?>(null) + private val launchActivityChannel = Channel() + 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()) { + 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 { val info = when (val app = selectedApp) { is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) } @@ -130,8 +254,6 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { this.options = filteredOptions if (!persistConfiguration) return - - val packageName = selectedApp.packageName viewModelScope.launch(Dispatchers.Default) { selection?.let { selectionRepository.updateSelection(packageName, it) } ?: selectionRepository.clearSelection(packageName) @@ -145,6 +267,10 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { val patches: PatchSelection?, ) + enum class Error(@StringRes val resourceId: Int) { + NoPlugins(R.string.downloader_no_plugins_available) + } + private companion object { /** * Returns a copy with all nonexistent options removed. diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt index b1d83d3a..415bdb46 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -24,6 +24,8 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.core.net.toUri import androidx.lifecycle.Lifecycle @@ -258,4 +260,6 @@ fun ScrollState.isScrollingUp(): State { } val LazyListState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value -val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value \ No newline at end of file +val ScrollState.isScrollingUp: Boolean @Composable get() = this.isScrollingUp().value + +fun Modifier.enabled(condition: Boolean) = if (condition) this else alpha(0.5f) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 774ecd50..d5980f3e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,9 +41,11 @@ %d patches selected No patches selected - Change version - %s selected - Automatically selected + Change source + Current: All downloaders + Current: %s + Current: Installed + Current: File Could not import legacy settings