feat: make bundles selectable (#1237)

This commit is contained in:
Tyff 2023-09-04 17:05:55 +12:00 committed by GitHub
parent 212db84d0b
commit 42e0346e25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 146 additions and 86 deletions

View file

@ -12,7 +12,6 @@ val viewModelModule = module {
viewModelOf(::AdvancedSettingsViewModel) viewModelOf(::AdvancedSettingsViewModel)
viewModelOf(::AppSelectorViewModel) viewModelOf(::AppSelectorViewModel)
viewModelOf(::VersionSelectorViewModel) viewModelOf(::VersionSelectorViewModel)
viewModelOf(::BundlesViewModel)
viewModelOf(::InstallerViewModel) viewModelOf(::InstallerViewModel)
viewModelOf(::UpdateProgressViewModel) viewModelOf(::UpdateProgressViewModel)
viewModelOf(::ManagerUpdateChangelogViewModel) viewModelOf(::ManagerUpdateChangelogViewModel)

View file

@ -1,6 +1,7 @@
package app.revanced.manager.ui.component.bundle 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.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.Icons
import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.icons.outlined.Warning import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material3.Checkbox
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
@ -28,11 +30,16 @@ import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.propsOrNullFlow import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.propsOrNullFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun BundleItem( fun BundleItem(
bundle: PatchBundleSource, bundle: PatchBundleSource,
onDelete: () -> Unit, onDelete: () -> Unit,
onUpdate: () -> Unit onUpdate: () -> Unit,
selectable: Boolean,
onSelect: () -> Unit,
isBundleSelected: Boolean,
toggleSelection: (Boolean) -> Unit,
) { ) {
var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) } var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) }
val state by bundle.state.collectAsStateWithLifecycle() val state by bundle.state.collectAsStateWithLifecycle()
@ -57,9 +64,21 @@ fun BundleItem(
modifier = Modifier modifier = Modifier
.height(64.dp) .height(64.dp)
.fillMaxWidth() .fillMaxWidth()
.clickable { .combinedClickable(
onClick = {
viewBundleDialogPage = true viewBundleDialogPage = true
}, },
onLongClick = onSelect,
),
leadingContent = {
if(selectable) {
Checkbox(
checked = isBundleSelected,
onCheckedChange = toggleSelection,
)
}
},
headlineContent = { headlineContent = {
Text( Text(
text = bundle.name, text = bundle.name,

View file

@ -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)
}
)
}
}
}

View file

@ -8,14 +8,20 @@ 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.Add 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.Apps
import androidx.compose.material.icons.outlined.DeleteOutline
import androidx.compose.material.icons.outlined.HelpOutline 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.Settings
import androidx.compose.material.icons.outlined.Source import androidx.compose.material.icons.outlined.Source
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -26,8 +32,11 @@ 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.domain.bundles.PatchBundleSource.Companion.isDefault
import app.revanced.manager.data.room.apps.installed.InstalledApp import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.ui.component.AppTopBar 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.component.bundle.ImportBundleDialog
import app.revanced.manager.ui.viewmodel.DashboardViewModel import app.revanced.manager.ui.viewmodel.DashboardViewModel
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
@ -51,6 +60,8 @@ fun DashboardScreen(
onAppClick: (InstalledApp) -> Unit onAppClick: (InstalledApp) -> Unit
) { ) {
var showImportBundleDialog by rememberSaveable { mutableStateOf(false) } var showImportBundleDialog by rememberSaveable { mutableStateOf(false) }
val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } }
val pages: Array<DashboardPage> = DashboardPage.values() val pages: Array<DashboardPage> = DashboardPage.values()
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0) val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
val androidContext = LocalContext.current val androidContext = LocalContext.current
@ -58,6 +69,10 @@ fun DashboardScreen(
val pagerState = rememberPagerState() val pagerState = rememberPagerState()
val composableScope = rememberCoroutineScope() val composableScope = rememberCoroutineScope()
LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage != DashboardPage.BUNDLES.ordinal) vm.cancelSourceSelection()
}
if (showImportBundleDialog) { if (showImportBundleDialog) {
fun dismiss() { fun dismiss() {
showImportBundleDialog = false showImportBundleDialog = false
@ -78,6 +93,42 @@ fun DashboardScreen(
Scaffold( Scaffold(
topBar = { topBar = {
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)
)
}
}
)
} else {
AppTopBar( AppTopBar(
title = stringResource(R.string.app_name), title = stringResource(R.string.app_name),
actions = { actions = {
@ -89,10 +140,13 @@ fun DashboardScreen(
} }
} }
) )
}
}, },
floatingActionButton = { floatingActionButton = {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
vm.cancelSourceSelection()
when (pagerState.currentPage) { when (pagerState.currentPage) {
DashboardPage.DASHBOARD.ordinal -> { DashboardPage.DASHBOARD.ordinal -> {
if (availablePatches < 1) { if (availablePatches < 1) {
@ -149,7 +203,38 @@ fun DashboardScreen(
} }
DashboardPage.BUNDLES -> { 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)
}
}
)
}
}
} }
} }
} }

View file

@ -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()
}
}
}

View file

@ -3,21 +3,30 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.ContentResolver import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.domain.repository.PatchBundleRepository
import io.ktor.http.Url import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class DashboardViewModel( class DashboardViewModel(
app: Application, private val app: Application,
private val patchBundleRepository: PatchBundleRepository private val patchBundleRepository: PatchBundleRepository
) : ViewModel() { ) : ViewModel() {
val availablePatches = val availablePatches =
patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } } patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } }
private val contentResolver: ContentResolver = app.contentResolver private val contentResolver: ContentResolver = app.contentResolver
val sources = patchBundleRepository.sources
val selectedSources = mutableStateListOf<PatchBundleSource>()
fun cancelSourceSelection() {
selectedSources.clear()
}
fun createLocalSource(name: String, patchBundle: Uri, integrations: Uri?) = fun createLocalSource(name: String, patchBundle: Uri, integrations: Uri?) =
viewModelScope.launch { viewModelScope.launch {
contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> contentResolver.openInputStream(patchBundle)!!.use { patchesStream ->
@ -32,4 +41,19 @@ class DashboardViewModel(
fun createRemoteSource(name: String, apiUrl: String, autoUpdate: Boolean) = fun createRemoteSource(name: String, apiUrl: String, autoUpdate: Boolean) =
viewModelScope.launch { patchBundleRepository.createRemote(name, apiUrl, autoUpdate) } 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()
}
}
} }

View file

@ -141,6 +141,7 @@
<string name="no_patches">No patches available to view</string> <string name="no_patches">No patches available to view</string>
<string name="patches_available">%d Patches available, tap to view</string> <string name="patches_available">%d Patches available, tap to view</string>
<string name="tap_on_patches">Tap on the patches to get more information about them</string> <string name="tap_on_patches">Tap on the patches to get more information about them</string>
<string name="bundles_selected">%s selected</string>
<string name="unsupported_app">Unsupported app</string> <string name="unsupported_app">Unsupported app</string>
<string name="unsupported_patches">Unsupported patches</string> <string name="unsupported_patches">Unsupported patches</string>
<string name="universal_patches">Universal patches</string> <string name="universal_patches">Universal patches</string>