Skip to content

Commit ebacf39

Browse files
minhnq1-aperoclaude
andcommitted
fix(settings): show the install card position instead of naming it
"Dialog | Bottom sheet" asked the user to picture two Material component names, and the only way to actually find out which was which was to pick one and install something. Two drawn thumbnails answer it at a glance, and replace the sentence of prose that had been standing in for them. The tile idiom is the one ThemeScreen already uses for theme presets, so Settings' two visual pickers now look like the same control. Also fixes the hierarchy this section had inverted: InstallMode and SecurityLevel labelled themselves titleSmall/onSurfaceVariant while the SwitchPreference rows beside them use a bodyLarge headline, so the two most consequential controls read as captions and the minor toggles shouted. Both are bodyLarge now. The section is grouped rather than one flat run of ten controls, which also puts auto-confirm next to the external-open modes it belongs to. Headers are hidden while searching, as the divider already was. Show Download Tab moves below that divider — it was left sitting under the last group header, reading as part of a group it has nothing to do with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 606d9e6 commit ebacf39

1 file changed

Lines changed: 147 additions & 46 deletions

File tree

  • app/src/main/java/app/pwhs/universalinstaller/presentation/setting

app/src/main/java/app/pwhs/universalinstaller/presentation/setting/SettingScreen.kt

Lines changed: 147 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package app.pwhs.universalinstaller.presentation.setting
22

