Skip to content

Commit 2cf840e

Browse files
authored
Update HomeScreen.kt
1 parent 5bea5cd commit 2cf840e

1 file changed

Lines changed: 167 additions & 56 deletions

File tree

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

Lines changed: 167 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import androidx.compose.material.icons.filled.ExpandMore
2626
import androidx.compose.material.icons.filled.PlayArrow
2727
import androidx.compose.material.icons.filled.Stop
2828
import androidx.compose.material.icons.filled.HourglassBottom
29-
import androidx.compose.material.ripple.rememberRipple
29+
import androidx.compose.material.ripple.rememberRipple // <-- added
3030
import androidx.compose.material3.*
3131
import androidx.compose.runtime.*
3232
import androidx.compose.runtime.saveable.rememberSaveable
@@ -66,7 +66,21 @@ import kotlinx.coroutines.withContext
6666
import kotlinx.coroutines.withTimeoutOrNull
6767
import org.json.JSONObject
6868

69-
// ... (sealed classes remain unchanged) ...
69+
/**
70+
* UI state returned by the Activity after the CA install flow finishes,
71+
* so the screen can show a matching snackbar.
72+
*/
73+
sealed class CaInstallOutcome {
74+
object Installed : CaInstallOutcome()
75+
data class NotInstalled(val downloadPath: String?) : CaInstallOutcome()
76+
data class Failed(val message: String) : CaInstallOutcome()
77+
}
78+
79+
// probe result classification (used by parseProbeResult)
80+
sealed class ProbeState {
81+
data class Ok(val latencyMs: Int) : ProbeState()
82+
data class Err(val message: String) : ProbeState()
83+
}
7084

7185
@OptIn(ExperimentalMaterial3Api::class)
7286
@Composable
@@ -90,7 +104,7 @@ fun HomeScreen(
90104

91105
var showInstallDialog by rememberSaveable { mutableStateOf(false) }
92106

93-
// Auto update check (kept, but no UI button for it now)
107+
// Auto update check
94108
var autoUpdateChecked by rememberSaveable { mutableStateOf(false) }
95109
LaunchedEffect(autoUpdateChecked) {
96110
if (autoUpdateChecked) return@LaunchedEffect
@@ -143,12 +157,6 @@ fun HomeScreen(
143157
}
144158

145159
Scaffold(
146-
topBar = {
147-
TopAppBar(
148-
title = { Text("Mooz VPN") }, // changed title
149-
// actions removed (no language toggle, no update check button)
150-
)
151-
},
152160
snackbarHost = { SnackbarHost(snackbar) },
153161
) { inner ->
154162
Column(
@@ -160,44 +168,6 @@ fun HomeScreen(
160168
verticalArrangement = Arrangement.Center,
161169
horizontalAlignment = Alignment.CenterHorizontally,
162170
) {
163-
// ========== REPLACED ConfigSharingBar ==========
164-
// Only Import (from clipboard) and Scan QR remain
165-
Row(
166-
modifier = Modifier.fillMaxWidth(),
167-
horizontalArrangement = Arrangement.SpaceEvenly
168-
) {
169-
// Import button: reads from clipboard and calls onImport
170-
val clipboardManager = LocalClipboardManager.current
171-
Button(
172-
onClick = {
173-
val clip = clipboardManager.getText()?.text
174-
if (!clip.isNullOrBlank()) {
175-
// Assume the clipboard contains a valid config string
176-
// (JSON or the format expected by ConfigStore)
177-
persist(MhrvConfig.fromJson(clip) ?: run {
178-
snackbar.showSnackbar("Invalid config in clipboard")
179-
return@Button
180-
})
181-
snackbar.showSnackbar("Config imported from clipboard")
182-
} else {
183-
snackbar.showSnackbar("Clipboard is empty")
184-
}
185-
}
186-
) {
187-
Text("Import")
188-
}
189-
190-
// Scan button (placeholder – integrate your QR scanner here)
191-
Button(
192-
onClick = {
193-
// TODO: Replace with actual QR scanning logic
194-
// For now, show a snackbar suggesting to implement
195-
snackbar.showSnackbar("QR scanning not yet implemented")
196-
}
197-
) {
198-
Text("Scan QR")
199-
}
200-
}
201171

202172
Spacer(Modifier.height(32.dp))
203173

@@ -241,26 +211,80 @@ fun HomeScreen(
241211

242212
Spacer(Modifier.height(24.dp))
243213

214+
// وضعیت فعلی (اختیاری)
244215
if (transitioning) {
245216
Text("", style = MaterialTheme.typography.titleMedium)
246217
} else {
247218
Text(
248-
if (isVpnRunning) "متصل" else "قطع",
219+
if (isVpnRunning) "Connected" else "Disconnected",
249220
style = MaterialTheme.typography.titleMedium,
250221
color = if (isVpnRunning) OkGreen else MaterialTheme.colorScheme.onSurfaceVariant,
251222
)
252223
}
253224
}
254225
}
255226

256-
// CA install dialog unchanged
227+
// دیالوگ نصب گواهی (حذف نشده ولی چون دکمه‌اش رو برداشتیم نمایش داده نمیشه)
257228
if (showInstallDialog) {
258-
// ... (unchanged, same as original) ...
229+
val exported = remember { CaInstall.export(ctx) }
230+
val fp = remember(exported) { if (exported) CaInstall.fingerprint(ctx) else null }
231+
val cn = remember(exported) { if (exported) CaInstall.subjectCn(ctx) else null }
232+
233+
AlertDialog(
234+
onDismissRequest = { showInstallDialog = false },
235+
title = { Text(stringResource(R.string.dialog_install_mitm_title)) },
236+
text = {
237+
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
238+
Text(
239+
"mhrv-rs creates a local certificate authority so it can decrypt " +
240+
"and re-encrypt HTTPS traffic before tunnelling it through the Apps " +
241+
"Script relay. Without this CA installed as trusted, apps will show " +
242+
"certificate errors."
243+
)
244+
Text(
245+
"On Android 11+ the system removed the inline install path, so " +
246+
"tapping Install will: (1) save a PEM copy to Downloads/mhrv-ca.crt, " +
247+
"(2) open the Settings app.\n\n" +
248+
"Inside Settings, tap the search bar and type \"CA certificate\". " +
249+
"Open the result labelled \"CA certificate\" (NOT \"VPN & app user " +
250+
"certificate\" or \"Wi-Fi certificate\"). Pick mhrv-ca.crt from " +
251+
"Downloads when prompted. If you don't have a screen lock, Android " +
252+
"will ask you to add one first — that's an OS requirement for " +
253+
"installing any user CA."
254+
)
255+
if (fp != null) {
256+
Text("Subject: ${cn ?: "(unknown)"}", style = MaterialTheme.typography.labelMedium)
257+
Text(
258+
text = "SHA-256: ${CaInstall.fingerprintHex(fp)}",
259+
style = MaterialTheme.typography.labelSmall,
260+
fontFamily = FontFamily.Monospace,
261+
)
262+
} else {
263+
Text(
264+
"Could not read the CA cert yet. Tap Start once so the " +
265+
"proxy generates it, then come back.",
266+
color = MaterialTheme.colorScheme.error,
267+
)
268+
}
269+
}
270+
},
271+
confirmButton = {
272+
TextButton(
273+
onClick = {
274+
showInstallDialog = false
275+
if (fp != null) onInstallCaConfirmed()
276+
},
277+
enabled = fp != null,
278+
) { Text("Install") }
279+
},
280+
dismissButton = {
281+
TextButton(onClick = { showInstallDialog = false }) { Text("Cancel") }
282+
},
283+
)
259284
}
260285
}
261286

262-
// ========== CircularConnectButton and helper functions remain exactly the same ==========
263-
// (no changes below this line)
287+
// ========== دکمه دایره‌ای شیک ==========
264288

265289
@Composable
266290
private fun CircularConnectButton(
@@ -270,17 +294,104 @@ private fun CircularConnectButton(
270294
onClick: () -> Unit,
271295
modifier: Modifier = Modifier,
272296
) {
273-
// ... (unchanged) ...
297+
val interactionSource = remember { MutableInteractionSource() }
298+
val isPressed by interactionSource.collectIsPressedAsState()
299+
val scale by animateFloatAsState(
300+
targetValue = if (isPressed) 0.92f else 1f,
301+
animationSpec = spring(dampingRatio = 0.5f, stiffness = 400f),
302+
label = "buttonScale"
303+
)
304+
305+
val buttonColor = when {
306+
!enabled -> MaterialTheme.colorScheme.surfaceVariant
307+
isRunning -> OkGreen
308+
else -> Color(0xFF9E9E9E) // خاکستری
309+
}
310+
311+
Box(
312+
modifier = modifier
313+
.size(140.dp)
314+
.scale(scale)
315+
.clip(CircleShape)
316+
.background(buttonColor)
317+
.clickable(
318+
interactionSource = interactionSource,
319+
indication = rememberRipple(bounded = true, radius = 70.dp),
320+
enabled = enabled && !transitioning,
321+
onClick = onClick
322+
),
323+
contentAlignment = Alignment.Center
324+
) {
325+
Column(horizontalAlignment = Alignment.CenterHorizontally) {
326+
if (transitioning) {
327+
CircularProgressIndicator(
328+
modifier = Modifier.size(48.dp),
329+
color = Color.White,
330+
strokeWidth = 4.dp
331+
)
332+
} else {
333+
Icon(
334+
imageVector = if (isRunning) Icons.Default.Stop else Icons.Default.PlayArrow,
335+
contentDescription = if (isRunning) "Disconnect" else "Connect",
336+
tint = Color.White,
337+
modifier = Modifier.size(56.dp)
338+
)
339+
Spacer(Modifier.height(4.dp))
340+
Text(
341+
text = if (isRunning) "قطع" else "اتصال",
342+
color = Color.White,
343+
style = MaterialTheme.typography.labelLarge,
344+
)
345+
}
346+
}
347+
}
274348
}
275349

350+
// ========== توابع کمکی (بدون تغییر) ==========
351+
276352
private fun summarizeUpdateCheck(json: String?): String {
277-
// ... (unchanged) ...
353+
if (json.isNullOrBlank()) return "Update check failed (no response)"
354+
return try {
355+
val obj = JSONObject(json)
356+
when (obj.optString("kind")) {
357+
"upToDate" -> "Up to date (running v${obj.optString("current")})"
358+
"updateAvailable" -> {
359+
val cur = obj.optString("current")
360+
val latest = obj.optString("latest")
361+
val url = obj.optString("url")
362+
"Update available: v$cur → v$latest $url"
363+
}
364+
"offline" -> "Offline: ${obj.optString("reason", "no details")}"
365+
"error" -> "Check failed: ${obj.optString("reason", "no details")}"
366+
else -> "Check failed (unknown response)"
367+
}
368+
} catch (_: Throwable) {
369+
"Check failed (bad json)"
370+
}
278371
}
279372

280373
private fun String.parseAsIpOrNull(): java.net.InetAddress? {
281-
// ... (unchanged) ...
374+
val s = trim()
375+
if (s.isEmpty() || s.any { it.isLetter() }) return null
376+
return try {
377+
java.net.InetAddress.getByName(s).takeIf {
378+
it.hostAddress?.let { addr -> addr == s || addr.contains(s) } == true
379+
}
380+
} catch (_: Throwable) {
381+
null
382+
}
282383
}
283384

284385
private fun parseProbeResult(json: String?): ProbeState {
285-
// ... (unchanged) ...
386+
if (json.isNullOrBlank()) return ProbeState.Err("no response")
387+
return try {
388+
val obj = JSONObject(json)
389+
if (obj.optBoolean("ok", false)) {
390+
ProbeState.Ok(obj.optInt("latencyMs", -1))
391+
} else {
392+
ProbeState.Err(obj.optString("error", "failed"))
393+
}
394+
} catch (_: Throwable) {
395+
ProbeState.Err("bad json")
396+
}
286397
}

0 commit comments

Comments
 (0)