24 Commits

Author SHA1 Message Date
zarazaex69 4fa5dd4bca fix: copy buton not display 2026-05-10 13:52:03 +03:00
zarazaex69 b2f3415421 feat: fix 2026-05-09 02:07:43 +03:00
zarazaex69 6e1c774d86 fix: ui lazyyyyy 2026-05-08 12:30:27 +03:00
zarazaex69 8e9e709d12 fix: server unselect bug 2026-05-08 12:04:49 +03:00
zarazaex69 649f305a82 fix: server select bug 2026-05-08 12:04:22 +03:00
zarazaex69 44005dffd3 fix: button lock 2026-05-08 11:58:42 +03:00
zarazaex69 bee7002f54 fix: big bugfix 2026-05-08 11:56:45 +03:00
zarazaex69 0363ebaabd fix: olcNG -> olcng 2026-05-08 11:22:08 +03:00
zarazaex69 88627bbf4f fix: titile remarke search 2026-05-08 11:19:53 +03:00
zarazaex69 ceec94e5db fix: unselect 2026-05-08 11:17:07 +03:00
zarazaex69 b30fc13b0d fix: race cond errors 2026-05-07 17:13:24 +03:00
zarazaex69 a9f5844b84 style: change selected item styling to transparent bg with primary
border
2026-05-07 16:51:02 +03:00
zarazaex69 518edd096b upd design 2026-05-06 20:05:03 +03:00
zarazaex69 450542d3b8 add olcng text bage 2026-05-06 17:44:49 +03:00
zarazaex69 b775851960 remove тень 2026-05-06 17:34:31 +03:00
zarazaex69 4c52c1e45c fix bs wl 2026-05-06 17:31:28 +03:00
zarazaex69 e55ad93c52 fix icons BTW 2026-05-06 17:23:45 +03:00
zarazaex69 db931be24e fix upd text 2026-05-06 17:10:11 +03:00
zarazaex69 1f1d110a82 tutorial chge text 2026-05-06 16:52:47 +03:00
zarazaex69 2912d17aca rename select app to split tunelling 2026-05-06 16:50:08 +03:00
zarazaex69 261bd389f6 rename no connect to wait 2026-05-06 16:49:26 +03:00
zarazaex69 2c9eb2d8af add server unselect 2026-05-06 16:48:28 +03:00
zarazaex69 35a396ea0a add space to wl and bl 2026-05-06 16:45:21 +03:00
zarazaex69 0fa72f5ee1 upd color 2026-05-06 16:41:02 +03:00
33 changed files with 1742 additions and 990 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<item name="app_name" type="string">olcNG</item> <item name="app_name" type="string">olcng</item>
</resources> </resources>
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name" translatable="false">olcNG</string> <string name="app_name" translatable="false">olcng</string>
</resources> </resources>
@@ -74,6 +74,7 @@ object AppConfig {
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2" const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2"
const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test" const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
const val PREF_SHOW_COPY_BUTTON = "pref_show_copy_button" const val PREF_SHOW_COPY_BUTTON = "pref_show_copy_button"
const val PREF_SHOW_SERVER_IP = "pref_show_server_ip"
const val PREF_DYNAMIC_COLORS = "pref_dynamic_colors" const val PREF_DYNAMIC_COLORS = "pref_dynamic_colors"
const val PREF_SUBSCRIPTIONS_BOTTOM = "pref_subscriptions_bottom" const val PREF_SUBSCRIPTIONS_BOTTOM = "pref_subscriptions_bottom"
@@ -3,8 +3,8 @@ package xyz.zarazaex.olc.dto
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) { data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
fun getTestDelayString(): String { fun getTestDelayString(): String {
return when { return when {
testDelayMillis == 0L -> "" testDelayMillis == 0L -> "—ms"
testDelayMillis < 0L -> "Error" testDelayMillis < 0L -> "-ms"
else -> "${testDelayMillis}ms" else -> "${testDelayMillis}ms"
} }
} }
@@ -2,5 +2,7 @@ package xyz.zarazaex.olc.dto
data class ServersCache( data class ServersCache(
val guid: String, val guid: String,
val profile: ProfileItem val profile: ProfileItem,
val testDelayMillis: Long = 0L,
val isSelected: Boolean = false
) )
@@ -612,18 +612,19 @@ object AngConfigManager {
Log.i(AppConfig.TAG, url) Log.i(AppConfig.TAG, url)
val userAgent = it.subscription.userAgent val userAgent = it.subscription.userAgent
val timeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 6000 val proxyTimeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 5000
val directTimeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 11000
var configText = try { var configText = try {
val httpPort = SettingsManager.getHttpPort() val httpPort = SettingsManager.getHttpPort()
HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout, httpPort) HttpUtil.getUrlContentWithUserAgent(url, userAgent, proxyTimeout, httpPort)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e) Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
"" ""
} }
if (configText.isEmpty()) { if (configText.isEmpty()) {
configText = try { configText = try {
HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout) HttpUtil.getUrlContentWithUserAgent(url, userAgent, directTimeout)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e) Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
"" ""
@@ -700,9 +700,15 @@ object MmkvManager {
/** Persists ISO country code for [ip]. */ /** Persists ISO country code for [ip]. */
fun setCountryCache(ip: String, code: String) { countryCacheStorage.encode(ip, code) } fun setCountryCache(ip: String, code: String) { countryCacheStorage.encode(ip, code) }
/** Loads the user's country filter preference (set of ISO codes to SHOW, empty = show all). */ /** Loads the user's country filter preference (set of ISO codes to EXCLUDE, empty = show all). */
fun getCountryFilter(): Set<String> = fun getCountryFilter(): Set<String> {
settingsStorage.decodeStringSet("pref_country_filter") ?: emptySet() // v2: semantics changed from "included" to "excluded" — reset old data on first read
if (!settingsStorage.decodeBool("pref_country_filter_v2_migrated", false)) {
settingsStorage.removeValueForKey("pref_country_filter")
settingsStorage.encode("pref_country_filter_v2_migrated", true)
}
return settingsStorage.decodeStringSet("pref_country_filter") ?: emptySet()
}
/** Saves the user's country filter preference. */ /** Saves the user's country filter preference. */
fun setCountryFilter(codes: Set<String>) { fun setCountryFilter(codes: Set<String>) {
@@ -330,7 +330,7 @@ object V2RayServiceManager {
// Only fetch IP info if the delay test was successful // Only fetch IP info if the delay test was successful
if (time >= 0) { if (time >= 0) {
SpeedtestManager.getRemoteIPInfo()?.let { ip -> SpeedtestManager.getRemoteIPInfo()?.let { ip ->
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip") MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result $ip")
} }
} }
} }
@@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
import xyz.zarazaex.olc.AppConfig import xyz.zarazaex.olc.AppConfig
import xyz.zarazaex.olc.dto.PingProgressUpdate import xyz.zarazaex.olc.dto.PingProgressUpdate
import xyz.zarazaex.olc.dto.PingResultItem import xyz.zarazaex.olc.dto.PingResultItem
import xyz.zarazaex.olc.handler.MmkvManager
import xyz.zarazaex.olc.handler.SettingsManager import xyz.zarazaex.olc.handler.SettingsManager
import xyz.zarazaex.olc.handler.V2RayNativeManager import xyz.zarazaex.olc.handler.V2RayNativeManager
import xyz.zarazaex.olc.handler.V2rayConfigManager import xyz.zarazaex.olc.handler.V2rayConfigManager
@@ -57,8 +58,15 @@ class RealPingWorkerService(
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
// Prepare configurations in parallel for faster startup // Prepare configurations in parallel for faster startup.
val shuffledGuids = guids.shuffled() // Keep the currently selected server at the front so it gets a result first.
val selectedGuid = MmkvManager.getSelectServer()
val shuffledGuids = if (selectedGuid != null && guids.contains(selectedGuid)) {
val rest = guids.filter { it != selectedGuid }.shuffled()
listOf(selectedGuid) + rest
} else {
guids.shuffled()
}
val deferredItems = shuffledGuids.map { guid -> val deferredItems = shuffledGuids.map { guid ->
async(Dispatchers.IO) { async(Dispatchers.IO) {
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid) val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
@@ -123,6 +123,9 @@ abstract class BaseActivity : AppCompatActivity() {
setSupportActionBar(it) setSupportActionBar(it)
supportActionBar?.setDisplayHomeAsUpEnabled(showHomeAsUp) supportActionBar?.setDisplayHomeAsUpEnabled(showHomeAsUp)
title?.let { t -> this.title = t } title?.let { t -> this.title = t }
val typedValue = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorOnSurface, typedValue, true)
it.setTitleTextColor(typedValue.data)
syncStatusBarWithToolbar(it) syncStatusBarWithToolbar(it)
} }
progressBar = findViewById(R.id.progress_bar) progressBar = findViewById(R.id.progress_bar)
@@ -12,7 +12,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar
import xyz.zarazaex.olc.AppConfig import xyz.zarazaex.olc.AppConfig
import xyz.zarazaex.olc.R import xyz.zarazaex.olc.R
import xyz.zarazaex.olc.contracts.MainAdapterListener import xyz.zarazaex.olc.contracts.MainAdapterListener
@@ -65,7 +64,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
} else { } else {
binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 1) binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 1)
} }
addCustomDividerToRecyclerView(binding.recyclerView, R.drawable.custom_divider) addCustomDividerToRecyclerView(binding.recyclerView, R.drawable.server_list_divider)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter, allowSwipe = false)) itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter, allowSwipe = false))
@@ -76,19 +75,21 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
// // Set the distance to trigger sync to 160dp // // Set the distance to trigger sync to 160dp
// binding.refreshLayout.setDistanceToTriggerSync((160 * resources.displayMetrics.density).toInt()) // binding.refreshLayout.setDistanceToTriggerSync((160 * resources.displayMetrics.density).toInt())
mainViewModel.updateListAction.observe(viewLifecycleOwner) { index -> // Each fragment subscribes independently to the shared flow and filters its own subId.
if (mainViewModel.subscriptionId != subId) { // No onResume subscription switch needed — the active fragment's subId is always correct.
return@observe lifecycleScope.launch {
mainViewModel.serverListFlow.collect { list ->
if (mainViewModel.subscriptionId == subId) {
adapter.setData(list)
}
} }
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
adapter.setData(mainViewModel.serversCache, index)
} }
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Tell ViewModel which tab is active so it can rebuild the correct list.
// This is the only place subscriptionId changes — no more races.
mainViewModel.subscriptionIdChanged(subId) mainViewModel.subscriptionIdChanged(subId)
} }
@@ -219,7 +220,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
*/ */
private fun removeServerSub(guid: String, position: Int) { private fun removeServerSub(guid: String, position: Int) {
mainViewModel.removeServer(guid) mainViewModel.removeServer(guid)
adapter.removeServerSub(guid, position) // adapter updates automatically via serverListFlow
} }
/** /**
@@ -229,17 +230,17 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
*/ */
private fun setSelectServer(guid: String) { private fun setSelectServer(guid: String) {
val selected = MmkvManager.getSelectServer() val selected = MmkvManager.getSelectServer()
if (guid != selected) { if (guid == selected) {
MmkvManager.setSelectServer("")
} else {
MmkvManager.setSelectServer(guid) MmkvManager.setSelectServer(guid)
val fromPosition = mainViewModel.getPosition(selected.orEmpty()) }
val toPosition = mainViewModel.getPosition(guid) // Republish snapshot so DiffUtil picks up the selection change in card background
adapter.setSelectServer(fromPosition, toPosition) mainViewModel.reloadServerList()
if (mainViewModel.isRunning.value == true) { if (mainViewModel.isRunning.value == true) {
ownerActivity.restartV2Ray() ownerActivity.restartV2Ray()
} }
} }
}
private inner class ActivityAdapterListener : MainAdapterListener { private inner class ActivityAdapterListener : MainAdapterListener {
override fun onEdit(guid: String, position: Int) { override fun onEdit(guid: String, position: Int) {
@@ -304,9 +305,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
return return
} }
// Find the position of the selected server val position = mainViewModel.serverListFlow.value.indexOfFirst { it.guid == selectedGuid }
val serversCache = mainViewModel.serversCache
val position = serversCache.indexOfFirst { it.guid == selectedGuid }
val recyclerView = binding.recyclerView val recyclerView = binding.recyclerView
if (position >= 0) { if (position >= 0) {
@@ -2,6 +2,7 @@ package xyz.zarazaex.olc.ui
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.net.VpnService import android.net.VpnService
import android.os.Bundle import android.os.Bundle
@@ -11,6 +12,8 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.material.color.MaterialColors
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
@@ -53,6 +56,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
private var isLiteTesting = false private var isLiteTesting = false
private var easterEggClickCount = 0 private var easterEggClickCount = 0
private var isEasterEggActive = false private var isEasterEggActive = false
private var liteActionJob: kotlinx.coroutines.Job? = null
/** Был ли VPN уже запущен в предыдущем колбэке — чтобы детектировать момент подключения */ /** Был ли VPN уже запущен в предыдущем колбэке — чтобы детектировать момент подключения */
private var wasRunning = false private var wasRunning = false
@@ -107,21 +111,33 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
toggle.syncState() toggle.syncState()
binding.navView.setNavigationItemSelectedListener(this) binding.navView.setNavigationItemSelectedListener(this)
val typedValue = android.util.TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorOnSurface, typedValue, true)
val onSurface = typedValue.data
binding.toolbar.setTitleTextColor(onSurface)
// MaterialToolbar с titleCentered рисует отдельный TextView — красим его явно
for (i in 0 until binding.toolbar.childCount) {
val child = binding.toolbar.getChildAt(i)
if (child is android.widget.TextView) {
child.setTextColor(onSurface)
}
}
ViewCompat.setOnApplyWindowInsetsListener(binding.drawerContentLayout) { v, insets -> ViewCompat.setOnApplyWindowInsetsListener(binding.drawerContentLayout) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(0, systemBars.top, 0, systemBars.bottom) v.setPadding(0, systemBars.top, 0, systemBars.bottom)
insets insets
} }
findViewById<android.widget.TextView>(R.id.drawer_settings)?.setOnClickListener { findViewById<android.view.View>(R.id.drawer_settings)?.setOnClickListener {
requestActivityLauncher.launch(Intent(this, SettingsActivity::class.java)) requestActivityLauncher.launch(Intent(this, SettingsActivity::class.java))
binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START) binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START)
} }
findViewById<android.widget.TextView>(R.id.drawer_per_app)?.setOnClickListener { findViewById<android.view.View>(R.id.drawer_per_app)?.setOnClickListener {
requestActivityLauncher.launch(Intent(this, PerAppProxyActivity::class.java)) requestActivityLauncher.launch(Intent(this, PerAppProxyActivity::class.java))
binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START) binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START)
} }
findViewById<android.widget.TextView>(R.id.drawer_check_update)?.setOnClickListener { findViewById<android.view.View>(R.id.drawer_check_update)?.setOnClickListener {
startActivity(Intent(this, CheckUpdateActivity::class.java)) startActivity(Intent(this, CheckUpdateActivity::class.java))
binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START) binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START)
} }
@@ -200,40 +216,53 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
// Молния — стоп-кнопка, всегда активна во время теста // Молния — стоп-кнопка, всегда активна во время теста
binding.btnSummaryLite.isEnabled = true binding.btnSummaryLite.isEnabled = true
binding.btnSummaryLite.alpha = 1.0f binding.btnSummaryLite.alpha = 1.0f
binding.btnSummaryLite.setImageResource(R.drawable.ic_stop_24dp) binding.btnSummaryLite.setIconResource(R.drawable.ic_stop_24dp)
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active)) binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimaryContainer, 0)
)
} else { } else {
setButtonsEnabled(true) setButtonsEnabled(true)
binding.btnSummaryLite.setImageResource(R.drawable.bolt_24) binding.btnSummaryLite.setIconResource(R.drawable.bolt_24)
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive)) binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
)
if (!isLiteTesting) {
showStatus("Проверка завершена")
}
} }
} }
mainViewModel.liteTestFinished.observe(this) { finished -> mainViewModel.liteTestFinished.observe(this) { finished ->
if (finished && isLiteTesting) { if (finished && isLiteTesting) {
isLiteTesting = false isLiteTesting = false
mainViewModel.sortByTestResults()
val firstReachable = mainViewModel.serversCache.firstOrNull { cache -> // Ищем лучший сервер ДО сортировки, прямо из текущего cache
(MmkvManager.decodeServerAffiliationInfo(cache.guid)?.testDelayMillis ?: 0L) > 0L val firstReachable = mainViewModel.serversCache
} .filter { (MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: 0L) > 0L }
.minByOrNull { MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: Long.MAX_VALUE }
if (firstReachable != null) { if (firstReachable != null) {
MmkvManager.setSelectServer(firstReachable.guid) MmkvManager.setSelectServer(firstReachable.guid)
mainViewModel.reloadServerList() // reload AFTER selection so indicator renders correctly }
mainViewModel.suppressPinSelected = false
mainViewModel.sortByTestResults()
mainViewModel.reloadServerList()
if (firstReachable != null) {
showStatus("Подключаемся к быстрейшему серверу") showStatus("Подключаемся к быстрейшему серверу")
// Блокируем кнопки на время подключения
setButtonsEnabled(false)
applyRunningState(isLoading = true, isRunning = false) applyRunningState(isLoading = true, isRunning = false)
startV2RayWithPermission() startV2RayWithPermission()
} else { } else {
mainViewModel.reloadServerList()
showStatus("Нет доступных серверов!") showStatus("Нет доступных серверов!")
setButtonsEnabled(true) setButtonsEnabled(true)
} }
} }
} }
mainViewModel.isRunning.observe(this) { isRunning -> mainViewModel.isRunning.observe(this) { isRunning ->
if (!isFabOperationInProgress) {
applyRunningState(false, isRunning) applyRunningState(false, isRunning)
}
// Как только VPN только что подключился — обновляем подписки через него // Как только VPN только что подключился — обновляем подписки через него
if (isRunning && !wasRunning) { if (isRunning && !wasRunning) {
updateSubsViaVpn() updateSubsViaVpn()
@@ -299,15 +328,19 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
} }
private fun handleFabAction() { private fun handleFabAction() {
// Если идёт подключение (isLoading) — позволяем прервать и остановить сервис
if (isFabOperationInProgress) { if (isFabOperationInProgress) {
Log.d(AppConfig.TAG, "FAB: cancel in-progress, stopping service")
isFabOperationInProgress = false
lifecycleScope.launch {
V2RayServiceManager.stopVService(this@MainActivity)
}
return return
} }
isFabOperationInProgress = true isFabOperationInProgress = true
val isRunning = mainViewModel.isRunning.value == true val isRunning = mainViewModel.isRunning.value == true
// Блокируем все кнопки сразу
setButtonsEnabled(false)
applyRunningState(isLoading = true, isRunning = false) applyRunningState(isLoading = true, isRunning = false)
lifecycleScope.launch { lifecycleScope.launch {
@@ -338,11 +371,21 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
} }
private fun handleLiteAction() { private fun handleLiteAction() {
// If testing is in progress - stop it // Отмена на любом этапе: обновление подписок или тест
if (mainViewModel.isTesting.value == true) { if (mainViewModel.isTesting.value == true || liteActionJob?.isActive == true) {
liteActionJob?.cancel()
liteActionJob = null
mainViewModel.cancelAllTests() mainViewModel.cancelAllTests()
showStatus("Тест остановлен") mainViewModel.suppressPinSelected = false
isLiteTesting = false isLiteTesting = false
isFabOperationInProgress = false
showStatus("Остановлено")
setButtonsEnabled(true)
binding.btnSummaryLite.setIconResource(R.drawable.bolt_24)
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
)
hideLoading()
return return
} }
@@ -351,10 +394,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
} }
isFabOperationInProgress = true isFabOperationInProgress = true
// Блокируем все кнопки сразу при нажатии liteActionJob = lifecycleScope.launch {
setButtonsEnabled(false)
lifecycleScope.launch {
try { try {
if (mainViewModel.isRunning.value == true) { if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this@MainActivity) V2RayServiceManager.stopVService(this@MainActivity)
@@ -363,12 +403,22 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
showStatus("Обновление профилей...") showStatus("Обновление профилей...")
showLoading() showLoading()
// Иконка молнии → стоп пока идёт обновление; FAB и меню блокируем
binding.btnSummaryLite.setIconResource(R.drawable.ic_stop_24dp)
binding.btnSummaryLite.isEnabled = true
binding.btnSummaryLite.alpha = 1.0f
binding.fab.isEnabled = false
binding.fab.alpha = 0.5f
val menu = binding.toolbar.menu
menu.findItem(R.id.real_ping_all)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
menu.findItem(R.id.filter_by_country)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
menu.findItem(R.id.sub_update)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
isLiteTesting = true isLiteTesting = true
mainViewModel.suppressPinSelected = true
val result = withContext(Dispatchers.IO) { mainViewModel.updateConfigViaSubAll() }
val removed = withContext(Dispatchers.IO) { mainViewModel.removeDuplicateByIpAll() }
launch(Dispatchers.IO) {
val result = mainViewModel.updateConfigViaSubAll()
val removed = mainViewModel.removeDuplicateByIpAll()
withContext(Dispatchers.Main) {
mainViewModel.reloadServerList() mainViewModel.reloadServerList()
if (result.configCount > 0) { if (result.configCount > 0) {
val status = if (removed > 0) val status = if (removed > 0)
@@ -383,13 +433,15 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
showStatus("Выполняется замер задержки. Ожидаем завершения...") showStatus("Выполняется замер задержки. Ожидаем завершения...")
mainViewModel.testAllRealPing() mainViewModel.testAllRealPing()
} } catch (e: kotlinx.coroutines.CancellationException) {
} // Пользователь нажал стоп — уже обработано выше
delay(1500)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Error in handleLiteAction", e) Log.e(AppConfig.TAG, "Error in handleLiteAction", e)
isLiteTesting = false
hideLoading()
} finally { } finally {
isFabOperationInProgress = false isFabOperationInProgress = false
liteActionJob = null
} }
} }
} }
@@ -420,6 +472,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
return return
} }
isFabOperationInProgress = true isFabOperationInProgress = true
applyRunningState(isLoading = true, isRunning = false)
lifecycleScope.launch { lifecycleScope.launch {
try { try {
@@ -427,10 +480,16 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
V2RayServiceManager.stopVService(this@MainActivity) V2RayServiceManager.stopVService(this@MainActivity)
delay(1000) delay(1000)
} }
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
// Сервер был снят с выбора — просто остановились, разблокируем UI
applyRunningState(isLoading = false, isRunning = false)
return@launch
}
startV2Ray() startV2Ray()
delay(1000) delay(1000)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Error in restartV2Ray", e) Log.e(AppConfig.TAG, "Error in restartV2Ray", e)
applyRunningState(isLoading = false, isRunning = mainViewModel.isRunning.value == true)
} finally { } finally {
isFabOperationInProgress = false isFabOperationInProgress = false
} }
@@ -470,33 +529,79 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
} }
private fun applyRunningState(isLoading: Boolean, isRunning: Boolean) { 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)
)
if (isLoading) { if (isLoading) {
// Идёт процесс подключения/отключения — блокируем всё // Во время подключения: только FAB доступен для отмены, всё остальное заблокировано
setButtonsEnabled(false) binding.fab.isEnabled = true
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive)) binding.fab.alpha = 1.0f
binding.fab.backgroundTintList = secContainer
binding.btnSummaryLite.isEnabled = false
binding.btnSummaryLite.alpha = 0.5f
val menu = binding.toolbar.menu
menu.findItem(R.id.real_ping_all)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
menu.findItem(R.id.filter_by_country)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
menu.findItem(R.id.sub_update)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
setStatusDot(DotState.LOADING)
return return
} }
if (isRunning) { if (isRunning) {
// Подключены: только FAB (отключить) активен, остальное блокируем
setSecondaryButtonsEnabled(false) setSecondaryButtonsEnabled(false)
binding.fab.isEnabled = true binding.fab.isEnabled = true
binding.fab.alpha = 1.0f binding.fab.alpha = 1.0f
binding.fab.backgroundTintList = accentColor() binding.fab.backgroundTintList = accentColor()
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive)) binding.btnSummaryLite.backgroundTintList = secContainer
binding.fab.contentDescription = getString(R.string.action_stop_service) binding.fab.contentDescription = getString(R.string.action_stop_service)
setTestState(getString(R.string.connection_connected)) setTestState(getString(R.string.connection_connected))
binding.layoutTest.isFocusable = true binding.layoutTest.isFocusable = true
setStatusDot(DotState.CONNECTED)
} else { } else {
setButtonsEnabled(true) setButtonsEnabled(true)
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive)) binding.fab.backgroundTintList = accentColor()
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive)) binding.btnSummaryLite.backgroundTintList = secContainer
binding.fab.contentDescription = getString(R.string.tasker_start_service) binding.fab.contentDescription = getString(R.string.tasker_start_service)
setTestState(getString(R.string.connection_not_connected)) setTestState(getString(R.string.connection_not_connected))
binding.layoutTest.isFocusable = false binding.layoutTest.isFocusable = false
setStatusDot(DotState.IDLE)
} }
} }
private enum class DotState { IDLE, CONNECTED, LOADING }
private fun setStatusDot(state: DotState) {
val dot = binding.statusDot
dot.animate().cancel()
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.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)
})
if (state == DotState.LOADING) {
pulseDot(dot)
}
}
private fun pulseDot(dot: android.view.View) {
dot.animate()
.alpha(0.25f)
.setDuration(600)
.withEndAction {
if (dot.isAttachedToWindow) {
dot.animate()
.alpha(1f)
.setDuration(600)
.withEndAction {
if (dot.isAttachedToWindow && mainViewModel.isTesting.value == true) {
pulseDot(dot)
}
}.start()
}
}.start()
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "") MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
@@ -509,6 +614,13 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu) menuInflater.inflate(R.menu.menu_main, menu)
val iconColor = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface, Color.BLACK)
for (i in 0 until menu.size()) {
menu.getItem(i).icon?.let {
DrawableCompat.setTint(DrawableCompat.wrap(it).mutate(), iconColor)
}
}
val searchItem = menu.findItem(R.id.search_view) val searchItem = menu.findItem(R.id.search_view)
val searchView = searchItem.actionView as SearchView val searchView = searchItem.actionView as SearchView
@@ -525,6 +637,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
searchView.alpha = 0f
searchView.animate().alpha(1f).setDuration(220).start()
return true return true
} }
@@ -547,7 +661,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
} }
R.id.sub_update -> { R.id.sub_update -> {
setButtonsEnabled(false) setSecondaryButtonsEnabled(false)
importConfigViaSub() importConfigViaSub()
true true
} }
@@ -672,6 +786,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
private fun importAllSubsOnStartup() { private fun importAllSubsOnStartup() {
showLoading() showLoading()
setTestState(getString(R.string.connection_updating_profiles))
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val result = AngConfigManager.updateConfigViaSubAll() val result = AngConfigManager.updateConfigViaSubAll()
val removed = mainViewModel.removeDuplicateByIpAll() val removed = mainViewModel.removeDuplicateByIpAll()
@@ -694,6 +809,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
*/ */
fun importConfigViaSub(): Boolean { fun importConfigViaSub(): Boolean {
showLoading() showLoading()
setTestState(getString(R.string.connection_updating_profiles))
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val result = mainViewModel.updateConfigViaSubAll() val result = mainViewModel.updateConfigViaSubAll()
@@ -899,14 +1015,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
val codes = allCountriesMap.keys.toTypedArray() val codes = allCountriesMap.keys.toTypedArray()
val labels = allCountriesMap.values.toTypedArray() val labels = allCountriesMap.values.toTypedArray()
// In exclude mode: checked = should be EXCLUDED // currentFilter stores excluded set (empty = show all)
// currentFilter stores included set (empty = show all) val checked = BooleanArray(codes.size) { codes[it] in currentFilter }
// Convert to excluded set for UI
val allCodes = codes.toSet()
val excludedByFilter = if (currentFilter.isEmpty()) emptySet()
else allCodes - currentFilter
val checked = BooleanArray(codes.size) { codes[it] in excludedByFilter }
val dialog = MaterialAlertDialogBuilder(this@MainActivity) val dialog = MaterialAlertDialogBuilder(this@MainActivity)
.setTitle("Исключить страны") .setTitle("Исключить страны")
@@ -915,9 +1025,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
} }
.setPositiveButton("Применить") { _, _ -> .setPositiveButton("Применить") { _, _ ->
val excluded = codes.filterIndexed { i, _ -> checked[i] }.toSet() val excluded = codes.filterIndexed { i, _ -> checked[i] }.toSet()
val included = if (excluded.isEmpty()) emptySet() mainViewModel.applyCountryFilter(excluded)
else allCodes - excluded
mainViewModel.applyCountryFilter(included)
val msg = if (excluded.isEmpty()) "Показаны все страны" val msg = if (excluded.isEmpty()) "Показаны все страны"
else "Скрыто: ${excluded.joinToString { CountryDetector.codeToFlag(it) }}" else "Скрыто: ${excluded.joinToString { CountryDetector.codeToFlag(it) }}"
showStatus(msg) showStatus(msg)
@@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import java.util.Collections import java.util.Collections
import xyz.zarazaex.olc.AppConfig import xyz.zarazaex.olc.AppConfig
import xyz.zarazaex.olc.R import xyz.zarazaex.olc.R
@@ -35,7 +36,11 @@ class MainRecyclerAdapter(
MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false) MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
private val showCopyButton = private val showCopyButton =
MmkvManager.decodeSettingsBool(AppConfig.PREF_SHOW_COPY_BUTTON, false) MmkvManager.decodeSettingsBool(AppConfig.PREF_SHOW_COPY_BUTTON, false)
private val showServerIp =
MmkvManager.decodeSettingsBool(AppConfig.PREF_SHOW_SERVER_IP, false)
private var data: MutableList<ServersCache> = mutableListOf() private var data: MutableList<ServersCache> = mutableListOf()
private var minReachablePing: Long? = null
private var maxReachablePing: Long? = null
private var recyclerView: RecyclerView? = null private var recyclerView: RecyclerView? = null
override fun onAttachedToRecyclerView(rv: RecyclerView) { override fun onAttachedToRecyclerView(rv: RecyclerView) {
@@ -54,45 +59,37 @@ class MainRecyclerAdapter(
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun setData(newData: MutableList<ServersCache>?, position: Int = -1) { fun setData(newData: List<ServersCache>) {
val parsedNewData = newData?.toList() ?: emptyList() val oldData = data
val parsedNewData = newData
if (data.isEmpty() || parsedNewData.isEmpty() || position >= 0) { if (oldData.isEmpty() || parsedNewData.isEmpty()) {
data = parsedNewData.toMutableList() data = parsedNewData.toMutableList()
if (position >= 0 && position in data.indices) { recomputePingRange()
notifyItemChanged(position)
} else {
notifyDataSetChanged() notifyDataSetChanged()
}
return return
} }
val oldData = data
val lm = recyclerView?.layoutManager as? androidx.recyclerview.widget.LinearLayoutManager val lm = recyclerView?.layoutManager as? androidx.recyclerview.widget.LinearLayoutManager
val firstVisible = lm?.findFirstVisibleItemPosition()?.coerceAtLeast(0) ?: 0 val firstVisible = lm?.findFirstVisibleItemPosition()?.coerceAtLeast(0) ?: 0
val isAtTop = firstVisible == 0 && (lm?.findViewByPosition(0)?.top ?: 0) >= 0 val isAtTop = firstVisible == 0 && (lm?.findViewByPosition(0)?.top ?: 0) >= 0
val firstVisibleGuid = if (!isAtTop) oldData.getOrNull(firstVisible)?.guid else null val firstVisibleGuid = if (!isAtTop) oldData.getOrNull(firstVisible)?.guid else null
val diffResult = val diffResult = androidx.recyclerview.widget.DiffUtil.calculateDiff(
androidx.recyclerview.widget.DiffUtil.calculateDiff(
object : androidx.recyclerview.widget.DiffUtil.Callback() { object : androidx.recyclerview.widget.DiffUtil.Callback() {
override fun getOldListSize() = oldData.size override fun getOldListSize() = oldData.size
override fun getNewListSize() = parsedNewData.size override fun getNewListSize() = parsedNewData.size
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean { override fun areItemsTheSame(oldPos: Int, newPos: Int) =
return oldData[oldPos].guid == parsedNewData[newPos].guid oldData[oldPos].guid == parsedNewData[newPos].guid
}
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean { override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
val oldProfile = oldData[oldPos].profile val old = oldData[oldPos]
val newProfile = parsedNewData[newPos].profile val new = parsedNewData[newPos]
val oldGuid = oldData[oldPos].guid return old.profile == new.profile &&
val newGuid = parsedNewData[newPos].guid old.profile.isFavorite == new.profile.isFavorite &&
return oldProfile == newProfile && old.isSelected == new.isSelected &&
oldProfile.isFavorite == newProfile.isFavorite && old.testDelayMillis == new.testDelayMillis
(oldGuid == MmkvManager.getSelectServer()) == (newGuid == MmkvManager.getSelectServer()) &&
MmkvManager.decodeServerAffiliationInfo(oldGuid)?.testDelayMillis ==
MmkvManager.decodeServerAffiliationInfo(newGuid)?.testDelayMillis
} }
override fun getChangePayload(oldPos: Int, newPos: Int): Any? { override fun getChangePayload(oldPos: Int, newPos: Int): Any? {
@@ -106,6 +103,7 @@ class MainRecyclerAdapter(
) )
data = parsedNewData.toMutableList() data = parsedNewData.toMutableList()
recomputePingRange()
diffResult.dispatchUpdatesTo(this) diffResult.dispatchUpdatesTo(this)
if (isAtTop) { if (isAtTop) {
@@ -162,26 +160,31 @@ class MainRecyclerAdapter(
// Name address // Name address
holder.itemMainBinding.tvName.text = profile.remarks holder.itemMainBinding.tvName.text = profile.remarks
holder.itemMainBinding.tvStatistics.text = getAddress(profile) val addressText = getAddress(profile)
holder.itemMainBinding.tvStatistics.text = addressText
holder.itemMainBinding.tvStatistics.visibility =
if (addressText.isEmpty()) View.GONE else View.VISIBLE
// TestResult // TestResult
val aff = MmkvManager.decodeServerAffiliationInfo(guid) val aff = MmkvManager.decodeServerAffiliationInfo(guid)
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty() holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
if ((aff?.testDelayMillis ?: 0L) < 0L) {
holder.itemMainBinding.tvTestResult.setTextColor( holder.itemMainBinding.tvTestResult.setTextColor(
ContextCompat.getColor(context, R.color.colorPingRed) getPingColor(context, aff?.testDelayMillis)
) )
} else { (holder.itemMainBinding.tvTestResult.layoutParams as? ViewGroup.MarginLayoutParams)?.marginStart =
holder.itemMainBinding.tvTestResult.setTextColor( if (addressText.isEmpty()) 0 else 6.dpToPx(context)
ContextCompat.getColor(context, R.color.colorPing)
)
}
// layoutIndicator val isSelected = data[position].isSelected
if (guid == MmkvManager.getSelectServer()) { holder.itemMainBinding.cardContainer.apply {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorIndicator) val selectedColor = MaterialColors.getColor(
} else { context,
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0) com.google.android.material.R.attr.colorSurfaceContainerHigh,
Color.TRANSPARENT
)
setCardBackgroundColor(if (isSelected) selectedColor else Color.TRANSPARENT)
strokeWidth = 0
strokeColor = Color.TRANSPARENT
} }
// subscription remarks // subscription remarks
@@ -223,9 +226,52 @@ class MainRecyclerAdapter(
* @return Formatted address string * @return Formatted address string
*/ */
private fun getAddress(profile: ProfileItem): String { private fun getAddress(profile: ProfileItem): String {
if (!showServerIp) {
return ""
}
return AngConfigManager.generateDescription(profile) return AngConfigManager.generateDescription(profile)
} }
private fun getPingColor(context: android.content.Context, delayMillis: Long?): Int {
val delay = delayMillis ?: return MaterialColors.getColor(
context,
com.google.android.material.R.attr.colorOnSurfaceVariant,
ContextCompat.getColor(context, R.color.colorPing)
)
if (delay == 0L) {
return MaterialColors.getColor(
context,
com.google.android.material.R.attr.colorOnSurfaceVariant,
ContextCompat.getColor(context, R.color.colorPing)
)
}
return when {
delay < 0L -> ContextCompat.getColor(context, R.color.colorPingRed)
minReachablePing == null || maxReachablePing == null -> ContextCompat.getColor(context, R.color.colorPingGood)
minReachablePing == maxReachablePing -> ContextCompat.getColor(context, R.color.colorPingGood)
else -> {
val min = minReachablePing ?: delay
val max = maxReachablePing ?: delay
val relative = ((delay - min).toFloat() / (max - min).toFloat()).coerceIn(0f, 1f)
when {
relative <= 0.33f -> ContextCompat.getColor(context, R.color.colorPingGood)
relative <= 0.66f -> ContextCompat.getColor(context, R.color.colorPingMedium)
else -> ContextCompat.getColor(context, R.color.colorPingRed)
}
}
}
}
private fun recomputePingRange() {
val delays = data.mapNotNull { item ->
MmkvManager.decodeServerAffiliationInfo(item.guid)
?.testDelayMillis
?.takeIf { it > 0L }
}
minReachablePing = delays.minOrNull()
maxReachablePing = delays.maxOrNull()
}
/** /**
* Gets the subscription remarks information * Gets the subscription remarks information
* @param profile The server configuration * @param profile The server configuration
@@ -241,18 +287,8 @@ class MainRecyclerAdapter(
return subRemarks?.toString() ?: "" return subRemarks?.toString() ?: ""
} }
fun removeServerSub(guid: String, position: Int) { private fun Int.dpToPx(context: android.content.Context): Int {
val idx = data.indexOfFirst { it.guid == guid } return (this * context.resources.displayMetrics.density).toInt()
if (idx >= 0) {
data.removeAt(idx)
notifyItemRemoved(idx)
notifyItemRangeChanged(idx, data.size - idx)
}
}
fun setSelectServer(fromPosition: Int, toPosition: Int) {
notifyItemChanged(fromPosition)
notifyItemChanged(toPosition)
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
@@ -301,6 +337,8 @@ class MainRecyclerAdapter(
BaseViewHolder(itemFooterBinding.root) BaseViewHolder(itemFooterBinding.root)
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
// ViewModel swaps both serverList and _serversCache, then publishSnapshot triggers setData.
// We optimistically swap local data + animate immediately for smooth drag UX.
mainViewModel.swapServer(fromPosition, toPosition) mainViewModel.swapServer(fromPosition, toPosition)
if (fromPosition < data.size && toPosition < data.size) { if (fromPosition < data.size && toPosition < data.size) {
Collections.swap(data, fromPosition, toPosition) Collections.swap(data, fromPosition, toPosition)
@@ -33,6 +33,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.Collections import java.util.Collections
@@ -42,15 +45,24 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
private var serverList = mutableListOf<String>() private var serverList = mutableListOf<String>()
var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty() var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
var keywordFilter = "" var keywordFilter = ""
/** ISO codes to show (empty = show all) */ /** ISO codes to EXCLUDE (empty = show all) */
var countryFilter: Set<String> = MmkvManager.getCountryFilter() var countryFilter: Set<String> = MmkvManager.getCountryFilter()
private set private set
val serversCache = mutableListOf<ServersCache>()
// Internal mutable cache — never exposed directly
private val _serversCache = mutableListOf<ServersCache>()
// Read-only snapshot for external consumers that need direct access (e.g. export, ping)
val serversCache: List<ServersCache> get() = _serversCache.toList()
// Single source of truth for the list UI — emits a new immutable snapshot on every change
private val _serverListFlow = MutableStateFlow<List<ServersCache>>(emptyList())
val serverListFlow: StateFlow<List<ServersCache>> = _serverListFlow.asStateFlow()
val isRunning by lazy { MutableLiveData<Boolean>() } val isRunning by lazy { MutableLiveData<Boolean>() }
val updateListAction by lazy { MutableLiveData<Int>() }
val updateTestResultAction by lazy { MutableLiveData<String>() } val updateTestResultAction by lazy { MutableLiveData<String>() }
val liteTestFinished = MutableLiveData<Boolean>() val liteTestFinished = MutableLiveData<Boolean>()
val isTesting by lazy { MutableLiveData<Boolean>().also { it.value = false } } val isTesting by lazy { MutableLiveData<Boolean>().also { it.value = false } }
var suppressPinSelected = false
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) } private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
/** /**
@@ -120,8 +132,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
list list
} }
if (!suppressPinSelected) pinSelectedGuidToTop(serverList)
updateCache() updateCache()
updateListAction.value = -1
} }
/** /**
@@ -131,10 +143,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun removeServer(guid: String) { fun removeServer(guid: String) {
serverList.remove(guid) serverList.remove(guid)
MmkvManager.removeServer(guid) MmkvManager.removeServer(guid)
val index = getPosition(guid) val index = _serversCache.indexOfFirst { it.guid == guid }
if (index >= 0) { if (index >= 0) {
serversCache.removeAt(index) _serversCache.removeAt(index)
} }
publishSnapshot()
} }
/** /**
@@ -148,17 +161,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
Collections.swap(serverList, fromPosition, toPosition) Collections.swap(serverList, fromPosition, toPosition)
Collections.swap(serversCache, fromPosition, toPosition) Collections.swap(_serversCache, fromPosition, toPosition)
publishSnapshot()
MmkvManager.encodeServerList(serverList, subscriptionId) MmkvManager.encodeServerList(serverList, subscriptionId)
} }
/** /**
* Updates the cache of servers. * Rebuilds _serversCache from serverList and publishes a new snapshot to serverListFlow.
*/ */
@Synchronized @Synchronized
fun updateCache() { fun updateCache() {
serversCache.clear() _serversCache.clear()
val kw = keywordFilter.trim() val kw = keywordFilter.trim()
val searchRegex = try { val searchRegex = try {
if (kw.isNotEmpty()) Regex(kw, setOf(RegexOption.IGNORE_CASE)) else null if (kw.isNotEmpty()) Regex(kw, setOf(RegexOption.IGNORE_CASE)) else null
@@ -166,17 +180,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
null null
} }
val activeCountryFilter = countryFilter val activeCountryFilter = countryFilter
val selectedGuid = MmkvManager.getSelectServer().orEmpty()
for (guid in serverList) { for (guid in serverList) {
val profile = MmkvManager.decodeServerConfig(guid) ?: continue val profile = MmkvManager.decodeServerConfig(guid) ?: continue
// Country filter
if (activeCountryFilter.isNotEmpty()) { if (activeCountryFilter.isNotEmpty()) {
val code = CountryDetector.getCountryCode(profile.remarks, profile.server) val code = CountryDetector.getCountryCode(profile.remarks, profile.server)
if (code !in activeCountryFilter) continue if (code in activeCountryFilter) continue
} }
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
if (kw.isEmpty()) { if (kw.isEmpty()) {
serversCache.add(ServersCache(guid, profile)) _serversCache.add(ServersCache(guid, profile, delay, guid == selectedGuid))
continue continue
} }
@@ -189,15 +205,49 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|| server.matchesPattern(searchRegex, kw) || server.matchesPattern(searchRegex, kw)
|| protocol.matchesPattern(searchRegex, kw) || protocol.matchesPattern(searchRegex, kw)
) { ) {
serversCache.add(ServersCache(guid, profile)) _serversCache.add(ServersCache(guid, profile, delay, guid == selectedGuid))
} }
} }
publishSnapshot()
} }
/** Sets a new country filter and reloads list. Pass empty set to show all. */ /** Emits an immutable copy of _serversCache to the Flow. Must be called on Main or from @Synchronized blocks. */
fun applyCountryFilter(codes: Set<String>) { private fun publishSnapshot() {
countryFilter = codes _serverListFlow.value = _serversCache.toList()
MmkvManager.setCountryFilter(codes) }
/** Builds a snapshot of ServersCache for the given subId without changing global state. */
fun reloadForSub(subId: String): MutableList<ServersCache>? {
val guids = when {
subId.isEmpty() -> MmkvManager.decodeAllServerList()
subId.startsWith("group_") -> {
val allSubs = MmkvManager.decodeSubscriptions()
val groupSubs = when (subId) {
"group_white" -> allSubs.filter {
it.subscription.remarks.startsWith("БЕЛЫЕ", ignoreCase = true) ||
it.subscription.remarks.startsWith("WHITE", ignoreCase = true)
}
"group_black" -> allSubs.filter {
it.subscription.remarks.startsWith("ЧЕРНЫЕ", ignoreCase = true) ||
it.subscription.remarks.startsWith("BLACK", ignoreCase = true)
}
else -> emptyList()
}
groupSubs.flatMap { MmkvManager.decodeServerList(it.guid) }.toMutableList()
}
else -> MmkvManager.decodeServerList(subId)
}
return guids.mapNotNull { guid ->
val profile = MmkvManager.decodeServerConfig(guid) ?: return@mapNotNull null
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
ServersCache(guid, profile, delay)
}.toMutableList()
}
/** Sets excluded countries and reloads list. Pass empty set to show all. */
fun applyCountryFilter(excludedCodes: Set<String>) {
countryFilter = excludedCodes
MmkvManager.setCountryFilter(excludedCodes)
reloadServerList() reloadServerList()
} }
@@ -273,9 +323,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun exportAllServer(): Int { fun exportAllServer(): Int {
val serverListCopy = val serverListCopy =
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) { if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
serverList serverList.toList()
} else { } else {
serversCache.map { it.guid }.toList() _serversCache.map { it.guid }
} }
val ret = AngConfigManager.shareNonCustomConfigsToClipboard( val ret = AngConfigManager.shareNonCustomConfigsToClipboard(
@@ -291,9 +341,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun testAllTcping() { fun testAllTcping() {
tcpingTestScope.coroutineContext[Job]?.cancelChildren() tcpingTestScope.coroutineContext[Job]?.cancelChildren()
SpeedtestManager.closeAllTcpSockets() SpeedtestManager.closeAllTcpSockets()
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) MmkvManager.clearAllTestDelayResults(_serversCache.map { it.guid })
val serversCopy = serversCache.toList() val serversCopy = _serversCache.toList()
for (item in serversCopy) { for (item in serversCopy) {
item.profile.let { outbound -> item.profile.let { outbound ->
val serverAddress = outbound.server val serverAddress = outbound.server
@@ -303,7 +353,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt()) val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt())
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult) MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
updateListAction.value = getPosition(item.guid) refreshPingInCache(listOf(item.guid))
} }
} }
} }
@@ -338,15 +388,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (removed > 0) { if (removed > 0) {
reloadServerList() reloadServerList()
} }
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) if (!suppressPinSelected) {
updateListAction.value = -1 MmkvManager.clearAllTestDelayResults(_serversCache.map { it.guid })
}
publishSnapshot()
isTesting.value = true isTesting.value = true
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
if (serversCache.isEmpty()) { if (_serversCache.isEmpty()) {
withContext(Dispatchers.Main) { reloadServerList() } withContext(Dispatchers.Main) { reloadServerList() }
} }
if (serversCache.isEmpty()) { if (_serversCache.isEmpty()) {
withContext(Dispatchers.Main) { isTesting.value = false } withContext(Dispatchers.Main) { isTesting.value = false }
return@launch return@launch
} }
@@ -359,7 +411,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
key = AppConfig.MSG_MEASURE_CONFIG, key = AppConfig.MSG_MEASURE_CONFIG,
subscriptionId = actualSubId, subscriptionId = actualSubId,
serverGuids = if (keywordFilter.isNotEmpty() || subscriptionId.startsWith("group_")) { serverGuids = if (keywordFilter.isNotEmpty() || subscriptionId.startsWith("group_")) {
serversCache.map { it.guid } _serversCache.map { it.guid }
} else { } else {
emptyList() emptyList()
} }
@@ -471,9 +523,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
* @return The position of the server. * @return The position of the server.
*/ */
fun getPosition(guid: String): Int { fun getPosition(guid: String): Int {
serversCache.forEachIndexed { index, it -> _serversCache.forEachIndexed { index, it ->
if (it.guid == guid) if (it.guid == guid) return index
return index
} }
return -1 return -1
} }
@@ -483,7 +534,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
* @return The number of removed servers. * @return The number of removed servers.
*/ */
fun removeDuplicateServer(): Int { fun removeDuplicateServer(): Int {
val serversCacheCopy = serversCache.toList().toMutableList() val serversCacheCopy = _serversCache.toList()
val deleteServer = mutableListOf<String>() val deleteServer = mutableListOf<String>()
serversCacheCopy.forEachIndexed { index, sc -> serversCacheCopy.forEachIndexed { index, sc ->
val profile = sc.profile val profile = sc.profile
@@ -510,9 +561,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
*/ */
fun removeDuplicateByIp(): Int { fun removeDuplicateByIp(): Int {
val selectedGuid = MmkvManager.getSelectServer() val selectedGuid = MmkvManager.getSelectServer()
// Group all currently visible servers by their IP address
val byIp = LinkedHashMap<String, MutableList<ServersCache>>() val byIp = LinkedHashMap<String, MutableList<ServersCache>>()
for (sc in serversCache) { for (sc in _serversCache) {
val ip = sc.profile.server?.trim()?.lowercase() ?: continue val ip = sc.profile.server?.trim()?.lowercase() ?: continue
byIp.getOrPut(ip) { mutableListOf() }.add(sc) byIp.getOrPut(ip) { mutableListOf() }.add(sc)
} }
@@ -609,11 +659,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) { if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
MmkvManager.removeAllServer() MmkvManager.removeAllServer()
} else { } else {
val serversCopy = serversCache.toList() val serversCopy = _serversCache.toList()
for (item in serversCopy) { for (item in serversCopy) {
MmkvManager.removeServer(item.guid) MmkvManager.removeServer(item.guid)
} }
serversCache.toList().count() serversCopy.count()
} }
return count return count
} }
@@ -625,19 +675,43 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
* Sorts serversCache in-place by test delay in real time (during a ping test). * Sorts serversCache in-place by test delay in real time (during a ping test).
* Favorites always come first, then sorted ascending by delay (failed/untested go to bottom). * Favorites always come first, then sorted ascending by delay (failed/untested go to bottom).
*/ */
@Synchronized
fun refreshPingInCache(guids: List<String>) {
val guidSet = guids.toHashSet()
for (i in _serversCache.indices) {
val item = _serversCache[i]
if (item.guid in guidSet) {
val delay = MmkvManager.decodeServerAffiliationInfo(item.guid)?.testDelayMillis ?: 0L
if (item.testDelayMillis != delay) {
_serversCache[i] = item.copy(testDelayMillis = delay)
}
}
}
publishSnapshot()
}
@Synchronized @Synchronized
fun sortServersCacheInPlace() { fun sortServersCacheInPlace() {
serversCache.sortWith(compareBy( for (i in _serversCache.indices) {
val item = _serversCache[i]
val delay = MmkvManager.decodeServerAffiliationInfo(item.guid)?.testDelayMillis ?: 0L
if (item.testDelayMillis != delay) {
_serversCache[i] = item.copy(testDelayMillis = delay)
}
}
_serversCache.sortWith(compareBy(
{ !it.profile.isFavorite }, { !it.profile.isFavorite },
{ {
val delay = MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: 0L val delay = it.testDelayMillis
when { when {
delay > 0L -> delay delay > 0L -> delay
delay == 0L -> Long.MAX_VALUE - 1 // untested delay == 0L -> Long.MAX_VALUE - 1
else -> Long.MAX_VALUE // failed else -> Long.MAX_VALUE
} }
} }
)) ))
if (!suppressPinSelected) pinSelectedCacheItemToTop(_serversCache)
publishSnapshot()
} }
fun sortByTestResults() { fun sortByTestResults() {
@@ -689,6 +763,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val serversBySubId = allServerDelays.groupBy { it.subId } val serversBySubId = allServerDelays.groupBy { it.subId }
serversBySubId.forEach { (subId, servers) -> serversBySubId.forEach { (subId, servers) ->
val sortedList = servers.map { it.guid }.toMutableList() val sortedList = servers.map { it.guid }.toMutableList()
pinSelectedGuidToTop(sortedList)
MmkvManager.encodeServerList(sortedList, subId) MmkvManager.encodeServerList(sortedList, subId)
} }
} }
@@ -716,11 +791,32 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
serverDelays.sortWith(compareBy({ !it.isFav }, { it.testDelayMillis })) serverDelays.sortWith(compareBy({ !it.isFav }, { it.testDelayMillis }))
val sortedServerList = serverDelays.map { it.guid }.toMutableList() val sortedServerList = serverDelays.map { it.guid }.toMutableList()
pinSelectedGuidToTop(sortedServerList)
// Save the sorted list for this subscription // Save the sorted list for this subscription
MmkvManager.encodeServerList(sortedServerList, subId) MmkvManager.encodeServerList(sortedServerList, subId)
} }
private fun pinSelectedGuidToTop(list: MutableList<String>) {
val selectedGuid = MmkvManager.getSelectServer().orEmpty()
if (selectedGuid.isEmpty()) return
val index = list.indexOf(selectedGuid)
if (index > 0) {
list.removeAt(index)
list.add(0, selectedGuid)
}
}
private fun pinSelectedCacheItemToTop(list: MutableList<ServersCache>) {
val selectedGuid = MmkvManager.getSelectServer().orEmpty()
if (selectedGuid.isEmpty()) return
val index = list.indexOfFirst { it.guid == selectedGuid }
if (index > 0) {
val selectedItem = list.removeAt(index)
list.add(0, selectedItem)
}
}
/** /**
* Initializes assets. * Initializes assets.
@@ -772,7 +868,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
reloadServerList() reloadServerList() // rebuilds _serversCache + publishSnapshot
isTesting.value = false isTesting.value = false
liteTestFinished.value = true liteTestFinished.value = true
liteTestFinished.value = false liteTestFinished.value = false
@@ -810,8 +906,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> { AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second) MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
sortServersCacheInPlace() refreshPingInCache(listOf(resultPair.first))
updateListAction.value = -1 if (!suppressPinSelected) sortServersCacheInPlace()
// publishSnapshot() already called inside refresh/sort above
} }
AppConfig.MSG_MEASURE_CONFIG_BATCH -> { AppConfig.MSG_MEASURE_CONFIG_BATCH -> {
@@ -819,8 +916,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
update.results.forEach { result -> update.results.forEach { result ->
MmkvManager.encodeServerTestDelayMillis(result.guid, result.delay) MmkvManager.encodeServerTestDelayMillis(result.guid, result.delay)
} }
sortServersCacheInPlace() refreshPingInCache(update.results.map { it.guid })
updateListAction.value = -1 if (!suppressPinSelected) sortServersCacheInPlace()
// publishSnapshot() already called inside refresh/sort above
} }
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> { AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {
@@ -1,7 +1,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval"> android:shape="oval">
<solid android:color="@color/divider_color_light" /> <solid android:color="?attr/colorSecondaryContainer" />
<size <size
android:width="48dp" android:width="24dp"
android:height="48dp" /> android:height="24dp" />
</shape> </shape>
@@ -0,0 +1,12 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="0dp"
android:left="28dp"
android:right="28dp"
android:top="0dp">
<shape>
<size android:height="1dp" />
<solid android:color="@color/server_list_divider" />
</shape>
</item>
</layer-list>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#FF888888" />
<size android:width="10dp" android:height="10dp" />
</shape>
@@ -1,13 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto" <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -24,22 +35,30 @@
android:focusable="true" android:focusable="true"
android:gravity="center|start" android:gravity="center|start"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp16"> android:padding="16dp">
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="24dp"
android:layout_height="@dimen/image_size_dp24" android:layout_height="24dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_source_code_24dp" /> app:srcCompat="@drawable/ic_source_code_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_spacing_dp16" android:paddingStart="16dp"
android:text="@string/title_source_code" android:text="@string/title_source_code"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout> </LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?attr/colorOutlineVariant" />
<LinearLayout <LinearLayout
android:id="@+id/layout_oss_licenses" android:id="@+id/layout_oss_licenses"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -49,22 +68,30 @@
android:focusable="true" android:focusable="true"
android:gravity="center|start" android:gravity="center|start"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp16"> android:padding="16dp">
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="24dp"
android:layout_height="@dimen/image_size_dp24" android:layout_height="24dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:srcCompat="@drawable/license_24px" /> app:srcCompat="@drawable/license_24px"
app:tint="?attr/colorOnSurfaceVariant" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_spacing_dp16" android:paddingStart="16dp"
android:text="@string/title_oss_license" android:text="@string/title_oss_license"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout> </LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?attr/colorOutlineVariant" />
<LinearLayout <LinearLayout
android:id="@+id/layout_feedback" android:id="@+id/layout_feedback"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -74,22 +101,29 @@
android:focusable="true" android:focusable="true"
android:gravity="center|start" android:gravity="center|start"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp16"> android:padding="16dp">
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="24dp"
android:layout_height="@dimen/image_size_dp24" android:layout_height="24dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_feedback_24dp" /> app:srcCompat="@drawable/ic_feedback_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_spacing_dp16" android:paddingStart="16dp"
android:text="@string/title_pref_feedback" android:text="@string/title_pref_feedback"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout> </LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?attr/colorOutlineVariant" />
<LinearLayout <LinearLayout
android:id="@+id/layout_tg_channel" android:id="@+id/layout_tg_channel"
@@ -100,22 +134,30 @@
android:focusable="true" android:focusable="true"
android:gravity="center|start" android:gravity="center|start"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp16"> android:padding="16dp">
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="24dp"
android:layout_height="@dimen/image_size_dp24" android:layout_height="24dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_telegram_24dp" /> app:srcCompat="@drawable/ic_telegram_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_spacing_dp16" android:paddingStart="16dp"
android:text="@string/title_tg_channel" android:text="@string/title_tg_channel"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout> </LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?attr/colorOutlineVariant" />
<LinearLayout <LinearLayout
android:id="@+id/layout_privacy_policy" android:id="@+id/layout_privacy_policy"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -125,47 +167,62 @@
android:focusable="true" android:focusable="true"
android:gravity="center|start" android:gravity="center|start"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp16"> android:padding="16dp">
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="24dp"
android:layout_height="@dimen/image_size_dp24" android:layout_height="24dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_privacy_24dp" /> app:srcCompat="@drawable/ic_privacy_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_spacing_dp16" android:paddingStart="16dp"
android:text="@string/title_privacy_policy" android:text="@string/title_privacy_policy"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout> </LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?attr/colorSurfaceContainerLowest"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp16"> android:padding="20dp">
<TextView <TextView
android:id="@+id/tv_version" android:id="@+id/tv_version"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/title_about" android:text="@string/title_about"
android:textAppearance="@style/TextAppearance.AppCompat.Small" /> android:textAppearance="@style/TextAppearance.Material3.LabelMedium"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView <TextView
android:id="@+id/tv_app_id" android:id="@+id/tv_app_id"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/title_about" android:text="@string/title_about"
android:textAppearance="@style/TextAppearance.AppCompat.Small" /> android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="?attr/colorOutline" />
</LinearLayout> </LinearLayout>
</LinearLayout> </com.google.android.material.card.MaterialCardView>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
@@ -5,33 +5,41 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:elevation="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:layout_constraintTop_toTopOf="parent" app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" </com.google.android.material.appbar.AppBarLayout>
app:titleTextAppearance="@style/TextAppearance.AppCompat.Title" />
<com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_bar" android:id="@+id/progress_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/toolbar" app:layout_constraintTop_toBottomOf="@id/app_bar"
android:indeterminate="true" android:indeterminate="true"
android:visibility="gone" android:visibility="gone"
app:indicatorColor="@color/color_fab_active" /> app:indicatorColor="?attr/colorPrimary"
app:trackColor="?attr/colorSurfaceVariant" />
<FrameLayout <FrameLayout
android:id="@+id/content_container" android:id="@+id/content_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar" app:layout_constraintTop_toBottomOf="@id/app_bar"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
+138 -98
View File
@@ -4,7 +4,7 @@
android:id="@+id/drawer_layout" android:id="@+id/drawer_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/md_theme_surface"> android:background="?attr/colorSurface">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -15,15 +15,17 @@
android:id="@+id/app_bar_layout" android:id="@+id/app_bar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/md_theme_surface" android:background="?attr/colorSurface"
app:elevation="0dp"> app:elevation="0dp"
app:liftOnScroll="false">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:title="@string/app_name" app:title="@string/app_name"
app:titleCentered="true" /> app:titleTextColor="?attr/colorOnSurface"
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
@@ -47,7 +49,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:indeterminate="true" android:indeterminate="true"
android:visibility="invisible" android:visibility="invisible"
app:indicatorColor="@color/md_theme_primary" /> app:indicatorColor="?attr/colorPrimary"
app:trackCornerRadius="0dp"
app:trackColor="@android:color/transparent" />
<LinearLayout <LinearLayout
android:id="@+id/tab_slot_top" android:id="@+id/tab_slot_top"
@@ -58,7 +62,7 @@
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:background="@color/divider_color_light" /> android:background="?attr/colorOutlineVariant" />
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager" android:id="@+id/view_pager"
@@ -68,16 +72,18 @@
android:scrollbars="vertical" android:scrollbars="vertical"
android:layout_weight="1" /> android:layout_weight="1" />
<!-- Bottom container: tab + action card -->
<LinearLayout <LinearLayout
android:id="@+id/bottom_container" android:id="@+id/bottom_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical"
android:background="?attr/colorSurfaceContainer">
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:background="@color/divider_color_light" /> android:background="?attr/colorOutlineVariant" />
<LinearLayout <LinearLayout
android:id="@+id/tab_slot_bottom" android:id="@+id/tab_slot_bottom"
@@ -88,101 +94,129 @@
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tab_group" android:id="@+id/tab_group"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="56dp"
android:minHeight="56dp"
android:background="?attr/colorSurface"
app:tabIndicatorFullWidth="true" app:tabIndicatorFullWidth="true"
app:tabMode="auto" app:tabIndicatorHeight="3dp"
app:tabMode="fixed"
app:tabGravity="fill" app:tabGravity="fill"
app:tabMaxWidth="0dp" app:tabMaxWidth="0dp"
app:tabRippleColor="@android:color/transparent" app:tabRippleColor="@android:color/transparent"
app:tabIndicatorColor="?attr/colorPrimary"
app:tabSelectedTextColor="?attr/colorPrimary"
app:tabTextColor="?attr/colorOnSurfaceVariant"
app:tabTextAppearance="@style/TabLayoutTextStyle" /> app:tabTextAppearance="@style/TabLayoutTextStyle" />
</LinearLayout> </LinearLayout>
<LinearLayout <!-- Bottom action row -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:paddingTop="12dp"
android:paddingBottom="8dp">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_summary_lite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/bolt_24"
app:tint="@color/colorWhite"
app:backgroundTint="@color/color_fab_inactive"
app:rippleColor="#33FFFFFF"
app:fabSize="normal"
app:maxImageSize="28dp"
app:elevation="4dp"
app:pressedTranslationZ="8dp"
app:hoveredFocusedTranslationZ="6dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="@string/tasker_start_service"
android:focusable="true"
android:nextFocusLeft="@+id/layout_test"
android:src="@drawable/shield_24"
app:tint="@color/colorWhite"
app:rippleColor="#33FFFFFF"
app:fabSize="normal"
app:maxImageSize="28dp"
app:elevation="4dp"
app:pressedTranslationZ="8dp"
app:hoveredFocusedTranslationZ="6dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="@string/connection_test_pending"
android:focusable="true"
android:nextFocusLeft="@+id/view_pager"
android:nextFocusRight="@+id/fab"
android:orientation="vertical"
android:gravity="center"
android:paddingTop="4dp"
android:paddingBottom="12dp"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp"> android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<!-- Bolt icon button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_summary_lite"
style="@style/Widget.ActionSquareButton"
android:layout_width="52dp"
android:layout_height="52dp"
app:icon="@drawable/bolt_24"
app:iconSize="22dp"
app:backgroundTint="?attr/colorSecondaryContainer"
app:iconTint="?attr/colorOnSecondaryContainer"
app:rippleColor="?attr/colorOnSecondaryContainer"
app:layout_constraintEnd_toStartOf="@+id/fab"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginEnd="8dp" />
<!-- Connect / Disconnect icon button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/fab"
style="@style/Widget.ActionSquareButton"
android:layout_width="52dp"
android:layout_height="52dp"
android:contentDescription="@string/tasker_start_service"
app:icon="@drawable/shield_24"
app:iconSize="26dp"
app:backgroundTint="?attr/colorPrimary"
app:iconTint="?attr/colorOnPrimary"
app:rippleColor="?attr/colorOnPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<!-- Status pill: dot + text, tappable for ping -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/layout_test"
android:layout_width="0dp"
android:layout_height="52dp"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/connection_test_pending"
app:cardBackgroundColor="?attr/colorSurfaceContainerHigh"
app:cardCornerRadius="26dp"
app:cardElevation="0dp"
app:strokeWidth="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_summary_lite"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="12dp">
<View
android:id="@+id/status_dot"
android:layout_width="10dp"
android:layout_height="10dp"
android:background="@drawable/status_dot"
android:layout_marginEnd="10dp" />
<TextView <TextView
android:id="@+id/tv_test_state" android:id="@+id/tv_test_state"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:layout_weight="1"
android:maxLines="2" android:maxLines="1"
android:minLines="1" android:ellipsize="end"
android:text="@string/connection_test_pending" android:text="@string/connection_test_pending"
android:textAppearance="@style/TextAppearance.AppCompat.Small" /> android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>
<!-- Navigation Drawer -->
<LinearLayout <LinearLayout
android:id="@+id/drawer_content_layout" android:id="@+id/drawer_content_layout"
android:layout_width="wrap_content" android:layout_width="280dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" android:layout_gravity="start"
android:background="?android:attr/windowBackground" android:background="?attr/colorSurfaceContainerLow"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.navigation.NavigationView <com.google.android.material.navigation.NavigationView
@@ -190,6 +224,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@android:color/transparent"
app:headerLayout="@layout/nav_header" /> app:headerLayout="@layout/nav_header" />
<LinearLayout <LinearLayout
@@ -198,61 +233,65 @@
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp"> android:padding="16dp">
<TextView <com.google.android.material.button.MaterialButton
android:id="@+id/drawer_settings" android:id="@+id/drawer_settings"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:drawablePadding="12dp" android:drawablePadding="12dp"
android:gravity="center_vertical"
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingBottom="12dp" android:paddingBottom="12dp"
android:text="@string/title_settings" android:text="@string/title_settings"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:background="?attr/selectableItemBackground" android:textColor="?attr/colorOnSurface"
android:clickable="true" app:iconGravity="start"
android:focusable="true" /> app:iconTint="?attr/colorOnSurfaceVariant" />
<TextView <com.google.android.material.button.MaterialButton
android:id="@+id/drawer_per_app" android:id="@+id/drawer_per_app"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:drawablePadding="12dp" android:drawablePadding="12dp"
android:gravity="center_vertical"
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingBottom="12dp" android:paddingBottom="12dp"
android:text="@string/per_app_proxy_settings" android:text="@string/per_app_proxy_settings"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:background="?attr/selectableItemBackground" android:textColor="?attr/colorOnSurface"
android:clickable="true" app:iconGravity="start"
android:focusable="true" /> app:iconTint="?attr/colorOnSurfaceVariant" />
<TextView <com.google.android.material.button.MaterialButton
android:id="@+id/drawer_check_update" android:id="@+id/drawer_check_update"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:drawablePadding="12dp" android:drawablePadding="12dp"
android:gravity="center_vertical"
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingBottom="16dp" android:paddingBottom="16dp"
android:text="@string/update_check_for_update" android:text="@string/update_check_for_update"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:background="?attr/selectableItemBackground" android:textColor="?attr/colorOnSurface"
android:clickable="true" app:iconGravity="start"
android:focusable="true" /> app:iconTint="?attr/colorOnSurfaceVariant" />
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:background="@color/divider_color_light" /> android:background="?attr/colorOutlineVariant" />
<TextView <TextView
android:id="@+id/tv_forked" android:id="@+id/tv_forked"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/drawer_forked_text" android:text="@string/drawer_forked_text"
android:textColor="#9E9E9E" android:textColor="?attr/colorOnSurfaceVariant"
android:textColorLink="#9E9E9E" android:textColorLink="?attr/colorPrimary"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:linksClickable="true" android:linksClickable="true"
android:textIsSelectable="true" /> android:textIsSelectable="true" />
@@ -262,8 +301,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="@string/drawer_developed_text" android:text="@string/drawer_developed_text"
android:textColor="#9E9E9E" android:textColor="?attr/colorOnSurfaceVariant"
android:textColorLink="#9E9E9E" android:textColorLink="?attr/colorPrimary"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:linksClickable="true" android:linksClickable="true"
android:textIsSelectable="true" /> android:textIsSelectable="true" />
</LinearLayout> </LinearLayout>
@@ -1,38 +1,51 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_bg" android:id="@+id/item_bg"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:paddingEnd="12dp"
android:paddingBottom="6dp"
android:gravity="center_vertical">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="14dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"> android:gravity="center_vertical">
<LinearLayout <LinearLayout
android:id="@+id/info_container" android:id="@+id/info_container"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center" android:layout_gravity="center"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:gravity="center" android:gravity="center_vertical"
android:nextFocusRight="@+id/layout_share" android:nextFocusRight="@+id/layout_share"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="@dimen/padding_spacing_dp4" android:paddingStart="0dp"
android:paddingTop="@dimen/padding_spacing_dp8" android:paddingTop="13dp"
android:paddingEnd="@dimen/padding_spacing_dp4" android:paddingEnd="4dp"
android:paddingBottom="@dimen/padding_spacing_dp8"> android:paddingBottom="13dp">
<LinearLayout <LinearLayout
android:id="@+id/layout_indicator" android:layout_width="0dp"
android:layout_width="@dimen/padding_spacing_dp4"
android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="vertical" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
@@ -47,10 +60,10 @@
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:paddingStart="@dimen/padding_spacing_dp8"> android:paddingStart="12dp">
<TextView <TextView
android:id="@+id/tv_name" android:id="@+id/tv_name"
@@ -58,28 +71,29 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxLines="2" android:maxLines="2"
android:minLines="1" android:minLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textColor="?attr/colorOnSurface" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingTop="@dimen/padding_spacing_dp8"> android:paddingTop="5dp">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_subscription" android:id="@+id/layout_subscription"
android:layout_width="@dimen/image_size_dp24" android:layout_width="18dp"
android:layout_height="@dimen/image_size_dp24" android:layout_height="18dp"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:layout_marginEnd="@dimen/padding_spacing_dp4" android:layout_marginEnd="4dp"
android:background="@drawable/ic_circle"> android:background="@drawable/ic_circle">
<TextView <TextView
android:id="@+id/tv_subscription" android:id="@+id/tv_subscription"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textSize="11sp" android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -91,7 +105,24 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:lines="1" android:lines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Small" /> android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tv_test_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:lines="1"
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="@color/colorPing"
android:textSize="11sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout> </LinearLayout>
@@ -101,9 +132,10 @@
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:gravity="end"> android:paddingEnd="4dp">
<ImageView <ImageView
android:id="@+id/iv_favorite" android:id="@+id/iv_favorite"
@@ -112,7 +144,7 @@
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:padding="@dimen/padding_spacing_dp8" android:padding="6dp"
android:src="@drawable/kid_star_outline_24" /> android:src="@drawable/kid_star_outline_24" />
<ImageView <ImageView
@@ -122,42 +154,15 @@
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:padding="@dimen/padding_spacing_dp8" android:padding="6dp"
android:visibility="gone" android:visibility="gone"
app:srcCompat="@drawable/ic_copy" app:srcCompat="@drawable/ic_copy"
app:tint="?attr/colorAccent" /> app:tint="?attr/colorPrimary" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout </com.google.android.material.card.MaterialCardView>
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="@dimen/padding_spacing_dp8"
android:paddingTop="@dimen/padding_spacing_dp8"
android:paddingEnd="@dimen/padding_spacing_dp8">
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_test_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="@color/colorPing"
android:textSize="11sp"
tools:text="214ms" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout> </LinearLayout>
@@ -4,8 +4,20 @@
android:id="@+id/item_bg" android:id="@+id/item_bg"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:gravity="center_vertical"> android:gravity="center_vertical">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout <LinearLayout
android:id="@+id/info_container" android:id="@+id/info_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -17,14 +29,14 @@
android:gravity="center" android:gravity="center"
android:nextFocusRight="@+id/layout_edit" android:nextFocusRight="@+id/layout_edit"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp8"> android:padding="8dp">
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8"> android:padding="8dp">
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -35,16 +47,17 @@
android:id="@+id/remarks" android:id="@+id/remarks"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<ImageView <ImageView
android:id="@+id/img_locked" android:id="@+id/img_locked"
android:layout_width="@dimen/padding_spacing_dp16" android:layout_width="16dp"
android:layout_height="@dimen/padding_spacing_dp16" android:layout_height="16dp"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginStart="@dimen/padding_spacing_dp16" android:layout_marginStart="16dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_lock_24dp" /> app:srcCompat="@drawable/ic_lock_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout> </LinearLayout>
@@ -52,17 +65,19 @@
android:id="@+id/domainIp" android:id="@+id/domainIp"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8" android:layout_marginTop="6dp"
android:lines="1" android:lines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Small" /> android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView <TextView
android:id="@+id/outboundTag" android:id="@+id/outboundTag"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8" android:layout_marginTop="4dp"
android:lines="1" android:lines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Small" /> android:textAppearance="@style/TextAppearance.Material3.LabelMedium"
android:textColor="?attr/colorPrimary" />
</LinearLayout> </LinearLayout>
@@ -71,7 +86,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8"> android:padding="8dp">
<LinearLayout <LinearLayout
android:id="@+id/layout_edit" android:id="@+id/layout_edit"
@@ -84,19 +99,20 @@
android:gravity="center" android:gravity="center"
android:nextFocusLeft="@+id/info_container" android:nextFocusLeft="@+id/info_container"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8"> android:padding="8dp">
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="24dp"
android:layout_height="@dimen/image_size_dp24" android:layout_height="24dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_edit_24dp" /> app:srcCompat="@drawable/ic_edit_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8" android:layout_marginTop="8dp"
android:orientation="horizontal"> android:orientation="horizontal">
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
@@ -109,5 +125,6 @@
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout> </LinearLayout>
@@ -4,8 +4,20 @@
android:id="@+id/item_bg" android:id="@+id/item_bg"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:gravity="center_vertical"> android:gravity="center_vertical">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout <LinearLayout
android:id="@+id/info_container" android:id="@+id/info_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -17,7 +29,7 @@
android:gravity="center" android:gravity="center"
android:nextFocusRight="@+id/layout_share" android:nextFocusRight="@+id/layout_share"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp8"> android:padding="8dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -34,7 +46,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:orientation="vertical" android:orientation="vertical"
android:paddingStart="@dimen/padding_spacing_dp8"> android:paddingStart="8dp">
<TextView <TextView
android:id="@+id/tv_name" android:id="@+id/tv_name"
@@ -42,7 +54,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxLines="2" android:maxLines="2"
android:minLines="1" android:minLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout> </LinearLayout>
@@ -63,13 +75,14 @@
android:gravity="center" android:gravity="center"
android:nextFocusLeft="@+id/info_container" android:nextFocusLeft="@+id/info_container"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8"> android:padding="8dp">
<ImageView <ImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/image_size_dp24" android:layout_height="24dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_share_24dp" /> app:srcCompat="@drawable/ic_share_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout> </LinearLayout>
@@ -83,13 +96,14 @@
android:focusable="true" android:focusable="true"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8"> android:padding="8dp">
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="24dp"
android:layout_height="@dimen/image_size_dp24" android:layout_height="24dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_edit_24dp" /> app:srcCompat="@drawable/ic_edit_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout> </LinearLayout>
@@ -103,17 +117,17 @@
android:focusable="true" android:focusable="true"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8"> android:padding="8dp">
<ImageView <ImageView
android:layout_width="@dimen/image_size_dp24" android:layout_width="24dp"
android:layout_height="@dimen/image_size_dp24" android:layout_height="24dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_delete_24dp" /> app:srcCompat="@drawable/ic_delete_24dp"
app:tint="?attr/colorError" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@@ -123,29 +137,30 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="@dimen/padding_spacing_dp8" android:paddingStart="8dp"
android:paddingEnd="@dimen/padding_spacing_dp8"> android:paddingEnd="8dp">
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingTop="@dimen/padding_spacing_dp8"> android:paddingTop="6dp">
<TextView <TextView
android:id="@+id/tv_url" android:id="@+id/tv_url"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:lines="2" android:lines="2"
android:textAppearance="@style/TextAppearance.AppCompat.Small" /> android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8" android:layout_marginTop="6dp"
android:orientation="horizontal"> android:orientation="horizontal">
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
@@ -162,31 +177,33 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_marginTop="@dimen/padding_spacing_dp8" android:layout_marginTop="6dp"
android:paddingStart="@dimen/padding_spacing_dp8" android:paddingStart="8dp"
android:paddingEnd="@dimen/padding_spacing_dp8"> android:paddingEnd="8dp"
android:paddingBottom="4dp">
<TextView <TextView
android:id="@+id/tv_last_updated" android:id="@+id/tv_last_updated"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"/> android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<ProgressBar <ProgressBar
android:id="@+id/progress_bar" android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleSmall" style="?android:attr/progressBarStyleSmall"
android:layout_width="16dp" android:layout_width="16dp"
android:layout_height="16dp" android:layout_height="16dp"
android:layout_marginStart="@dimen/padding_spacing_dp8" android:layout_marginStart="8dp"
android:visibility="gone" /> android:visibility="gone" />
<TextView <TextView
android:id="@+id/tv_update_status" android:id="@+id/tv_update_status"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_spacing_dp8" android:layout_marginStart="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:visibility="gone" /> android:visibility="gone" />
</LinearLayout> </LinearLayout>
@@ -195,4 +212,6 @@
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout> </LinearLayout>
@@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.AppCompat.Button.Borderless" style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:onClick="onModeHelpClicked" android:onClick="onModeHelpClicked"
android:text="@string/title_mode_help" android:text="@string/title_mode_help"
android:textAlignment="textStart" android:textAlignment="textStart"
android:textStyle="italic" android:textStyle="italic"
android:textColor="?attr/colorPrimary" android:textColor="?attr/colorPrimary"
app:iconTint="?attr/colorPrimary"
tools:ignore="UsingOnClickInXml" /> tools:ignore="UsingOnClickInXml" />
@@ -1,29 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="color_fab_active">#C0C0C0</color> <color name="colorPing">#90CAF9</color>
<color name="color_fab_inactive">#646464</color> <color name="colorPingGood">#86D993</color>
<color name="divider_color_light">#424242</color> <color name="colorPingMedium">#F1C76A</color>
<color name="status_connected">#66BB6A</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>
<color name="divider_color_light">@color/md_theme_outlineVariant</color>
<color name="server_list_divider">#2649454F</color>
<color name="md_theme_primary">#C0C0C0</color> <!-- M3 Dark scheme — Purple/Violet tonal palette -->
<color name="md_theme_onPrimary">#303030</color> <color name="md_theme_primary">#D0BCFF</color>
<color name="md_theme_primaryContainer">#474747</color> <color name="md_theme_onPrimary">#381E72</color>
<color name="md_theme_onPrimaryContainer">#E0E0E0</color> <color name="md_theme_primaryContainer">#4F378B</color>
<color name="md_theme_onPrimaryContainer">#EADDFF</color>
<color name="md_theme_secondary">#90CAF9</color> <color name="md_theme_secondary">#CCC2DC</color>
<color name="md_theme_onSecondary">#FFFFFF</color> <color name="md_theme_onSecondary">#332D41</color>
<color name="md_theme_secondaryContainer">#6F3800</color> <color name="md_theme_secondaryContainer">#4A4458</color>
<color name="md_theme_onSecondaryContainer">#FFE8D6</color> <color name="md_theme_onSecondaryContainer">#E8DEF8</color>
<color name="md_theme_tertiary">#64B5F6</color> <color name="md_theme_tertiary">#EFB8C8</color>
<color name="md_theme_onTertiary">#00382E</color> <color name="md_theme_onTertiary">#492532</color>
<color name="md_theme_tertiaryContainer">#005143</color> <color name="md_theme_tertiaryContainer">#633B48</color>
<color name="md_theme_onTertiaryContainer">#BBDEFB</color> <color name="md_theme_onTertiaryContainer">#FFD8E4</color>
<!-- Error colors --> <!-- Error colors -->
<color name="md_theme_error">#FFB4AB</color> <color name="md_theme_error">#F2B8B5</color>
<color name="md_theme_errorContainer">#93000A</color> <color name="md_theme_errorContainer">#8C1D18</color>
<color name="md_theme_onError">#690005</color> <color name="md_theme_onError">#601410</color>
<color name="md_theme_onErrorContainer">#FFDAD6</color> <color name="md_theme_onErrorContainer">#F9DEDC</color>
<!-- Background colors --> <!-- Background colors -->
<color name="md_theme_background">#1C1B1F</color> <color name="md_theme_background">#1C1B1F</color>
@@ -35,15 +42,22 @@
<color name="md_theme_surfaceVariant">#49454F</color> <color name="md_theme_surfaceVariant">#49454F</color>
<color name="md_theme_onSurfaceVariant">#CAC4D0</color> <color name="md_theme_onSurfaceVariant">#CAC4D0</color>
<color name="md_theme_inverseSurface">#E6E1E5</color> <color name="md_theme_inverseSurface">#E6E1E5</color>
<color name="md_theme_inverseOnSurface">#1C1B1F</color> <color name="md_theme_inverseOnSurface">#313033</color>
<!-- Surface containers — M3 dark elevation tones -->
<color name="md_theme_surfaceContainerLowest">#0F0D13</color>
<color name="md_theme_surfaceContainerLow">#1D1B20</color>
<color name="md_theme_surfaceContainer">#211F26</color>
<color name="md_theme_surfaceContainerHigh">#2B2930</color>
<color name="md_theme_surfaceContainerHighest">#36343B</color>
<!-- Outline colors --> <!-- Outline colors -->
<color name="md_theme_outline">#938F99</color> <color name="md_theme_outline">#938F99</color>
<color name="md_theme_outlineVariant">#49454F</color> <color name="md_theme_outlineVariant">#49454F</color>
<!-- Other colors --> <!-- Other colors -->
<color name="md_theme_inversePrimary">#000000</color> <color name="md_theme_inversePrimary">#6750A4</color>
<color name="md_theme_shadow">#000000</color> <color name="md_theme_shadow">#000000</color>
<color name="md_theme_surfaceTint">#C0C0C0</color> <color name="md_theme_surfaceTint">#D0BCFF</color>
<color name="md_theme_scrim">#000000</color> <color name="md_theme_scrim">#000000</color>
</resources> </resources>
@@ -1,9 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Night theme: inherit the shared AppThemeBase and only override night-specific items --> <!-- Night theme: inherit shared base, only override night-specific items -->
<style name="AppThemeDayNight" parent="AppThemeBase"> <style name="AppThemeDayNight" parent="AppThemeBase">
<!-- Night mode specific overrides -->
<item name="android:windowLightStatusBar">false</item> <item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar" tools:targetApi="27">false</item> <item name="android:windowLightNavigationBar" tools:targetApi="27">false</item>
</style> </style>
+359 -121
View File
@@ -2,12 +2,18 @@
<resources> <resources>
<string name="app_widget_name">v2rayNG</string> <string name="app_widget_name">v2rayNG</string>
<string name="app_tile_name">v2rayNG</string> <string name="app_tile_name">v2rayNG</string>
<string name="app_tile_first_use">Первое использование этой функции, пожалуйста, используйте приложение, чтобы добавить сервер</string> <string
name="app_tile_first_use"
>Первое использование этой функции, пожалуйста, используйте приложение, чтобы добавить сервер</string>
<string name="navigation_drawer_open">Открыть панель навигации</string> <string name="navigation_drawer_open">Открыть панель навигации</string>
<string name="navigation_drawer_close">Закрыть панель навигации</string> <string name="navigation_drawer_close">Закрыть панель навигации</string>
<string name="migration_success">Успешный перенос данных!</string> <string name="migration_success">Успешный перенос данных!</string>
<string name="drawer_forked_text">forked from <a href="https://github.com/2dust/v2rayng">V2RayNG</a></string> <string name="drawer_forked_text">forked from <a
<string name="drawer_developed_text">developed by developers from <a href="https://t.me/openlibrecommunity">Olc</a></string> href="https://github.com/2dust/v2rayng"
>V2RayNG</a></string>
<string name="drawer_developed_text">developed by developers from <a
href="https://t.me/openlibrecommunity"
>Olc</a></string>
<string name="action_stop_service">Остановить службу</string> <string name="action_stop_service">Остановить службу</string>
<string name="migration_fail">Перенос данных не выполнен!</string> <string name="migration_fail">Перенос данных не выполнен!</string>
<string name="pull_down_to_refresh">Потяните вниз для обновления!</string> <string name="pull_down_to_refresh">Потяните вниз для обновления!</string>
@@ -15,7 +21,9 @@
<!-- Notifications --> <!-- Notifications -->
<string name="notification_action_stop_v2ray">Остановить</string> <string name="notification_action_stop_v2ray">Остановить</string>
<string name="toast_permission_denied">Разрешение не получено</string> <string name="toast_permission_denied">Разрешение не получено</string>
<string name="toast_permission_denied_notification">Разрешение на отображение уведомлений не получено</string> <string
name="toast_permission_denied_notification"
>Разрешение на отображение уведомлений не получено</string>
<string name="notification_action_more">Ещё…</string> <string name="notification_action_more">Ещё…</string>
<string name="toast_services_start">Запуск служб</string> <string name="toast_services_start">Запуск служб</string>
<string name="toast_services_stop">Остановка служб</string> <string name="toast_services_stop">Остановка служб</string>
@@ -29,19 +37,41 @@
<string name="menu_item_edit_config">Изменить профиль</string> <string name="menu_item_edit_config">Изменить профиль</string>
<string name="menu_item_del_config">Удалить профиль</string> <string name="menu_item_del_config">Удалить профиль</string>
<string name="menu_item_import_config_qrcode">Импорт из QR-кода</string> <string name="menu_item_import_config_qrcode">Импорт из QR-кода</string>
<string name="menu_item_import_config_clipboard">Импорт из буфера обмена</string> <string
name="menu_item_import_config_clipboard"
>Импорт из буфера обмена</string>
<string name="menu_item_import_config_local">Импорт из файла</string> <string name="menu_item_import_config_local">Импорт из файла</string>
<string name="menu_item_import_config_policy_group">Добавить группу политик</string> <string
<string name="menu_item_import_config_manually_vmess">Ручной ввод VMess</string> name="menu_item_import_config_policy_group"
<string name="menu_item_import_config_manually_vless">Ручной ввод VLESS</string> >Добавить группу политик</string>
<string name="menu_item_import_config_manually_ss">Ручной ввод Shadowsocks</string> <string
<string name="menu_item_import_config_manually_socks">Ручной ввод SOCKS</string> name="menu_item_import_config_manually_vmess"
<string name="menu_item_import_config_manually_http">Ручной ввод HTTP</string> >Ручной ввод VMess</string>
<string name="menu_item_import_config_manually_trojan">Ручной ввод Trojan</string> <string
<string name="menu_item_import_config_manually_wireguard">Ручной ввод WireGuard</string> name="menu_item_import_config_manually_vless"
<string name="menu_item_import_config_manually_hysteria2">Ручной ввод Hysteria2</string> >Ручной ввод VLESS</string>
<string
name="menu_item_import_config_manually_ss"
>Ручной ввод Shadowsocks</string>
<string
name="menu_item_import_config_manually_socks"
>Ручной ввод SOCKS</string>
<string
name="menu_item_import_config_manually_http"
>Ручной ввод HTTP</string>
<string
name="menu_item_import_config_manually_trojan"
>Ручной ввод Trojan</string>
<string
name="menu_item_import_config_manually_wireguard"
>Ручной ввод WireGuard</string>
<string
name="menu_item_import_config_manually_hysteria2"
>Ручной ввод Hysteria2</string>
<string name="del_config_comfirm">Подтверждаете удаление?</string> <string name="del_config_comfirm">Подтверждаете удаление?</string>
<string name="del_invalid_config_comfirm">Выполните проверку перед удалением! Подтверждаете удаление?</string> <string
name="del_invalid_config_comfirm"
>Выполните проверку перед удалением! Подтверждаете удаление?</string>
<string name="server_lab_remarks">Название</string> <string name="server_lab_remarks">Название</string>
<string name="server_lab_address">Адрес</string> <string name="server_lab_address">Адрес</string>
<string name="server_lab_port">Порт</string> <string name="server_lab_port">Порт</string>
@@ -82,45 +112,73 @@
<string name="server_lab_encryption">Шифрование</string> <string name="server_lab_encryption">Шифрование</string>
<string name="server_lab_flow">Поток</string> <string name="server_lab_flow">Поток</string>
<string name="server_lab_public_key">Открытый ключ</string> <string name="server_lab_public_key">Открытый ключ</string>
<string name="server_lab_preshared_key">Дополнительный ключ шифрования (необязательно)</string> <string
name="server_lab_preshared_key"
>Дополнительный ключ шифрования (необязательно)</string>
<string name="server_lab_short_id">ShortID</string> <string name="server_lab_short_id">ShortID</string>
<string name="server_lab_spider_x">SpiderX</string> <string name="server_lab_spider_x">SpiderX</string>
<string name="server_lab_mldsa65_verify">mldsa65Verify</string> <string name="server_lab_mldsa65_verify">mldsa65Verify</string>
<string name="server_lab_secret_key">Закрытый ключ</string> <string name="server_lab_secret_key">Закрытый ключ</string>
<string name="server_lab_reserved">Reserved (необязательно, через запятую)</string> <string
<string name="server_lab_local_address">Локальный адрес (необязательно, IPv4/IPv6 через запятую)</string> name="server_lab_reserved"
<string name="server_lab_local_mtu">MTU (необязательно, по умолчанию 1420)</string> >Reserved (необязательно, через запятую)</string>
<string
name="server_lab_local_address"
>Локальный адрес (необязательно, IPv4/IPv6 через запятую)</string>
<string
name="server_lab_local_mtu"
>MTU (необязательно, по умолчанию 1420)</string>
<string name="toast_success">Успешно</string> <string name="toast_success">Успешно</string>
<string name="toast_failure">Ошибка</string> <string name="toast_failure">Ошибка</string>
<string name="toast_none_data">Ничего нет</string> <string name="toast_none_data">Ничего нет</string>
<string name="toast_incorrect_protocol">Неправильный протокол</string> <string name="toast_incorrect_protocol">Неправильный протокол</string>
<string name="toast_decoding_failed">Невозможно декодировать</string> <string name="toast_decoding_failed">Невозможно декодировать</string>
<string name="title_file_chooser">Выберите профиль</string> <string name="title_file_chooser">Выберите профиль</string>
<string name="toast_require_file_manager">Установите файловый менеджер</string> <string
name="toast_require_file_manager"
>Установите файловый менеджер</string>
<string name="server_customize_config">Изменить профиль</string> <string name="server_customize_config">Изменить профиль</string>
<string name="toast_config_file_invalid">Неправильный профиль</string> <string name="toast_config_file_invalid">Неправильный профиль</string>
<string name="server_lab_content">Данные</string> <string name="server_lab_content">Данные</string>
<string name="toast_none_data_clipboard">В буфере обмена нет данных</string> <string name="toast_none_data_clipboard">В буфере обмена нет данных</string>
<string name="toast_invalid_url">Неправильный URL</string> <string name="toast_invalid_url">Неправильный URL</string>
<string name="toast_insecure_url_protocol">Не используйте небезопасный HTTP-протокол в адресе подписки</string> <string
<string name="server_lab_need_inbound">Убедитесь, что входящий порт соответствует настройкам</string> name="toast_insecure_url_protocol"
>Не используйте небезопасный HTTP-протокол в адресе подписки</string>
<string
name="server_lab_need_inbound"
>Убедитесь, что входящий порт соответствует настройкам</string>
<string name="toast_malformed_josn">Профиль повреждён</string> <string name="toast_malformed_josn">Профиль повреждён</string>
<string name="server_lab_request_host6">Узел (SNI) (необязательно)</string> <string name="server_lab_request_host6">Узел (SNI) (необязательно)</string>
<string name="toast_action_not_allowed">Это действие запрещено</string> <string name="toast_action_not_allowed">Это действие запрещено</string>
<string name="server_obfs_password">Пароль obfs</string> <string name="server_obfs_password">Пароль obfs</string>
<string name="server_lab_port_hop">Смена портов (переопределяет порт)</string> <string
name="server_lab_port_hop"
>Смена портов (переопределяет порт)</string>
<string name="server_lab_port_hop_interval">Интервал смены портов</string> <string name="server_lab_port_hop_interval">Интервал смены портов</string>
<string name="server_lab_bandwidth_down">Входящая пропускная способность (допускаются: k/m/g/t)</string> <string
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string> name="server_lab_bandwidth_down"
>Входящая пропускная способность (допускаются: k/m/g/t)</string>
<string
name="server_lab_bandwidth_up"
>Исходящая пропускная способность (допускаются: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">Режим XHTTP</string> <string name="server_lab_xhttp_mode">Режим XHTTP</string>
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string> <string
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string> name="server_lab_xhttp_extra"
>Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
<string
name="server_lab_final_mask"
>finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string> <string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string> <string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string> <string
name="server_lab_pinned_ca256"
>Отпечаток сертификата (SHA-256)</string>
<!-- UserAssetActivity --> <!-- UserAssetActivity -->
<string name="toast_asset_copy_failed">Невозможно скопировать файл, используйте файловый менеджер</string> <string
name="toast_asset_copy_failed"
>Невозможно скопировать файл, используйте файловый менеджер</string>
<string name="menu_item_add_asset">Добавить ресурс</string> <string name="menu_item_add_asset">Добавить ресурс</string>
<string name="menu_item_add_file">Добавить файлы</string> <string name="menu_item_add_file">Добавить файлы</string>
<string name="menu_item_add_url">Добавить URL</string> <string name="menu_item_add_url">Добавить URL</string>
@@ -130,7 +188,9 @@
<string name="title_user_asset_add_url">Добавить URL ресурса</string> <string name="title_user_asset_add_url">Добавить URL ресурса</string>
<string name="msg_file_not_found">Файл не найден</string> <string name="msg_file_not_found">Файл не найден</string>
<string name="msg_remark_is_duplicate">Название уже существует</string> <string name="msg_remark_is_duplicate">Название уже существует</string>
<string name="asset_geo_files_sources">Источник геофайлов (необязательно)</string> <string
name="asset_geo_files_sources"
>Источник геофайлов (необязательно)</string>
<!-- PerAppProxyActivity --> <!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">Загрузка…</string> <string name="msg_dialog_progress">Загрузка…</string>
@@ -143,28 +203,61 @@
<string name="msg_downloading_content">Загрузка данных</string> <string name="msg_downloading_content">Загрузка данных</string>
<string name="menu_item_export_proxy_app">Экспорт в буфер обмена</string> <string name="menu_item_export_proxy_app">Экспорт в буфер обмена</string>
<string name="menu_item_import_proxy_app">Импорт из буфера обмена</string> <string name="menu_item_import_proxy_app">Импорт из буфера обмена</string>
<string name="per_app_proxy_settings">Выбор приложений</string> <string name="per_app_proxy_settings">Раздельное туннелирование</string>
<string name="per_app_proxy_settings_enable">Выбор приложений</string> <string
name="per_app_proxy_settings_enable"
>Раздельное туннелирование</string>
<!-- Preferences --> <!-- Preferences -->
<string name="title_settings">Настройки</string> <string name="title_settings">Настройки</string>
<string name="title_advanced">Расширенные настройки</string> <string name="title_advanced">Расширенные настройки</string>
<string name="title_core_settings">Настройки ядра</string> <string name="title_core_settings">Настройки ядра</string>
<string name="title_vpn_settings">Настройки VPN</string> <string name="title_vpn_settings">Настройки VPN</string>
<string name="title_pref_per_app_proxy">Прокси для выбранных приложений</string> <string name="title_pref_per_app_proxy">Раздельное туннелирование</string>
<string name="summary_pref_per_app_proxy">Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню.</string> <string
name="summary_pref_per_app_proxy"
>Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню.</string>
<string name="title_pref_is_booted">Автоподключение при запуске</string> <string name="title_pref_is_booted">Автоподключение при запуске</string>
<string name="summary_pref_is_booted">Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string> <string
name="summary_pref_is_booted"
>Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string>
<string name="title_pref_auto_sort_after_test">Автосортировка профилей</string> <string
<string name="summary_pref_auto_sort_after_test">Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string> name="title_pref_auto_sort_after_test"
>Автосортировка профилей</string>
<string
name="summary_pref_auto_sort_after_test"
>Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string>
<string
name="title_pref_show_copy_button"
>Показывать кнопку копирования</string>
<string
name="summary_pref_show_copy_button"
>Показывать кнопку для копирования конфигурации сервера в буфер обмена</string>
<string
name="title_pref_show_server_ip"
>Показывать IP / хост сервера</string>
<string
name="summary_pref_show_server_ip"
>Отображать IP-адрес или хост сервера под названием</string>
<string name="title_mux_settings">Настройки мультиплексирования</string> <string name="title_mux_settings">Настройки мультиплексирования</string>
<string name="title_pref_mux_enabled">Использовать мультиплексирование</string> <string
<string name="summary_pref_mux_enabled">Быстрее, но это может привести к нестабильному соединению.\nНиже можно настроить обработку TCP, UDP и QUIC.</string> name="title_pref_mux_enabled"
<string name="title_pref_mux_concurency">TCP-соединения (диапазон от 1 до 1024)</string> >Использовать мультиплексирование</string>
<string name="title_pref_mux_xudp_concurency">XUDP-соединения (диапазон от 1 до 1024)</string> <string
<string name="title_pref_mux_xudp_quic">Обработка QUIC в мультиплексном туннеле</string> name="summary_pref_mux_enabled"
>Быстрее, но это может привести к нестабильному соединению.\nНиже можно настроить обработку TCP, UDP и QUIC.</string>
<string
name="title_pref_mux_concurency"
>TCP-соединения (диапазон от 1 до 1024)</string>
<string
name="title_pref_mux_xudp_concurency"
>XUDP-соединения (диапазон от 1 до 1024)</string>
<string
name="title_pref_mux_xudp_quic"
>Обработка QUIC в мультиплексном туннеле</string>
<string-array name="mux_xudp_quic_entries"> <string-array name="mux_xudp_quic_entries">
<item>Отклонять</item> <item>Отклонять</item>
<item>Разрешать</item> <item>Разрешать</item>
@@ -172,49 +265,87 @@
</string-array> </string-array>
<string name="title_pref_speed_enabled">Показывать скорость</string> <string name="title_pref_speed_enabled">Показывать скорость</string>
<string name="summary_pref_speed_enabled">Показывать текущую скорость в уведомлении.\nЗначок будет меняться в зависимости от использования.</string> <string
name="summary_pref_speed_enabled"
>Показывать текущую скорость в уведомлении.\nЗначок будет меняться в зависимости от использования.</string>
<string name="title_pref_sniffing_enabled">Анализировать пакеты</string> <string name="title_pref_sniffing_enabled">Анализировать пакеты</string>
<string name="summary_pref_sniffing_enabled">Пытаться определять доменные имена в пакетах (по умолчанию включено)</string> <string
<string name="title_pref_route_only_enabled">Домен только для маршрутизации</string> name="summary_pref_sniffing_enabled"
<string name="summary_pref_route_only_enabled">Использовать доменное имя только для маршрутизации и сохранять целевой адрес в виде IP.</string> >Пытаться определять доменные имена в пакетах (по умолчанию включено)</string>
<string
name="title_pref_route_only_enabled"
>Домен только для маршрутизации</string>
<string
name="summary_pref_route_only_enabled"
>Использовать доменное имя только для маршрутизации и сохранять целевой адрес в виде IP.</string>
<string name="title_pref_local_dns_enabled">Использовать локальную DNS</string> <string
<string name="summary_pref_local_dns_enabled">Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая»)</string> name="title_pref_local_dns_enabled"
>Использовать локальную DNS</string>
<string
name="summary_pref_local_dns_enabled"
>Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая»)</string>
<string name="title_pref_fake_dns_enabled">Использовать поддельную DNS</string> <string
<string name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string> name="title_pref_fake_dns_enabled"
>Использовать поддельную DNS</string>
<string
name="summary_pref_fake_dns_enabled"
>Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string>
<string name="title_pref_prefer_ipv6">Предпочитать IPv6</string> <string name="title_pref_prefer_ipv6">Предпочитать IPv6</string>
<string name="summary_pref_prefer_ipv6">Использовать маршрутизацию IPv6 предпочитать IPv6-адреса</string> <string
name="summary_pref_prefer_ipv6"
>Использовать маршрутизацию IPv6 предпочитать IPv6-адреса</string>
<string name="title_pref_remote_dns">Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно)</string> <string
name="title_pref_remote_dns"
>Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно)</string>
<string name="summary_pref_remote_dns">DNS</string> <string name="summary_pref_remote_dns">DNS</string>
<string name="title_pref_vpn_dns">VPN DNS (только IPv4/v6)</string> <string name="title_pref_vpn_dns">VPN DNS (только IPv4/v6)</string>
<string name="title_pref_vpn_bypass_lan">VPN обходит LAN</string> <string name="title_pref_vpn_bypass_lan">VPN обходит LAN</string>
<string name="title_pref_vpn_interface_address">Адрес интерфейса VPN</string> <string
name="title_pref_vpn_interface_address"
>Адрес интерфейса VPN</string>
<string name="title_pref_vpn_mtu">VPN MTU (по умолчанию 1500)</string> <string name="title_pref_vpn_mtu">VPN MTU (по умолчанию 1500)</string>
<string name="title_pref_domestic_dns">Внутренняя DNS (необязательно)</string> <string
name="title_pref_domestic_dns"
>Внутренняя DNS (необязательно)</string>
<string name="summary_pref_domestic_dns">DNS</string> <string name="summary_pref_domestic_dns">DNS</string>
<string name="title_pref_dns_hosts">Узлы DNS (формат: домен:адрес,…)</string> <string
name="title_pref_dns_hosts"
>Узлы DNS (формат: домен:адрес,…)</string>
<string name="summary_pref_dns_hosts">домен:адрес,…</string> <string name="summary_pref_dns_hosts">домен:адрес,…</string>
<string name="title_pref_delay_test_url">Сервис проверки задержки</string> <string name="title_pref_delay_test_url">Сервис проверки задержки</string>
<string name="summary_pref_delay_test_url">URL</string> <string name="summary_pref_delay_test_url">URL</string>
<string name="title_pref_ip_api_url">Сервис проверки текущего соединения</string> <string
name="title_pref_ip_api_url"
>Сервис проверки текущего соединения</string>
<string name="summary_pref_ip_api_url">URL</string> <string name="summary_pref_ip_api_url">URL</string>
<string name="title_pref_proxy_sharing_enabled">Разрешать соединения из LAN</string> <string
<string name="summary_pref_proxy_sharing_enabled">Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string> name="title_pref_proxy_sharing_enabled"
<string name="toast_warning_pref_proxysharing_short">Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети</string> >Разрешать соединения из LAN</string>
<string
name="summary_pref_proxy_sharing_enabled"
>Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string>
<string
name="toast_warning_pref_proxysharing_short"
>Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети</string>
<string name="title_pref_allow_insecure">Разрешать небезопасные соединения</string> <string
<string name="summary_pref_allow_insecure">Для TLS по умолчанию разрешены небезопасные соединения</string> name="title_pref_allow_insecure"
>Разрешать небезопасные соединения</string>
<string
name="summary_pref_allow_insecure"
>Для TLS по умолчанию разрешены небезопасные соединения</string>
<string name="title_pref_socks_port">Порт локального прокси</string> <string name="title_pref_socks_port">Порт локального прокси</string>
<string name="summary_pref_socks_port">Порт локального прокси</string> <string name="summary_pref_socks_port">Порт локального прокси</string>
@@ -222,28 +353,50 @@
<string name="title_pref_local_dns_port">Порт локальной DNS</string> <string name="title_pref_local_dns_port">Порт локальной DNS</string>
<string name="summary_pref_local_dns_port">Порт локальной DNS</string> <string name="summary_pref_local_dns_port">Порт локальной DNS</string>
<string name="title_pref_confirm_remove">Подтверждать удаление профиля</string> <string
<string name="summary_pref_confirm_remove">Обязательное подтверждение удаления профиля</string> name="title_pref_confirm_remove"
>Подтверждать удаление профиля</string>
<string
name="summary_pref_confirm_remove"
>Обязательное подтверждение удаления профиля</string>
<string name="title_pref_start_scan_immediate">Сканировать при запуске</string> <string
<string name="summary_pref_start_scan_immediate">Начинать сканирование сразу при запуске приложения или запускать функцию сканирования камерой или из изображения через панель инструментов</string> name="title_pref_start_scan_immediate"
>Сканировать при запуске</string>
<string
name="summary_pref_start_scan_immediate"
>Начинать сканирование сразу при запуске приложения или запускать функцию сканирования камерой или из изображения через панель инструментов</string>
<string name="title_pref_append_http_proxy">Дополнительный HTTP-прокси</string> <string
<string name="summary_pref_append_http_proxy">HTTP-прокси будет использоваться напрямую (из браузера и других поддерживающих приложений), минуя виртуальный сетевой адаптер (Android 10+)</string> name="title_pref_append_http_proxy"
>Дополнительный HTTP-прокси</string>
<string
name="summary_pref_append_http_proxy"
>HTTP-прокси будет использоваться напрямую (из браузера и других поддерживающих приложений), минуя виртуальный сетевой адаптер (Android 10+)</string>
<string name="title_pref_double_column_display">Профили в два столбца</string> <string
<string name="summary_pref_double_column_display">Список профилей отображается двумя столбцами, что позволяет показать больше информации на экране. Требуется перезапуск приложения.</string> name="title_pref_double_column_display"
>Профили в два столбца</string>
<string
name="summary_pref_double_column_display"
>Список профилей отображается двумя столбцами, что позволяет показать больше информации на экране. Требуется перезапуск приложения.</string>
<string name="title_pref_group_all_display">Общая вкладка групп</string> <string name="title_pref_group_all_display">Общая вкладка групп</string>
<string name="summary_pref_group_all_display">Показывать дополнительную вкладку со всеми профилями групп</string> <string
name="summary_pref_group_all_display"
>Показывать дополнительную вкладку со всеми профилями групп</string>
<string name="title_pref_show_copy_button">Кнопка копирования</string>
<string name="summary_pref_show_copy_button">Показывать кнопку копирования конфигурации сервера в буфер обмена</string>
<!-- AboutActivity --> <!-- AboutActivity -->
<string name="title_pref_feedback">Обратная связь</string> <string name="title_pref_feedback">Обратная связь</string>
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string> <string
<string name="summary_pref_tg_group">Присоединиться к группе в Telegram</string> name="summary_pref_feedback"
<string name="toast_tg_app_not_found">Приложение Telegram не найдено</string> >Предложить улучшение или сообщить об ошибке на GitHub</string>
<string
name="summary_pref_tg_group"
>Присоединиться к группе в Telegram</string>
<string
name="toast_tg_app_not_found"
>Приложение Telegram не найдено</string>
<string name="title_privacy_policy">Политика конфиденциальности</string> <string name="title_privacy_policy">Политика конфиденциальности</string>
<string name="title_about">О приложении</string> <string name="title_about">О приложении</string>
<string name="title_source_code">Исходный код</string> <string name="title_source_code">Исходный код</string>
@@ -252,33 +405,57 @@
<string name="title_pref_promotion">Содействие</string> <string name="title_pref_promotion">Содействие</string>
<string name="title_pref_auto_update_subscription">Автоматически обновлять подписки</string> <string
<string name="summary_pref_auto_update_subscription">Автоматическое обновление подписок в фоновом режиме с указанным интервалом. В зависимости от устройства эта функция может работать не всегда.</string> name="title_pref_auto_update_subscription"
<string name="title_pref_auto_update_interval">Интервал автообновления (минут, не менее 15)</string> >Автоматически обновлять подписки</string>
<string
name="summary_pref_auto_update_subscription"
>Автоматическое обновление подписок в фоновом режиме с указанным интервалом. В зависимости от устройства эта функция может работать не всегда.</string>
<string
name="title_pref_auto_update_interval"
>Интервал автообновления (минут, не менее 15)</string>
<string name="title_core_loglevel">Подробность ведения журнала</string> <string name="title_core_loglevel">Подробность ведения журнала</string>
<string name="title_outbound_domain_resolve_method">Предопределение исходящего домена</string> <string
name="title_outbound_domain_resolve_method"
>Предопределение исходящего домена</string>
<string name="title_mode">Режим</string> <string name="title_mode">Режим</string>
<string name="title_mode_help">Нажмите для получения дополнительной информации</string> <string
name="title_mode_help"
>Нажмите для получения дополнительной информации</string>
<string name="title_language">Язык</string> <string name="title_language">Язык</string>
<string name="title_ui_settings">Настройки интерфейса</string> <string name="title_ui_settings">Настройки интерфейса</string>
<string name="title_pref_ui_mode_night">Тема интерфейса</string> <string name="title_pref_ui_mode_night">Тема интерфейса</string>
<string name="title_pref_dynamic_colors">Динамические цвета (Material You)</string> <string
<string name="summary_pref_dynamic_colors">Использовать цвета обоев (Android 12+). Требует перезапуска.</string> name="title_pref_dynamic_colors"
>Динамические цвета (Material You)</string>
<string
name="summary_pref_dynamic_colors"
>Использовать цвета обоев (Android 12+). Требует перезапуска.</string>
<string name="restart_required">Требуется перезапуск приложения</string> <string name="restart_required">Требуется перезапуск приложения</string>
<string name="title_pref_subscriptions_bottom">Подписки снизу</string> <string name="title_pref_subscriptions_bottom">Подписки снизу</string>
<string name="summary_pref_subscriptions_bottom">Переместить вкладки подписок под список серверов</string> <string
name="summary_pref_subscriptions_bottom"
>Переместить вкладки подписок под список серверов</string>
<string name="title_pref_use_hev_tunnel">Использовать Hev TUN</string> <string name="title_pref_use_hev_tunnel">Использовать Hev TUN</string>
<string name="summary_pref_use_hev_tunnel">Если включено, TUN будет использовать hev-socks5-tunnel; иначе будет использован xray-core</string> <string
<string name="title_pref_hev_tunnel_loglevel">Подробность журнала HevTun</string> name="summary_pref_use_hev_tunnel"
<string name="title_pref_hev_tunnel_rw_timeout">Ожидание чтения/записи HevTun (секунд, по умолчанию TCP,UDP 300,60)</string> >Если включено, TUN будет использовать hev-socks5-tunnel; иначе будет использован xray-core</string>
<string
name="title_pref_hev_tunnel_loglevel"
>Подробность журнала HevTun</string>
<string
name="title_pref_hev_tunnel_rw_timeout"
>Ожидание чтения/записи HevTun (секунд, по умолчанию TCP,UDP 300,60)</string>
<string name="title_logcat">Журнал</string> <string name="title_logcat">Журнал</string>
<string name="logcat_copy">Копировать</string> <string name="logcat_copy">Копировать</string>
<string name="logcat_clear">Очистить</string> <string name="logcat_clear">Очистить</string>
<string name="title_service_restart">Перезапуск службы</string> <string name="title_service_restart">Перезапуск службы</string>
<string name="title_del_all_config">Удалить профили</string> <string name="title_del_all_config">Удалить профили</string>
<string name="title_del_duplicate_config">Удалить дубликаты профилей</string> <string
name="title_del_duplicate_config"
>Удалить дубликаты профилей</string>
<string name="title_del_invalid_config">Удалить нерабочие профили</string> <string name="title_del_invalid_config">Удалить нерабочие профили</string>
<string name="title_export_all">Экспорт профилей в буфер обмена</string> <string name="title_export_all">Экспорт профилей в буфер обмена</string>
<string name="title_sub_setting">Группы</string> <string name="title_sub_setting">Группы</string>
@@ -288,26 +465,44 @@
<string name="sub_setting_filter">Название фильтра</string> <string name="sub_setting_filter">Название фильтра</string>
<string name="sub_setting_enable">Использовать обновление</string> <string name="sub_setting_enable">Использовать обновление</string>
<string name="sub_auto_update">Использовать автообновление</string> <string name="sub_auto_update">Использовать автообновление</string>
<string name="sub_allow_insecure_url">Разрешать незащищённые HTTP-адреса</string> <string
name="sub_allow_insecure_url"
>Разрешать незащищённые HTTP-адреса</string>
<string name="sub_setting_pre_profile">Предыдущий профиль прокси</string> <string name="sub_setting_pre_profile">Предыдущий профиль прокси</string>
<string name="sub_setting_next_profile">Следующий профиль прокси</string> <string name="sub_setting_next_profile">Следующий профиль прокси</string>
<string name="sub_setting_pre_profile_tip">Профиль должен быть уникальным</string> <string
name="sub_setting_pre_profile_tip"
>Профиль должен быть уникальным</string>
<string name="title_sub_update">Обновить подписку</string> <string name="title_sub_update">Обновить подписку</string>
<string name="title_ping_all_server">Проверить профили</string> <string name="title_ping_all_server">Проверить профили</string>
<string name="title_real_ping_all_server">Проверить задержку профилей</string> <string
name="title_real_ping_all_server"
>Проверить задержку профилей</string>
<string name="title_user_asset_setting">Файлы ресурсов</string> <string name="title_user_asset_setting">Файлы ресурсов</string>
<string name="title_sort_by_test_results">Сортировать по результатам теста</string> <string
name="title_sort_by_test_results"
>Сортировать по результатам теста</string>
<string name="title_filter_config">Фильтр профилей</string> <string name="title_filter_config">Фильтр профилей</string>
<string name="filter_config_all">Все</string> <string name="filter_config_all">Все</string>
<string name="title_del_duplicate_config_count">Удалено дубликатов профилей: %d</string> <string
name="title_del_duplicate_config_count"
>Удалено дубликатов профилей: %d</string>
<string name="title_del_config_count">Удалено профилей: %d</string> <string name="title_del_config_count">Удалено профилей: %d</string>
<string name="title_import_config_count">Импортировано профилей: %d</string> <string name="title_import_config_count">Импортировано профилей: %d</string>
<string name="title_export_config_count">Экспортировано профилей: %d</string> <string
name="title_export_config_count"
>Экспортировано профилей: %d</string>
<string name="title_update_config_count">Обновлено профилей: %d</string> <string name="title_update_config_count">Обновлено профилей: %d</string>
<string name="title_updating">Обновление…</string> <string name="title_updating">Обновление…</string>
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string> <string
<string name="title_update_subscription_no_subscription">Нет подписок</string> name="title_update_subscription_result"
<string name="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string> >Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
<string
name="title_update_subscription_no_subscription"
>Нет подписок</string>
<string
name="toast_server_not_found_in_group"
>Выбранный профиль не найден в текущей группе</string>
<string name="toast_fragment_not_available">Фрагмент недоступен</string> <string name="toast_fragment_not_available">Фрагмент недоступен</string>
<string name="title_locate_selected_config">Найти выбранный профиль</string> <string name="title_locate_selected_config">Найти выбранный профиль</string>
@@ -317,17 +512,33 @@
<!-- RoutingSettingActivity --> <!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">Доменная стратегия</string> <string name="routing_settings_domain_strategy">Доменная стратегия</string>
<string name="routing_settings_title">Маршрутизация</string> <string name="routing_settings_title">Маршрутизация</string>
<string name="routing_settings_tips">Введите требуемые домены/IP через запятую</string> <string
name="routing_settings_tips"
>Введите требуемые домены/IP через запятую</string>
<string name="routing_settings_save">Сохранить</string> <string name="routing_settings_save">Сохранить</string>
<string name="routing_settings_delete">Очистить</string> <string name="routing_settings_delete">Очистить</string>
<string name="routing_settings_rule_title">Настройка правил маршрутизации</string> <string
name="routing_settings_rule_title"
>Настройка правил маршрутизации</string>
<string name="routing_settings_add_rule">Добавить правило</string> <string name="routing_settings_add_rule">Добавить правило</string>
<string name="routing_settings_import_predefined_rulesets">Импорт набора правил</string> <string
<string name="routing_settings_import_rulesets_tip">Существующие правила будут удалены. Продолжить?</string> name="routing_settings_import_predefined_rulesets"
<string name="routing_settings_import_rulesets_from_clipboard">Импорт правил из буфера обмена</string> >Импорт набора правил</string>
<string name="routing_settings_import_rulesets_from_qrcode">Импорт правил из QR-кода</string> <string
<string name="routing_settings_export_rulesets_to_clipboard">Экспорт правил в буфер обмена</string> name="routing_settings_import_rulesets_tip"
<string name="routing_settings_locked">Постоянное (сохранится при импорте правил)</string> >Существующие правила будут удалены. Продолжить?</string>
<string
name="routing_settings_import_rulesets_from_clipboard"
>Импорт правил из буфера обмена</string>
<string
name="routing_settings_import_rulesets_from_qrcode"
>Импорт правил из QR-кода</string>
<string
name="routing_settings_export_rulesets_to_clipboard"
>Экспорт правил в буфер обмена</string>
<string
name="routing_settings_locked"
>Постоянное (сохранится при импорте правил)</string>
<string name="routing_settings_domain">Домен</string> <string name="routing_settings_domain">Домен</string>
<string name="routing_settings_ip">IP</string> <string name="routing_settings_ip">IP</string>
<string name="routing_settings_port">Порт</string> <string name="routing_settings_port">Порт</string>
@@ -340,44 +551,71 @@
<string name="connection_test_pending">Проверить соединение</string> <string name="connection_test_pending">Проверить соединение</string>
<string name="connection_test_testing">Проверка…</string> <string name="connection_test_testing">Проверка…</string>
<string name="connection_test_testing_count">Проверка профилей (%d)</string> <string name="connection_test_testing_count">Проверка профилей (%d)</string>
<string name="connection_test_available">Успешно: соединение заняло %d мс</string> <string name="connection_test_available">%d мс</string>
<string name="connection_test_error">Сбой проверки интернет-соединения: %s</string> <string
name="connection_test_error"
>Сбой проверки интернет-соединения: %s</string>
<string name="connection_test_fail">Интернет недоступен</string> <string name="connection_test_fail">Интернет недоступен</string>
<string name="connection_test_error_status_code">Код ошибки: #%d</string> <string name="connection_test_error_status_code">Код ошибки: #%d</string>
<string name="connection_connected">Соединено, нажмите для проверки</string> <string name="connection_connected">нажмите для проверки</string>
<string name="connection_not_connected">Нет соединения</string> <string name="connection_not_connected">Ожидаем действий</string>
<string name="connection_runing_task_left">Запущено проверок: %s</string> <string name="connection_updating_profiles">Обновление профилей…</string>
<string name="connection_runing_task_left">Проверено %s</string>
<string name="import_subscription_success">Подписка импортирована</string> <string name="import_subscription_success">Подписка импортирована</string>
<string name="import_subscription_failure">Невозможно импортировать подписку</string> <string
name="import_subscription_failure"
>Невозможно импортировать подписку</string>
<string name="title_fragment_settings">Настройки фрагментирования</string> <string name="title_fragment_settings">Настройки фрагментирования</string>
<string name="title_pref_fragment_packets">Фрагментирование пакетов</string> <string name="title_pref_fragment_packets">Фрагментирование пакетов</string>
<string name="title_pref_fragment_length">Длина фрагмента (от - до)</string> <string name="title_pref_fragment_length">Длина фрагмента (от - до)</string>
<string name="title_pref_fragment_interval">Интервал фрагментов (от - до)</string> <string
<string name="title_pref_fragment_enabled">Использовать фрагментирование</string> name="title_pref_fragment_interval"
>Интервал фрагментов (от - до)</string>
<string
name="title_pref_fragment_enabled"
>Использовать фрагментирование</string>
<string name="update_check_for_update">Проверить обновление</string> <string name="update_check_for_update">Проверить обновление</string>
<string name="update_already_latest_version">Установлена последняя версия</string> <string
name="update_already_latest_version"
>Установлена последняя версия</string>
<string name="update_new_version_found">Найдена новая версия: %s</string> <string name="update_new_version_found">Найдена новая версия: %s</string>
<string name="update_now">Обновить</string> <string name="update_now">Обновить</string>
<string name="update_check_pre_release">Искать предварительный выпуск</string> <string
name="update_check_pre_release"
>Искать предварительный выпуск</string>
<string name="update_checking_for_update">Проверка обновления…</string> <string name="update_checking_for_update">Проверка обновления…</string>
<string name="title_policy_group_type">Тип группы политик</string> <string name="title_policy_group_type">Тип группы политик</string>
<string name="title_policy_group_subscription_id">Из группы подписки</string> <string
<string name="title_policy_group_subscription_filter">Название фильтра</string> name="title_policy_group_subscription_id"
>Из группы подписки</string>
<string
name="title_policy_group_subscription_filter"
>Название фильтра</string>
<!-- BackupActivity --> <!-- BackupActivity -->
<string name="title_configuration_backup_restore">Резервное копирование</string> <string
<string name="title_configuration_backup">Резервирование конфигурации</string> name="title_configuration_backup_restore"
<string name="title_configuration_restore">Восстановление конфигурации</string> >Резервное копирование</string>
<string
name="title_configuration_backup"
>Резервирование конфигурации</string>
<string
name="title_configuration_restore"
>Восстановление конфигурации</string>
<string name="title_configuration_share">Поделиться конфигурацией</string> <string name="title_configuration_share">Поделиться конфигурацией</string>
<string name="title_webdav_config_setting">Настройки WebDAV</string> <string name="title_webdav_config_setting">Настройки WebDAV</string>
<string name="title_webdav_config_setting_unknown">Необходимо настроить WebDAV</string> <string
name="title_webdav_config_setting_unknown"
>Необходимо настроить WebDAV</string>
<string name="title_webdav_url">URL сервера</string> <string name="title_webdav_url">URL сервера</string>
<string name="title_webdav_user">Пользователь</string> <string name="title_webdav_user">Пользователь</string>
<string name="title_webdav_pass">Пароль</string> <string name="title_webdav_pass">Пароль</string>
<string name="title_webdav_remote_path">Удалённый путь (необязательно)</string> <string
name="title_webdav_remote_path"
>Удалённый путь (необязательно)</string>
<string-array name="share_method"> <string-array name="share_method">
<item>QR-код</item> <item>QR-код</item>
@@ -1,7 +1,3 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="TabLayoutTextStyle" parent="TextAppearance.Design.Tab">
<item name="textAllCaps">false</item>
</style>
</resources> </resources>
+35 -23
View File
@@ -1,54 +1,66 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="colorPing">#1565C0</color> <color name="colorPing">#006494</color>
<color name="colorPingRed">#FF0099</color> <color name="colorPingGood">#2E7D32</color>
<color name="colorConfigType">#1976D2</color> <color name="colorPingMedium">#9A6700</color>
<color name="status_connected">#4CAF50</color>
<color name="colorPingRed">#BA1A1A</color>
<color name="colorConfigType">#1565C0</color>
<color name="colorWhite">#FFFFFF</color> <color name="colorWhite">#FFFFFF</color>
<color name="color_fab_active">#000000</color> <color name="color_fab_active">@color/md_theme_primary</color>
<color name="color_fab_inactive">#9C9C9C</color> <color name="color_fab_inactive">@color/md_theme_secondaryContainer</color>
<color name="divider_color_light">#E0E0E0</color> <color name="divider_color_light">@color/md_theme_outlineVariant</color>
<color name="server_list_divider">#22CAC4D0</color>
<color name="colorIndicator">@color/md_theme_primary</color> <color name="colorIndicator">@color/md_theme_primary</color>
<color name="md_theme_primary">#000000</color> <!-- M3 Light scheme — Purple/Violet tonal palette -->
<color name="md_theme_primary">#6750A4</color>
<color name="md_theme_onPrimary">#FFFFFF</color> <color name="md_theme_onPrimary">#FFFFFF</color>
<color name="md_theme_primaryContainer">#E0E0E0</color> <color name="md_theme_primaryContainer">#EADDFF</color>
<color name="md_theme_onPrimaryContainer">#000000</color> <color name="md_theme_onPrimaryContainer">#21005D</color>
<color name="md_theme_secondary">#1976D2</color> <color name="md_theme_secondary">#625B71</color>
<color name="md_theme_onSecondary">#FFFFFF</color> <color name="md_theme_onSecondary">#FFFFFF</color>
<color name="md_theme_secondaryContainer">#FFE8D6</color> <color name="md_theme_secondaryContainer">#E8DEF8</color>
<color name="md_theme_onSecondaryContainer">#2B1700</color> <color name="md_theme_onSecondaryContainer">#1D192B</color>
<color name="md_theme_tertiary">#1565C0</color> <color name="md_theme_tertiary">#7D5260</color>
<color name="md_theme_onTertiary">#FFFFFF</color> <color name="md_theme_onTertiary">#FFFFFF</color>
<color name="md_theme_tertiaryContainer">#BBDEFB</color> <color name="md_theme_tertiaryContainer">#FFD8E4</color>
<color name="md_theme_onTertiaryContainer">#00201A</color> <color name="md_theme_onTertiaryContainer">#31111D</color>
<!-- Error colors --> <!-- Error colors -->
<color name="md_theme_error">#BA1A1A</color> <color name="md_theme_error">#B3261E</color>
<color name="md_theme_errorContainer">#FFDAD6</color> <color name="md_theme_errorContainer">#F9DEDC</color>
<color name="md_theme_onError">#FFFFFF</color> <color name="md_theme_onError">#FFFFFF</color>
<color name="md_theme_onErrorContainer">#410002</color> <color name="md_theme_onErrorContainer">#410E0B</color>
<!-- Background colors --> <!-- Background colors -->
<color name="md_theme_background">#FFFFFF</color> <color name="md_theme_background">#FFFBFE</color>
<color name="md_theme_onBackground">#1C1B1F</color> <color name="md_theme_onBackground">#1C1B1F</color>
<!-- Surface colors --> <!-- Surface colors — M3 tonal surface hierarchy -->
<color name="md_theme_surface">#FFFFFF</color> <color name="md_theme_surface">#FFFBFE</color>
<color name="md_theme_onSurface">#1C1B1F</color> <color name="md_theme_onSurface">#1C1B1F</color>
<color name="md_theme_surfaceVariant">#E7E0EC</color> <color name="md_theme_surfaceVariant">#E7E0EC</color>
<color name="md_theme_onSurfaceVariant">#49454F</color> <color name="md_theme_onSurfaceVariant">#49454F</color>
<color name="md_theme_inverseSurface">#313033</color> <color name="md_theme_inverseSurface">#313033</color>
<color name="md_theme_inverseOnSurface">#F4EFF4</color> <color name="md_theme_inverseOnSurface">#F4EFF4</color>
<!-- Surface containers — M3 elevation tones -->
<color name="md_theme_surfaceContainerLowest">#FFFFFF</color>
<color name="md_theme_surfaceContainerLow">#F7F2FA</color>
<color name="md_theme_surfaceContainer">#F3EDF7</color>
<color name="md_theme_surfaceContainerHigh">#ECE6F0</color>
<color name="md_theme_surfaceContainerHighest">#E6E0E9</color>
<!-- Outline colors --> <!-- Outline colors -->
<color name="md_theme_outline">#79747E</color> <color name="md_theme_outline">#79747E</color>
<color name="md_theme_outlineVariant">#CAC4D0</color> <color name="md_theme_outlineVariant">#CAC4D0</color>
<!-- Other colors --> <!-- Other colors -->
<color name="md_theme_inversePrimary">#C0C0C0</color> <color name="md_theme_inversePrimary">#D0BCFF</color>
<color name="md_theme_shadow">#000000</color> <color name="md_theme_shadow">#000000</color>
<color name="md_theme_surfaceTint">#000000</color> <color name="md_theme_surfaceTint">#6750A4</color>
<color name="md_theme_scrim">#000000</color> <color name="md_theme_scrim">#000000</color>
</resources> </resources>
+5 -2
View File
@@ -146,7 +146,7 @@
<string name="menu_item_import_proxy_app">Import from Clipboard</string> <string name="menu_item_import_proxy_app">Import from Clipboard</string>
<string name="per_app_proxy_settings">Раздельное туннелирование</string> <string name="per_app_proxy_settings">Раздельное туннелирование</string>
<string name="per_app_proxy_settings_enable">Включить</string> <string name="per_app_proxy_settings_enable">Включить</string>
<string name="split_tunneling_description">Выберите приложения, которые будут использовать VPN. В режиме обхода — выбранные приложения НЕ используют VPN.</string> <string name="split_tunneling_description">Выберите приложения, которые будут использовать VPN — остальные приложения, например банки, будут идти напрямую через ваш домашний интернет.\n\nВ режиме обхода всё зеркально — выбранные приложения не используют VPN.</string>
<!-- Preferences --> <!-- Preferences -->
<string name="title_settings">Settings</string> <string name="title_settings">Settings</string>
@@ -163,6 +163,8 @@
<string name="title_pref_show_copy_button">Show copy button</string> <string name="title_pref_show_copy_button">Show copy button</string>
<string name="summary_pref_show_copy_button">Show button to copy server configuration to clipboard</string> <string name="summary_pref_show_copy_button">Show button to copy server configuration to clipboard</string>
<string name="title_pref_show_server_ip">Show server IP / host</string>
<string name="summary_pref_show_server_ip">Display the server IP address or host under the server name</string>
<string name="title_mux_settings">Mux Settings</string> <string name="title_mux_settings">Mux Settings</string>
<string name="title_pref_mux_enabled">Enable Mux</string> <string name="title_pref_mux_enabled">Enable Mux</string>
@@ -353,7 +355,8 @@
<string name="connection_test_error_status_code">Error code: #%d</string> <string name="connection_test_error_status_code">Error code: #%d</string>
<string name="connection_connected">Connected, tap to check connection</string> <string name="connection_connected">Connected, tap to check connection</string>
<string name="connection_not_connected">Готово к подключению</string> <string name="connection_not_connected">Готово к подключению</string>
<string name="connection_runing_task_left">Проверено успешно: %s</string> <string name="connection_updating_profiles">Updating profiles…</string>
<string name="connection_runing_task_left">Проверено %s</string>
<string name="import_subscription_success">Subscription imported Successfully</string> <string name="import_subscription_success">Subscription imported Successfully</string>
<string name="import_subscription_failure">Import subscription failed</string> <string name="import_subscription_failure">Import subscription failed</string>
+74 -20
View File
@@ -1,58 +1,65 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Common base theme: put all shared items here so day/night can reuse --> <!-- Common base theme -->
<style name="AppThemeBase" parent="Theme.Material3.DayNight"> <style name="AppThemeBase" parent="Theme.Material3.DayNight">
<!-- Primary colors - main tone: black --> <!-- Primary -->
<item name="colorPrimary">@color/md_theme_primary</item> <item name="colorPrimary">@color/md_theme_primary</item>
<item name="colorOnPrimary">@color/md_theme_onPrimary</item> <item name="colorOnPrimary">@color/md_theme_onPrimary</item>
<item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item> <item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item> <item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item>
<!-- Secondary -->
<item name="colorSecondary">@color/md_theme_secondary</item> <item name="colorSecondary">@color/md_theme_secondary</item>
<item name="colorOnSecondary">@color/md_theme_onSecondary</item> <item name="colorOnSecondary">@color/md_theme_onSecondary</item>
<item name="colorSecondaryContainer">@color/md_theme_secondaryContainer</item> <item name="colorSecondaryContainer">@color/md_theme_secondaryContainer</item>
<item name="colorOnSecondaryContainer">@color/md_theme_onSecondaryContainer</item> <item name="colorOnSecondaryContainer">@color/md_theme_onSecondaryContainer</item>
<!-- Tertiary -->
<item name="colorTertiary">@color/md_theme_tertiary</item> <item name="colorTertiary">@color/md_theme_tertiary</item>
<item name="colorOnTertiary">@color/md_theme_onTertiary</item> <item name="colorOnTertiary">@color/md_theme_onTertiary</item>
<item name="colorTertiaryContainer">@color/md_theme_tertiaryContainer</item> <item name="colorTertiaryContainer">@color/md_theme_tertiaryContainer</item>
<item name="colorOnTertiaryContainer">@color/md_theme_onTertiaryContainer</item> <item name="colorOnTertiaryContainer">@color/md_theme_onTertiaryContainer</item>
<!-- Error colors --> <!-- Error -->
<item name="colorError">@color/md_theme_error</item> <item name="colorError">@color/md_theme_error</item>
<item name="colorOnError">@color/md_theme_onError</item> <item name="colorOnError">@color/md_theme_onError</item>
<item name="colorErrorContainer">@color/md_theme_errorContainer</item> <item name="colorErrorContainer">@color/md_theme_errorContainer</item>
<item name="colorOnErrorContainer">@color/md_theme_onErrorContainer</item> <item name="colorOnErrorContainer">@color/md_theme_onErrorContainer</item>
<!-- Surface colors --> <!-- Surface -->
<item name="colorSurface">@color/md_theme_surface</item> <item name="colorSurface">@color/md_theme_surface</item>
<item name="colorOnSurface">@color/md_theme_onSurface</item> <item name="colorOnSurface">@color/md_theme_onSurface</item>
<item name="colorSurfaceVariant">@color/md_theme_surfaceVariant</item> <item name="colorSurfaceVariant">@color/md_theme_surfaceVariant</item>
<item name="colorOnSurfaceVariant">@color/md_theme_onSurfaceVariant</item> <item name="colorOnSurfaceVariant">@color/md_theme_onSurfaceVariant</item>
<item name="colorSurfaceInverse">@color/md_theme_inverseSurface</item> <item name="colorSurfaceInverse">@color/md_theme_inverseSurface</item>
<item name="colorOnSurfaceInverse">@color/md_theme_inverseOnSurface</item> <item name="colorOnSurfaceInverse">@color/md_theme_inverseOnSurface</item>
<item name="colorSurfaceContainer">@color/md_theme_surface</item> <item name="colorSurfaceContainer">@color/md_theme_surfaceContainer</item>
<item name="colorSurfaceContainerHigh">@color/md_theme_surface</item> <item name="colorSurfaceContainerHigh">@color/md_theme_surfaceContainerHigh</item>
<item name="colorSurfaceContainerHighest">@color/md_theme_surface</item> <item name="colorSurfaceContainerHighest">@color/md_theme_surfaceContainerHighest</item>
<item name="colorSurfaceContainerLow">@color/md_theme_surface</item> <item name="colorSurfaceContainerLow">@color/md_theme_surfaceContainerLow</item>
<item name="colorSurfaceContainerLowest">@color/md_theme_surface</item> <item name="colorSurfaceContainerLowest">@color/md_theme_surfaceContainerLowest</item>
<!-- Background colors --> <!-- Background -->
<item name="android:colorBackground">@color/md_theme_background</item> <item name="android:colorBackground">@color/md_theme_background</item>
<item name="colorOnBackground">@color/md_theme_onBackground</item> <item name="colorOnBackground">@color/md_theme_onBackground</item>
<!-- Outline colors --> <!-- Outline -->
<item name="colorOutline">@color/md_theme_outline</item> <item name="colorOutline">@color/md_theme_outline</item>
<item name="colorOutlineVariant">@color/md_theme_outlineVariant</item> <item name="colorOutlineVariant">@color/md_theme_outlineVariant</item>
<!-- Other colors --> <!-- Misc -->
<item name="colorPrimaryInverse">@color/md_theme_inversePrimary</item> <item name="colorPrimaryInverse">@color/md_theme_inversePrimary</item>
<!-- Status bar and navigation bar - system bars --> <!-- System bars — transparent so edge-to-edge works -->
<item name="android:statusBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@color/md_theme_surface</item> <item name="android:navigationBarColor">@android:color/transparent</item>
<!-- Typography: use Roboto (Google's Material font) everywhere --> <!-- Shape — M3 uses larger corner radii -->
<item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.App.SmallComponent</item>
<item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item>
<item name="shapeAppearanceLargeComponent">@style/ShapeAppearance.App.LargeComponent</item>
<!-- Typography: Roboto -->
<item name="android:fontFamily">sans-serif</item> <item name="android:fontFamily">sans-serif</item>
<item name="fontFamily">sans-serif</item> <item name="fontFamily">sans-serif</item>
<item name="android:editTextStyle">@style/RobotoEditTextStyle</item> <item name="android:editTextStyle">@style/RobotoEditTextStyle</item>
@@ -60,7 +67,23 @@
<item name="android:dialogTheme">@style/RobotoAlertDialogTheme</item> <item name="android:dialogTheme">@style/RobotoAlertDialogTheme</item>
</style> </style>
<style name="RobotoEditTextStyle" parent="Widget.AppCompat.EditText"> <!-- M3 Shape tokens -->
<style name="ShapeAppearance.App.SmallComponent" parent="ShapeAppearance.Material3.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">8dp</item>
</style>
<style name="ShapeAppearance.App.MediumComponent" parent="ShapeAppearance.Material3.MediumComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">12dp</item>
</style>
<style name="ShapeAppearance.App.LargeComponent" parent="ShapeAppearance.Material3.LargeComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">16dp</item>
</style>
<style name="RobotoEditTextStyle" parent="Widget.Material3.TextInputEditText.OutlinedBox">
<item name="android:fontFamily">sans-serif</item> <item name="android:fontFamily">sans-serif</item>
<item name="android:textSize">16sp</item> <item name="android:textSize">16sp</item>
</style> </style>
@@ -68,16 +91,15 @@
<style name="RobotoAlertDialogTheme" parent="ThemeOverlay.Material3.MaterialAlertDialog"> <style name="RobotoAlertDialogTheme" parent="ThemeOverlay.Material3.MaterialAlertDialog">
<item name="android:fontFamily">sans-serif</item> <item name="android:fontFamily">sans-serif</item>
<item name="fontFamily">sans-serif</item> <item name="fontFamily">sans-serif</item>
<item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item>
</style> </style>
<!-- Day/Night theme: inherit common values and set light-mode-specific items --> <!-- Light -->
<style name="AppThemeDayNight" parent="AppThemeBase"> <style name="AppThemeDayNight" parent="AppThemeBase">
<!-- day/night-specific overrides (light/default) -->
<item name="android:windowLightStatusBar">true</item> <item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar" tools:targetApi="27">true</item> <item name="android:windowLightNavigationBar" tools:targetApi="27">true</item>
</style> </style>
<!-- Theme without ActionBar -->
<style name="AppThemeDayNight.NoActionBar" parent="AppThemeDayNight"> <style name="AppThemeDayNight.NoActionBar" parent="AppThemeDayNight">
<item name="windowActionBar">false</item> <item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item> <item name="windowNoTitle">true</item>
@@ -89,8 +111,40 @@
<item name="android:windowIsTranslucent">true</item> <item name="android:windowIsTranslucent">true</item>
</style> </style>
<!-- Switch uses primary color -->
<style name="BrandedSwitch" parent="AppThemeDayNight"> <style name="BrandedSwitch" parent="AppThemeDayNight">
<item name="colorPrimary">@color/color_fab_active</item> <item name="colorPrimary">@color/md_theme_primary</item>
</style>
<!-- Rounded-square FAB shape overlay (16dp corners) -->
<style name="ShapeAppearance.App.RoundedSquare" parent="ShapeAppearance.Material3.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">16dp</item>
</style>
<!-- Square icon button: no insets, no minHeight, no text -->
<style name="Widget.ActionSquareButton" parent="Widget.Material3.Button">
<item name="android:minWidth">0dp</item>
<item name="android:minHeight">0dp</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="android:insetLeft">0dp</item>
<item name="android:insetRight">0dp</item>
<item name="android:paddingStart">0dp</item>
<item name="android:paddingEnd">0dp</item>
<item name="android:paddingTop">0dp</item>
<item name="android:paddingBottom">0dp</item>
<item name="iconGravity">textStart</item>
<item name="iconPadding">0dp</item>
<item name="android:text"></item>
<item name="shapeAppearance">@style/ShapeAppearance.App.RoundedSquare</item>
<item name="elevation">0dp</item>
<item name="android:stateListAnimator">@null</item>
</style>
<!-- Tab label -->
<style name="TabLayoutTextStyle" parent="TextAppearance.Material3.LabelLarge">
<item name="android:textSize">14sp</item>
</style> </style>
</resources> </resources>
@@ -34,6 +34,12 @@
android:summary="@string/summary_pref_show_copy_button" android:summary="@string/summary_pref_show_copy_button"
android:title="@string/title_pref_show_copy_button" /> android:title="@string/title_pref_show_copy_button" />
<CheckBoxPreference
android:key="pref_show_server_ip"
android:defaultValue="false"
android:summary="@string/summary_pref_show_server_ip"
android:title="@string/title_pref_show_server_ip" />
<ListPreference <ListPreference
android:defaultValue="auto" android:defaultValue="auto"
android:entries="@array/language_select" android:entries="@array/language_select"
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name" translatable="false">olcNG</string> <string name="app_name" translatable="false">olcng</string>
</resources> </resources>