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