Skip to content

Commit ced5cf1

Browse files
feat(ui): hide mode-irrelevant fields on desktop and Android
1 parent a216fb2 commit ced5cf1

2 files changed

Lines changed: 163 additions & 117 deletions

File tree

android/app/src/main/java/com/therealaleph/mhrv/ui/HomeScreen.kt

Lines changed: 93 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -355,33 +355,37 @@ fun HomeScreen(
355355
Spacer(Modifier.height(4.dp))
356356

357357
val appsScriptEnabled = cfg.mode == Mode.APPS_SCRIPT || cfg.mode == Mode.FULL
358-
// Wrapped in a collapsible so a long ID list (10+ deployments
359-
// is normal in full-tunnel rotations) doesn't dominate the
360-
// screen once it's set up. Starts expanded for first-run users
361-
// (no IDs/key yet) so the form is immediately discoverable.
362-
CollapsibleSection(
363-
title = stringResource(R.string.sec_apps_script_relay),
364-
initiallyExpanded = appsScriptEnabled &&
365-
(cfg.appsScriptUrls.isEmpty() || cfg.authKey.isBlank()),
366-
) {
367-
DeploymentIdsField(
368-
urls = cfg.appsScriptUrls,
369-
onChange = { persist(cfg.copy(appsScriptUrls = it)) },
370-
enabled = appsScriptEnabled,
371-
)
358+
// Apps Script section only renders for the modes that
359+
// actually use Apps Script. google_only / google_drive have
360+
// no Deployment ID or Auth key concept — showing them
361+
// greyed-out (the previous behavior) just confused
362+
// first-time users. Wrapped in a collapsible so a long ID
363+
// list (10+ deployments is normal in full-tunnel rotations)
364+
// doesn't dominate the screen once set up.
365+
if (appsScriptEnabled) {
366+
CollapsibleSection(
367+
title = stringResource(R.string.sec_apps_script_relay),
368+
initiallyExpanded = cfg.appsScriptUrls.isEmpty() || cfg.authKey.isBlank(),
369+
) {
370+
DeploymentIdsField(
371+
urls = cfg.appsScriptUrls,
372+
onChange = { persist(cfg.copy(appsScriptUrls = it)) },
373+
enabled = true,
374+
)
372375

373-
OutlinedTextField(
374-
value = cfg.authKey,
375-
onValueChange = { persist(cfg.copy(authKey = it)) },
376-
label = { Text(stringResource(R.string.field_auth_key)) },
377-
singleLine = true,
378-
enabled = appsScriptEnabled,
379-
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
380-
modifier = Modifier.fillMaxWidth(),
381-
supportingText = {
382-
Text(stringResource(R.string.help_auth_key))
383-
},
384-
)
376+
OutlinedTextField(
377+
value = cfg.authKey,
378+
onValueChange = { persist(cfg.copy(authKey = it)) },
379+
label = { Text(stringResource(R.string.field_auth_key)) },
380+
singleLine = true,
381+
enabled = true,
382+
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
383+
modifier = Modifier.fillMaxWidth(),
384+
supportingText = {
385+
Text(stringResource(R.string.help_auth_key))
386+
},
387+
)
388+
}
385389
}
386390

387391
// ── Google Drive section ──────────────────────────────────────
@@ -494,7 +498,11 @@ fun HomeScreen(
494498
)
495499
}
496500

497-
// Advanced settings: collapsed by default.
501+
// Advanced settings: collapsed by default. The block
502+
// contains a mix of always-applicable knobs (verify_ssl,
503+
// log_level) and Apps-Script-only knobs (parallel_relay,
504+
// upstream_socks5); the inner composable hides the latter
505+
// when the current mode doesn't use Apps Script.
498506
CollapsibleSection(title = stringResource(R.string.sec_advanced)) {
499507
AdvancedSettings(
500508
cfg = cfg,
@@ -506,12 +514,17 @@ fun HomeScreen(
506514
// Secondary action — FilledTonalButton signals "helper" against
507515
// the primary Connect/Disconnect button at the top. Kept down
508516
// here because cert install is a one-time setup step; daily
509-
// users never tap it again.
510-
FilledTonalButton(
511-
onClick = { showInstallDialog = true },
512-
modifier = Modifier.fillMaxWidth(),
513-
) {
514-
Text(stringResource(R.string.btn_install_mitm))
517+
// users never tap it again. Only meaningful when MITM is
518+
// active: apps_script does the TLS interception, full owns
519+
// a tunnel-node + cert. google_only and google_drive do
520+
// not MITM so hiding the button keeps the flow honest.
521+
if (cfg.mode == Mode.APPS_SCRIPT || cfg.mode == Mode.FULL) {
522+
FilledTonalButton(
523+
onClick = { showInstallDialog = true },
524+
modifier = Modifier.fillMaxWidth(),
525+
) {
526+
Text(stringResource(R.string.btn_install_mitm))
527+
}
515528
}
516529

517530
// "Usage today (estimated)" — visible only while a proxy is
@@ -532,12 +545,18 @@ fun HomeScreen(
532545
// Wrapped in a collapsible so the big prose block doesn't
533546
// dominate the form after the user has learned the flow.
534547
// Starts expanded once for a fresh install so the first-run
535-
// instructions are immediately visible.
536-
CollapsibleSection(
537-
title = stringResource(R.string.sec_how_to_use),
538-
initiallyExpanded = cfg.appsScriptUrls.isEmpty() || cfg.authKey.isBlank(),
539-
) {
540-
HowToUseBody(cfg.listenPort)
548+
// instructions are immediately visible. The body is
549+
// Apps-Script-flavoured (Deployment IDs, MITM cert, Code.gs)
550+
// so it's only relevant in apps_script / full — Drive and
551+
// google_only have their own onboarding inside their
552+
// respective sections.
553+
if (cfg.mode == Mode.APPS_SCRIPT || cfg.mode == Mode.FULL) {
554+
CollapsibleSection(
555+
title = stringResource(R.string.sec_how_to_use),
556+
initiallyExpanded = cfg.appsScriptUrls.isEmpty() || cfg.authKey.isBlank(),
557+
) {
558+
HowToUseBody(cfg.listenPort)
559+
}
541560
}
542561
}
543562
}
@@ -1896,6 +1915,11 @@ private fun AdvancedSettings(
18961915
cfg: MhrvConfig,
18971916
onChange: (MhrvConfig) -> Unit,
18981917
) {
1918+
// parallel_relay and upstream_socks5 only have an effect on the
1919+
// Apps Script relay path; they're no-ops in google_only and
1920+
// google_drive. Hide them in those modes so users don't think
1921+
// they're tunable knobs that just don't take effect.
1922+
val appsScriptRelevant = cfg.mode == Mode.APPS_SCRIPT || cfg.mode == Mode.FULL
18991923
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
19001924
// verify_ssl
19011925
Row(
@@ -1947,36 +1971,38 @@ private fun AdvancedSettings(
19471971
}
19481972
}
19491973

1950-
// parallel_relay slider
1951-
Column {
1952-
Text(
1953-
stringResource(R.string.adv_parallel_relay, cfg.parallelRelay),
1954-
style = MaterialTheme.typography.bodyMedium,
1955-
)
1956-
Slider(
1957-
value = cfg.parallelRelay.toFloat(),
1958-
onValueChange = { onChange(cfg.copy(parallelRelay = it.toInt().coerceIn(1, 5))) },
1959-
valueRange = 1f..5f,
1960-
steps = 3, // yields 1,2,3,4,5 positions
1961-
)
1962-
Text(
1963-
stringResource(R.string.adv_parallel_relay_help),
1964-
style = MaterialTheme.typography.labelSmall,
1965-
color = MaterialTheme.colorScheme.onSurfaceVariant,
1974+
if (appsScriptRelevant) {
1975+
// parallel_relay slider
1976+
Column {
1977+
Text(
1978+
stringResource(R.string.adv_parallel_relay, cfg.parallelRelay),
1979+
style = MaterialTheme.typography.bodyMedium,
1980+
)
1981+
Slider(
1982+
value = cfg.parallelRelay.toFloat(),
1983+
onValueChange = { onChange(cfg.copy(parallelRelay = it.toInt().coerceIn(1, 5))) },
1984+
valueRange = 1f..5f,
1985+
steps = 3, // yields 1,2,3,4,5 positions
1986+
)
1987+
Text(
1988+
stringResource(R.string.adv_parallel_relay_help),
1989+
style = MaterialTheme.typography.labelSmall,
1990+
color = MaterialTheme.colorScheme.onSurfaceVariant,
1991+
)
1992+
}
1993+
1994+
OutlinedTextField(
1995+
value = cfg.upstreamSocks5,
1996+
onValueChange = { onChange(cfg.copy(upstreamSocks5 = it)) },
1997+
label = { Text(stringResource(R.string.adv_upstream_socks5)) },
1998+
placeholder = { Text("host:port") },
1999+
singleLine = true,
2000+
modifier = Modifier.fillMaxWidth(),
2001+
supportingText = {
2002+
Text(stringResource(R.string.adv_upstream_socks5_help))
2003+
},
19662004
)
19672005
}
1968-
1969-
OutlinedTextField(
1970-
value = cfg.upstreamSocks5,
1971-
onValueChange = { onChange(cfg.copy(upstreamSocks5 = it)) },
1972-
label = { Text(stringResource(R.string.adv_upstream_socks5)) },
1973-
placeholder = { Text("host:port") },
1974-
singleLine = true,
1975-
modifier = Modifier.fillMaxWidth(),
1976-
supportingText = {
1977-
Text(stringResource(R.string.adv_upstream_socks5_help))
1978-
},
1979-
)
19802006
}
19812007
}
19822008

src/bin/ui.rs

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -983,10 +983,15 @@ impl eframe::App for App {
983983

984984
let google_only = self.form.mode == "google_only";
985985
let google_drive = self.form.mode == "google_drive";
986+
// Apps Script relay only applies to apps_script + full. Hide
987+
// the section entirely in google_only / google_drive — those
988+
// modes have no Deployment ID or Auth key concept and the
989+
// greyed-out fields were just confusing first-time users.
990+
let needs_apps_script = !google_only && !google_drive;
986991

987992
// ── Section: Apps Script relay ────────────────────────────────
988-
section(ui, "Apps Script relay", |ui| {
989-
ui.add_enabled_ui(!google_only && !google_drive, |ui| {
993+
if needs_apps_script {
994+
section(ui, "Apps Script relay", |ui| {
990995
form_row(ui, "Deployment IDs", Some(
991996
"One deployment ID per line. Proxy round-robins between them and sidelines \
992997
any ID that hits its daily quota for 10 minutes before retrying."
@@ -1022,7 +1027,7 @@ impl eframe::App for App {
10221027
.desired_width(f32::INFINITY));
10231028
});
10241029
});
1025-
});
1030+
}
10261031

10271032
// ── Section: Network ──────────────────────────────────────────
10281033
section(ui, "Network", |ui| {
@@ -1082,9 +1087,16 @@ impl eframe::App for App {
10821087
egui::Label::new(egui::RichText::new("Ports")
10831088
.color(egui::Color32::from_gray(200))),
10841089
);
1085-
ui.label(egui::RichText::new("HTTP").small());
1086-
ui.add(egui::TextEdit::singleline(&mut self.form.listen_port).desired_width(70.0));
1087-
ui.add_space(10.0);
1090+
// google_drive doesn't bind the HTTP port at all
1091+
// (the Drive client is SOCKS5-only). Hiding it
1092+
// avoids implying it does something. The form still
1093+
// tracks the value so a switch back to apps_script
1094+
// recovers the user's previous setting.
1095+
if !google_drive {
1096+
ui.label(egui::RichText::new("HTTP").small());
1097+
ui.add(egui::TextEdit::singleline(&mut self.form.listen_port).desired_width(70.0));
1098+
ui.add_space(10.0);
1099+
}
10881100
ui.label(egui::RichText::new("SOCKS5").small());
10891101
ui.add(egui::TextEdit::singleline(&mut self.form.socks5_port).desired_width(70.0));
10901102
});
@@ -1212,26 +1224,32 @@ impl eframe::App for App {
12121224
.rounding(6.0)
12131225
.inner_margin(egui::Margin::same(10.0));
12141226
frame.show(ui, |ui| {
1215-
form_row(ui, "Upstream SOCKS5", Some(
1216-
"Optional. host:port of a local xray / v2ray / sing-box SOCKS5 inbound. \
1217-
When set, non-HTTP / raw-TCP traffic (Telegram MTProto, IMAP, SSH, …) \
1218-
is chained through it instead of direct. HTTP/HTTPS still go through \
1219-
the Apps Script relay."
1220-
), |ui| {
1221-
ui.add(egui::TextEdit::singleline(&mut self.form.upstream_socks5)
1222-
.hint_text("empty = direct; 127.0.0.1:50529 for local xray")
1223-
.desired_width(f32::INFINITY));
1224-
});
1227+
// Apps-Script-specific tweaks. Drive mode bypasses
1228+
// the relay entirely and google_only doesn't relay
1229+
// either, so these knobs are no-ops there — hide
1230+
// rather than just disable.
1231+
if needs_apps_script {
1232+
form_row(ui, "Upstream SOCKS5", Some(
1233+
"Optional. host:port of a local xray / v2ray / sing-box SOCKS5 inbound. \
1234+
When set, non-HTTP / raw-TCP traffic (Telegram MTProto, IMAP, SSH, …) \
1235+
is chained through it instead of direct. HTTP/HTTPS still go through \
1236+
the Apps Script relay."
1237+
), |ui| {
1238+
ui.add(egui::TextEdit::singleline(&mut self.form.upstream_socks5)
1239+
.hint_text("empty = direct; 127.0.0.1:50529 for local xray")
1240+
.desired_width(f32::INFINITY));
1241+
});
12251242

1226-
form_row(ui, "Parallel dispatch", Some(
1227-
"Fire N Apps Script IDs in parallel per request and take the first \
1228-
response. 0/1 = off. 2-3 kills long-tail latency at N× quota cost. \
1229-
Only effective with multiple IDs configured."
1230-
), |ui| {
1231-
ui.add(egui::DragValue::new(&mut self.form.parallel_relay)
1232-
.speed(1)
1233-
.range(0..=8));
1234-
});
1243+
form_row(ui, "Parallel dispatch", Some(
1244+
"Fire N Apps Script IDs in parallel per request and take the first \
1245+
response. 0/1 = off. 2-3 kills long-tail latency at N× quota cost. \
1246+
Only effective with multiple IDs configured."
1247+
), |ui| {
1248+
ui.add(egui::DragValue::new(&mut self.form.parallel_relay)
1249+
.speed(1)
1250+
.range(0..=8));
1251+
});
1252+
}
12351253

12361254
form_row(ui, "Log level", None, |ui| {
12371255
egui::ComboBox::from_id_source("loglevel")
@@ -1247,33 +1265,35 @@ impl eframe::App for App {
12471265
ui.add_space(120.0 + 8.0);
12481266
ui.checkbox(&mut self.form.verify_ssl, "Verify TLS server certificate (recommended)");
12491267
});
1250-
ui.horizontal(|ui| {
1251-
ui.add_space(120.0 + 8.0);
1252-
ui.checkbox(&mut self.form.show_auth_key, "Show auth key");
1253-
});
1254-
ui.horizontal(|ui| {
1255-
ui.add_space(120.0 + 8.0);
1256-
ui.checkbox(&mut self.form.normalize_x_graphql, "Normalize X/Twitter GraphQL URLs")
1268+
if needs_apps_script {
1269+
ui.horizontal(|ui| {
1270+
ui.add_space(120.0 + 8.0);
1271+
ui.checkbox(&mut self.form.show_auth_key, "Show auth key");
1272+
});
1273+
ui.horizontal(|ui| {
1274+
ui.add_space(120.0 + 8.0);
1275+
ui.checkbox(&mut self.form.normalize_x_graphql, "Normalize X/Twitter GraphQL URLs")
1276+
.on_hover_text(
1277+
"Trim the `features` / `fieldToggles` query params from x.com/i/api/graphql/… \
1278+
requests before relaying. Massively improves cache hit rate when browsing \
1279+
Twitter/X. Off by default — some endpoints may reject trimmed requests. \
1280+
Credit: seramo_ir + Persian Python community (issue #16).",
1281+
);
1282+
});
1283+
ui.horizontal(|ui| {
1284+
ui.add_space(120.0 + 8.0);
1285+
ui.checkbox(
1286+
&mut self.form.youtube_via_relay,
1287+
"Send YouTube through relay (no SNI rewrite)",
1288+
)
12571289
.on_hover_text(
1258-
"Trim the `features` / `fieldToggles` query params from x.com/i/api/graphql/… \
1259-
requests before relaying. Massively improves cache hit rate when browsing \
1260-
Twitter/X. Off by default — some endpoints may reject trimmed requests. \
1261-
Credit: seramo_ir + Persian Python community (issue #16).",
1290+
"YouTube normally uses the same direct Google-edge tunnel as google.com (TLS SNI is \
1291+
the front domain, not youtube.com). That can trigger restricted mode or sign-out \
1292+
prompts. Enable this to route youtube.com / youtu.be / ytimg.com through the Apps \
1293+
Script relay instead — slower for video, but the visible SNI matches the site.",
12621294
);
1263-
});
1264-
ui.horizontal(|ui| {
1265-
ui.add_space(120.0 + 8.0);
1266-
ui.checkbox(
1267-
&mut self.form.youtube_via_relay,
1268-
"Send YouTube through relay (no SNI rewrite)",
1269-
)
1270-
.on_hover_text(
1271-
"YouTube normally uses the same direct Google-edge tunnel as google.com (TLS SNI is \
1272-
the front domain, not youtube.com). That can trigger restricted mode or sign-out \
1273-
prompts. Enable this to route youtube.com / youtu.be / ytimg.com through the Apps \
1274-
Script relay instead — slower for video, but the visible SNI matches the site.",
1275-
);
1276-
});
1295+
});
1296+
}
12771297
});
12781298
});
12791299

0 commit comments

Comments
 (0)