23 Commits

Author SHA1 Message Date
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 1741 additions and 999 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="app_name" type="string">olcNG</item>
<item name="app_name" type="string">olcng</item>
</resources>
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">olcNG</string>
<string name="app_name" translatable="false">olcng</string>
</resources>
@@ -74,6 +74,7 @@ object AppConfig {
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_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_SUBSCRIPTIONS_BOTTOM = "pref_subscriptions_bottom"
@@ -3,8 +3,8 @@ package xyz.zarazaex.olc.dto
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
fun getTestDelayString(): String {
return when {
testDelayMillis == 0L -> ""
testDelayMillis < 0L -> "Error"
testDelayMillis == 0L -> "—ms"
testDelayMillis < 0L -> "-ms"
else -> "${testDelayMillis}ms"
}
}
@@ -2,5 +2,7 @@ package xyz.zarazaex.olc.dto
data class ServersCache(
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)
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 {
val httpPort = SettingsManager.getHttpPort()
HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout, httpPort)
HttpUtil.getUrlContentWithUserAgent(url, userAgent, proxyTimeout, httpPort)
} catch (e: Exception) {
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
""
}
if (configText.isEmpty()) {
configText = try {
HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout)
HttpUtil.getUrlContentWithUserAgent(url, userAgent, directTimeout)
} catch (e: Exception) {
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]. */
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). */
fun getCountryFilter(): Set<String> =
settingsStorage.decodeStringSet("pref_country_filter") ?: emptySet()
/** Loads the user's country filter preference (set of ISO codes to EXCLUDE, empty = show all). */
fun getCountryFilter(): Set<String> {
// 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. */
fun setCountryFilter(codes: Set<String>) {
@@ -330,7 +330,7 @@ object V2RayServiceManager {
// Only fetch IP info if the delay test was successful
if (time >= 0) {
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.dto.PingProgressUpdate
import xyz.zarazaex.olc.dto.PingResultItem
import xyz.zarazaex.olc.handler.MmkvManager
import xyz.zarazaex.olc.handler.SettingsManager
import xyz.zarazaex.olc.handler.V2RayNativeManager
import xyz.zarazaex.olc.handler.V2rayConfigManager
@@ -57,8 +58,15 @@ class RealPingWorkerService(
scope.launch(Dispatchers.IO) {
try {
// Prepare configurations in parallel for faster startup
val shuffledGuids = guids.shuffled()
// Prepare configurations in parallel for faster startup.
// 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 ->
async(Dispatchers.IO) {
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
@@ -135,7 +143,7 @@ class RealPingWorkerService(
private fun sendBatchUpdate(update: PingProgressUpdate) {
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_BATCH, update)
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "${update.finished} / ${update.total}")
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "${update.finished}/${update.total}")
}
fun cancel() {
@@ -123,6 +123,9 @@ abstract class BaseActivity : AppCompatActivity() {
setSupportActionBar(it)
supportActionBar?.setDisplayHomeAsUpEnabled(showHomeAsUp)
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)
}
progressBar = findViewById(R.id.progress_bar)
@@ -12,7 +12,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar
import xyz.zarazaex.olc.AppConfig
import xyz.zarazaex.olc.R
import xyz.zarazaex.olc.contracts.MainAdapterListener
@@ -65,7 +64,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
} else {
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
itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter, allowSwipe = false))
@@ -76,19 +75,21 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
// // Set the distance to trigger sync to 160dp
// binding.refreshLayout.setDistanceToTriggerSync((160 * resources.displayMetrics.density).toInt())
mainViewModel.updateListAction.observe(viewLifecycleOwner) { index ->
if (mainViewModel.subscriptionId != subId) {
return@observe
}
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
adapter.setData(mainViewModel.serversCache, index)
// Each fragment subscribes independently to the shared flow and filters its own subId.
// No onResume subscription switch needed — the active fragment's subId is always correct.
lifecycleScope.launch {
mainViewModel.serverListFlow.collect { list ->
if (mainViewModel.subscriptionId == subId) {
adapter.setData(list)
}
}
}
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
}
override fun 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)
}
@@ -219,7 +220,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
*/
private fun removeServerSub(guid: String, position: Int) {
mainViewModel.removeServer(guid)
adapter.removeServerSub(guid, position)
// adapter updates automatically via serverListFlow
}
/**
@@ -229,15 +230,15 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
*/
private fun setSelectServer(guid: String) {
val selected = MmkvManager.getSelectServer()
if (guid != selected) {
if (guid == selected) {
MmkvManager.setSelectServer("")
} else {
MmkvManager.setSelectServer(guid)
val fromPosition = mainViewModel.getPosition(selected.orEmpty())
val toPosition = mainViewModel.getPosition(guid)
adapter.setSelectServer(fromPosition, toPosition)
if (mainViewModel.isRunning.value == true) {
ownerActivity.restartV2Ray()
}
}
// Republish snapshot so DiffUtil picks up the selection change in card background
mainViewModel.reloadServerList()
if (mainViewModel.isRunning.value == true) {
ownerActivity.restartV2Ray()
}
}
@@ -304,9 +305,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
return
}
// Find the position of the selected server
val serversCache = mainViewModel.serversCache
val position = serversCache.indexOfFirst { it.guid == selectedGuid }
val position = mainViewModel.serverListFlow.value.indexOfFirst { it.guid == selectedGuid }
val recyclerView = binding.recyclerView
if (position >= 0) {
@@ -327,4 +326,4 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
ownerActivity.toast(R.string.toast_server_not_found_in_group)
}
}
}
}
@@ -2,6 +2,7 @@ package xyz.zarazaex.olc.ui
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.net.Uri
import android.net.VpnService
import android.os.Bundle
@@ -11,6 +12,8 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.material.color.MaterialColors
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
@@ -53,6 +56,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
private var isLiteTesting = false
private var easterEggClickCount = 0
private var isEasterEggActive = false
private var liteActionJob: kotlinx.coroutines.Job? = null
/** Был ли VPN уже запущен в предыдущем колбэке — чтобы детектировать момент подключения */
private var wasRunning = false
@@ -107,21 +111,33 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
toggle.syncState()
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 ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(0, systemBars.top, 0, systemBars.bottom)
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))
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))
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))
binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START)
}
@@ -200,40 +216,53 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
// Молния — стоп-кнопка, всегда активна во время теста
binding.btnSummaryLite.isEnabled = true
binding.btnSummaryLite.alpha = 1.0f
binding.btnSummaryLite.setImageResource(R.drawable.ic_stop_24dp)
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active))
binding.btnSummaryLite.setIconResource(R.drawable.ic_stop_24dp)
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimaryContainer, 0)
)
} else {
setButtonsEnabled(true)
binding.btnSummaryLite.setImageResource(R.drawable.bolt_24)
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
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)
)
if (!isLiteTesting) {
showStatus("Проверка завершена")
}
}
}
mainViewModel.liteTestFinished.observe(this) { finished ->
if (finished && isLiteTesting) {
isLiteTesting = false
mainViewModel.sortByTestResults()
val firstReachable = mainViewModel.serversCache.firstOrNull { cache ->
(MmkvManager.decodeServerAffiliationInfo(cache.guid)?.testDelayMillis ?: 0L) > 0L
}
// Ищем лучший сервер ДО сортировки, прямо из текущего cache
val firstReachable = mainViewModel.serversCache
.filter { (MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: 0L) > 0L }
.minByOrNull { MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: Long.MAX_VALUE }
if (firstReachable != null) {
MmkvManager.setSelectServer(firstReachable.guid)
mainViewModel.reloadServerList() // reload AFTER selection so indicator renders correctly
}
mainViewModel.suppressPinSelected = false
mainViewModel.sortByTestResults()
mainViewModel.reloadServerList()
if (firstReachable != null) {
showStatus("Подключаемся к быстрейшему серверу")
// Блокируем кнопки на время подключения
setButtonsEnabled(false)
applyRunningState(isLoading = true, isRunning = false)
startV2RayWithPermission()
} else {
mainViewModel.reloadServerList()
showStatus("Нет доступных серверов!")
setButtonsEnabled(true)
}
}
}
mainViewModel.isRunning.observe(this) { isRunning ->
applyRunningState(false, isRunning)
if (!isFabOperationInProgress) {
applyRunningState(false, isRunning)
}
// Как только VPN только что подключился — обновляем подписки через него
if (isRunning && !wasRunning) {
updateSubsViaVpn()
@@ -299,15 +328,19 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
}
private fun handleFabAction() {
// Если идёт подключение (isLoading) — позволяем прервать и остановить сервис
if (isFabOperationInProgress) {
Log.d(AppConfig.TAG, "FAB: cancel in-progress, stopping service")
isFabOperationInProgress = false
lifecycleScope.launch {
V2RayServiceManager.stopVService(this@MainActivity)
}
return
}
isFabOperationInProgress = true
val isRunning = mainViewModel.isRunning.value == true
// Блокируем все кнопки сразу
setButtonsEnabled(false)
applyRunningState(isLoading = true, isRunning = false)
lifecycleScope.launch {
@@ -338,11 +371,21 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
}
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()
showStatus("Тест остановлен")
mainViewModel.suppressPinSelected = 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
}
@@ -351,10 +394,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
}
isFabOperationInProgress = true
// Блокируем все кнопки сразу при нажатии
setButtonsEnabled(false)
lifecycleScope.launch {
liteActionJob = lifecycleScope.launch {
try {
if (mainViewModel.isRunning.value == true) {
V2RayServiceManager.stopVService(this@MainActivity)
@@ -363,33 +403,45 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
showStatus("Обновление профилей...")
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
mainViewModel.suppressPinSelected = true
launch(Dispatchers.IO) {
val result = mainViewModel.updateConfigViaSubAll()
val removed = mainViewModel.removeDuplicateByIpAll()
withContext(Dispatchers.Main) {
mainViewModel.reloadServerList()
if (result.configCount > 0) {
val status = if (removed > 0)
"Обновлено ${result.configCount} профилей, удалено $removed дубл. IP. Запуск теста..."
else
"Обновлено ${result.configCount} профилей. Запуск теста..."
showStatus(status)
} else {
showStatus("Запуск теста...")
}
hideLoading()
val result = withContext(Dispatchers.IO) { mainViewModel.updateConfigViaSubAll() }
val removed = withContext(Dispatchers.IO) { mainViewModel.removeDuplicateByIpAll() }
showStatus("Выполняется замер задержки. Ожидаем завершения...")
mainViewModel.testAllRealPing()
}
mainViewModel.reloadServerList()
if (result.configCount > 0) {
val status = if (removed > 0)
"Обновлено ${result.configCount} профилей, удалено $removed дубл. IP. Запуск теста..."
else
"Обновлено ${result.configCount} профилей. Запуск теста..."
showStatus(status)
} else {
showStatus("Запуск теста...")
}
delay(1500)
hideLoading()
showStatus("Выполняется замер задержки. Ожидаем завершения...")
mainViewModel.testAllRealPing()
} catch (e: kotlinx.coroutines.CancellationException) {
// Пользователь нажал стоп — уже обработано выше
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Error in handleLiteAction", e)
isLiteTesting = false
hideLoading()
} finally {
isFabOperationInProgress = false
liteActionJob = null
}
}
}
@@ -420,6 +472,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
return
}
isFabOperationInProgress = true
applyRunningState(isLoading = true, isRunning = false)
lifecycleScope.launch {
try {
@@ -427,10 +480,16 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
V2RayServiceManager.stopVService(this@MainActivity)
delay(1000)
}
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
// Сервер был снят с выбора — просто остановились, разблокируем UI
applyRunningState(isLoading = false, isRunning = false)
return@launch
}
startV2Ray()
delay(1000)
} catch (e: Exception) {
Log.e(AppConfig.TAG, "Error in restartV2Ray", e)
applyRunningState(isLoading = false, isRunning = mainViewModel.isRunning.value == true)
} finally {
isFabOperationInProgress = false
}
@@ -470,33 +529,79 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
}
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) {
// Идёт процесс подключения/отключения — блокируем всё
setButtonsEnabled(false)
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
// Во время подключения: только FAB доступен для отмены, всё остальное заблокировано
binding.fab.isEnabled = true
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
}
if (isRunning) {
// Подключены: только FAB (отключить) активен, остальное блокируем
setSecondaryButtonsEnabled(false)
binding.fab.isEnabled = true
binding.fab.alpha = 1.0f
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)
setTestState(getString(R.string.connection_connected))
binding.layoutTest.isFocusable = true
setStatusDot(DotState.CONNECTED)
} else {
setButtonsEnabled(true)
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
binding.fab.backgroundTintList = accentColor()
binding.btnSummaryLite.backgroundTintList = secContainer
binding.fab.contentDescription = getString(R.string.tasker_start_service)
setTestState(getString(R.string.connection_not_connected))
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() {
super.onResume()
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
@@ -509,6 +614,13 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
override fun onCreateOptionsMenu(menu: Menu): Boolean {
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 searchView = searchItem.actionView as SearchView
@@ -525,6 +637,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
searchView.alpha = 0f
searchView.animate().alpha(1f).setDuration(220).start()
return true
}
@@ -547,7 +661,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
}
R.id.sub_update -> {
setButtonsEnabled(false)
setSecondaryButtonsEnabled(false)
importConfigViaSub()
true
}
@@ -672,6 +786,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
private fun importAllSubsOnStartup() {
showLoading()
setTestState(getString(R.string.connection_updating_profiles))
lifecycleScope.launch(Dispatchers.IO) {
val result = AngConfigManager.updateConfigViaSubAll()
val removed = mainViewModel.removeDuplicateByIpAll()
@@ -694,6 +809,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
*/
fun importConfigViaSub(): Boolean {
showLoading()
setTestState(getString(R.string.connection_updating_profiles))
lifecycleScope.launch(Dispatchers.IO) {
val result = mainViewModel.updateConfigViaSubAll()
@@ -899,14 +1015,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
val codes = allCountriesMap.keys.toTypedArray()
val labels = allCountriesMap.values.toTypedArray()
// In exclude mode: checked = should be EXCLUDED
// currentFilter stores included set (empty = show all)
// 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 }
// currentFilter stores excluded set (empty = show all)
val checked = BooleanArray(codes.size) { codes[it] in currentFilter }
val dialog = MaterialAlertDialogBuilder(this@MainActivity)
.setTitle("Исключить страны")
@@ -915,9 +1025,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
}
.setPositiveButton("Применить") { _, _ ->
val excluded = codes.filterIndexed { i, _ -> checked[i] }.toSet()
val included = if (excluded.isEmpty()) emptySet()
else allCodes - excluded
mainViewModel.applyCountryFilter(included)
mainViewModel.applyCountryFilter(excluded)
val msg = if (excluded.isEmpty()) "Показаны все страны"
else "Скрыто: ${excluded.joinToString { CountryDetector.codeToFlag(it) }}"
showStatus(msg)
@@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import java.util.Collections
import xyz.zarazaex.olc.AppConfig
import xyz.zarazaex.olc.R
@@ -35,7 +36,11 @@ class MainRecyclerAdapter(
MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
private val showCopyButton =
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 minReachablePing: Long? = null
private var maxReachablePing: Long? = null
private var recyclerView: RecyclerView? = null
override fun onAttachedToRecyclerView(rv: RecyclerView) {
@@ -54,58 +59,51 @@ class MainRecyclerAdapter(
}
@SuppressLint("NotifyDataSetChanged")
fun setData(newData: MutableList<ServersCache>?, position: Int = -1) {
val parsedNewData = newData?.toList() ?: emptyList()
fun setData(newData: List<ServersCache>) {
val oldData = data
val parsedNewData = newData
if (data.isEmpty() || parsedNewData.isEmpty() || position >= 0) {
if (oldData.isEmpty() || parsedNewData.isEmpty()) {
data = parsedNewData.toMutableList()
if (position >= 0 && position in data.indices) {
notifyItemChanged(position)
} else {
notifyDataSetChanged()
}
recomputePingRange()
notifyDataSetChanged()
return
}
val oldData = data
val lm = recyclerView?.layoutManager as? androidx.recyclerview.widget.LinearLayoutManager
val firstVisible = lm?.findFirstVisibleItemPosition()?.coerceAtLeast(0) ?: 0
val isAtTop = firstVisible == 0 && (lm?.findViewByPosition(0)?.top ?: 0) >= 0
val firstVisibleGuid = if (!isAtTop) oldData.getOrNull(firstVisible)?.guid else null
val diffResult =
androidx.recyclerview.widget.DiffUtil.calculateDiff(
object : androidx.recyclerview.widget.DiffUtil.Callback() {
override fun getOldListSize() = oldData.size
override fun getNewListSize() = parsedNewData.size
val diffResult = androidx.recyclerview.widget.DiffUtil.calculateDiff(
object : androidx.recyclerview.widget.DiffUtil.Callback() {
override fun getOldListSize() = oldData.size
override fun getNewListSize() = parsedNewData.size
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldData[oldPos].guid == parsedNewData[newPos].guid
}
override fun areItemsTheSame(oldPos: Int, newPos: Int) =
oldData[oldPos].guid == parsedNewData[newPos].guid
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
val oldProfile = oldData[oldPos].profile
val newProfile = parsedNewData[newPos].profile
val oldGuid = oldData[oldPos].guid
val newGuid = parsedNewData[newPos].guid
return oldProfile == newProfile &&
oldProfile.isFavorite == newProfile.isFavorite &&
(oldGuid == MmkvManager.getSelectServer()) == (newGuid == MmkvManager.getSelectServer()) &&
MmkvManager.decodeServerAffiliationInfo(oldGuid)?.testDelayMillis ==
MmkvManager.decodeServerAffiliationInfo(newGuid)?.testDelayMillis
}
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
val old = oldData[oldPos]
val new = parsedNewData[newPos]
return old.profile == new.profile &&
old.profile.isFavorite == new.profile.isFavorite &&
old.isSelected == new.isSelected &&
old.testDelayMillis == new.testDelayMillis
}
override fun getChangePayload(oldPos: Int, newPos: Int): Any? {
if (oldData[oldPos].profile.isFavorite != parsedNewData[newPos].profile.isFavorite) {
return PAYLOAD_FAVORITE
}
return super.getChangePayload(oldPos, newPos)
}
},
true
)
override fun getChangePayload(oldPos: Int, newPos: Int): Any? {
if (oldData[oldPos].profile.isFavorite != parsedNewData[newPos].profile.isFavorite) {
return PAYLOAD_FAVORITE
}
return super.getChangePayload(oldPos, newPos)
}
},
true
)
data = parsedNewData.toMutableList()
recomputePingRange()
diffResult.dispatchUpdatesTo(this)
if (isAtTop) {
@@ -162,26 +160,31 @@ class MainRecyclerAdapter(
// Name address
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
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
if ((aff?.testDelayMillis ?: 0L) < 0L) {
holder.itemMainBinding.tvTestResult.setTextColor(
ContextCompat.getColor(context, R.color.colorPingRed)
)
} else {
holder.itemMainBinding.tvTestResult.setTextColor(
ContextCompat.getColor(context, R.color.colorPing)
)
}
holder.itemMainBinding.tvTestResult.setTextColor(
getPingColor(context, aff?.testDelayMillis)
)
(holder.itemMainBinding.tvTestResult.layoutParams as? ViewGroup.MarginLayoutParams)?.marginStart =
if (addressText.isEmpty()) 0 else 6.dpToPx(context)
// layoutIndicator
if (guid == MmkvManager.getSelectServer()) {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorIndicator)
} else {
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
val isSelected = data[position].isSelected
holder.itemMainBinding.cardContainer.apply {
val selectedColor = MaterialColors.getColor(
context,
com.google.android.material.R.attr.colorSurfaceContainerHigh,
Color.TRANSPARENT
)
setCardBackgroundColor(if (isSelected) selectedColor else Color.TRANSPARENT)
strokeWidth = 0
strokeColor = Color.TRANSPARENT
}
// subscription remarks
@@ -223,9 +226,52 @@ class MainRecyclerAdapter(
* @return Formatted address string
*/
private fun getAddress(profile: ProfileItem): String {
if (!showServerIp) {
return ""
}
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
* @param profile The server configuration
@@ -241,18 +287,8 @@ class MainRecyclerAdapter(
return subRemarks?.toString() ?: ""
}
fun removeServerSub(guid: String, position: Int) {
val idx = data.indexOfFirst { it.guid == guid }
if (idx >= 0) {
data.removeAt(idx)
notifyItemRemoved(idx)
notifyItemRangeChanged(idx, data.size - idx)
}
}
fun setSelectServer(fromPosition: Int, toPosition: Int) {
notifyItemChanged(fromPosition)
notifyItemChanged(toPosition)
private fun Int.dpToPx(context: android.content.Context): Int {
return (this * context.resources.displayMetrics.density).toInt()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
@@ -301,6 +337,8 @@ class MainRecyclerAdapter(
BaseViewHolder(itemFooterBinding.root)
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)
if (fromPosition < data.size && toPosition < data.size) {
Collections.swap(data, fromPosition, toPosition)
@@ -33,6 +33,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
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.withContext
import java.util.Collections
@@ -42,15 +45,24 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
private var serverList = mutableListOf<String>()
var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
var keywordFilter = ""
/** ISO codes to show (empty = show all) */
/** ISO codes to EXCLUDE (empty = show all) */
var countryFilter: Set<String> = MmkvManager.getCountryFilter()
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 updateListAction by lazy { MutableLiveData<Int>() }
val updateTestResultAction by lazy { MutableLiveData<String>() }
val liteTestFinished = MutableLiveData<Boolean>()
val isTesting by lazy { MutableLiveData<Boolean>().also { it.value = false } }
var suppressPinSelected = false
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
/**
@@ -120,8 +132,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
list
}
if (!suppressPinSelected) pinSelectedGuidToTop(serverList)
updateCache()
updateListAction.value = -1
}
/**
@@ -131,10 +143,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun removeServer(guid: String) {
serverList.remove(guid)
MmkvManager.removeServer(guid)
val index = getPosition(guid)
val index = _serversCache.indexOfFirst { it.guid == guid }
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(serversCache, fromPosition, toPosition)
Collections.swap(_serversCache, fromPosition, toPosition)
publishSnapshot()
MmkvManager.encodeServerList(serverList, subscriptionId)
}
/**
* Updates the cache of servers.
* Rebuilds _serversCache from serverList and publishes a new snapshot to serverListFlow.
*/
@Synchronized
fun updateCache() {
serversCache.clear()
_serversCache.clear()
val kw = keywordFilter.trim()
val searchRegex = try {
if (kw.isNotEmpty()) Regex(kw, setOf(RegexOption.IGNORE_CASE)) else null
@@ -166,17 +180,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
null
}
val activeCountryFilter = countryFilter
val selectedGuid = MmkvManager.getSelectServer().orEmpty()
for (guid in serverList) {
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
// Country filter
if (activeCountryFilter.isNotEmpty()) {
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()) {
serversCache.add(ServersCache(guid, profile))
_serversCache.add(ServersCache(guid, profile, delay, guid == selectedGuid))
continue
}
@@ -189,15 +205,49 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|| server.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. */
fun applyCountryFilter(codes: Set<String>) {
countryFilter = codes
MmkvManager.setCountryFilter(codes)
/** Emits an immutable copy of _serversCache to the Flow. Must be called on Main or from @Synchronized blocks. */
private fun publishSnapshot() {
_serverListFlow.value = _serversCache.toList()
}
/** 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()
}
@@ -273,9 +323,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun exportAllServer(): Int {
val serverListCopy =
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
serverList
serverList.toList()
} else {
serversCache.map { it.guid }.toList()
_serversCache.map { it.guid }
}
val ret = AngConfigManager.shareNonCustomConfigsToClipboard(
@@ -291,9 +341,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun testAllTcping() {
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
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) {
item.profile.let { outbound ->
val serverAddress = outbound.server
@@ -303,7 +353,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt())
launch(Dispatchers.Main) {
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) {
reloadServerList()
}
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
updateListAction.value = -1
if (!suppressPinSelected) {
MmkvManager.clearAllTestDelayResults(_serversCache.map { it.guid })
}
publishSnapshot()
isTesting.value = true
viewModelScope.launch(Dispatchers.Default) {
if (serversCache.isEmpty()) {
if (_serversCache.isEmpty()) {
withContext(Dispatchers.Main) { reloadServerList() }
}
if (serversCache.isEmpty()) {
if (_serversCache.isEmpty()) {
withContext(Dispatchers.Main) { isTesting.value = false }
return@launch
}
@@ -359,7 +411,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
key = AppConfig.MSG_MEASURE_CONFIG,
subscriptionId = actualSubId,
serverGuids = if (keywordFilter.isNotEmpty() || subscriptionId.startsWith("group_")) {
serversCache.map { it.guid }
_serversCache.map { it.guid }
} else {
emptyList()
}
@@ -471,9 +523,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
* @return The position of the server.
*/
fun getPosition(guid: String): Int {
serversCache.forEachIndexed { index, it ->
if (it.guid == guid)
return index
_serversCache.forEachIndexed { index, it ->
if (it.guid == guid) return index
}
return -1
}
@@ -483,7 +534,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
* @return The number of removed servers.
*/
fun removeDuplicateServer(): Int {
val serversCacheCopy = serversCache.toList().toMutableList()
val serversCacheCopy = _serversCache.toList()
val deleteServer = mutableListOf<String>()
serversCacheCopy.forEachIndexed { index, sc ->
val profile = sc.profile
@@ -510,9 +561,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
*/
fun removeDuplicateByIp(): Int {
val selectedGuid = MmkvManager.getSelectServer()
// Group all currently visible servers by their IP address
val byIp = LinkedHashMap<String, MutableList<ServersCache>>()
for (sc in serversCache) {
for (sc in _serversCache) {
val ip = sc.profile.server?.trim()?.lowercase() ?: continue
byIp.getOrPut(ip) { mutableListOf() }.add(sc)
}
@@ -609,11 +659,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
MmkvManager.removeAllServer()
} else {
val serversCopy = serversCache.toList()
val serversCopy = _serversCache.toList()
for (item in serversCopy) {
MmkvManager.removeServer(item.guid)
}
serversCache.toList().count()
serversCopy.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).
* 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
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 },
{
val delay = MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: 0L
val delay = it.testDelayMillis
when {
delay > 0L -> delay
delay == 0L -> Long.MAX_VALUE - 1 // untested
else -> Long.MAX_VALUE // failed
delay == 0L -> Long.MAX_VALUE - 1
else -> Long.MAX_VALUE
}
}
))
if (!suppressPinSelected) pinSelectedCacheItemToTop(_serversCache)
publishSnapshot()
}
fun sortByTestResults() {
@@ -689,6 +763,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val serversBySubId = allServerDelays.groupBy { it.subId }
serversBySubId.forEach { (subId, servers) ->
val sortedList = servers.map { it.guid }.toMutableList()
pinSelectedGuidToTop(sortedList)
MmkvManager.encodeServerList(sortedList, subId)
}
}
@@ -716,11 +791,32 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
serverDelays.sortWith(compareBy({ !it.isFav }, { it.testDelayMillis }))
val sortedServerList = serverDelays.map { it.guid }.toMutableList()
pinSelectedGuidToTop(sortedServerList)
// Save the sorted list for this subscription
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.
@@ -772,7 +868,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
withContext(Dispatchers.Main) {
reloadServerList()
reloadServerList() // rebuilds _serversCache + publishSnapshot
isTesting.value = false
liteTestFinished.value = true
liteTestFinished.value = false
@@ -810,8 +906,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
sortServersCacheInPlace()
updateListAction.value = -1
refreshPingInCache(listOf(resultPair.first))
if (!suppressPinSelected) sortServersCacheInPlace()
// publishSnapshot() already called inside refresh/sort above
}
AppConfig.MSG_MEASURE_CONFIG_BATCH -> {
@@ -819,8 +916,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
update.results.forEach { result ->
MmkvManager.encodeServerTestDelayMillis(result.guid, result.delay)
}
sortServersCacheInPlace()
updateListAction.value = -1
refreshPingInCache(update.results.map { it.guid })
if (!suppressPinSelected) sortServersCacheInPlace()
// publishSnapshot() already called inside refresh/sort above
}
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {
@@ -1,7 +1,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/divider_color_light" />
<solid android:color="?attr/colorSecondaryContainer" />
<size
android:width="48dp"
android:height="48dp" />
</shape>
android:width="24dp"
android:height="24dp" />
</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>
+186 -129
View File
@@ -1,171 +1,228 @@
<?xml version="1.0" encoding="utf-8"?>
<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_height="match_parent"
android:fitsSystemWindows="true">
<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="top"
android:orientation="vertical">
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:id="@+id/layout_soure_ccode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp16">
android:layout_height="match_parent"
android:gravity="top"
android:orientation="vertical">
<ImageView
android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_source_code_24dp" />
<TextView
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/layout_soure_ccode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_spacing_dp16"
android:text="@string/title_source_code"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_source_code_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:text="@string/title_source_code"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?attr/colorOutlineVariant" />
<LinearLayout
android:id="@+id/layout_oss_licenses"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/license_24px"
app:tint="?attr/colorOnSurfaceVariant" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:text="@string/title_oss_license"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?attr/colorOutlineVariant" />
<LinearLayout
android:id="@+id/layout_feedback"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_feedback_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:text="@string/title_pref_feedback"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?attr/colorOutlineVariant" />
<LinearLayout
android:id="@+id/layout_tg_channel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_telegram_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:text="@string/title_tg_channel"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?attr/colorOutlineVariant" />
<LinearLayout
android:id="@+id/layout_privacy_policy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_privacy_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:text="@string/title_privacy_policy"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layout_oss_licenses"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp16">
</com.google.android.material.card.MaterialCardView>
<ImageView
android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/license_24px" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_spacing_dp16"
android:text="@string/title_oss_license"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_feedback"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp16">
<ImageView
android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_feedback_24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_spacing_dp16"
android:text="@string/title_pref_feedback"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_tg_channel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp16">
<ImageView
android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_telegram_24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_spacing_dp16"
android:text="@string/title_tg_channel"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_privacy_policy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp16">
<ImageView
android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_privacy_24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_spacing_dp16"
android:text="@string/title_privacy_policy"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
<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
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp16">
android:padding="20dp">
<TextView
android:id="@+id/tv_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_about"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
android:textAppearance="@style/TextAppearance.Material3.LabelMedium"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/tv_app_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/title_about"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="?attr/colorOutline" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
@@ -5,33 +5,41 @@
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
app:elevation="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:titleTextAppearance="@style/TextAppearance.AppCompat.Title" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintTop_toBottomOf="@id/app_bar"
android:indeterminate="true"
android:visibility="gone"
app:indicatorColor="@color/color_fab_active" />
app:indicatorColor="?attr/colorPrimary"
app:trackColor="?attr/colorSurfaceVariant" />
<FrameLayout
android:id="@+id/content_container"
android:layout_width="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_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
</FrameLayout>
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
+150 -110
View File
@@ -4,7 +4,7 @@
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/md_theme_surface">
android:background="?attr/colorSurface">
<LinearLayout
android:layout_width="match_parent"
@@ -15,15 +15,17 @@
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/md_theme_surface"
app:elevation="0dp">
android:background="?attr/colorSurface"
app:elevation="0dp"
app:liftOnScroll="false">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/app_name"
app:titleCentered="true" />
app:titleTextColor="?attr/colorOnSurface"
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
</com.google.android.material.appbar.AppBarLayout>
@@ -47,7 +49,9 @@
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="invisible"
app:indicatorColor="@color/md_theme_primary" />
app:indicatorColor="?attr/colorPrimary"
app:trackCornerRadius="0dp"
app:trackColor="@android:color/transparent" />
<LinearLayout
android:id="@+id/tab_slot_top"
@@ -58,7 +62,7 @@
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider_color_light" />
android:background="?attr/colorOutlineVariant" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
@@ -68,16 +72,18 @@
android:scrollbars="vertical"
android:layout_weight="1" />
<!-- Bottom container: tab + action card -->
<LinearLayout
android:id="@+id/bottom_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
android:background="?attr/colorSurfaceContainer">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider_color_light" />
android:background="?attr/colorOutlineVariant" />
<LinearLayout
android:id="@+id/tab_slot_bottom"
@@ -85,104 +91,132 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabIndicatorFullWidth="true"
app:tabMode="auto"
app:tabGravity="fill"
app:tabMaxWidth="0dp"
app:tabRippleColor="@android:color/transparent"
app:tabTextAppearance="@style/TabLayoutTextStyle" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_group"
android:layout_width="match_parent"
android:layout_height="56dp"
android:minHeight="56dp"
android:background="?attr/colorSurface"
app:tabIndicatorFullWidth="true"
app:tabIndicatorHeight="3dp"
app:tabMode="fixed"
app:tabGravity="fill"
app:tabMaxWidth="0dp"
app:tabRippleColor="@android:color/transparent"
app:tabIndicatorColor="?attr/colorPrimary"
app:tabSelectedTextColor="?attr/colorPrimary"
app:tabTextColor="?attr/colorOnSurfaceVariant"
app:tabTextAppearance="@style/TabLayoutTextStyle" />
</LinearLayout>
<LinearLayout
<!-- Bottom action row -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
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:paddingEnd="16dp">
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<TextView
android:id="@+id/tv_test_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:maxLines="2"
android:minLines="1"
android:text="@string/connection_test_pending"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<!-- 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
android:id="@+id/tv_test_state"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:maxLines="1"
android:ellipsize="end"
android:text="@string/connection_test_pending"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</RelativeLayout>
</LinearLayout>
<!-- Navigation Drawer -->
<LinearLayout
android:id="@+id/drawer_content_layout"
android:layout_width="wrap_content"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="?android:attr/windowBackground"
android:background="?attr/colorSurfaceContainerLow"
android:orientation="vertical">
<com.google.android.material.navigation.NavigationView
@@ -190,6 +224,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@android:color/transparent"
app:headerLayout="@layout/nav_header" />
<LinearLayout
@@ -198,61 +233,65 @@
android:orientation="vertical"
android:padding="16dp">
<TextView
<com.google.android.material.button.MaterialButton
android:id="@+id/drawer_settings"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:drawablePadding="12dp"
android:gravity="center_vertical"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:text="@string/title_settings"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="?attr/colorOnSurface"
app:iconGravity="start"
app:iconTint="?attr/colorOnSurfaceVariant" />
<TextView
<com.google.android.material.button.MaterialButton
android:id="@+id/drawer_per_app"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:drawablePadding="12dp"
android:gravity="center_vertical"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:text="@string/per_app_proxy_settings"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="?attr/colorOnSurface"
app:iconGravity="start"
app:iconTint="?attr/colorOnSurfaceVariant" />
<TextView
<com.google.android.material.button.MaterialButton
android:id="@+id/drawer_check_update"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:drawablePadding="12dp"
android:gravity="center_vertical"
android:paddingTop="12dp"
android:paddingBottom="16dp"
android:text="@string/update_check_for_update"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="?attr/colorOnSurface"
app:iconGravity="start"
app:iconTint="?attr/colorOnSurfaceVariant" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginBottom="12dp"
android:background="@color/divider_color_light" />
android:background="?attr/colorOutlineVariant" />
<TextView
android:id="@+id/tv_forked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/drawer_forked_text"
android:textColor="#9E9E9E"
android:textColorLink="#9E9E9E"
android:textColor="?attr/colorOnSurfaceVariant"
android:textColorLink="?attr/colorPrimary"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:linksClickable="true"
android:textIsSelectable="true" />
@@ -262,8 +301,9 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/drawer_developed_text"
android:textColor="#9E9E9E"
android:textColorLink="#9E9E9E"
android:textColor="?attr/colorOnSurfaceVariant"
android:textColorLink="?attr/colorPrimary"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:linksClickable="true"
android:textIsSelectable="true" />
</LinearLayout>
@@ -1,163 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_bg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:paddingEnd="12dp"
android:paddingBottom="6dp"
android:gravity="center_vertical">
<LinearLayout
android:id="@+id/info_container"
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:nextFocusRight="@+id/layout_share"
android:orientation="horizontal"
android:paddingStart="@dimen/padding_spacing_dp4"
android:paddingTop="@dimen/padding_spacing_dp8"
android:paddingEnd="@dimen/padding_spacing_dp4"
android:paddingBottom="@dimen/padding_spacing_dp8">
<LinearLayout
android:id="@+id/layout_indicator"
android:layout_width="@dimen/padding_spacing_dp4"
android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="vertical" />
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="14dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:id="@+id/info_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_gravity="center"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:nextFocusRight="@+id/layout_share"
android:orientation="horizontal"
android:paddingStart="0dp"
android:paddingTop="13dp"
android:paddingEnd="4dp"
android:paddingBottom="13dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/padding_spacing_dp8">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:minLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
android:layout_weight="1"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="@dimen/padding_spacing_dp8">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_subscription"
android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24"
android:layout_gravity="bottom"
android:layout_marginEnd="@dimen/padding_spacing_dp4"
android:background="@drawable/ic_circle">
<TextView
android:id="@+id/tv_subscription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textSize="11sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:orientation="vertical"
android:paddingStart="12dp">
<TextView
android:id="@+id/tv_statistics"
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
android:maxLines="2"
android:minLines="1"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textColor="?attr/colorOnSurface" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="5dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_subscription"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="bottom"
android:layout_marginEnd="4dp"
android:background="@drawable/ic_circle">
<TextView
android:id="@+id/tv_subscription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/tv_statistics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lines="1"
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
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end">
<ImageView
android:id="@+id/iv_favorite"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/padding_spacing_dp8"
android:src="@drawable/kid_star_outline_24" />
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_copy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/padding_spacing_dp8"
android:visibility="gone"
app:srcCompat="@drawable/ic_copy"
app:tint="?attr/colorAccent" />
<ImageView
android:id="@+id/iv_favorite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="6dp"
android:src="@drawable/kid_star_outline_24" />
<ImageView
android:id="@+id/iv_copy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="6dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_copy"
app:tint="?attr/colorPrimary" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
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>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
@@ -4,110 +4,127 @@
android:id="@+id/item_bg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:gravity="center_vertical">
<LinearLayout
android:id="@+id/info_container"
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:nextFocusRight="@+id/layout_edit"
android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp8">
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="0dp"
android:id="@+id/info_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8">
android:layout_gravity="center"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:nextFocusRight="@+id/layout_edit"
android:orientation="horizontal"
android:padding="8dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:layout_weight="1"
android:orientation="vertical"
android:padding="8dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/remarks"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<ImageView
android:id="@+id/img_locked"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="center"
android:layout_marginStart="16dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_lock_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<TextView
android:id="@+id/remarks"
android:id="@+id/domainIp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
android:layout_marginTop="6dp"
android:lines="1"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<ImageView
android:id="@+id/img_locked"
android:layout_width="@dimen/padding_spacing_dp16"
android:layout_height="@dimen/padding_spacing_dp16"
android:layout_gravity="center"
android:layout_marginStart="@dimen/padding_spacing_dp16"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_lock_24dp" />
<TextView
android:id="@+id/outboundTag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:lines="1"
android:textAppearance="@style/TextAppearance.Material3.LabelMedium"
android:textColor="?attr/colorPrimary" />
</LinearLayout>
<TextView
android:id="@+id/domainIp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:lines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<TextView
android:id="@+id/outboundTag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:lines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8">
<LinearLayout
android:id="@+id/layout_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/menu_item_edit_config"
android:focusable="true"
android:gravity="center"
android:nextFocusLeft="@+id/info_container"
android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8">
android:padding="8dp">
<ImageView
android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_edit_24dp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:orientation="horizontal">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/chk_enable"
<LinearLayout
android:id="@+id/layout_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:theme="@style/BrandedSwitch" />
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/menu_item_edit_config"
android:focusable="true"
android:gravity="center"
android:nextFocusLeft="@+id/info_container"
android:orientation="vertical"
android:padding="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_edit_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/chk_enable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:theme="@style/BrandedSwitch" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
@@ -4,195 +4,214 @@
android:id="@+id/item_bg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:gravity="center_vertical">
<LinearLayout
android:id="@+id/info_container"
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:nextFocusRight="@+id/layout_share"
android:orientation="horizontal"
android:padding="@dimen/padding_spacing_dp8">
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:id="@+id/info_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_gravity="center"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:nextFocusRight="@+id/layout_share"
android:orientation="horizontal"
android:padding="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:orientation="vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="@dimen/padding_spacing_dp8">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:minLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/layout_share"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/title_configuration_share"
android:focusable="true"
android:gravity="center"
android:nextFocusLeft="@+id/info_container"
android:layout_weight="1"
android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8">
android:paddingStart="8dp">
<ImageView
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_share_24dp" />
android:layout_height="wrap_content"
android:maxLines="2"
android:minLines="1"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/menu_item_edit_config"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8">
android:orientation="horizontal">
<ImageView
android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_edit_24dp" />
<LinearLayout
android:id="@+id/layout_share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/title_configuration_share"
android:focusable="true"
android:gravity="center"
android:nextFocusLeft="@+id/info_container"
android:orientation="vertical"
android:padding="8dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="24dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_share_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/menu_item_edit_config"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:padding="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_edit_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_remove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/menu_item_del_config"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:padding="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_delete_24dp"
app:tint="?attr/colorError" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layout_remove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/menu_item_del_config"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/padding_spacing_dp8">
<ImageView
android:layout_width="@dimen/image_size_dp24"
android:layout_height="@dimen/image_size_dp24"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_delete_24dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layout_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<LinearLayout
android:id="@+id/layout_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="@dimen/padding_spacing_dp8"
android:paddingEnd="@dimen/padding_spacing_dp8">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:paddingTop="6dp">
<TextView
android:id="@+id/tv_url"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lines="2"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:orientation="horizontal">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/chk_enable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:theme="@style/BrandedSwitch" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:id="@+id/layout_last_updated"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:paddingTop="@dimen/padding_spacing_dp8">
android:layout_marginTop="6dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingBottom="4dp">
<TextView
android:id="@+id/tv_url"
android:id="@+id/tv_last_updated"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleSmall"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="8dp"
android:visibility="gone" />
<TextView
android:id="@+id/tv_update_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lines="2"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
android:layout_marginStart="8dp"
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:visibility="gone" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:orientation="horizontal">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/chk_enable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:theme="@style/BrandedSwitch" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layout_last_updated"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="@dimen/padding_spacing_dp8"
android:paddingStart="@dimen/padding_spacing_dp8"
android:paddingEnd="@dimen/padding_spacing_dp8">
<TextView
android:id="@+id/tv_last_updated"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"/>
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleSmall"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="@dimen/padding_spacing_dp8"
android:visibility="gone"/>
<TextView
android:id="@+id/tv_update_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/padding_spacing_dp8"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</LinearLayout>
@@ -1,12 +1,14 @@
<?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"
style="@style/Widget.AppCompat.Button.Borderless"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:onClick="onModeHelpClicked"
android:text="@string/title_mode_help"
android:textAlignment="textStart"
android:textStyle="italic"
android:textColor="?attr/colorPrimary"
app:iconTint="?attr/colorPrimary"
tools:ignore="UsingOnClickInXml" />
@@ -1,29 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="color_fab_active">#C0C0C0</color>
<color name="color_fab_inactive">#646464</color>
<color name="divider_color_light">#424242</color>
<color name="colorPing">#90CAF9</color>
<color name="colorPingGood">#86D993</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>
<color name="md_theme_onPrimary">#303030</color>
<color name="md_theme_primaryContainer">#474747</color>
<color name="md_theme_onPrimaryContainer">#E0E0E0</color>
<!-- M3 Dark scheme — Purple/Violet tonal palette -->
<color name="md_theme_primary">#D0BCFF</color>
<color name="md_theme_onPrimary">#381E72</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_onSecondary">#FFFFFF</color>
<color name="md_theme_secondaryContainer">#6F3800</color>
<color name="md_theme_onSecondaryContainer">#FFE8D6</color>
<color name="md_theme_secondary">#CCC2DC</color>
<color name="md_theme_onSecondary">#332D41</color>
<color name="md_theme_secondaryContainer">#4A4458</color>
<color name="md_theme_onSecondaryContainer">#E8DEF8</color>
<color name="md_theme_tertiary">#64B5F6</color>
<color name="md_theme_onTertiary">#00382E</color>
<color name="md_theme_tertiaryContainer">#005143</color>
<color name="md_theme_onTertiaryContainer">#BBDEFB</color>
<color name="md_theme_tertiary">#EFB8C8</color>
<color name="md_theme_onTertiary">#492532</color>
<color name="md_theme_tertiaryContainer">#633B48</color>
<color name="md_theme_onTertiaryContainer">#FFD8E4</color>
<!-- Error colors -->
<color name="md_theme_error">#FFB4AB</color>
<color name="md_theme_errorContainer">#93000A</color>
<color name="md_theme_onError">#690005</color>
<color name="md_theme_onErrorContainer">#FFDAD6</color>
<color name="md_theme_error">#F2B8B5</color>
<color name="md_theme_errorContainer">#8C1D18</color>
<color name="md_theme_onError">#601410</color>
<color name="md_theme_onErrorContainer">#F9DEDC</color>
<!-- Background colors -->
<color name="md_theme_background">#1C1B1F</color>
@@ -35,15 +42,22 @@
<color name="md_theme_surfaceVariant">#49454F</color>
<color name="md_theme_onSurfaceVariant">#CAC4D0</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 -->
<color name="md_theme_outline">#938F99</color>
<color name="md_theme_outlineVariant">#49454F</color>
<!-- 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_surfaceTint">#C0C0C0</color>
<color name="md_theme_surfaceTint">#D0BCFF</color>
<color name="md_theme_scrim">#000000</color>
</resources>
@@ -1,11 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<!-- Night mode specific overrides -->
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar" tools:targetApi="27">false</item>
</style>
</resources>
</resources>
+360 -122
View File
@@ -1,13 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<string name="app_widget_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_close">Закрыть панель навигации</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_developed_text">developed by developers from <a href="https://t.me/openlibrecommunity">Olc</a></string>
<string name="drawer_forked_text">forked from <a
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="migration_fail">Перенос данных не выполнен!</string>
<string name="pull_down_to_refresh">Потяните вниз для обновления!</string>
@@ -15,7 +21,9 @@
<!-- Notifications -->
<string name="notification_action_stop_v2ray">Остановить</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="toast_services_start">Запуск служб</string>
<string name="toast_services_stop">Остановка служб</string>
@@ -29,19 +37,41 @@
<string name="menu_item_edit_config">Изменить профиль</string>
<string name="menu_item_del_config">Удалить профиль</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_policy_group">Добавить группу политик</string>
<string name="menu_item_import_config_manually_vmess">Ручной ввод VMess</string>
<string name="menu_item_import_config_manually_vless">Ручной ввод 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="menu_item_import_config_policy_group"
>Добавить группу политик</string>
<string
name="menu_item_import_config_manually_vmess"
>Ручной ввод VMess</string>
<string
name="menu_item_import_config_manually_vless"
>Ручной ввод 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_invalid_config_comfirm">Выполните проверку перед удалением! Подтверждаете удаление?</string>
<string
name="del_invalid_config_comfirm"
>Выполните проверку перед удалением! Подтверждаете удаление?</string>
<string name="server_lab_remarks">Название</string>
<string name="server_lab_address">Адрес</string>
<string name="server_lab_port">Порт</string>
@@ -82,45 +112,73 @@
<string name="server_lab_encryption">Шифрование</string>
<string name="server_lab_flow">Поток</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_spider_x">SpiderX</string>
<string name="server_lab_mldsa65_verify">mldsa65Verify</string>
<string name="server_lab_secret_key">Закрытый ключ</string>
<string name="server_lab_reserved">Reserved (необязательно, через запятую)</string>
<string name="server_lab_local_address">Локальный адрес (необязательно, IPv4/IPv6 через запятую)</string>
<string name="server_lab_local_mtu">MTU (необязательно, по умолчанию 1420)</string>
<string
name="server_lab_reserved"
>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_failure">Ошибка</string>
<string name="toast_none_data">Ничего нет</string>
<string name="toast_incorrect_protocol">Неправильный протокол</string>
<string name="toast_decoding_failed">Невозможно декодировать</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="toast_config_file_invalid">Неправильный профиль</string>
<string name="server_lab_content">Данные</string>
<string name="toast_none_data_clipboard">В буфере обмена нет данных</string>
<string name="toast_invalid_url">Неправильный URL</string>
<string name="toast_insecure_url_protocol">Не используйте небезопасный HTTP-протокол в адресе подписки</string>
<string name="server_lab_need_inbound">Убедитесь, что входящий порт соответствует настройкам</string>
<string
name="toast_insecure_url_protocol"
>Не используйте небезопасный HTTP-протокол в адресе подписки</string>
<string
name="server_lab_need_inbound"
>Убедитесь, что входящий порт соответствует настройкам</string>
<string name="toast_malformed_josn">Профиль повреждён</string>
<string name="server_lab_request_host6">Узел (SNI) (необязательно)</string>
<string name="toast_action_not_allowed">Это действие запрещено</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_bandwidth_down">Входящая пропускная способность (допускаются: k/m/g/t)</string>
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string>
<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_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
<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_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
<string
name="server_lab_pinned_ca256"
>Отпечаток сертификата (SHA-256)</string>
<!-- 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_file">Добавить файлы</string>
<string name="menu_item_add_url">Добавить URL</string>
@@ -130,7 +188,9 @@
<string name="title_user_asset_add_url">Добавить URL ресурса</string>
<string name="msg_file_not_found">Файл не найден</string>
<string name="msg_remark_is_duplicate">Название уже существует</string>
<string name="asset_geo_files_sources">Источник геофайлов (необязательно)</string>
<string
name="asset_geo_files_sources"
>Источник геофайлов (необязательно)</string>
<!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">Загрузка…</string>
@@ -143,28 +203,61 @@
<string name="msg_downloading_content">Загрузка данных</string>
<string name="menu_item_export_proxy_app">Экспорт в буфер обмена</string>
<string name="menu_item_import_proxy_app">Импорт из буфера обмена</string>
<string name="per_app_proxy_settings">Выбор приложений</string>
<string name="per_app_proxy_settings_enable">Выбор приложений</string>
<string name="per_app_proxy_settings">Раздельное туннелирование</string>
<string
name="per_app_proxy_settings_enable"
>Раздельное туннелирование</string>
<!-- Preferences -->
<string name="title_settings">Настройки</string>
<string name="title_advanced">Расширенные настройки</string>
<string name="title_core_settings">Настройки ядра</string>
<string name="title_vpn_settings">Настройки VPN</string>
<string name="title_pref_per_app_proxy">Прокси для выбранных приложений</string>
<string name="summary_pref_per_app_proxy">Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню.</string>
<string name="title_pref_per_app_proxy">Раздельное туннелирование</string>
<string
name="summary_pref_per_app_proxy"
>Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню.</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 name="summary_pref_auto_sort_after_test">Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string>
<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_pref_mux_enabled">Использовать мультиплексирование</string>
<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
name="title_pref_mux_enabled"
>Использовать мультиплексирование</string>
<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">
<item>Отклонять</item>
<item>Разрешать</item>
@@ -172,49 +265,87 @@
</string-array>
<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="summary_pref_sniffing_enabled">Пытаться определять доменные имена в пакетах (по умолчанию включено)</string>
<string name="title_pref_route_only_enabled">Домен только для маршрутизации</string>
<string name="summary_pref_route_only_enabled">Использовать доменное имя только для маршрутизации и сохранять целевой адрес в виде IP.</string>
<string
name="summary_pref_sniffing_enabled"
>Пытаться определять доменные имена в пакетах (по умолчанию включено)</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 name="summary_pref_local_dns_enabled">Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая»)</string>
<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 name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string>
<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="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="title_pref_vpn_dns">VPN DNS (только IPv4/v6)</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_domestic_dns">Внутренняя DNS (необязательно)</string>
<string
name="title_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="title_pref_delay_test_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="title_pref_proxy_sharing_enabled">Разрешать соединения из LAN</string>
<string name="summary_pref_proxy_sharing_enabled">Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string>
<string name="toast_warning_pref_proxysharing_short">Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети</string>
<string
name="title_pref_proxy_sharing_enabled"
>Разрешать соединения из 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 name="summary_pref_allow_insecure">Для TLS по умолчанию разрешены небезопасные соединения</string>
<string
name="title_pref_allow_insecure"
>Разрешать небезопасные соединения</string>
<string
name="summary_pref_allow_insecure"
>Для TLS по умолчанию разрешены небезопасные соединения</string>
<string name="title_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="summary_pref_local_dns_port">Порт локальной DNS</string>
<string name="title_pref_confirm_remove">Подтверждать удаление профиля</string>
<string name="summary_pref_confirm_remove">Обязательное подтверждение удаления профиля</string>
<string
name="title_pref_confirm_remove"
>Подтверждать удаление профиля</string>
<string
name="summary_pref_confirm_remove"
>Обязательное подтверждение удаления профиля</string>
<string name="title_pref_start_scan_immediate">Сканировать при запуске</string>
<string name="summary_pref_start_scan_immediate">Начинать сканирование сразу при запуске приложения или запускать функцию сканирования камерой или из изображения через панель инструментов</string>
<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 name="summary_pref_append_http_proxy">HTTP-прокси будет использоваться напрямую (из браузера и других поддерживающих приложений), минуя виртуальный сетевой адаптер (Android 10+)</string>
<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 name="summary_pref_double_column_display">Список профилей отображается двумя столбцами, что позволяет показать больше информации на экране. Требуется перезапуск приложения.</string>
<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="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 -->
<string name="title_pref_feedback">Обратная связь</string>
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string>
<string name="summary_pref_tg_group">Присоединиться к группе в Telegram</string>
<string name="toast_tg_app_not_found">Приложение Telegram не найдено</string>
<string
name="summary_pref_feedback"
>Предложить улучшение или сообщить об ошибке на 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_about">О приложении</string>
<string name="title_source_code">Исходный код</string>
@@ -252,33 +405,57 @@
<string name="title_pref_promotion">Содействие</string>
<string name="title_pref_auto_update_subscription">Автоматически обновлять подписки</string>
<string name="summary_pref_auto_update_subscription">Автоматическое обновление подписок в фоновом режиме с указанным интервалом. В зависимости от устройства эта функция может работать не всегда.</string>
<string name="title_pref_auto_update_interval">Интервал автообновления (минут, не менее 15)</string>
<string
name="title_pref_auto_update_subscription"
>Автоматически обновлять подписки</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_outbound_domain_resolve_method">Предопределение исходящего домена</string>
<string
name="title_outbound_domain_resolve_method"
>Предопределение исходящего домена</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_ui_settings">Настройки интерфейса</string>
<string name="title_pref_ui_mode_night">Тема интерфейса</string>
<string name="title_pref_dynamic_colors">Динамические цвета (Material You)</string>
<string name="summary_pref_dynamic_colors">Использовать цвета обоев (Android 12+). Требует перезапуска.</string>
<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="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="summary_pref_use_hev_tunnel">Если включено, 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="summary_pref_use_hev_tunnel"
>Если включено, 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="logcat_copy">Копировать</string>
<string name="logcat_clear">Очистить</string>
<string name="title_service_restart">Перезапуск службы</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_export_all">Экспорт профилей в буфер обмена</string>
<string name="title_sub_setting">Группы</string>
@@ -288,26 +465,44 @@
<string name="sub_setting_filter">Название фильтра</string>
<string name="sub_setting_enable">Использовать обновление</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_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_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_sort_by_test_results">Сортировать по результатам теста</string>
<string
name="title_sort_by_test_results"
>Сортировать по результатам теста</string>
<string name="title_filter_config">Фильтр профилей</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_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_updating">Обновление…</string>
<string name="title_update_subscription_result">Обновлено профилей: %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="title_update_subscription_result"
>Обновлено профилей: %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="title_locate_selected_config">Найти выбранный профиль</string>
@@ -317,17 +512,33 @@
<!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">Доменная стратегия</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_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_import_predefined_rulesets">Импорт набора правил</string>
<string name="routing_settings_import_rulesets_tip">Существующие правила будут удалены. Продолжить?</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_import_predefined_rulesets"
>Импорт набора правил</string>
<string
name="routing_settings_import_rulesets_tip"
>Существующие правила будут удалены. Продолжить?</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_ip">IP</string>
<string name="routing_settings_port">Порт</string>
@@ -340,44 +551,71 @@
<string name="connection_test_pending">Проверить соединение</string>
<string name="connection_test_testing">Проверка…</string>
<string name="connection_test_testing_count">Проверка профилей (%d)</string>
<string name="connection_test_available">Успешно: соединение заняло %d мс</string>
<string name="connection_test_error">Сбой проверки интернет-соединения: %s</string>
<string name="connection_test_available">%d мс</string>
<string
name="connection_test_error"
>Сбой проверки интернет-соединения: %s</string>
<string name="connection_test_fail">Интернет недоступен</string>
<string name="connection_test_error_status_code">Код ошибки: #%d</string>
<string name="connection_connected">Соединено, нажмите для проверки</string>
<string name="connection_not_connected">Нет соединения</string>
<string name="connection_runing_task_left">Запущено проверок: %s</string>
<string name="connection_connected">нажмите для проверки</string>
<string name="connection_not_connected">Ожидаем действий</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_failure">Невозможно импортировать подписку</string>
<string
name="import_subscription_failure"
>Невозможно импортировать подписку</string>
<string name="title_fragment_settings">Настройки фрагментирования</string>
<string name="title_pref_fragment_packets">Фрагментирование пакетов</string>
<string name="title_pref_fragment_length">Длина фрагмента (от - до)</string>
<string name="title_pref_fragment_interval">Интервал фрагментов (от - до)</string>
<string name="title_pref_fragment_enabled">Использовать фрагментирование</string>
<string
name="title_pref_fragment_interval"
>Интервал фрагментов (от - до)</string>
<string
name="title_pref_fragment_enabled"
>Использовать фрагментирование</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_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="title_policy_group_type">Тип группы политик</string>
<string name="title_policy_group_subscription_id">Из группы подписки</string>
<string name="title_policy_group_subscription_filter">Название фильтра</string>
<string
name="title_policy_group_subscription_id"
>Из группы подписки</string>
<string
name="title_policy_group_subscription_filter"
>Название фильтра</string>
<!-- BackupActivity -->
<string name="title_configuration_backup_restore">Резервное копирование</string>
<string name="title_configuration_backup">Резервирование конфигурации</string>
<string name="title_configuration_restore">Восстановление конфигурации</string>
<string
name="title_configuration_backup_restore"
>Резервное копирование</string>
<string
name="title_configuration_backup"
>Резервирование конфигурации</string>
<string
name="title_configuration_restore"
>Восстановление конфигурации</string>
<string name="title_configuration_share">Поделиться конфигурацией</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_user">Пользователь</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">
<item>QR-код</item>
+1 -5
View File
@@ -1,7 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<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"?>
<resources>
<color name="colorPing">#1565C0</color>
<color name="colorPingRed">#FF0099</color>
<color name="colorConfigType">#1976D2</color>
<color name="colorPing">#006494</color>
<color name="colorPingGood">#2E7D32</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="color_fab_active">#000000</color>
<color name="color_fab_inactive">#9C9C9C</color>
<color name="divider_color_light">#E0E0E0</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">#22CAC4D0</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_primaryContainer">#E0E0E0</color>
<color name="md_theme_onPrimaryContainer">#000000</color>
<color name="md_theme_primaryContainer">#EADDFF</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_secondaryContainer">#FFE8D6</color>
<color name="md_theme_onSecondaryContainer">#2B1700</color>
<color name="md_theme_secondaryContainer">#E8DEF8</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_tertiaryContainer">#BBDEFB</color>
<color name="md_theme_onTertiaryContainer">#00201A</color>
<color name="md_theme_tertiaryContainer">#FFD8E4</color>
<color name="md_theme_onTertiaryContainer">#31111D</color>
<!-- Error colors -->
<color name="md_theme_error">#BA1A1A</color>
<color name="md_theme_errorContainer">#FFDAD6</color>
<color name="md_theme_error">#B3261E</color>
<color name="md_theme_errorContainer">#F9DEDC</color>
<color name="md_theme_onError">#FFFFFF</color>
<color name="md_theme_onErrorContainer">#410002</color>
<color name="md_theme_onErrorContainer">#410E0B</color>
<!-- Background colors -->
<color name="md_theme_background">#FFFFFF</color>
<color name="md_theme_background">#FFFBFE</color>
<color name="md_theme_onBackground">#1C1B1F</color>
<!-- Surface colors -->
<color name="md_theme_surface">#FFFFFF</color>
<!-- Surface colors — M3 tonal surface hierarchy -->
<color name="md_theme_surface">#FFFBFE</color>
<color name="md_theme_onSurface">#1C1B1F</color>
<color name="md_theme_surfaceVariant">#E7E0EC</color>
<color name="md_theme_onSurfaceVariant">#49454F</color>
<color name="md_theme_inverseSurface">#313033</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 -->
<color name="md_theme_outline">#79747E</color>
<color name="md_theme_outlineVariant">#CAC4D0</color>
<!-- 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_surfaceTint">#000000</color>
<color name="md_theme_surfaceTint">#6750A4</color>
<color name="md_theme_scrim">#000000</color>
</resources>
+5 -2
View File
@@ -146,7 +146,7 @@
<string name="menu_item_import_proxy_app">Import from Clipboard</string>
<string name="per_app_proxy_settings">Раздельное туннелирование</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 -->
<string name="title_settings">Settings</string>
@@ -163,6 +163,8 @@
<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="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_pref_mux_enabled">Enable Mux</string>
@@ -353,7 +355,8 @@
<string name="connection_test_error_status_code">Error code: #%d</string>
<string name="connection_connected">Connected, tap to check connection</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_failure">Import subscription failed</string>
+74 -20
View File
@@ -1,58 +1,65 @@
<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">
<!-- Primary colors - main tone: black -->
<!-- Primary -->
<item name="colorPrimary">@color/md_theme_primary</item>
<item name="colorOnPrimary">@color/md_theme_onPrimary</item>
<item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item>
<!-- Secondary -->
<item name="colorSecondary">@color/md_theme_secondary</item>
<item name="colorOnSecondary">@color/md_theme_onSecondary</item>
<item name="colorSecondaryContainer">@color/md_theme_secondaryContainer</item>
<item name="colorOnSecondaryContainer">@color/md_theme_onSecondaryContainer</item>
<!-- Tertiary -->
<item name="colorTertiary">@color/md_theme_tertiary</item>
<item name="colorOnTertiary">@color/md_theme_onTertiary</item>
<item name="colorTertiaryContainer">@color/md_theme_tertiaryContainer</item>
<item name="colorOnTertiaryContainer">@color/md_theme_onTertiaryContainer</item>
<!-- Error colors -->
<!-- Error -->
<item name="colorError">@color/md_theme_error</item>
<item name="colorOnError">@color/md_theme_onError</item>
<item name="colorErrorContainer">@color/md_theme_errorContainer</item>
<item name="colorOnErrorContainer">@color/md_theme_onErrorContainer</item>
<!-- Surface colors -->
<!-- Surface -->
<item name="colorSurface">@color/md_theme_surface</item>
<item name="colorOnSurface">@color/md_theme_onSurface</item>
<item name="colorSurfaceVariant">@color/md_theme_surfaceVariant</item>
<item name="colorOnSurfaceVariant">@color/md_theme_onSurfaceVariant</item>
<item name="colorSurfaceInverse">@color/md_theme_inverseSurface</item>
<item name="colorOnSurfaceInverse">@color/md_theme_inverseOnSurface</item>
<item name="colorSurfaceContainer">@color/md_theme_surface</item>
<item name="colorSurfaceContainerHigh">@color/md_theme_surface</item>
<item name="colorSurfaceContainerHighest">@color/md_theme_surface</item>
<item name="colorSurfaceContainerLow">@color/md_theme_surface</item>
<item name="colorSurfaceContainerLowest">@color/md_theme_surface</item>
<item name="colorSurfaceContainer">@color/md_theme_surfaceContainer</item>
<item name="colorSurfaceContainerHigh">@color/md_theme_surfaceContainerHigh</item>
<item name="colorSurfaceContainerHighest">@color/md_theme_surfaceContainerHighest</item>
<item name="colorSurfaceContainerLow">@color/md_theme_surfaceContainerLow</item>
<item name="colorSurfaceContainerLowest">@color/md_theme_surfaceContainerLowest</item>
<!-- Background colors -->
<!-- Background -->
<item name="android:colorBackground">@color/md_theme_background</item>
<item name="colorOnBackground">@color/md_theme_onBackground</item>
<!-- Outline colors -->
<!-- Outline -->
<item name="colorOutline">@color/md_theme_outline</item>
<item name="colorOutlineVariant">@color/md_theme_outlineVariant</item>
<!-- Other colors -->
<!-- Misc -->
<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: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="fontFamily">sans-serif</item>
<item name="android:editTextStyle">@style/RobotoEditTextStyle</item>
@@ -60,7 +67,23 @@
<item name="android:dialogTheme">@style/RobotoAlertDialogTheme</item>
</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:textSize">16sp</item>
</style>
@@ -68,16 +91,15 @@
<style name="RobotoAlertDialogTheme" parent="ThemeOverlay.Material3.MaterialAlertDialog">
<item name="android:fontFamily">sans-serif</item>
<item name="fontFamily">sans-serif</item>
<item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item>
</style>
<!-- Day/Night theme: inherit common values and set light-mode-specific items -->
<!-- Light -->
<style name="AppThemeDayNight" parent="AppThemeBase">
<!-- day/night-specific overrides (light/default) -->
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar" tools:targetApi="27">true</item>
</style>
<!-- Theme without ActionBar -->
<style name="AppThemeDayNight.NoActionBar" parent="AppThemeDayNight">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
@@ -89,8 +111,40 @@
<item name="android:windowIsTranslucent">true</item>
</style>
<!-- Switch uses primary color -->
<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>
</resources>
@@ -34,6 +34,12 @@
android:summary="@string/summary_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
android:defaultValue="auto"
android:entries="@array/language_select"
@@ -310,4 +316,4 @@
</PreferenceCategory>
</PreferenceScreen>
</PreferenceScreen>
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">olcNG</string>
<string name="app_name" translatable="false">olcng</string>
</resources>