From 42e0346e259af117f09b148db0963433f4a9183b Mon Sep 17 00:00:00 2001 From: Tyff Date: Mon, 4 Sep 2023 17:05:55 +1200 Subject: [PATCH] feat: make bundles selectable (#1237) --- .../revanced/manager/di/ViewModelModule.kt | 1 - .../manager/ui/component/bundle/BundleItem.kt | 29 ++++- .../manager/ui/screen/BundlesScreen.kt | 35 ------ .../manager/ui/screen/DashboardScreen.kt | 105 ++++++++++++++++-- .../manager/ui/viewmodel/BundlesViewModel.kt | 33 ------ .../ui/viewmodel/DashboardViewModel.kt | 28 ++++- app/src/main/res/values/strings.xml | 1 + 7 files changed, 146 insertions(+), 86 deletions(-) delete mode 100644 app/src/main/java/app/revanced/manager/ui/screen/BundlesScreen.kt delete mode 100644 app/src/main/java/app/revanced/manager/ui/viewmodel/BundlesViewModel.kt diff --git a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt index 00ba56cf..5f12a2ad 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -12,7 +12,6 @@ val viewModelModule = module { viewModelOf(::AdvancedSettingsViewModel) viewModelOf(::AppSelectorViewModel) viewModelOf(::VersionSelectorViewModel) - viewModelOf(::BundlesViewModel) viewModelOf(::InstallerViewModel) viewModelOf(::UpdateProgressViewModel) viewModelOf(::ManagerUpdateChangelogViewModel) diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt index 04f3f64a..e71e8366 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleItem.kt @@ -1,6 +1,7 @@ package app.revanced.manager.ui.component.bundle -import androidx.compose.foundation.clickable +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -8,6 +9,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.Warning +import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -28,11 +30,16 @@ import app.revanced.manager.domain.bundles.PatchBundleSource import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.propsOrNullFlow import kotlinx.coroutines.flow.map +@OptIn(ExperimentalFoundationApi::class) @Composable fun BundleItem( bundle: PatchBundleSource, onDelete: () -> Unit, - onUpdate: () -> Unit + onUpdate: () -> Unit, + selectable: Boolean, + onSelect: () -> Unit, + isBundleSelected: Boolean, + toggleSelection: (Boolean) -> Unit, ) { var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) } val state by bundle.state.collectAsStateWithLifecycle() @@ -57,9 +64,21 @@ fun BundleItem( modifier = Modifier .height(64.dp) .fillMaxWidth() - .clickable { - viewBundleDialogPage = true - }, + .combinedClickable( + onClick = { + viewBundleDialogPage = true + }, + onLongClick = onSelect, + ), + leadingContent = { + if(selectable) { + Checkbox( + checked = isBundleSelected, + onCheckedChange = toggleSelection, + ) + } + }, + headlineContent = { Text( text = bundle.name, diff --git a/app/src/main/java/app/revanced/manager/ui/screen/BundlesScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/BundlesScreen.kt deleted file mode 100644 index a12b222c..00000000 --- a/app/src/main/java/app/revanced/manager/ui/screen/BundlesScreen.kt +++ /dev/null @@ -1,35 +0,0 @@ -package app.revanced.manager.ui.screen - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import app.revanced.manager.ui.component.bundle.BundleItem -import app.revanced.manager.ui.viewmodel.BundlesViewModel -import org.koin.androidx.compose.getViewModel - -@Composable -fun BundlesScreen( - vm: BundlesViewModel = getViewModel(), -) { - val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList()) - - Column( - modifier = Modifier - .fillMaxSize(), - ) { - sources.forEach { - BundleItem( - bundle = it, - onDelete = { - vm.delete(it) - }, - onUpdate = { - vm.update(it) - } - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt index 33dee138..3ab50324 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -8,14 +8,20 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.outlined.Apps +import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.HelpOutline +import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Source import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -26,8 +32,11 @@ 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.domain.bundles.PatchBundleSource.Companion.isDefault import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.bundle.BundleItem +import app.revanced.manager.ui.component.bundle.BundleTopBar import app.revanced.manager.ui.component.bundle.ImportBundleDialog import app.revanced.manager.ui.viewmodel.DashboardViewModel import app.revanced.manager.util.toast @@ -51,6 +60,8 @@ fun DashboardScreen( onAppClick: (InstalledApp) -> Unit ) { var showImportBundleDialog by rememberSaveable { mutableStateOf(false) } + + val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } } val pages: Array = DashboardPage.values() val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0) val androidContext = LocalContext.current @@ -58,6 +69,10 @@ fun DashboardScreen( val pagerState = rememberPagerState() val composableScope = rememberCoroutineScope() + LaunchedEffect(pagerState.currentPage) { + if (pagerState.currentPage != DashboardPage.BUNDLES.ordinal) vm.cancelSourceSelection() + } + if (showImportBundleDialog) { fun dismiss() { showImportBundleDialog = false @@ -78,21 +93,60 @@ fun DashboardScreen( Scaffold( topBar = { - AppTopBar( - title = stringResource(R.string.app_name), - actions = { - IconButton(onClick = {}) { - Icon(Icons.Outlined.HelpOutline, stringResource(R.string.help)) + if (bundlesSelectable) { + BundleTopBar( + title = stringResource(R.string.bundles_selected, vm.selectedSources.size), + onBackClick = vm::cancelSourceSelection, + onBackIcon = { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.back) + ) + }, + actions = { + IconButton( + onClick = { + vm.selectedSources.forEach { if (!it.isDefault) vm.delete(it) } + vm.cancelSourceSelection() + } + ) { + Icon( + Icons.Outlined.DeleteOutline, + stringResource(R.string.delete) + ) + } + IconButton( + onClick = { + vm.selectedSources.forEach { vm.update(it) } + vm.cancelSourceSelection() + } + ) { + Icon( + Icons.Outlined.Refresh, + stringResource(R.string.refresh) + ) + } } - IconButton(onClick = onSettingsClick) { - Icon(Icons.Outlined.Settings, stringResource(R.string.settings)) + ) + } else { + AppTopBar( + title = stringResource(R.string.app_name), + actions = { + IconButton(onClick = {}) { + Icon(Icons.Outlined.HelpOutline, stringResource(R.string.help)) + } + IconButton(onClick = onSettingsClick) { + Icon(Icons.Outlined.Settings, stringResource(R.string.settings)) + } } - } - ) + ) + } }, floatingActionButton = { FloatingActionButton( onClick = { + vm.cancelSourceSelection() + when (pagerState.currentPage) { DashboardPage.DASHBOARD.ordinal -> { if (availablePatches < 1) { @@ -149,7 +203,38 @@ fun DashboardScreen( } DashboardPage.BUNDLES -> { - BundlesScreen() + + val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList()) + + Column( + modifier = Modifier + .fillMaxSize(), + ) { + sources.forEach { + + BundleItem( + bundle = it, + onDelete = { + vm.delete(it) + }, + onUpdate = { + vm.update(it) + }, + selectable = bundlesSelectable, + onSelect = { + vm.selectedSources.add(it) + }, + isBundleSelected = vm.selectedSources.contains(it), + toggleSelection = { bundleIsNotSelected -> + if (bundleIsNotSelected) { + vm.selectedSources.add(it) + } else { + vm.selectedSources.remove(it) + } + } + ) + } + } } } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/BundlesViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/BundlesViewModel.kt deleted file mode 100644 index 89d2876a..00000000 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/BundlesViewModel.kt +++ /dev/null @@ -1,33 +0,0 @@ -package app.revanced.manager.ui.viewmodel - -import android.app.Application -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.revanced.manager.R -import app.revanced.manager.domain.bundles.PatchBundleSource -import app.revanced.manager.domain.bundles.RemotePatchBundle -import app.revanced.manager.domain.repository.PatchBundleRepository -import app.revanced.manager.util.uiSafe -import kotlinx.coroutines.launch - -class BundlesViewModel( - private val app: Application, - private val patchBundleRepository: PatchBundleRepository -) : ViewModel() { - val sources = patchBundleRepository.sources - - fun delete(bundle: PatchBundleSource) = - viewModelScope.launch { patchBundleRepository.remove(bundle) } - - fun update(bundle: PatchBundleSource) = viewModelScope.launch { - if (bundle !is RemotePatchBundle) return@launch - - uiSafe( - app, - R.string.source_download_fail, - RemotePatchBundle.updateFailMsg - ) { - bundle.update() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt index 362aff44..ba52b82a 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt @@ -3,21 +3,30 @@ package app.revanced.manager.ui.viewmodel import android.app.Application import android.content.ContentResolver import android.net.Uri +import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.domain.bundles.RemotePatchBundle import app.revanced.manager.domain.repository.PatchBundleRepository -import io.ktor.http.Url +import app.revanced.manager.util.uiSafe import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch class DashboardViewModel( - app: Application, + private val app: Application, private val patchBundleRepository: PatchBundleRepository ) : ViewModel() { val availablePatches = patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } } private val contentResolver: ContentResolver = app.contentResolver + val sources = patchBundleRepository.sources + val selectedSources = mutableStateListOf() + fun cancelSourceSelection() { + selectedSources.clear() + } fun createLocalSource(name: String, patchBundle: Uri, integrations: Uri?) = viewModelScope.launch { contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> @@ -32,4 +41,19 @@ class DashboardViewModel( fun createRemoteSource(name: String, apiUrl: String, autoUpdate: Boolean) = viewModelScope.launch { patchBundleRepository.createRemote(name, apiUrl, autoUpdate) } + + fun delete(bundle: PatchBundleSource) = + viewModelScope.launch { patchBundleRepository.remove(bundle) } + + fun update(bundle: PatchBundleSource) = viewModelScope.launch { + if (bundle !is RemotePatchBundle) return@launch + + uiSafe( + app, + R.string.source_download_fail, + RemotePatchBundle.updateFailMsg + ) { + bundle.update() + } + } } \ 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 73c2a734..204d2cc8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -141,6 +141,7 @@ No patches available to view %d Patches available, tap to view Tap on the patches to get more information about them + %s selected Unsupported app Unsupported patches Universal patches