Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ branch names, credentials, signing material, or other secrets.
- MegaProxy is selected for split tunneling by default. Existing lists gain it once on upgrade;
subsequent explicit exclusion must survive reloads, unrelated edits and profile changes.

- Keep UI labels and explanations concise in both languages. Put implementation details in the
current EN/RU documentation; retain actionable limits, security risks and privacy consequences in UI.
- All user-visible UI text must use Android string resources and be supplied in both the default
English resources and Russian resources. The app language selects UI words; number, date and
other data formatting follows the system locale. Do not localize standardized data-unit symbols.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ internal fun ConfigSubscriptionScreen(activity: Activity, onBack: () -> Unit, dr
OutlinedTextField(draft.url, { draft.url = it }, label = { FieldLabel(stringResource(R.string.subscription_url)) },
modifier = Modifier.fillMaxWidth(), singleLine = true, enabled = !busy)
OutlinedTextField(draft.backups, { draft.backups = it }, label = { FieldLabel(stringResource(R.string.subscription_backups)) },
supportingText = { Text(stringResource(R.string.subscription_backups_hint)) },
modifier = Modifier.fillMaxWidth(), enabled = !busy)
Row(
Modifier.fillMaxWidth().toggleable(
Expand Down
140 changes: 71 additions & 69 deletions app/src/main/java/net/megaproxy487/ConnectionTestScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -145,80 +145,82 @@ internal fun ConnectionTestScreen(activity: Activity, autoStart: Boolean, onBack
},
contentWindowInsets = WindowInsets.safeDrawing,
) { padding ->
BoxWithConstraints(Modifier.fillMaxSize().padding(padding)) {
val logHeight = maxOf(160.dp, maxHeight * 0.55f)
Column(
Modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
val statusLabel = when (state) {
TestState.IDLE -> activity.uiText(R.string.test_ready)
TestState.RUNNING -> activity.uiText(R.string.test_testing)
TestState.SUCCEEDED -> activity.uiText(R.string.test_passed)
TestState.FAILED -> activity.uiText(R.string.test_failed)
}
Card(Modifier.fillMaxWidth().semantics {
liveRegion = LiveRegionMode.Polite
stateDescription = statusLabel
}) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
val statusColor = when (state) {
TestState.SUCCEEDED -> MaterialTheme.colorScheme.primary
TestState.FAILED -> MaterialTheme.colorScheme.error
else -> MaterialTheme.colorScheme.onSurface
}
Text(
if (state == TestState.RUNNING) activity.uiText(R.string.test_testing_progress) else statusLabel,
style = MaterialTheme.typography.titleLarge,
color = statusColor,
)
if (state == TestState.RUNNING) {
CircularProgressIndicator(modifier = Modifier.padding(top = 8.dp))
}
exitIp?.let {
SelectionContainer { Text(stringResource(R.string.proxy_exit_ip, it)) }
ScreenContent(Modifier.padding(padding)) {
BoxWithConstraints(Modifier.fillMaxSize()) {
val logHeight = maxOf(160.dp, maxHeight * 0.55f)
Column(
Modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
val statusLabel = when (state) {
TestState.IDLE -> activity.uiText(R.string.test_ready)
TestState.RUNNING -> activity.uiText(R.string.test_testing)
TestState.SUCCEEDED -> activity.uiText(R.string.test_passed)
TestState.FAILED -> activity.uiText(R.string.test_failed)
}
Card(Modifier.fillMaxWidth().semantics {
liveRegion = LiveRegionMode.Polite
stateDescription = statusLabel
}) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
val statusColor = when (state) {
TestState.SUCCEEDED -> MaterialTheme.colorScheme.primary
TestState.FAILED -> MaterialTheme.colorScheme.error
else -> MaterialTheme.colorScheme.onSurface
}
Text(
if (state == TestState.RUNNING) activity.uiText(R.string.test_testing_progress) else statusLabel,
style = MaterialTheme.typography.titleLarge,
color = statusColor,
)
if (state == TestState.RUNNING) {
CircularProgressIndicator(modifier = Modifier.padding(top = 8.dp))
}
exitIp?.let {
SelectionContainer { Text(stringResource(R.string.proxy_exit_ip, it)) }
}
countryCode?.let { Text(stringResource(R.string.proxy_exit_country, formatCountry(it, systemFormattingLocale()))) }
if (http3Fallback) Text(stringResource(R.string.http3_fallback_warning),
color = MaterialTheme.colorScheme.tertiary)
http3.forEach { probe ->
val status = stringResource(if (probe.confirmed) R.string.test_http3_confirmed else R.string.test_http3_unavailable)
Text(
stringResource(
R.string.test_http3_provider,
probe.provider,
stringResource(if (probe.confirmed) R.string.test_status_success_icon else R.string.test_status_failed_icon),
),
Modifier.clearAndSetSemantics {
contentDescription = activity.uiText(R.string.test_http3_provider_a11y, probe.provider, status)
},
)
}
if (http3.isNotEmpty()) {
Text(stringResource(R.string.test_http3_help), style = MaterialTheme.typography.bodySmall)
}
}
countryCode?.let { Text(stringResource(R.string.proxy_exit_country, formatCountry(it, systemFormattingLocale()))) }
if (http3Fallback) Text(stringResource(R.string.http3_fallback_warning),
color = MaterialTheme.colorScheme.tertiary)
http3.forEach { probe ->
val status = stringResource(if (probe.confirmed) R.string.test_http3_confirmed else R.string.test_http3_unavailable)
}
WrappingActions(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(shape = RoundedCornerShape(12.dp), onClick = runTest, enabled = state != TestState.RUNNING) { Text(stringResource(R.string.run_again)) }
Button(shape = RoundedCornerShape(12.dp), onClick = {
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText(activity.uiText(R.string.connection_test_clipboard_label), TestDiagnosticLog.entries.joinToString("\n")))
}, enabled = TestDiagnosticLog.entries.isNotEmpty()) { Text(stringResource(R.string.copy_log)) }
}
Text(
activity.uiText(R.string.test_privacy_help),
style = MaterialTheme.typography.bodySmall,
)
Box(Modifier.fillMaxWidth().heightIn(min = 160.dp, max = logHeight)) {
SelectionContainer {
Text(
stringResource(
R.string.test_http3_provider,
probe.provider,
stringResource(if (probe.confirmed) R.string.test_status_success_icon else R.string.test_status_failed_icon),
),
Modifier.clearAndSetSemantics {
contentDescription = activity.uiText(R.string.test_http3_provider_a11y, probe.provider, status)
TestDiagnosticLog.entries.joinToString("\n").ifEmpty {
if (state == TestState.RUNNING) activity.uiText(R.string.test_waiting) else activity.uiText(R.string.test_empty)
},
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
)
}
if (http3.isNotEmpty()) {
Text(stringResource(R.string.test_http3_help), style = MaterialTheme.typography.bodySmall)
}
}
}
WrappingActions(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(shape = RoundedCornerShape(12.dp), onClick = runTest, enabled = state != TestState.RUNNING) { Text(stringResource(R.string.run_again)) }
Button(shape = RoundedCornerShape(12.dp), onClick = {
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText(activity.uiText(R.string.connection_test_clipboard_label), TestDiagnosticLog.entries.joinToString("\n")))
}, enabled = TestDiagnosticLog.entries.isNotEmpty()) { Text(stringResource(R.string.copy_log)) }
}
Text(
activity.uiText(R.string.test_privacy_help),
style = MaterialTheme.typography.bodySmall,
)
Box(Modifier.fillMaxWidth().heightIn(min = 160.dp, max = logHeight)) {
SelectionContainer {
Text(
TestDiagnosticLog.entries.joinToString("\n").ifEmpty {
if (state == TestState.RUNNING) activity.uiText(R.string.test_waiting) else activity.uiText(R.string.test_empty)
},
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
)
}
}
}
Expand Down
Loading
Loading