From abeb58191e15dea1dcfc388bea1547167f652ac3 Mon Sep 17 00:00:00 2001 From: zarazaex69 Date: Wed, 27 May 2026 16:55:09 +0300 Subject: [PATCH] refactor(ui): improve connection testing, dialog styling, and status indicators - major release, update app --- .gitignore | 2 + .../olc/handler/V2RayServiceManager.kt | 24 ++++--- .../java/xyz/zarazaex/olc/ui/MainActivity.kt | 69 ++++++++++++------- .../zarazaex/olc/ui/MainRecyclerAdapter.kt | 14 ++-- .../app/src/main/res/layout/dialog_donate.xml | 52 +++++++------- .../res/layout/dialog_title_with_close.xml | 57 ++++++--------- .../app/src/main/res/values-night/colors.xml | 1 + V2rayNG/app/src/main/res/values/colors.xml | 1 + 8 files changed, 124 insertions(+), 96 deletions(-) diff --git a/.gitignore b/.gitignore index e310089a..bddd505f 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,5 @@ material-design-icons .kiro/steering/kirograph.md .kirograph/config.json .kirograph/kirograph.db +.kirograph/.session-id +.kirograph/token-savings.jsonl diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/handler/V2RayServiceManager.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/handler/V2RayServiceManager.kt index 6ae1a56b..c5444326 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/handler/V2RayServiceManager.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/handler/V2RayServiceManager.kt @@ -305,20 +305,28 @@ object V2RayServiceManager { var time = -1L var errorStr = "" - try { - time = coreController.measureDelay(SettingsManager.getDelayTestUrl()) - } catch (e: Exception) { - Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e) - errorStr = e.message?.substringAfter("\":") ?: "empty message" - } - if (time == -1L) { + val urls = listOf( + SettingsManager.getDelayTestUrl(), + SettingsManager.getDelayTestUrl(true) + ) + for (url in urls) { + if (time >= 0) break try { - time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true)) + time = coreController.measureDelay(url) } catch (e: Exception) { Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e) errorStr = e.message?.substringAfter("\":") ?: "empty message" } } + // One more retry after a brief pause to reduce false negatives + if (time == -1L) { + kotlinx.coroutines.delay(500) + try { + time = coreController.measureDelay(urls[0]) + } catch (e: Exception) { + errorStr = e.message?.substringAfter("\":") ?: "empty message" + } + } val result = if (time >= 0) { service.getString(R.string.connection_test_available, time) diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainActivity.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainActivity.kt index 9ca1e17f..c0c6e401 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainActivity.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainActivity.kt @@ -1,5 +1,7 @@ package xyz.zarazaex.olc.ui +import android.animation.ArgbEvaluator +import android.animation.ValueAnimator import android.content.Intent import android.content.res.ColorStateList import android.graphics.Color @@ -203,7 +205,13 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect } private fun setupViewModel() { - mainViewModel.updateTestResultAction.observe(this) { setTestState(it) } + mainViewModel.updateTestResultAction.observe(this) { result -> + setTestState(result) + if (result != null && mainViewModel.isRunning.value == true) { + val isSuccess = result.contains(Regex("\\d+\\s*(ms|мс|毫秒)")) + setStatusDot(if (isSuccess) DotState.CONNECTED else DotState.FAILURE) + } + } mainViewModel.isTesting.observe(this) { testing -> if (testing) { @@ -218,13 +226,13 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect binding.btnSummaryLite.isEnabled = true binding.btnSummaryLite.alpha = 1.0f binding.btnSummaryLite.setIconResource(R.drawable.ic_stop_24dp) - binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf( - com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimaryContainer, 0) + animateButtonTint(binding.btnSummaryLite, + com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0) ) } else { setButtonsEnabled(true) binding.btnSummaryLite.setIconResource(R.drawable.bolt_24) - binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf( + animateButtonTint(binding.btnSummaryLite, com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0) ) if (!isLiteTesting) { @@ -383,7 +391,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect showStatus("Остановлено") setButtonsEnabled(true) binding.btnSummaryLite.setIconResource(R.drawable.bolt_24) - binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf( + animateButtonTint(binding.btnSummaryLite, com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0) ) hideLoading() @@ -490,9 +498,9 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect delay(1000) } catch (e: Exception) { Log.e(AppConfig.TAG, "Error in restartV2Ray", e) - applyRunningState(isLoading = false, isRunning = mainViewModel.isRunning.value == true) } finally { isFabOperationInProgress = false + applyRunningState(isLoading = false, isRunning = mainViewModel.isRunning.value == true) } } } @@ -530,6 +538,16 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect return ColorStateList.valueOf(color) } + private fun animateButtonTint(view: com.google.android.material.button.MaterialButton, toColor: Int, duration: Long = 300L) { + val from = view.backgroundTintList?.defaultColor ?: toColor + if (from == toColor) { view.backgroundTintList = ColorStateList.valueOf(toColor); return } + ValueAnimator.ofArgb(from, toColor).apply { + this.duration = duration + addUpdateListener { view.backgroundTintList = ColorStateList.valueOf(it.animatedValue as Int) } + start() + } + } + private fun applyRunningState(isLoading: Boolean, isRunning: Boolean) { val secContainer = ColorStateList.valueOf( com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0) @@ -562,7 +580,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect binding.fab.alpha = 1.0f binding.fab.backgroundTintList = accentColor() binding.fab.iconTint = onPrimary - binding.btnSummaryLite.backgroundTintList = secContainer + animateButtonTint(binding.btnSummaryLite, secContainer.defaultColor) binding.fab.contentDescription = getString(R.string.action_stop_service) setTestState(getString(R.string.connection_connected)) binding.layoutTest.isFocusable = true @@ -571,7 +589,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect setButtonsEnabled(true) binding.fab.backgroundTintList = secContainer binding.fab.iconTint = onSecContainer - binding.btnSummaryLite.backgroundTintList = secContainer + animateButtonTint(binding.btnSummaryLite, secContainer.defaultColor) binding.fab.contentDescription = getString(R.string.tasker_start_service) if (mainViewModel.isTesting.value != true && statusResetJob?.isActive != true) { setTestState(getString(R.string.connection_not_connected)) @@ -581,7 +599,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect } } - private enum class DotState { IDLE, CONNECTED, LOADING } + private enum class DotState { IDLE, CONNECTED, LOADING, FAILURE } private fun setStatusDot(state: DotState) { val dot = binding.statusDot @@ -589,6 +607,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect dot.alpha = 1f; dot.scaleX = 1f; dot.scaleY = 1f dot.backgroundTintList = ColorStateList.valueOf(when (state) { DotState.CONNECTED -> ContextCompat.getColor(this, R.color.status_connected) + DotState.FAILURE -> ContextCompat.getColor(this, R.color.status_failure) DotState.LOADING -> com.google.android.material.color.MaterialColors.getColor(this, androidx.appcompat.R.attr.colorPrimary, 0) DotState.IDLE -> com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorOutline, 0) }) @@ -894,20 +913,26 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect copyToClipboard(btcValue.text.toString()) } - MaterialAlertDialogBuilder(this) - .setTitle(R.string.donate_dialog_title) + val titleStr = getString(R.string.donate_dialog_title) + val dialog = MaterialAlertDialogBuilder(this) .setView(view) - .setNegativeButton(R.string.donate_btn_dont_show) { dialog, _ -> + .setNegativeButton(R.string.donate_btn_dont_show) { d, _ -> MmkvManager.encodeSettings(AppConfig.PREF_DONATE_DIALOG_DISMISSED, true) - dialog.dismiss() - } - .setPositiveButton(R.string.donate_btn_postpone) { dialog, _ -> - val postponeUntil = System.currentTimeMillis() + 24L * 60 * 60 * 1000 - MmkvManager.encodeSettings(AppConfig.PREF_DONATE_DIALOG_POSTPONE_UNTIL, postponeUntil) - dialog.dismiss() + d.dismiss() } .setCancelable(true) - .show() + .create() + // Closing (X / outside / back) postpones for 24h + val postpone = { + val postponeUntil = System.currentTimeMillis() + 24L * 60 * 60 * 1000 + MmkvManager.encodeSettings(AppConfig.PREF_DONATE_DIALOG_POSTPONE_UNTIL, postponeUntil) + } + dialog.setOnCancelListener { postpone() } + dialog.setCustomTitle(buildDialogTitleWithClose(titleStr) { + postpone() + dialog.dismiss() + }) + dialog.show() } private fun copyToClipboard(text: String) { @@ -1117,7 +1142,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect } val dialog = MaterialAlertDialogBuilder(this@MainActivity) - .setTitle("Исключить страны") .setAdapter(adapter, null) .setPositiveButton("Применить") { _, _ -> val excluded = codes.filterIndexed { i, _ -> checked[i] }.toSet() @@ -1131,8 +1155,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect showStatus("Показаны все страны") } .create() - dialog.show() dialog.setCustomTitle(buildDialogTitleWithClose("Исключить страны") { dialog.dismiss() }) + dialog.show() } } } @@ -1158,7 +1182,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect val message = result.releaseNotes?.let { xyz.zarazaex.olc.util.MarkdownUtil.parseBasic(it) } ?: "" val titleStr = getString(R.string.update_new_version_found, result.latestVersion) val dialog = MaterialAlertDialogBuilder(this) - .setTitle(titleStr) .setMessage(message) .setPositiveButton(R.string.update_now) { _, _ -> result.downloadUrl?.let { @@ -1166,8 +1189,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect } } .create() - dialog.show() dialog.setCustomTitle(buildDialogTitleWithClose(titleStr) { dialog.dismiss() }) + dialog.show() } private fun buildDialogTitleWithClose(title: String, onClose: () -> Unit): View { diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainRecyclerAdapter.kt index a80e3838..66aa528c 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainRecyclerAdapter.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainRecyclerAdapter.kt @@ -166,10 +166,14 @@ class MainRecyclerAdapter( if (addressText.isEmpty()) View.GONE else View.VISIBLE // TestResult - val aff = MmkvManager.decodeServerAffiliationInfo(guid) - holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty() + val delayMillis = data[position].testDelayMillis + holder.itemMainBinding.tvTestResult.text = when { + delayMillis == 0L -> "—ms" + delayMillis < 0L -> "-ms" + else -> "${delayMillis}ms" + } holder.itemMainBinding.tvTestResult.setTextColor( - getPingColor(context, aff?.testDelayMillis) + getPingColor(context, delayMillis) ) (holder.itemMainBinding.tvTestResult.layoutParams as? ViewGroup.MarginLayoutParams)?.marginStart = if (addressText.isEmpty()) 0 else 6.dpToPx(context) @@ -264,9 +268,7 @@ class MainRecyclerAdapter( private fun recomputePingRange() { val delays = data.mapNotNull { item -> - MmkvManager.decodeServerAffiliationInfo(item.guid) - ?.testDelayMillis - ?.takeIf { it > 0L } + item.testDelayMillis.takeIf { it > 0L } } minReachablePing = delays.minOrNull() maxReachablePing = delays.maxOrNull() diff --git a/V2rayNG/app/src/main/res/layout/dialog_donate.xml b/V2rayNG/app/src/main/res/layout/dialog_donate.xml index 172359b4..32a384ed 100644 --- a/V2rayNG/app/src/main/res/layout/dialog_donate.xml +++ b/V2rayNG/app/src/main/res/layout/dialog_donate.xml @@ -27,7 +27,7 @@ android:gravity="center_vertical" android:orientation="horizontal" android:paddingStart="14dp" - android:paddingEnd="6dp" + android:paddingEnd="10dp" android:paddingTop="10dp" android:paddingBottom="10dp"> @@ -56,15 +56,16 @@ android:typeface="monospace" /> - + android:padding="10dp" + android:scaleType="centerInside" + android:src="@drawable/ic_share_24dp" + app:tint="?attr/colorOnSurfaceVariant" /> @@ -84,7 +85,7 @@ android:gravity="center_vertical" android:orientation="horizontal" android:paddingStart="14dp" - android:paddingEnd="6dp" + android:paddingEnd="10dp" android:paddingTop="10dp" android:paddingBottom="10dp"> @@ -114,15 +115,16 @@ android:typeface="monospace" /> - + android:padding="10dp" + android:scaleType="centerInside" + android:src="@drawable/ic_copy" + app:tint="?attr/colorOnSurfaceVariant" /> @@ -142,7 +144,7 @@ android:gravity="center_vertical" android:orientation="horizontal" android:paddingStart="14dp" - android:paddingEnd="6dp" + android:paddingEnd="10dp" android:paddingTop="10dp" android:paddingBottom="10dp"> @@ -172,15 +174,16 @@ android:typeface="monospace" /> - + android:padding="10dp" + android:scaleType="centerInside" + android:src="@drawable/ic_copy" + app:tint="?attr/colorOnSurfaceVariant" /> @@ -199,7 +202,7 @@ android:gravity="center_vertical" android:orientation="horizontal" android:paddingStart="14dp" - android:paddingEnd="6dp" + android:paddingEnd="10dp" android:paddingTop="10dp" android:paddingBottom="10dp"> @@ -229,15 +232,16 @@ android:typeface="monospace" /> - + android:padding="10dp" + android:scaleType="centerInside" + android:src="@drawable/ic_copy" + app:tint="?attr/colorOnSurfaceVariant" /> diff --git a/V2rayNG/app/src/main/res/layout/dialog_title_with_close.xml b/V2rayNG/app/src/main/res/layout/dialog_title_with_close.xml index 0f48174f..b5b43538 100644 --- a/V2rayNG/app/src/main/res/layout/dialog_title_with_close.xml +++ b/V2rayNG/app/src/main/res/layout/dialog_title_with_close.xml @@ -1,42 +1,29 @@ - + android:paddingStart="24dp" + android:paddingEnd="8dp" + android:paddingTop="16dp" + android:paddingBottom="8dp"> - + android:layout_gravity="center_vertical" + android:gravity="start" + android:paddingEnd="32dp" + android:textAppearance="@style/TextAppearance.Material3.TitleMedium" + android:textColor="?attr/colorOnSurface" /> - - - - - - - - + + diff --git a/V2rayNG/app/src/main/res/values-night/colors.xml b/V2rayNG/app/src/main/res/values-night/colors.xml index 689c8bcb..5b12e65c 100644 --- a/V2rayNG/app/src/main/res/values-night/colors.xml +++ b/V2rayNG/app/src/main/res/values-night/colors.xml @@ -4,6 +4,7 @@ #86D993 #F1C76A #66BB6A + #EF5350 #FFB4AB @color/md_theme_primary @color/md_theme_secondaryContainer diff --git a/V2rayNG/app/src/main/res/values/colors.xml b/V2rayNG/app/src/main/res/values/colors.xml index 614436ee..c9522072 100644 --- a/V2rayNG/app/src/main/res/values/colors.xml +++ b/V2rayNG/app/src/main/res/values/colors.xml @@ -4,6 +4,7 @@ #2E7D32 #9A6700 #4CAF50 + #E53935 #BA1A1A #1565C0 #FFFFFF