Compare commits

...

28 Commits
1.8.3 ... 1.8.6

Author SHA1 Message Date
2dust
99307ab8f0 up 1.8.6 2023-07-30 11:05:31 +08:00
2dust
1ec23a7b39 Merge pull request #2302 from dep4/fix/package-name
AppConfig: use current package name to receive broadcasts
2023-05-23 06:15:07 +08:00
Bruce Mills
685f9e220c AppConfig: use current package name to receive broadcasts 2023-05-21 15:54:25 -04:00
2dust
e77b7eb52e Merge pull request #2290 from CUMOON/master
Correction of Persian translation
2023-05-21 08:35:38 +08:00
2dust
29014704e0 Merge pull request #2288 from hadi-norouzi/feature/tv
feature: add leanback for android tv compatiblity
2023-05-21 08:35:13 +08:00
Moon
b3570d9c0b Correction of Persian translation 2023-05-17 12:49:41 +03:30
Hadi Norouzi
6e427cee82 feature: add leanback for android tv compatiblity 2023-05-16 11:33:29 +03:30
2dust
0a1b6d00d9 Merge pull request #2284 from solokot/master
RU: Improvements for google translation
2023-05-15 20:24:19 +08:00
solokot
00ffe66f36 RU: Improvements for google translation 2023-05-15 09:13:11 +03:00
2dust
63661fbdaa Merge pull request #2273 from hadi-norouzi/master
[Feature] separate package names for pre release and dev builds
2023-05-13 19:25:53 +08:00
Hadi Norouzi
4d4a4543c5 feature: import subscription from v2rayng://install-sub deeplink 2023-05-11 20:42:15 +03:30
Hadi Norouzi
2349805968 feature: separate package names for pre release and dev builds 2023-05-10 20:16:11 +03:30
2dust
bdbd0b154e up 1.8.5 2023-05-07 15:35:42 +08:00
2dust
653137ec58 bug fixes 2023-05-07 15:34:11 +08:00
2dust
39f65850be Merge pull request #2258 from Amir-yazdanmanesh/master
Fix persian translation
2023-05-07 15:19:33 +08:00
2dust
2d8db97417 Merge branch 'master' into master 2023-05-07 15:19:25 +08:00
2dust
17a6f80798 Merge pull request #2255 from hadi-norouzi/feature/share_sub
Feature/share sub
2023-05-07 15:18:28 +08:00
2dust
381fe859ff Merge pull request #2254 from hadi-norouzi/master
Fix status bar and navigation bar, remove FooterView TouchHelper
2023-05-07 15:14:04 +08:00
am.yazdanmanesh
e2bdf17b82 Fix persian translation 2023-05-05 21:54:18 +03:30
Hadi Norouzi
d20afc6801 Share subscription implemented 2023-05-04 18:03:50 +03:30
Hadi Norouzi
54046a27e6 Fix fa strings and test layout direction 2023-05-04 17:04:35 +03:30
Hadi Norouzi
36c000d18a Fix status bar and navigation bar, remove FooterView TouchHelper 2023-05-04 16:34:41 +03:30
2dust
2f3c2cf4d5 up 1.8.4 2023-04-29 20:46:28 +08:00
2dust
892358d5d8 Merge pull request #2227 from AlirezaIvaz/master
Add themed icon support
2023-04-27 09:40:08 +08:00
Alireza Ivaz
6dced903cd Add themed icon support 2023-04-26 17:05:11 +03:30
2dust
f8a98a426e Merge pull request #2213 from solokot/master
Updated Russian translation
2023-04-22 14:40:57 +08:00
solokot
011506e99f Updated Russian translation 2023-04-21 21:33:22 +03:00
2dust
966151d3fe bug fixes 2023-04-19 19:45:11 +08:00
29 changed files with 490 additions and 153 deletions

View File

