Skip to content

Commit 8161d9b

Browse files
authored
feat: Prompt bundle selection before APK selection in simple mode (#511)
1 parent 779ebbc commit 8161d9b

3 files changed

Lines changed: 317 additions & 75 deletions

File tree

app/src/main/java/app/morphe/manager/ui/screen/home/HomeDialogs.kt

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import android.annotation.SuppressLint
99
import android.os.Build
1010
import android.widget.Toast
1111
import androidx.compose.animation.AnimatedVisibility
12+
import androidx.compose.foundation.BorderStroke
1213
import androidx.compose.foundation.layout.*
1314
import androidx.compose.foundation.selection.selectable
1415
import androidx.compose.foundation.selection.selectableGroup
@@ -324,6 +325,26 @@ fun HomeDialogs(
324325
}
325326
}
326327

328+
// Simple mode bundle selection dialog - shown when 2+ bundles have patches for the same app
329+
if (homeViewModel.showSimpleBundleSelectDialog) {
330+
val candidates = homeViewModel.simpleBundleSelectCandidates
331+
val bundleRecommendedVersions = homeViewModel.pendingPackageName?.let {
332+
homeViewModel.recommendedBundleVersions[it]
333+
} ?: emptyMap()
334+
SimpleBundleSelectDialog(
335+
candidates = candidates.map { (bundle, patches) ->
336+
SimpleBundleCandidate(
337+
uid = bundle.uid,
338+
displayTitle = homeViewModel.getBundleDisplayName(bundle.uid) ?: bundle.name,
339+
patchCount = patches.size,
340+
recommendedVersion = bundleRecommendedVersions[bundle.uid]?.version
341+
)
342+
},
343+
onSelect = { uid -> homeViewModel.proceedWithSelectedBundle(uid) },
344+
onDismiss = { homeViewModel.dismissSimpleBundleSelectDialog() }
345+
)
346+
}
347+
327348
// Expert Mode Dialog
328349
if (homeViewModel.showExpertModeDialog) {
329350
ExpertModeDialog(
@@ -1990,3 +2011,136 @@ fun MppImportDialog(
19902011
}
19912012
}
19922013
}
2014+
2015+
/** A single selectable bundle entry for [SimpleBundleSelectDialog]. */
2016+
data class SimpleBundleCandidate(
2017+
val uid: Int,
2018+
val displayTitle: String,
2019+
val patchCount: Int,
2020+
val recommendedVersion: String? = null
2021+
)
2022+
2023+
/**
2024+
* Dialog shown in Simple mode when 2+ patch sources have patches for the selected app.
2025+
* Lets the user pick exactly one source to apply.
2026+
*/
2027+
@SuppressLint("LocalContextResourcesRead")
2028+
@Composable
2029+
fun SimpleBundleSelectDialog(
2030+
candidates: List<SimpleBundleCandidate>,
2031+
onSelect: (uid: Int) -> Unit,
2032+
onDismiss: () -> Unit
2033+
) {
2034+
val context = LocalContext.current
2035+
var selected by remember { mutableStateOf(candidates.firstOrNull()?.uid) }
2036+
2037+
MorpheDialog(
2038+
onDismissRequest = onDismiss,
2039+
title = stringResource(R.string.home_simple_bundle_select_title),
2040+
compactPadding = true,
2041+
footer = {
2042+
MorpheDialogButtonRow(
2043+
primaryText = stringResource(R.string.continue_),
2044+
onPrimaryClick = { selected?.let { onSelect(it) } },
2045+
primaryEnabled = selected != null,
2046+
secondaryText = stringResource(android.R.string.cancel),
2047+
onSecondaryClick = onDismiss
2048+
)
2049+
}
2050+
) {
2051+
Column(
2052+
verticalArrangement = Arrangement.spacedBy(8.dp),
2053+
modifier = Modifier
2054+
.fillMaxWidth()
2055+
.selectableGroup()
2056+
) {
2057+
candidates.forEach { candidate ->
2058+
val isSelected = selected == candidate.uid
2059+
val selectedLabel = stringResource(R.string.selected)
2060+
val borderColor = if (isSelected)
2061+
MaterialTheme.colorScheme.primary
2062+
else
2063+
MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
2064+
2065+
Surface(
2066+
modifier = Modifier
2067+
.fillMaxWidth()
2068+
.selectable(
2069+
selected = isSelected,
2070+
onClick = { selected = candidate.uid },
2071+
role = Role.RadioButton
2072+
)
2073+
.semantics {
2074+
contentDescription = buildString {
2075+
append(candidate.displayTitle)
2076+
if (isSelected) append(", $selectedLabel")
2077+
}
2078+
},
2079+
shape = RoundedCornerShape(14.dp),
2080+
color = if (isSelected)
2081+
MaterialTheme.colorScheme.primary.copy(alpha = 0.08f)
2082+
else
2083+
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
2084+
border = BorderStroke(
2085+
width = if (isSelected) 1.5.dp else 1.dp,
2086+
color = borderColor
2087+
),
2088+
tonalElevation = if (isSelected) 0.dp else 1.dp
2089+
) {
2090+
Row(
2091+
modifier = Modifier
2092+
.fillMaxWidth()
2093+
.padding(horizontal = 14.dp, vertical = 12.dp),
2094+
horizontalArrangement = Arrangement.spacedBy(12.dp),
2095+
verticalAlignment = Alignment.CenterVertically
2096+
) {
2097+
// Checkmark - fixed width so text aligns across all rows
2098+
Box(
2099+
modifier = Modifier.size(20.dp),
2100+
contentAlignment = Alignment.Center
2101+
) {
2102+
if (isSelected) {
2103+
Icon(
2104+
imageVector = Icons.Filled.Check,
2105+
contentDescription = null,
2106+
tint = MaterialTheme.colorScheme.primary,
2107+
modifier = Modifier.size(20.dp)
2108+
)
2109+
}
2110+
}
2111+
2112+
Column(modifier = Modifier.weight(1f)) {
2113+
Text(
2114+
text = candidate.displayTitle,
2115+
style = MaterialTheme.typography.bodyLarge,
2116+
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
2117+
color = if (isSelected) MaterialTheme.colorScheme.primary
2118+
else LocalDialogTextColor.current,
2119+
maxLines = 1,
2120+
overflow = TextOverflow.Ellipsis
2121+
)
2122+
val subtitle = buildString {
2123+
append(context.resources.getQuantityString(
2124+
R.plurals.patch_count,
2125+
candidate.patchCount,
2126+
candidate.patchCount
2127+
))
2128+
if (candidate.recommendedVersion != null) {
2129+
append(" · v${candidate.recommendedVersion}")
2130+
}
2131+
}
2132+
Text(
2133+
text = subtitle,
2134+
style = MaterialTheme.typography.bodySmall,
2135+
color = if (isSelected)
2136+
MaterialTheme.colorScheme.primary.copy(alpha = 0.7f)
2137+
else
2138+
LocalDialogSecondaryTextColor.current
2139+
)
2140+
}
2141+
}
2142+
}
2143+
}
2144+
}
2145+
}
2146+
}

0 commit comments

Comments
 (0)