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