mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-03-13 08:41:57 +08:00
Compare commits
4 Commits
api@1.0.0-
...
feat/repla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27de80b65f | ||
|
|
a7c3a69f3d | ||
|
|
e3558413b7 | ||
|
|
5eea987b82 |
@@ -1,3 +1,10 @@
|
||||
# app [1.26.0-dev.35](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.34...v1.26.0-dev.35) (2026-03-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for writing downloader UI with fragments ([#2998](https://github.com/ReVanced/revanced-manager/issues/2998)) ([7471710](https://github.com/ReVanced/revanced-manager/commit/7471710f287b4febb9546a7d7cd59d654b2754e9))
|
||||
|
||||
# app [1.26.0-dev.34](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.33...v1.26.0-dev.34) (2026-03-10)
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 1.26.0-dev.34
|
||||
version = 1.26.0-dev.35
|
||||
|
||||
@@ -135,10 +135,20 @@ class PatchBundleRepository(
|
||||
}
|
||||
|
||||
private suspend fun loadFromDb(): List<PatchBundleEntity> {
|
||||
val all = dao.all()
|
||||
val all = dao.all().toMutableList()
|
||||
if (all.isEmpty()) {
|
||||
dao.upsert(defaultSource)
|
||||
return listOf(defaultSource)
|
||||
all += defaultSource
|
||||
}
|
||||
|
||||
// Temporary testing sources for dashboard source-management UX.
|
||||
if (all.none { it.uid == testSourceOne.uid }) {
|
||||
dao.upsert(testSourceOne)
|
||||
all += testSourceOne
|
||||
}
|
||||
if (all.none { it.uid == testSourceTwo.uid }) {
|
||||
dao.upsert(testSourceTwo)
|
||||
all += testSourceTwo
|
||||
}
|
||||
|
||||
return all
|
||||
@@ -414,5 +424,21 @@ class PatchBundleRepository(
|
||||
source = Source.API,
|
||||
autoUpdate = false
|
||||
)
|
||||
|
||||
val testSourceOne = PatchBundleEntity(
|
||||
uid = 1001,
|
||||
name = "Stub source A",
|
||||
versionHash = null,
|
||||
source = Source.Remote(io.ktor.http.Url("https://example.invalid/revanced/stub-a.json")),
|
||||
autoUpdate = false
|
||||
)
|
||||
|
||||
val testSourceTwo = PatchBundleEntity(
|
||||
uid = 1002,
|
||||
name = "Stub source B",
|
||||
versionHash = null,
|
||||
source = Source.Remote(io.ktor.http.Url("https://example.invalid/revanced/stub-b.json")),
|
||||
autoUpdate = false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -27,57 +27,23 @@ import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||
import app.revanced.manager.util.transparentListItemColors
|
||||
|
||||
@Composable
|
||||
fun AutoUpdatesDialog(onSubmit: (Boolean, Boolean) -> Unit) {
|
||||
var patchesEnabled by rememberSaveable { mutableStateOf(true) }
|
||||
var managerEnabled by rememberSaveable { mutableStateOf(true) }
|
||||
fun AutoUpdatesDialog(onSubmit: (Boolean) -> Unit) {
|
||||
var enabled by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onSubmit(managerEnabled, patchesEnabled) }) {
|
||||
Text(stringResource(R.string.save))
|
||||
TextButton(onClick = { onSubmit(enabled) }) {
|
||||
Text(stringResource(R.string.confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { onSubmit(false) }) {
|
||||
Text(stringResource(R.string.no))
|
||||
}
|
||||
},
|
||||
icon = { Icon(Icons.Outlined.Update, null) },
|
||||
title = { Text(text = stringResource(R.string.auto_updates_dialog_title)) },
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(text = stringResource(R.string.auto_updates_dialog_description))
|
||||
|
||||
Column {
|
||||
AutoUpdatesItem(
|
||||
headline = R.string.auto_updates_dialog_manager,
|
||||
icon = Icons.Outlined.Update,
|
||||
checked = managerEnabled,
|
||||
onCheckedChange = { managerEnabled = it }
|
||||
)
|
||||
HorizontalDivider()
|
||||
AutoUpdatesItem(
|
||||
headline = R.string.auto_updates_dialog_patches,
|
||||
icon = Icons.Outlined.Source,
|
||||
checked = patchesEnabled,
|
||||
onCheckedChange = { patchesEnabled = it }
|
||||
)
|
||||
}
|
||||
|
||||
Text(text = stringResource(R.string.auto_updates_dialog_note))
|
||||
}
|
||||
}
|
||||
text = { Text(text = stringResource(R.string.auto_updates_dialog_description)) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AutoUpdatesItem(
|
||||
@StringRes headline: Int,
|
||||
icon: ImageVector,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) = ListItem(
|
||||
leadingContent = { Icon(icon, null) },
|
||||
headlineContent = { Text(stringResource(headline)) },
|
||||
trailingContent = { HapticCheckbox(checked = checked, onCheckedChange = null) },
|
||||
modifier = Modifier.clickable { onCheckedChange(!checked) },
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
|
||||
@@ -24,7 +24,8 @@ private inline fun <T> NumberInputDialog(
|
||||
name: String,
|
||||
crossinline onSubmit: (T?) -> Unit,
|
||||
crossinline validator: @DisallowComposableCalls (T) -> Boolean,
|
||||
crossinline toNumberOrNull: @DisallowComposableCalls String.() -> T?
|
||||
crossinline toNumberOrNull: @DisallowComposableCalls String.() -> T?,
|
||||
readOnly: Boolean = false
|
||||
) {
|
||||
var fieldValue by rememberSaveable {
|
||||
mutableStateOf(current?.toString().orEmpty())
|
||||
@@ -43,12 +44,14 @@ private inline fun <T> NumberInputDialog(
|
||||
OutlinedTextField(
|
||||
value = fieldValue,
|
||||
onValueChange = { fieldValue = it },
|
||||
enabled = !readOnly,
|
||||
readOnly = readOnly,
|
||||
placeholder = {
|
||||
Text(stringResource(R.string.dialog_input_placeholder))
|
||||
},
|
||||
isError = validatorFailed,
|
||||
isError = !readOnly && validatorFailed,
|
||||
supportingText = {
|
||||
if (validatorFailed) {
|
||||
if (!readOnly && validatorFailed) {
|
||||
Text(
|
||||
stringResource(R.string.input_dialog_value_invalid),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -59,16 +62,18 @@ private inline fun <T> NumberInputDialog(
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { numberFieldValue?.let(onSubmit) },
|
||||
enabled = numberFieldValue != null && !validatorFailed,
|
||||
) {
|
||||
Text(stringResource(R.string.save))
|
||||
if (!readOnly) {
|
||||
TextButton(
|
||||
onClick = { numberFieldValue?.let(onSubmit) },
|
||||
enabled = numberFieldValue != null && !validatorFailed,
|
||||
) {
|
||||
Text(stringResource(R.string.save))
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { onSubmit(null) }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
Text(stringResource(if (readOnly) R.string.close else R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -79,21 +84,24 @@ fun IntInputDialog(
|
||||
current: Int?,
|
||||
name: String,
|
||||
validator: (Int) -> Boolean = { true },
|
||||
onSubmit: (Int?) -> Unit
|
||||
) = NumberInputDialog(current, name, onSubmit, validator, String::toIntOrNull)
|
||||
onSubmit: (Int?) -> Unit,
|
||||
readOnly: Boolean = false
|
||||
) = NumberInputDialog(current, name, onSubmit, validator, String::toIntOrNull, readOnly)
|
||||
|
||||
@Composable
|
||||
fun LongInputDialog(
|
||||
current: Long?,
|
||||
name: String,
|
||||
validator: (Long) -> Boolean = { true },
|
||||
onSubmit: (Long?) -> Unit
|
||||
) = NumberInputDialog(current, name, onSubmit, validator, String::toLongOrNull)
|
||||
onSubmit: (Long?) -> Unit,
|
||||
readOnly: Boolean = false
|
||||
) = NumberInputDialog(current, name, onSubmit, validator, String::toLongOrNull, readOnly)
|
||||
|
||||
@Composable
|
||||
fun FloatInputDialog(
|
||||
current: Float?,
|
||||
name: String,
|
||||
validator: (Float) -> Boolean = { true },
|
||||
onSubmit: (Float?) -> Unit
|
||||
) = NumberInputDialog(current, name, onSubmit, validator, String::toFloatOrNull)
|
||||
onSubmit: (Float?) -> Unit,
|
||||
readOnly: Boolean = false
|
||||
) = NumberInputDialog(current, name, onSubmit, validator, String::toFloatOrNull, readOnly)
|
||||
|
||||
@@ -18,6 +18,7 @@ fun TextInputDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onConfirm: (String) -> Unit,
|
||||
validator: (String) -> Boolean = String::isNotEmpty,
|
||||
readOnly: Boolean = false
|
||||
) {
|
||||
val (value, setValue) = rememberSaveable(initial) {
|
||||
mutableStateOf(initial)
|
||||
@@ -29,23 +30,30 @@ fun TextInputDialog(
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { onConfirm(value) },
|
||||
enabled = valid
|
||||
) {
|
||||
Text(stringResource(R.string.ok))
|
||||
if (!readOnly) {
|
||||
TextButton(
|
||||
onClick = { onConfirm(value) },
|
||||
enabled = valid
|
||||
) {
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
Text(stringResource(if (readOnly) R.string.close else R.string.cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(title)
|
||||
},
|
||||
text = {
|
||||
TextField(value = value, onValueChange = setValue)
|
||||
TextField(
|
||||
value = value,
|
||||
onValueChange = setValue,
|
||||
enabled = !readOnly,
|
||||
readOnly = readOnly
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -52,8 +52,14 @@ import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||
import app.revanced.manager.ui.component.FullscreenDialog
|
||||
import app.revanced.manager.ui.component.TextInputDialog
|
||||
import app.revanced.manager.ui.component.haptics.HapticSwitch
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.ui.screen.PatchesSelectorScreen
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -82,12 +88,27 @@ fun BundleInformationDialog(
|
||||
}
|
||||
|
||||
if (viewCurrentBundlePatches) {
|
||||
BundlePatchesDialog(
|
||||
src = src,
|
||||
onDismissRequest = {
|
||||
viewCurrentBundlePatches = false
|
||||
val viewModel: PatchesSelectorViewModel = koinViewModel(
|
||||
key = "bundle-patches-${src.uid}",
|
||||
parameters = {
|
||||
parametersOf(
|
||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||
app = SelectedApp.Search(packageName = "", version = null),
|
||||
currentSelection = null,
|
||||
options = emptyMap(),
|
||||
readOnly = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
FullscreenDialog(onDismissRequest = { viewCurrentBundlePatches = false }) {
|
||||
PatchesSelectorScreen(
|
||||
onSave = { _, _ -> },
|
||||
onBackClick = { viewCurrentBundlePatches = false },
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
FullscreenDialog(
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
package app.revanced.manager.ui.component.bundle
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.ui.component.ArrowButton
|
||||
import app.revanced.manager.ui.component.FullscreenDialog
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BundlePatchesDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
src: PatchBundleSource,
|
||||
) {
|
||||
var showAllVersions by rememberSaveable { mutableStateOf(false) }
|
||||
var showOptions by rememberSaveable { mutableStateOf(false) }
|
||||
val patchBundleRepository: PatchBundleRepository = koinInject()
|
||||
val patches by remember(src.uid) {
|
||||
patchBundleRepository.bundleInfoFlow.mapNotNull { it[src.uid]?.patches }
|
||||
}.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
FullscreenDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
BundleTopBar(
|
||||
title = stringResource(R.string.patches),
|
||||
onBackClick = onDismissRequest,
|
||||
backIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(paddingValues),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(16.dp)
|
||||
) {
|
||||
items(patches) { patch ->
|
||||
PatchItem(
|
||||
patch,
|
||||
showAllVersions,
|
||||
onExpandVersions = { showAllVersions = !showAllVersions },
|
||||
showOptions,
|
||||
onExpandOptions = { showOptions = !showOptions }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun PatchItem(
|
||||
patch: PatchInfo,
|
||||
expandVersions: Boolean,
|
||||
onExpandVersions: () -> Unit,
|
||||
expandOptions: Boolean,
|
||||
onExpandOptions: () -> Unit
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (patch.options.isNullOrEmpty()) Modifier else Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable(onClick = onExpandOptions),
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Absolute.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = patch.name,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
if (!patch.options.isNullOrEmpty()) {
|
||||
ArrowButton(expanded = expandOptions, onClick = null)
|
||||
}
|
||||
}
|
||||
patch.description?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (patch.compatiblePackages.isNullOrEmpty()) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
PatchInfoChip(
|
||||
text = "$PACKAGE_ICON ${stringResource(R.string.patches_view_any_package)}"
|
||||
)
|
||||
PatchInfoChip(
|
||||
text = "$VERSION_ICON ${stringResource(R.string.patches_view_any_version)}"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
patch.compatiblePackages.forEach { compatiblePackage ->
|
||||
val packageName = compatiblePackage.packageName
|
||||
val versions = compatiblePackage.versions.orEmpty().reversed()
|
||||
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
PatchInfoChip(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = "$PACKAGE_ICON $packageName"
|
||||
)
|
||||
|
||||
if (versions.isNotEmpty()) {
|
||||
if (expandVersions) {
|
||||
versions.forEach { version ->
|
||||
PatchInfoChip(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = "$VERSION_ICON $version"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
PatchInfoChip(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = "$VERSION_ICON ${versions.first()}"
|
||||
)
|
||||
}
|
||||
if (versions.size > 1) {
|
||||
PatchInfoChip(
|
||||
onClick = onExpandVersions,
|
||||
text = if (expandVersions) stringResource(R.string.less) else "+${versions.size - 1}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!patch.options.isNullOrEmpty()) {
|
||||
AnimatedVisibility(visible = expandOptions) {
|
||||
val options = patch.options
|
||||
|
||||
Column {
|
||||
options.forEachIndexed { i, option ->
|
||||
OutlinedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
disabledContentColor = MaterialTheme.colorScheme.onSurface
|
||||
), shape = when {
|
||||
options.size == 1 -> RoundedCornerShape(8.dp)
|
||||
i == 0 -> RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)
|
||||
i == options.lastIndex -> RoundedCornerShape(
|
||||
bottomStart = 8.dp,
|
||||
bottomEnd = 8.dp
|
||||
)
|
||||
|
||||
else -> RoundedCornerShape(0.dp)
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = option.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = option.description,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PatchInfoChip(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null,
|
||||
text: String
|
||||
) {
|
||||
val shape = RoundedCornerShape(8.0.dp)
|
||||
val cardModifier = if (onClick != null) {
|
||||
Modifier
|
||||
.clip(shape)
|
||||
.clickable(onClick = onClick)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
OutlinedCard(
|
||||
modifier = modifier.then(cardModifier),
|
||||
colors = CardColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
disabledContentColor = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
shape = shape,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.20f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
softWrap = false,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const val PACKAGE_ICON = "\uD83D\uDCE6"
|
||||
const val VERSION_ICON = "\uD83C\uDFAF"
|
||||
@@ -91,7 +91,8 @@ private class OptionEditorScope<T : Any>(
|
||||
val selectionWarningEnabled: Boolean,
|
||||
val showSelectionWarning: () -> Unit,
|
||||
val value: T?,
|
||||
val setValue: (T?) -> Unit
|
||||
val setValue: (T?) -> Unit,
|
||||
val readOnly: Boolean = false
|
||||
) {
|
||||
fun submitDialog(value: T?) {
|
||||
setValue(value)
|
||||
@@ -106,8 +107,13 @@ private class OptionEditorScope<T : Any>(
|
||||
}
|
||||
|
||||
fun clickAction() {
|
||||
checkSafeguard {
|
||||
if (readOnly) {
|
||||
// In readonly mode, bypass the safeguard and open the dialog directly
|
||||
editor.clickAction(this)
|
||||
} else {
|
||||
checkSafeguard {
|
||||
editor.clickAction(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +129,10 @@ private interface OptionEditor<T : Any> {
|
||||
|
||||
@Composable
|
||||
fun ListItemTrailingContent(scope: OptionEditorScope<T>) {
|
||||
IconButton(onClick = { scope.checkSafeguard { clickAction(scope) } }) {
|
||||
IconButton(
|
||||
onClick = { scope.checkSafeguard { clickAction(scope) } },
|
||||
enabled = !scope.readOnly
|
||||
) {
|
||||
Icon(Icons.Outlined.Edit, stringResource(R.string.edit))
|
||||
}
|
||||
}
|
||||
@@ -152,13 +161,14 @@ private inline fun <T : Any> WithOptionEditor(
|
||||
value: T?,
|
||||
noinline setValue: (T?) -> Unit,
|
||||
selectionWarningEnabled: Boolean,
|
||||
readOnly: Boolean = false,
|
||||
crossinline onDismissDialog: @DisallowComposableCalls () -> Unit = {},
|
||||
block: OptionEditorScope<T>.() -> Unit
|
||||
) {
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var showSelectionWarningDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val scope = remember(editor, option, value, setValue, selectionWarningEnabled) {
|
||||
val scope = remember(editor, option, value, setValue, selectionWarningEnabled, readOnly) {
|
||||
OptionEditorScope(
|
||||
editor,
|
||||
option,
|
||||
@@ -170,7 +180,8 @@ private inline fun <T : Any> WithOptionEditor(
|
||||
selectionWarningEnabled,
|
||||
showSelectionWarning = { showSelectionWarningDialog = true },
|
||||
value,
|
||||
setValue
|
||||
setValue,
|
||||
readOnly = readOnly
|
||||
)
|
||||
}
|
||||
|
||||
@@ -189,7 +200,8 @@ fun <T : Any> OptionItem(
|
||||
option: Option<T>,
|
||||
value: T?,
|
||||
setValue: (T?) -> Unit,
|
||||
selectionWarningEnabled: Boolean
|
||||
selectionWarningEnabled: Boolean,
|
||||
readOnly: Boolean = false
|
||||
) {
|
||||
val editor = remember(option.type, option.presets) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -202,7 +214,7 @@ fun <T : Any> OptionItem(
|
||||
else baseOptionEditor
|
||||
}
|
||||
|
||||
WithOptionEditor(editor, option, value, setValue, selectionWarningEnabled) {
|
||||
WithOptionEditor(editor, option, value, setValue, selectionWarningEnabled, readOnly) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(onClick = ::clickAction),
|
||||
headlineContent = { Text(option.name) },
|
||||
@@ -255,12 +267,14 @@ private object StringOptionEditor : OptionEditor<String> {
|
||||
OutlinedTextField(
|
||||
value = fieldValue,
|
||||
onValueChange = { fieldValue = it },
|
||||
enabled = !scope.readOnly,
|
||||
readOnly = scope.readOnly,
|
||||
placeholder = {
|
||||
Text(stringResource(R.string.dialog_input_placeholder))
|
||||
},
|
||||
isError = validatorFailed,
|
||||
isError = !scope.readOnly && validatorFailed,
|
||||
supportingText = {
|
||||
if (validatorFailed) {
|
||||
if (!scope.readOnly && validatorFailed) {
|
||||
Text(
|
||||
stringResource(R.string.input_dialog_value_invalid),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -268,51 +282,55 @@ private object StringOptionEditor : OptionEditor<String> {
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
|
||||
IconButton(
|
||||
onClick = { showDropdownMenu = true }
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.MoreVert,
|
||||
stringResource(R.string.string_option_menu_description)
|
||||
)
|
||||
}
|
||||
trailingIcon = if (!scope.readOnly) {
|
||||
{
|
||||
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
|
||||
IconButton(
|
||||
onClick = { showDropdownMenu = true }
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.MoreVert,
|
||||
stringResource(R.string.string_option_menu_description)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showDropdownMenu,
|
||||
onDismissRequest = { showDropdownMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(Icons.Outlined.Folder, null)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.path_selector))
|
||||
},
|
||||
onClick = {
|
||||
showDropdownMenu = false
|
||||
if (fs.hasStoragePermission()) {
|
||||
showFileDialog = true
|
||||
} else {
|
||||
permissionLauncher.launch(permissionName)
|
||||
DropdownMenu(
|
||||
expanded = showDropdownMenu,
|
||||
onDismissRequest = { showDropdownMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(Icons.Outlined.Folder, null)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.path_selector))
|
||||
},
|
||||
onClick = {
|
||||
showDropdownMenu = false
|
||||
if (fs.hasStoragePermission()) {
|
||||
showFileDialog = true
|
||||
} else {
|
||||
permissionLauncher.launch(permissionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else null
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = !validatorFailed,
|
||||
onClick = { scope.submitDialog(fieldValue) }) {
|
||||
Text(stringResource(R.string.save))
|
||||
if (!scope.readOnly) {
|
||||
TextButton(
|
||||
enabled = !validatorFailed,
|
||||
onClick = { scope.submitDialog(fieldValue) }) {
|
||||
Text(stringResource(R.string.save))
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = scope.dismissDialog) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
Text(stringResource(if (scope.readOnly) R.string.close else R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -325,16 +343,17 @@ private abstract class NumberOptionEditor<T : Number> : OptionEditor<T> {
|
||||
title: String,
|
||||
current: T?,
|
||||
validator: (T?) -> Boolean,
|
||||
onSubmit: (T?) -> Unit
|
||||
onSubmit: (T?) -> Unit,
|
||||
readOnly: Boolean
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Dialog(scope: OptionEditorScope<T>) {
|
||||
NumberDialog(scope.option.name, scope.value, scope.option.validator) {
|
||||
NumberDialog(scope.option.name, scope.value, scope.option.validator, {
|
||||
if (it == null) return@NumberDialog scope.dismissDialog()
|
||||
|
||||
scope.submitDialog(it)
|
||||
}
|
||||
}, scope.readOnly)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,8 +363,9 @@ private object IntOptionEditor : NumberOptionEditor<Int>() {
|
||||
title: String,
|
||||
current: Int?,
|
||||
validator: (Int?) -> Boolean,
|
||||
onSubmit: (Int?) -> Unit
|
||||
) = IntInputDialog(current, title, validator, onSubmit)
|
||||
onSubmit: (Int?) -> Unit,
|
||||
readOnly: Boolean
|
||||
) = IntInputDialog(current, title, validator, onSubmit, readOnly)
|
||||
}
|
||||
|
||||
private object LongOptionEditor : NumberOptionEditor<Long>() {
|
||||
@@ -354,8 +374,9 @@ private object LongOptionEditor : NumberOptionEditor<Long>() {
|
||||
title: String,
|
||||
current: Long?,
|
||||
validator: (Long?) -> Boolean,
|
||||
onSubmit: (Long?) -> Unit
|
||||
) = LongInputDialog(current, title, validator, onSubmit)
|
||||
onSubmit: (Long?) -> Unit,
|
||||
readOnly: Boolean
|
||||
) = LongInputDialog(current, title, validator, onSubmit, readOnly)
|
||||
}
|
||||
|
||||
private object FloatOptionEditor : NumberOptionEditor<Float>() {
|
||||
@@ -364,8 +385,9 @@ private object FloatOptionEditor : NumberOptionEditor<Float>() {
|
||||
title: String,
|
||||
current: Float?,
|
||||
validator: (Float?) -> Boolean,
|
||||
onSubmit: (Float?) -> Unit
|
||||
) = FloatInputDialog(current, title, validator, onSubmit)
|
||||
onSubmit: (Float?) -> Unit,
|
||||
readOnly: Boolean
|
||||
) = FloatInputDialog(current, title, validator, onSubmit, readOnly)
|
||||
}
|
||||
|
||||
private object BooleanOptionEditor : OptionEditor<Boolean> {
|
||||
@@ -381,7 +403,8 @@ private object BooleanOptionEditor : OptionEditor<Boolean> {
|
||||
scope.checkSafeguard {
|
||||
scope.setValue(value)
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !scope.readOnly
|
||||
)
|
||||
}
|
||||
|
||||
@@ -422,6 +445,7 @@ private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<
|
||||
scope.value,
|
||||
scope.setValue,
|
||||
scope.selectionWarningEnabled,
|
||||
readOnly = scope.readOnly,
|
||||
onDismissDialog = scope.dismissDialog
|
||||
) inner@{
|
||||
var hidePresetsDialog by rememberSaveable {
|
||||
@@ -433,26 +457,28 @@ private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<
|
||||
AlertDialogExtended(
|
||||
onDismissRequest = scope.dismissDialog,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (selectedPreset != null) scope.submitDialog(
|
||||
scope.option.presets?.get(
|
||||
selectedPreset
|
||||
if (!scope.readOnly) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (selectedPreset != null) scope.submitDialog(
|
||||
scope.option.presets?.get(
|
||||
selectedPreset
|
||||
)
|
||||
)
|
||||
)
|
||||
else {
|
||||
this@inner.openDialog()
|
||||
// Hide the presets dialog so it doesn't show up in the background.
|
||||
hidePresetsDialog = true
|
||||
else {
|
||||
this@inner.openDialog()
|
||||
// Hide the presets dialog so it doesn't show up in the background.
|
||||
hidePresetsDialog = true
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(if (selectedPreset != null) R.string.save else R.string.continue_))
|
||||
}
|
||||
) {
|
||||
Text(stringResource(if (selectedPreset != null) R.string.save else R.string.continue_))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = scope.dismissDialog) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
Text(stringResource(if (scope.readOnly) R.string.close else R.string.cancel))
|
||||
}
|
||||
},
|
||||
title = { Text(scope.option.name) },
|
||||
@@ -466,13 +492,14 @@ private class PresetOptionEditor<T : Any>(private val innerEditor: OptionEditor<
|
||||
@Composable
|
||||
fun Item(title: String, value: Any?, presetKey: String?) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { selectedPreset = presetKey },
|
||||
modifier = if (!scope.readOnly) Modifier.clickable { selectedPreset = presetKey } else Modifier,
|
||||
headlineContent = { Text(title) },
|
||||
supportingContent = value?.toString()?.let { { Text(it) } },
|
||||
leadingContent = {
|
||||
HapticRadioButton(
|
||||
selected = selectedPreset == presetKey,
|
||||
onClick = { selectedPreset = presetKey }
|
||||
onClick = if (!scope.readOnly) { { selectedPreset = presetKey } } else null,
|
||||
enabled = !scope.readOnly
|
||||
)
|
||||
},
|
||||
colors = transparentListItemColors
|
||||
@@ -602,7 +629,7 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
||||
stringResource(R.string.delete)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
} else if (!scope.readOnly) {
|
||||
IconButton(onClick = items::clear) {
|
||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||
}
|
||||
@@ -611,7 +638,7 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (deleteMode) return@Scaffold
|
||||
if (deleteMode || scope.readOnly) return@Scaffold
|
||||
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.add)) },
|
||||
@@ -643,45 +670,52 @@ private class ListOptionEditor<T : Serializable>(private val elementEditor: Opti
|
||||
elementOption,
|
||||
value = item.value,
|
||||
setValue = { items[index] = item.copy(value = it) },
|
||||
selectionWarningEnabled = scope.selectionWarningEnabled
|
||||
selectionWarningEnabled = scope.selectionWarningEnabled,
|
||||
readOnly = scope.readOnly
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier.combinedClickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = interactionSource,
|
||||
onLongClickLabel = stringResource(R.string.select),
|
||||
onLongClick = {
|
||||
if (!deleteMode) {
|
||||
deletionTargets.add(item.key)
|
||||
deleteMode = true
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (!deleteMode) {
|
||||
clickAction()
|
||||
return@combinedClickable
|
||||
}
|
||||
modifier = if (!scope.readOnly) {
|
||||
Modifier.combinedClickable(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = interactionSource,
|
||||
onLongClickLabel = stringResource(R.string.select),
|
||||
onLongClick = {
|
||||
if (!deleteMode) {
|
||||
deletionTargets.add(item.key)
|
||||
deleteMode = true
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (!deleteMode) {
|
||||
clickAction()
|
||||
return@combinedClickable
|
||||
}
|
||||
|
||||
if (item.key in deletionTargets) {
|
||||
deletionTargets.remove(
|
||||
item.key
|
||||
)
|
||||
deleteMode = deletionTargets.isNotEmpty()
|
||||
} else deletionTargets.add(item.key)
|
||||
},
|
||||
),
|
||||
tonalElevation = if (deleteMode && item.key in deletionTargets) 8.dp else 0.dp,
|
||||
leadingContent = {
|
||||
IconButton(
|
||||
modifier = Modifier.draggableHandle(interactionSource = interactionSource),
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.DragHandle,
|
||||
stringResource(R.string.drag_handle)
|
||||
)
|
||||
}
|
||||
if (item.key in deletionTargets) {
|
||||
deletionTargets.remove(
|
||||
item.key
|
||||
)
|
||||
deleteMode = deletionTargets.isNotEmpty()
|
||||
} else deletionTargets.add(item.key)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Modifier.clickable(onClick = ::clickAction)
|
||||
},
|
||||
tonalElevation = if (deleteMode && item.key in deletionTargets) 8.dp else 0.dp,
|
||||
leadingContent = if (!scope.readOnly) {
|
||||
{
|
||||
IconButton(
|
||||
modifier = Modifier.draggableHandle(interactionSource = interactionSource),
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.DragHandle,
|
||||
stringResource(R.string.drag_handle)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
headlineContent = {
|
||||
if (item.value == null) return@ListItem Text(
|
||||
stringResource(R.string.empty),
|
||||
|
||||
@@ -52,10 +52,14 @@ fun IntegerItem(
|
||||
}
|
||||
|
||||
if (dialogOpen) {
|
||||
IntInputDialog(current = value, name = stringResource(headline)) { new ->
|
||||
dialogOpen = false
|
||||
new?.let(onValueChange)
|
||||
}
|
||||
IntInputDialog(
|
||||
current = value,
|
||||
name = stringResource(headline),
|
||||
onSubmit = { new ->
|
||||
dialogOpen = false
|
||||
new?.let(onValueChange)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SettingsListItem(
|
||||
|
||||
@@ -47,6 +47,7 @@ data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.V
|
||||
val app: SelectedApp,
|
||||
val currentSelection: PatchSelection?,
|
||||
val options: @RawValue Options,
|
||||
val readOnly: Boolean = false,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ import androidx.compose.material.icons.outlined.BugReport
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Notifications
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.Source
|
||||
import androidx.compose.material.icons.outlined.Update
|
||||
@@ -43,10 +43,8 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -60,6 +58,7 @@ 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.Extensions.asRemoteOrNull
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||
@@ -68,15 +67,20 @@ import app.revanced.manager.ui.component.AutoUpdatesDialog
|
||||
import app.revanced.manager.ui.component.AvailableUpdateDialog
|
||||
import app.revanced.manager.ui.component.NotificationCard
|
||||
import app.revanced.manager.ui.component.ConfirmDialog
|
||||
import app.revanced.manager.ui.component.bundle.BundleInformationDialog
|
||||
import app.revanced.manager.ui.component.bundle.BundleTopBar
|
||||
import app.revanced.manager.ui.component.bundle.ImportPatchBundleDialog
|
||||
import app.revanced.manager.ui.component.haptics.HapticFloatingActionButton
|
||||
import app.revanced.manager.ui.component.haptics.HapticTab
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.ui.viewmodel.DashboardViewModel
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||
import app.revanced.manager.util.RequestInstallAppsContract
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
enum class DashboardPage(
|
||||
val titleResId: Int,
|
||||
@@ -99,12 +103,12 @@ fun DashboardScreen(
|
||||
onDownloaderClick: () -> Unit,
|
||||
onAppClick: (String) -> Unit
|
||||
) {
|
||||
var selectedSourceCount by rememberSaveable { mutableIntStateOf(0) }
|
||||
val bundlesSelectable by remember { derivedStateOf { selectedSourceCount > 0 } }
|
||||
val availablePatches by vm.availablePatches.collectAsStateWithLifecycle(0)
|
||||
val showNewDownloaderNotification by vm.newDownloadersAvailable.collectAsStateWithLifecycle(
|
||||
false
|
||||
)
|
||||
val sources by vm.sources.collectAsStateWithLifecycle(emptyList())
|
||||
val patchCounts by vm.patchCounts.collectAsStateWithLifecycle(emptyMap())
|
||||
val showNewDownloaderNotification by vm.newDownloadersAvailable.collectAsStateWithLifecycle(false)
|
||||
val usePrerelease by vm.prefs.usePatchesPrereleases.getAsState()
|
||||
|
||||
val androidContext = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
val composableScope = rememberCoroutineScope()
|
||||
@@ -113,9 +117,24 @@ fun DashboardScreen(
|
||||
initialPageOffsetFraction = 0f
|
||||
) { DashboardPage.entries.size }
|
||||
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
if (pagerState.currentPage != DashboardPage.BUNDLES.ordinal) vm.cancelSourceSelection()
|
||||
}
|
||||
var sourceEditMode by rememberSaveable { mutableStateOf(false) }
|
||||
var selectedSourceUids by rememberSaveable { mutableStateOf(setOf<Int>()) }
|
||||
|
||||
val selectedSourceCount by remember { derivedStateOf { selectedSourceUids.size } }
|
||||
|
||||
val readOnlyPatchesVm: PatchesSelectorViewModel = koinViewModel(
|
||||
key = "dashboard-patches-selector",
|
||||
parameters = {
|
||||
parametersOf(
|
||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||
app = SelectedApp.Search(packageName = "", version = null),
|
||||
currentSelection = null,
|
||||
options = emptyMap(),
|
||||
readOnly = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val firstLaunch by vm.prefs.firstLaunch.getAsState()
|
||||
if (firstLaunch) AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
|
||||
@@ -138,7 +157,6 @@ fun DashboardScreen(
|
||||
var showUpdateDialog by rememberSaveable { mutableStateOf(true) }
|
||||
val showManagerUpdateDialogOnLaunch by vm.prefs.showManagerUpdateDialogOnLaunch.getAsState()
|
||||
val availableUpdate = vm.updatedManagerVersion
|
||||
|
||||
if (showUpdateDialog && showManagerUpdateDialogOnLaunch && availableUpdate != null) {
|
||||
AvailableUpdateDialog(
|
||||
onDismiss = { showUpdateDialog = false },
|
||||
@@ -155,31 +173,53 @@ fun DashboardScreen(
|
||||
if (granted) onAppSelectorClick()
|
||||
}
|
||||
if (showAndroid11Dialog) Android11Dialog(
|
||||
onDismissRequest = {
|
||||
showAndroid11Dialog = false
|
||||
},
|
||||
onContinue = {
|
||||
installAppsPermissionLauncher.launch(androidContext.packageName)
|
||||
}
|
||||
onDismissRequest = { showAndroid11Dialog = false },
|
||||
onContinue = { installAppsPermissionLauncher.launch(androidContext.packageName) }
|
||||
)
|
||||
|
||||
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||
if (showDeleteConfirmationDialog) {
|
||||
ConfirmDialog(
|
||||
onDismiss = { showDeleteConfirmationDialog = false },
|
||||
onConfirm = vm::deleteSources,
|
||||
onConfirm = {
|
||||
vm.deleteSources(selectedSourceUids)
|
||||
selectedSourceUids = emptySet()
|
||||
sourceEditMode = false
|
||||
},
|
||||
title = stringResource(R.string.delete),
|
||||
description = stringResource(R.string.patches_delete_multiple_dialog_description),
|
||||
icon = Icons.Outlined.Delete
|
||||
)
|
||||
}
|
||||
|
||||
var aboutSourceUid by rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
aboutSourceUid?.let { uid ->
|
||||
val src = sources.firstOrNull { it.uid == uid }
|
||||
if (src != null) {
|
||||
BundleInformationDialog(
|
||||
src = src,
|
||||
patchCount = patchCounts[src.uid] ?: 0,
|
||||
onDismissRequest = { aboutSourceUid = null },
|
||||
onDeleteRequest = {
|
||||
vm.deleteSources(setOf(src.uid))
|
||||
aboutSourceUid = null
|
||||
},
|
||||
onUpdate = { vm.updateSource(src.uid) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (bundlesSelectable) {
|
||||
val onPatchesTab = pagerState.currentPage == DashboardPage.BUNDLES.ordinal
|
||||
|
||||
if (onPatchesTab && sourceEditMode) {
|
||||
BundleTopBar(
|
||||
title = stringResource(R.string.patches_selected, selectedSourceCount),
|
||||
onBackClick = vm::cancelSourceSelection,
|
||||
onBackClick = {
|
||||
sourceEditMode = false
|
||||
selectedSourceUids = emptySet()
|
||||
},
|
||||
backIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
@@ -188,22 +228,10 @@ fun DashboardScreen(
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
showDeleteConfirmationDialog = true
|
||||
}
|
||||
onClick = { showDeleteConfirmationDialog = true },
|
||||
enabled = selectedSourceUids.isNotEmpty()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.DeleteOutline,
|
||||
stringResource(R.string.delete)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = vm::updateSources
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Refresh,
|
||||
stringResource(R.string.refresh)
|
||||
)
|
||||
Icon(Icons.Outlined.DeleteOutline, stringResource(R.string.delete))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -212,18 +240,13 @@ fun DashboardScreen(
|
||||
title = stringResource(R.string.app_name),
|
||||
actions = {
|
||||
if (!vm.updatedManagerVersion.isNullOrEmpty()) {
|
||||
IconButton(
|
||||
onClick = onUpdateClick,
|
||||
) {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
Badge(modifier = Modifier.size(6.dp))
|
||||
}
|
||||
) {
|
||||
IconButton(onClick = onUpdateClick) {
|
||||
BadgedBox(badge = { Badge(modifier = Modifier.size(6.dp)) }) {
|
||||
Icon(Icons.Outlined.Update, stringResource(R.string.update))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = onAnnouncementsClick) {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
@@ -232,12 +255,10 @@ fun DashboardScreen(
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Notifications,
|
||||
stringResource(R.string.announcements)
|
||||
)
|
||||
Icon(Icons.Outlined.Notifications, stringResource(R.string.announcements))
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = onSettingsClick) {
|
||||
Icon(Icons.Outlined.Settings, stringResource(R.string.settings))
|
||||
}
|
||||
@@ -247,18 +268,15 @@ fun DashboardScreen(
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
val onPatchesTab = pagerState.currentPage == DashboardPage.BUNDLES.ordinal
|
||||
HapticFloatingActionButton(
|
||||
onClick = {
|
||||
vm.cancelSourceSelection()
|
||||
|
||||
when (pagerState.currentPage) {
|
||||
DashboardPage.DASHBOARD.ordinal -> {
|
||||
if (availablePatches < 1) {
|
||||
androidContext.toast(resources.getString(R.string.no_patch_found))
|
||||
composableScope.launch {
|
||||
pagerState.animateScrollToPage(
|
||||
DashboardPage.BUNDLES.ordinal
|
||||
)
|
||||
pagerState.animateScrollToPage(DashboardPage.BUNDLES.ordinal)
|
||||
}
|
||||
return@HapticFloatingActionButton
|
||||
}
|
||||
@@ -271,11 +289,22 @@ fun DashboardScreen(
|
||||
}
|
||||
|
||||
DashboardPage.BUNDLES.ordinal -> {
|
||||
showAddBundleDialog = true
|
||||
if (sourceEditMode) {
|
||||
showAddBundleDialog = true
|
||||
} else {
|
||||
sourceEditMode = true
|
||||
selectedSourceUids = emptySet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { Icon(Icons.Default.Add, stringResource(R.string.add)) }
|
||||
) {
|
||||
val icon = when {
|
||||
onPatchesTab && !sourceEditMode -> Icons.Outlined.Edit
|
||||
else -> Icons.Default.Add
|
||||
}
|
||||
Icon(icon, stringResource(if (onPatchesTab && !sourceEditMode) R.string.edit else R.string.add))
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(Modifier.padding(paddingValues)) {
|
||||
@@ -286,7 +315,13 @@ fun DashboardScreen(
|
||||
DashboardPage.entries.forEachIndexed { index, page ->
|
||||
HapticTab(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } },
|
||||
onClick = {
|
||||
composableScope.launch { pagerState.animateScrollToPage(index) }
|
||||
if (index != DashboardPage.BUNDLES.ordinal) {
|
||||
sourceEditMode = false
|
||||
selectedSourceUids = emptySet()
|
||||
}
|
||||
},
|
||||
text = { Text(stringResource(page.titleResId)) },
|
||||
icon = { Icon(page.icon, null) },
|
||||
selectedContentColor = MaterialTheme.colorScheme.primary,
|
||||
@@ -371,24 +406,46 @@ fun DashboardScreen(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
pageContent = { index ->
|
||||
when (DashboardPage.entries[index]) {
|
||||
DashboardPage.DASHBOARD -> {
|
||||
InstalledAppsScreen(
|
||||
onAppClick = { onAppClick(it.currentPackageName) }
|
||||
)
|
||||
}
|
||||
DashboardPage.DASHBOARD -> InstalledAppsScreen(
|
||||
onAppClick = { onAppClick(it.currentPackageName) }
|
||||
)
|
||||
|
||||
DashboardPage.BUNDLES -> {
|
||||
BackHandler {
|
||||
if (bundlesSelectable) vm.cancelSourceSelection() else composableScope.launch {
|
||||
pagerState.animateScrollToPage(
|
||||
DashboardPage.DASHBOARD.ordinal
|
||||
)
|
||||
if (sourceEditMode) {
|
||||
sourceEditMode = false
|
||||
selectedSourceUids = emptySet()
|
||||
} else composableScope.launch {
|
||||
pagerState.animateScrollToPage(DashboardPage.DASHBOARD.ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
BundleListScreen(
|
||||
eventsFlow = vm.bundleListEventsFlow,
|
||||
setSelectedSourceCount = { selectedSourceCount = it }
|
||||
PatchesSelectorScreen(
|
||||
onSave = { _, _ -> },
|
||||
onBackClick = {
|
||||
if (sourceEditMode) {
|
||||
sourceEditMode = false
|
||||
selectedSourceUids = emptySet()
|
||||
} else composableScope.launch {
|
||||
pagerState.animateScrollToPage(DashboardPage.DASHBOARD.ordinal)
|
||||
}
|
||||
},
|
||||
viewModel = readOnlyPatchesVm,
|
||||
sourceEditMode = sourceEditMode,
|
||||
selectedSourceUids = selectedSourceUids,
|
||||
onToggleSourceSelection = { uid ->
|
||||
selectedSourceUids = if (uid in selectedSourceUids) {
|
||||
selectedSourceUids - uid
|
||||
} else {
|
||||
selectedSourceUids + uid
|
||||
}
|
||||
},
|
||||
onSourceMenuAction = { uid, action ->
|
||||
when (action) {
|
||||
SourceMenuAction.MORE -> aboutSourceUid = uid
|
||||
SourceMenuAction.REFRESH -> vm.updateSource(uid)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,34 +10,42 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Deselect
|
||||
import androidx.compose.material.icons.outlined.FilterList
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.Restore
|
||||
import androidx.compose.material.icons.outlined.Save
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.SwapHoriz
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -45,6 +53,7 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SecondaryScrollableTabRow
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
@@ -54,6 +63,7 @@ import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -65,12 +75,16 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.patcher.patch.Option
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
@@ -96,14 +110,24 @@ import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
enum class SourceMenuAction { MORE, REFRESH }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, FlowPreview::class)
|
||||
@Composable
|
||||
fun PatchesSelectorScreen(
|
||||
onSave: (PatchSelection?, Options) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: PatchesSelectorViewModel
|
||||
viewModel: PatchesSelectorViewModel,
|
||||
sourceEditMode: Boolean = false,
|
||||
selectedSourceUids: Set<Int> = emptySet(),
|
||||
onToggleSourceSelection: ((Int) -> Unit)? = null,
|
||||
onCurrentSourceChanged: ((Int?) -> Unit)? = null,
|
||||
onSourceMenuAction: ((uid: Int, action: SourceMenuAction) -> Unit)? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val readOnly = viewModel.readOnly
|
||||
val bundles by viewModel.bundlesFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val sourceStates by viewModel.sourceStateByUidFlow.collectAsStateWithLifecycle(initialValue = emptyMap())
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = 0,
|
||||
initialPageOffsetFraction = 0f
|
||||
@@ -117,6 +141,7 @@ fun PatchesSelectorScreen(
|
||||
val (searchExpanded, setSearchExpanded) = rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var selectedReadonlyPackages by rememberSaveable { mutableStateOf(setOf<String>()) }
|
||||
var showBottomSheet by rememberSaveable { mutableStateOf(false) }
|
||||
val showSaveButton by remember {
|
||||
derivedStateOf { viewModel.selectionIsValid(bundles) }
|
||||
@@ -146,6 +171,57 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
}
|
||||
|
||||
fun PatchInfo.matchesReadonlyPackageFilter(): Boolean {
|
||||
if (!readOnly || selectedReadonlyPackages.isEmpty()) return true
|
||||
|
||||
// Universal patches apply to any package and should remain visible.
|
||||
val compatiblePackageList = compatiblePackages ?: return true
|
||||
return compatiblePackageList.any { it.packageName in selectedReadonlyPackages }
|
||||
}
|
||||
|
||||
fun List<PatchInfo>.applyReadonlyPackageFilter() =
|
||||
if (!readOnly) this else filter(PatchInfo::matchesReadonlyPackageFilter)
|
||||
|
||||
LaunchedEffect(bundles, pagerState.currentPage) {
|
||||
onCurrentSourceChanged?.invoke(bundles.getOrNull(pagerState.currentPage)?.uid)
|
||||
}
|
||||
|
||||
fun LazyListScope.sourceStateItem(uid: Int) {
|
||||
when (val state = sourceStates[uid]) {
|
||||
is PatchBundleSource.State.Failed -> {
|
||||
item(key = "source-error-$uid") {
|
||||
ListHeader(title = stringResource(R.string.patches_error))
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.patches_error_description)) },
|
||||
supportingContent = { Text(state.throwable.message.orEmpty()) },
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PatchBundleSource.State.Missing -> {
|
||||
item(key = "source-missing-$uid") {
|
||||
ListHeader(title = stringResource(R.string.patches_missing))
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.patches_not_downloaded)) },
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.emptyPatchesItem(uid: Int) {
|
||||
item(key = "source-empty-$uid") {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.no_patches_selected)) },
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pendingScopeAction?.let { action ->
|
||||
val currentBundle = bundles.getOrNull(pagerState.currentPage) ?: return@let
|
||||
|
||||
@@ -185,87 +261,150 @@ fun PatchesSelectorScreen(
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
data class PackageFilterOption(
|
||||
val packageName: String,
|
||||
val displayName: String,
|
||||
val patchCount: Int,
|
||||
)
|
||||
|
||||
val packageOptions = remember(currentBundle?.uid, bundles) {
|
||||
val pm = context.packageManager
|
||||
|
||||
// Count how many patches explicitly target each package.
|
||||
val packagePatchCounts = mutableMapOf<String, Int>()
|
||||
currentBundle
|
||||
?.patches
|
||||
.orEmpty()
|
||||
.forEach { patch ->
|
||||
patch.compatiblePackages
|
||||
?.map { it.packageName }
|
||||
?.distinct()
|
||||
?.forEach { packageName ->
|
||||
packagePatchCounts[packageName] = (packagePatchCounts[packageName] ?: 0) + 1
|
||||
}
|
||||
}
|
||||
|
||||
packagePatchCounts.map { (packageName, count) ->
|
||||
val appInfo = runCatching { pm.getApplicationInfo(packageName, 0) }.getOrNull()
|
||||
val appLabel = appInfo
|
||||
?.let { runCatching { pm.getApplicationLabel(it).toString() }.getOrNull() }
|
||||
.orEmpty()
|
||||
.ifBlank { packageName }
|
||||
|
||||
PackageFilterOption(
|
||||
packageName = packageName,
|
||||
displayName = appLabel,
|
||||
patchCount = count,
|
||||
)
|
||||
}.sortedWith(
|
||||
compareByDescending<PackageFilterOption> { it.patchCount }
|
||||
.thenBy { it.displayName.lowercase() }
|
||||
)
|
||||
}
|
||||
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
CheckedFilterChip(
|
||||
selected = viewModel.filter and SHOW_INCOMPATIBLE == 0,
|
||||
onClick = { viewModel.toggleFlag(SHOW_INCOMPATIBLE) },
|
||||
label = { Text(stringResource(R.string.this_version)) }
|
||||
)
|
||||
if (!readOnly) {
|
||||
CheckedFilterChip(
|
||||
selected = viewModel.filter and SHOW_INCOMPATIBLE == 0,
|
||||
onClick = { viewModel.toggleFlag(SHOW_INCOMPATIBLE) },
|
||||
label = { Text(stringResource(R.string.this_version)) }
|
||||
)
|
||||
}
|
||||
|
||||
CheckedFilterChip(
|
||||
selected = viewModel.filter and SHOW_UNIVERSAL != 0,
|
||||
onClick = { viewModel.toggleFlag(SHOW_UNIVERSAL) },
|
||||
label = { Text(stringResource(R.string.universal)) },
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.patch_selector_sheet_actions_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
fun guardedAction(action: () -> Unit) {
|
||||
showBottomSheet = false
|
||||
if (viewModel.selectionWarningEnabled) {
|
||||
showSelectionWarning = true
|
||||
} else {
|
||||
action()
|
||||
if (readOnly) {
|
||||
packageOptions.forEach { packageOption ->
|
||||
CheckedFilterChip(
|
||||
selected = packageOption.packageName in selectedReadonlyPackages,
|
||||
onClick = {
|
||||
selectedReadonlyPackages = if (packageOption.packageName in selectedReadonlyPackages) {
|
||||
selectedReadonlyPackages - packageOption.packageName
|
||||
} else {
|
||||
selectedReadonlyPackages + packageOption.packageName
|
||||
}
|
||||
},
|
||||
label = {
|
||||
Text("${packageOption.displayName} (${packageOption.patchCount})")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ActionItem(
|
||||
icon = Icons.Outlined.Restore,
|
||||
text = stringResource(R.string.restore_default_selection),
|
||||
onClick = {
|
||||
guardedAction {
|
||||
executeScopedAction { uid ->
|
||||
viewModel.restoreDefaults(uid)
|
||||
}
|
||||
if (!readOnly) {
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.patch_selector_sheet_actions_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
fun guardedAction(action: () -> Unit) {
|
||||
showBottomSheet = false
|
||||
if (viewModel.selectionWarningEnabled) {
|
||||
showSelectionWarning = true
|
||||
} else {
|
||||
action()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ActionItem(
|
||||
icon = Icons.Outlined.Deselect,
|
||||
text = stringResource(R.string.deselect_all),
|
||||
onClick = {
|
||||
guardedAction {
|
||||
executeScopedAction { uid ->
|
||||
viewModel.deselectAll(bundles, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ActionItem(
|
||||
icon = Icons.Outlined.SwapHoriz,
|
||||
text = stringResource(R.string.invert_selection),
|
||||
onClick = {
|
||||
guardedAction {
|
||||
executeScopedAction { uid ->
|
||||
viewModel.invertSelection(bundles, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (bundles.size > 1 && currentBundle != null) {
|
||||
ActionItem(
|
||||
icon = Icons.Outlined.Deselect,
|
||||
text = stringResource(R.string.deselect_all_except, currentBundle.name),
|
||||
icon = Icons.Outlined.Restore,
|
||||
text = stringResource(R.string.restore_default_selection),
|
||||
onClick = {
|
||||
guardedAction {
|
||||
viewModel.deselectAllExcept(bundles, currentBundle.uid)
|
||||
executeScopedAction { uid ->
|
||||
viewModel.restoreDefaults(uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ActionItem(
|
||||
icon = Icons.Outlined.Deselect,
|
||||
text = stringResource(R.string.deselect_all),
|
||||
onClick = {
|
||||
guardedAction {
|
||||
executeScopedAction { uid ->
|
||||
viewModel.deselectAll(bundles, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ActionItem(
|
||||
icon = Icons.Outlined.SwapHoriz,
|
||||
text = stringResource(R.string.invert_selection),
|
||||
onClick = {
|
||||
guardedAction {
|
||||
executeScopedAction { uid ->
|
||||
viewModel.invertSelection(bundles, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (bundles.size > 1 && currentBundle != null) {
|
||||
ActionItem(
|
||||
icon = Icons.Outlined.Deselect,
|
||||
text = stringResource(R.string.deselect_all_except, currentBundle.name),
|
||||
onClick = {
|
||||
guardedAction {
|
||||
viewModel.deselectAllExcept(bundles, currentBundle.uid)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,7 +432,8 @@ fun PatchesSelectorScreen(
|
||||
values = viewModel.getOptions(bundle, patch),
|
||||
reset = { viewModel.resetOptions(bundle, patch) },
|
||||
set = { key, value -> viewModel.setOption(bundle, patch, key, value) },
|
||||
selectionWarningEnabled = viewModel.selectionWarningEnabled
|
||||
selectionWarningEnabled = viewModel.selectionWarningEnabled,
|
||||
readOnly = readOnly
|
||||
)
|
||||
}
|
||||
|
||||
@@ -308,6 +448,7 @@ fun PatchesSelectorScreen(
|
||||
patches: List<PatchInfo>,
|
||||
visible: Boolean,
|
||||
compatible: Boolean,
|
||||
section: String,
|
||||
header: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
if (patches.isNotEmpty() && visible) {
|
||||
@@ -319,17 +460,14 @@ fun PatchesSelectorScreen(
|
||||
|
||||
items(
|
||||
items = patches,
|
||||
key = { it.name },
|
||||
key = { "$section:${it.name}" },
|
||||
contentType = { 1 }
|
||||
) { patch ->
|
||||
PatchItem(
|
||||
patch = patch,
|
||||
onOptionsDialog = { viewModel.optionsDialog = uid to patch },
|
||||
selected = compatible && viewModel.isSelected(
|
||||
uid,
|
||||
patch
|
||||
),
|
||||
onToggle = {
|
||||
selected = compatible && viewModel.isSelected(uid, patch),
|
||||
onToggle = if (readOnly) ({}) else ({
|
||||
when {
|
||||
// Open incompatible dialog if the patch is not supported
|
||||
!compatible -> viewModel.openIncompatibleDialog(patch)
|
||||
@@ -344,8 +482,11 @@ fun PatchesSelectorScreen(
|
||||
// Toggle the patch otherwise
|
||||
else -> viewModel.togglePatch(uid, patch)
|
||||
}
|
||||
},
|
||||
compatible = compatible
|
||||
}),
|
||||
compatible = compatible,
|
||||
readOnly = readOnly,
|
||||
packageName = viewModel.packageName,
|
||||
showAllPackages = readOnly
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -410,26 +551,40 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
}
|
||||
) {
|
||||
val bundle = bundles[pagerState.currentPage]
|
||||
val bundle = bundles.getOrNull(pagerState.currentPage)
|
||||
if (bundle == null) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.patches_error)) },
|
||||
supportingContent = { Text(stringResource(R.string.patches_error_description)) },
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
return@SearchBar
|
||||
}
|
||||
|
||||
LazyColumnWithScrollbar(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
fun List<PatchInfo>.searched() = filter {
|
||||
it.name.contains(query, true)
|
||||
}
|
||||
sourceStateItem(bundle.uid)
|
||||
fun List<PatchInfo>.searched() =
|
||||
applyReadonlyPackageFilter().filter { it.name.contains(query, true) }
|
||||
|
||||
val compatiblePatches = bundle.compatible.searched()
|
||||
val universalPatches = bundle.universal.searched()
|
||||
val incompatiblePatches = bundle.incompatible.searched()
|
||||
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.compatible.searched(),
|
||||
patches = compatiblePatches,
|
||||
visible = true,
|
||||
compatible = true
|
||||
compatible = true,
|
||||
section = "compatible"
|
||||
)
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.universal.searched(),
|
||||
patches = universalPatches,
|
||||
visible = viewModel.filter and SHOW_UNIVERSAL != 0,
|
||||
compatible = true
|
||||
compatible = true,
|
||||
section = "universal"
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.universal_patches),
|
||||
@@ -438,20 +593,25 @@ fun PatchesSelectorScreen(
|
||||
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.incompatible.searched(),
|
||||
patches = incompatiblePatches,
|
||||
visible = viewModel.filter and SHOW_INCOMPATIBLE != 0,
|
||||
compatible = viewModel.allowIncompatiblePatches
|
||||
compatible = viewModel.allowIncompatiblePatches,
|
||||
section = "incompatible"
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.incompatible_patches),
|
||||
onHelpClick = { showIncompatiblePatchesDialog = true }
|
||||
)
|
||||
}
|
||||
|
||||
if (compatiblePatches.isEmpty() && universalPatches.isEmpty() && incompatiblePatches.isEmpty()) {
|
||||
emptyPatchesItem(bundle.uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (!showSaveButton) return@Scaffold
|
||||
if (readOnly || !showSaveButton) return@Scaffold
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = !searchExpanded,
|
||||
@@ -519,36 +679,48 @@ fun PatchesSelectorScreen(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
|
||||
) {
|
||||
bundles.forEachIndexed { index, bundle ->
|
||||
var showTabMenu by remember { mutableStateOf(false) }
|
||||
|
||||
HapticTab(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = {
|
||||
composableScope.launch {
|
||||
pagerState.animateScrollToPage(
|
||||
index
|
||||
)
|
||||
if (sourceEditMode && readOnly) {
|
||||
onToggleSourceSelection?.invoke(bundle.uid)
|
||||
} else {
|
||||
composableScope.launch {
|
||||
pagerState.animateScrollToPage(index)
|
||||
}
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val selectionState = viewModel.getBundleSelectionState(bundle)
|
||||
val toggleableState = when (selectionState) {
|
||||
true -> ToggleableState.On
|
||||
false -> ToggleableState.Off
|
||||
null -> ToggleableState.Indeterminate
|
||||
}
|
||||
|
||||
HapticTriStateCheckbox(
|
||||
state = toggleableState,
|
||||
onClick = {
|
||||
when {
|
||||
viewModel.selectionWarningEnabled -> showSelectionWarning = true
|
||||
selectionState == false -> viewModel.restoreDefaults(bundle.uid)
|
||||
else -> viewModel.deselectAll(bundles, bundle.uid)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (sourceEditMode && readOnly) {
|
||||
HapticCheckbox(
|
||||
checked = bundle.uid in selectedSourceUids,
|
||||
onCheckedChange = { onToggleSourceSelection?.invoke(bundle.uid) }
|
||||
)
|
||||
} else {
|
||||
val selectionState = viewModel.getBundleSelectionState(bundle)
|
||||
val toggleableState = when (selectionState) {
|
||||
true -> ToggleableState.On
|
||||
false -> ToggleableState.Off
|
||||
null -> ToggleableState.Indeterminate
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
HapticTriStateCheckbox(
|
||||
state = toggleableState,
|
||||
onClick = if (readOnly) ({}) else ({
|
||||
when {
|
||||
viewModel.selectionWarningEnabled -> showSelectionWarning = true
|
||||
selectionState == false -> viewModel.restoreDefaults(bundle.uid)
|
||||
else -> viewModel.deselectAll(bundles, bundle.uid)
|
||||
}
|
||||
}),
|
||||
enabled = !readOnly
|
||||
)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
@@ -561,6 +733,42 @@ fun PatchesSelectorScreen(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
if (onSourceMenuAction != null) {
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = { showTabMenu = true },
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.MoreVert,
|
||||
contentDescription = stringResource(R.string.more),
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showTabMenu,
|
||||
onDismissRequest = { showTabMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.refresh)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Refresh, null) },
|
||||
onClick = {
|
||||
showTabMenu = false
|
||||
onSourceMenuAction(bundle.uid, SourceMenuAction.REFRESH)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.more)) },
|
||||
leadingIcon = { Icon(Icons.Outlined.Info, null) },
|
||||
onClick = {
|
||||
showTabMenu = false
|
||||
onSourceMenuAction(bundle.uid, SourceMenuAction.MORE)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedContentColor = MaterialTheme.colorScheme.primary,
|
||||
@@ -582,17 +790,25 @@ fun PatchesSelectorScreen(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = patchLazyListStates[index]
|
||||
) {
|
||||
sourceStateItem(bundle.uid)
|
||||
|
||||
val compatiblePatches = bundle.compatible.applyReadonlyPackageFilter()
|
||||
val universalPatches = bundle.universal.applyReadonlyPackageFilter()
|
||||
val incompatiblePatches = bundle.incompatible.applyReadonlyPackageFilter()
|
||||
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.compatible,
|
||||
patches = compatiblePatches,
|
||||
visible = true,
|
||||
compatible = true
|
||||
compatible = true,
|
||||
section = "compatible"
|
||||
)
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.universal,
|
||||
patches = universalPatches,
|
||||
visible = viewModel.filter and SHOW_UNIVERSAL != 0,
|
||||
compatible = true
|
||||
compatible = true,
|
||||
section = "universal"
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.universal_patches),
|
||||
@@ -600,15 +816,20 @@ fun PatchesSelectorScreen(
|
||||
}
|
||||
patchList(
|
||||
uid = bundle.uid,
|
||||
patches = bundle.incompatible,
|
||||
patches = incompatiblePatches,
|
||||
visible = viewModel.filter and SHOW_INCOMPATIBLE != 0,
|
||||
compatible = viewModel.allowIncompatiblePatches
|
||||
compatible = viewModel.allowIncompatiblePatches,
|
||||
section = "incompatible"
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.incompatible_patches),
|
||||
onHelpClick = { showIncompatiblePatchesDialog = true }
|
||||
)
|
||||
}
|
||||
|
||||
if (compatiblePatches.isEmpty() && universalPatches.isEmpty() && incompatiblePatches.isEmpty()) {
|
||||
emptyPatchesItem(bundle.uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -630,26 +851,41 @@ private fun UniversalPatchWarningDialog(
|
||||
@Composable
|
||||
private fun PatchItem(
|
||||
patch: PatchInfo,
|
||||
onOptionsDialog: () -> Unit,
|
||||
onOptionsDialog: (() -> Unit)?,
|
||||
selected: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
compatible: Boolean = true
|
||||
compatible: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
packageName: String = "",
|
||||
showAllPackages: Boolean = false
|
||||
) = ListItem(
|
||||
modifier = Modifier
|
||||
.let { if (!compatible) it.alpha(0.5f) else it }
|
||||
.clickable(onClick = onToggle)
|
||||
.let { if (!readOnly) it.clickable(onClick = onToggle) else it }
|
||||
.fillMaxSize(),
|
||||
leadingContent = {
|
||||
HapticCheckbox(
|
||||
checked = selected,
|
||||
onCheckedChange = { onToggle() },
|
||||
enabled = compatible
|
||||
enabled = compatible && !readOnly
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(patch.name) },
|
||||
supportingContent = patch.description?.let { { Text(it) } },
|
||||
supportingContent = {
|
||||
Column {
|
||||
patch.description?.let {
|
||||
Text(it)
|
||||
}
|
||||
// Show compatibility information
|
||||
PatchCompatibilityInfo(
|
||||
patch = patch,
|
||||
packageName = packageName,
|
||||
showAllPackages = showAllPackages
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
if (patch.options?.isNotEmpty() == true) {
|
||||
if (patch.options?.isNotEmpty() == true && onOptionsDialog != null) {
|
||||
IconButton(onClick = onOptionsDialog, enabled = compatible) {
|
||||
Icon(Icons.Outlined.Settings, null)
|
||||
}
|
||||
@@ -658,6 +894,114 @@ private fun PatchItem(
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun PatchCompatibilityInfo(
|
||||
patch: PatchInfo,
|
||||
packageName: String = "",
|
||||
showAllPackages: Boolean = false
|
||||
) {
|
||||
if (patch.compatiblePackages == null) {
|
||||
// Universal patch
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
) {
|
||||
CompatibilityChip(text = "📦 ${stringResource(R.string.patches_view_any_package)}")
|
||||
CompatibilityChip(text = "🎯 ${stringResource(R.string.patches_view_any_version)}")
|
||||
}
|
||||
} else {
|
||||
// Specific package compatibility
|
||||
if (showAllPackages) {
|
||||
// Show all packages (readonly mode)
|
||||
patch.compatiblePackages.forEach { compatiblePackage ->
|
||||
val versions = compatiblePackage.versions.orEmpty().reversed()
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
) {
|
||||
CompatibilityChip(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = "📦 ${compatiblePackage.packageName}"
|
||||
)
|
||||
if (versions.isEmpty()) {
|
||||
CompatibilityChip(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = "🎯 ${stringResource(R.string.patches_view_any_version)}"
|
||||
)
|
||||
} else {
|
||||
versions.forEach { version ->
|
||||
CompatibilityChip(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = "🎯 $version"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Show only selected app's compatible versions (patching mode)
|
||||
val selectedPackageCompat = patch.compatiblePackages.find { it.packageName == packageName }
|
||||
if (selectedPackageCompat != null) {
|
||||
val versions = selectedPackageCompat.versions.orEmpty().reversed()
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
) {
|
||||
if (versions.isEmpty()) {
|
||||
CompatibilityChip(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = "🎯 ${stringResource(R.string.patches_view_any_version)}"
|
||||
)
|
||||
} else {
|
||||
versions.forEach { version ->
|
||||
CompatibilityChip(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = "🎯 $version"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CompatibilityChip(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String
|
||||
) {
|
||||
OutlinedCard(
|
||||
modifier = modifier,
|
||||
colors = CardColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
disabledContentColor = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
shape = RoundedCornerShape(8.0.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.20f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
softWrap = false,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListHeader(
|
||||
title: String,
|
||||
@@ -780,7 +1124,8 @@ private fun OptionsDialog(
|
||||
reset: () -> Unit,
|
||||
set: (String, Any?) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
selectionWarningEnabled: Boolean
|
||||
selectionWarningEnabled: Boolean,
|
||||
readOnly: Boolean
|
||||
) = FullscreenDialog(onDismissRequest = onDismissRequest) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -788,8 +1133,10 @@ private fun OptionsDialog(
|
||||
title = patch.name,
|
||||
onBackClick = onDismissRequest,
|
||||
actions = {
|
||||
IconButton(onClick = reset) {
|
||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||
if (!readOnly) {
|
||||
IconButton(onClick = reset) {
|
||||
Icon(Icons.Outlined.Restore, stringResource(R.string.reset))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -809,10 +1156,9 @@ private fun OptionsDialog(
|
||||
OptionItem(
|
||||
option = option as Option<Any>,
|
||||
value = value,
|
||||
setValue = {
|
||||
set(name, it)
|
||||
},
|
||||
selectionWarningEnabled = selectionWarningEnabled
|
||||
setValue = if (readOnly) ({}) else ({ set(name, it) }),
|
||||
selectionWarningEnabled = selectionWarningEnabled,
|
||||
readOnly = readOnly
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.AnnouncementRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderRepository
|
||||
@@ -23,14 +24,11 @@ import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlin.time.Clock
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -46,6 +44,12 @@ class DashboardViewModel(
|
||||
) : ViewModel() {
|
||||
val availablePatches =
|
||||
patchBundleRepository.bundleInfoFlow.map { it.values.sumOf { bundle -> bundle.patches.size } }
|
||||
val sources = patchBundleRepository.sources
|
||||
val patchCounts = patchBundleRepository.patchCountsFlow
|
||||
val sourceStates = patchBundleRepository.sources.map { sources ->
|
||||
sources.associate { src -> src.uid to src.state }
|
||||
}
|
||||
|
||||
private val contentResolver: ContentResolver = app.contentResolver
|
||||
private val powerManager = app.getSystemService<PowerManager>()!!
|
||||
|
||||
@@ -132,14 +136,14 @@ class DashboardViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch {
|
||||
fun applyAutoUpdatePrefs(enabled: Boolean) = viewModelScope.launch {
|
||||
prefs.firstLaunch.update(false)
|
||||
|
||||
prefs.managerAutoUpdates.update(manager)
|
||||
prefs.managerAutoUpdates.update(enabled)
|
||||
|
||||
if (manager) checkForManagerUpdates()
|
||||
if (enabled) {
|
||||
checkForManagerUpdates()
|
||||
|
||||
if (patches) {
|
||||
with(patchBundleRepository) {
|
||||
sources
|
||||
.first()
|
||||
@@ -160,12 +164,52 @@ class DashboardViewModel(
|
||||
fun updateSources() = sendEvent(BundleListViewModel.Event.UPDATE_SELECTED)
|
||||
fun deleteSources() = sendEvent(BundleListViewModel.Event.DELETE_SELECTED)
|
||||
|
||||
fun refreshPatchSources() = viewModelScope.launch {
|
||||
patchBundleRepository.reload()
|
||||
}
|
||||
|
||||
fun deleteSources(uids: Set<Int>) = viewModelScope.launch {
|
||||
if (uids.isEmpty()) return@launch
|
||||
|
||||
val selected = patchBundleRepository.sources.first().filter { it.uid in uids }
|
||||
patchBundleRepository.remove(*selected.toTypedArray())
|
||||
}
|
||||
|
||||
fun updateSource(uid: Int) = viewModelScope.launch {
|
||||
val source = patchBundleRepository.sources.first()
|
||||
.firstOrNull { it.uid == uid } as? RemotePatchBundle ?: return@launch
|
||||
|
||||
patchBundleRepository.update(source, showToast = true, force = true)
|
||||
}
|
||||
|
||||
fun toggleSourceAutoUpdate(uid: Int, enabled: Boolean) = viewModelScope.launch {
|
||||
val source = patchBundleRepository.sources.first()
|
||||
.firstOrNull { it.uid == uid } as? RemotePatchBundle ?: return@launch
|
||||
|
||||
with(patchBundleRepository) {
|
||||
source.setAutoUpdate(enabled)
|
||||
}
|
||||
patchBundleRepository.reload()
|
||||
}
|
||||
|
||||
fun setDefaultSourcePrerelease(enabled: Boolean) = viewModelScope.launch {
|
||||
prefs.usePatchesPrereleases.update(enabled)
|
||||
|
||||
val source = patchBundleRepository.sources.first()
|
||||
.firstOrNull { it.uid == 0 } as? RemotePatchBundle ?: return@launch
|
||||
|
||||
patchBundleRepository.update(source, force = true)
|
||||
patchBundleRepository.reload()
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
fun createLocalSource(patchBundle: Uri) = viewModelScope.launch {
|
||||
patchBundleRepository.createLocal { contentResolver.openInputStream(patchBundle)!! }
|
||||
patchBundleRepository.reload()
|
||||
}
|
||||
|
||||
fun createRemoteSource(apiUrl: String, autoUpdate: Boolean) = viewModelScope.launch {
|
||||
patchBundleRepository.createRemote(apiUrl, autoUpdate)
|
||||
patchBundleRepository.reload()
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -50,9 +51,11 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
private val app: Application = get()
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
private val prefs: PreferencesManager = get()
|
||||
private val patchBundleRepository: PatchBundleRepository = get()
|
||||
|
||||
private val packageName = input.app.packageName
|
||||
val packageName = input.app.packageName
|
||||
val appVersion = input.app.version
|
||||
val readOnly = input.readOnly
|
||||
|
||||
var selectionWarningEnabled by mutableStateOf(true)
|
||||
private set
|
||||
@@ -61,8 +64,43 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
|
||||
val allowIncompatiblePatches =
|
||||
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
|
||||
val bundlesFlow =
|
||||
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.app.version)
|
||||
val sourceStateByUidFlow = patchBundleRepository.sources.map { sources ->
|
||||
sources.associate { src -> src.uid to src.state }
|
||||
}
|
||||
|
||||
val bundlesFlow = if (readOnly) {
|
||||
combine(
|
||||
patchBundleRepository.sources,
|
||||
patchBundleRepository.bundleInfoFlow
|
||||
) { sources, bundleInfoMap ->
|
||||
sources.map { src ->
|
||||
val bundleInfo = bundleInfoMap[src.uid]
|
||||
val allPatches = bundleInfo?.patches?.distinctBy(PatchInfo::name).orEmpty()
|
||||
val compatible = mutableListOf<PatchInfo>()
|
||||
val incompatible = mutableListOf<PatchInfo>()
|
||||
val universal = mutableListOf<PatchInfo>()
|
||||
|
||||
allPatches.forEach { patch ->
|
||||
when {
|
||||
patch.compatiblePackages == null -> universal
|
||||
else -> compatible
|
||||
}.add(patch)
|
||||
}
|
||||
|
||||
PatchBundleInfo.Scoped(
|
||||
name = bundleInfo?.name ?: src.name,
|
||||
version = bundleInfo?.version ?: src.version,
|
||||
uid = src.uid,
|
||||
patches = allPatches,
|
||||
compatible = compatible,
|
||||
incompatible = incompatible,
|
||||
universal = universal
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
patchBundleRepository.scopedBundleInfoFlow(packageName, input.app.version)
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
|
||||
@@ -77,10 +77,9 @@ Second \"item\" text"</string>
|
||||
<string name="legacy_import_failed">Could not import legacy settings</string>
|
||||
|
||||
<string name="auto_updates_dialog_title">Configure updates</string>
|
||||
<string name="auto_updates_dialog_description">Do you want ReVanced Manager to periodically check for updates for the following components?</string>
|
||||
<string name="auto_updates_dialog_manager">ReVanced Manager</string>
|
||||
<string name="auto_updates_dialog_patches">ReVanced Patches</string>
|
||||
<string name="auto_updates_dialog_note">These settings can be changed later.</string>
|
||||
<string name="auto_updates_dialog_description">Do you want ReVanced Manager to periodically check for updates?
|
||||
|
||||
This can be changed later in the settings.</string>
|
||||
|
||||
<string name="announcements_filter_tag">Filter by tag</string>
|
||||
<string name="announcements_show_archived">Show archived</string>
|
||||
|
||||
Reference in New Issue
Block a user