mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2f3415421 | |||
| 6e1c774d86 | |||
| 8e9e709d12 | |||
| 649f305a82 | |||
| 44005dffd3 | |||
| bee7002f54 | |||
| 0363ebaabd | |||
| 88627bbf4f | |||
| ceec94e5db | |||
| b30fc13b0d | |||
| a9f5844b84 |
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<item name="app_name" type="string">olcNG</item>
|
<item name="app_name" type="string">olcng</item>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">olcNG</string>
|
<string name="app_name" translatable="false">olcng</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package xyz.zarazaex.olc.dto
|
|||||||
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
|
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
|
||||||
fun getTestDelayString(): String {
|
fun getTestDelayString(): String {
|
||||||
return when {
|
return when {
|
||||||
testDelayMillis == 0L -> ""
|
testDelayMillis == 0L -> "—ms"
|
||||||
testDelayMillis < 0L -> "Error"
|
testDelayMillis < 0L -> "-ms"
|
||||||
else -> "${testDelayMillis}ms"
|
else -> "${testDelayMillis}ms"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@ package xyz.zarazaex.olc.dto
|
|||||||
|
|
||||||
data class ServersCache(
|
data class ServersCache(
|
||||||
val guid: String,
|
val guid: String,
|
||||||
val profile: ProfileItem
|
val profile: ProfileItem,
|
||||||
|
val testDelayMillis: Long = 0L,
|
||||||
|
val isSelected: Boolean = false
|
||||||
)
|
)
|
||||||
@@ -612,18 +612,19 @@ object AngConfigManager {
|
|||||||
Log.i(AppConfig.TAG, url)
|
Log.i(AppConfig.TAG, url)
|
||||||
val userAgent = it.subscription.userAgent
|
val userAgent = it.subscription.userAgent
|
||||||
|
|
||||||
val timeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 6000
|
val proxyTimeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 5000
|
||||||
|
val directTimeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 11000
|
||||||
|
|
||||||
var configText = try {
|
var configText = try {
|
||||||
val httpPort = SettingsManager.getHttpPort()
|
val httpPort = SettingsManager.getHttpPort()
|
||||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout, httpPort)
|
HttpUtil.getUrlContentWithUserAgent(url, userAgent, proxyTimeout, httpPort)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
if (configText.isEmpty()) {
|
if (configText.isEmpty()) {
|
||||||
configText = try {
|
configText = try {
|
||||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout)
|
HttpUtil.getUrlContentWithUserAgent(url, userAgent, directTimeout)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
||||||
""
|
""
|
||||||
|
|||||||
@@ -700,9 +700,15 @@ object MmkvManager {
|
|||||||
/** Persists ISO country code for [ip]. */
|
/** Persists ISO country code for [ip]. */
|
||||||
fun setCountryCache(ip: String, code: String) { countryCacheStorage.encode(ip, code) }
|
fun setCountryCache(ip: String, code: String) { countryCacheStorage.encode(ip, code) }
|
||||||
|
|
||||||
/** Loads the user's country filter preference (set of ISO codes to SHOW, empty = show all). */
|
/** Loads the user's country filter preference (set of ISO codes to EXCLUDE, empty = show all). */
|
||||||
fun getCountryFilter(): Set<String> =
|
fun getCountryFilter(): Set<String> {
|
||||||
settingsStorage.decodeStringSet("pref_country_filter") ?: emptySet()
|
// v2: semantics changed from "included" to "excluded" — reset old data on first read
|
||||||
|
if (!settingsStorage.decodeBool("pref_country_filter_v2_migrated", false)) {
|
||||||
|
settingsStorage.removeValueForKey("pref_country_filter")
|
||||||
|
settingsStorage.encode("pref_country_filter_v2_migrated", true)
|
||||||
|
}
|
||||||
|
return settingsStorage.decodeStringSet("pref_country_filter") ?: emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
/** Saves the user's country filter preference. */
|
/** Saves the user's country filter preference. */
|
||||||
fun setCountryFilter(codes: Set<String>) {
|
fun setCountryFilter(codes: Set<String>) {
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ object V2RayServiceManager {
|
|||||||
// Only fetch IP info if the delay test was successful
|
// Only fetch IP info if the delay test was successful
|
||||||
if (time >= 0) {
|
if (time >= 0) {
|
||||||
SpeedtestManager.getRemoteIPInfo()?.let { ip ->
|
SpeedtestManager.getRemoteIPInfo()?.let { ip ->
|
||||||
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip")
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result $ip")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
|
|||||||
import xyz.zarazaex.olc.AppConfig
|
import xyz.zarazaex.olc.AppConfig
|
||||||
import xyz.zarazaex.olc.dto.PingProgressUpdate
|
import xyz.zarazaex.olc.dto.PingProgressUpdate
|
||||||
import xyz.zarazaex.olc.dto.PingResultItem
|
import xyz.zarazaex.olc.dto.PingResultItem
|
||||||
|
import xyz.zarazaex.olc.handler.MmkvManager
|
||||||
import xyz.zarazaex.olc.handler.SettingsManager
|
import xyz.zarazaex.olc.handler.SettingsManager
|
||||||
import xyz.zarazaex.olc.handler.V2RayNativeManager
|
import xyz.zarazaex.olc.handler.V2RayNativeManager
|
||||||
import xyz.zarazaex.olc.handler.V2rayConfigManager
|
import xyz.zarazaex.olc.handler.V2rayConfigManager
|
||||||
@@ -57,8 +58,15 @@ class RealPingWorkerService(
|
|||||||
|
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
// Prepare configurations in parallel for faster startup
|
// Prepare configurations in parallel for faster startup.
|
||||||
val shuffledGuids = guids.shuffled()
|
// Keep the currently selected server at the front so it gets a result first.
|
||||||
|
val selectedGuid = MmkvManager.getSelectServer()
|
||||||
|
val shuffledGuids = if (selectedGuid != null && guids.contains(selectedGuid)) {
|
||||||
|
val rest = guids.filter { it != selectedGuid }.shuffled()
|
||||||
|
listOf(selectedGuid) + rest
|
||||||
|
} else {
|
||||||
|
guids.shuffled()
|
||||||
|
}
|
||||||
val deferredItems = shuffledGuids.map { guid ->
|
val deferredItems = shuffledGuids.map { guid ->
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||||
@@ -135,7 +143,7 @@ class RealPingWorkerService(
|
|||||||
|
|
||||||
private fun sendBatchUpdate(update: PingProgressUpdate) {
|
private fun sendBatchUpdate(update: PingProgressUpdate) {
|
||||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_BATCH, update)
|
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() {
|
fun cancel() {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import xyz.zarazaex.olc.AppConfig
|
import xyz.zarazaex.olc.AppConfig
|
||||||
import xyz.zarazaex.olc.R
|
import xyz.zarazaex.olc.R
|
||||||
import xyz.zarazaex.olc.contracts.MainAdapterListener
|
import xyz.zarazaex.olc.contracts.MainAdapterListener
|
||||||
@@ -76,19 +75,21 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
|||||||
// // Set the distance to trigger sync to 160dp
|
// // Set the distance to trigger sync to 160dp
|
||||||
// binding.refreshLayout.setDistanceToTriggerSync((160 * resources.displayMetrics.density).toInt())
|
// binding.refreshLayout.setDistanceToTriggerSync((160 * resources.displayMetrics.density).toInt())
|
||||||
|
|
||||||
mainViewModel.updateListAction.observe(viewLifecycleOwner) { index ->
|
// Each fragment subscribes independently to the shared flow and filters its own subId.
|
||||||
if (mainViewModel.subscriptionId != subId) {
|
// No onResume subscription switch needed — the active fragment's subId is always correct.
|
||||||
return@observe
|
lifecycleScope.launch {
|
||||||
}
|
mainViewModel.serverListFlow.collect { list ->
|
||||||
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
if (mainViewModel.subscriptionId == subId) {
|
||||||
adapter.setData(mainViewModel.serversCache, index)
|
adapter.setData(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
// Tell ViewModel which tab is active so it can rebuild the correct list.
|
||||||
|
// This is the only place subscriptionId changes — no more races.
|
||||||
mainViewModel.subscriptionIdChanged(subId)
|
mainViewModel.subscriptionIdChanged(subId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +220,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
|||||||
*/
|
*/
|
||||||
private fun removeServerSub(guid: String, position: Int) {
|
private fun removeServerSub(guid: String, position: Int) {
|
||||||
mainViewModel.removeServer(guid)
|
mainViewModel.removeServer(guid)
|
||||||
adapter.removeServerSub(guid, position)
|
// adapter updates automatically via serverListFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -231,19 +232,13 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
|||||||
val selected = MmkvManager.getSelectServer()
|
val selected = MmkvManager.getSelectServer()
|
||||||
if (guid == selected) {
|
if (guid == selected) {
|
||||||
MmkvManager.setSelectServer("")
|
MmkvManager.setSelectServer("")
|
||||||
val position = mainViewModel.getPosition(guid)
|
|
||||||
adapter.setSelectServer(position, position)
|
|
||||||
if (mainViewModel.isRunning.value == true) {
|
|
||||||
ownerActivity.restartV2Ray()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
MmkvManager.setSelectServer(guid)
|
MmkvManager.setSelectServer(guid)
|
||||||
val fromPosition = mainViewModel.getPosition(selected.orEmpty())
|
}
|
||||||
val toPosition = mainViewModel.getPosition(guid)
|
// Republish snapshot so DiffUtil picks up the selection change in card background
|
||||||
adapter.setSelectServer(fromPosition, toPosition)
|
mainViewModel.reloadServerList()
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
ownerActivity.restartV2Ray()
|
ownerActivity.restartV2Ray()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,9 +305,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the position of the selected server
|
val position = mainViewModel.serverListFlow.value.indexOfFirst { it.guid == selectedGuid }
|
||||||
val serversCache = mainViewModel.serversCache
|
|
||||||
val position = serversCache.indexOfFirst { it.guid == selectedGuid }
|
|
||||||
val recyclerView = binding.recyclerView
|
val recyclerView = binding.recyclerView
|
||||||
|
|
||||||
if (position >= 0) {
|
if (position >= 0) {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
private var isLiteTesting = false
|
private var isLiteTesting = false
|
||||||
private var easterEggClickCount = 0
|
private var easterEggClickCount = 0
|
||||||
private var isEasterEggActive = false
|
private var isEasterEggActive = false
|
||||||
|
private var liteActionJob: kotlinx.coroutines.Job? = null
|
||||||
/** Был ли VPN уже запущен в предыдущем колбэке — чтобы детектировать момент подключения */
|
/** Был ли VPN уже запущен в предыдущем колбэке — чтобы детектировать момент подключения */
|
||||||
private var wasRunning = false
|
private var wasRunning = false
|
||||||
|
|
||||||
@@ -225,34 +226,43 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(
|
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(
|
||||||
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
|
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
|
||||||
)
|
)
|
||||||
|
if (!isLiteTesting) {
|
||||||
|
showStatus("Проверка завершена")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mainViewModel.liteTestFinished.observe(this) { finished ->
|
mainViewModel.liteTestFinished.observe(this) { finished ->
|
||||||
if (finished && isLiteTesting) {
|
if (finished && isLiteTesting) {
|
||||||
isLiteTesting = false
|
isLiteTesting = false
|
||||||
mainViewModel.sortByTestResults()
|
|
||||||
|
|
||||||
val firstReachable = mainViewModel.serversCache.firstOrNull { cache ->
|
// Ищем лучший сервер ДО сортировки, прямо из текущего cache
|
||||||
(MmkvManager.decodeServerAffiliationInfo(cache.guid)?.testDelayMillis ?: 0L) > 0L
|
val firstReachable = mainViewModel.serversCache
|
||||||
}
|
.filter { (MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: 0L) > 0L }
|
||||||
|
.minByOrNull { MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: Long.MAX_VALUE }
|
||||||
|
|
||||||
if (firstReachable != null) {
|
if (firstReachable != null) {
|
||||||
MmkvManager.setSelectServer(firstReachable.guid)
|
MmkvManager.setSelectServer(firstReachable.guid)
|
||||||
mainViewModel.reloadServerList() // reload AFTER selection so indicator renders correctly
|
}
|
||||||
|
|
||||||
|
mainViewModel.suppressPinSelected = false
|
||||||
|
mainViewModel.sortByTestResults()
|
||||||
|
mainViewModel.reloadServerList()
|
||||||
|
|
||||||
|
if (firstReachable != null) {
|
||||||
showStatus("Подключаемся к быстрейшему серверу")
|
showStatus("Подключаемся к быстрейшему серверу")
|
||||||
// Блокируем кнопки на время подключения
|
|
||||||
setButtonsEnabled(false)
|
|
||||||
applyRunningState(isLoading = true, isRunning = false)
|
applyRunningState(isLoading = true, isRunning = false)
|
||||||
startV2RayWithPermission()
|
startV2RayWithPermission()
|
||||||
} else {
|
} else {
|
||||||
mainViewModel.reloadServerList()
|
|
||||||
showStatus("Нет доступных серверов!")
|
showStatus("Нет доступных серверов!")
|
||||||
setButtonsEnabled(true)
|
setButtonsEnabled(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mainViewModel.isRunning.observe(this) { isRunning ->
|
mainViewModel.isRunning.observe(this) { isRunning ->
|
||||||
applyRunningState(false, isRunning)
|
if (!isFabOperationInProgress) {
|
||||||
|
applyRunningState(false, isRunning)
|
||||||
|
}
|
||||||
// Как только VPN только что подключился — обновляем подписки через него
|
// Как только VPN только что подключился — обновляем подписки через него
|
||||||
if (isRunning && !wasRunning) {
|
if (isRunning && !wasRunning) {
|
||||||
updateSubsViaVpn()
|
updateSubsViaVpn()
|
||||||
@@ -318,15 +328,19 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFabAction() {
|
private fun handleFabAction() {
|
||||||
|
// Если идёт подключение (isLoading) — позволяем прервать и остановить сервис
|
||||||
if (isFabOperationInProgress) {
|
if (isFabOperationInProgress) {
|
||||||
|
Log.d(AppConfig.TAG, "FAB: cancel in-progress, stopping service")
|
||||||
|
isFabOperationInProgress = false
|
||||||
|
lifecycleScope.launch {
|
||||||
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isFabOperationInProgress = true
|
isFabOperationInProgress = true
|
||||||
|
|
||||||
val isRunning = mainViewModel.isRunning.value == true
|
val isRunning = mainViewModel.isRunning.value == true
|
||||||
|
|
||||||
// Блокируем все кнопки сразу
|
|
||||||
setButtonsEnabled(false)
|
|
||||||
applyRunningState(isLoading = true, isRunning = false)
|
applyRunningState(isLoading = true, isRunning = false)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -357,11 +371,21 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLiteAction() {
|
private fun handleLiteAction() {
|
||||||
// If testing is in progress - stop it
|
// Отмена на любом этапе: обновление подписок или тест
|
||||||
if (mainViewModel.isTesting.value == true) {
|
if (mainViewModel.isTesting.value == true || liteActionJob?.isActive == true) {
|
||||||
|
liteActionJob?.cancel()
|
||||||
|
liteActionJob = null
|
||||||
mainViewModel.cancelAllTests()
|
mainViewModel.cancelAllTests()
|
||||||
showStatus("Тест остановлен")
|
mainViewModel.suppressPinSelected = false
|
||||||
isLiteTesting = false
|
isLiteTesting = false
|
||||||
|
isFabOperationInProgress = false
|
||||||
|
showStatus("Остановлено")
|
||||||
|
setButtonsEnabled(true)
|
||||||
|
binding.btnSummaryLite.setIconResource(R.drawable.bolt_24)
|
||||||
|
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(
|
||||||
|
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
|
||||||
|
)
|
||||||
|
hideLoading()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,10 +394,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
isFabOperationInProgress = true
|
isFabOperationInProgress = true
|
||||||
|
|
||||||
// Блокируем все кнопки сразу при нажатии
|
liteActionJob = lifecycleScope.launch {
|
||||||
setButtonsEnabled(false)
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
try {
|
||||||
if (mainViewModel.isRunning.value == true) {
|
if (mainViewModel.isRunning.value == true) {
|
||||||
V2RayServiceManager.stopVService(this@MainActivity)
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
@@ -382,33 +403,45 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
|
|
||||||
showStatus("Обновление профилей...")
|
showStatus("Обновление профилей...")
|
||||||
showLoading()
|
showLoading()
|
||||||
|
// Иконка молнии → стоп пока идёт обновление; FAB и меню блокируем
|
||||||
|
binding.btnSummaryLite.setIconResource(R.drawable.ic_stop_24dp)
|
||||||
|
binding.btnSummaryLite.isEnabled = true
|
||||||
|
binding.btnSummaryLite.alpha = 1.0f
|
||||||
|
binding.fab.isEnabled = false
|
||||||
|
binding.fab.alpha = 0.5f
|
||||||
|
val menu = binding.toolbar.menu
|
||||||
|
menu.findItem(R.id.real_ping_all)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
|
||||||
|
menu.findItem(R.id.filter_by_country)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
|
||||||
|
menu.findItem(R.id.sub_update)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
|
||||||
isLiteTesting = true
|
isLiteTesting = true
|
||||||
|
mainViewModel.suppressPinSelected = true
|
||||||
|
|
||||||
launch(Dispatchers.IO) {
|
val result = withContext(Dispatchers.IO) { mainViewModel.updateConfigViaSubAll() }
|
||||||
val result = mainViewModel.updateConfigViaSubAll()
|
val removed = withContext(Dispatchers.IO) { mainViewModel.removeDuplicateByIpAll() }
|
||||||
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()
|
|
||||||
|
|
||||||
showStatus("Выполняется замер задержки. Ожидаем завершения...")
|
mainViewModel.reloadServerList()
|
||||||
mainViewModel.testAllRealPing()
|
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) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Error in handleLiteAction", e)
|
Log.e(AppConfig.TAG, "Error in handleLiteAction", e)
|
||||||
|
isLiteTesting = false
|
||||||
|
hideLoading()
|
||||||
} finally {
|
} finally {
|
||||||
isFabOperationInProgress = false
|
isFabOperationInProgress = false
|
||||||
|
liteActionJob = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,6 +472,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
isFabOperationInProgress = true
|
isFabOperationInProgress = true
|
||||||
|
applyRunningState(isLoading = true, isRunning = false)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
@@ -446,10 +480,16 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
V2RayServiceManager.stopVService(this@MainActivity)
|
V2RayServiceManager.stopVService(this@MainActivity)
|
||||||
delay(1000)
|
delay(1000)
|
||||||
}
|
}
|
||||||
|
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||||
|
// Сервер был снят с выбора — просто остановились, разблокируем UI
|
||||||
|
applyRunningState(isLoading = false, isRunning = false)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
startV2Ray()
|
startV2Ray()
|
||||||
delay(1000)
|
delay(1000)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Error in restartV2Ray", e)
|
Log.e(AppConfig.TAG, "Error in restartV2Ray", e)
|
||||||
|
applyRunningState(isLoading = false, isRunning = mainViewModel.isRunning.value == true)
|
||||||
} finally {
|
} finally {
|
||||||
isFabOperationInProgress = false
|
isFabOperationInProgress = false
|
||||||
}
|
}
|
||||||
@@ -493,8 +533,16 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
|
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
|
||||||
)
|
)
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
setButtonsEnabled(false)
|
// Во время подключения: только FAB доступен для отмены, всё остальное заблокировано
|
||||||
|
binding.fab.isEnabled = true
|
||||||
|
binding.fab.alpha = 1.0f
|
||||||
binding.fab.backgroundTintList = secContainer
|
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)
|
setStatusDot(DotState.LOADING)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -528,9 +576,30 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
dot.alpha = 1f; dot.scaleX = 1f; dot.scaleY = 1f
|
dot.alpha = 1f; dot.scaleX = 1f; dot.scaleY = 1f
|
||||||
dot.backgroundTintList = ColorStateList.valueOf(when (state) {
|
dot.backgroundTintList = ColorStateList.valueOf(when (state) {
|
||||||
DotState.CONNECTED -> ContextCompat.getColor(this, R.color.status_connected)
|
DotState.CONNECTED -> ContextCompat.getColor(this, R.color.status_connected)
|
||||||
DotState.LOADING -> com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimaryContainer, 0)
|
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)
|
DotState.IDLE -> com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorOutline, 0)
|
||||||
})
|
})
|
||||||
|
if (state == DotState.LOADING) {
|
||||||
|
pulseDot(dot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pulseDot(dot: android.view.View) {
|
||||||
|
dot.animate()
|
||||||
|
.alpha(0.25f)
|
||||||
|
.setDuration(600)
|
||||||
|
.withEndAction {
|
||||||
|
if (dot.isAttachedToWindow) {
|
||||||
|
dot.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(600)
|
||||||
|
.withEndAction {
|
||||||
|
if (dot.isAttachedToWindow && mainViewModel.isTesting.value == true) {
|
||||||
|
pulseDot(dot)
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -568,6 +637,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
|
|
||||||
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
|
searchView.alpha = 0f
|
||||||
|
searchView.animate().alpha(1f).setDuration(220).start()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,7 +661,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.sub_update -> {
|
R.id.sub_update -> {
|
||||||
setButtonsEnabled(false)
|
setSecondaryButtonsEnabled(false)
|
||||||
importConfigViaSub()
|
importConfigViaSub()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -944,14 +1015,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
val codes = allCountriesMap.keys.toTypedArray()
|
val codes = allCountriesMap.keys.toTypedArray()
|
||||||
val labels = allCountriesMap.values.toTypedArray()
|
val labels = allCountriesMap.values.toTypedArray()
|
||||||
|
|
||||||
// In exclude mode: checked = should be EXCLUDED
|
// currentFilter stores excluded set (empty = show all)
|
||||||
// currentFilter stores included set (empty = show all)
|
val checked = BooleanArray(codes.size) { codes[it] in currentFilter }
|
||||||
// Convert to excluded set for UI
|
|
||||||
val allCodes = codes.toSet()
|
|
||||||
val excludedByFilter = if (currentFilter.isEmpty()) emptySet()
|
|
||||||
else allCodes - currentFilter
|
|
||||||
|
|
||||||
val checked = BooleanArray(codes.size) { codes[it] in excludedByFilter }
|
|
||||||
|
|
||||||
val dialog = MaterialAlertDialogBuilder(this@MainActivity)
|
val dialog = MaterialAlertDialogBuilder(this@MainActivity)
|
||||||
.setTitle("Исключить страны")
|
.setTitle("Исключить страны")
|
||||||
@@ -960,9 +1025,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
.setPositiveButton("Применить") { _, _ ->
|
.setPositiveButton("Применить") { _, _ ->
|
||||||
val excluded = codes.filterIndexed { i, _ -> checked[i] }.toSet()
|
val excluded = codes.filterIndexed { i, _ -> checked[i] }.toSet()
|
||||||
val included = if (excluded.isEmpty()) emptySet()
|
mainViewModel.applyCountryFilter(excluded)
|
||||||
else allCodes - excluded
|
|
||||||
mainViewModel.applyCountryFilter(included)
|
|
||||||
val msg = if (excluded.isEmpty()) "Показаны все страны"
|
val msg = if (excluded.isEmpty()) "Показаны все страны"
|
||||||
else "Скрыто: ${excluded.joinToString { CountryDetector.codeToFlag(it) }}"
|
else "Скрыто: ${excluded.joinToString { CountryDetector.codeToFlag(it) }}"
|
||||||
showStatus(msg)
|
showStatus(msg)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.graphics.Color
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
@@ -60,57 +59,48 @@ class MainRecyclerAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun setData(newData: MutableList<ServersCache>?, position: Int = -1) {
|
fun setData(newData: List<ServersCache>) {
|
||||||
val parsedNewData = newData?.toList() ?: emptyList()
|
val oldData = data
|
||||||
|
val parsedNewData = newData
|
||||||
|
|
||||||
if (data.isEmpty() || parsedNewData.isEmpty() || position >= 0) {
|
if (oldData.isEmpty() || parsedNewData.isEmpty()) {
|
||||||
data = parsedNewData.toMutableList()
|
data = parsedNewData.toMutableList()
|
||||||
recomputePingRange()
|
recomputePingRange()
|
||||||
if (position >= 0 && position in data.indices) {
|
notifyDataSetChanged()
|
||||||
notifyItemChanged(position)
|
|
||||||
} else {
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val oldData = data
|
|
||||||
val lm = recyclerView?.layoutManager as? androidx.recyclerview.widget.LinearLayoutManager
|
val lm = recyclerView?.layoutManager as? androidx.recyclerview.widget.LinearLayoutManager
|
||||||
val firstVisible = lm?.findFirstVisibleItemPosition()?.coerceAtLeast(0) ?: 0
|
val firstVisible = lm?.findFirstVisibleItemPosition()?.coerceAtLeast(0) ?: 0
|
||||||
val isAtTop = firstVisible == 0 && (lm?.findViewByPosition(0)?.top ?: 0) >= 0
|
val isAtTop = firstVisible == 0 && (lm?.findViewByPosition(0)?.top ?: 0) >= 0
|
||||||
val firstVisibleGuid = if (!isAtTop) oldData.getOrNull(firstVisible)?.guid else null
|
val firstVisibleGuid = if (!isAtTop) oldData.getOrNull(firstVisible)?.guid else null
|
||||||
|
|
||||||
val diffResult =
|
val diffResult = androidx.recyclerview.widget.DiffUtil.calculateDiff(
|
||||||
androidx.recyclerview.widget.DiffUtil.calculateDiff(
|
object : androidx.recyclerview.widget.DiffUtil.Callback() {
|
||||||
object : androidx.recyclerview.widget.DiffUtil.Callback() {
|
override fun getOldListSize() = oldData.size
|
||||||
override fun getOldListSize() = oldData.size
|
override fun getNewListSize() = parsedNewData.size
|
||||||
override fun getNewListSize() = parsedNewData.size
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
|
override fun areItemsTheSame(oldPos: Int, newPos: Int) =
|
||||||
return oldData[oldPos].guid == parsedNewData[newPos].guid
|
oldData[oldPos].guid == parsedNewData[newPos].guid
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
|
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
|
||||||
val oldProfile = oldData[oldPos].profile
|
val old = oldData[oldPos]
|
||||||
val newProfile = parsedNewData[newPos].profile
|
val new = parsedNewData[newPos]
|
||||||
val oldGuid = oldData[oldPos].guid
|
return old.profile == new.profile &&
|
||||||
val newGuid = parsedNewData[newPos].guid
|
old.profile.isFavorite == new.profile.isFavorite &&
|
||||||
return oldProfile == newProfile &&
|
old.isSelected == new.isSelected &&
|
||||||
oldProfile.isFavorite == newProfile.isFavorite &&
|
old.testDelayMillis == new.testDelayMillis
|
||||||
(oldGuid == MmkvManager.getSelectServer()) == (newGuid == MmkvManager.getSelectServer()) &&
|
}
|
||||||
MmkvManager.decodeServerAffiliationInfo(oldGuid)?.testDelayMillis ==
|
|
||||||
MmkvManager.decodeServerAffiliationInfo(newGuid)?.testDelayMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldPos: Int, newPos: Int): Any? {
|
override fun getChangePayload(oldPos: Int, newPos: Int): Any? {
|
||||||
if (oldData[oldPos].profile.isFavorite != parsedNewData[newPos].profile.isFavorite) {
|
if (oldData[oldPos].profile.isFavorite != parsedNewData[newPos].profile.isFavorite) {
|
||||||
return PAYLOAD_FAVORITE
|
return PAYLOAD_FAVORITE
|
||||||
}
|
}
|
||||||
return super.getChangePayload(oldPos, newPos)
|
return super.getChangePayload(oldPos, newPos)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
data = parsedNewData.toMutableList()
|
data = parsedNewData.toMutableList()
|
||||||
recomputePingRange()
|
recomputePingRange()
|
||||||
@@ -184,31 +174,17 @@ class MainRecyclerAdapter(
|
|||||||
(holder.itemMainBinding.tvTestResult.layoutParams as? ViewGroup.MarginLayoutParams)?.marginStart =
|
(holder.itemMainBinding.tvTestResult.layoutParams as? ViewGroup.MarginLayoutParams)?.marginStart =
|
||||||
if (addressText.isEmpty()) 0 else 6.dpToPx(context)
|
if (addressText.isEmpty()) 0 else 6.dpToPx(context)
|
||||||
|
|
||||||
// Keep selected state very quiet: only a soft surface step, no hard outline.
|
val isSelected = data[position].isSelected
|
||||||
val isSelected = guid == MmkvManager.getSelectServer()
|
|
||||||
holder.itemMainBinding.cardContainer.apply {
|
holder.itemMainBinding.cardContainer.apply {
|
||||||
val surfaceColor = MaterialColors.getColor(
|
val selectedColor = MaterialColors.getColor(
|
||||||
context,
|
|
||||||
com.google.android.material.R.attr.colorSurfaceContainerLow,
|
|
||||||
Color.TRANSPARENT
|
|
||||||
)
|
|
||||||
val selectedSurfaceColor = MaterialColors.getColor(
|
|
||||||
context,
|
context,
|
||||||
com.google.android.material.R.attr.colorSurfaceContainerHigh,
|
com.google.android.material.R.attr.colorSurfaceContainerHigh,
|
||||||
surfaceColor
|
Color.TRANSPARENT
|
||||||
)
|
)
|
||||||
|
|
||||||
setCardBackgroundColor(if (isSelected) selectedSurfaceColor else surfaceColor)
|
setCardBackgroundColor(if (isSelected) selectedColor else Color.TRANSPARENT)
|
||||||
strokeWidth = if (isSelected) 1.dpToPx(context) else 0
|
strokeWidth = 0
|
||||||
strokeColor = if (isSelected) {
|
strokeColor = Color.TRANSPARENT
|
||||||
MaterialColors.getColor(
|
|
||||||
context,
|
|
||||||
com.google.android.material.R.attr.colorOutlineVariant,
|
|
||||||
Color.TRANSPARENT
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Color.TRANSPARENT
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// subscription remarks
|
// subscription remarks
|
||||||
@@ -311,21 +287,6 @@ class MainRecyclerAdapter(
|
|||||||
return subRemarks?.toString() ?: ""
|
return subRemarks?.toString() ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeServerSub(guid: String, position: Int) {
|
|
||||||
val idx = data.indexOfFirst { it.guid == guid }
|
|
||||||
if (idx >= 0) {
|
|
||||||
data.removeAt(idx)
|
|
||||||
recomputePingRange()
|
|
||||||
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 {
|
private fun Int.dpToPx(context: android.content.Context): Int {
|
||||||
return (this * context.resources.displayMetrics.density).toInt()
|
return (this * context.resources.displayMetrics.density).toInt()
|
||||||
}
|
}
|
||||||
@@ -376,6 +337,8 @@ class MainRecyclerAdapter(
|
|||||||
BaseViewHolder(itemFooterBinding.root)
|
BaseViewHolder(itemFooterBinding.root)
|
||||||
|
|
||||||
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
||||||
|
// ViewModel swaps both serverList and _serversCache, then publishSnapshot triggers setData.
|
||||||
|
// We optimistically swap local data + animate immediately for smooth drag UX.
|
||||||
mainViewModel.swapServer(fromPosition, toPosition)
|
mainViewModel.swapServer(fromPosition, toPosition)
|
||||||
if (fromPosition < data.size && toPosition < data.size) {
|
if (fromPosition < data.size && toPosition < data.size) {
|
||||||
Collections.swap(data, fromPosition, toPosition)
|
Collections.swap(data, fromPosition, toPosition)
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancelChildren
|
import kotlinx.coroutines.cancelChildren
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
@@ -42,15 +45,24 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
private var serverList = mutableListOf<String>()
|
private var serverList = mutableListOf<String>()
|
||||||
var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
|
var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
|
||||||
var keywordFilter = ""
|
var keywordFilter = ""
|
||||||
/** ISO codes to show (empty = show all) */
|
/** ISO codes to EXCLUDE (empty = show all) */
|
||||||
var countryFilter: Set<String> = MmkvManager.getCountryFilter()
|
var countryFilter: Set<String> = MmkvManager.getCountryFilter()
|
||||||
private set
|
private set
|
||||||
val serversCache = mutableListOf<ServersCache>()
|
|
||||||
|
// Internal mutable cache — never exposed directly
|
||||||
|
private val _serversCache = mutableListOf<ServersCache>()
|
||||||
|
// Read-only snapshot for external consumers that need direct access (e.g. export, ping)
|
||||||
|
val serversCache: List<ServersCache> get() = _serversCache.toList()
|
||||||
|
|
||||||
|
// Single source of truth for the list UI — emits a new immutable snapshot on every change
|
||||||
|
private val _serverListFlow = MutableStateFlow<List<ServersCache>>(emptyList())
|
||||||
|
val serverListFlow: StateFlow<List<ServersCache>> = _serverListFlow.asStateFlow()
|
||||||
|
|
||||||
val isRunning by lazy { MutableLiveData<Boolean>() }
|
val isRunning by lazy { MutableLiveData<Boolean>() }
|
||||||
val updateListAction by lazy { MutableLiveData<Int>() }
|
|
||||||
val updateTestResultAction by lazy { MutableLiveData<String>() }
|
val updateTestResultAction by lazy { MutableLiveData<String>() }
|
||||||
val liteTestFinished = MutableLiveData<Boolean>()
|
val liteTestFinished = MutableLiveData<Boolean>()
|
||||||
val isTesting by lazy { MutableLiveData<Boolean>().also { it.value = false } }
|
val isTesting by lazy { MutableLiveData<Boolean>().also { it.value = false } }
|
||||||
|
var suppressPinSelected = false
|
||||||
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
|
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,9 +132,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
list
|
list
|
||||||
}
|
}
|
||||||
|
|
||||||
pinSelectedGuidToTop(serverList)
|
if (!suppressPinSelected) pinSelectedGuidToTop(serverList)
|
||||||
updateCache()
|
updateCache()
|
||||||
updateListAction.value = -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,10 +143,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
fun removeServer(guid: String) {
|
fun removeServer(guid: String) {
|
||||||
serverList.remove(guid)
|
serverList.remove(guid)
|
||||||
MmkvManager.removeServer(guid)
|
MmkvManager.removeServer(guid)
|
||||||
val index = getPosition(guid)
|
val index = _serversCache.indexOfFirst { it.guid == guid }
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
serversCache.removeAt(index)
|
_serversCache.removeAt(index)
|
||||||
}
|
}
|
||||||
|
publishSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,17 +161,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Collections.swap(serverList, fromPosition, toPosition)
|
Collections.swap(serverList, fromPosition, toPosition)
|
||||||
Collections.swap(serversCache, fromPosition, toPosition)
|
Collections.swap(_serversCache, fromPosition, toPosition)
|
||||||
|
publishSnapshot()
|
||||||
|
|
||||||
MmkvManager.encodeServerList(serverList, subscriptionId)
|
MmkvManager.encodeServerList(serverList, subscriptionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the cache of servers.
|
* Rebuilds _serversCache from serverList and publishes a new snapshot to serverListFlow.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun updateCache() {
|
fun updateCache() {
|
||||||
serversCache.clear()
|
_serversCache.clear()
|
||||||
val kw = keywordFilter.trim()
|
val kw = keywordFilter.trim()
|
||||||
val searchRegex = try {
|
val searchRegex = try {
|
||||||
if (kw.isNotEmpty()) Regex(kw, setOf(RegexOption.IGNORE_CASE)) else null
|
if (kw.isNotEmpty()) Regex(kw, setOf(RegexOption.IGNORE_CASE)) else null
|
||||||
@@ -167,17 +180,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
val activeCountryFilter = countryFilter
|
val activeCountryFilter = countryFilter
|
||||||
|
val selectedGuid = MmkvManager.getSelectServer().orEmpty()
|
||||||
for (guid in serverList) {
|
for (guid in serverList) {
|
||||||
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
|
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
|
||||||
|
|
||||||
// Country filter
|
|
||||||
if (activeCountryFilter.isNotEmpty()) {
|
if (activeCountryFilter.isNotEmpty()) {
|
||||||
val code = CountryDetector.getCountryCode(profile.remarks, profile.server)
|
val code = CountryDetector.getCountryCode(profile.remarks, profile.server)
|
||||||
if (code !in activeCountryFilter) continue
|
if (code in activeCountryFilter) continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
||||||
|
|
||||||
if (kw.isEmpty()) {
|
if (kw.isEmpty()) {
|
||||||
serversCache.add(ServersCache(guid, profile))
|
_serversCache.add(ServersCache(guid, profile, delay, guid == selectedGuid))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,15 +205,49 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|| server.matchesPattern(searchRegex, kw)
|
|| server.matchesPattern(searchRegex, kw)
|
||||||
|| protocol.matchesPattern(searchRegex, kw)
|
|| protocol.matchesPattern(searchRegex, kw)
|
||||||
) {
|
) {
|
||||||
serversCache.add(ServersCache(guid, profile))
|
_serversCache.add(ServersCache(guid, profile, delay, guid == selectedGuid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
publishSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets a new country filter and reloads list. Pass empty set to show all. */
|
/** Emits an immutable copy of _serversCache to the Flow. Must be called on Main or from @Synchronized blocks. */
|
||||||
fun applyCountryFilter(codes: Set<String>) {
|
private fun publishSnapshot() {
|
||||||
countryFilter = codes
|
_serverListFlow.value = _serversCache.toList()
|
||||||
MmkvManager.setCountryFilter(codes)
|
}
|
||||||
|
|
||||||
|
/** Builds a snapshot of ServersCache for the given subId without changing global state. */
|
||||||
|
fun reloadForSub(subId: String): MutableList<ServersCache>? {
|
||||||
|
val guids = when {
|
||||||
|
subId.isEmpty() -> MmkvManager.decodeAllServerList()
|
||||||
|
subId.startsWith("group_") -> {
|
||||||
|
val allSubs = MmkvManager.decodeSubscriptions()
|
||||||
|
val groupSubs = when (subId) {
|
||||||
|
"group_white" -> allSubs.filter {
|
||||||
|
it.subscription.remarks.startsWith("БЕЛЫЕ", ignoreCase = true) ||
|
||||||
|
it.subscription.remarks.startsWith("WHITE", ignoreCase = true)
|
||||||
|
}
|
||||||
|
"group_black" -> allSubs.filter {
|
||||||
|
it.subscription.remarks.startsWith("ЧЕРНЫЕ", ignoreCase = true) ||
|
||||||
|
it.subscription.remarks.startsWith("BLACK", ignoreCase = true)
|
||||||
|
}
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
groupSubs.flatMap { MmkvManager.decodeServerList(it.guid) }.toMutableList()
|
||||||
|
}
|
||||||
|
else -> MmkvManager.decodeServerList(subId)
|
||||||
|
}
|
||||||
|
return guids.mapNotNull { guid ->
|
||||||
|
val profile = MmkvManager.decodeServerConfig(guid) ?: return@mapNotNull null
|
||||||
|
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
||||||
|
ServersCache(guid, profile, delay)
|
||||||
|
}.toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets excluded countries and reloads list. Pass empty set to show all. */
|
||||||
|
fun applyCountryFilter(excludedCodes: Set<String>) {
|
||||||
|
countryFilter = excludedCodes
|
||||||
|
MmkvManager.setCountryFilter(excludedCodes)
|
||||||
reloadServerList()
|
reloadServerList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,9 +323,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
fun exportAllServer(): Int {
|
fun exportAllServer(): Int {
|
||||||
val serverListCopy =
|
val serverListCopy =
|
||||||
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||||
serverList
|
serverList.toList()
|
||||||
} else {
|
} else {
|
||||||
serversCache.map { it.guid }.toList()
|
_serversCache.map { it.guid }
|
||||||
}
|
}
|
||||||
|
|
||||||
val ret = AngConfigManager.shareNonCustomConfigsToClipboard(
|
val ret = AngConfigManager.shareNonCustomConfigsToClipboard(
|
||||||
@@ -292,9 +341,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
fun testAllTcping() {
|
fun testAllTcping() {
|
||||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||||
SpeedtestManager.closeAllTcpSockets()
|
SpeedtestManager.closeAllTcpSockets()
|
||||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
MmkvManager.clearAllTestDelayResults(_serversCache.map { it.guid })
|
||||||
|
|
||||||
val serversCopy = serversCache.toList()
|
val serversCopy = _serversCache.toList()
|
||||||
for (item in serversCopy) {
|
for (item in serversCopy) {
|
||||||
item.profile.let { outbound ->
|
item.profile.let { outbound ->
|
||||||
val serverAddress = outbound.server
|
val serverAddress = outbound.server
|
||||||
@@ -304,7 +353,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt())
|
val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt())
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
|
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
|
||||||
updateListAction.value = getPosition(item.guid)
|
refreshPingInCache(listOf(item.guid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,15 +388,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
if (removed > 0) {
|
if (removed > 0) {
|
||||||
reloadServerList()
|
reloadServerList()
|
||||||
}
|
}
|
||||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
if (!suppressPinSelected) {
|
||||||
updateListAction.value = -1
|
MmkvManager.clearAllTestDelayResults(_serversCache.map { it.guid })
|
||||||
|
}
|
||||||
|
publishSnapshot()
|
||||||
isTesting.value = true
|
isTesting.value = true
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
if (serversCache.isEmpty()) {
|
if (_serversCache.isEmpty()) {
|
||||||
withContext(Dispatchers.Main) { reloadServerList() }
|
withContext(Dispatchers.Main) { reloadServerList() }
|
||||||
}
|
}
|
||||||
if (serversCache.isEmpty()) {
|
if (_serversCache.isEmpty()) {
|
||||||
withContext(Dispatchers.Main) { isTesting.value = false }
|
withContext(Dispatchers.Main) { isTesting.value = false }
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@@ -360,7 +411,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
key = AppConfig.MSG_MEASURE_CONFIG,
|
key = AppConfig.MSG_MEASURE_CONFIG,
|
||||||
subscriptionId = actualSubId,
|
subscriptionId = actualSubId,
|
||||||
serverGuids = if (keywordFilter.isNotEmpty() || subscriptionId.startsWith("group_")) {
|
serverGuids = if (keywordFilter.isNotEmpty() || subscriptionId.startsWith("group_")) {
|
||||||
serversCache.map { it.guid }
|
_serversCache.map { it.guid }
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
@@ -472,9 +523,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
* @return The position of the server.
|
* @return The position of the server.
|
||||||
*/
|
*/
|
||||||
fun getPosition(guid: String): Int {
|
fun getPosition(guid: String): Int {
|
||||||
serversCache.forEachIndexed { index, it ->
|
_serversCache.forEachIndexed { index, it ->
|
||||||
if (it.guid == guid)
|
if (it.guid == guid) return index
|
||||||
return index
|
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
@@ -484,7 +534,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
* @return The number of removed servers.
|
* @return The number of removed servers.
|
||||||
*/
|
*/
|
||||||
fun removeDuplicateServer(): Int {
|
fun removeDuplicateServer(): Int {
|
||||||
val serversCacheCopy = serversCache.toList().toMutableList()
|
val serversCacheCopy = _serversCache.toList()
|
||||||
val deleteServer = mutableListOf<String>()
|
val deleteServer = mutableListOf<String>()
|
||||||
serversCacheCopy.forEachIndexed { index, sc ->
|
serversCacheCopy.forEachIndexed { index, sc ->
|
||||||
val profile = sc.profile
|
val profile = sc.profile
|
||||||
@@ -511,9 +561,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
*/
|
*/
|
||||||
fun removeDuplicateByIp(): Int {
|
fun removeDuplicateByIp(): Int {
|
||||||
val selectedGuid = MmkvManager.getSelectServer()
|
val selectedGuid = MmkvManager.getSelectServer()
|
||||||
// Group all currently visible servers by their IP address
|
|
||||||
val byIp = LinkedHashMap<String, MutableList<ServersCache>>()
|
val byIp = LinkedHashMap<String, MutableList<ServersCache>>()
|
||||||
for (sc in serversCache) {
|
for (sc in _serversCache) {
|
||||||
val ip = sc.profile.server?.trim()?.lowercase() ?: continue
|
val ip = sc.profile.server?.trim()?.lowercase() ?: continue
|
||||||
byIp.getOrPut(ip) { mutableListOf() }.add(sc)
|
byIp.getOrPut(ip) { mutableListOf() }.add(sc)
|
||||||
}
|
}
|
||||||
@@ -610,11 +659,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||||
MmkvManager.removeAllServer()
|
MmkvManager.removeAllServer()
|
||||||
} else {
|
} else {
|
||||||
val serversCopy = serversCache.toList()
|
val serversCopy = _serversCache.toList()
|
||||||
for (item in serversCopy) {
|
for (item in serversCopy) {
|
||||||
MmkvManager.removeServer(item.guid)
|
MmkvManager.removeServer(item.guid)
|
||||||
}
|
}
|
||||||
serversCache.toList().count()
|
serversCopy.count()
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
@@ -626,20 +675,43 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
* Sorts serversCache in-place by test delay in real time (during a ping test).
|
* Sorts serversCache in-place by test delay in real time (during a ping test).
|
||||||
* Favorites always come first, then sorted ascending by delay (failed/untested go to bottom).
|
* Favorites always come first, then sorted ascending by delay (failed/untested go to bottom).
|
||||||
*/
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun refreshPingInCache(guids: List<String>) {
|
||||||
|
val guidSet = guids.toHashSet()
|
||||||
|
for (i in _serversCache.indices) {
|
||||||
|
val item = _serversCache[i]
|
||||||
|
if (item.guid in guidSet) {
|
||||||
|
val delay = MmkvManager.decodeServerAffiliationInfo(item.guid)?.testDelayMillis ?: 0L
|
||||||
|
if (item.testDelayMillis != delay) {
|
||||||
|
_serversCache[i] = item.copy(testDelayMillis = delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
publishSnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun sortServersCacheInPlace() {
|
fun sortServersCacheInPlace() {
|
||||||
serversCache.sortWith(compareBy(
|
for (i in _serversCache.indices) {
|
||||||
|
val item = _serversCache[i]
|
||||||
|
val delay = MmkvManager.decodeServerAffiliationInfo(item.guid)?.testDelayMillis ?: 0L
|
||||||
|
if (item.testDelayMillis != delay) {
|
||||||
|
_serversCache[i] = item.copy(testDelayMillis = delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_serversCache.sortWith(compareBy(
|
||||||
{ !it.profile.isFavorite },
|
{ !it.profile.isFavorite },
|
||||||
{
|
{
|
||||||
val delay = MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: 0L
|
val delay = it.testDelayMillis
|
||||||
when {
|
when {
|
||||||
delay > 0L -> delay
|
delay > 0L -> delay
|
||||||
delay == 0L -> Long.MAX_VALUE - 1 // untested
|
delay == 0L -> Long.MAX_VALUE - 1
|
||||||
else -> Long.MAX_VALUE // failed
|
else -> Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
pinSelectedCacheItemToTop(serversCache)
|
if (!suppressPinSelected) pinSelectedCacheItemToTop(_serversCache)
|
||||||
|
publishSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sortByTestResults() {
|
fun sortByTestResults() {
|
||||||
@@ -796,7 +868,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
reloadServerList()
|
reloadServerList() // rebuilds _serversCache + publishSnapshot
|
||||||
isTesting.value = false
|
isTesting.value = false
|
||||||
liteTestFinished.value = true
|
liteTestFinished.value = true
|
||||||
liteTestFinished.value = false
|
liteTestFinished.value = false
|
||||||
@@ -834,8 +906,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
|
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
|
||||||
val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return
|
val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return
|
||||||
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
|
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
|
||||||
sortServersCacheInPlace()
|
refreshPingInCache(listOf(resultPair.first))
|
||||||
updateListAction.value = -1
|
if (!suppressPinSelected) sortServersCacheInPlace()
|
||||||
|
// publishSnapshot() already called inside refresh/sort above
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_CONFIG_BATCH -> {
|
AppConfig.MSG_MEASURE_CONFIG_BATCH -> {
|
||||||
@@ -843,8 +916,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
update.results.forEach { result ->
|
update.results.forEach { result ->
|
||||||
MmkvManager.encodeServerTestDelayMillis(result.guid, result.delay)
|
MmkvManager.encodeServerTestDelayMillis(result.guid, result.delay)
|
||||||
}
|
}
|
||||||
sortServersCacheInPlace()
|
refreshPingInCache(update.results.map { it.guid })
|
||||||
updateListAction.value = -1
|
if (!suppressPinSelected) sortServersCacheInPlace()
|
||||||
|
// publishSnapshot() already called inside refresh/sort above
|
||||||
}
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {
|
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
app:title="@string/app_name"
|
app:title="@string/app_name"
|
||||||
app:titleCentered="true"
|
|
||||||
app:titleTextColor="?attr/colorOnSurface"
|
app:titleTextColor="?attr/colorOnSurface"
|
||||||
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
|
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
|
||||||
|
|
||||||
@@ -95,9 +94,11 @@
|
|||||||
<com.google.android.material.tabs.TabLayout
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/tab_group"
|
android:id="@+id/tab_group"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="56dp"
|
||||||
|
android:minHeight="56dp"
|
||||||
android:background="?attr/colorSurface"
|
android:background="?attr/colorSurface"
|
||||||
app:tabIndicatorFullWidth="true"
|
app:tabIndicatorFullWidth="true"
|
||||||
|
app:tabIndicatorHeight="3dp"
|
||||||
app:tabMode="fixed"
|
app:tabMode="fixed"
|
||||||
app:tabGravity="fill"
|
app:tabGravity="fill"
|
||||||
app:tabMaxWidth="0dp"
|
app:tabMaxWidth="0dp"
|
||||||
@@ -173,6 +174,8 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:paddingEnd="12dp">
|
android:paddingEnd="12dp">
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,16 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="12dp"
|
android:paddingStart="12dp"
|
||||||
android:paddingTop="5dp"
|
android:paddingTop="6dp"
|
||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:paddingBottom="5dp"
|
android:paddingBottom="6dp"
|
||||||
android:gravity="center_vertical">
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/card_container"
|
android:id="@+id/card_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
|
app:cardBackgroundColor="@android:color/transparent"
|
||||||
app:cardCornerRadius="14dp"
|
app:cardCornerRadius="14dp"
|
||||||
app:cardElevation="0dp"
|
app:cardElevation="0dp"
|
||||||
app:strokeWidth="0dp">
|
app:strokeWidth="0dp">
|
||||||
@@ -31,9 +31,9 @@
|
|||||||
android:nextFocusRight="@+id/layout_share"
|
android:nextFocusRight="@+id/layout_share"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingStart="0dp"
|
android:paddingStart="0dp"
|
||||||
android:paddingTop="11dp"
|
android:paddingTop="13dp"
|
||||||
android:paddingEnd="4dp"
|
android:paddingEnd="4dp"
|
||||||
android:paddingBottom="11dp">
|
android:paddingBottom="13dp">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_widget_name">v2rayNG</string>
|
<string name="app_widget_name">v2rayNG</string>
|
||||||
<string name="app_tile_name">v2rayNG</string>
|
<string name="app_tile_name">v2rayNG</string>
|
||||||
<string name="app_tile_first_use">Первое использование этой функции, пожалуйста, используйте приложение, чтобы добавить сервер</string>
|
<string
|
||||||
|
name="app_tile_first_use"
|
||||||
|
>Первое использование этой функции, пожалуйста, используйте приложение, чтобы добавить сервер</string>
|
||||||
<string name="navigation_drawer_open">Открыть панель навигации</string>
|
<string name="navigation_drawer_open">Открыть панель навигации</string>
|
||||||
<string name="navigation_drawer_close">Закрыть панель навигации</string>
|
<string name="navigation_drawer_close">Закрыть панель навигации</string>
|
||||||
<string name="migration_success">Успешный перенос данных!</string>
|
<string name="migration_success">Успешный перенос данных!</string>
|
||||||
<string name="drawer_forked_text">forked from <a href="https://github.com/2dust/v2rayng">V2RayNG</a></string>
|
<string name="drawer_forked_text">forked from <a
|
||||||
<string name="drawer_developed_text">developed by developers from <a href="https://t.me/openlibrecommunity">Olc</a></string>
|
href="https://github.com/2dust/v2rayng"
|
||||||
|
>V2RayNG</a></string>
|
||||||
|
<string name="drawer_developed_text">developed by developers from <a
|
||||||
|
href="https://t.me/openlibrecommunity"
|
||||||
|
>Olc</a></string>
|
||||||
<string name="action_stop_service">Остановить службу</string>
|
<string name="action_stop_service">Остановить службу</string>
|
||||||
<string name="migration_fail">Перенос данных не выполнен!</string>
|
<string name="migration_fail">Перенос данных не выполнен!</string>
|
||||||
<string name="pull_down_to_refresh">Потяните вниз для обновления!</string>
|
<string name="pull_down_to_refresh">Потяните вниз для обновления!</string>
|
||||||
@@ -15,7 +21,9 @@
|
|||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
<string name="notification_action_stop_v2ray">Остановить</string>
|
<string name="notification_action_stop_v2ray">Остановить</string>
|
||||||
<string name="toast_permission_denied">Разрешение не получено</string>
|
<string name="toast_permission_denied">Разрешение не получено</string>
|
||||||
<string name="toast_permission_denied_notification">Разрешение на отображение уведомлений не получено</string>
|
<string
|
||||||
|
name="toast_permission_denied_notification"
|
||||||
|
>Разрешение на отображение уведомлений не получено</string>
|
||||||
<string name="notification_action_more">Ещё…</string>
|
<string name="notification_action_more">Ещё…</string>
|
||||||
<string name="toast_services_start">Запуск служб</string>
|
<string name="toast_services_start">Запуск служб</string>
|
||||||
<string name="toast_services_stop">Остановка служб</string>
|
<string name="toast_services_stop">Остановка служб</string>
|
||||||
@@ -29,19 +37,41 @@
|
|||||||
<string name="menu_item_edit_config">Изменить профиль</string>
|
<string name="menu_item_edit_config">Изменить профиль</string>
|
||||||
<string name="menu_item_del_config">Удалить профиль</string>
|
<string name="menu_item_del_config">Удалить профиль</string>
|
||||||
<string name="menu_item_import_config_qrcode">Импорт из QR-кода</string>
|
<string name="menu_item_import_config_qrcode">Импорт из QR-кода</string>
|
||||||
<string name="menu_item_import_config_clipboard">Импорт из буфера обмена</string>
|
<string
|
||||||
|
name="menu_item_import_config_clipboard"
|
||||||
|
>Импорт из буфера обмена</string>
|
||||||
<string name="menu_item_import_config_local">Импорт из файла</string>
|
<string name="menu_item_import_config_local">Импорт из файла</string>
|
||||||
<string name="menu_item_import_config_policy_group">Добавить группу политик</string>
|
<string
|
||||||
<string name="menu_item_import_config_manually_vmess">Ручной ввод VMess</string>
|
name="menu_item_import_config_policy_group"
|
||||||
<string name="menu_item_import_config_manually_vless">Ручной ввод VLESS</string>
|
>Добавить группу политик</string>
|
||||||
<string name="menu_item_import_config_manually_ss">Ручной ввод Shadowsocks</string>
|
<string
|
||||||
<string name="menu_item_import_config_manually_socks">Ручной ввод SOCKS</string>
|
name="menu_item_import_config_manually_vmess"
|
||||||
<string name="menu_item_import_config_manually_http">Ручной ввод HTTP</string>
|
>Ручной ввод VMess</string>
|
||||||
<string name="menu_item_import_config_manually_trojan">Ручной ввод Trojan</string>
|
<string
|
||||||
<string name="menu_item_import_config_manually_wireguard">Ручной ввод WireGuard</string>
|
name="menu_item_import_config_manually_vless"
|
||||||
<string name="menu_item_import_config_manually_hysteria2">Ручной ввод Hysteria2</string>
|
>Ручной ввод VLESS</string>
|
||||||
|
<string
|
||||||
|
name="menu_item_import_config_manually_ss"
|
||||||
|
>Ручной ввод Shadowsocks</string>
|
||||||
|
<string
|
||||||
|
name="menu_item_import_config_manually_socks"
|
||||||
|
>Ручной ввод SOCKS</string>
|
||||||
|
<string
|
||||||
|
name="menu_item_import_config_manually_http"
|
||||||
|
>Ручной ввод HTTP</string>
|
||||||
|
<string
|
||||||
|
name="menu_item_import_config_manually_trojan"
|
||||||
|
>Ручной ввод Trojan</string>
|
||||||
|
<string
|
||||||
|
name="menu_item_import_config_manually_wireguard"
|
||||||
|
>Ручной ввод WireGuard</string>
|
||||||
|
<string
|
||||||
|
name="menu_item_import_config_manually_hysteria2"
|
||||||
|
>Ручной ввод Hysteria2</string>
|
||||||
<string name="del_config_comfirm">Подтверждаете удаление?</string>
|
<string name="del_config_comfirm">Подтверждаете удаление?</string>
|
||||||
<string name="del_invalid_config_comfirm">Выполните проверку перед удалением! Подтверждаете удаление?</string>
|
<string
|
||||||
|
name="del_invalid_config_comfirm"
|
||||||
|
>Выполните проверку перед удалением! Подтверждаете удаление?</string>
|
||||||
<string name="server_lab_remarks">Название</string>
|
<string name="server_lab_remarks">Название</string>
|
||||||
<string name="server_lab_address">Адрес</string>
|
<string name="server_lab_address">Адрес</string>
|
||||||
<string name="server_lab_port">Порт</string>
|
<string name="server_lab_port">Порт</string>
|
||||||
@@ -82,45 +112,73 @@
|
|||||||
<string name="server_lab_encryption">Шифрование</string>
|
<string name="server_lab_encryption">Шифрование</string>
|
||||||
<string name="server_lab_flow">Поток</string>
|
<string name="server_lab_flow">Поток</string>
|
||||||
<string name="server_lab_public_key">Открытый ключ</string>
|
<string name="server_lab_public_key">Открытый ключ</string>
|
||||||
<string name="server_lab_preshared_key">Дополнительный ключ шифрования (необязательно)</string>
|
<string
|
||||||
|
name="server_lab_preshared_key"
|
||||||
|
>Дополнительный ключ шифрования (необязательно)</string>
|
||||||
<string name="server_lab_short_id">ShortID</string>
|
<string name="server_lab_short_id">ShortID</string>
|
||||||
<string name="server_lab_spider_x">SpiderX</string>
|
<string name="server_lab_spider_x">SpiderX</string>
|
||||||
<string name="server_lab_mldsa65_verify">mldsa65Verify</string>
|
<string name="server_lab_mldsa65_verify">mldsa65Verify</string>
|
||||||
<string name="server_lab_secret_key">Закрытый ключ</string>
|
<string name="server_lab_secret_key">Закрытый ключ</string>
|
||||||
<string name="server_lab_reserved">Reserved (необязательно, через запятую)</string>
|
<string
|
||||||
<string name="server_lab_local_address">Локальный адрес (необязательно, IPv4/IPv6 через запятую)</string>
|
name="server_lab_reserved"
|
||||||
<string name="server_lab_local_mtu">MTU (необязательно, по умолчанию 1420)</string>
|
>Reserved (необязательно, через запятую)</string>
|
||||||
|
<string
|
||||||
|
name="server_lab_local_address"
|
||||||
|
>Локальный адрес (необязательно, IPv4/IPv6 через запятую)</string>
|
||||||
|
<string
|
||||||
|
name="server_lab_local_mtu"
|
||||||
|
>MTU (необязательно, по умолчанию 1420)</string>
|
||||||
<string name="toast_success">Успешно</string>
|
<string name="toast_success">Успешно</string>
|
||||||
<string name="toast_failure">Ошибка</string>
|
<string name="toast_failure">Ошибка</string>
|
||||||
<string name="toast_none_data">Ничего нет</string>
|
<string name="toast_none_data">Ничего нет</string>
|
||||||
<string name="toast_incorrect_protocol">Неправильный протокол</string>
|
<string name="toast_incorrect_protocol">Неправильный протокол</string>
|
||||||
<string name="toast_decoding_failed">Невозможно декодировать</string>
|
<string name="toast_decoding_failed">Невозможно декодировать</string>
|
||||||
<string name="title_file_chooser">Выберите профиль</string>
|
<string name="title_file_chooser">Выберите профиль</string>
|
||||||
<string name="toast_require_file_manager">Установите файловый менеджер</string>
|
<string
|
||||||
|
name="toast_require_file_manager"
|
||||||
|
>Установите файловый менеджер</string>
|
||||||
<string name="server_customize_config">Изменить профиль</string>
|
<string name="server_customize_config">Изменить профиль</string>
|
||||||
<string name="toast_config_file_invalid">Неправильный профиль</string>
|
<string name="toast_config_file_invalid">Неправильный профиль</string>
|
||||||
<string name="server_lab_content">Данные</string>
|
<string name="server_lab_content">Данные</string>
|
||||||
<string name="toast_none_data_clipboard">В буфере обмена нет данных</string>
|
<string name="toast_none_data_clipboard">В буфере обмена нет данных</string>
|
||||||
<string name="toast_invalid_url">Неправильный URL</string>
|
<string name="toast_invalid_url">Неправильный URL</string>
|
||||||
<string name="toast_insecure_url_protocol">Не используйте небезопасный HTTP-протокол в адресе подписки</string>
|
<string
|
||||||
<string name="server_lab_need_inbound">Убедитесь, что входящий порт соответствует настройкам</string>
|
name="toast_insecure_url_protocol"
|
||||||
|
>Не используйте небезопасный HTTP-протокол в адресе подписки</string>
|
||||||
|
<string
|
||||||
|
name="server_lab_need_inbound"
|
||||||
|
>Убедитесь, что входящий порт соответствует настройкам</string>
|
||||||
<string name="toast_malformed_josn">Профиль повреждён</string>
|
<string name="toast_malformed_josn">Профиль повреждён</string>
|
||||||
<string name="server_lab_request_host6">Узел (SNI) (необязательно)</string>
|
<string name="server_lab_request_host6">Узел (SNI) (необязательно)</string>
|
||||||
<string name="toast_action_not_allowed">Это действие запрещено</string>
|
<string name="toast_action_not_allowed">Это действие запрещено</string>
|
||||||
<string name="server_obfs_password">Пароль obfs</string>
|
<string name="server_obfs_password">Пароль obfs</string>
|
||||||
<string name="server_lab_port_hop">Смена портов (переопределяет порт)</string>
|
<string
|
||||||
|
name="server_lab_port_hop"
|
||||||
|
>Смена портов (переопределяет порт)</string>
|
||||||
<string name="server_lab_port_hop_interval">Интервал смены портов</string>
|
<string name="server_lab_port_hop_interval">Интервал смены портов</string>
|
||||||
<string name="server_lab_bandwidth_down">Входящая пропускная способность (допускаются: k/m/g/t)</string>
|
<string
|
||||||
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string>
|
name="server_lab_bandwidth_down"
|
||||||
|
>Входящая пропускная способность (допускаются: k/m/g/t)</string>
|
||||||
|
<string
|
||||||
|
name="server_lab_bandwidth_up"
|
||||||
|
>Исходящая пропускная способность (допускаются: k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">Режим XHTTP</string>
|
<string name="server_lab_xhttp_mode">Режим XHTTP</string>
|
||||||
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
|
<string
|
||||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
name="server_lab_xhttp_extra"
|
||||||
|
>Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
|
||||||
|
<string
|
||||||
|
name="server_lab_final_mask"
|
||||||
|
>finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
|
<string
|
||||||
|
name="server_lab_pinned_ca256"
|
||||||
|
>Отпечаток сертификата (SHA-256)</string>
|
||||||
|
|
||||||
<!-- UserAssetActivity -->
|
<!-- UserAssetActivity -->
|
||||||
<string name="toast_asset_copy_failed">Невозможно скопировать файл, используйте файловый менеджер</string>
|
<string
|
||||||
|
name="toast_asset_copy_failed"
|
||||||
|
>Невозможно скопировать файл, используйте файловый менеджер</string>
|
||||||
<string name="menu_item_add_asset">Добавить ресурс</string>
|
<string name="menu_item_add_asset">Добавить ресурс</string>
|
||||||
<string name="menu_item_add_file">Добавить файлы</string>
|
<string name="menu_item_add_file">Добавить файлы</string>
|
||||||
<string name="menu_item_add_url">Добавить URL</string>
|
<string name="menu_item_add_url">Добавить URL</string>
|
||||||
@@ -130,7 +188,9 @@
|
|||||||
<string name="title_user_asset_add_url">Добавить URL ресурса</string>
|
<string name="title_user_asset_add_url">Добавить URL ресурса</string>
|
||||||
<string name="msg_file_not_found">Файл не найден</string>
|
<string name="msg_file_not_found">Файл не найден</string>
|
||||||
<string name="msg_remark_is_duplicate">Название уже существует</string>
|
<string name="msg_remark_is_duplicate">Название уже существует</string>
|
||||||
<string name="asset_geo_files_sources">Источник геофайлов (необязательно)</string>
|
<string
|
||||||
|
name="asset_geo_files_sources"
|
||||||
|
>Источник геофайлов (необязательно)</string>
|
||||||
|
|
||||||
<!-- PerAppProxyActivity -->
|
<!-- PerAppProxyActivity -->
|
||||||
<string name="msg_dialog_progress">Загрузка…</string>
|
<string name="msg_dialog_progress">Загрузка…</string>
|
||||||
@@ -144,7 +204,9 @@
|
|||||||
<string name="menu_item_export_proxy_app">Экспорт в буфер обмена</string>
|
<string name="menu_item_export_proxy_app">Экспорт в буфер обмена</string>
|
||||||
<string name="menu_item_import_proxy_app">Импорт из буфера обмена</string>
|
<string name="menu_item_import_proxy_app">Импорт из буфера обмена</string>
|
||||||
<string name="per_app_proxy_settings">Раздельное туннелирование</string>
|
<string name="per_app_proxy_settings">Раздельное туннелирование</string>
|
||||||
<string name="per_app_proxy_settings_enable">Раздельное туннелирование</string>
|
<string
|
||||||
|
name="per_app_proxy_settings_enable"
|
||||||
|
>Раздельное туннелирование</string>
|
||||||
|
|
||||||
<!-- Preferences -->
|
<!-- Preferences -->
|
||||||
<string name="title_settings">Настройки</string>
|
<string name="title_settings">Настройки</string>
|
||||||
@@ -152,24 +214,50 @@
|
|||||||
<string name="title_core_settings">Настройки ядра</string>
|
<string name="title_core_settings">Настройки ядра</string>
|
||||||
<string name="title_vpn_settings">Настройки VPN</string>
|
<string name="title_vpn_settings">Настройки VPN</string>
|
||||||
<string name="title_pref_per_app_proxy">Раздельное туннелирование</string>
|
<string name="title_pref_per_app_proxy">Раздельное туннелирование</string>
|
||||||
<string name="summary_pref_per_app_proxy">Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню.</string>
|
<string
|
||||||
|
name="summary_pref_per_app_proxy"
|
||||||
|
>Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню.</string>
|
||||||
<string name="title_pref_is_booted">Автоподключение при запуске</string>
|
<string name="title_pref_is_booted">Автоподключение при запуске</string>
|
||||||
<string name="summary_pref_is_booted">Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string>
|
<string
|
||||||
|
name="summary_pref_is_booted"
|
||||||
|
>Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_sort_after_test">Автосортировка профилей</string>
|
<string
|
||||||
<string name="summary_pref_auto_sort_after_test">Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string>
|
name="title_pref_auto_sort_after_test"
|
||||||
|
>Автосортировка профилей</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_auto_sort_after_test"
|
||||||
|
>Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string>
|
||||||
|
|
||||||
<string name="title_pref_show_copy_button">Показывать кнопку копирования</string>
|
<string
|
||||||
<string name="summary_pref_show_copy_button">Показывать кнопку для копирования конфигурации сервера в буфер обмена</string>
|
name="title_pref_show_copy_button"
|
||||||
<string name="title_pref_show_server_ip">Показывать IP / хост сервера</string>
|
>Показывать кнопку копирования</string>
|
||||||
<string name="summary_pref_show_server_ip">Отображать IP-адрес или хост сервера под названием</string>
|
<string
|
||||||
|
name="summary_pref_show_copy_button"
|
||||||
|
>Показывать кнопку для копирования конфигурации сервера в буфер обмена</string>
|
||||||
|
<string
|
||||||
|
name="title_pref_show_server_ip"
|
||||||
|
>Показывать IP / хост сервера</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_show_server_ip"
|
||||||
|
>Отображать IP-адрес или хост сервера под названием</string>
|
||||||
|
|
||||||
<string name="title_mux_settings">Настройки мультиплексирования</string>
|
<string name="title_mux_settings">Настройки мультиплексирования</string>
|
||||||
<string name="title_pref_mux_enabled">Использовать мультиплексирование</string>
|
<string
|
||||||
<string name="summary_pref_mux_enabled">Быстрее, но это может привести к нестабильному соединению.\nНиже можно настроить обработку TCP, UDP и QUIC.</string>
|
name="title_pref_mux_enabled"
|
||||||
<string name="title_pref_mux_concurency">TCP-соединения (диапазон от 1 до 1024)</string>
|
>Использовать мультиплексирование</string>
|
||||||
<string name="title_pref_mux_xudp_concurency">XUDP-соединения (диапазон от 1 до 1024)</string>
|
<string
|
||||||
<string name="title_pref_mux_xudp_quic">Обработка QUIC в мультиплексном туннеле</string>
|
name="summary_pref_mux_enabled"
|
||||||
|
>Быстрее, но это может привести к нестабильному соединению.\nНиже можно настроить обработку TCP, UDP и QUIC.</string>
|
||||||
|
<string
|
||||||
|
name="title_pref_mux_concurency"
|
||||||
|
>TCP-соединения (диапазон от 1 до 1024)</string>
|
||||||
|
<string
|
||||||
|
name="title_pref_mux_xudp_concurency"
|
||||||
|
>XUDP-соединения (диапазон от 1 до 1024)</string>
|
||||||
|
<string
|
||||||
|
name="title_pref_mux_xudp_quic"
|
||||||
|
>Обработка QUIC в мультиплексном туннеле</string>
|
||||||
<string-array name="mux_xudp_quic_entries">
|
<string-array name="mux_xudp_quic_entries">
|
||||||
<item>Отклонять</item>
|
<item>Отклонять</item>
|
||||||
<item>Разрешать</item>
|
<item>Разрешать</item>
|
||||||
@@ -177,49 +265,87 @@
|
|||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string name="title_pref_speed_enabled">Показывать скорость</string>
|
<string name="title_pref_speed_enabled">Показывать скорость</string>
|
||||||
<string name="summary_pref_speed_enabled">Показывать текущую скорость в уведомлении.\nЗначок будет меняться в зависимости от использования.</string>
|
<string
|
||||||
|
name="summary_pref_speed_enabled"
|
||||||
|
>Показывать текущую скорость в уведомлении.\nЗначок будет меняться в зависимости от использования.</string>
|
||||||
|
|
||||||
<string name="title_pref_sniffing_enabled">Анализировать пакеты</string>
|
<string name="title_pref_sniffing_enabled">Анализировать пакеты</string>
|
||||||
<string name="summary_pref_sniffing_enabled">Пытаться определять доменные имена в пакетах (по умолчанию включено)</string>
|
<string
|
||||||
<string name="title_pref_route_only_enabled">Домен только для маршрутизации</string>
|
name="summary_pref_sniffing_enabled"
|
||||||
<string name="summary_pref_route_only_enabled">Использовать доменное имя только для маршрутизации и сохранять целевой адрес в виде IP.</string>
|
>Пытаться определять доменные имена в пакетах (по умолчанию включено)</string>
|
||||||
|
<string
|
||||||
|
name="title_pref_route_only_enabled"
|
||||||
|
>Домен только для маршрутизации</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_route_only_enabled"
|
||||||
|
>Использовать доменное имя только для маршрутизации и сохранять целевой адрес в виде IP.</string>
|
||||||
|
|
||||||
<string name="title_pref_local_dns_enabled">Использовать локальную DNS</string>
|
<string
|
||||||
<string name="summary_pref_local_dns_enabled">Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая»)</string>
|
name="title_pref_local_dns_enabled"
|
||||||
|
>Использовать локальную DNS</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_local_dns_enabled"
|
||||||
|
>Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая»)</string>
|
||||||
|
|
||||||
<string name="title_pref_fake_dns_enabled">Использовать поддельную DNS</string>
|
<string
|
||||||
<string name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string>
|
name="title_pref_fake_dns_enabled"
|
||||||
|
>Использовать поддельную DNS</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_fake_dns_enabled"
|
||||||
|
>Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string>
|
||||||
|
|
||||||
<string name="title_pref_prefer_ipv6">Предпочитать IPv6</string>
|
<string name="title_pref_prefer_ipv6">Предпочитать IPv6</string>
|
||||||
<string name="summary_pref_prefer_ipv6">Использовать маршрутизацию IPv6 предпочитать IPv6-адреса</string>
|
<string
|
||||||
|
name="summary_pref_prefer_ipv6"
|
||||||
|
>Использовать маршрутизацию IPv6 предпочитать IPv6-адреса</string>
|
||||||
|
|
||||||
<string name="title_pref_remote_dns">Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно)</string>
|
<string
|
||||||
|
name="title_pref_remote_dns"
|
||||||
|
>Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно)</string>
|
||||||
<string name="summary_pref_remote_dns">DNS</string>
|
<string name="summary_pref_remote_dns">DNS</string>
|
||||||
|
|
||||||
<string name="title_pref_vpn_dns">VPN DNS (только IPv4/v6)</string>
|
<string name="title_pref_vpn_dns">VPN DNS (только IPv4/v6)</string>
|
||||||
<string name="title_pref_vpn_bypass_lan">VPN обходит LAN</string>
|
<string name="title_pref_vpn_bypass_lan">VPN обходит LAN</string>
|
||||||
|
|
||||||
<string name="title_pref_vpn_interface_address">Адрес интерфейса VPN</string>
|
<string
|
||||||
|
name="title_pref_vpn_interface_address"
|
||||||
|
>Адрес интерфейса VPN</string>
|
||||||
<string name="title_pref_vpn_mtu">VPN MTU (по умолчанию 1500)</string>
|
<string name="title_pref_vpn_mtu">VPN MTU (по умолчанию 1500)</string>
|
||||||
|
|
||||||
<string name="title_pref_domestic_dns">Внутренняя DNS (необязательно)</string>
|
<string
|
||||||
|
name="title_pref_domestic_dns"
|
||||||
|
>Внутренняя DNS (необязательно)</string>
|
||||||
<string name="summary_pref_domestic_dns">DNS</string>
|
<string name="summary_pref_domestic_dns">DNS</string>
|
||||||
|
|
||||||
<string name="title_pref_dns_hosts">Узлы DNS (формат: домен:адрес,…)</string>
|
<string
|
||||||
|
name="title_pref_dns_hosts"
|
||||||
|
>Узлы DNS (формат: домен:адрес,…)</string>
|
||||||
<string name="summary_pref_dns_hosts">домен:адрес,…</string>
|
<string name="summary_pref_dns_hosts">домен:адрес,…</string>
|
||||||
|
|
||||||
<string name="title_pref_delay_test_url">Сервис проверки задержки</string>
|
<string name="title_pref_delay_test_url">Сервис проверки задержки</string>
|
||||||
<string name="summary_pref_delay_test_url">URL</string>
|
<string name="summary_pref_delay_test_url">URL</string>
|
||||||
|
|
||||||
<string name="title_pref_ip_api_url">Сервис проверки текущего соединения</string>
|
<string
|
||||||
|
name="title_pref_ip_api_url"
|
||||||
|
>Сервис проверки текущего соединения</string>
|
||||||
<string name="summary_pref_ip_api_url">URL</string>
|
<string name="summary_pref_ip_api_url">URL</string>
|
||||||
|
|
||||||
<string name="title_pref_proxy_sharing_enabled">Разрешать соединения из LAN</string>
|
<string
|
||||||
<string name="summary_pref_proxy_sharing_enabled">Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string>
|
name="title_pref_proxy_sharing_enabled"
|
||||||
<string name="toast_warning_pref_proxysharing_short">Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети</string>
|
>Разрешать соединения из LAN</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_proxy_sharing_enabled"
|
||||||
|
>Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string>
|
||||||
|
<string
|
||||||
|
name="toast_warning_pref_proxysharing_short"
|
||||||
|
>Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети</string>
|
||||||
|
|
||||||
<string name="title_pref_allow_insecure">Разрешать небезопасные соединения</string>
|
<string
|
||||||
<string name="summary_pref_allow_insecure">Для TLS по умолчанию разрешены небезопасные соединения</string>
|
name="title_pref_allow_insecure"
|
||||||
|
>Разрешать небезопасные соединения</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_allow_insecure"
|
||||||
|
>Для TLS по умолчанию разрешены небезопасные соединения</string>
|
||||||
|
|
||||||
<string name="title_pref_socks_port">Порт локального прокси</string>
|
<string name="title_pref_socks_port">Порт локального прокси</string>
|
||||||
<string name="summary_pref_socks_port">Порт локального прокси</string>
|
<string name="summary_pref_socks_port">Порт локального прокси</string>
|
||||||
@@ -227,26 +353,50 @@
|
|||||||
<string name="title_pref_local_dns_port">Порт локальной DNS</string>
|
<string name="title_pref_local_dns_port">Порт локальной DNS</string>
|
||||||
<string name="summary_pref_local_dns_port">Порт локальной DNS</string>
|
<string name="summary_pref_local_dns_port">Порт локальной DNS</string>
|
||||||
|
|
||||||
<string name="title_pref_confirm_remove">Подтверждать удаление профиля</string>
|
<string
|
||||||
<string name="summary_pref_confirm_remove">Обязательное подтверждение удаления профиля</string>
|
name="title_pref_confirm_remove"
|
||||||
|
>Подтверждать удаление профиля</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_confirm_remove"
|
||||||
|
>Обязательное подтверждение удаления профиля</string>
|
||||||
|
|
||||||
<string name="title_pref_start_scan_immediate">Сканировать при запуске</string>
|
<string
|
||||||
<string name="summary_pref_start_scan_immediate">Начинать сканирование сразу при запуске приложения или запускать функцию сканирования камерой или из изображения через панель инструментов</string>
|
name="title_pref_start_scan_immediate"
|
||||||
|
>Сканировать при запуске</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_start_scan_immediate"
|
||||||
|
>Начинать сканирование сразу при запуске приложения или запускать функцию сканирования камерой или из изображения через панель инструментов</string>
|
||||||
|
|
||||||
<string name="title_pref_append_http_proxy">Дополнительный HTTP-прокси</string>
|
<string
|
||||||
<string name="summary_pref_append_http_proxy">HTTP-прокси будет использоваться напрямую (из браузера и других поддерживающих приложений), минуя виртуальный сетевой адаптер (Android 10+)</string>
|
name="title_pref_append_http_proxy"
|
||||||
|
>Дополнительный HTTP-прокси</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_append_http_proxy"
|
||||||
|
>HTTP-прокси будет использоваться напрямую (из браузера и других поддерживающих приложений), минуя виртуальный сетевой адаптер (Android 10+)</string>
|
||||||
|
|
||||||
<string name="title_pref_double_column_display">Профили в два столбца</string>
|
<string
|
||||||
<string name="summary_pref_double_column_display">Список профилей отображается двумя столбцами, что позволяет показать больше информации на экране. Требуется перезапуск приложения.</string>
|
name="title_pref_double_column_display"
|
||||||
|
>Профили в два столбца</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_double_column_display"
|
||||||
|
>Список профилей отображается двумя столбцами, что позволяет показать больше информации на экране. Требуется перезапуск приложения.</string>
|
||||||
|
|
||||||
<string name="title_pref_group_all_display">Общая вкладка групп</string>
|
<string name="title_pref_group_all_display">Общая вкладка групп</string>
|
||||||
<string name="summary_pref_group_all_display">Показывать дополнительную вкладку со всеми профилями групп</string>
|
<string
|
||||||
|
name="summary_pref_group_all_display"
|
||||||
|
>Показывать дополнительную вкладку со всеми профилями групп</string>
|
||||||
|
|
||||||
<!-- AboutActivity -->
|
<!-- AboutActivity -->
|
||||||
<string name="title_pref_feedback">Обратная связь</string>
|
<string name="title_pref_feedback">Обратная связь</string>
|
||||||
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string>
|
<string
|
||||||
<string name="summary_pref_tg_group">Присоединиться к группе в Telegram</string>
|
name="summary_pref_feedback"
|
||||||
<string name="toast_tg_app_not_found">Приложение Telegram не найдено</string>
|
>Предложить улучшение или сообщить об ошибке на GitHub</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_tg_group"
|
||||||
|
>Присоединиться к группе в Telegram</string>
|
||||||
|
<string
|
||||||
|
name="toast_tg_app_not_found"
|
||||||
|
>Приложение Telegram не найдено</string>
|
||||||
<string name="title_privacy_policy">Политика конфиденциальности</string>
|
<string name="title_privacy_policy">Политика конфиденциальности</string>
|
||||||
<string name="title_about">О приложении</string>
|
<string name="title_about">О приложении</string>
|
||||||
<string name="title_source_code">Исходный код</string>
|
<string name="title_source_code">Исходный код</string>
|
||||||
@@ -255,33 +405,57 @@
|
|||||||
|
|
||||||
<string name="title_pref_promotion">Содействие</string>
|
<string name="title_pref_promotion">Содействие</string>
|
||||||
|
|
||||||
<string name="title_pref_auto_update_subscription">Автоматически обновлять подписки</string>
|
<string
|
||||||
<string name="summary_pref_auto_update_subscription">Автоматическое обновление подписок в фоновом режиме с указанным интервалом. В зависимости от устройства эта функция может работать не всегда.</string>
|
name="title_pref_auto_update_subscription"
|
||||||
<string name="title_pref_auto_update_interval">Интервал автообновления (минут, не менее 15)</string>
|
>Автоматически обновлять подписки</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_auto_update_subscription"
|
||||||
|
>Автоматическое обновление подписок в фоновом режиме с указанным интервалом. В зависимости от устройства эта функция может работать не всегда.</string>
|
||||||
|
<string
|
||||||
|
name="title_pref_auto_update_interval"
|
||||||
|
>Интервал автообновления (минут, не менее 15)</string>
|
||||||
|
|
||||||
<string name="title_core_loglevel">Подробность ведения журнала</string>
|
<string name="title_core_loglevel">Подробность ведения журнала</string>
|
||||||
<string name="title_outbound_domain_resolve_method">Предопределение исходящего домена</string>
|
<string
|
||||||
|
name="title_outbound_domain_resolve_method"
|
||||||
|
>Предопределение исходящего домена</string>
|
||||||
<string name="title_mode">Режим</string>
|
<string name="title_mode">Режим</string>
|
||||||
<string name="title_mode_help">Нажмите для получения дополнительной информации</string>
|
<string
|
||||||
|
name="title_mode_help"
|
||||||
|
>Нажмите для получения дополнительной информации</string>
|
||||||
<string name="title_language">Язык</string>
|
<string name="title_language">Язык</string>
|
||||||
<string name="title_ui_settings">Настройки интерфейса</string>
|
<string name="title_ui_settings">Настройки интерфейса</string>
|
||||||
<string name="title_pref_ui_mode_night">Тема интерфейса</string>
|
<string name="title_pref_ui_mode_night">Тема интерфейса</string>
|
||||||
<string name="title_pref_dynamic_colors">Динамические цвета (Material You)</string>
|
<string
|
||||||
<string name="summary_pref_dynamic_colors">Использовать цвета обоев (Android 12+). Требует перезапуска.</string>
|
name="title_pref_dynamic_colors"
|
||||||
|
>Динамические цвета (Material You)</string>
|
||||||
|
<string
|
||||||
|
name="summary_pref_dynamic_colors"
|
||||||
|
>Использовать цвета обоев (Android 12+). Требует перезапуска.</string>
|
||||||
<string name="restart_required">Требуется перезапуск приложения</string>
|
<string name="restart_required">Требуется перезапуск приложения</string>
|
||||||
<string name="title_pref_subscriptions_bottom">Подписки снизу</string>
|
<string name="title_pref_subscriptions_bottom">Подписки снизу</string>
|
||||||
<string name="summary_pref_subscriptions_bottom">Переместить вкладки подписок под список серверов</string>
|
<string
|
||||||
|
name="summary_pref_subscriptions_bottom"
|
||||||
|
>Переместить вкладки подписок под список серверов</string>
|
||||||
<string name="title_pref_use_hev_tunnel">Использовать Hev TUN</string>
|
<string name="title_pref_use_hev_tunnel">Использовать Hev TUN</string>
|
||||||
<string name="summary_pref_use_hev_tunnel">Если включено, TUN будет использовать hev-socks5-tunnel; иначе будет использован xray-core</string>
|
<string
|
||||||
<string name="title_pref_hev_tunnel_loglevel">Подробность журнала HevTun</string>
|
name="summary_pref_use_hev_tunnel"
|
||||||
<string name="title_pref_hev_tunnel_rw_timeout">Ожидание чтения/записи HevTun (секунд, по умолчанию TCP,UDP 300,60)</string>
|
>Если включено, TUN будет использовать hev-socks5-tunnel; иначе будет использован xray-core</string>
|
||||||
|
<string
|
||||||
|
name="title_pref_hev_tunnel_loglevel"
|
||||||
|
>Подробность журнала HevTun</string>
|
||||||
|
<string
|
||||||
|
name="title_pref_hev_tunnel_rw_timeout"
|
||||||
|
>Ожидание чтения/записи HevTun (секунд, по умолчанию TCP,UDP 300,60)</string>
|
||||||
|
|
||||||
<string name="title_logcat">Журнал</string>
|
<string name="title_logcat">Журнал</string>
|
||||||
<string name="logcat_copy">Копировать</string>
|
<string name="logcat_copy">Копировать</string>
|
||||||
<string name="logcat_clear">Очистить</string>
|
<string name="logcat_clear">Очистить</string>
|
||||||
<string name="title_service_restart">Перезапуск службы</string>
|
<string name="title_service_restart">Перезапуск службы</string>
|
||||||
<string name="title_del_all_config">Удалить профили</string>
|
<string name="title_del_all_config">Удалить профили</string>
|
||||||
<string name="title_del_duplicate_config">Удалить дубликаты профилей</string>
|
<string
|
||||||
|
name="title_del_duplicate_config"
|
||||||
|
>Удалить дубликаты профилей</string>
|
||||||
<string name="title_del_invalid_config">Удалить нерабочие профили</string>
|
<string name="title_del_invalid_config">Удалить нерабочие профили</string>
|
||||||
<string name="title_export_all">Экспорт профилей в буфер обмена</string>
|
<string name="title_export_all">Экспорт профилей в буфер обмена</string>
|
||||||
<string name="title_sub_setting">Группы</string>
|
<string name="title_sub_setting">Группы</string>
|
||||||
@@ -291,26 +465,44 @@
|
|||||||
<string name="sub_setting_filter">Название фильтра</string>
|
<string name="sub_setting_filter">Название фильтра</string>
|
||||||
<string name="sub_setting_enable">Использовать обновление</string>
|
<string name="sub_setting_enable">Использовать обновление</string>
|
||||||
<string name="sub_auto_update">Использовать автообновление</string>
|
<string name="sub_auto_update">Использовать автообновление</string>
|
||||||
<string name="sub_allow_insecure_url">Разрешать незащищённые HTTP-адреса</string>
|
<string
|
||||||
|
name="sub_allow_insecure_url"
|
||||||
|
>Разрешать незащищённые HTTP-адреса</string>
|
||||||
<string name="sub_setting_pre_profile">Предыдущий профиль прокси</string>
|
<string name="sub_setting_pre_profile">Предыдущий профиль прокси</string>
|
||||||
<string name="sub_setting_next_profile">Следующий профиль прокси</string>
|
<string name="sub_setting_next_profile">Следующий профиль прокси</string>
|
||||||
<string name="sub_setting_pre_profile_tip">Профиль должен быть уникальным</string>
|
<string
|
||||||
|
name="sub_setting_pre_profile_tip"
|
||||||
|
>Профиль должен быть уникальным</string>
|
||||||
<string name="title_sub_update">Обновить подписку</string>
|
<string name="title_sub_update">Обновить подписку</string>
|
||||||
<string name="title_ping_all_server">Проверить профили</string>
|
<string name="title_ping_all_server">Проверить профили</string>
|
||||||
<string name="title_real_ping_all_server">Проверить задержку профилей</string>
|
<string
|
||||||
|
name="title_real_ping_all_server"
|
||||||
|
>Проверить задержку профилей</string>
|
||||||
<string name="title_user_asset_setting">Файлы ресурсов</string>
|
<string name="title_user_asset_setting">Файлы ресурсов</string>
|
||||||
<string name="title_sort_by_test_results">Сортировать по результатам теста</string>
|
<string
|
||||||
|
name="title_sort_by_test_results"
|
||||||
|
>Сортировать по результатам теста</string>
|
||||||
<string name="title_filter_config">Фильтр профилей</string>
|
<string name="title_filter_config">Фильтр профилей</string>
|
||||||
<string name="filter_config_all">Все</string>
|
<string name="filter_config_all">Все</string>
|
||||||
<string name="title_del_duplicate_config_count">Удалено дубликатов профилей: %d</string>
|
<string
|
||||||
|
name="title_del_duplicate_config_count"
|
||||||
|
>Удалено дубликатов профилей: %d</string>
|
||||||
<string name="title_del_config_count">Удалено профилей: %d</string>
|
<string name="title_del_config_count">Удалено профилей: %d</string>
|
||||||
<string name="title_import_config_count">Импортировано профилей: %d</string>
|
<string name="title_import_config_count">Импортировано профилей: %d</string>
|
||||||
<string name="title_export_config_count">Экспортировано профилей: %d</string>
|
<string
|
||||||
|
name="title_export_config_count"
|
||||||
|
>Экспортировано профилей: %d</string>
|
||||||
<string name="title_update_config_count">Обновлено профилей: %d</string>
|
<string name="title_update_config_count">Обновлено профилей: %d</string>
|
||||||
<string name="title_updating">Обновление…</string>
|
<string name="title_updating">Обновление…</string>
|
||||||
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
|
<string
|
||||||
<string name="title_update_subscription_no_subscription">Нет подписок</string>
|
name="title_update_subscription_result"
|
||||||
<string name="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string>
|
>Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
|
||||||
|
<string
|
||||||
|
name="title_update_subscription_no_subscription"
|
||||||
|
>Нет подписок</string>
|
||||||
|
<string
|
||||||
|
name="toast_server_not_found_in_group"
|
||||||
|
>Выбранный профиль не найден в текущей группе</string>
|
||||||
<string name="toast_fragment_not_available">Фрагмент недоступен</string>
|
<string name="toast_fragment_not_available">Фрагмент недоступен</string>
|
||||||
<string name="title_locate_selected_config">Найти выбранный профиль</string>
|
<string name="title_locate_selected_config">Найти выбранный профиль</string>
|
||||||
|
|
||||||
@@ -320,17 +512,33 @@
|
|||||||
<!-- RoutingSettingActivity -->
|
<!-- RoutingSettingActivity -->
|
||||||
<string name="routing_settings_domain_strategy">Доменная стратегия</string>
|
<string name="routing_settings_domain_strategy">Доменная стратегия</string>
|
||||||
<string name="routing_settings_title">Маршрутизация</string>
|
<string name="routing_settings_title">Маршрутизация</string>
|
||||||
<string name="routing_settings_tips">Введите требуемые домены/IP через запятую</string>
|
<string
|
||||||
|
name="routing_settings_tips"
|
||||||
|
>Введите требуемые домены/IP через запятую</string>
|
||||||
<string name="routing_settings_save">Сохранить</string>
|
<string name="routing_settings_save">Сохранить</string>
|
||||||
<string name="routing_settings_delete">Очистить</string>
|
<string name="routing_settings_delete">Очистить</string>
|
||||||
<string name="routing_settings_rule_title">Настройка правил маршрутизации</string>
|
<string
|
||||||
|
name="routing_settings_rule_title"
|
||||||
|
>Настройка правил маршрутизации</string>
|
||||||
<string name="routing_settings_add_rule">Добавить правило</string>
|
<string name="routing_settings_add_rule">Добавить правило</string>
|
||||||
<string name="routing_settings_import_predefined_rulesets">Импорт набора правил</string>
|
<string
|
||||||
<string name="routing_settings_import_rulesets_tip">Существующие правила будут удалены. Продолжить?</string>
|
name="routing_settings_import_predefined_rulesets"
|
||||||
<string name="routing_settings_import_rulesets_from_clipboard">Импорт правил из буфера обмена</string>
|
>Импорт набора правил</string>
|
||||||
<string name="routing_settings_import_rulesets_from_qrcode">Импорт правил из QR-кода</string>
|
<string
|
||||||
<string name="routing_settings_export_rulesets_to_clipboard">Экспорт правил в буфер обмена</string>
|
name="routing_settings_import_rulesets_tip"
|
||||||
<string name="routing_settings_locked">Постоянное (сохранится при импорте правил)</string>
|
>Существующие правила будут удалены. Продолжить?</string>
|
||||||
|
<string
|
||||||
|
name="routing_settings_import_rulesets_from_clipboard"
|
||||||
|
>Импорт правил из буфера обмена</string>
|
||||||
|
<string
|
||||||
|
name="routing_settings_import_rulesets_from_qrcode"
|
||||||
|
>Импорт правил из QR-кода</string>
|
||||||
|
<string
|
||||||
|
name="routing_settings_export_rulesets_to_clipboard"
|
||||||
|
>Экспорт правил в буфер обмена</string>
|
||||||
|
<string
|
||||||
|
name="routing_settings_locked"
|
||||||
|
>Постоянное (сохранится при импорте правил)</string>
|
||||||
<string name="routing_settings_domain">Домен</string>
|
<string name="routing_settings_domain">Домен</string>
|
||||||
<string name="routing_settings_ip">IP</string>
|
<string name="routing_settings_ip">IP</string>
|
||||||
<string name="routing_settings_port">Порт</string>
|
<string name="routing_settings_port">Порт</string>
|
||||||
@@ -343,45 +551,71 @@
|
|||||||
<string name="connection_test_pending">Проверить соединение</string>
|
<string name="connection_test_pending">Проверить соединение</string>
|
||||||
<string name="connection_test_testing">Проверка…</string>
|
<string name="connection_test_testing">Проверка…</string>
|
||||||
<string name="connection_test_testing_count">Проверка профилей (%d)</string>
|
<string name="connection_test_testing_count">Проверка профилей (%d)</string>
|
||||||
<string name="connection_test_available">Успешно: соединение заняло %d мс</string>
|
<string name="connection_test_available">%d мс</string>
|
||||||
<string name="connection_test_error">Сбой проверки интернет-соединения: %s</string>
|
<string
|
||||||
|
name="connection_test_error"
|
||||||
|
>Сбой проверки интернет-соединения: %s</string>
|
||||||
<string name="connection_test_fail">Интернет недоступен</string>
|
<string name="connection_test_fail">Интернет недоступен</string>
|
||||||
<string name="connection_test_error_status_code">Код ошибки: #%d</string>
|
<string name="connection_test_error_status_code">Код ошибки: #%d</string>
|
||||||
<string name="connection_connected">Соединено, нажмите для проверки</string>
|
<string name="connection_connected">нажмите для проверки</string>
|
||||||
<string name="connection_not_connected">Ожидаем действий</string>
|
<string name="connection_not_connected">Ожидаем действий</string>
|
||||||
<string name="connection_updating_profiles">Обновление профилей…</string>
|
<string name="connection_updating_profiles">Обновление профилей…</string>
|
||||||
<string name="connection_runing_task_left">Запущено проверок: %s</string>
|
<string name="connection_runing_task_left">Проверено %s</string>
|
||||||
|
|
||||||
<string name="import_subscription_success">Подписка импортирована</string>
|
<string name="import_subscription_success">Подписка импортирована</string>
|
||||||
<string name="import_subscription_failure">Невозможно импортировать подписку</string>
|
<string
|
||||||
|
name="import_subscription_failure"
|
||||||
|
>Невозможно импортировать подписку</string>
|
||||||
<string name="title_fragment_settings">Настройки фрагментирования</string>
|
<string name="title_fragment_settings">Настройки фрагментирования</string>
|
||||||
<string name="title_pref_fragment_packets">Фрагментирование пакетов</string>
|
<string name="title_pref_fragment_packets">Фрагментирование пакетов</string>
|
||||||
<string name="title_pref_fragment_length">Длина фрагмента (от - до)</string>
|
<string name="title_pref_fragment_length">Длина фрагмента (от - до)</string>
|
||||||
<string name="title_pref_fragment_interval">Интервал фрагментов (от - до)</string>
|
<string
|
||||||
<string name="title_pref_fragment_enabled">Использовать фрагментирование</string>
|
name="title_pref_fragment_interval"
|
||||||
|
>Интервал фрагментов (от - до)</string>
|
||||||
|
<string
|
||||||
|
name="title_pref_fragment_enabled"
|
||||||
|
>Использовать фрагментирование</string>
|
||||||
|
|
||||||
<string name="update_check_for_update">Проверить обновление</string>
|
<string name="update_check_for_update">Проверить обновление</string>
|
||||||
<string name="update_already_latest_version">Установлена последняя версия</string>
|
<string
|
||||||
|
name="update_already_latest_version"
|
||||||
|
>Установлена последняя версия</string>
|
||||||
<string name="update_new_version_found">Найдена новая версия: %s</string>
|
<string name="update_new_version_found">Найдена новая версия: %s</string>
|
||||||
<string name="update_now">Обновить</string>
|
<string name="update_now">Обновить</string>
|
||||||
<string name="update_check_pre_release">Искать предварительный выпуск</string>
|
<string
|
||||||
|
name="update_check_pre_release"
|
||||||
|
>Искать предварительный выпуск</string>
|
||||||
<string name="update_checking_for_update">Проверка обновления…</string>
|
<string name="update_checking_for_update">Проверка обновления…</string>
|
||||||
|
|
||||||
<string name="title_policy_group_type">Тип группы политик</string>
|
<string name="title_policy_group_type">Тип группы политик</string>
|
||||||
<string name="title_policy_group_subscription_id">Из группы подписки</string>
|
<string
|
||||||
<string name="title_policy_group_subscription_filter">Название фильтра</string>
|
name="title_policy_group_subscription_id"
|
||||||
|
>Из группы подписки</string>
|
||||||
|
<string
|
||||||
|
name="title_policy_group_subscription_filter"
|
||||||
|
>Название фильтра</string>
|
||||||
|
|
||||||
<!-- BackupActivity -->
|
<!-- BackupActivity -->
|
||||||
<string name="title_configuration_backup_restore">Резервное копирование</string>
|
<string
|
||||||
<string name="title_configuration_backup">Резервирование конфигурации</string>
|
name="title_configuration_backup_restore"
|
||||||
<string name="title_configuration_restore">Восстановление конфигурации</string>
|
>Резервное копирование</string>
|
||||||
|
<string
|
||||||
|
name="title_configuration_backup"
|
||||||
|
>Резервирование конфигурации</string>
|
||||||
|
<string
|
||||||
|
name="title_configuration_restore"
|
||||||
|
>Восстановление конфигурации</string>
|
||||||
<string name="title_configuration_share">Поделиться конфигурацией</string>
|
<string name="title_configuration_share">Поделиться конфигурацией</string>
|
||||||
<string name="title_webdav_config_setting">Настройки WebDAV</string>
|
<string name="title_webdav_config_setting">Настройки WebDAV</string>
|
||||||
<string name="title_webdav_config_setting_unknown">Необходимо настроить WebDAV</string>
|
<string
|
||||||
|
name="title_webdav_config_setting_unknown"
|
||||||
|
>Необходимо настроить WebDAV</string>
|
||||||
<string name="title_webdav_url">URL сервера</string>
|
<string name="title_webdav_url">URL сервера</string>
|
||||||
<string name="title_webdav_user">Пользователь</string>
|
<string name="title_webdav_user">Пользователь</string>
|
||||||
<string name="title_webdav_pass">Пароль</string>
|
<string name="title_webdav_pass">Пароль</string>
|
||||||
<string name="title_webdav_remote_path">Удалённый путь (необязательно)</string>
|
<string
|
||||||
|
name="title_webdav_remote_path"
|
||||||
|
>Удалённый путь (необязательно)</string>
|
||||||
|
|
||||||
<string-array name="share_method">
|
<string-array name="share_method">
|
||||||
<item>QR-код</item>
|
<item>QR-код</item>
|
||||||
|
|||||||
@@ -356,7 +356,7 @@
|
|||||||
<string name="connection_connected">Connected, tap to check connection</string>
|
<string name="connection_connected">Connected, tap to check connection</string>
|
||||||
<string name="connection_not_connected">Готово к подключению</string>
|
<string name="connection_not_connected">Готово к подключению</string>
|
||||||
<string name="connection_updating_profiles">Updating profiles…</string>
|
<string name="connection_updating_profiles">Updating profiles…</string>
|
||||||
<string name="connection_runing_task_left">Проверено успешно: %s</string>
|
<string name="connection_runing_task_left">Проверено %s</string>
|
||||||
|
|
||||||
<string name="import_subscription_success">Subscription imported Successfully</string>
|
<string name="import_subscription_success">Subscription imported Successfully</string>
|
||||||
<string name="import_subscription_failure">Import subscription failed</string>
|
<string name="import_subscription_failure">Import subscription failed</string>
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
|
|
||||||
<!-- Tab label -->
|
<!-- Tab label -->
|
||||||
<style name="TabLayoutTextStyle" parent="TextAppearance.Material3.LabelLarge">
|
<style name="TabLayoutTextStyle" parent="TextAppearance.Material3.LabelLarge">
|
||||||
<item name="android:textSize">13sp</item>
|
<item name="android:textSize">14sp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">olcNG</string>
|
<string name="app_name" translatable="false">olcng</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user