@@ -17,8 +17,8 @@ android {
minSdkVersion 21
targetSdkVersion Integer.parseInt("$targetSdkVer")
multiDexEnabled true
versionCode 514
versionName "1.8.3"
versionCode 518
versionName "1.8.6"
}
buildTypes {
@@ -36,6 +36,21 @@ android {
}
}
// flavorDimensions "versions"
//
// productFlavors {
// dev {
// applicationIdSuffix = ".dev"
// versionNameSuffix = "-dev"
// }
// pre_release {
// applicationIdSuffix = ".pre"
// versionNameSuffix = "-pre-release"
// }
// prod {
// }
// }
sourceSets {
main {
jniLibs.srcDirs = ['libs']
@@ -72,6 +87,7 @@ android {
buildFeatures {
viewBinding true
buildConfig true
}
namespace 'com.v2ray.ang'
testNamespace 'com.v2ray.angTest'
@@ -85,16 +101,16 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.fragment:fragment-ktx:1.5.6'
implementation 'androidx.fragment:fragment-ktx:1.5.7'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
// Androidx ktx
implementation 'androidx.activity:activity-ktx:1.7.0'
implementation 'androidx.activity:activity-ktx:1.7.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'

View File

@@ -1,13 +0,0 @@
package com.v2ray.ang;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="app_name" type="string">v2rayNG (DEV)</item>
</resources>

View File

@@ -11,6 +11,8 @@
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
@@ -31,6 +33,7 @@
android:name=".AngApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:banner="@mipmap/ic_banner"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppThemeDayNight"
@@ -45,6 +48,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
@@ -109,8 +113,9 @@
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="v2rayng"
android:host="install-config" />
<data android:scheme="v2rayng"/>
<data android:host="install-config"/>
<data android:host="install-sub"/>
</intent-filter>
</activity>

View File

@@ -5,7 +5,7 @@ package com.v2ray.ang
* App Config Const
*/
object AppConfig {
const val ANG_PACKAGE = "com.v2ray.ang"
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
const val DIR_ASSETS = "assets"
// legacy

View File

@@ -558,8 +558,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
* read content from uri
*/
private fun readContentFromUri(uri: Uri) {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
RxPermissions(this)
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
.request(permission)
.subscribe {
if (it) {
try {
@@ -608,13 +613,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
// }
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) {
moveTaskToBack(false)
return true
}
return super.onKeyDown(keyCode, event)
}
fun showCircle() {
binding.fabProgressCircle.show()
}

View File

@@ -228,7 +228,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
BaseViewHolder(itemMainBinding.root), ItemTouchHelperViewHolder
class FooterViewHolder(val itemFooterBinding: ItemRecyclerFooterBinding) :
BaseViewHolder(itemFooterBinding.root), ItemTouchHelperViewHolder
BaseViewHolder(itemFooterBinding.root)
override fun onItemDismiss(position: Int) {
val guid = mActivity.mainViewModel.serversCache.getOrNull(position)?.guid ?: return

View File

@@ -2,20 +2,34 @@ package com.v2ray.ang.ui
import android.content.Intent
import android.graphics.Color
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ItemQrcodeBinding
import com.v2ray.ang.databinding.ItemRecyclerSubSettingBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.Utils
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>() {
class SubSettingRecyclerAdapter(val activity: SubSettingActivity) :
RecyclerView.Adapter<SubSettingRecyclerAdapter.MainViewHolder>() {
private var mActivity: SubSettingActivity = activity
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
private val share_method: Array<out String> by lazy {
mActivity.resources.getStringArray(R.array.share_sub_method)
}
override fun getItemCount() = mActivity.subscriptions.size
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
@@ -31,7 +45,8 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
holder.itemSubSettingBinding.layoutEdit.setOnClickListener {
mActivity.startActivity(Intent(mActivity, SubEditActivity::class.java)
mActivity.startActivity(
Intent(mActivity, SubEditActivity::class.java)
.putExtra("subId", subId)
)
}
@@ -40,11 +55,51 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
subStorage?.encode(subId, Gson().toJson(subItem))
notifyItemChanged(position)
}
if (TextUtils.isEmpty(subItem.url)) {
holder.itemSubSettingBinding.layoutShare.visibility = View.INVISIBLE
} else {
holder.itemSubSettingBinding.layoutShare.setOnClickListener {
AlertDialog.Builder(mActivity)
.setItems(share_method.asList().toTypedArray()) { _, i ->
try {
when (i) {
0 -> {
val ivBinding =
ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
ivBinding.ivQcode.setImageBitmap(
QRCodeDecoder.createQRCode(
subItem.url
)
)
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
}
1 -> {
Utils.setClipboard(mActivity, subItem.url)
}
else -> mActivity.toast("else")
}
} catch (e: Exception) {
e.printStackTrace()
}
}.show()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
return MainViewHolder(ItemRecyclerSubSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false))
return MainViewHolder(
ItemRecyclerSubSettingBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) : RecyclerView.ViewHolder(itemSubSettingBinding.root)
class MainViewHolder(val itemSubSettingBinding: ItemRecyclerSubSettingBinding) :
RecyclerView.ViewHolder(itemSubSettingBinding.root)
}

View File

@@ -3,10 +3,12 @@ package com.v2ray.ang.ui
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager
import java.net.URLDecoder
class UrlSchemeActivity : BaseActivity() {
private lateinit var binding: ActivityLogcatBinding
@@ -17,34 +19,68 @@ class UrlSchemeActivity : BaseActivity() {
val view = binding.root
setContentView(view)
var shareUrl: String = ""
try {
intent?.apply {
when (action) {
Intent.ACTION_SEND -> {
if ("text/plain" == type) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
shareUrl = it
intent.apply {
if (action == Intent.ACTION_SEND) {
if ("text/plain" == type) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
val uri = Uri.parse(it)
if (uri.scheme?.startsWith(AppConfig.HTTPS_PROTOCOL) == true || uri.scheme?.startsWith(
AppConfig.HTTP_PROTOCOL
) == true
) {
val name = uri.getQueryParameter("name") ?: "Subscription"
importSubscription(it, name)
} else {
importConfig(it)
}
}
}
Intent.ACTION_VIEW -> {
val uri: Uri? = intent.data
shareUrl = uri?.getQueryParameter("url")!!
} else if (action == Intent.ACTION_VIEW) {
when (data?.host) {
"install-config" -> {
val uri: Uri? = intent.data
val shareUrl: String = uri?.getQueryParameter("url")!!
toast(shareUrl)
importConfig(shareUrl)
}
"install-sub" -> {
val uri: Uri? = intent.data
val url = uri?.getQueryParameter("url")!!
val name = uri.getQueryParameter("name") ?: "Subscription"
importSubscription(url, name)
}
else -> {
toast(R.string.toast_failure)
}
}
}
}
toast(shareUrl)
val count = AngConfigManager.importBatchConfig(shareUrl, "", false)
if (count > 0) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
startActivity(Intent(this, MainActivity::class.java))
finish()
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun importSubscription(url: String, name: String) {
val decodedUrl = URLDecoder.decode(url, "UTF-8")
val check = AngConfigManager.importSubscription(name, decodedUrl)
if (check) toast(R.string.import_subscription_success) else toast(R.string.import_subscription_failure)
}
private fun importConfig(shareUrl: String) {
val count = AngConfigManager.importBatchConfig(shareUrl, "", false)
if (count > 0) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
}
}

View File

@@ -4,6 +4,7 @@ import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.OpenableColumns
import android.util.Log
@@ -75,7 +76,14 @@ class UserAssetActivity : BaseActivity() {
}
private fun showFileChooser() {
RxPermissions(this).request(Manifest.permission.READ_EXTERNAL_STORAGE).subscribe {
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
RxPermissions(this)
.request(permission)
.subscribe {
if (it) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
@@ -91,7 +99,8 @@ class UserAssetActivity : BaseActivity() {
} catch (ex: android.content.ActivityNotFoundException) {
toast(R.string.toast_require_file_manager)
}
}
} else
toast(R.string.toast_permission_denied)
}
}

View File

@@ -20,11 +20,27 @@ import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
import java.net.URI
import java.util.*
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.toast
object AngConfigManager {
private val mainStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
private val serverRawStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
private val settingsStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE) }
private val mainStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_MAIN,
MMKV.MULTI_PROCESS_MODE
)
}
private val serverRawStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SERVER_RAW,
MMKV.MULTI_PROCESS_MODE
)
}
private val settingsStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SETTING,
MMKV.MULTI_PROCESS_MODE
)
}
private val subStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_SUB, MMKV.MULTI_PROCESS_MODE) }
/**
@@ -82,8 +98,14 @@ object AngConfigManager {
).forEach { key ->
settingsStorage?.encode(key, sharedPreferences.getBoolean(key, false))
}
settingsStorage?.encode(AppConfig.PREF_SNIFFING_ENABLED, sharedPreferences.getBoolean(AppConfig.PREF_SNIFFING_ENABLED, true))
settingsStorage?.encode(AppConfig.PREF_PER_APP_PROXY_SET, sharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, setOf()))
settingsStorage?.encode(
AppConfig.PREF_SNIFFING_ENABLED,
sharedPreferences.getBoolean(AppConfig.PREF_SNIFFING_ENABLED, true)
)
settingsStorage?.encode(
AppConfig.PREF_PER_APP_PROXY_SET,
sharedPreferences.getStringSet(AppConfig.PREF_PER_APP_PROXY_SET, setOf())
)
}
private fun migrateVmessBean(angConfig: AngConfig, sharedPreferences: SharedPreferences) {
@@ -125,7 +147,8 @@ object AngConfigManager {
if (TextUtils.isEmpty(vmessBean.security) && TextUtils.isEmpty(vmessBean.id)) {
server.users = null
} else {
val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
val socksUsersBean =
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = vmessBean.security
socksUsersBean.pass = vmessBean.id
server.users = listOf(socksUsersBean)
@@ -135,17 +158,27 @@ object AngConfigManager {
}
}
config.outboundBean?.streamSettings?.let { streamSetting ->
val sni = streamSetting.populateTransportSettings(vmessBean.network, vmessBean.headerType,
vmessBean.requestHost, vmessBean.path, vmessBean.path, vmessBean.requestHost, vmessBean.path,
vmessBean.headerType, vmessBean.path)
val sni = streamSetting.populateTransportSettings(
vmessBean.network,
vmessBean.headerType,
vmessBean.requestHost,
vmessBean.path,
vmessBean.path,
vmessBean.requestHost,
vmessBean.path,
vmessBean.headerType,
vmessBean.path
)
val allowInsecure = if (vmessBean.allowInsecure.isBlank()) {
settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
} else {
vmessBean.allowInsecure.toBoolean()
}
var fingerprint = streamSetting.tlsSettings?.fingerprint
streamSetting.populateTlsSettings(vmessBean.streamSecurity, allowInsecure,
vmessBean.sni.ifBlank { sni }, fingerprint, null, null, null, null)
streamSetting.populateTlsSettings(
vmessBean.streamSecurity, allowInsecure,
vmessBean.sni.ifBlank { sni }, fingerprint, null, null, null, null
)
}
}
val key = MmkvManager.encodeServerConfig(vmessBean.guid, config)
@@ -168,14 +201,21 @@ object AngConfigManager {
/**
* import config form qrcode or...
*/
private fun importConfig(str: String?, subid: String, removedSelectedServer: ServerConfig?): Int {
private fun importConfig(
str: String?,
subid: String,
removedSelectedServer: ServerConfig?
): Int {
try {
if (str == null || TextUtils.isEmpty(str)) {
return R.string.toast_none_data
}
//maybe sub
if (TextUtils.isEmpty(subid) && (str.startsWith(HTTP_PROTOCOL) || str.startsWith(HTTPS_PROTOCOL))) {
if (TextUtils.isEmpty(subid) && (str.startsWith(HTTP_PROTOCOL) || str.startsWith(
HTTPS_PROTOCOL
))
) {
MmkvManager.importUrlAsSubscription(str)
return 0
}
@@ -201,9 +241,9 @@ object AngConfigManager {
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
if (TextUtils.isEmpty(vmessQRCode.add)
|| TextUtils.isEmpty(vmessQRCode.port)
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
|| TextUtils.isEmpty(vmessQRCode.port)
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
return R.string.toast_incorrect_protocol
}
@@ -213,16 +253,28 @@ object AngConfigManager {
vnext.address = vmessQRCode.add
vnext.port = Utils.parseInt(vmessQRCode.port)
vnext.users[0].id = vmessQRCode.id
vnext.users[0].security = if (TextUtils.isEmpty(vmessQRCode.scy)) DEFAULT_SECURITY else vmessQRCode.scy
vnext.users[0].security =
if (TextUtils.isEmpty(vmessQRCode.scy)) DEFAULT_SECURITY else vmessQRCode.scy
vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid)
}
val sni = streamSetting.populateTransportSettings(vmessQRCode.net, vmessQRCode.type, vmessQRCode.host,
vmessQRCode.path, vmessQRCode.path, vmessQRCode.host, vmessQRCode.path, vmessQRCode.type, vmessQRCode.path)
val sni = streamSetting.populateTransportSettings(
vmessQRCode.net,
vmessQRCode.type,
vmessQRCode.host,
vmessQRCode.path,
vmessQRCode.path,
vmessQRCode.host,
vmessQRCode.path,
vmessQRCode.type,
vmessQRCode.path
)
val fingerprint = vmessQRCode.fp ?: streamSetting.tlsSettings?.fingerprint
streamSetting.populateTlsSettings(vmessQRCode.tls, allowInsecure,
if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni,
fingerprint, vmessQRCode.alpn, null, null, null)
streamSetting.populateTlsSettings(
vmessQRCode.tls, allowInsecure,
if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni,
fingerprint, vmessQRCode.alpn, null, null, null
)
}
}
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
@@ -232,7 +284,8 @@ object AngConfigManager {
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks = Utils.urlDecode(result.substring(indexSplit + 1, result.length))
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
@@ -243,13 +296,17 @@ object AngConfigManager {
//part decode
val indexS = result.indexOf("@")
result = if (indexS > 0) {
Utils.decode(result.substring(0, indexS)) + result.substring(indexS, result.length)
Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
Utils.decode(result)
}
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
val match = legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol
val match = legacyPattern.matchEntire(result)
?: return R.string.toast_incorrect_protocol
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
@@ -264,7 +321,8 @@ object AngConfigManager {
config = ServerConfig.create(EConfigType.SOCKS)
if (indexSplit > 0) {
try {
config.remarks = Utils.urlDecode(result.substring(indexSplit + 1, result.length))
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
@@ -275,18 +333,23 @@ object AngConfigManager {
//part decode
val indexS = result.indexOf("@")
if (indexS > 0) {
result = Utils.decode(result.substring(0, indexS)) + result.substring(indexS, result.length)
result = Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
result = Utils.decode(result)
}
val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex()
val match = legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol
val match =
legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
val socksUsersBean = V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
val socksUsersBean =
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = match.groupValues[1].lowercase()
socksUsersBean.pass = match.groupValues[2]
server.users = listOf(socksUsersBean)
@@ -302,17 +365,29 @@ object AngConfigManager {
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val sni = config.outboundBean?.streamSettings?.populateTransportSettings(queryParam["type"] ?: "tcp", queryParam["headerType"],
queryParam["host"], queryParam["path"], queryParam["seed"], queryParam["quicSecurity"], queryParam["key"],
queryParam["mode"], queryParam["serviceName"])
val sni = config.outboundBean?.streamSettings?.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"]
)
fingerprint = queryParam["fp"] ?: ""
config.outboundBean?.streamSettings?.populateTlsSettings(queryParam["security"] ?: TLS,
allowInsecure, queryParam["sni"] ?: sni!!, fingerprint, queryParam["alpn"],
null, null, null)
config.outboundBean?.streamSettings?.populateTlsSettings(
queryParam["security"] ?: TLS,
allowInsecure, queryParam["sni"] ?: sni!!, fingerprint, queryParam["alpn"],
null, null, null
)
flow = queryParam["flow"] ?: ""
} else {
config.outboundBean?.streamSettings?.populateTlsSettings(TLS, allowInsecure, "",
fingerprint, null, null, null, null)
config.outboundBean?.streamSettings?.populateTlsSettings(
TLS, allowInsecure, "",
fingerprint, null, null, null, null
)
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
@@ -335,27 +410,41 @@ object AngConfigManager {
vnext.port = uri.port
vnext.users[0].id = uri.userInfo
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
vnext.users[0].flow =queryParam["flow"] ?: ""
vnext.users[0].flow = queryParam["flow"] ?: ""
}
val sni = streamSetting.populateTransportSettings(queryParam["type"] ?: "tcp", queryParam["headerType"],
queryParam["host"], queryParam["path"], queryParam["seed"], queryParam["quicSecurity"], queryParam["key"],
queryParam["mode"], queryParam["serviceName"])
val sni = streamSetting.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"]
)
fingerprint = queryParam["fp"] ?: ""
val pbk = queryParam["pbk"] ?: ""
val sid = queryParam["sid"] ?: ""
val spx = Utils.urlDecode(queryParam["spx"] ?: "")
streamSetting.populateTlsSettings(queryParam["security"] ?: "", allowInsecure,
queryParam["sni"] ?: sni, fingerprint, queryParam["alpn"], pbk, sid, spx)
val spx = Utils.urlDecode(queryParam["spx"] ?: "")
streamSetting.populateTlsSettings(
queryParam["security"] ?: "", allowInsecure,
queryParam["sni"] ?: sni, fingerprint, queryParam["alpn"], pbk, sid, spx
)
}
if (config == null){
if (config == null) {
return R.string.toast_incorrect_protocol
}
config.subscriptionId = subid
val guid = MmkvManager.encodeServerConfig("", config)
if (removedSelectedServer != null &&
config.getProxyOutbound()?.getServerAddress() == removedSelectedServer.getProxyOutbound()?.getServerAddress() &&
config.getProxyOutbound()?.getServerPort() == removedSelectedServer.getProxyOutbound()?.getServerPort()) {
config.getProxyOutbound()
?.getServerAddress() == removedSelectedServer.getProxyOutbound()
?.getServerAddress() &&
config.getProxyOutbound()
?.getServerPort() == removedSelectedServer.getProxyOutbound()?.getServerPort()
) {
mainStorage?.encode(KEY_SELECTED_SERVER, guid)
}
} catch (e: Exception) {
@@ -365,14 +454,18 @@ object AngConfigManager {
return 0
}
private fun tryParseNewVmess(uriString: String, config: ServerConfig, allowInsecure: Boolean): Boolean {
private fun tryParseNewVmess(
uriString: String,
config: ServerConfig,
allowInsecure: Boolean
): Boolean {
return runCatching {
val uri = URI(uriString)
check(uri.scheme == "vmess")
val (_, protocol, tlsStr, uuid, alterId) =
Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})")
.matchEntire(uri.userInfo)?.groupValues
?: error("parse user info fail.")
Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})")
.matchEntire(uri.userInfo)?.groupValues
?: error("parse user info fail.")
val tls = tlsStr.isNotBlank()
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
@@ -387,12 +480,19 @@ object AngConfigManager {
vnext.users[0].alterId = alterId.toInt()
}
var fingerprint = streamSetting.tlsSettings?.fingerprint
val sni = streamSetting.populateTransportSettings(protocol, queryParam["type"],
queryParam["host"]?.split("|")?.get(0) ?: "",
queryParam["path"]?.takeIf { it.trim() != "/" } ?: "", queryParam["seed"], queryParam["security"],
queryParam["key"], queryParam["mode"], queryParam["serviceName"])
streamSetting.populateTlsSettings(if (tls) TLS else "", allowInsecure, sni, fingerprint, null,
null, null, null)
val sni = streamSetting.populateTransportSettings(protocol,
queryParam["type"],
queryParam["host"]?.split("|")?.get(0) ?: "",
queryParam["path"]?.takeIf { it.trim() != "/" } ?: "",
queryParam["seed"],
queryParam["security"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"])
streamSetting.populateTlsSettings(
if (tls) TLS else "", allowInsecure, sni, fingerprint, null,
null, null, null
)
true
}.getOrElse { false }
}
@@ -480,12 +580,16 @@ object AngConfigManager {
vmessQRCode.add = outbound.getServerAddress().orEmpty()
vmessQRCode.port = outbound.getServerPort().toString()
vmessQRCode.id = outbound.getPassword().orEmpty()
vmessQRCode.aid = outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
vmessQRCode.scy = outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
vmessQRCode.aid =
outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
vmessQRCode.scy =
outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
vmessQRCode.net = streamSetting.network
vmessQRCode.tls = streamSetting.security
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
vmessQRCode.alpn = Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()).orEmpty()
vmessQRCode.alpn =
Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString())
.orEmpty()
vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty()
outbound.getTransportSettingDetails()?.let { transportDetails ->
vmessQRCode.type = transportDetails[0]
@@ -495,25 +599,34 @@ object AngConfigManager {
val json = Gson().toJson(vmessQRCode)
Utils.encode(json)
}
EConfigType.CUSTOM, EConfigType.WIREGUARD -> ""
EConfigType.SHADOWSOCKS -> {
val remark = "#" + Utils.urlEncode(config.remarks)
val pw = Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
val url = String.format("%s@%s:%s",
pw,
Utils.getIpv6Address(outbound.getServerAddress()!!),
outbound.getServerPort())
url + remark
}
EConfigType.SOCKS -> {
val remark = "#" + Utils.urlEncode(config.remarks)
val pw = Utils.encode("${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}")
val url = String.format("%s@%s:%s",
val pw =
Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
val url = String.format(
"%s@%s:%s",
pw,
Utils.getIpv6Address(outbound.getServerAddress()!!),
outbound.getServerPort())
outbound.getServerPort()
)
url + remark
}
EConfigType.SOCKS -> {
val remark = "#" + Utils.urlEncode(config.remarks)
val pw =
Utils.encode("${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}")
val url = String.format(
"%s@%s:%s",
pw,
Utils.getIpv6Address(outbound.getServerAddress()!!),
outbound.getServerPort()
)
url + remark
}
EConfigType.VLESS,
EConfigType.TROJAN -> {
val remark = "#" + Utils.urlEncode(config.remarks)
@@ -537,12 +650,14 @@ object AngConfigManager {
}
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
(streamSetting.tlsSettings?: streamSetting.realitySettings)?.let { tlsSetting ->
(streamSetting.tlsSettings
?: streamSetting.realitySettings)?.let { tlsSetting ->
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
dicQuery["alpn"] = Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
dicQuery["alpn"] =
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
dicQuery["fp"] = tlsSetting.fingerprint!!
@@ -567,12 +682,14 @@ object AngConfigManager {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
}
"kcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
}
}
"ws" -> {
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
@@ -581,6 +698,7 @@ object AngConfigManager {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"http", "h2" -> {
dicQuery["type"] = "http"
if (!TextUtils.isEmpty(transportDetails[1])) {
@@ -590,11 +708,13 @@ object AngConfigManager {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"quic" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
}
"grpc" -> {
dicQuery["mode"] = transportDetails[0]
dicQuery["serviceName"] = transportDetails[2]
@@ -602,13 +722,15 @@ object AngConfigManager {
}
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format("%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()!!),
outbound.getServerPort())
val url = String.format(
"%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()!!),
outbound.getServerPort()
)
url + query + remark
}
}
@@ -756,17 +878,34 @@ object AngConfigManager {
var count = 0
servers.lines()
.reversed()
.forEach {
val resId = importConfig(it, subid, removedSelectedServer)
if (resId == 0) {
count++
}
.reversed()
.forEach {
val resId = importConfig(it, subid, removedSelectedServer)
if (resId == 0) {
count++
}
}
return count
} catch (e: Exception) {
e.printStackTrace()
}
return 0
}
fun importSubscription(remark: String, url: String, enabled: Boolean = true): Boolean {
val subId = Utils.getUuid()
val subItem = SubscriptionItem()
subItem.remarks = remark
subItem.url = url
subItem.enabled = enabled
if (TextUtils.isEmpty(subItem.remarks) || TextUtils.isEmpty(subItem.url)) {
return false
}
subStorage?.encode(subId, Gson().toJson(subItem))
return true
}
}

View File

@@ -7,6 +7,7 @@ import android.util.Base64
import java.util.*
import android.content.ClipData
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri
@@ -399,5 +400,9 @@ object Utils {
return URL(url.protocol, IDN.toASCII(url.host, IDN.ALLOW_UNASSIGNED), url.port, url.file)
.toExternalForm()
}
fun isTv(context: Context): Boolean =
context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}

View File

@@ -50,7 +50,7 @@
android:layout_width="match_parent"
android:layout_height="@dimen/connection_test_height"
android:background="?attr/colorPrimary"
android:gravity="center|left"
android:gravity="center|start"
android:nextFocusRight="@+id/fab"
android:clickable="true"
android:focusable="true"

View File

@@ -16,7 +16,8 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
tools:listitem="@layout/item_recycler_sub_setting"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</RelativeLayout>

View File

@@ -73,12 +73,13 @@
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/layout_margin_spacing"
android:visibility="invisible">
>
<ImageView
android:layout_width="@dimen/png_height"
android:layout_height="@dimen/png_height"
android:src="@drawable/ic_share_black_24dp" />
android:src="@drawable/ic_share_black_24dp"
app:tint="?attr/colorMainText"/>
</LinearLayout>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_banner_background"/>
<foreground>
<inset android:drawable="@mipmap/ic_banner_foreground" android:inset="10%"/>
</foreground>
</adaptive-icon>

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -10,7 +10,7 @@
<string name="migration_fail">انتقال داده انجام نشد!</string>
<!-- Notifications -->
<string name="notification_action_stop_v2ray">متوقف</string>
<string name="notification_action_stop_v2ray">توقف</string>
<string name="toast_permission_denied">قادر به دریافت مجوز نیست</string>
<string name="notification_action_more">برای اطلاعات بیشتر کلیک کنید</string>
<string name="toast_services_start">شروع خدمات</string>
@@ -35,7 +35,7 @@
<string name="menu_item_import_config_custom_local">پیکربندی سفارشی را به صورت محلی وارد کنید</string>
<string name="menu_item_import_config_custom_url">پیکربندی سفارشی را از طریق نشانی اینترنتی وارد کنید</string>
<string name="menu_item_import_config_custom_url_scan">نشانی اینترنتی اسکن پیکربندی سفارشی را وارد کنید</string>
<string name="del_config_comfirm">تایید حذف؟</string>
<string name="del_config_comfirm">حذف شود؟</string>
<string name="server_lab_remarks">ملاحظات</string>
<string name="server_lab_address">نشانی</string>
<string name="server_lab_port">پورت</string>
@@ -112,8 +112,8 @@
<string name="title_pref_fake_dns_enabled">فعال کردن DNS جعلی</string>
<string name="summary_pref_fake_dns_enabled">DNS محلی آدرس IP جعلی را برمی‌گرداند (سریع‌تر می‌باشد، اما ممکن است برای برخی از برنامه‌ها کار نکند)</string>
<string name="title_pref_prefer_ipv6">IPv6 را ترجیح دهید</string>
<string name="summary_pref_prefer_ipv6">نشانی و مسیرهای IPv6 را ترجیح دهید</string>
<string name="title_pref_prefer_ipv6">ترجیح دادن IPv6</string>
<string name="summary_pref_prefer_ipv6">ترجیح دادن نشانی و مسیر های IPv6</string>
<string name="title_pref_routing">مسیریابی</string>
<string name="title_pref_routing_domain_strategy">استراتژی دامنه</string>
@@ -133,7 +133,7 @@
<string name="toast_warning_pref_proxysharing_short">اتصالات از طریق LAN را مجاز کنید، مطمئن شوید که در یک شبکه قابل اعتماد هستید</string>
<string name="title_pref_allow_insecure">allowInsecure</string>
<string name="summary_pref_allow_insecure">هنگامی که TLS، به طور پیش‌فرض allowInsecure</string>
<string name="summary_pref_allow_insecure">هنگام استفاده از TLS، به طور پیش‌فرض allowInsecure فعال است</string>
<string name="title_pref_socks_port">پورت پروکسی SOCKS5</string>
<string name="summary_pref_socks_port">پورت پروکسی SOCKS5</string>
@@ -147,29 +147,29 @@
<string name="title_pref_confirm_remove">تایید حذف پرونده پیکربندی</string>
<string name="summary_pref_confirm_remove">آیا برای حذف پرونده پیکربندی نیاز به تایید دوم توسط کاربر است</string>
<string name="title_pref_start_scan_immediate">Start scanning immediately</string>
<string name="summary_pref_start_scan_immediate">Open the camera to scan immediately at startup, otherwise you can choose to scan the code or select a photo in the toolbar</string>
<string name="title_pref_start_scan_immediate">فورا اسکن را شروع کن</string>
<string name="summary_pref_start_scan_immediate">دوربین را برای اسکن بلافاصله در هنگام راه اندازی باز کنید، در غیر این صورت می توانید کد را اسکن کنید یا عکسی را در نوار ابزار انتخاب کنید.</string>
<string name="title_pref_feedback">بازخورد</string>
<string name="summary_pref_feedback">بهبودهای بازخورد یا اشکالات در گیت‌هاب</string>
<string name="summary_pref_feedback">بازخورد یا گزارش اشکالات در گیت‌هاب</string>
<string name="summary_pref_tg_group">عضویت در گروه تلگرام</string>
<string name="toast_tg_app_not_found">برنامه تلگرام پیدا نشد</string>
<string name="title_pref_promotion">تبلیغات،</string>
<string name="title_pref_promotion">تبلیغات</string>
<string name="summary_pref_promotion">تبلیغات، برای جزئیات بیشتر کلیک کنید (کمک مالی کنید تا حذف شود)</string>
<string name="title_core_loglevel">سطح گزارشات</string>
<string name="title_mode">حالت</string>
<string name="title_mode_help">برای راهنمایی بیشتر روی این متن، کلیک کنید</string>
<string name="title_language">زبان</string>
<string name="title_ui_settings">UI settings</string>
<string name="title_ui_settings">تنظیمات رابط کاربری</string>
<string name="title_logcat">گزارشات</string>
<string name="logcat_copy">کپی</string>
<string name="logcat_clear">پاک کردن</string>
<string name="title_service_restart">راه‌اندازی مجدد خدمات</string>
<string name="title_del_all_config">حذف تمام پیکربندی</string>
<string name="title_del_duplicate_config">Delete duplicate config</string>
<string name="title_del_duplicate_config">حذف کانفیگ های تکراری</string>
<string name="title_del_invalid_config">تنظیمات نامعتبر را حذف کنید (ابتدا آزمایش کنید)</string>
<string name="title_export_all">خروجی گرفتن پیکربندی‌های غیرسفارشی در کلیپ‌بورد</string>
<string name="title_sub_setting">تنظیمات گروه‌ی اشتراک</string>
@@ -181,9 +181,9 @@
<string name="title_real_ping_all_server">تاخیر واقعی همه پیکربندی</string>
<string name="title_user_asset_setting">پرونده‌های دارایی جغرافیا</string>
<string name="title_sort_by_test_results">مرتب‌سازی بر اساس نتایج آزمایش</string>
<string name="title_filter_config">فیلتر پرونده پیکربندی</string>
<string name="title_filter_config">فیلتر پرونده پیکربندی ها</string>
<string name="filter_config_all">همه گروه‌های اشتراک</string>
<string name="title_del_duplicate_config_count">Delete %d duplicate configurations</string>
<string name="title_del_duplicate_config_count">حذف %d کانفیگ تکراری</string>
<string name="tasker_start_service">شروع خدمات</string>
<string name="tasker_setting_confirm">تایید</string>
@@ -211,6 +211,11 @@
<item>خروجی گرفتن پیکربندی کامل در کلیپ‌بورد</item>
</string-array>
<string-array name="share_sub_method">
<item>QRcode</item>
<item>خروجی گرفتن در کلیپ‌بورد</item>
</string-array>
<string-array name="routing_tag">
<item>نشانی اینترنتی یا آی‌پی پروکسی</item>
<item>نشانی اینترنتی یا آی‌پی مستقیم</item>
@@ -229,5 +234,7 @@
<item>VPN</item>
<item>فقط پروکسی</item>
</string-array>
<string name="import_subscription_success">اشتراک با موفقیت ذخیره شد</string>
<string name="import_subscription_failure">ذخیره اشتراک ناموفق بود</string>
</resources>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppThemeDayNight" parent="Theme.AppCompat.DayNight.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="colorMainBg">@color/colorBg</item>
<item name="colorMainText">@color/colorText</item>
<item name="android:statusBarColor">@color/colorPrimary</item>
<item name="android:navigationBarColor">@color/colorPrimary</item>
</style>
</resources>

View File

@@ -152,8 +152,8 @@
<string name="title_pref_confirm_remove">Подтверждение удаления профиля</string>
<string name="summary_pref_confirm_remove">Требовать двойное подтверждение удаления профиля</string>
<string name="title_pref_start_scan_immediate">Start scanning immediately</string>
<string name="summary_pref_start_scan_immediate">Open the camera to scan immediately at startup, otherwise you can choose to scan the code or select a photo in the toolbar</string>
<string name="title_pref_start_scan_immediate">Сканирование при запуске</string>
<string name="summary_pref_start_scan_immediate">Начинать сканирование сразу при запуске приложения или запускать функцию сканирования камерой или из изображения через панель инструментов</string>
<string name="title_pref_feedback">Обратная связь</string>
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string>
@@ -167,7 +167,7 @@
<string name="title_mode">Режим</string>
<string name="title_mode_help">Нажмите для получения дополнительной информации</string>
<string name="title_language">Язык</string>
<string name="title_ui_settings">UI settings</string>
<string name="title_ui_settings">Настройки интерфейса</string>
<string name="title_logcat">Системный журнал</string>
<string name="logcat_copy">Копировать</string>
@@ -216,6 +216,11 @@
<item>Экспорт всего профиля в буфер обмена</item>
</string-array>
<string-array name="share_sub_method">
<item>QR-код</item>
<item>Экспорт в буфер обмена</item>
</string-array>
<string-array name="routing_tag">
<item>Проксируемые</item>
<item>Прямые</item>
@@ -234,5 +239,7 @@
<item>VPN</item>
<item>Только прокси</item>
</string-array>
<string name="import_subscription_success">Подписка импортирована</string>
<string name="import_subscription_failure">Подписка не импортирована</string>
</resources>

View File

@@ -210,6 +210,11 @@
<item>Sao chép thành cấu hình tùy chỉnh</item>
</string-array>
<string-array name="share_sub_method">
<item>Xuất ra mã QR (Chụp màn hình để lưu)</item>
<item>Sao chép cấu hình này</item>
</string-array>
<string-array name="routing_tag">
<item>Proxy URL hoặc IP</item>
<item>Direct URL hoặc IP</item>

View File

@@ -209,6 +209,12 @@
<item>导出至剪贴板</item>
<item>导出完整配置至剪贴板</item>
</string-array>
<string-array name="share_sub_method">
<item>二维码</item>
<item>导出至剪贴板</item>
</string-array>
share_method
<string-array name="routing_tag">
<item>代理的网址或IP</item>
@@ -228,5 +234,7 @@
<item>VPN</item>
<item>仅代理</item>
</string-array>
<string name="import_subscription_success">订阅导入成功</string>
<string name="import_subscription_failure">导入订阅失败</string>
</resources>

View File

@@ -210,6 +210,11 @@
<item>匯出完整組態至剪貼簿</item>
</string-array>
<string-array name="share_sub_method">
<item>QR Code</item>
<item>匯出至剪貼簿</item>
</string-array>
<string-array name="routing_tag">
<item>Proxy URL 或 IP</item>
<item>直接連線 URL 或 IP</item>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_banner_background">#FFFFFF</color>
</resources>

View File

@@ -212,12 +212,20 @@
<string name="connection_connected">Connected, tap to check connection</string>
<string name="connection_not_connected">Not connected</string>
<string name="import_subscription_success">Subscription imported Successfully</string>
<string name="import_subscription_failure">Import subscription failed</string>
<string-array name="share_method">
<item>QRcode</item>
<item>Export to clipboard</item>
<item>Export full configuration to clipboard</item>
</string-array>
<string-array name="share_sub_method">
<item>QRcode</item>
<item>Export to clipboard</item>
</string-array>
<string-array name="routing_tag">
<item>proxy URL or IP</item>
<item>direct URL or IP</item>

View File

@@ -6,6 +6,8 @@
<item name="colorAccent">@color/colorAccent</item>
<item name="colorMainBg">@color/colorBg</item>
<item name="colorMainText">@color/colorText</item>
<item name="android:statusBarColor">@color/colorPrimary</item>
<item name="android:navigationBarColor">@color/colorPrimary</item>
</style>
<style name="AppThemeDayNight.NoActionBar" parent="AppThemeDayNight">

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">v2rayNG (PR)</string>
</resources>