mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2024-11-10 09:07:47 +01:00
feat: make bundles selectable (#1237)
This commit is contained in:
parent
212db84d0b
commit
42e0346e25
7 changed files with 146 additions and 86 deletions
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue