@@ -26,7 +26,7 @@ import androidx.compose.material.icons.filled.ExpandMore
2626import androidx.compose.material.icons.filled.PlayArrow
2727import androidx.compose.material.icons.filled.Stop
2828import androidx.compose.material.icons.filled.HourglassBottom
29- import androidx.compose.material.ripple.rememberRipple
29+ import androidx.compose.material.ripple.rememberRipple // <-- added
3030import androidx.compose.material3.*
3131import androidx.compose.runtime.*
3232import androidx.compose.runtime.saveable.rememberSaveable
@@ -66,7 +66,21 @@ import kotlinx.coroutines.withContext
6666import kotlinx.coroutines.withTimeoutOrNull
6767import 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
266290private 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+
276352private 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
280373private 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
284385private 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