3+
import androidx.compose.foundation.BorderStroke
4+
import androidx.compose.foundation.background
35
import androidx.compose.foundation.clickable
6+
import androidx.compose.foundation.selection.selectable
47
import androidx.compose.foundation.layout.Arrangement
8+
import androidx.compose.foundation.layout.Box
59
import androidx.compose.foundation.layout.Column
610
import androidx.compose.foundation.layout.PaddingValues
711
import androidx.compose.foundation.layout.Row
@@ -24,6 +28,7 @@ import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
2428
import androidx.compose.material.icons.rounded.AdminPanelSettings
2529
import androidx.compose.material.icons.rounded.Badge
2630
import androidx.compose.material.icons.rounded.BugReport
31+
import androidx.compose.material.icons.rounded.CheckCircle
2732
import androidx.compose.material.icons.rounded.Close
2833
import androidx.compose.material.icons.rounded.Fingerprint
2934
import androidx.compose.material.icons.automirrored.rounded.HelpOutline
@@ -51,6 +56,7 @@ import androidx.compose.material3.ListItemDefaults
5156
import androidx.compose.material3.MaterialTheme
5257
import androidx.compose.material3.OutlinedTextField
5358
import androidx.compose.material3.Scaffold
59+
import androidx.compose.material3.Surface
5460
import androidx.compose.material3.SegmentedButton
5561
import androidx.compose.material3.SegmentedButtonDefaults
5662
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
@@ -73,13 +79,15 @@ import androidx.compose.ui.focus.FocusRequester
7379
import androidx.compose.ui.graphics.Color
7480
import androidx.compose.ui.input.nestedscroll.nestedScroll
7581
import androidx.compose.ui.res.stringResource
82+
import androidx.compose.ui.semantics.Role
7683
import androidx.compose.ui.text.input.ImeAction
7784
import androidx.compose.ui.text.input.KeyboardType
7885
import coil3.compose.AsyncImage
7986
import coil3.request.ImageRequest
8087
import androidx.compose.ui.graphics.vector.rememberVectorPainter
8188
import androidx.compose.ui.draw.clip
8289
import androidx.compose.ui.platform.LocalContext
90+
import androidx.compose.ui.unit.Dp
8391
import androidx.compose.ui.unit.dp
8492
import app.pwhs.universalinstaller.R
8593
import app.pwhs.universalinstaller.domain.model.ExternalOpenMode
@@ -353,6 +361,9 @@ private fun SettingUi(
353361
// ── Installation Section ─────────────────────
354362
if (matchesQuery(q, installLabels)) item {
355363
SettingsSection(title = stringResource(R.string.setting_section_installation), icon = Icons.Rounded.SettingsApplications) {
364+
// Group headers only while unfiltered: a header whose items were all
365+
// searched away is a label over nothing. Same rule the divider below uses.
366+
if (q.isBlank()) OptionGroupHeader(stringResource(R.string.setting_group_installing))
356367
SearchableItem(q, stringResource(R.string.setting_install_mode_title), "shizuku root default") {
357368
InstallModeSelector(
358369
currentMode = InstallMode.from(uiState.useShizuku, uiState.useRoot),
@@ -389,6 +400,7 @@ private fun SettingUi(
389400
}
390401
}
391402

403+
if (q.isBlank()) OptionGroupHeader(stringResource(R.string.setting_group_after_install))
392404
SearchableItem(q, stringResource(R.string.setting_delete_apk_title)) {
393405
SwitchPreference(
394406
title = stringResource(R.string.setting_delete_apk_title),
@@ -404,6 +416,17 @@ private fun SettingUi(
404416
onCheckedChange = onAutoOpenAfterInstallChanged,
405417
)
406418
}
419+
420+
// Auto-confirm moved up next to the external-open modes: it only applies
421+
// to APKs opened from another app, so it belongs to that question rather
422+
// than sitting among the after-install toggles it used to follow.
423+
if (q.isBlank()) OptionGroupHeader(stringResource(R.string.setting_external_open_title))
424+
SearchableItem(q, stringResource(R.string.setting_external_open_title), stringResource(R.string.setting_external_open_notification_sub)) {
425+
ExternalOpenModeSelector(
426+
current = externalOpenMode,
427+
onChange = onExternalOpenModeChanged,
428+
)
429+
}
407430
SearchableItem(q, stringResource(R.string.setting_auto_confirm_title), stringResource(R.string.setting_auto_confirm_subtitle)) {
408431
SwitchPreference(
409432
title = stringResource(R.string.setting_auto_confirm_title),
@@ -412,18 +435,22 @@ private fun SettingUi(
412435
onCheckedChange = onAutoConfirmExternalInstallChanged,
413436
)
414437
}
438+
439+
if (q.isBlank()) OptionGroupHeader(stringResource(R.string.setting_install_ui_style_title))
415440
SearchableItem(q, stringResource(R.string.setting_install_ui_style_title), stringResource(R.string.setting_install_ui_style_sub)) {
416441
InstallUiStyleSelector(
417442
current = installUiStyle,
418443
onChange = onInstallUiStyleChanged,
419444
)
420445
}
421-
SearchableItem(q, stringResource(R.string.setting_external_open_title), stringResource(R.string.setting_external_open_notification_sub)) {
422-
ExternalOpenModeSelector(
423-
current = externalOpenMode,
424-
onChange = onExternalOpenModeChanged,
425-
)
446+
447+
// Divider only makes sense when the full (unfiltered) list shows.
448+
// Below it sit the two settings that belong to no group above — they
449+
// change what the rest of the system sees, not how a single install runs.
450+
if (q.isBlank()) {
451+
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
426452
}
453+
427454
SearchableItem(q, stringResource(R.string.setting_show_download_tab_title), stringResource(R.string.setting_show_download_tab_subtitle)) {
428455
SwitchPreference(
429456
title = stringResource(R.string.setting_show_download_tab_title),
@@ -432,12 +459,6 @@ private fun SettingUi(
432459
onCheckedChange = onShowDownloadTabChanged,
433460
)
434461
}
435-
436-
// Divider only makes sense when the full (unfiltered) list shows.
437-
if (q.isBlank()) {
438-
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
439-
}
440-
441462
SearchableItem(q, stringResource(R.string.setting_default_installer_title), stringResource(R.string.setting_default_installer_subtitle)) {
442463
SwitchPreference(
443464
title = stringResource(R.string.setting_default_installer_title),
@@ -955,10 +976,12 @@ private fun InstallModeSelector(
955976
.fillMaxWidth()
956977
.padding(horizontal = 16.dp, vertical = 8.dp),
957978
) {
979+
// bodyLarge, not titleSmall: this sits among SwitchPreference rows whose headline is
980+
// bodyLarge. Styling it as a caption made the section's most consequential control read
981+
// as a footnote while the minor toggles shouted.
958982
Text(
959983
text = stringResource(R.string.setting_install_mode_title),
960-
style = MaterialTheme.typography.titleSmall,
961-
color = MaterialTheme.colorScheme.onSurfaceVariant,
984+
style = MaterialTheme.typography.bodyLarge,
962985
modifier = Modifier.padding(bottom = 8.dp),
963986
)
964987
// Root stays tappable whenever libsu shipped (it's only in the row then) — tapping it
@@ -1038,8 +1061,7 @@ private fun SecurityLevelSelector(
10381061
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) {
10391062
Text(
10401063
text = stringResource(R.string.setting_security_level_title),
1041-
style = MaterialTheme.typography.titleSmall,
1042-
color = MaterialTheme.colorScheme.onSurfaceVariant,
1064+
style = MaterialTheme.typography.bodyLarge,
10431065
modifier = Modifier.padding(bottom = 8.dp),
10441066
)
10451067
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
@@ -1086,13 +1108,9 @@ private fun ExternalOpenModeSelector(
10861108
current: ExternalOpenMode,
10871109
onChange: (ExternalOpenMode) -> Unit,
10881110
) {
1111+
// No title of its own: the group header above already reads "When another app opens an APK",
1112+
// and repeating it here was the second of the two headings this control used to carry.
10891113
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) {
1090-
Text(
1091-
text = stringResource(R.string.setting_external_open_title),
1092-
style = MaterialTheme.typography.titleSmall,
1093-
color = MaterialTheme.colorScheme.onSurfaceVariant,
1094-
modifier = Modifier.padding(bottom = 8.dp),
1095-
)
10961114
listOf(
10971115
Triple(ExternalOpenMode.Dialog, R.string.setting_external_open_dialog, R.string.setting_external_open_dialog_sub),
10981116
Triple(ExternalOpenMode.Notification, R.string.setting_external_open_notification, R.string.setting_external_open_notification_sub),
@@ -1125,41 +1143,124 @@ private fun ExternalOpenModeSelector(
11251143

11261144

11271145
/**
1128-
* Dialog or bottom sheet. A segmented control, matching the security-level picker — two options
1129-
* whose labels say everything, unlike the external-open modes which need a line of warning each.
1146+
* Where the install card sits: centered, or anchored at the bottom.
1147+
*
1148+
* Two thumbnails rather than the segmented control this used to be. The setting decides what the
1149+
* install UI *looks like*, and "Dialog | Bottom sheet" asked the user to picture two Material
1150+
* component names — the only way to find out was to pick one and install something. A drawn
1151+
* mock-up answers the question at a glance, and drops the paragraph of prose that stood in for it.
1152+
*
1153+
* The card idiom (fixed-size tile, primary border and a check when selected) is the one
1154+
* [app.pwhs.universalinstaller.presentation.setting.theme.ThemeScreen] already uses for its theme
1155+
* presets, so the two visual pickers in Settings look like the same control.
11301156
*/
1131-
@OptIn(ExperimentalMaterial3Api::class)
11321157
@Composable
11331158
private fun InstallUiStyleSelector(
11341159
current: InstallUiStyle,
11351160
onChange: (InstallUiStyle) -> Unit,
11361161
) {
1137-
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) {
1138-
Text(
1139-
text = stringResource(R.string.setting_install_ui_style_title),
1140-
style = MaterialTheme.typography.titleSmall,
1141-
color = MaterialTheme.colorScheme.onSurfaceVariant,
1142-
modifier = Modifier.padding(bottom = 8.dp),
1143-
)
1144-
val options = listOf(
1162+
Row(
1163+
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
1164+
horizontalArrangement = Arrangement.spacedBy(12.dp),
1165+
) {
1166+
listOf(
11451167
InstallUiStyle.Dialog to R.string.setting_install_ui_style_dialog,
11461168
InstallUiStyle.Sheet to R.string.setting_install_ui_style_sheet,
1147-
)
1148-
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
1149-
options.forEachIndexed { index, (style, labelRes) ->
1150-
SegmentedButton(
1151-
selected = style == current,
1152-
onClick = { if (style != current) onChange(style) },
1153-
shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size),
1154-
label = { Text(stringResource(labelRes)) },
1169+
).forEach { (style, labelRes) ->
1170+
InstallUiStyleCard(
1171+
style = style,
1172+
label = stringResource(labelRes),
1173+
selected = style == current,
1174+
onClick = { if (style != current) onChange(style) },
1175+
)
1176+
}
1177+
}
1178+
}
1179+
1180+
/**
1181+
* One thumbnail: a phone-shaped frame with a miniature install card drawn where the real one lands.
1182+
*/
1183+
@Composable
1184+
private fun InstallUiStyleCard(
1185+
style: InstallUiStyle,
1186+
label: String,
1187+
selected: Boolean,
1188+
onClick: () -> Unit,
1189+
) {
1190+
val accent = MaterialTheme.colorScheme.primary
1191+
Column(horizontalAlignment = Alignment.CenterHorizontally) {
1192+
Surface(
1193+
// selectable(), not clickable(): this is one of a set of mutually exclusive choices,
1194+
// and TalkBack should announce it as such rather than as a plain button.
1195+
modifier = Modifier
1196+
.size(width = 92.dp, height = 124.dp)
1197+
.selectable(selected = selected, role = Role.RadioButton, onClick = onClick),
1198+
shape = RoundedCornerShape(12.dp),
1199+
color = MaterialTheme.colorScheme.surfaceVariant,
1200+
border = BorderStroke(
1201+
width = if (selected) 2.dp else 1.dp,
1202+
color = if (selected) accent else MaterialTheme.colorScheme.outlineVariant,
1203+
),
1204+
) {
1205+
Box(
1206+
modifier = Modifier.fillMaxSize().padding(10.dp),
1207+
// The whole point of the thumbnail: the mini card sits where the real one will.
1208+
contentAlignment = if (style == InstallUiStyle.Sheet) {
1209+
Alignment.BottomCenter
1210+
} else {
1211+
Alignment.Center
1212+
},
1213+
) {
1214+
Column(
1215+
modifier = Modifier
1216+
// A sheet spans the width; a centered card is inset on both sides.
1217+
.fillMaxWidth(if (style == InstallUiStyle.Sheet) 1f else 0.82f)
1218+
.clip(RoundedCornerShape(6.dp))
1219+
.background(MaterialTheme.colorScheme.surface)
1220+
.padding(horizontal = 6.dp, vertical = 7.dp),
1221+
verticalArrangement = Arrangement.spacedBy(4.dp),
1222+
) {
1223+
MiniCardLine(0.7f, MaterialTheme.colorScheme.onSurface.copy(alpha = 0.55f))
1224+
MiniCardLine(1f, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f))
1225+
Spacer(Modifier.height(1.dp))
1226+
// Stands in for the Install button, in the accent color so the mock-up reads
1227+
// as this app's card rather than a generic grey box.
1228+
MiniCardLine(0.45f, accent, height = 6.dp)
1229+
}
1230+
}
1231+
}
1232+
Spacer(Modifier.height(6.dp))
1233+
Row(verticalAlignment = Alignment.CenterVertically) {
1234+
if (selected) {
1235+
Icon(
1236+
imageVector = Icons.Rounded.CheckCircle,
1237+
contentDescription = null,
1238+
tint = accent,
1239+
modifier = Modifier.size(14.dp),
11551240
)
1241+
Spacer(Modifier.width(4.dp))
11561242
}
1243+
Text(
1244+
text = label,
1245+
style = MaterialTheme.typography.bodyMedium,
1246+
color = if (selected) accent else MaterialTheme.colorScheme.onSurfaceVariant,
1247+
)
11571248
}
1158-
Spacer(Modifier.height(8.dp))
1159-
Text(
1160-
text = stringResource(R.string.setting_install_ui_style_sub),
1161-
style = MaterialTheme.typography.bodySmall,
1162-
color = MaterialTheme.colorScheme.onSurfaceVariant,
1163-
)
11641249
}
11651250
}
1251+
1252+
/** A single bar inside the thumbnail, standing in for a line of text or a button. */
1253+
@Composable
1254+
private fun MiniCardLine(
1255+
widthFraction: Float,
1256+
color: Color,
1257+
height: Dp = 4.dp,
1258+
) {
1259+
Box(
1260+
modifier = Modifier
1261+
.fillMaxWidth(widthFraction)
1262+
.height(height)
1263+
.clip(RoundedCornerShape(2.dp))
1264+
.background(color),
1265+
)
1266+
}

0 commit comments

Comments
 (0)