@@ -9,6 +9,7 @@ import android.annotation.SuppressLint
99import android.os.Build
1010import android.widget.Toast
1111import androidx.compose.animation.AnimatedVisibility
12+ import androidx.compose.foundation.BorderStroke
1213import androidx.compose.foundation.layout.*
1314import androidx.compose.foundation.selection.selectable
1415import 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