refactor(ui): improve connection testing, dialog styling, and status indicators

- major release, update app
This commit is contained in:
zarazaex69
2026-05-27 16:55:09 +03:00
parent f55ac36b5a
commit abeb58191e
8 changed files with 124 additions and 96 deletions
+2
View File
@@ -73,3 +73,5 @@ material-design-icons
.kiro/steering/kirograph.md
.kirograph/config.json
.kirograph/kirograph.db
.kirograph/.session-id
.kirograph/token-savings.jsonl
@@ -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)
@@ -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 {
@@ -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()
@@ -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" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
<ImageButton
android:id="@+id/donate_card_open"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/donate_open_link"
app:icon="@drawable/ic_share_24dp"
app:iconSize="20dp"
app:iconTint="?attr/colorOnSurfaceVariant" />
android:padding="10dp"
android:scaleType="centerInside"
android:src="@drawable/ic_share_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
@@ -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" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
<ImageButton
android:id="@+id/donate_ton_copy"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/donate_copy"
app:icon="@drawable/ic_copy"
app:iconSize="20dp"
app:iconTint="?attr/colorOnSurfaceVariant" />
android:padding="10dp"
android:scaleType="centerInside"
android:src="@drawable/ic_copy"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
@@ -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" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
<ImageButton
android:id="@+id/donate_trc_copy"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/donate_copy"
app:icon="@drawable/ic_copy"
app:iconSize="20dp"
app:iconTint="?attr/colorOnSurfaceVariant" />
android:padding="10dp"
android:scaleType="centerInside"
android:src="@drawable/ic_copy"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
@@ -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" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
<ImageButton
android:id="@+id/donate_btc_copy"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/donate_copy"
app:icon="@drawable/ic_copy"
app:iconSize="20dp"
app:iconTint="?attr/colorOnSurfaceVariant" />
android:padding="10dp"
android:scaleType="centerInside"
android:src="@drawable/ic_copy"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
@@ -1,42 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:paddingStart="24dp"
android:paddingEnd="8dp"
android:paddingTop="16dp"
android:paddingBottom="8dp">
<FrameLayout
<TextView
android:id="@+id/dialog_title_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="8dp"
android:paddingTop="20dp"
android:paddingBottom="16dp">
android:layout_gravity="center_vertical"
android:gravity="start"
android:paddingEnd="32dp"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/dialog_title_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:paddingEnd="32dp"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
android:textColor="?attr/colorOnSurface" />
<ImageButton
android:id="@+id/dialog_close_btn"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end|center_vertical"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Закрыть"
android:src="@drawable/ic_close_24dp"
android:tint="?attr/colorOnSurfaceVariant" />
</FrameLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/colorOutline" />
</LinearLayout>
<ImageButton
android:id="@+id/dialog_close_btn"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="end|center_vertical"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Закрыть"
android:src="@drawable/ic_close_24dp"
android:tint="?attr/colorOnSurfaceVariant" />
</FrameLayout>
@@ -4,6 +4,7 @@
<color name="colorPingGood">#86D993</color>
<color name="colorPingMedium">#F1C76A</color>
<color name="status_connected">#66BB6A</color>
<color name="status_failure">#EF5350</color>
<color name="colorPingRed">#FFB4AB</color>
<color name="color_fab_active">@color/md_theme_primary</color>
<color name="color_fab_inactive">@color/md_theme_secondaryContainer</color>
@@ -4,6 +4,7 @@
<color name="colorPingGood">#2E7D32</color>
<color name="colorPingMedium">#9A6700</color>
<color name="status_connected">#4CAF50</color>
<color name="status_failure">#E53935</color>
<color name="colorPingRed">#BA1A1A</color>
<color name="colorConfigType">#1565C0</color>
<color name="colorWhite">#FFFFFF</color>