diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt
index a4fbce81..99201949 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt
@@ -1,33 +1,35 @@
package app.revanced.manager.ui.component.bundle
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
+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.material.icons.outlined.Lightbulb
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.Icon
-import androidx.compose.material3.ListItem
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
+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.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.domain.bundles.PatchBundleSource
+import app.revanced.manager.patcher.patch.PatchInfo
+import app.revanced.manager.ui.component.ArrowButton
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
-import app.revanced.manager.ui.component.NotificationCard
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -35,7 +37,8 @@ fun BundlePatchesDialog(
onDismissRequest: () -> Unit,
bundle: PatchBundleSource,
) {
- var informationCardVisible by remember { mutableStateOf(true) }
+ var showAllVersions by rememberSaveable { mutableStateOf(false) }
+ var showOptions by rememberSaveable { mutableStateOf(false) }
val state by bundle.state.collectAsStateWithLifecycle()
Dialog(
@@ -62,44 +65,212 @@ fun BundlePatchesDialog(
LazyColumnWithScrollbar(
modifier = Modifier
.fillMaxWidth()
- .padding(paddingValues)
- .padding(16.dp)
+ .padding(paddingValues),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ contentPadding = PaddingValues(16.dp)
) {
- item {
- AnimatedVisibility(visible = informationCardVisible) {
- NotificationCard(
- icon = Icons.Outlined.Lightbulb,
- text = stringResource(R.string.tap_on_patches),
- onDismiss = { informationCardVisible = false }
- )
- }
- }
-
state.patchBundleOrNull()?.let { bundle ->
- items(bundle.patches.size) { bundleIndex ->
- val patch = bundle.patches[bundleIndex]
- ListItem(
- headlineContent = {
- Text(
- text = patch.name,
- style = MaterialTheme.typography.bodyLarge,
- color = MaterialTheme.colorScheme.onSurface
- )
- },
- supportingContent = {
- patch.description?.let {
- Text(
- text = it,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
+ items(bundle.patches) { patch ->
+ PatchItem(
+ patch,
+ showAllVersions,
+ onExpandVersions = { showAllVersions = !showAllVersions },
+ showOptions,
+ onExpandOptions = { showOptions = !showOptions }
)
- HorizontalDivider()
}
}
}
}
}
}
+
+@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.bundle_view_patches_any_package)}"
+ )
+ PatchInfoChip(
+ text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_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.title,
+ 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"
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 36e2e43e..81522032 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -287,6 +287,7 @@
reorder
More
+ Less
Continue
Dismiss
Do not show this again
@@ -305,6 +306,9 @@
Auto update
Automatically update this bundle when ReVanced starts
View patches
+ Any version
+ Any package
+
About ReVanced Manager
ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance.
An update is available