Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c1824c7598 | |||
| 18eebf999d | |||
| e37d6b7523 | |||
| 8c5375ec62 |
+1
-1
Submodule AndroidLibXrayLite updated: 203f317afd...03a47ef527
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 720
|
versionCode = 721
|
||||||
versionName = "2.1.0"
|
versionName = "2.1.1"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
|||||||
@@ -88,6 +88,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.PerAppProxyActivity"
|
android:name=".ui.PerAppProxyActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.AppPickerActivity"
|
||||||
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ScannerActivity"
|
android:name=".ui.ScannerActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.databinding.ActivityAppPickerBinding
|
||||||
|
import com.v2ray.ang.dto.AppInfo
|
||||||
|
import com.v2ray.ang.util.AppManagerUtil
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.text.Collator
|
||||||
|
|
||||||
|
class AppPickerActivity : BaseActivity() {
|
||||||
|
companion object {
|
||||||
|
private const val EXTRA_SELECTED_PACKAGES = "selected_packages"
|
||||||
|
private const val EXTRA_PICKER_TITLE = "picker_title"
|
||||||
|
|
||||||
|
fun createIntent(
|
||||||
|
context: Context,
|
||||||
|
selectedPackages: Collection<String> = emptyList(),
|
||||||
|
title: String? = null
|
||||||
|
): Intent = Intent(context, AppPickerActivity::class.java).apply {
|
||||||
|
putStringArrayListExtra(EXTRA_SELECTED_PACKAGES, ArrayList(selectedPackages))
|
||||||
|
title?.let { putExtra(EXTRA_PICKER_TITLE, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSelectedPackages(intent: Intent?): List<String> {
|
||||||
|
return intent?.getStringArrayListExtra(EXTRA_SELECTED_PACKAGES).orEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val binding by lazy { ActivityAppPickerBinding.inflate(layoutInflater) }
|
||||||
|
private val initialSelectedPackages by lazy {
|
||||||
|
intent.getStringArrayListExtra(EXTRA_SELECTED_PACKAGES).orEmpty()
|
||||||
|
}
|
||||||
|
private val selectedPackages = LinkedHashSet<String>()
|
||||||
|
private var appsAll: List<AppInfo> = emptyList()
|
||||||
|
private val adapter = AppSelectorAdapter(selectedPackages)
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = resolveScreenTitle())
|
||||||
|
|
||||||
|
selectedPackages.addAll(initialSelectedPackages)
|
||||||
|
setupRecyclerView()
|
||||||
|
loadApps()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.menu_app_picker, menu)
|
||||||
|
|
||||||
|
val searchItem = menu.findItem(R.id.search_view)
|
||||||
|
if (searchItem != null) {
|
||||||
|
val searchView = searchItem.actionView as SearchView
|
||||||
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
filterApps(newText.orEmpty())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||||
|
R.id.select_all -> {
|
||||||
|
selectAllVisible()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.invert_selection -> {
|
||||||
|
invertVisibleSelection()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
setResult(
|
||||||
|
RESULT_OK,
|
||||||
|
Intent().apply {
|
||||||
|
putStringArrayListExtra(EXTRA_SELECTED_PACKAGES, getSelectedPackages())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
super.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
addCustomDividerToRecyclerView(binding.recyclerView, this, R.drawable.custom_divider)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadApps() {
|
||||||
|
showLoading()
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val apps = withContext(Dispatchers.IO) {
|
||||||
|
val appsList = AppManagerUtil.loadNetworkAppList(this@AppPickerActivity)
|
||||||
|
sortApps(appsList)
|
||||||
|
}
|
||||||
|
|
||||||
|
appsAll = apps
|
||||||
|
updateDisplayedApps(apps)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e("AppPickerActivity", "Failed to load app list", e)
|
||||||
|
} finally {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterApps(content: String) {
|
||||||
|
val key = content.uppercase()
|
||||||
|
val filteredApps = appsAll.filter { app ->
|
||||||
|
key.isEmpty() || matchesSearch(app, key)
|
||||||
|
}
|
||||||
|
updateDisplayedApps(filteredApps)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortApps(apps: List<AppInfo>): List<AppInfo> {
|
||||||
|
val collator = Collator.getInstance()
|
||||||
|
return apps.sortedWith { p1, p2 ->
|
||||||
|
val p1Selected = selectedPackages.contains(p1.packageName)
|
||||||
|
val p2Selected = selectedPackages.contains(p2.packageName)
|
||||||
|
when {
|
||||||
|
p1Selected && !p2Selected -> -1
|
||||||
|
!p1Selected && p2Selected -> 1
|
||||||
|
p1.isSystemApp && !p2.isSystemApp -> 1
|
||||||
|
!p1.isSystemApp && p2.isSystemApp -> -1
|
||||||
|
else -> collator.compare(p1.appName, p2.appName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun matchesSearch(app: AppInfo, keyword: String): Boolean {
|
||||||
|
return app.appName.uppercase().contains(keyword) || app.packageName.uppercase().contains(keyword)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDisplayedApps(apps: List<AppInfo>) {
|
||||||
|
adapter.submitList(apps)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectAllVisible() {
|
||||||
|
adapter.apps.forEach { app -> selectedPackages.add(app.packageName) }
|
||||||
|
adapter.refreshSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invertVisibleSelection() {
|
||||||
|
adapter.apps.forEach { app ->
|
||||||
|
if (selectedPackages.contains(app.packageName)) {
|
||||||
|
selectedPackages.remove(app.packageName)
|
||||||
|
} else {
|
||||||
|
selectedPackages.add(app.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.refreshSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSelectedPackages(): ArrayList<String> {
|
||||||
|
return ArrayList(selectedPackages.sorted())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveScreenTitle(): String {
|
||||||
|
return intent.getStringExtra(EXTRA_PICKER_TITLE) ?: getString(R.string.per_app_proxy_settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
|
||||||
|
import com.v2ray.ang.dto.AppInfo
|
||||||
|
|
||||||
|
class AppSelectorAdapter(
|
||||||
|
private val selectedPackages: MutableSet<String>
|
||||||
|
) : RecyclerView.Adapter<AppSelectorAdapter.AppViewHolder>() {
|
||||||
|
|
||||||
|
var apps: List<AppInfo> = emptyList()
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
|
||||||
|
val binding = ItemRecyclerBypassListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
return AppViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = apps.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
|
||||||
|
holder.bind(apps[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitList(newApps: List<AppInfo>) {
|
||||||
|
apps = newApps
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshSelection() {
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class AppViewHolder(private val binding: ItemRecyclerBypassListBinding) : RecyclerView.ViewHolder(binding.root),
|
||||||
|
View.OnClickListener {
|
||||||
|
private lateinit var appInfo: AppInfo
|
||||||
|
|
||||||
|
fun bind(item: AppInfo) {
|
||||||
|
appInfo = item
|
||||||
|
binding.icon.setImageDrawable(item.appIcon)
|
||||||
|
binding.name.text = if (item.isSystemApp) {
|
||||||
|
String.format("** %s", item.appName)
|
||||||
|
} else {
|
||||||
|
item.appName
|
||||||
|
}
|
||||||
|
binding.packageName.text = item.packageName
|
||||||
|
binding.checkBox.isChecked = selectedPackages.contains(item.packageName)
|
||||||
|
itemView.setOnClickListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View?) {
|
||||||
|
val packageName = appInfo.packageName
|
||||||
|
if (selectedPackages.contains(packageName)) {
|
||||||
|
selectedPackages.remove(packageName)
|
||||||
|
} else {
|
||||||
|
selectedPackages.add(packageName)
|
||||||
|
}
|
||||||
|
binding.checkBox.isChecked = selectedPackages.contains(packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,6 +4,7 @@ import android.os.Bundle
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
|
import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
|
||||||
@@ -23,12 +24,19 @@ import kotlinx.coroutines.launch
|
|||||||
class RoutingEditActivity : BaseActivity() {
|
class RoutingEditActivity : BaseActivity() {
|
||||||
private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) }
|
private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) }
|
||||||
private val position by lazy { intent.getIntExtra("position", -1) }
|
private val position by lazy { intent.getIntExtra("position", -1) }
|
||||||
|
private val processPickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == RESULT_OK) {
|
||||||
|
val selectedPackages = AppPickerActivity.getSelectedPackages(result.data)
|
||||||
|
binding.etProcess.text = Utils.getEditable(selectedPackages.joinToString(","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_rule_title))
|
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_rule_title))
|
||||||
|
|
||||||
setupOutboundTagInput()
|
setupOutboundTagInput()
|
||||||
|
setupProcessPicker()
|
||||||
|
|
||||||
val rulesetItem = SettingsManager.getRoutingRuleset(position)
|
val rulesetItem = SettingsManager.getRoutingRuleset(position)
|
||||||
if (rulesetItem != null) {
|
if (rulesetItem != null) {
|
||||||
@@ -39,9 +47,31 @@ class RoutingEditActivity : BaseActivity() {
|
|||||||
|
|
||||||
SettingsManager.canUseProcessRouting().let { canUse ->
|
SettingsManager.canUseProcessRouting().let { canUse ->
|
||||||
binding.etProcess.isEnabled = canUse
|
binding.etProcess.isEnabled = canUse
|
||||||
|
binding.btnProcessPicker.isEnabled = canUse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupProcessPicker() {
|
||||||
|
binding.btnProcessPicker.setOnClickListener {
|
||||||
|
processPickerLauncher.launch(
|
||||||
|
AppPickerActivity.createIntent(
|
||||||
|
context = this,
|
||||||
|
selectedPackages = getSelectedProcessPackages(),
|
||||||
|
title = getString(R.string.routing_settings_process)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSelectedProcessPackages(): List<String> {
|
||||||
|
return binding.etProcess.text
|
||||||
|
.toString()
|
||||||
|
.split(",")
|
||||||
|
.map { it.trim() }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
.distinct()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the AutoCompleteTextView for outbound tag:
|
* Sets up the AutoCompleteTextView for outbound tag:
|
||||||
* suggestions = built-in tags (proxy/direct/block) + all existing profile remarks.
|
* suggestions = built-in tags (proxy/direct/block) + all existing profile remarks.
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||||
|
|
||||||
|
|
||||||
@@ -114,13 +114,34 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/routing_settings_process" />
|
android:text="@string/routing_settings_process" />
|
||||||
|
|
||||||
<EditText
|
<RelativeLayout
|
||||||
android:id="@+id/et_process"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/routing_settings_tips"
|
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||||
android:inputType="textMultiLine"
|
android:layout_marginBottom="@dimen/padding_spacing_dp8">
|
||||||
android:maxLines="10" />
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_process"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/routing_settings_tips"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:maxLines="10"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:paddingStart="0dp"
|
||||||
|
android:paddingEnd="48dp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_process_picker"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/routing_settings_process"
|
||||||
|
android:src="@drawable/ic_per_apps_24dp" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/search_view"
|
||||||
|
android:icon="@drawable/ic_description_24dp"
|
||||||
|
android:title="@string/menu_item_search"
|
||||||
|
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/select_all"
|
||||||
|
android:icon="@drawable/ic_select_all_24dp"
|
||||||
|
android:title="@string/menu_item_select_all"
|
||||||
|
app:showAsAction="withText" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/invert_selection"
|
||||||
|
android:icon="@drawable/ic_select_all_24dp"
|
||||||
|
android:title="@string/menu_item_invert_selection"
|
||||||
|
app:showAsAction="withText" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
<string name="summary_pref_ipv6_enabled">Добавить IPv6-адрес и маршруты в VPN-интерфейс</string>
|
<string name="summary_pref_ipv6_enabled">Добавить IPv6-адрес и маршруты в VPN-интерфейс</string>
|
||||||
|
|
||||||
<string name="title_pref_prefer_ipv6">Предпочитать IPv6</string>
|
<string name="title_pref_prefer_ipv6">Предпочитать IPv6</string>
|
||||||
<string name="summary_pref_prefer_ipv6">Предпочитать IPv6-адреса при разрешении доменных имён</string>
|
<string name="summary_pref_prefer_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>
|
||||||
@@ -198,8 +198,10 @@
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -221,7 +223,7 @@
|
|||||||
|
|
||||||
<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>
|
||||||
<string name="title_pref_dynamic_socks_port">Динамически менять порт</string>
|
<string name="title_pref_dynamic_socks_port">Динамически менять порт локального прокси</string>
|
||||||
<string name="summary_pref_dynamic_socks_port">Использовать случайный порт локального прокси при каждом создании соединения</string>
|
<string name="summary_pref_dynamic_socks_port">Использовать случайный порт локального прокси при каждом создании соединения</string>
|
||||||
<string name="title_pref_socks_username">Пользователь локального прокси (необязательно)</string>
|
<string name="title_pref_socks_username">Пользователь локального прокси (необязательно)</string>
|
||||||
<string name="summary_pref_socks_username">Имя пользователя</string>
|
<string name="summary_pref_socks_username">Имя пользователя</string>
|
||||||
@@ -245,6 +247,7 @@
|
|||||||
|
|
||||||
<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 name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string>
|
||||||
@@ -306,7 +309,7 @@
|
|||||||
<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_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
|
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибки: %3$d, пропущено: %4$d)</string>
|
||||||
<string name="title_update_subscription_no_subscription">Нет подписок</string>
|
<string name="title_update_subscription_no_subscription">Нет подписок</string>
|
||||||
<string name="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string>
|
<string name="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string>
|
||||||
<string name="toast_fragment_not_available">Фрагмент недоступен</string>
|
<string name="toast_fragment_not_available">Фрагмент недоступен</string>
|
||||||
@@ -318,7 +321,7 @@
|
|||||||
<!-- 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">Разделяйте запятой (,). Укажите только одно: domain, ip или process</string>
|
<string name="routing_settings_tips">Через запятую (,)\nТолько что-то одно: домен, 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>
|
||||||
@@ -331,6 +334,7 @@
|
|||||||
<string name="routing_settings_locked">Постоянное (сохранится при импорте правил)</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_process">Процесс (название пакета — поддерживается только при использовании Xray TUN, включённой функции routeOnly и ОС Android 10+)</string>
|
||||||
<string name="routing_settings_port">Порт</string>
|
<string name="routing_settings_port">Порт</string>
|
||||||
<string name="routing_settings_protocol">Протокол</string>
|
<string name="routing_settings_protocol">Протокол</string>
|
||||||
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
|
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
|
||||||
@@ -353,8 +357,8 @@
|
|||||||
<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 name="title_pref_fragment_interval">Интервал фрагментов (от — до)</string>
|
||||||
<string name="title_pref_fragment_enabled">Использовать фрагментирование</string>
|
<string name="title_pref_fragment_enabled">Использовать фрагментирование</string>
|
||||||
|
|
||||||
<string name="update_check_for_update">Проверить обновление</string>
|
<string name="update_check_for_update">Проверить обновление</string>
|
||||||
@@ -429,6 +433,7 @@
|
|||||||
<item>Определять и добавлять к узлам DNS</item>
|
<item>Определять и добавлять к узлам DNS</item>
|
||||||
<item>Определять и заменять домен</item>
|
<item>Определять и заменять домен</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="policy_group_type">
|
<string-array name="policy_group_type">
|
||||||
<item>Наименьшая задержка</item>
|
<item>Наименьшая задержка</item>
|
||||||
<item>Наименьшая нагрузка</item>
|
<item>Наименьшая нагрузка</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user