mirror of
https://github.com/2dust/v2rayNG.git
synced 2026-10-29 14:39:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa4b05503a | ||
|
|
7179f0838f | ||
|
|
0d1c57000f | ||
|
|
40b8706ccd | ||
|
|
0f961c54c8 | ||
|
|
1b78a510f2 | ||
|
|
01b4630fdd | ||
|
|
6214bc19d4 | ||
|
|
b8d13bf52c | ||
|
|
3165bcf4fe | ||
|
|
eedb287cc9 | ||
|
|
59ff87606f | ||
|
|
700740a187 | ||
|
|
bb59a71cb9 | ||
|
|
552cc2425d | ||
|
|
c8f373bd3f | ||
|
|
118cf5bc7d | ||
|
|
58ca4cfe23 | ||
|
|
e1b73f2fa8 | ||
|
|
143ab036bf | ||
|
|
a258ba8bc1 | ||
|
|
e1b190d816 | ||
|
|
4c9c9afdc9 | ||
|
|
6fe3893bfd | ||
|
|
61c4e40ec1 | ||
|
|
4a1510eb65 | ||
|
|
44a56f56bc | ||
|
|
3687436792 | ||
|
|
ffc9614417 | ||
|
|
fcacb9cfb0 | ||
|
|
a4dd3d08de | ||
|
|
ab09f816a0 | ||
|
|
504f22d75b | ||
|
|
996b039b48 | ||
|
|
e151890c55 | ||
|
|
04d407f5a1 | ||
|
|
5029d75475 | ||
|
|
85b1f694c9 | ||
|
|
c63e18b751 | ||
|
|
89f53233cb | ||
|
|
be1f2cf00b | ||
|
|
bd21bbc3f9 | ||
|
|
6e5d742e40 | ||
|
|
4472162614 | ||
|
|
1f7cef32dc | ||
|
|
7e68e2eaad | ||
|
|
4c7c964e65 | ||
|
|
1bd1bceadb | ||
|
|
0e80f6b431 | ||
|
|
1ad5f30ede | ||
|
|
e4c0b09e1b | ||
|
|
073376e46d | ||
|
|
08c2a4deb1 | ||
|
|
60ab1af384 | ||
|
|
c48fbd3fc8 | ||
|
|
b98895219a | ||
|
|
0f65b1d348 | ||
|
|
a1cfa2aa7a | ||
|
|
53fce9a8f5 | ||
|
|
cb39a10ada | ||
|
|
c1c93b62d1 | ||
|
|
a9b1242686 | ||
|
|
173f60a1da | ||
|
|
b9c2decb34 | ||
|
|
200c59f773 | ||
|
|
49835a5cdb | ||
|
|
53c6506308 | ||
|
|
01b47581c0 | ||
|
|
362e541cb6 | ||
|
|
157acc8daa | ||
|
|
5d76fcd0fa | ||
|
|
4004cee279 | ||
|
|
06bcd4c16b | ||
|
|
8d61aedeca |
No files matched your search
@@ -35,11 +35,11 @@ jobs:
|
||||
run: |
|
||||
echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
|
||||
--channel=0 \
|
||||
--install "ndk;29.0.14206865"
|
||||
echo "NDK_HOME=$ANDROID_HOME/ndk/29.0.14206865" >> $GITHUB_ENV
|
||||
--install "ndk;30.0.16248370"
|
||||
echo "NDK_HOME=$ANDROID_HOME/ndk/30.0.16248370" >> $GITHUB_ENV
|
||||
sed -i '10i\
|
||||
\
|
||||
ndkVersion = "29.0.14206865"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
|
||||
ndkVersion = "30.0.16248370"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
|
||||
|
||||
- name: Restore cached libhevtun
|
||||
id: cache-libhevtun-restore
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
out-file-path: V2rayNG/app/libs/
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v6.0.0
|
||||
uses: actions/setup-java@v6.1.0
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
|
||||
+1
-1
Submodule AndroidLibXrayLite updated: c634d1baea...76a6e21722.
@@ -13,8 +13,8 @@ android {
|
||||
applicationId = "com.v2ray.ang"
|
||||
minSdk = 24
|
||||
targetSdk = 37
|
||||
versionCode = 747
|
||||
versionName = "2.3.7"
|
||||
versionCode = 754
|
||||
versionName = "2.3.14"
|
||||
|
||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||
splits {
|
||||
@@ -138,6 +138,7 @@ android {
|
||||
"ru",
|
||||
"fa",
|
||||
"ar",
|
||||
"az",
|
||||
"bn",
|
||||
"bqi-rIR"
|
||||
)
|
||||
@@ -149,6 +150,17 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
it.useJUnitPlatform()
|
||||
// Treat Jupiter discovery warnings as failures: JUnit 4 failed on an invalid @Test
|
||||
// method, such as one that returns a value, while Jupiter only warns and skips it.
|
||||
// The key is @API(status = EXPERIMENTAL) in JUnit 6; after a JUnit upgrade, check
|
||||
// that a temporary `@Test fun probe() = 1` still fails the unit test task.
|
||||
it.systemProperty("junit.platform.discovery.issue.severity.critical", "WARNING")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -201,10 +213,12 @@ dependencies {
|
||||
implementation(libs.reorderable)
|
||||
|
||||
// Testing Libraries
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testImplementation(libs.junit.jupiter)
|
||||
testRuntimeOnly(libs.junit.platform.launcher)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
testImplementation(libs.org.mockito.mockito.inline)
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(libs.mockito.kotlin)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
}
|
||||
@@ -153,6 +153,7 @@
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.userasset.UserAssetUrlActivity"
|
||||
android:windowSoftInputMode="stateUnchanged|adjustResize"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
|
||||
@@ -1,106 +1,86 @@
|
||||
{
|
||||
"stats":{},
|
||||
"stats": {},
|
||||
"log": {
|
||||
"loglevel": "warning"
|
||||
},
|
||||
"policy":{
|
||||
"levels": {
|
||||
"8": {
|
||||
"handshake": 4,
|
||||
"connIdle": 300,
|
||||
"uplinkOnly": 1,
|
||||
"downlinkOnly": 1
|
||||
"policy": {
|
||||
"levels": {
|
||||
"8": {
|
||||
"handshake": 4,
|
||||
"connIdle": 300,
|
||||
"uplinkOnly": 1,
|
||||
"downlinkOnly": 1
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"statsOutboundUplink": true,
|
||||
"statsOutboundDownlink": true
|
||||
}
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "socks",
|
||||
"port": 10808,
|
||||
"protocol": "socks",
|
||||
"settings": {
|
||||
"auth": "noauth",
|
||||
"udp": true,
|
||||
"userLevel": 8
|
||||
},
|
||||
"sniffing": {
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "proxy",
|
||||
"protocol": "vmess",
|
||||
"settings": {
|
||||
"address": "v2ray.cool",
|
||||
"port": 10086
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp"
|
||||
},
|
||||
"mux": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"streamSettings": {
|
||||
"sockopt": {
|
||||
"domainStrategy": "UseIP",
|
||||
"happyEyeballs": {
|
||||
"tryDelayMs": 250,
|
||||
"interleave": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"statsOutboundUplink": true,
|
||||
"statsOutboundDownlink": true
|
||||
}
|
||||
},
|
||||
"inbounds": [{
|
||||
"tag": "socks",
|
||||
"port": 10808,
|
||||
"protocol": "socks",
|
||||
"settings": {
|
||||
"auth": "noauth",
|
||||
"udp": true,
|
||||
"userLevel": 8
|
||||
"tag": "direct"
|
||||
},
|
||||
"sniffing": {
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [{
|
||||
"tag": "proxy",
|
||||
"protocol": "vmess",
|
||||
"settings": {
|
||||
"vnext": [
|
||||
{
|
||||
"address": "v2ray.cool",
|
||||
"port": 10086,
|
||||
"users": [
|
||||
{
|
||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||
"alterId": 0,
|
||||
"security": "auto",
|
||||
"level": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"servers": [
|
||||
{
|
||||
"address": "v2ray.cool",
|
||||
"method": "chacha20",
|
||||
"ota": false,
|
||||
"password": "123456",
|
||||
"port": 10086,
|
||||
"level": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp"
|
||||
},
|
||||
"mux": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"streamSettings": {
|
||||
"sockopt": {
|
||||
"domainStrategy": "UseIP",
|
||||
"happyEyeballs": {
|
||||
"tryDelayMs": 250,
|
||||
"interleave": 2
|
||||
{
|
||||
"protocol": "blackhole",
|
||||
"tag": "block",
|
||||
"settings": {
|
||||
"response": {
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"protocol": "blackhole",
|
||||
"tag": "block",
|
||||
"settings": {
|
||||
"response": {
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": []
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": []
|
||||
},
|
||||
"dns": {
|
||||
"hosts": {},
|
||||
"servers": []
|
||||
"hosts": {},
|
||||
"servers": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +1,103 @@
|
||||
{
|
||||
"stats":{},
|
||||
"stats": {},
|
||||
"log": {
|
||||
"loglevel": "warning"
|
||||
},
|
||||
"policy":{
|
||||
"levels": {
|
||||
"8": {
|
||||
"handshake": 4,
|
||||
"connIdle": 300,
|
||||
"uplinkOnly": 1,
|
||||
"downlinkOnly": 1
|
||||
"policy": {
|
||||
"levels": {
|
||||
"8": {
|
||||
"handshake": 4,
|
||||
"connIdle": 300,
|
||||
"uplinkOnly": 1,
|
||||
"downlinkOnly": 1
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"statsOutboundUplink": true,
|
||||
"statsOutboundDownlink": true
|
||||
}
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "socks",
|
||||
"port": 10808,
|
||||
"protocol": "socks",
|
||||
"settings": {
|
||||
"auth": "noauth",
|
||||
"udp": true,
|
||||
"userLevel": 8
|
||||
},
|
||||
"sniffing": {
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "tun",
|
||||
"protocol": "tun",
|
||||
"settings": {
|
||||
"name": "xray0",
|
||||
"MTU": 1500,
|
||||
"userLevel": 8
|
||||
},
|
||||
"sniffing": {
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "proxy",
|
||||
"protocol": "vmess",
|
||||
"settings": {
|
||||
"address": "v2ray.cool",
|
||||
"port": 10086
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp"
|
||||
},
|
||||
"mux": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"streamSettings": {
|
||||
"sockopt": {
|
||||
"domainStrategy": "UseIP",
|
||||
"happyEyeballs": {
|
||||
"tryDelayMs": 250,
|
||||
"interleave": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"statsOutboundUplink": true,
|
||||
"statsOutboundDownlink": true
|
||||
}
|
||||
},
|
||||
"inbounds": [{
|
||||
"tag": "socks",
|
||||
"port": 10808,
|
||||
"protocol": "socks",
|
||||
"settings": {
|
||||
"auth": "noauth",
|
||||
"udp": true,
|
||||
"userLevel": 8
|
||||
"tag": "direct"
|
||||
},
|
||||
"sniffing": {
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "tun",
|
||||
"protocol": "tun",
|
||||
"settings": {
|
||||
"name": "xray0",
|
||||
"MTU": 1500,
|
||||
"userLevel": 8
|
||||
},
|
||||
"sniffing": {
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls",
|
||||
"quic"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [{
|
||||
"tag": "proxy",
|
||||
"protocol": "vmess",
|
||||
"settings": {
|
||||
"vnext": [
|
||||
{
|
||||
"address": "v2ray.cool",
|
||||
"port": 10086,
|
||||
"users": [
|
||||
{
|
||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||
"alterId": 0,
|
||||
"security": "auto",
|
||||
"level": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"servers": [
|
||||
{
|
||||
"address": "v2ray.cool",
|
||||
"method": "chacha20",
|
||||
"ota": false,
|
||||
"password": "123456",
|
||||
"port": 10086,
|
||||
"level": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp"
|
||||
},
|
||||
"mux": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"streamSettings": {
|
||||
"sockopt": {
|
||||
"domainStrategy": "UseIP",
|
||||
"happyEyeballs": {
|
||||
"tryDelayMs": 250,
|
||||
"interleave": 2
|
||||
{
|
||||
"protocol": "blackhole",
|
||||
"tag": "block",
|
||||
"settings": {
|
||||
"response": {
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"protocol": "blackhole",
|
||||
"tag": "block",
|
||||
"settings": {
|
||||
"response": {
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": []
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": []
|
||||
},
|
||||
"dns": {
|
||||
"hosts": {},
|
||||
"servers": []
|
||||
"hosts": {},
|
||||
"servers": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,6 +166,7 @@ object AppConfig {
|
||||
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
|
||||
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
|
||||
const val WIREGUARD_LOCAL_MTU = "1420"
|
||||
const val WIREGUARD_LOCAL_REMOTE_DNS = "1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001"
|
||||
const val LOOPBACK = "127.0.0.1"
|
||||
|
||||
/** Shared defaults for settings shown in the UI and consumed by config generation. */
|
||||
@@ -188,6 +189,7 @@ object AppConfig {
|
||||
const val MSG_STATE_RESTART = 5
|
||||
const val MSG_MEASURE_DELAY = 6
|
||||
const val MSG_MEASURE_DELAY_RESULT = 61
|
||||
const val MSG_MEASURE_DELAY_CANCEL = 62
|
||||
const val MSG_MEASURE_CONFIG_START = 7
|
||||
const val MSG_MEASURE_CONFIG_CANCEL = 71
|
||||
const val MSG_MEASURE_CONFIG_SUCCESS = 72
|
||||
@@ -200,7 +202,6 @@ object AppConfig {
|
||||
/** Notification channel IDs and names. */
|
||||
// Use a new ID because Android does not let an app raise an existing channel's importance.
|
||||
const val RAY_NG_CHANNEL_ID = "CORE_M_CH_ID_V2"
|
||||
const val RAY_NG_CHANNEL_NAME = "Core Background Service"
|
||||
|
||||
/** Protocols Scheme **/
|
||||
const val VMESS = "vmess://"
|
||||
@@ -239,6 +240,7 @@ object AppConfig {
|
||||
const val ROOT_TUN2SOCKS_BIN = "libhevsockstun.so"
|
||||
const val ROOT_FWD_CHAIN = "CORE_FWD" // FORWARD chain for LAN/tethering sharing
|
||||
const val ROOT_DNS_CHAIN = "CORE_DNS" // nat chain for tethered-client DNS DNAT
|
||||
const val ROOT_DNS_OUT_CHAIN = "CORE_DNS_OUT" // nat chain for device DNS DNAT (root mode)
|
||||
const val ROOT_V6_CHAIN = "CORE6_FILTER" // ip6tables filter/OUTPUT chain: blackhole native IPv6 when it isn't tunneled
|
||||
const val ROOT_V6_FWD_CHAIN = "CORE6_FWD" // ip6tables FORWARD chain: route or reject tethered clients' native IPv6
|
||||
const val ROOT_V6_PRE_CHAIN = "CORE6_PRE" // ip6tables mangle/PREROUTING chain: mark forwarded clients' IPv6 into the tun
|
||||
|
||||
@@ -413,13 +413,7 @@ object CoreConfigManager {
|
||||
"${AppConfig.TAG_BALANCER_PRE}-${resolvedOutbound.tag}"
|
||||
}
|
||||
val strategyType = BalancerStrategyType.from(resolvedOutbound.profile.policyGroupType)
|
||||
val fallbackTag = if (strategyType.supportsObservatory && resolvedOutbound.profile.policyGroupTestOutbounds != false) {
|
||||
resolvedOutbound.profile.policyGroupFallbackTag
|
||||
?.takeIf { it.isNotEmpty() && it != AppConfig.TAG_PROXY }
|
||||
// Xray excludes dead random/roundRobin candidates only when fallbackTag is set;
|
||||
// without this default, an enabled empty field creates no observatory.
|
||||
?: membersToAdd.first().tag
|
||||
} else null
|
||||
val fallbackTag = resolvePolicyGroupFallbackTag(strategyType, resolvedOutbound.profile, membersToAdd.first().tag)
|
||||
val strategy = buildBalancerStrategy(
|
||||
strategyType = strategyType,
|
||||
selector = listOf(memberTagPrefix),
|
||||
@@ -435,6 +429,27 @@ object CoreConfigManager {
|
||||
policyGroupBalancerTags[resolvedOutbound.tag] = balancerTag
|
||||
}
|
||||
|
||||
internal fun resolvePolicyGroupFallbackTag(
|
||||
strategyType: BalancerStrategyType,
|
||||
profile: ProfileItem,
|
||||
firstMemberTag: String,
|
||||
): String? {
|
||||
if (strategyType.requiresObservatory || strategyType.requiresBurstObservatory) {
|
||||
// Missing or failed probes must not let a matched rule use Xray's default outbound.
|
||||
return firstMemberTag
|
||||
}
|
||||
return if (strategyType.supportsObservatory && profile.policyGroupTestOutbounds != false) {
|
||||
profile.policyGroupFallbackTag
|
||||
?.takeIf { it.isNotEmpty() && it != AppConfig.TAG_PROXY }
|
||||
// Xray excludes dead random/roundRobin candidates only when fallbackTag is set;
|
||||
// without this default, an enabled empty field creates no observatory.
|
||||
?: firstMemberTag
|
||||
} else null
|
||||
}
|
||||
|
||||
internal fun shouldUseStandardObservatory(strategyType: BalancerStrategyType, fallbackTag: String?): Boolean =
|
||||
strategyType.requiresObservatory || (strategyType.supportsObservatory && fallbackTag != null)
|
||||
|
||||
/**
|
||||
* Trim runtime sections that are not needed for latency testing.
|
||||
*/
|
||||
@@ -1249,7 +1264,7 @@ object CoreConfigManager {
|
||||
fallbackTag = fallbackTag,
|
||||
strategy = V2rayConfig.RoutingBean.StrategyObject(type = strategyType.policyGroupType)
|
||||
)
|
||||
val observatory = if (strategyType.requiresObservatory || fallbackTag != null) {
|
||||
val observatory = if (shouldUseStandardObservatory(strategyType, fallbackTag)) {
|
||||
V2rayConfig.ObservatoryObject(
|
||||
subjectSelector = selector,
|
||||
probeUrl = probeUrl,
|
||||
|
||||
@@ -252,15 +252,31 @@ object CoreOutboundBuilder {
|
||||
ipv4Addresses.ifEmpty { listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4) }
|
||||
}
|
||||
|
||||
val rawDNS = profileItem.remoteDNS
|
||||
?.split(",")
|
||||
?.map { it.trim() }
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.ifEmpty { null }
|
||||
?: listOf(AppConfig.WIREGUARD_LOCAL_REMOTE_DNS)
|
||||
|
||||
val remotes = if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) == true) {
|
||||
rawDNS
|
||||
} else {
|
||||
val ipv4Dns = rawDNS.filter { !it.contains(":") }
|
||||
ipv4Dns.ifEmpty { listOf(AppConfig.WIREGUARD_LOCAL_REMOTE_DNS) }
|
||||
}
|
||||
|
||||
outboundBean?.settings?.let { wireguard ->
|
||||
wireguard.secretKey = profileItem.secretKey
|
||||
wireguard.address = addresses
|
||||
wireguard.port = null
|
||||
wireguard.peers?.firstOrNull()?.let { peer ->
|
||||
peer.publicKey = profileItem.publicKey.orEmpty()
|
||||
peer.preSharedKey = profileItem.preSharedKey?.nullIfBlank()
|
||||
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
||||
}
|
||||
wireguard.mtu = profileItem.mtu
|
||||
wireguard.remoteDNS = remotes
|
||||
wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() }
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,13 @@ import com.v2ray.ang.service.NetworkMonitor
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import libv2ray.CoreCallbackHandler
|
||||
import libv2ray.CoreController
|
||||
import libv2ray.ProcessFinder
|
||||
@@ -47,6 +52,7 @@ object CoreServiceManager {
|
||||
private var processFinder: XrayProcessFinder? = null
|
||||
private var browserDialer: IDialerService? = null
|
||||
private var networkMonitor: NetworkMonitor? = null
|
||||
private val connectionTestScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
@Volatile
|
||||
private var isReloading = false
|
||||
@@ -184,6 +190,7 @@ object CoreServiceManager {
|
||||
* @return True if the core was stopped successfully, false otherwise.
|
||||
*/
|
||||
fun stopCoreLoop(): Boolean {
|
||||
connectionTestScope.coroutineContext.cancelChildren()
|
||||
val service = getService() ?: return false
|
||||
|
||||
networkMonitor?.unregister()
|
||||
@@ -254,6 +261,7 @@ object CoreServiceManager {
|
||||
val tunFd = currentVpnInterface
|
||||
|
||||
isReloading = true
|
||||
connectionTestScope.coroutineContext.cancelChildren()
|
||||
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core reload start...")
|
||||
|
||||
coreController.stopLoop()
|
||||
@@ -308,13 +316,15 @@ object CoreServiceManager {
|
||||
* Tests with primary URL first, then falls back to alternative URL if needed.
|
||||
* Also fetches remote IP information if the delay test was successful.
|
||||
*/
|
||||
private fun measureV2rayDelay() {
|
||||
if (!isRunning()) {
|
||||
private fun measureV2rayDelay(requestId: String) {
|
||||
val service = getService() ?: return
|
||||
if (!isRunning() || isReloading) {
|
||||
MessageHelper.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_CANCEL, "", requestId)
|
||||
return
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val service = getService() ?: return@launch
|
||||
connectionTestScope.coroutineContext.cancelChildren()
|
||||
connectionTestScope.launch {
|
||||
var time = -1L
|
||||
var errorStr = ""
|
||||
|
||||
@@ -325,6 +335,7 @@ object CoreServiceManager {
|
||||
errorStr = e.message?.substringAfter("\":").orEmpty()
|
||||
}
|
||||
if (time == -1L) {
|
||||
ensureActive()
|
||||
try {
|
||||
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
|
||||
} catch (e: Exception) {
|
||||
@@ -333,6 +344,7 @@ object CoreServiceManager {
|
||||
}
|
||||
}
|
||||
|
||||
ensureActive()
|
||||
val endpoint = if (time >= 0) SpeedtestManager.getRemoteIPInfo() else null
|
||||
val result = ConnectionTestResult(
|
||||
delayMillis = time,
|
||||
@@ -340,7 +352,17 @@ object CoreServiceManager {
|
||||
country = endpoint?.country,
|
||||
ipAddress = endpoint?.ipAddress,
|
||||
)
|
||||
MessageHelper.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_RESULT, result)
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
if (isRunning()) {
|
||||
MessageHelper.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_RESULT, result, requestId)
|
||||
} else {
|
||||
MessageHelper.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_CANCEL, "", requestId)
|
||||
}
|
||||
}
|
||||
}.invokeOnCompletion { cause ->
|
||||
if (cause is CancellationException) {
|
||||
MessageHelper.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_CANCEL, "", requestId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +500,8 @@ object CoreServiceManager {
|
||||
}
|
||||
|
||||
AppConfig.MSG_MEASURE_DELAY -> {
|
||||
measureV2rayDelay()
|
||||
if (isOrderedBroadcast) resultCode = Activity.RESULT_OK
|
||||
measureV2rayDelay(intent.getStringExtra("content").orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
import com.v2ray.ang.dto.entities.SubscriptionUsageInfo
|
||||
|
||||
data class GroupMapItem(
|
||||
var id: String,
|
||||
var remarks: String,
|
||||
val usageInfo: SubscriptionUsageInfo? = null,
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.v2ray.ang.dto
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
/** One persisted delay-test result delivered from the task process to the UI. */
|
||||
data class RealPingResult(
|
||||
val guid: String,
|
||||
val delayMillis: Long,
|
||||
) : Serializable
|
||||
@@ -69,7 +69,8 @@ data class V2rayConfig(
|
||||
var settings: OutSettingsBean? = null,
|
||||
var streamSettings: StreamSettingsBean? = null,
|
||||
val sendThrough: String? = null,
|
||||
var mux: MuxBean? = MuxBean(false)
|
||||
var mux: MuxBean? = MuxBean(false),
|
||||
var targetStrategy: String? = null
|
||||
) {
|
||||
data class OutSettingsBean(
|
||||
/*Common */
|
||||
@@ -98,7 +99,7 @@ data class V2rayConfig(
|
||||
val peers: List<WireGuardBean>? = null,
|
||||
var reserved: List<Int>? = null,
|
||||
var mtu: Int? = null,
|
||||
var domainStrategy: String? = null,
|
||||
var remoteDNS: List<String>? = null,
|
||||
) {
|
||||
data class WireGuardBean(
|
||||
var publicKey: String = "",
|
||||
|
||||
@@ -56,6 +56,7 @@ data class ProfileItem(
|
||||
var localAddress: String? = null,
|
||||
var reserved: String? = null,
|
||||
var mtu: Int? = null,
|
||||
var remoteDNS: String? = null,
|
||||
|
||||
var obfsPassword: String? = null,
|
||||
var portHopping: String? = null,
|
||||
|
||||
@@ -14,5 +14,6 @@ data class SubscriptionItem(
|
||||
var allowInsecureUrl: Boolean = false,
|
||||
var userAgent: String? = null,
|
||||
var requestHeaders: String? = null,
|
||||
var usageInfo: SubscriptionUsageInfo? = null,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.v2ray.ang.dto.entities
|
||||
|
||||
/**
|
||||
* Account information a subscription panel (3x-ui, Marzban, Remnawave, ...) publishes in the
|
||||
* HTTP response headers of the subscription URL. Every field is optional because panels send
|
||||
* only the headers they have a value for.
|
||||
*
|
||||
* Persisted as JSON inside [SubscriptionItem]; only add fields, never rename or remove one.
|
||||
*/
|
||||
data class SubscriptionUsageInfo(
|
||||
/** Bytes uploaded, from the `Subscription-Userinfo` header. */
|
||||
val upload: Long = 0,
|
||||
/** Bytes downloaded, from the `Subscription-Userinfo` header. */
|
||||
val download: Long = 0,
|
||||
/** Traffic quota in bytes; 0 means unlimited or not provided. */
|
||||
val total: Long = 0,
|
||||
/** Expiry as Unix epoch seconds; 0 means no expiry or not provided. */
|
||||
val expire: Long = 0,
|
||||
/** Decoded `Profile-Title` header. */
|
||||
val title: String? = null,
|
||||
/** Validated `Support-Url` header (usually a Telegram link). */
|
||||
val supportUrl: String? = null,
|
||||
/** Validated `Profile-Web-Page-Url` header. */
|
||||
val webPageUrl: String? = null,
|
||||
) {
|
||||
/** Bytes consumed so far (upload + download). */
|
||||
val used: Long get() = upload + download
|
||||
}
|
||||
@@ -12,6 +12,7 @@ enum class Language(val code: String, val languageTag: String) {
|
||||
RUSSIAN("ru", "ru"),
|
||||
PERSIAN("fa", "fa"),
|
||||
ARABIC("ar", "ar"),
|
||||
AZERBAIJANI("az", "az"),
|
||||
BANGLA("bn", "bn"),
|
||||
BAKHTIARI("bqi-rIR", "bqi-IR");
|
||||
|
||||
@@ -48,6 +49,7 @@ enum class Language(val code: String, val languageTag: String) {
|
||||
"ru" -> RUSSIAN
|
||||
"fa" -> PERSIAN
|
||||
"ar" -> ARABIC
|
||||
"az" -> AZERBAIJANI
|
||||
"bn" -> BANGLA
|
||||
"bqi" -> BAKHTIARI
|
||||
else -> AUTO
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
package com.v2ray.ang.enums
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.v2ray.ang.R
|
||||
|
||||
/**
|
||||
* Enum defining different notification channels.
|
||||
* Each channel has a unique channelId, notificationId, and display name.
|
||||
*/
|
||||
enum class NotificationChannelType(
|
||||
val channelId: String,
|
||||
val channelName: String,
|
||||
@StringRes val channelNameRes: Int,
|
||||
val notificationId: Int
|
||||
) {
|
||||
SUBSCRIPTION_UPDATE(
|
||||
channelId = "subscription_update_channel",
|
||||
channelName = "Subscription Update Service",
|
||||
channelNameRes = R.string.notification_channel_subscription_updates,
|
||||
notificationId = 13
|
||||
),
|
||||
CORE_TEST(
|
||||
channelId = "core_test_channel",
|
||||
channelName = "Core Test Service",
|
||||
channelNameRes = R.string.notification_channel_connection_checks,
|
||||
notificationId = 12
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,111 +7,20 @@ import android.widget.Toast
|
||||
import com.v2ray.ang.ui.compose.AppSnackbarManager
|
||||
import com.v2ray.ang.ui.compose.ToastType
|
||||
|
||||
/**
|
||||
* Shows a toast message with the given resource ID.
|
||||
*
|
||||
* @param message The resource ID of the message to show.
|
||||
*/
|
||||
fun Context.toast(message: Int) {
|
||||
val text = getString(message)
|
||||
dispatchMessage(text, ToastType.NORMAL) {
|
||||
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
|
||||
|
||||
/**
|
||||
* Shows a toast message with the given text.
|
||||
*
|
||||
* @param message The text of the message to show.
|
||||
*/
|
||||
fun Context.toast(message: CharSequence) {
|
||||
dispatchMessage(message, ToastType.NORMAL) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a toast message with the given resource ID.
|
||||
*
|
||||
* @param message The resource ID of the message to show.
|
||||
*/
|
||||
fun Context.toastSuccess(message: Int) {
|
||||
val text = getString(message)
|
||||
dispatchMessage(text, ToastType.SUCCESS) {
|
||||
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a toast message with the given text.
|
||||
*
|
||||
* @param message The text of the message to show.
|
||||
*/
|
||||
fun Context.toastSuccess(message: CharSequence) {
|
||||
dispatchMessage(message, ToastType.SUCCESS) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a toast message with the given resource ID.
|
||||
*
|
||||
* @param message The resource ID of the message to show.
|
||||
*/
|
||||
fun Context.toastError(message: Int) {
|
||||
val text = getString(message)
|
||||
dispatchMessage(text, ToastType.ERROR) {
|
||||
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a toast message with the given text.
|
||||
*
|
||||
* @param message The text of the message to show.
|
||||
*/
|
||||
fun Context.toastError(message: CharSequence) {
|
||||
dispatchMessage(message, ToastType.ERROR) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an info toast message with the given resource ID.
|
||||
*
|
||||
* @param message The resource ID of the message to show.
|
||||
*/
|
||||
fun Context.toastInfo(message: Int) {
|
||||
val text = getString(message)
|
||||
dispatchMessage(text, ToastType.INFO) {
|
||||
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an info toast message with the given text.
|
||||
*
|
||||
* @param message The text of the message to show.
|
||||
*/
|
||||
fun Context.toastInfo(message: CharSequence) {
|
||||
dispatchMessage(message, ToastType.INFO) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun runOnMain(crossinline block: () -> Unit) {
|
||||
private fun runOnMain(block: () -> Unit) {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
block()
|
||||
} else {
|
||||
Handler(Looper.getMainLooper()).post { block() }
|
||||
mainHandler.post(block)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun dispatchMessage(
|
||||
private fun Context.dispatchMessage(
|
||||
message: CharSequence,
|
||||
type: ToastType,
|
||||
long: Boolean = false,
|
||||
crossinline fallback: () -> Unit
|
||||
long: Boolean = false
|
||||
) {
|
||||
val handledBySnackbar = AppSnackbarManager.show(
|
||||
message = message,
|
||||
@@ -119,6 +28,90 @@ private inline fun dispatchMessage(
|
||||
long = long
|
||||
)
|
||||
if (!handledBySnackbar) {
|
||||
runOnMain { fallback() }
|
||||
val appContext = applicationContext
|
||||
val duration = if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
|
||||
runOnMain {
|
||||
Toast.makeText(appContext, message, duration).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a toast message with the given resource ID.
|
||||
*
|
||||
* @param message The resource ID of the message to show.
|
||||
* @param long Whether to display the message for a longer duration.
|
||||
*/
|
||||
fun Context.toast(message: Int, long: Boolean = false) {
|
||||
dispatchMessage(getString(message), ToastType.NORMAL, long)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a toast message with the given text.
|
||||
*
|
||||
* @param message The text of the message to show.
|
||||
* @param long Whether to display the message for a longer duration.
|
||||
*/
|
||||
fun Context.toast(message: CharSequence, long: Boolean = false) {
|
||||
dispatchMessage(message, ToastType.NORMAL, long)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a success toast message with the given resource ID.
|
||||
*
|
||||
* @param message The resource ID of the message to show.
|
||||
* @param long Whether to display the message for a longer duration.
|
||||
*/
|
||||
fun Context.toastSuccess(message: Int, long: Boolean = false) {
|
||||
dispatchMessage(getString(message), ToastType.SUCCESS, long)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a success toast message with the given text.
|
||||
*
|
||||
* @param message The text of the message to show.
|
||||
* @param long Whether to display the message for a longer duration.
|
||||
*/
|
||||
fun Context.toastSuccess(message: CharSequence, long: Boolean = false) {
|
||||
dispatchMessage(message, ToastType.SUCCESS, long)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an error toast message with the given resource ID.
|
||||
*
|
||||
* @param message The resource ID of the message to show.
|
||||
* @param long Whether to display the message for a longer duration.
|
||||
*/
|
||||
fun Context.toastError(message: Int, long: Boolean = false) {
|
||||
dispatchMessage(getString(message), ToastType.ERROR, long)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an error toast message with the given text.
|
||||
*
|
||||
* @param message The text of the message to show.
|
||||
* @param long Whether to display the message for a longer duration.
|
||||
*/
|
||||
fun Context.toastError(message: CharSequence, long: Boolean = false) {
|
||||
dispatchMessage(message, ToastType.ERROR, long)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an info toast message with the given resource ID.
|
||||
*
|
||||
* @param message The resource ID of the message to show.
|
||||
* @param long Whether to display the message for a longer duration.
|
||||
*/
|
||||
fun Context.toastInfo(message: Int, long: Boolean = false) {
|
||||
dispatchMessage(getString(message), ToastType.INFO, long)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an info toast message with the given text.
|
||||
*
|
||||
* @param message The text of the message to show.
|
||||
* @param long Whether to display the message for a longer duration.
|
||||
*/
|
||||
fun Context.toastInfo(message: CharSequence, long: Boolean = false) {
|
||||
dispatchMessage(message, ToastType.INFO, long)
|
||||
}
|
||||
@@ -23,7 +23,7 @@ object Hysteria2Fmt : FmtBase() {
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
config.remarks = Utils.decodeURIComponent(uri.fragment.orEmpty()).let { it.ifEmpty { "none" } }
|
||||
config.server = uri.idnHost
|
||||
config.serverPort = uri.port.toString()
|
||||
config.serverPort = (if (uri.port == -1) AppConfig.DEFAULT_PORT else uri.port).toString()
|
||||
config.password = uri.userInfo
|
||||
config.security = AppConfig.TLS
|
||||
config.network = NetworkType.HYSTERIA.type
|
||||
|
||||
@@ -32,7 +32,9 @@ object WireguardFmt : FmtBase() {
|
||||
config.publicKey = queryParam["publickey"].orEmpty()
|
||||
config.preSharedKey = queryParam["presharedkey"]?.nullIfBlank()
|
||||
config.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||
config.remoteDNS = queryParam["dns"] ?: AppConfig.WIREGUARD_LOCAL_REMOTE_DNS
|
||||
config.reserved = queryParam["reserved"] ?: "0,0,0"
|
||||
config.finalMask = (queryParam["fm"] ?: queryParam["finalmask"] ?: queryParam["finalMask"])?.nullIfBlank()
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -81,6 +83,7 @@ object WireguardFmt : FmtBase() {
|
||||
config.remarks = System.currentTimeMillis().toString()
|
||||
config.localAddress = interfaceParams["address"] ?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||
config.mtu = Utils.parseInt(interfaceParams["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||
config.remoteDNS = interfaceParams["dns"] ?: AppConfig.WIREGUARD_LOCAL_REMOTE_DNS
|
||||
config.publicKey = peerParams["publickey"].orEmpty()
|
||||
config.preSharedKey = peerParams["presharedkey"]?.nullIfBlank()
|
||||
val endpoint = peerParams["endpoint"].orEmpty()
|
||||
@@ -93,6 +96,7 @@ object WireguardFmt : FmtBase() {
|
||||
config.serverPort = ""
|
||||
}
|
||||
config.reserved = peerParams["reserved"] ?: "0,0,0"
|
||||
config.finalMask = (peerParams["finalmask"] ?: peerParams["fm"] ?: interfaceParams["finalmask"] ?: interfaceParams["fm"])?.nullIfBlank()
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -118,6 +122,10 @@ object WireguardFmt : FmtBase() {
|
||||
if (config.preSharedKey != null) {
|
||||
dicQuery["presharedkey"] = config.preSharedKey.removeWhiteSpace().orEmpty()
|
||||
}
|
||||
if (config.remoteDNS != null) {
|
||||
dicQuery["dns"] = config.remoteDNS.removeWhiteSpace().orEmpty()
|
||||
}
|
||||
config.finalMask?.nullIfBlank()?.let { dicQuery["fm"] = it }
|
||||
|
||||
return toUri(config, config.secretKey, dicQuery)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.v2ray.ang.dto.UrlContentRequest
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.SubscriptionCache
|
||||
import com.v2ray.ang.dto.entities.SubscriptionItem
|
||||
import com.v2ray.ang.dto.entities.SubscriptionUsageInfo
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.isNotNullEmpty
|
||||
import com.v2ray.ang.fmt.CustomFmt
|
||||
@@ -26,6 +27,7 @@ import com.v2ray.ang.util.JsonUtil
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.QRCodeDecoder
|
||||
import com.v2ray.ang.util.Utils
|
||||
import okhttp3.Headers
|
||||
import java.net.URI
|
||||
|
||||
object AngConfigManager {
|
||||
@@ -488,6 +490,17 @@ object AngConfigManager {
|
||||
val proxyUsername = SettingsManager.getSocksUsername()
|
||||
val proxyPassword = SettingsManager.getSocksPassword()
|
||||
|
||||
// Account info (traffic, expiry, support link) the panel sends in the response headers.
|
||||
var usageInfo: SubscriptionUsageInfo? = null
|
||||
val captureUsageInfo: (Headers) -> Unit = { headers ->
|
||||
usageInfo = SubscriptionInfoParser.parse(
|
||||
userInfo = headers["Subscription-Userinfo"],
|
||||
profileTitle = headers["Profile-Title"],
|
||||
supportUrl = headers["Support-Url"],
|
||||
webPageUrl = headers["Profile-Web-Page-Url"],
|
||||
)
|
||||
}
|
||||
|
||||
var configText = try {
|
||||
val httpPort = SettingsManager.getHttpPort()
|
||||
HttpUtil.getUrlContentWithUserAgent(
|
||||
@@ -499,7 +512,8 @@ object AngConfigManager {
|
||||
httpPort = httpPort,
|
||||
proxyUsername = proxyUsername,
|
||||
proxyPassword = proxyPassword
|
||||
)
|
||||
),
|
||||
captureUsageInfo
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
||||
@@ -512,7 +526,8 @@ object AngConfigManager {
|
||||
url = url,
|
||||
userAgent = userAgent,
|
||||
requestHeaders = requestHeaders
|
||||
)
|
||||
),
|
||||
captureUsageInfo
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
||||
@@ -526,6 +541,8 @@ object AngConfigManager {
|
||||
val count = parseConfigViaSub(configText, it.guid, false)
|
||||
if (count > 0) {
|
||||
it.subscription.lastUpdated = System.currentTimeMillis()
|
||||
// Replaced on every successful update so the box disappears if the panel stops sending the headers.
|
||||
it.subscription.usageInfo = usageInfo
|
||||
MmkvManager.encodeSubscription(it.guid, it.subscription)
|
||||
LogUtil.i(AppConfig.TAG, "Subscription updated: ${it.subscription.remarks}, $count configs")
|
||||
return SubscriptionUpdateResult(
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.dto.entities.AssetUrlCache
|
||||
import com.v2ray.ang.dto.entities.AssetUrlItem
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.extension.isGroupType
|
||||
import com.v2ray.ang.dto.entities.RulesetItem
|
||||
import com.v2ray.ang.dto.entities.ServerAffiliationInfo
|
||||
import com.v2ray.ang.dto.entities.SubscriptionCache
|
||||
@@ -349,7 +350,9 @@ object MmkvManager {
|
||||
val serverList = if (append) {
|
||||
decodeServerList(subscriptionId)
|
||||
} else {
|
||||
mutableListOf()
|
||||
replacedServers.filter { guid ->
|
||||
decodeServerConfig(guid)?.configType?.isGroupType() == true
|
||||
}.toMutableList()
|
||||
}
|
||||
val indexedServers = serverList.toHashSet()
|
||||
profiles.keys.forEach { guid ->
|
||||
@@ -373,7 +376,7 @@ object MmkvManager {
|
||||
val referencedByOtherGroups = decodeServersReferencedByOtherGroups(subscriptionId)
|
||||
val removablePayloads = ProfileReplacement.findRemovablePayloads(
|
||||
replacedServers = replacedServers,
|
||||
replacementServers = profiles.keys,
|
||||
replacementServers = serverList.toSet(),
|
||||
protectedServer = protectedServer,
|
||||
serversReferencedByOtherGroups = referencedByOtherGroups,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.v2ray.ang.handler
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
@@ -17,6 +16,7 @@ import com.v2ray.ang.core.CoreServiceManager
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.extension.delay
|
||||
import com.v2ray.ang.extension.toSpeedString
|
||||
import com.v2ray.ang.helper.NotificationHelper
|
||||
import com.v2ray.ang.ui.main.MainActivity
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -157,12 +157,17 @@ object NotificationManager {
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(): String {
|
||||
val channelId = AppConfig.RAY_NG_CHANNEL_ID
|
||||
val channelName = AppConfig.RAY_NG_CHANNEL_NAME
|
||||
// Foreground-service notifications must remain visible; LOW is silent but valid.
|
||||
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
|
||||
chan.lightColor = Color.DKGRAY
|
||||
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
getNotificationManager()?.createNotificationChannel(chan)
|
||||
val service = getService() ?: return channelId
|
||||
NotificationHelper.ensureNotificationChannel(
|
||||
context = service,
|
||||
channelId = channelId,
|
||||
channelNameRes = R.string.notification_channel_service,
|
||||
// Foreground-service notifications must remain visible; LOW is silent but valid.
|
||||
importance = NotificationManager.IMPORTANCE_LOW,
|
||||
) {
|
||||
lightColor = Color.DKGRAY
|
||||
lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
}
|
||||
return channelId
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.v2ray.ang.handler
|
||||
|
||||
import com.v2ray.ang.dto.entities.SubscriptionUsageInfo
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Parses the account information subscription panels put in the response headers of the
|
||||
* subscription URL (the de-facto standard shared by 3x-ui, Marzban, Remnawave, Clash, v2rayN,
|
||||
* Hiddify and others):
|
||||
*
|
||||
* - `Subscription-Userinfo: upload=<bytes>; download=<bytes>; total=<bytes>; expire=<unix seconds>`
|
||||
* - `Profile-Title: <text>` or `base64:<base64 text>`
|
||||
* - `Support-Url: <link>` (usually a Telegram link)
|
||||
* - `Profile-Web-Page-Url: <link>` (the panel's subscription information page)
|
||||
*
|
||||
* Pure functions only, so the logic is covered by plain JVM tests.
|
||||
*/
|
||||
object SubscriptionInfoParser {
|
||||
private const val BASE64_PREFIX = "base64:"
|
||||
private const val MAX_TITLE_LENGTH = 100
|
||||
private const val MAX_URL_LENGTH = 2048
|
||||
|
||||
/** Values at or above this are epoch milliseconds, not seconds (year 5138 in seconds). */
|
||||
private const val MILLIS_THRESHOLD = 100_000_000_000L
|
||||
|
||||
private val SAFE_SCHEMES = setOf("http", "https", "tg")
|
||||
private val TELEGRAM_HOSTS = setOf("t.me", "telegram.me", "telegram.dog", "telegram.org")
|
||||
|
||||
/**
|
||||
* Builds usage info from raw header values.
|
||||
*
|
||||
* @return null when the panel sent none of the supported headers, so the UI shows nothing.
|
||||
*/
|
||||
fun parse(
|
||||
userInfo: String?,
|
||||
profileTitle: String?,
|
||||
supportUrl: String?,
|
||||
webPageUrl: String?,
|
||||
decodeBase64: (String) -> String = Utils::decode,
|
||||
): SubscriptionUsageInfo? {
|
||||
val fields = parseUserInfoFields(userInfo)
|
||||
val title = decodeTitle(profileTitle, decodeBase64)
|
||||
val support = sanitizeLink(supportUrl)
|
||||
val webPage = sanitizeLink(webPageUrl)
|
||||
|
||||
if (fields == null && title == null && support == null && webPage == null) return null
|
||||
|
||||
return SubscriptionUsageInfo(
|
||||
upload = fields?.get("upload") ?: 0,
|
||||
download = fields?.get("download") ?: 0,
|
||||
total = fields?.get("total") ?: 0,
|
||||
expire = normalizeExpire(fields?.get("expire") ?: 0),
|
||||
title = title,
|
||||
supportUrl = support,
|
||||
webPageUrl = webPage,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The subscription URL itself answers with the panel's information page when it is opened in
|
||||
* a browser (3x-ui, Marzban), so it is the fallback target of the "info page" button.
|
||||
*/
|
||||
fun withWebPageFallback(info: SubscriptionUsageInfo, subscriptionUrl: String): SubscriptionUsageInfo {
|
||||
if (info.webPageUrl != null) return info
|
||||
return info.copy(webPageUrl = sanitizeLink(subscriptionUrl))
|
||||
}
|
||||
|
||||
/** Returns [raw] when it is an http(s) or tg:// link that is safe to hand to ACTION_VIEW. */
|
||||
fun sanitizeLink(raw: String?): String? {
|
||||
val value = raw?.trim().orEmpty()
|
||||
if (value.isEmpty() || value.length > MAX_URL_LENGTH) return null
|
||||
val uri = runCatching { URI(value) }.getOrNull() ?: return null
|
||||
val scheme = uri.scheme?.lowercase(Locale.ROOT) ?: return null
|
||||
if (scheme !in SAFE_SCHEMES) return null
|
||||
if (scheme != "tg" && uri.host.isNullOrEmpty()) return null
|
||||
return value
|
||||
}
|
||||
|
||||
/** True for links that open in the Telegram app. */
|
||||
fun isTelegramLink(url: String?): Boolean {
|
||||
val uri = runCatching { URI(url.orEmpty().trim()) }.getOrNull() ?: return false
|
||||
val scheme = uri.scheme?.lowercase(Locale.ROOT) ?: return false
|
||||
if (scheme == "tg") return true
|
||||
return uri.host?.lowercase(Locale.ROOT) in TELEGRAM_HOSTS
|
||||
}
|
||||
|
||||
private fun parseUserInfoFields(header: String?): Map<String, Long>? {
|
||||
if (header.isNullOrBlank()) return null
|
||||
val result = HashMap<String, Long>()
|
||||
for (part in header.split(';')) {
|
||||
val separator = part.indexOf('=')
|
||||
if (separator <= 0) continue
|
||||
val key = part.substring(0, separator).trim().lowercase(Locale.ROOT)
|
||||
val raw = part.substring(separator + 1).trim()
|
||||
val number = raw.toLongOrNull() ?: raw.toDoubleOrNull()?.takeIf { it.isFinite() }?.toLong()
|
||||
if (number != null) result[key] = number.coerceAtLeast(0)
|
||||
}
|
||||
return result.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun normalizeExpire(value: Long): Long =
|
||||
if (value >= MILLIS_THRESHOLD) value / 1000 else value
|
||||
|
||||
private fun decodeTitle(raw: String?, decodeBase64: (String) -> String): String? {
|
||||
val value = raw?.trim().orEmpty()
|
||||
if (value.isEmpty()) return null
|
||||
val decoded = when {
|
||||
value.startsWith(BASE64_PREFIX, ignoreCase = true) ->
|
||||
decodeBase64(value.substring(BASE64_PREFIX.length).trim())
|
||||
|
||||
value.contains('%') ->
|
||||
runCatching { URLDecoder.decode(value, "UTF-8") }.getOrDefault(value)
|
||||
|
||||
else -> value
|
||||
}
|
||||
return decoded.trim().take(MAX_TITLE_LENGTH).ifEmpty { null }
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import com.v2ray.ang.util.LogUtil
|
||||
import java.io.Serializable
|
||||
|
||||
object MessageHelper {
|
||||
|
||||
const val EXTRA_REQUEST_ID = "requestId"
|
||||
|
||||
/**
|
||||
* Sends a message to the service.
|
||||
@@ -67,8 +67,8 @@ object MessageHelper {
|
||||
* @param what The message identifier.
|
||||
* @param content The message content.
|
||||
*/
|
||||
fun sendMsg2UI(ctx: Context, what: Int, content: Serializable) {
|
||||
sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content)
|
||||
fun sendMsg2UI(ctx: Context, what: Int, content: Serializable, requestId: String? = null) {
|
||||
sendMsg(ctx, AppConfig.BROADCAST_ACTION_ACTIVITY, what, content, requestId)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,11 +77,12 @@ object MessageHelper {
|
||||
* @param ctx The context.
|
||||
* @param message The test service message containing key, subscriptionId, and serverGuids.
|
||||
*/
|
||||
fun sendMsg2TestService(ctx: Context, message: TestServiceMessage) {
|
||||
fun sendMsg2TestService(ctx: Context, message: TestServiceMessage, requestId: String? = null) {
|
||||
try {
|
||||
val intent = Intent()
|
||||
intent.component = ComponentName(ctx, CoreTestService::class.java)
|
||||
intent.putExtra("content", message)
|
||||
requestId?.let { intent.putExtra(EXTRA_REQUEST_ID, it) }
|
||||
when (message.key) {
|
||||
AppConfig.MSG_MEASURE_CONFIG_START -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -102,6 +103,9 @@ object MessageHelper {
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to send message to test service", e)
|
||||
if (message.key == AppConfig.MSG_MEASURE_CONFIG_START) {
|
||||
sendMsg2UI(ctx, AppConfig.MSG_MEASURE_CONFIG_CANCEL, "", requestId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,9 +150,11 @@ object MessageHelper {
|
||||
* @param what The message identifier.
|
||||
* @param content The message content.
|
||||
*/
|
||||
private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable) {
|
||||
private fun sendMsg(ctx: Context, action: String, what: Int, content: Serializable, requestId: String? = null) {
|
||||
try {
|
||||
ctx.sendBroadcast(messageIntent(action, what, content))
|
||||
ctx.sendBroadcast(messageIntent(action, what, content).apply {
|
||||
requestId?.let { putExtra(EXTRA_REQUEST_ID, it) }
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to send message with action: $action", e)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.enums.NotificationChannelType
|
||||
import com.v2ray.ang.handler.AppLocaleManager
|
||||
|
||||
/**
|
||||
* Unified notification helper for different notification channels.
|
||||
@@ -124,20 +126,43 @@ object NotificationHelper {
|
||||
return cachedNotificationManager!!
|
||||
}
|
||||
|
||||
private fun ensureChannelCreated(channelType: NotificationChannelType, context: Context) {
|
||||
private fun ensureChannelCreated(channelType: NotificationChannelType, context: Context) =
|
||||
ensureNotificationChannel(
|
||||
context = context,
|
||||
channelId = channelType.channelId,
|
||||
channelNameRes = channelType.channelNameRes,
|
||||
importance = NotificationManager.IMPORTANCE_LOW,
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a channel or updates only its localized name.
|
||||
*
|
||||
* Android lets apps rename an existing channel, while its behavior remains under user control.
|
||||
*/
|
||||
internal fun ensureNotificationChannel(
|
||||
context: Context,
|
||||
channelId: String,
|
||||
@StringRes channelNameRes: Int,
|
||||
importance: Int,
|
||||
configureNewChannel: NotificationChannel.() -> Unit = {},
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
if (notificationManager.getNotificationChannel(channelType.channelId) != null) return
|
||||
|
||||
val channel = NotificationChannel(
|
||||
channelType.channelId,
|
||||
channelType.channelName,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val localizedName = AppLocaleManager.localizedContext(context).getString(channelNameRes)
|
||||
val existingChannel = notificationManager.getNotificationChannel(channelId)
|
||||
if (existingChannel == null) {
|
||||
NotificationChannel(channelId, localizedName, importance)
|
||||
.apply {
|
||||
lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
configureNewChannel()
|
||||
}
|
||||
.also(notificationManager::createNotificationChannel)
|
||||
} else if (existingChannel.name.toString() != localizedName) {
|
||||
existingChannel.name = localizedName
|
||||
notificationManager.createNotificationChannel(existingChannel)
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun buildNotificationBuilder(
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.v2ray.ang.root.RootManager.refresh
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Detects whether the device grants root (`su`) access.
|
||||
@@ -43,21 +42,17 @@ object RootManager {
|
||||
|
||||
private fun probe(): Boolean {
|
||||
return try {
|
||||
val process = ProcessBuilder("su", "-c", "id -u")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
val output = process.inputStream.bufferedReader().use { it.readText() }.trim()
|
||||
val finished = process.waitFor(10, TimeUnit.SECONDS)
|
||||
if (!finished) {
|
||||
process.destroy()
|
||||
val result = RootProcessRunner.run(listOf("su", "-c", "id -u"), 10000, 1024)
|
||||
if (result.code == -1) {
|
||||
LogUtil.w(AppConfig.TAG, "RootManager: su probe timed out")
|
||||
return false
|
||||
}
|
||||
val isRoot = process.exitValue() == 0 && output.lineSequence().lastOrNull()?.trim() == "0"
|
||||
val isRoot = result.code == 0 && result.output.lineSequence().filter { it.isNotBlank() }.lastOrNull()?.trim() == "0"
|
||||
LogUtil.i(AppConfig.TAG, "RootManager: root available = $isRoot")
|
||||
isRoot
|
||||
} catch (e: Exception) {
|
||||
LogUtil.w(AppConfig.TAG, "RootManager: no root access (${e.message})")
|
||||
if (e is InterruptedException) Thread.currentThread().interrupt()
|
||||
LogUtil.w(AppConfig.TAG, "RootManager: root probe failed", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.v2ray.ang.root
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/** Owns one bounded root command. Neither inherited stdout nor output volume extends its deadline. */
|
||||
internal object RootProcessRunner {
|
||||
data class Outcome(val code: Int, val output: String)
|
||||
|
||||
fun run(command: List<String>, timeoutMillis: Long, outputLimit: Int = 65536): Outcome {
|
||||
val process = ProcessBuilder(command).redirectErrorStream(true).start()
|
||||
val captured = ByteArrayOutputStream()
|
||||
val buffer = ByteArray(4096)
|
||||
val deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis)
|
||||
var code = -1
|
||||
try {
|
||||
// Process.waitFor(timeout, unit) is API 26. Polling supports API 24–25 too.
|
||||
// Read only available bytes: a detached helper may retain stdout after su exits.
|
||||
while (System.nanoTime() < deadline) {
|
||||
val available = process.inputStream.available()
|
||||
if (available > 0) {
|
||||
val size = process.inputStream.read(buffer, 0, minOf(buffer.size, available))
|
||||
val remaining = outputLimit - captured.size()
|
||||
if (size > 0 && remaining > 0) captured.write(buffer, 0, minOf(size, remaining))
|
||||
}
|
||||
try {
|
||||
code = process.exitValue()
|
||||
if (process.inputStream.available() == 0) break
|
||||
} catch (_: IllegalThreadStateException) {
|
||||
if (available == 0) Thread.sleep(10)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (code == -1) process.destroy()
|
||||
process.inputStream.close()
|
||||
process.outputStream.close()
|
||||
process.errorStream.close()
|
||||
}
|
||||
return Outcome(code, captured.toString("UTF-8"))
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ object RootProxyManager {
|
||||
val lanShare = forceLanShare || MmkvManager.decodeSettingsBool(AppConfig.PREF_ROOT_LAN_SHARING)
|
||||
val corePid = Process.myPid()
|
||||
|
||||
val config = buildHevConfig(socksUsername, socksPassword, port, ipv6)
|
||||
val config = buildHevConfig(socksUsername, socksPassword, port, ipv6, SettingsManager.getVpnMtu(), lanShare)
|
||||
if (!writeHevConfig(cfgFile, config)) {
|
||||
return null
|
||||
}
|
||||
@@ -172,6 +172,12 @@ object RootProxyManager {
|
||||
// mark the device's own packets into the tun (Root mode only)
|
||||
if (captureDeviceTraffic) {
|
||||
append(buildMangleMarking("iptables", appUid, perAppEnabled, bypassApps, selectedUids))
|
||||
// DNS DNAT: rewrite DNS queries destined for LAN/private resolvers to a
|
||||
// public IP so the core's routing engine can resolve them via the configured
|
||||
// remote DNS (dns-out) instead of falling through to a geoip:private → direct
|
||||
// rule that sends the query back to the LAN resolver (ISP DNS poisoning /
|
||||
// CDN mis-resolution). Mirrors the tethered-client DNAT in buildLanShareSetup.
|
||||
append(buildDnsDnat("iptables"))
|
||||
}
|
||||
// optionally route hotspot / USB-tethered clients through the tun too
|
||||
if (lanShare) {
|
||||
@@ -186,6 +192,7 @@ object RootProxyManager {
|
||||
appendLine("ip -6 route replace default dev $TUN table $TABLE 2>/dev/null || true")
|
||||
appendLine("ip -6 rule add fwmark $MARK table $TABLE priority $PRIORITY 2>/dev/null || true")
|
||||
append(buildMangleMarking("ip6tables", appUid, perAppEnabled, bypassApps, selectedUids))
|
||||
append(buildDnsDnat("ip6tables"))
|
||||
} else {
|
||||
// v6 disabled: blackhole native v6 egress for the captured apps so they
|
||||
// fall back to v4-through-proxy, matching what a v4-only VpnService does.
|
||||
@@ -202,14 +209,28 @@ object RootProxyManager {
|
||||
* tun address when IPv6 is enabled; whether v6 actually flows in is decided separately by
|
||||
* the v6 route into [TABLE].
|
||||
*/
|
||||
private fun buildHevConfig(socksUsername: String?, socksPassword: String?, socksPort: Int, ipv6: Boolean): String {
|
||||
internal fun buildHevConfig(
|
||||
socksUsername: String?,
|
||||
socksPassword: String?,
|
||||
socksPort: Int,
|
||||
ipv6: Boolean,
|
||||
mtu: Int,
|
||||
lanSharing: Boolean,
|
||||
): String {
|
||||
val v4 = AppConfig.ROOT_TUN_ADDR_V4.substringBefore("/")
|
||||
val v6 = AppConfig.ROOT_TUN_ADDR_V6.substringBefore("/")
|
||||
return buildString {
|
||||
appendLine("tunnel:")
|
||||
appendLine(" name: '$TUN'")
|
||||
appendLine(" mtu: ${SettingsManager.getVpnMtu()}")
|
||||
appendLine(" mtu: $mtu")
|
||||
appendLine(" multi-queue: true")
|
||||
if (lanSharing) {
|
||||
// VNET setup or TCP-GSO EINVAL on Android can leave IPv6 packets above the LAN MTU.
|
||||
// Remove when both fallback forwarding tests pass with jumbo and TSO enabled.
|
||||
appendLine(" offload:")
|
||||
appendLine(" jumbo: false")
|
||||
if (ipv6) appendLine(" tso: false")
|
||||
}
|
||||
appendLine(" ipv4: '$v4'")
|
||||
if (ipv6) appendLine(" ipv6: '$v6'")
|
||||
appendLine("socks5:")
|
||||
@@ -301,6 +322,40 @@ object RootProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DNAT DNS queries (port 53) destined for LAN/private resolvers to a public IP.
|
||||
*
|
||||
* In root mode, DNS is marked into the tun and reaches the core's SOCKS inbound as a
|
||||
* UDP relay to the original resolver address (e.g. 192.168.1.1:53). The core's routing
|
||||
* rule `{inboundTag=socks, port=53, outboundTag=dns-out}` should intercept it, but when
|
||||
* the resolver is a private IP the query also matches a user's `geoip:private → direct`
|
||||
* routing rule, which sends it straight to the LAN resolver (ISP DNS) — causing DNS
|
||||
* poisoning, CDN mis-resolution, and the "some sites work, some don't" symptom.
|
||||
*
|
||||
* Rewriting the destination to a public IP (the configured remote DNS or 1.1.1.1
|
||||
* fallback) prevents `geoip:private` from matching, so `dns-out` can resolve via the
|
||||
* configured remote DNS servers (e.g. Cloudflare DoH) through the proxy.
|
||||
*
|
||||
* Uses the same resolver-selection logic as [buildLanShareSetup].
|
||||
*/
|
||||
private fun buildDnsDnat(cmd: String): String {
|
||||
val dnsChain = AppConfig.ROOT_DNS_OUT_CHAIN
|
||||
val dns = SettingsManager.getRemoteDnsServers()
|
||||
.firstOrNull { Utils.isPureIpAddress(it) && !it.contains(":") }
|
||||
?: AppConfig.ROOT_LAN_DNS
|
||||
val lanCidrs = if (cmd == "ip6tables") bypassCidrsV6 else bypassCidrs
|
||||
return buildString {
|
||||
appendLine("$cmd -t nat -N $dnsChain 2>/dev/null || true")
|
||||
appendLine("$cmd -t nat -F $dnsChain")
|
||||
lanCidrs.forEach {
|
||||
appendLine("$cmd -t nat -A $dnsChain -d $it -p udp --dport 53 -j DNAT --to $dns")
|
||||
appendLine("$cmd -t nat -A $dnsChain -d $it -p tcp --dport 53 -j DNAT --to $dns")
|
||||
}
|
||||
appendLine("$cmd -t nat -D OUTPUT -j $dnsChain 2>/dev/null || true")
|
||||
appendLine("$cmd -t nat -A OUTPUT -j $dnsChain")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blackhole native IPv6 egress for the captured app population when IPv6 is NOT routed
|
||||
* into the tun. A v4-only VpnService has no v6 route, so the kernel rejects apps' v6 and
|
||||
@@ -477,6 +532,10 @@ object RootProxyManager {
|
||||
appendLine("iptables -t nat -D PREROUTING -j ${AppConfig.ROOT_DNS_CHAIN} 2>/dev/null || true")
|
||||
appendLine("iptables -t nat -F ${AppConfig.ROOT_DNS_CHAIN} 2>/dev/null || true")
|
||||
appendLine("iptables -t nat -X ${AppConfig.ROOT_DNS_CHAIN} 2>/dev/null || true")
|
||||
// device DNS DNAT chain (root mode)
|
||||
appendLine("iptables -t nat -D OUTPUT -j ${AppConfig.ROOT_DNS_OUT_CHAIN} 2>/dev/null || true")
|
||||
appendLine("iptables -t nat -F ${AppConfig.ROOT_DNS_OUT_CHAIN} 2>/dev/null || true")
|
||||
appendLine("iptables -t nat -X ${AppConfig.ROOT_DNS_OUT_CHAIN} 2>/dev/null || true")
|
||||
// IPv6 LAN-sharing chains (forward accept/reject + forwarded-client marking)
|
||||
appendLine("ip6tables -D FORWARD -j ${AppConfig.ROOT_V6_FWD_CHAIN} 2>/dev/null || true")
|
||||
appendLine("ip6tables -F ${AppConfig.ROOT_V6_FWD_CHAIN} 2>/dev/null || true")
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Minimal root command runner backed by the `su` binary.
|
||||
@@ -24,32 +23,29 @@ object RootShell {
|
||||
val dir = File(context.filesDir, AppConfig.ROOT_RUNTIME_DIR).apply { mkdirs() }
|
||||
val file = File(dir, name).apply {
|
||||
writeText(script)
|
||||
setExecutable(true, false)
|
||||
setReadable(false, false)
|
||||
setReadable(true, true)
|
||||
setWritable(false, false)
|
||||
setWritable(true, true)
|
||||
}
|
||||
val safePath = file.absolutePath.replace("'", "'\\''")
|
||||
return exec("sh '$safePath'")
|
||||
// Android toybox timeout owns the root shell, so killing the su client cannot leave
|
||||
// a timed-out setup shell installing rules after rollback has already started.
|
||||
return exec("timeout -s TERM -k 2 25 sh '$safePath'")
|
||||
}
|
||||
|
||||
private fun exec(command: String, timeoutSeconds: Long = 30): Result {
|
||||
return try {
|
||||
val process = ProcessBuilder("su", "-c", command)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
val output = process.inputStream.bufferedReader().use { it.readText() }
|
||||
val finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS)
|
||||
if (!finished) {
|
||||
process.destroy()
|
||||
LogUtil.e(AppConfig.TAG, "RootShell: timed out: $command")
|
||||
return Result(-1, output)
|
||||
}
|
||||
val result = Result(process.exitValue(), output)
|
||||
val outcome = RootProcessRunner.run(listOf("su", "-c", command), timeoutSeconds * 1000)
|
||||
val result = Result(outcome.code, outcome.output)
|
||||
if (!result.success) {
|
||||
LogUtil.w(AppConfig.TAG, "RootShell: '$command' exited ${result.code}: ${output.trim()}")
|
||||
LogUtil.w(AppConfig.TAG, "RootShell: root command exited ${result.code}: ${result.output.trim()}")
|
||||
}
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "RootShell: failed to run '$command'", e)
|
||||
Result(-1, e.message ?: e.javaClass.simpleName)
|
||||
if (e is InterruptedException) Thread.currentThread().interrupt()
|
||||
LogUtil.e(AppConfig.TAG, "RootShell: root command failed", e)
|
||||
Result(-1, e.javaClass.simpleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.core.CoreNativeManager
|
||||
import com.v2ray.ang.dto.RealPingEvent
|
||||
import com.v2ray.ang.dto.RealPingResult
|
||||
import com.v2ray.ang.dto.TestServiceMessage
|
||||
import com.v2ray.ang.enums.NotificationChannelType
|
||||
import com.v2ray.ang.extension.serializable
|
||||
@@ -19,7 +20,7 @@ import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.helper.MessageHelper
|
||||
import com.v2ray.ang.helper.NotificationHelper
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class CoreTestService : Service() {
|
||||
|
||||
@@ -28,7 +29,7 @@ class CoreTestService : Service() {
|
||||
}
|
||||
|
||||
// manage active batch workers so each batch is independent and cancellable
|
||||
private val activeWorkers = Collections.synchronizedList(mutableListOf<RealPingWorkerService>())
|
||||
private val activeWorkers = ConcurrentHashMap<RealPingWorkerService, String>()
|
||||
private val cancelAction by lazy {
|
||||
val intent = Intent(this, CoreTestService::class.java).putExtra(
|
||||
"content",
|
||||
@@ -69,10 +70,7 @@ class CoreTestService : Service() {
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
LogUtil.i(AppConfig.TAG, "CoreTestService is being destroyed, cancelling ${activeWorkers.size} active workers")
|
||||
// cancel any active workers
|
||||
val snapshot = ArrayList(activeWorkers)
|
||||
snapshot.forEach { it.cancel() }
|
||||
activeWorkers.clear()
|
||||
cancelWorkers()
|
||||
NotificationHelper.stopForeground(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
@@ -99,7 +97,9 @@ class CoreTestService : Service() {
|
||||
}
|
||||
|
||||
when (message.key) {
|
||||
AppConfig.MSG_MEASURE_CONFIG_START -> handleMeasureStart(message, startId)
|
||||
AppConfig.MSG_MEASURE_CONFIG_START -> handleMeasureStart(
|
||||
message, startId, intent.getStringExtra(MessageHelper.EXTRA_REQUEST_ID).orEmpty()
|
||||
)
|
||||
AppConfig.MSG_MEASURE_CONFIG_CANCEL -> handleMeasureCancel()
|
||||
else -> {
|
||||
NotificationHelper.stopForeground(this); stopSelf(startId)
|
||||
@@ -108,7 +108,7 @@ class CoreTestService : Service() {
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun handleMeasureStart(message: TestServiceMessage, startId: Int) {
|
||||
private fun handleMeasureStart(message: TestServiceMessage, startId: Int, requestId: String) {
|
||||
LogUtil.i(AppConfig.TAG, "CoreTestService starting worker subscription ${message.subscriptionId}")
|
||||
|
||||
val guidsList = when {
|
||||
@@ -123,17 +123,22 @@ class CoreTestService : Service() {
|
||||
context = this,
|
||||
guids = guidsList,
|
||||
onlyTcp = message.onlyTcp,
|
||||
onEvent = { event -> handleWorkerEvent(event, message) { activeWorkers.remove(worker) } }
|
||||
onEvent = { event ->
|
||||
if (activeWorkers.containsKey(worker)) {
|
||||
handleWorkerEvent(event, message, requestId) { activeWorkers.remove(worker) }
|
||||
}
|
||||
}
|
||||
)
|
||||
activeWorkers.add(worker)
|
||||
activeWorkers[worker] = requestId
|
||||
worker.start()
|
||||
} else {
|
||||
MessageHelper.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_CANCEL, "", requestId)
|
||||
NotificationHelper.stopForeground(this)
|
||||
stopSelf(startId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleWorkerEvent(event: RealPingEvent, message: TestServiceMessage, onWorkerDone: () -> Unit) {
|
||||
private fun handleWorkerEvent(event: RealPingEvent, message: TestServiceMessage, requestId: String, onWorkerDone: () -> Unit) {
|
||||
when (event) {
|
||||
is RealPingEvent.Progress -> {
|
||||
NotificationHelper.updateNotification(
|
||||
@@ -142,12 +147,17 @@ class CoreTestService : Service() {
|
||||
title = getString(R.string.app_name),
|
||||
content = getString(R.string.connection_running_task_left, event.text)
|
||||
)
|
||||
MessageHelper.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, event.text)
|
||||
MessageHelper.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, event.text, requestId)
|
||||
}
|
||||
|
||||
is RealPingEvent.Result -> {
|
||||
MmkvManager.encodeServerTestDelayMillis(event.guid, event.delayMillis)
|
||||
MessageHelper.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, event.guid)
|
||||
MessageHelper.sendMsg2UI(
|
||||
this,
|
||||
AppConfig.MSG_MEASURE_CONFIG_SUCCESS,
|
||||
RealPingResult(event.guid, event.delayMillis),
|
||||
requestId,
|
||||
)
|
||||
}
|
||||
|
||||
is RealPingEvent.Finish -> {
|
||||
@@ -161,7 +171,7 @@ class CoreTestService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
MessageHelper.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_FINISH, event.status)
|
||||
MessageHelper.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_FINISH, event.status, requestId)
|
||||
onWorkerDone()
|
||||
if (activeWorkers.isEmpty()) {
|
||||
NotificationHelper.stopForeground(this)
|
||||
@@ -172,12 +182,19 @@ class CoreTestService : Service() {
|
||||
}
|
||||
|
||||
private fun handleMeasureCancel() {
|
||||
MessageHelper.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_FINISH, "0")
|
||||
LogUtil.i(AppConfig.TAG, "CoreTestService received cancel message, cancelling ${activeWorkers.size} active workers")
|
||||
val snapshot = ArrayList(activeWorkers)
|
||||
snapshot.forEach { it.cancel() }
|
||||
activeWorkers.clear()
|
||||
cancelWorkers()
|
||||
NotificationHelper.stopForeground(this)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun cancelWorkers() {
|
||||
// Reply for each cancelled request, never for a newer UI request that has not started here.
|
||||
activeWorkers.entries.toList().forEach { (worker, requestId) ->
|
||||
if (activeWorkers.remove(worker, requestId)) {
|
||||
worker.cancel()
|
||||
MessageHelper.sendMsg2UI(this, AppConfig.MSG_MEASURE_CONFIG_CANCEL, "", requestId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,21 @@ import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import com.v2ray.ang.contracts.IDialerService
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.FutureTask
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
class DialerWebviewService : IDialerService {
|
||||
private var webView: WebView? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val lifecycleVersion = AtomicLong()
|
||||
private val keepAliveInterval = 30_000L // 30 seconds
|
||||
|
||||
private val keepAliveRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
webView?.let {
|
||||
it.resumeTimers()
|
||||
it.onResume()
|
||||
}
|
||||
val view = webView ?: return
|
||||
view.resumeTimers()
|
||||
view.onResume()
|
||||
handler.postDelayed(this, keepAliveInterval)
|
||||
}
|
||||
}
|
||||
@@ -30,45 +33,88 @@ class DialerWebviewService : IDialerService {
|
||||
* @param context Service context
|
||||
*/
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun start(context: Context, dialerAddr: String) {
|
||||
if (webView != null) stop()
|
||||
if (dialerAddr.isEmpty()) return
|
||||
override fun start(context: Context, dialerAddr: String) = updateOnMainThread {
|
||||
stopOnMainThread()
|
||||
if (dialerAddr.isEmpty()) return@updateOnMainThread
|
||||
val dialerUrl = "http://$dialerAddr/"
|
||||
|
||||
webView = WebView(context.applicationContext).apply {
|
||||
settings.apply {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
// Allow JS to run even if not triggered by user
|
||||
mediaPlaybackRequiresUserGesture = false
|
||||
// Prevent aggressive caching issues
|
||||
cacheMode = WebSettings.LOAD_DEFAULT
|
||||
}
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
view?.onResume()
|
||||
view?.resumeTimers()
|
||||
val view = WebView(context.applicationContext)
|
||||
webView = view
|
||||
try {
|
||||
view.apply {
|
||||
settings.apply {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
// Allow JS to run even if not triggered by user
|
||||
mediaPlaybackRequiresUserGesture = false
|
||||
// Prevent aggressive caching issues
|
||||
cacheMode = WebSettings.LOAD_DEFAULT
|
||||
}
|
||||
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
if (view === webView) {
|
||||
view?.onResume()
|
||||
view?.resumeTimers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadUrl(dialerUrl)
|
||||
}
|
||||
|
||||
loadUrl(dialerUrl)
|
||||
handler.post(keepAliveRunnable)
|
||||
} catch (e: Exception) {
|
||||
try {
|
||||
stopOnMainThread()
|
||||
} catch (cleanupError: Exception) {
|
||||
e.addSuppressed(cleanupError)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
handler.post(keepAliveRunnable)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
override fun stop() = updateOnMainThread { stopOnMainThread() }
|
||||
|
||||
private fun stopOnMainThread() {
|
||||
handler.removeCallbacks(keepAliveRunnable)
|
||||
webView?.apply {
|
||||
stopLoading()
|
||||
pauseTimers()
|
||||
// Important to call onPause to stop internal Chromium threads properly
|
||||
onPause()
|
||||
destroy()
|
||||
}
|
||||
val view = webView ?: return
|
||||
webView = null
|
||||
view.apply {
|
||||
try {
|
||||
stopLoading()
|
||||
pauseTimers()
|
||||
// Important to call onPause to stop internal Chromium threads properly
|
||||
onPause()
|
||||
} finally {
|
||||
destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOnMainThread(action: () -> Unit) {
|
||||
val version = lifecycleVersion.incrementAndGet()
|
||||
val update = {
|
||||
// A stop or replacement can overtake a worker's queued start on the main thread.
|
||||
if (lifecycleVersion.get() == version) action()
|
||||
}
|
||||
if (Looper.myLooper() == handler.looper) {
|
||||
update()
|
||||
return
|
||||
}
|
||||
|
||||
// WebView requires its creating looper on all Android versions. Keep the dialer's
|
||||
// synchronous contract so reload waits for cleanup/setup and receives any failure.
|
||||
val task = FutureTask(update)
|
||||
check(handler.post(task)) { "WebView looper is shutting down" }
|
||||
try {
|
||||
task.get()
|
||||
} catch (e: ExecutionException) {
|
||||
throw e.cause ?: e
|
||||
} catch (e: InterruptedException) {
|
||||
task.cancel(false)
|
||||
Thread.currentThread().interrupt()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -96,6 +96,13 @@ abstract class BaseViewModel(application: Application) : AndroidViewModel(applic
|
||||
return localizedContext.getString(resId, *formatArgs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a localized quantity string from a plurals resource.
|
||||
*/
|
||||
fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any): String {
|
||||
return localizedContext.resources.getQuantityString(resId, quantity, *formatArgs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send finish activity event.
|
||||
*/
|
||||
|
||||
@@ -32,7 +32,6 @@ class CheckUpdateViewModel(application: Application) : BaseViewModel(application
|
||||
|
||||
fun checkForUpdates() {
|
||||
launchLoading {
|
||||
toast(R.string.update_checking_for_update)
|
||||
try {
|
||||
val result = UpdateCheckerManager.checkForUpdate(_checkPreRelease.value)
|
||||
if (result.hasUpdate) {
|
||||
|
||||
@@ -55,8 +55,8 @@ import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.util.AppIconFetcher
|
||||
import sh.calvin.reorderable.ReorderableCollectionItemScope
|
||||
|
||||
@@ -37,6 +37,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.v2ray.ang.R
|
||||
|
||||
@@ -47,6 +48,7 @@ fun ConfirmDialog(
|
||||
confirmText: String = stringResource(R.string.action_ok),
|
||||
dismissText: String? = stringResource(R.string.action_cancel),
|
||||
confirmIcon: @Composable (() -> Unit)? = null,
|
||||
messageTextAlign: TextAlign = TextAlign.Start,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
@@ -61,7 +63,14 @@ fun ConfirmDialog(
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = title?.let { { Text(it) } },
|
||||
text = { Text(message, style = MaterialTheme.typography.bodyMedium) },
|
||||
text = {
|
||||
Text(
|
||||
text = message,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = messageTextAlign
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { onConfirm(); onDismiss() },
|
||||
@@ -91,9 +100,11 @@ fun DeleteConfirmDialog(
|
||||
message: String,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
itemName: String? = null,
|
||||
) {
|
||||
ConfirmDialog(
|
||||
message = message,
|
||||
message = if (itemName.isNullOrBlank()) message else "$message\n\n$itemName",
|
||||
messageTextAlign = TextAlign.Center,
|
||||
confirmText = stringResource(R.string.action_delete),
|
||||
confirmIcon = {
|
||||
Icon(
|
||||
|
||||
@@ -38,6 +38,8 @@ fun FormTextField(
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
placeholder: String? = null,
|
||||
maxLines: Int = 5,
|
||||
isError: Boolean = false,
|
||||
supportingText: String? = null,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
@@ -49,6 +51,8 @@ fun FormTextField(
|
||||
onValueChange = onValueChange,
|
||||
label = { Text(label) },
|
||||
placeholder = placeholder?.let { { Text(it) } },
|
||||
isError = isError,
|
||||
supportingText = supportingText?.let { { Text(it) } },
|
||||
singleLine = false,
|
||||
maxLines = maxLines,
|
||||
enabled = enabled,
|
||||
|
||||
@@ -111,12 +111,12 @@ val colorFabInactiveDark = Color(0xFF646464) // Dark Gray
|
||||
val dividerColorLight = Color(0xFFE0E0E0) // Light Gray
|
||||
val dividerColorDark = Color(0xFF424242) // Dark Gray
|
||||
|
||||
// Toast Colors 70%
|
||||
val toastNormalBgLight = Color(0xB3353A3E) // Dark Gray
|
||||
val toastNormalBgDark = Color(0xB34A4F54) // Darker Gray
|
||||
val toastSuccessBg = Color(0xB3388E3C) // Green
|
||||
val toastErrorBg = Color(0xB3D50000) // Red
|
||||
val toastInfoBg = Color(0xB33F51B5) // Indigo Blue
|
||||
// Toast Colors 85%
|
||||
val toastNormalBgLight = Color(0xD9353A3E) // Dark Gray
|
||||
val toastNormalBgDark = Color(0xD94A4F54) // Darker Gray
|
||||
val toastSuccessBg = Color(0xD9388E3C) // Green
|
||||
val toastErrorBg = Color(0xD9D50000) // Red
|
||||
val toastInfoBg = Color(0xD93F51B5) // Indigo Blue
|
||||
val toastIconCircleBg = Color(0x33FFFFFF) // Semi-transparent White
|
||||
val toastTextColor = Color.White // White
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsChangeManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.handler.SubscriptionInfoParser
|
||||
import com.v2ray.ang.ui.AboutActivity
|
||||
import com.v2ray.ang.ui.backup.BackupActivity
|
||||
import com.v2ray.ang.ui.base.HelperBaseComponentActivity
|
||||
@@ -116,6 +117,7 @@ class MainActivity : HelperBaseComponentActivity() {
|
||||
is MainAction.EditServer -> editServer(action.guid, action.profile)
|
||||
is MainAction.ShareClipboard -> shareToClipboard(action.guid)
|
||||
is MainAction.ShareFullContent -> shareFullContentAsync(action.guid)
|
||||
is MainAction.OpenSubscriptionLink -> openSubscriptionLink(action.url)
|
||||
else -> mainViewModel.onAction(action)
|
||||
}
|
||||
},
|
||||
@@ -136,6 +138,12 @@ class MainActivity : HelperBaseComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun openSubscriptionLink(url: String) {
|
||||
// Links come from a remote panel, so only http(s) and tg:// are ever handed to ACTION_VIEW.
|
||||
val safeUrl = SubscriptionInfoParser.sanitizeLink(url) ?: return
|
||||
Utils.openUri(this, safeUrl)
|
||||
}
|
||||
|
||||
private fun navigateTo(destination: MainDestination) {
|
||||
val intent = when (destination) {
|
||||
MainDestination.Subscriptions -> Intent(this, SubSettingActivity::class.java)
|
||||
@@ -186,8 +194,7 @@ class MainActivity : HelperBaseComponentActivity() {
|
||||
toast(R.string.title_file_chooser)
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN &&
|
||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN
|
||||
) {
|
||||
checkAndRequestPermission(PermissionType.ACCESS_LOCAL_NETWORK) {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.v2ray.ang.ui.main
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -20,9 +22,13 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
@@ -33,6 +39,7 @@ import com.v2ray.ang.ui.compose.AppDivider
|
||||
import com.v2ray.ang.ui.compose.colorFabActive
|
||||
import com.v2ray.ang.ui.compose.colorFabInactiveDark
|
||||
import com.v2ray.ang.ui.compose.colorFabInactiveLight
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun MainBottomBar(
|
||||
@@ -41,6 +48,15 @@ fun MainBottomBar(
|
||||
isDarkTheme: Boolean,
|
||||
onAction: (MainAction) -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val rotationAnim = remember { Animatable(0f) }
|
||||
|
||||
LaunchedEffect(isRunning) {
|
||||
if (!isRunning) {
|
||||
rotationAnim.snapTo(0f)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -68,7 +84,17 @@ fun MainBottomBar(
|
||||
}
|
||||
}
|
||||
FloatingActionButton(
|
||||
onClick = { onAction(MainAction.ToggleService) },
|
||||
onClick = {
|
||||
if (!isRunning) {
|
||||
scope.launch {
|
||||
rotationAnim.animateTo(
|
||||
targetValue = 720f,
|
||||
animationSpec = tween(durationMillis = 3000)
|
||||
)
|
||||
}
|
||||
}
|
||||
onAction(MainAction.ToggleService)
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(end = 24.dp)
|
||||
@@ -85,7 +111,9 @@ fun MainBottomBar(
|
||||
if (isRunning) R.string.acc_stop else R.string.acc_start
|
||||
),
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.graphicsLayer { rotationZ = rotationAnim.value }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ sealed interface MainAction {
|
||||
data object RemoveInvalidServers : MainAction
|
||||
data object SortByTestResults : MainAction
|
||||
data object UpdateSubscriptions : MainAction
|
||||
data class UpdateGroupSubscription(val groupId: String) : MainAction
|
||||
data object ExportAll : MainAction
|
||||
|
||||
data object ImportQRcode : MainAction
|
||||
@@ -63,6 +64,7 @@ sealed interface MainAction {
|
||||
data class ShareClipboard(val guid: String) : MainAction
|
||||
data class ShareFullContent(val guid: String) : MainAction
|
||||
data object DismissQRCodeDialog : MainAction
|
||||
data class OpenSubscriptionLink(val url: String) : MainAction
|
||||
|
||||
data class ImportBatchConfig(val configText: String) : MainAction
|
||||
|
||||
|
||||
@@ -57,9 +57,9 @@ interface MainDataSource : Closeable {
|
||||
fun share2Clipboard(guid: String): Boolean
|
||||
|
||||
fun sendMsg2Service(msgId: Int, content: String)
|
||||
fun sendMsg2TestService(msg: TestServiceMessage)
|
||||
fun sendMsg2TestService(msg: TestServiceMessage, requestId: String? = null)
|
||||
fun cancelAllPing()
|
||||
fun testCurrentServerRealPing()
|
||||
fun testCurrentServerRealPing(requestId: String)
|
||||
|
||||
fun syncSubscriptions()
|
||||
fun initAssets()
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
package com.v2ray.ang.ui.main
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.ui.compose.DeleteConfirmDialog
|
||||
|
||||
data class ServerDeleteTarget(
|
||||
val guid: String,
|
||||
val profileName: String,
|
||||
) {
|
||||
companion object {
|
||||
val Saver = listSaver<ServerDeleteTarget?, String>(
|
||||
save = { target -> target?.let { listOf(it.guid, it.profileName) }.orEmpty() },
|
||||
restore = { ServerDeleteTarget(it[0], it[1]) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainDialogs(
|
||||
showDelAllConfirm: Boolean,
|
||||
@@ -16,7 +29,7 @@ fun MainDialogs(
|
||||
showDelInvalidConfirm: Boolean,
|
||||
onDismissDelInvalid: () -> Unit,
|
||||
onConfirmDelInvalid: () -> Unit,
|
||||
showRemoveConfirm: String?,
|
||||
showRemoveConfirm: ServerDeleteTarget?,
|
||||
onDismissRemove: () -> Unit,
|
||||
onConfirmRemove: (String) -> Unit,
|
||||
) {
|
||||
@@ -41,11 +54,11 @@ fun MainDialogs(
|
||||
onDismiss = onDismissDelInvalid
|
||||
)
|
||||
}
|
||||
if (showRemoveConfirm != null) {
|
||||
val guid = showRemoveConfirm
|
||||
showRemoveConfirm?.let { target ->
|
||||
DeleteConfirmDialog(
|
||||
message = stringResource(R.string.confirm_delete_profile),
|
||||
onConfirm = { onConfirmRemove(guid) },
|
||||
itemName = target.profileName,
|
||||
onConfirm = { onConfirmRemove(target.guid) },
|
||||
onDismiss = onDismissRemove
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,68 @@
|
||||
package com.v2ray.ang.ui.main
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRowDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.v2ray.ang.dto.GroupMapItem
|
||||
import com.v2ray.ang.dto.entities.ServersCache
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
private val TAB_BAR_PADDING = 3.dp
|
||||
private val TAB_SPACING = 2.dp
|
||||
private val TAB_HEIGHT = 30.dp
|
||||
private val SELECTED_TAB_MARGIN = 12.dp
|
||||
|
||||
/**
|
||||
* Subscription group tabs: a floating rounded bar whose selected tab is a tinted pill and whose
|
||||
* server count is a small badge.
|
||||
*
|
||||
* While all tabs fit, they share the bar width equally (like Telegram's folder tabs); once they
|
||||
* no longer fit, they keep their natural width and the bar scrolls horizontally, keeping the
|
||||
* selected tab in view.
|
||||
*/
|
||||
@Composable
|
||||
fun GroupTabBar(
|
||||
groups: List<GroupMapItem>,
|
||||
@@ -30,33 +72,91 @@ fun GroupTabBar(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val selectedIndex = selectedTabIndex.coerceIn(0, groups.lastIndex)
|
||||
ScrollableTabRow(
|
||||
selectedTabIndex = selectedIndex,
|
||||
val colors = MaterialTheme.colorScheme
|
||||
// The plain surface color equals the screen background in dark mode, so the bar needs its own tone there.
|
||||
val dark = colors.surface.luminance() < 0.5f
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.95f)),
|
||||
edgePadding = 16.dp,
|
||||
indicator = { tabPositions ->
|
||||
TabRowDefaults.SecondaryIndicator(
|
||||
modifier = Modifier.tabIndicatorOffset(tabPositions[selectedIndex]),
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
shape = CircleShape,
|
||||
color = if (dark) colors.surfaceContainerHigh else colors.surface,
|
||||
border = BorderStroke(0.5.dp, colors.outlineVariant.copy(alpha = if (dark) 0.6f else 0.35f)),
|
||||
shadowElevation = 3.dp
|
||||
) {
|
||||
groups.forEachIndexed { index, group ->
|
||||
val serverFlow = remember(group.id, mainViewModel) {
|
||||
mainViewModel.serversForGroup(group.id)
|
||||
BoxWithConstraints {
|
||||
val barPaddingPx = with(LocalDensity.current) { TAB_BAR_PADDING.roundToPx() }
|
||||
val availablePx = constraints.maxWidth - 2 * barPaddingPx
|
||||
Row(
|
||||
Modifier
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(TAB_BAR_PADDING)
|
||||
) {
|
||||
EqualOrNaturalTabsLayout(availablePx = availablePx) {
|
||||
groups.forEachIndexed { index, group ->
|
||||
key(group.id) {
|
||||
val serverFlow = remember(group.id, mainViewModel) {
|
||||
mainViewModel.serversForGroup(group.id)
|
||||
}
|
||||
GroupTabItem(
|
||||
group = group,
|
||||
selected = index == selectedIndex,
|
||||
serverFlow = serverFlow,
|
||||
onClick = { onTabClick(index) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GroupTabItem(
|
||||
group = group,
|
||||
selected = index == selectedIndex,
|
||||
serverFlow = serverFlow,
|
||||
onClick = { onTabClick(index) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays the tabs out in a row. If every tab fits in an equal share of [availablePx] they all get
|
||||
* that share; otherwise, if they still fit together, the spare space is spread evenly; otherwise
|
||||
* they keep their natural width (the parent scrolls).
|
||||
*/
|
||||
@Composable
|
||||
private fun EqualOrNaturalTabsLayout(
|
||||
availablePx: Int,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Layout(content = content) { measurables, _ ->
|
||||
if (measurables.isEmpty()) return@Layout layout(0, 0) {}
|
||||
|
||||
val spacing = TAB_SPACING.roundToPx()
|
||||
val tabHeight = TAB_HEIGHT.roundToPx()
|
||||
val count = measurables.size
|
||||
val gaps = spacing * (count - 1)
|
||||
val natural = measurables.map { it.maxIntrinsicWidth(tabHeight) }
|
||||
val equalShare = (availablePx - gaps) / count
|
||||
|
||||
val widths = when {
|
||||
availablePx <= 0 -> natural
|
||||
natural.all { it <= equalShare } -> List(count) { equalShare }
|
||||
natural.sum() + gaps <= availablePx -> {
|
||||
val extra = (availablePx - gaps - natural.sum()) / count
|
||||
natural.map { it + extra }
|
||||
}
|
||||
else -> natural
|
||||
}
|
||||
|
||||
val placeables = measurables.mapIndexed { i, measurable ->
|
||||
measurable.measure(Constraints.fixed(widths[i], tabHeight))
|
||||
}
|
||||
layout(widths.sum() + gaps, tabHeight) {
|
||||
var x = 0
|
||||
placeables.forEach { placeable ->
|
||||
placeable.placeRelative(x, 0)
|
||||
x += placeable.width + spacing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun GroupTabItem(
|
||||
group: GroupMapItem,
|
||||
@@ -65,25 +165,61 @@ private fun GroupTabItem(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val servers by serverFlow.collectAsStateWithLifecycle()
|
||||
val text = if (group.id.isEmpty()) {
|
||||
group.remarks
|
||||
} else {
|
||||
"${group.remarks} (${servers.size})"
|
||||
}
|
||||
// The default group (empty id) never showed a count.
|
||||
val count = if (group.id.isEmpty()) 0 else servers.size
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val bringIntoView = remember { BringIntoViewRequester() }
|
||||
var size by remember { mutableStateOf(IntSize.Zero) }
|
||||
val marginPx = with(LocalDensity.current) { SELECTED_TAB_MARGIN.toPx() }
|
||||
|
||||
Tab(
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.widthIn(min = 56.dp)
|
||||
.heightIn(min = 48.dp),
|
||||
text = {
|
||||
Text(
|
||||
text = text,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
// Bring the tab in with some room around it so the pill never ends up flush against the bar's
|
||||
// rounded edge; at the first and last tab the scroll stops at the bar's own inner padding.
|
||||
LaunchedEffect(selected, size) {
|
||||
if (selected && size != IntSize.Zero) {
|
||||
bringIntoView.bringIntoView(
|
||||
Rect(-marginPx, 0f, size.width + marginPx, size.height.toFloat())
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// One selectable node: name, count and selected state are announced together.
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.bringIntoViewRequester(bringIntoView)
|
||||
.onSizeChanged { size = it }
|
||||
.clip(CircleShape)
|
||||
.background(if (selected) colors.primary.copy(alpha = 0.12f) else Color.Transparent)
|
||||
.selectable(selected = selected, role = Role.Tab, onClick = onClick)
|
||||
.padding(horizontal = 12.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = group.remarks,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (selected) colors.primary else colors.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (count > 0) {
|
||||
Spacer(Modifier.width(3.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 16.dp, minHeight = 16.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (selected) colors.primary else colors.onSurfaceVariant.copy(alpha = 0.55f))
|
||||
.padding(horizontal = 4.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = count.toString(),
|
||||
style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp, lineHeight = 12.sp),
|
||||
color = if (selected) colors.onPrimary else colors.surface,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ fun ShareMethodDialog(
|
||||
more: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onAction: (MainAction) -> Unit,
|
||||
onRemove: (String) -> Unit,
|
||||
onRemove: (String, String) -> Unit,
|
||||
) {
|
||||
val menuActions = serverMenuActions(
|
||||
isComplexProfile = profile.configType.isComplexType(),
|
||||
@@ -95,7 +95,7 @@ fun ShareMethodDialog(
|
||||
ServerMenuAction.ShareClipboard -> onAction(MainAction.ShareClipboard(guid))
|
||||
ServerMenuAction.ShareFullContent -> onAction(MainAction.ShareFullContent(guid))
|
||||
ServerMenuAction.Edit -> onAction(MainAction.EditServer(guid, profile))
|
||||
ServerMenuAction.Delete -> onRemove(guid)
|
||||
ServerMenuAction.Delete -> onRemove(guid, profile.remarks)
|
||||
}
|
||||
},
|
||||
onDismiss = onDismiss
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.v2ray.ang.AngApplication
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.ConnectionTestResult
|
||||
import com.v2ray.ang.dto.RealPingResult
|
||||
import com.v2ray.ang.dto.SubscriptionUpdateResult
|
||||
import com.v2ray.ang.dto.TestServiceMessage
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
@@ -24,10 +25,9 @@ import com.v2ray.ang.handler.SubscriptionUpdater
|
||||
import com.v2ray.ang.helper.MessageHelper
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class MainRepository(
|
||||
@@ -39,40 +39,44 @@ class MainRepository(
|
||||
|
||||
private val closed = AtomicBoolean(false)
|
||||
|
||||
private val _mainServiceEvent = MutableSharedFlow<MainServiceEvent>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 64,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
// Probe results are finite and must remain lossless until the ViewModel coalesces them.
|
||||
private val mainServiceEventChannel = Channel<MainServiceEvent>(Channel.UNLIMITED)
|
||||
|
||||
override val mainServiceEvent: SharedFlow<MainServiceEvent> = _mainServiceEvent.asSharedFlow()
|
||||
override val mainServiceEvent: Flow<MainServiceEvent> = mainServiceEventChannel.receiveAsFlow()
|
||||
|
||||
private val serviceReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val safeIntent = intent ?: return
|
||||
val requestId = safeIntent.getStringExtra(MessageHelper.EXTRA_REQUEST_ID).orEmpty()
|
||||
val event = when (safeIntent.getIntExtra("key", 0)) {
|
||||
AppConfig.MSG_STATE_RUNNING -> MainServiceEvent.StateRunning
|
||||
AppConfig.MSG_STATE_NOT_RUNNING -> MainServiceEvent.StateNotRunning
|
||||
AppConfig.MSG_STATE_START_SUCCESS -> MainServiceEvent.StateStartSuccess
|
||||
AppConfig.MSG_STATE_START_FAILURE -> MainServiceEvent.StateStartFailure
|
||||
AppConfig.MSG_STATE_START_FAILURE -> MainServiceEvent.StateStartFailure(
|
||||
safeIntent.getStringExtra("content")
|
||||
)
|
||||
|
||||
AppConfig.MSG_STATE_STOP_SUCCESS -> MainServiceEvent.StateStopSuccess
|
||||
AppConfig.MSG_MEASURE_DELAY_RESULT -> safeIntent
|
||||
.serializable<ConnectionTestResult>("content")
|
||||
?.let { MainServiceEvent.MeasureDelayResult(it) }
|
||||
?.let { MainServiceEvent.MeasureDelayResult(it, requestId) }
|
||||
AppConfig.MSG_MEASURE_DELAY_CANCEL -> MainServiceEvent.MeasureDelayCancelled(requestId)
|
||||
|
||||
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> MainServiceEvent.MeasureConfigSuccess
|
||||
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> safeIntent
|
||||
.serializable<RealPingResult>("content")
|
||||
?.let { MainServiceEvent.MeasureConfigSuccess(it, requestId) }
|
||||
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> MainServiceEvent.MeasureConfigNotify(
|
||||
safeIntent.getStringExtra("content").orEmpty()
|
||||
safeIntent.getStringExtra("content").orEmpty(), requestId
|
||||
)
|
||||
|
||||
AppConfig.MSG_MEASURE_CONFIG_FINISH -> MainServiceEvent.MeasureConfigFinish(
|
||||
safeIntent.getStringExtra("content")
|
||||
requestId
|
||||
)
|
||||
AppConfig.MSG_MEASURE_CONFIG_CANCEL -> MainServiceEvent.MeasureConfigCancelled(requestId)
|
||||
|
||||
else -> null
|
||||
}
|
||||
event?.let { _mainServiceEvent.tryEmit(it) }
|
||||
event?.let { mainServiceEventChannel.trySend(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +102,7 @@ class MainRepository(
|
||||
}.onFailure {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to unregister main service receiver", it)
|
||||
}
|
||||
mainServiceEventChannel.close()
|
||||
}
|
||||
|
||||
override fun getSelectedSubscriptionId(): String =
|
||||
@@ -205,8 +210,8 @@ class MainRepository(
|
||||
override fun sendMsg2Service(msgId: Int, content: String) =
|
||||
MessageHelper.sendMsg2Service(app, msgId, content)
|
||||
|
||||
override fun sendMsg2TestService(msg: TestServiceMessage) =
|
||||
MessageHelper.sendMsg2TestService(app, msg)
|
||||
override fun sendMsg2TestService(msg: TestServiceMessage, requestId: String?) =
|
||||
MessageHelper.sendMsg2TestService(app, msg, requestId)
|
||||
|
||||
override fun cancelAllPing() {
|
||||
sendMsg2TestService(
|
||||
@@ -214,8 +219,10 @@ class MainRepository(
|
||||
)
|
||||
}
|
||||
|
||||
override fun testCurrentServerRealPing() {
|
||||
sendMsg2Service(AppConfig.MSG_MEASURE_DELAY, "")
|
||||
override fun testCurrentServerRealPing(requestId: String) {
|
||||
MessageHelper.sendMsg2ServiceForResult(app, AppConfig.MSG_MEASURE_DELAY, requestId) { handled ->
|
||||
if (!handled) mainServiceEventChannel.trySend(MainServiceEvent.MeasureDelayCancelled(requestId))
|
||||
}
|
||||
}
|
||||
|
||||
override fun syncSubscriptions() {
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -57,11 +58,17 @@ fun MainScreen(
|
||||
var showDelAllConfirm by remember { mutableStateOf(false) }
|
||||
var showDelDuplicateConfirm by remember { mutableStateOf(false) }
|
||||
var showDelInvalidConfirm by remember { mutableStateOf(false) }
|
||||
var showRemoveConfirm by remember { mutableStateOf<String?>(null) }
|
||||
var showRemoveConfirm by rememberSaveable(stateSaver = ServerDeleteTarget.Saver) {
|
||||
mutableStateOf<ServerDeleteTarget?>(null)
|
||||
}
|
||||
|
||||
var shareTarget by remember { mutableStateOf<Triple<String, ProfileItem, Boolean>?>(null) }
|
||||
val removeServer: (String) -> Unit = { guid ->
|
||||
if (confirmRemove) showRemoveConfirm = guid else onAction(MainAction.RemoveServer(guid))
|
||||
val removeServer: (String, String) -> Unit = { guid, profileName ->
|
||||
if (confirmRemove) {
|
||||
showRemoveConfirm = ServerDeleteTarget(guid, profileName)
|
||||
} else {
|
||||
onAction(MainAction.RemoveServer(guid))
|
||||
}
|
||||
}
|
||||
|
||||
val pagerState = rememberPagerState(
|
||||
@@ -238,6 +245,9 @@ fun MainScreen(
|
||||
shareTarget = Triple(guid, profile, true)
|
||||
},
|
||||
onRemoveServer = removeServer,
|
||||
usageInfo = group.usageInfo,
|
||||
groupTitle = group.remarks,
|
||||
onOpenLink = { url -> onAction(MainAction.OpenSubscriptionLink(url)) },
|
||||
contentPadding = PaddingValues(
|
||||
start = 0.dp,
|
||||
top = 0.dp,
|
||||
|
||||
@@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -32,10 +34,15 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
@@ -49,6 +56,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.LocateTarget
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.SubscriptionUsageInfo
|
||||
import com.v2ray.ang.ui.compose.ItemDivider
|
||||
import com.v2ray.ang.ui.compose.ReorderableGridItem
|
||||
import com.v2ray.ang.ui.compose.ReorderableListItem
|
||||
@@ -75,7 +83,10 @@ fun GroupPagerPage(
|
||||
onEditServer: (String, ProfileItem) -> Unit,
|
||||
onShareServer: (String, ProfileItem) -> Unit,
|
||||
onMoreServer: (String, ProfileItem) -> Unit,
|
||||
onRemoveServer: (String) -> Unit,
|
||||
onRemoveServer: (String, String) -> Unit,
|
||||
usageInfo: SubscriptionUsageInfo?,
|
||||
groupTitle: String,
|
||||
onOpenLink: (String) -> Unit,
|
||||
contentPadding: PaddingValues
|
||||
) {
|
||||
val groupStateFlow = remember(groupId) {
|
||||
@@ -98,22 +109,50 @@ fun GroupPagerPage(
|
||||
remove = onRemoveServer,
|
||||
)
|
||||
}
|
||||
ServerListPage(
|
||||
rows = groupState.rows,
|
||||
selectedGuid = selectedGuid,
|
||||
locateTarget = locateTarget?.takeIf { it.groupId == groupId },
|
||||
canReorder = canReorder,
|
||||
doubleColumnDisplay = doubleColumnDisplay,
|
||||
groupId = groupId,
|
||||
lazyListStates = lazyListStates,
|
||||
lazyGridStates = lazyGridStates,
|
||||
actions = actions,
|
||||
onLocateHandled = { mainViewModel.onAction(MainAction.LocateHandled) },
|
||||
onMoveServer = { fromIndex, toIndex ->
|
||||
mainViewModel.moveServer(groupId, fromIndex, toIndex)
|
||||
},
|
||||
contentPadding = contentPadding
|
||||
)
|
||||
// The box floats over the list (the list scrolls underneath it), so the list is padded by the box height.
|
||||
val density = LocalDensity.current
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
var infoHeightPx by remember { mutableIntStateOf(0) }
|
||||
val listPadding = if (usageInfo == null) {
|
||||
contentPadding
|
||||
} else {
|
||||
PaddingValues(
|
||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||
top = contentPadding.calculateTopPadding() + with(density) { infoHeightPx.toDp() },
|
||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||
bottom = contentPadding.calculateBottomPadding()
|
||||
)
|
||||
}
|
||||
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
ServerListPage(
|
||||
rows = groupState.rows,
|
||||
selectedGuid = selectedGuid,
|
||||
locateTarget = locateTarget?.takeIf { it.groupId == groupId },
|
||||
canReorder = canReorder,
|
||||
doubleColumnDisplay = doubleColumnDisplay,
|
||||
groupId = groupId,
|
||||
lazyListStates = lazyListStates,
|
||||
lazyGridStates = lazyGridStates,
|
||||
actions = actions,
|
||||
onLocateHandled = { mainViewModel.onAction(MainAction.LocateHandled) },
|
||||
onMoveServer = { fromIndex, toIndex ->
|
||||
mainViewModel.moveServer(groupId, fromIndex, toIndex)
|
||||
},
|
||||
contentPadding = listPadding
|
||||
)
|
||||
if (usageInfo != null) {
|
||||
SubscriptionInfoCard(
|
||||
info = usageInfo,
|
||||
fallbackTitle = groupTitle,
|
||||
onOpenLink = onOpenLink,
|
||||
onRefresh = { mainViewModel.onAction(MainAction.UpdateGroupSubscription(groupId)) },
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.onSizeChanged { infoHeightPx = it.height }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ServerRowActions(
|
||||
@@ -121,7 +160,7 @@ private class ServerRowActions(
|
||||
val edit: (String, ProfileItem) -> Unit,
|
||||
val share: (String, ProfileItem) -> Unit,
|
||||
val more: (String, ProfileItem) -> Unit,
|
||||
val remove: (String) -> Unit,
|
||||
val remove: (String, String) -> Unit,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -374,7 +413,7 @@ private fun ServerListItem(
|
||||
Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { actions.remove(row.guid) }, Modifier.size(36.dp)) {
|
||||
IconButton(onClick = { actions.remove(row.guid, row.remarks) }, Modifier.size(36.dp)) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_delete_24dp),
|
||||
stringResource(R.string.acc_delete),
|
||||
@@ -406,8 +445,22 @@ private fun ServerListItem(
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(row.typeDescription, style = MaterialTheme.typography.bodySmall, color = colorConfigType, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(testResult, style = MaterialTheme.typography.bodySmall, color = if (row.testDelayMillis < 0L) colorPingRed else colorPing, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(
|
||||
row.typeDescription,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = colorConfigType,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
testResult,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (row.testDelayMillis < 0L) colorPingRed else colorPing,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package com.v2ray.ang.ui.main
|
||||
|
||||
import com.v2ray.ang.dto.ConnectionTestResult
|
||||
import com.v2ray.ang.dto.RealPingResult
|
||||
|
||||
sealed class MainServiceEvent {
|
||||
data object StateRunning : MainServiceEvent()
|
||||
data object StateNotRunning : MainServiceEvent()
|
||||
data object StateStartSuccess : MainServiceEvent()
|
||||
data object StateStartFailure : MainServiceEvent()
|
||||
data class StateStartFailure(val message: String? = null) : MainServiceEvent()
|
||||
data object StateStopSuccess : MainServiceEvent()
|
||||
data class MeasureDelayResult(val result: ConnectionTestResult) : MainServiceEvent()
|
||||
data object MeasureConfigSuccess : MainServiceEvent()
|
||||
data class MeasureConfigNotify(val progress: String) : MainServiceEvent()
|
||||
data class MeasureConfigFinish(val finishedCount: String?) : MainServiceEvent()
|
||||
data class MeasureDelayResult(val result: ConnectionTestResult, val requestId: String) : MainServiceEvent()
|
||||
data class MeasureDelayCancelled(val requestId: String) : MainServiceEvent()
|
||||
data class MeasureConfigSuccess(val result: RealPingResult, val requestId: String) : MainServiceEvent()
|
||||
data class MeasureConfigNotify(val progress: String, val requestId: String) : MainServiceEvent()
|
||||
data class MeasureConfigFinish(val requestId: String) : MainServiceEvent()
|
||||
data class MeasureConfigCancelled(val requestId: String) : MainServiceEvent()
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package com.v2ray.ang.ui.main
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.entities.SubscriptionUsageInfo
|
||||
import com.v2ray.ang.extension.toTrafficString
|
||||
import com.v2ray.ang.handler.SubscriptionInfoParser
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
private const val MILLIS_PER_SECOND = 1000L
|
||||
private const val EXPIRY_DATE_PATTERN = "yyyy-MM-dd"
|
||||
private const val CARD_ALPHA = 0.97f
|
||||
|
||||
/** Locale-neutral values the subscription information box renders. */
|
||||
internal data class SubscriptionInfoUiModel(
|
||||
val title: String?,
|
||||
val usedBytes: Long,
|
||||
/** Traffic quota in bytes; 0 means unlimited. */
|
||||
val totalBytes: Long,
|
||||
/** Used share of the quota in 0..1; null when the quota is unlimited. */
|
||||
val trafficFraction: Float?,
|
||||
/** Expiry in epoch milliseconds; null when the subscription never expires. */
|
||||
val expireMillis: Long?,
|
||||
val expired: Boolean,
|
||||
val supportUrl: String?,
|
||||
val webPageUrl: String?,
|
||||
) {
|
||||
val trafficLow: Boolean get() = trafficFraction != null && trafficFraction >= TRAFFIC_WARNING_FRACTION
|
||||
|
||||
companion object {
|
||||
const val TRAFFIC_WARNING_FRACTION = 0.9f
|
||||
}
|
||||
}
|
||||
|
||||
internal fun buildSubscriptionInfoUiModel(
|
||||
info: SubscriptionUsageInfo,
|
||||
nowMillis: Long,
|
||||
): SubscriptionInfoUiModel {
|
||||
val unlimited = info.total <= 0L
|
||||
val expireMillis = info.expire.takeIf { it > 0L }?.let { it * MILLIS_PER_SECOND }
|
||||
return SubscriptionInfoUiModel(
|
||||
title = info.title,
|
||||
usedBytes = info.used,
|
||||
totalBytes = if (unlimited) 0L else info.total,
|
||||
trafficFraction = if (unlimited) null else (info.used.toDouble() / info.total).toFloat().coerceIn(0f, 1f),
|
||||
expireMillis = expireMillis,
|
||||
expired = expireMillis != null && expireMillis <= nowMillis,
|
||||
supportUrl = info.supportUrl,
|
||||
webPageUrl = info.webPageUrl,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed box on top of a subscription group showing the account information published by the
|
||||
* subscription panel: traffic, expiry date and shortcuts (support link, panel page, update).
|
||||
*/
|
||||
@Composable
|
||||
internal fun SubscriptionInfoCard(
|
||||
info: SubscriptionUsageInfo,
|
||||
fallbackTitle: String,
|
||||
onOpenLink: (String) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val model = remember(info) { buildSubscriptionInfoUiModel(info, System.currentTimeMillis()) }
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
// Almost opaque: the list scrolls underneath without showing through readably.
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = CARD_ALPHA),
|
||||
border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
|
||||
) {
|
||||
Column(Modifier.padding(start = 16.dp, end = 8.dp, top = 2.dp, bottom = 10.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = model.title ?: fallbackTitle,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
model.supportUrl?.let { url ->
|
||||
CardIconButton(
|
||||
iconRes = if (SubscriptionInfoParser.isTelegramLink(url)) {
|
||||
R.drawable.ic_telegram_24dp
|
||||
} else {
|
||||
R.drawable.ic_feedback_24dp
|
||||
},
|
||||
contentDescription = stringResource(R.string.sub_info_open_support),
|
||||
onClick = { onOpenLink(url) },
|
||||
)
|
||||
}
|
||||
model.webPageUrl?.let { url ->
|
||||
CardIconButton(
|
||||
iconRes = R.drawable.ic_about_24dp,
|
||||
contentDescription = stringResource(R.string.sub_info_open_page),
|
||||
onClick = { onOpenLink(url) },
|
||||
)
|
||||
}
|
||||
CardIconButton(
|
||||
iconRes = R.drawable.ic_refresh_24dp,
|
||||
contentDescription = stringResource(R.string.title_sub_update),
|
||||
onClick = onRefresh,
|
||||
)
|
||||
}
|
||||
|
||||
// One semantics node for the read-only figures so TalkBack reads them in one go.
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 8.dp)
|
||||
.semantics(mergeDescendants = true) {}
|
||||
) {
|
||||
StatsRow(model)
|
||||
model.trafficFraction?.let { fraction ->
|
||||
Spacer(Modifier.height(4.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = { fraction },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.clearAndSetSemantics {},
|
||||
color = if (model.trafficLow) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardIconButton(iconRes: Int, contentDescription: String, onClick: () -> Unit) {
|
||||
IconButton(onClick = onClick, modifier = Modifier.size(32.dp)) {
|
||||
Icon(painterResource(iconRes), contentDescription, Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatsRow(model: SubscriptionInfoUiModel) {
|
||||
val totalText = if (model.totalBytes > 0L) {
|
||||
model.totalBytes.toTrafficString()
|
||||
} else {
|
||||
stringResource(R.string.sub_info_unlimited)
|
||||
}
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
text = stringResource(R.string.sub_info_traffic, model.usedBytes.toTrafficString(), totalText),
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (model.trafficLow) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
model.expireMillis?.let { expireMillis ->
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(
|
||||
text = Utils.formatTimestamp(expireMillis, EXPIRY_DATE_PATTERN),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (model.expired) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.v2ray.ang.ui.main
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
/** Main-thread request ownership; a late reply must not complete a newer test. */
|
||||
internal class MainTestRequests {
|
||||
data class Bulk(val id: String, val groupId: String)
|
||||
|
||||
private var current: String? = null
|
||||
var bulk: Bulk? = null
|
||||
private set
|
||||
val isTesting: Boolean get() = current != null || bulk != null
|
||||
|
||||
fun beginCurrent(): String = UUID.randomUUID().toString().also { current = it }
|
||||
|
||||
fun completeCurrent(id: String): Boolean {
|
||||
if (id != current) return false
|
||||
current = null
|
||||
return true
|
||||
}
|
||||
|
||||
fun invalidateCurrent() {
|
||||
current = null
|
||||
}
|
||||
|
||||
fun beginBulk(groupId: String): Bulk = Bulk(UUID.randomUUID().toString(), groupId).also { bulk = it }
|
||||
|
||||
fun completeBulk(id: String): Bulk? {
|
||||
val request = bulk?.takeIf { it.id == id } ?: return null
|
||||
bulk = null
|
||||
return request
|
||||
}
|
||||
|
||||
fun cancelBulk() {
|
||||
bulk = null
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.ConnectionTestResult
|
||||
import com.v2ray.ang.dto.GroupMapItem
|
||||
import com.v2ray.ang.dto.LocateTarget
|
||||
import com.v2ray.ang.dto.RealPingResult
|
||||
import com.v2ray.ang.dto.TestServiceMessage
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.dto.entities.ServersCache
|
||||
@@ -17,6 +18,7 @@ import com.v2ray.ang.extension.delay
|
||||
import com.v2ray.ang.extension.isComplexType
|
||||
import com.v2ray.ang.extension.matchesPattern
|
||||
import com.v2ray.ang.extension.moveItem
|
||||
import com.v2ray.ang.handler.SubscriptionInfoParser
|
||||
import com.v2ray.ang.ui.base.BaseViewModel
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -24,6 +26,7 @@ import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -40,6 +43,30 @@ import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.regex.PatternSyntaxException
|
||||
|
||||
private fun applyTestDelayResults(
|
||||
servers: List<ServersCache>,
|
||||
updates: Map<String, Long>,
|
||||
): List<ServersCache> = servers.map { server ->
|
||||
val delayMillis = updates[server.guid]
|
||||
if (delayMillis == null || delayMillis == server.testDelayMillis) {
|
||||
server
|
||||
} else {
|
||||
server.copy(testDelayMillis = delayMillis)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyTestDelayResultsToRows(
|
||||
rows: List<ServerRowUiModel>,
|
||||
updates: Map<String, Long>,
|
||||
): List<ServerRowUiModel> = rows.map { row ->
|
||||
val delayMillis = updates[row.guid]
|
||||
if (delayMillis == null || delayMillis == row.testDelayMillis) {
|
||||
row
|
||||
} else {
|
||||
row.copy(testDelayMillis = delayMillis)
|
||||
}
|
||||
}
|
||||
|
||||
class MainViewModel(
|
||||
application: Application,
|
||||
private val dataSource: MainDataSource
|
||||
@@ -77,9 +104,11 @@ class MainViewModel(
|
||||
private var preloadJob: Job? = null
|
||||
private var selectedGroupLoadJob: Job? = null
|
||||
private var reloadJob: Job? = null
|
||||
private var testResultFlushJob: Job? = null
|
||||
private val pendingTestResults = linkedMapOf<String, Long>()
|
||||
|
||||
@Volatile
|
||||
private var testingGroupId: String? = null
|
||||
private val testRequests = MainTestRequests()
|
||||
private var bulkTestJob: Job? = null
|
||||
|
||||
private val initialPageReady = CompletableDeferred<Unit>()
|
||||
|
||||
@@ -106,31 +135,87 @@ class MainViewModel(
|
||||
updateRunningState(true)
|
||||
}
|
||||
|
||||
MainServiceEvent.StateStartFailure -> {
|
||||
toastError(R.string.toast_services_failure)
|
||||
is MainServiceEvent.StateStartFailure -> {
|
||||
if (!event.message.isNullOrBlank()) {
|
||||
toastError(event.message)
|
||||
} else {
|
||||
toastError(R.string.toast_services_failure)
|
||||
}
|
||||
updateRunningState(false)
|
||||
}
|
||||
|
||||
MainServiceEvent.StateStopSuccess -> updateRunningState(false)
|
||||
is MainServiceEvent.MeasureDelayResult -> {
|
||||
_uiState.update { it.copy(status = MainStatus.ConnectionTest(event.result)) }
|
||||
if (!uiState.value.isRunning || !testRequests.completeCurrent(event.requestId)) return
|
||||
_uiState.update { it.copy(isTesting = testRequests.isTesting, status = MainStatus.ConnectionTest(event.result)) }
|
||||
}
|
||||
|
||||
MainServiceEvent.MeasureConfigSuccess -> {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
val gid = testingGroupId ?: uiState.value.selectedGroupId
|
||||
cacheMutex.withLock { groupDataCache.remove(gid) }
|
||||
updateGroupUi(gid, loadGroup(gid, forceRefresh = true))
|
||||
}
|
||||
is MainServiceEvent.MeasureConfigSuccess -> {
|
||||
val request = testRequests.bulk?.takeIf { it.id == event.requestId } ?: return
|
||||
queueTestResult(event.result, request)
|
||||
}
|
||||
|
||||
is MainServiceEvent.MeasureConfigNotify -> {
|
||||
_uiState.update { it.copy(status = MainStatus.TestProgress(event.progress)) }
|
||||
if (event.requestId == testRequests.bulk?.id) {
|
||||
_uiState.update { it.copy(status = MainStatus.TestProgress(event.progress)) }
|
||||
}
|
||||
}
|
||||
|
||||
is MainServiceEvent.MeasureConfigFinish -> {
|
||||
onTestsFinished()
|
||||
val request = testRequests.bulk?.takeIf { it.id == event.requestId } ?: return
|
||||
val scheduledFlush = testResultFlushJob
|
||||
testResultFlushJob = viewModelScope.launch {
|
||||
scheduledFlush?.cancelAndJoin()
|
||||
if (testRequests.bulk?.id != request.id) return@launch
|
||||
flushPendingTestResults(request)
|
||||
onTestsFinished(request.id)
|
||||
}
|
||||
}
|
||||
|
||||
is MainServiceEvent.MeasureDelayCancelled -> {
|
||||
if (testRequests.completeCurrent(event.requestId)) resetTestStatus()
|
||||
}
|
||||
|
||||
is MainServiceEvent.MeasureConfigCancelled -> {
|
||||
if (testRequests.completeBulk(event.requestId) != null) {
|
||||
cancelPendingTestResults()
|
||||
resetTestStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun queueTestResult(result: RealPingResult, request: MainTestRequests.Bulk) {
|
||||
pendingTestResults[result.guid] = result.delayMillis
|
||||
if (testResultFlushJob?.isActive == true) return
|
||||
|
||||
testResultFlushJob = viewModelScope.launch {
|
||||
while (pendingTestResults.isNotEmpty()) {
|
||||
delay(TEST_RESULT_FLUSH_INTERVAL_MS)
|
||||
flushPendingTestResults(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun flushPendingTestResults(request: MainTestRequests.Bulk) {
|
||||
if (pendingTestResults.isEmpty()) return
|
||||
if (testRequests.bulk?.id != request.id) return
|
||||
|
||||
val updates = cacheMutex.withLock {
|
||||
val drained = pendingTestResults.toMap()
|
||||
pendingTestResults.clear()
|
||||
groupDataCache[request.groupId]?.let { cached ->
|
||||
groupDataCache[request.groupId] = applyTestDelayResults(cached, drained)
|
||||
}
|
||||
drained
|
||||
}
|
||||
if (updates.isEmpty()) return
|
||||
if (testRequests.bulk?.id != request.id) return
|
||||
mutableServerGroupState(request.groupId).update { current ->
|
||||
current.copy(
|
||||
servers = applyTestDelayResults(current.servers, updates),
|
||||
rows = applyTestDelayResultsToRows(current.rows, updates),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +284,8 @@ class MainViewModel(
|
||||
MainAction.RemoveDuplicateServers -> removeDuplicateServerAsync()
|
||||
MainAction.RemoveInvalidServers -> removeInvalidServerAsync()
|
||||
MainAction.SortByTestResults -> sortByTestResultsAsync()
|
||||
MainAction.UpdateSubscriptions -> importConfigViaSub()
|
||||
MainAction.UpdateSubscriptions -> importConfigViaSub(uiState.value.selectedGroupId)
|
||||
is MainAction.UpdateGroupSubscription -> importConfigViaSub(action.groupId)
|
||||
MainAction.ExportAll -> exportAllAsync()
|
||||
is MainAction.SelectGroup -> subscriptionIdChanged(action.groupId)
|
||||
is MainAction.SelectServer -> updateSelectedGuid(action.guid)
|
||||
@@ -226,7 +312,8 @@ class MainViewModel(
|
||||
MainAction.LocateSelectedServer,
|
||||
is MainAction.EditServer,
|
||||
is MainAction.ShareClipboard,
|
||||
is MainAction.ShareFullContent -> {
|
||||
is MainAction.ShareFullContent,
|
||||
is MainAction.OpenSubscriptionLink -> {
|
||||
// Handled by Activity via its onAction lambda
|
||||
}
|
||||
}
|
||||
@@ -369,7 +456,13 @@ class MainViewModel(
|
||||
cacheMutex.withLock { groupDataCache.clear() }
|
||||
}
|
||||
val groups = dataSource.getSubscriptions().map {
|
||||
GroupMapItem(id = it.guid, remarks = it.subscription.remarks)
|
||||
GroupMapItem(
|
||||
id = it.guid,
|
||||
remarks = it.subscription.remarks,
|
||||
usageInfo = it.subscription.usageInfo?.let { info ->
|
||||
SubscriptionInfoParser.withWebPageFallback(info, it.subscription.url)
|
||||
},
|
||||
)
|
||||
}
|
||||
val selectedGroup = resolveSelectedGroup(groups)
|
||||
val validIds = groups.mapTo(HashSet()) { it.id }
|
||||
@@ -448,8 +541,7 @@ class MainViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun importConfigViaSub() {
|
||||
val subId = uiState.value.selectedGroupId
|
||||
private fun importConfigViaSub(subId: String) {
|
||||
launchLoading {
|
||||
withContext(ioDispatcher) {
|
||||
try {
|
||||
@@ -464,7 +556,13 @@ class MainViewModel(
|
||||
toast(R.string.title_update_subscription_no_subscription)
|
||||
|
||||
result.successCount > 0 && result.failureCount + result.skipCount == 0 ->
|
||||
toast(dataSource.getString(R.string.title_update_config_count, result.configCount))
|
||||
toast(
|
||||
getQuantityString(
|
||||
R.plurals.title_update_config_count,
|
||||
result.configCount,
|
||||
result.configCount,
|
||||
)
|
||||
)
|
||||
|
||||
else ->
|
||||
toast(dataSource.getString(R.string.title_update_subscription_result, result.configCount, result.successCount, result.failureCount, result.skipCount))
|
||||
@@ -718,22 +816,30 @@ class MainViewModel(
|
||||
|
||||
// ---------- Testing ----------
|
||||
fun cancelAllPing() {
|
||||
bulkTestJob?.cancel()
|
||||
bulkTestJob = null
|
||||
testRequests.cancelBulk()
|
||||
testRequests.invalidateCurrent()
|
||||
cancelPendingTestResults()
|
||||
resetTestStatus()
|
||||
dataSource.cancelAllPing()
|
||||
testingGroupId = null
|
||||
}
|
||||
|
||||
private fun resetTestStatus() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isTesting = false,
|
||||
status = if (it.isRunning) MainStatus.Connected else MainStatus.Disconnected
|
||||
isTesting = testRequests.isTesting,
|
||||
status = if (testRequests.isTesting) MainStatus.Testing
|
||||
else if (it.isRunning) MainStatus.Connected else MainStatus.Disconnected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun testAllRealPing(onlyTcp: Boolean = false) {
|
||||
dataSource.cancelAllPing()
|
||||
cancelAllPing()
|
||||
val groupId = uiState.value.selectedGroupId
|
||||
val servers = currentServers()
|
||||
if (servers.isEmpty()) {
|
||||
_uiState.update { it.copy(isTesting = false) }
|
||||
return
|
||||
}
|
||||
val serverGuids = servers.map { it.guid }
|
||||
@@ -749,42 +855,54 @@ class MainViewModel(
|
||||
}
|
||||
)
|
||||
}
|
||||
testingGroupId = groupId
|
||||
val request = testRequests.beginBulk(groupId)
|
||||
val message = TestServiceMessage(
|
||||
key = AppConfig.MSG_MEASURE_CONFIG_START,
|
||||
subscriptionId = groupId,
|
||||
serverGuids = if (keywordFilter.isNotEmpty()) serverGuids else emptyList(),
|
||||
onlyTcp = onlyTcp
|
||||
)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isTesting = true,
|
||||
status = MainStatus.Testing
|
||||
)
|
||||
}
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
dataSource.clearAllTestDelayResults(serverGuids)
|
||||
cacheMutex.withLock { groupDataCache.remove(groupId) }
|
||||
dataSource.sendMsg2TestService(
|
||||
TestServiceMessage(
|
||||
key = AppConfig.MSG_MEASURE_CONFIG_START,
|
||||
subscriptionId = groupId,
|
||||
serverGuids = if (keywordFilter.isNotEmpty()) serverGuids else emptyList(),
|
||||
onlyTcp = onlyTcp
|
||||
)
|
||||
)
|
||||
bulkTestJob = viewModelScope.launch {
|
||||
withContext(ioDispatcher) {
|
||||
dataSource.clearAllTestDelayResults(serverGuids)
|
||||
val resetGuids = serverGuids.toHashSet()
|
||||
cacheMutex.withLock {
|
||||
groupDataCache[groupId]?.let { cached ->
|
||||
groupDataCache[groupId] = cached.map { server ->
|
||||
if (server.guid !in resetGuids || server.testDelayMillis == 0L) server
|
||||
else server.copy(testDelayMillis = 0L)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSource.sendMsg2TestService(message, request.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun testCurrentServerRealPing() {
|
||||
_uiState.update { it.copy(status = MainStatus.Testing) }
|
||||
dataSource.testCurrentServerRealPing()
|
||||
private fun cancelPendingTestResults() {
|
||||
testResultFlushJob?.cancel()
|
||||
testResultFlushJob = null
|
||||
pendingTestResults.clear()
|
||||
}
|
||||
|
||||
private fun onTestsFinished() {
|
||||
fun testCurrentServerRealPing() {
|
||||
if (!uiState.value.isRunning) return
|
||||
val requestId = testRequests.beginCurrent()
|
||||
_uiState.update { it.copy(isTesting = true, status = MainStatus.Testing) }
|
||||
dataSource.testCurrentServerRealPing(requestId)
|
||||
}
|
||||
|
||||
private fun onTestsFinished(requestId: String) {
|
||||
if (testRequests.completeBulk(requestId) == null) return
|
||||
resetTestStatus()
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
cacheMutex.withLock { groupDataCache.clear() }
|
||||
testingGroupId = null
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isTesting = false,
|
||||
status = if (it.isRunning) MainStatus.Connected else MainStatus.Disconnected
|
||||
)
|
||||
}
|
||||
reloadAllGroups(_uiState.value.groups.map { it.id })
|
||||
}
|
||||
}
|
||||
@@ -812,10 +930,12 @@ class MainViewModel(
|
||||
|
||||
// ---------- Running state ----------
|
||||
private fun updateRunningState(running: Boolean, clearTestingText: Boolean = true) {
|
||||
if (!running || clearTestingText) testRequests.invalidateCurrent()
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
isRunning = running,
|
||||
status = if (!clearTestingText && state.isTesting) state.status
|
||||
isTesting = testRequests.isTesting,
|
||||
status = if (!clearTestingText && state.isRunning == running) state.status
|
||||
else if (running) MainStatus.Connected else MainStatus.Disconnected
|
||||
)
|
||||
}
|
||||
@@ -842,4 +962,8 @@ class MainViewModel(
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TEST_RESULT_FLUSH_INTERVAL_MS = 500L
|
||||
}
|
||||
}
|
||||
@@ -41,13 +41,13 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.AppInfo
|
||||
import com.v2ray.ang.extension.toastInfo
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.ui.base.BaseComponentActivity
|
||||
import com.v2ray.ang.ui.compose.AppDivider
|
||||
import com.v2ray.ang.ui.compose.AppDropdownMenuItems
|
||||
import com.v2ray.ang.ui.compose.AppListItem
|
||||
import com.v2ray.ang.ui.compose.AppTopBar
|
||||
import com.v2ray.ang.ui.compose.ConfirmDialog
|
||||
import com.v2ray.ang.ui.compose.ItemDivider
|
||||
import com.v2ray.ang.ui.compose.NavigationBarsBottomPadding
|
||||
import com.v2ray.ang.ui.compose.verticalScrollbar
|
||||
@@ -87,9 +87,6 @@ class PerAppProxyActivity : BaseComponentActivity() {
|
||||
onBackClick = { finish() },
|
||||
onPerAppProxyChanged = { viewModel.setPerAppProxyEnabled(it) },
|
||||
onBypassAppsChanged = { viewModel.setBypassAppsEnabled(it) },
|
||||
onInfoClick = {
|
||||
toastInfo(R.string.summary_pref_per_app_proxy)
|
||||
},
|
||||
onToggleApp = { viewModel.toggle(it) },
|
||||
onSearch = { viewModel.filterApps(it) },
|
||||
onSelectAll = { viewModel.selectAll() },
|
||||
@@ -118,7 +115,6 @@ fun PerAppProxyScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onPerAppProxyChanged: (Boolean) -> Unit,
|
||||
onBypassAppsChanged: (Boolean) -> Unit,
|
||||
onInfoClick: () -> Unit,
|
||||
onToggleApp: (String) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onSelectAll: () -> Unit,
|
||||
@@ -130,6 +126,8 @@ fun PerAppProxyScreen(
|
||||
var showSearch by rememberSaveable { mutableStateOf(false) }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
var showInfoDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val onInfoClick = { showInfoDialog = true }
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -281,4 +279,13 @@ fun PerAppProxyScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showInfoDialog) {
|
||||
ConfirmDialog(
|
||||
message = stringResource(R.string.summary_pref_per_app_proxy),
|
||||
dismissText = null,
|
||||
onConfirm = {},
|
||||
onDismiss = { showInfoDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.entities.RulesetItem
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.ui.apppicker.AppPickerActivity
|
||||
@@ -86,7 +85,6 @@ class RoutingEditActivity : BaseComponentActivity() {
|
||||
|
||||
private fun saveServer(rulesetItem: RulesetItem): Boolean {
|
||||
if (rulesetItem.remarks.isNullOrEmpty()) {
|
||||
toast(R.string.sub_setting_remarks)
|
||||
return false
|
||||
}
|
||||
if (position < 0 && rulesetItem.id.isEmpty()) {
|
||||
@@ -124,6 +122,7 @@ fun RoutingEditScreen(
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
var remarks by rememberSaveable { mutableStateOf(initial?.remarks ?: "") }
|
||||
var isRemarksError by rememberSaveable { mutableStateOf(false) }
|
||||
var locked by rememberSaveable { mutableStateOf(initial?.locked == true) }
|
||||
var domain by rememberSaveable { mutableStateOf(initial?.domain?.joinToString(",") ?: "") }
|
||||
var ip by rememberSaveable { mutableStateOf(initial?.ip?.joinToString(",") ?: "") }
|
||||
@@ -193,7 +192,15 @@ fun RoutingEditScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { onSave(buildRuleset()) }) {
|
||||
IconButton(onClick = {
|
||||
val remarksErr = remarks.isBlank()
|
||||
isRemarksError = remarksErr
|
||||
|
||||
val hasError = remarksErr
|
||||
if (!hasError) {
|
||||
onSave(buildRuleset())
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_fab_check),
|
||||
contentDescription = stringResource(R.string.acc_save)
|
||||
@@ -216,7 +223,8 @@ fun RoutingEditScreen(
|
||||
FormTextField(
|
||||
label = stringResource(R.string.sub_setting_remarks),
|
||||
value = remarks,
|
||||
onValueChange = { remarks = it }
|
||||
onValueChange = { remarks = it },
|
||||
isError = isRemarksError
|
||||
)
|
||||
SettingsSwitchItem(
|
||||
title = stringResource(R.string.routing_settings_locked),
|
||||
@@ -309,6 +317,7 @@ fun RoutingEditScreen(
|
||||
if (showDeleteConfirm) {
|
||||
DeleteConfirmDialog(
|
||||
message = stringResource(R.string.confirm_delete_routing_rule),
|
||||
itemName = initial?.remarks.orEmpty(),
|
||||
onConfirm = onDelete,
|
||||
onDismiss = { showDeleteConfirm = false }
|
||||
)
|
||||
|
||||
@@ -105,20 +105,23 @@ abstract class BaseServerActivity : BaseComponentActivity() {
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FormTextField(
|
||||
stringResource(R.string.server_lab_remarks),
|
||||
state.remarks,
|
||||
{ state.remarks = it }
|
||||
label = stringResource(R.string.server_lab_remarks),
|
||||
value = state.remarks,
|
||||
onValueChange = { state.remarks = it },
|
||||
isError = state.isRemarksError
|
||||
)
|
||||
FormTextField(
|
||||
stringResource(R.string.server_lab_address),
|
||||
state.address,
|
||||
{ state.address = it }
|
||||
label = stringResource(R.string.server_lab_address),
|
||||
value = state.address,
|
||||
onValueChange = { state.address = it },
|
||||
isError = state.isAddressError
|
||||
)
|
||||
FormTextField(
|
||||
stringResource(R.string.server_lab_port),
|
||||
state.port,
|
||||
{ state.port = it },
|
||||
keyboardType = KeyboardType.Number
|
||||
label = stringResource(R.string.server_lab_port),
|
||||
value = state.port,
|
||||
onValueChange = { state.port = it },
|
||||
keyboardType = KeyboardType.Number,
|
||||
isError = state.isPortError
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -365,33 +368,27 @@ abstract class BaseServerActivity : BaseComponentActivity() {
|
||||
}
|
||||
|
||||
protected fun validateBasicConfig(state: ServerUiState): Boolean {
|
||||
if (state.remarks.isBlank()) {
|
||||
toast(R.string.server_lab_remarks)
|
||||
return false
|
||||
}
|
||||
if (state.address.isBlank()) {
|
||||
toast(R.string.server_lab_address)
|
||||
return false
|
||||
}
|
||||
if (
|
||||
state.configType != EConfigType.HYSTERIA2 &&
|
||||
(state.port.toIntOrNull() ?: 0) <= 0
|
||||
) {
|
||||
toast(R.string.server_lab_port)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
val remarksErr = state.remarks.isBlank()
|
||||
val addressErr = state.address.isBlank()
|
||||
val portErr = state.configType != EConfigType.HYSTERIA2 && (state.port.toIntOrNull() ?: 0) <= 0
|
||||
|
||||
state.isRemarksError = remarksErr
|
||||
state.isAddressError = addressErr
|
||||
state.isPortError = portErr
|
||||
|
||||
val hasError = remarksErr || addressErr || portErr
|
||||
return !hasError
|
||||
}
|
||||
|
||||
protected open fun validateProtocolConfig(config: ProfileItem): Boolean = true
|
||||
|
||||
protected open fun validateCommonConfig(config: ProfileItem): Boolean {
|
||||
protected open fun validateCommonConfig(state: ServerUiState, config: ProfileItem): Boolean {
|
||||
|
||||
if (config.password.isNullOrBlank()) {
|
||||
state.isPasswordError = true
|
||||
if (config.configType == EConfigType.VMESS ||
|
||||
config.configType == EConfigType.VLESS
|
||||
) {
|
||||
toast(R.string.server_lab_id)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -399,7 +396,6 @@ abstract class BaseServerActivity : BaseComponentActivity() {
|
||||
config.configType == EConfigType.SHADOWSOCKS ||
|
||||
config.configType == EConfigType.HYSTERIA2
|
||||
) {
|
||||
toast(R.string.server_lab_id3)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -425,7 +421,7 @@ abstract class BaseServerActivity : BaseComponentActivity() {
|
||||
protected fun saveServer(state: ServerUiState): Boolean {
|
||||
if (!validateBasicConfig(state)) return false
|
||||
val config = state.toProfileItem(initialConfig)
|
||||
if (!validateCommonConfig(config)) return false
|
||||
if (!validateCommonConfig(state, config)) return false
|
||||
if (!validateProtocolConfig(config)) return false
|
||||
|
||||
config.description = AngConfigManager.generateDescription(config)
|
||||
@@ -491,6 +487,7 @@ abstract class BaseServerActivity : BaseComponentActivity() {
|
||||
if (showDeleteDialog) {
|
||||
DeleteConfirmDialog(
|
||||
message = stringResource(R.string.confirm_delete_profile),
|
||||
itemName = initialConfig.remarks,
|
||||
onConfirm = {
|
||||
showDeleteDialog = false
|
||||
deleteServer(editGuid)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.v2ray.ang.ui.server
|
||||
|
||||
/** Removes one draft member and its row key together, resolving its current position at confirmation. */
|
||||
internal fun withoutProxyChainMember(
|
||||
members: List<String>,
|
||||
memberKeys: List<String>,
|
||||
memberKey: String,
|
||||
): Pair<List<String>, List<String>> {
|
||||
val index = memberKeys.indexOf(memberKey)
|
||||
if (index < 0) return members to memberKeys
|
||||
|
||||
return members.toMutableList().also { it.removeAt(index) } to
|
||||
memberKeys.toMutableList().also { it.removeAt(index) }
|
||||
}
|
||||
@@ -1,526 +0,0 @@
|
||||
package com.v2ray.ang.ui.server
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.v2ray.ang.AppConfig.DEFAULT_PORT
|
||||
import com.v2ray.ang.AppConfig.REALITY
|
||||
import com.v2ray.ang.AppConfig.TLS
|
||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.AngConfigManager
|
||||
import com.v2ray.ang.handler.CertificateFingerprintManager
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.ui.base.BaseComponentActivity
|
||||
import com.v2ray.ang.ui.compose.AppTopBar
|
||||
import com.v2ray.ang.ui.compose.DeleteConfirmDialog
|
||||
import com.v2ray.ang.ui.compose.FormDropdownField
|
||||
import com.v2ray.ang.ui.compose.FormTextField
|
||||
import com.v2ray.ang.ui.compose.SettingsSwitchItem
|
||||
import com.v2ray.ang.ui.compose.verticalScrollbar
|
||||
import com.v2ray.ang.util.JsonUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ServerActivity : BaseComponentActivity() {
|
||||
|
||||
private val editGuid by lazy { intent.getStringExtra("guid").orEmpty() }
|
||||
private val isRunning by lazy {
|
||||
intent.getBooleanExtra("isRunning", false)
|
||||
&& editGuid.isNotEmpty()
|
||||
&& editGuid == MmkvManager.getSelectServer()
|
||||
}
|
||||
private val createConfigType by lazy {
|
||||
EConfigType.fromInt(intent.getIntExtra("createConfigType", EConfigType.VMESS.value))
|
||||
?: EConfigType.VMESS
|
||||
}
|
||||
private val subscriptionId by lazy { intent.getStringExtra("subscriptionId") }
|
||||
|
||||
private lateinit var initialConfig: ProfileItem
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val existingConfig = MmkvManager.decodeServerConfig(editGuid)
|
||||
initialConfig = existingConfig ?: ProfileItem.create(createConfigType)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent() {
|
||||
ServerScreen(
|
||||
guid = editGuid,
|
||||
configType = initialConfig.configType,
|
||||
initialConfig = initialConfig,
|
||||
isRunning = isRunning,
|
||||
onBackClick = { finish() },
|
||||
onSave = { saveServer(it) },
|
||||
onDelete = { deleteServer(editGuid, isRunning) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveServer(config: ProfileItem): Boolean {
|
||||
if (config.remarks.isBlank()) {
|
||||
toast(R.string.server_lab_remarks)
|
||||
return false
|
||||
}
|
||||
|
||||
if (config.server.isNullOrBlank()) {
|
||||
toast(R.string.server_lab_address)
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
config.configType != EConfigType.HYSTERIA2 &&
|
||||
(config.serverPort?.toIntOrNull() ?: 0) <= 0
|
||||
) {
|
||||
toast(R.string.server_lab_port)
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
config.configType != EConfigType.SOCKS &&
|
||||
config.configType != EConfigType.HTTP &&
|
||||
config.password.isNullOrBlank()
|
||||
) {
|
||||
val message = when (config.configType) {
|
||||
EConfigType.TROJAN,
|
||||
EConfigType.SHADOWSOCKS,
|
||||
EConfigType.HYSTERIA2 -> R.string.server_lab_id3
|
||||
|
||||
else -> R.string.server_lab_id
|
||||
}
|
||||
toast(message)
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
config.configType == EConfigType.TROJAN &&
|
||||
config.security.isNullOrBlank()
|
||||
) {
|
||||
toast(R.string.server_lab_stream_security)
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
!config.xhttpExtra.isNullOrBlank() &&
|
||||
JsonUtil.parseString(config.xhttpExtra) == null
|
||||
) {
|
||||
toast(R.string.server_lab_xhttp_extra)
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
!config.finalMask.isNullOrBlank() &&
|
||||
JsonUtil.parseString(config.finalMask) == null
|
||||
) {
|
||||
toast(R.string.server_lab_final_mask)
|
||||
return false
|
||||
}
|
||||
|
||||
config.description =
|
||||
AngConfigManager.generateDescription(config)
|
||||
|
||||
if (
|
||||
config.subscriptionId.isEmpty() &&
|
||||
!subscriptionId.isNullOrEmpty()
|
||||
) {
|
||||
config.subscriptionId = subscriptionId.orEmpty()
|
||||
}
|
||||
|
||||
val savedGuid = MmkvManager.encodeServerConfig(
|
||||
editGuid,
|
||||
config
|
||||
)
|
||||
|
||||
toastSuccess(R.string.toast_success)
|
||||
|
||||
ProfileEditorResult.run {
|
||||
finishSaved(
|
||||
guid = savedGuid,
|
||||
restartService = isRunning
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun deleteServer(
|
||||
guid: String,
|
||||
isRunning: Boolean
|
||||
) {
|
||||
if (
|
||||
guid.isEmpty() ||
|
||||
guid == MmkvManager.getSelectServer()
|
||||
) {
|
||||
toast(R.string.toast_action_not_allowed)
|
||||
return
|
||||
}
|
||||
|
||||
MmkvManager.removeServer(guid)
|
||||
|
||||
ProfileEditorResult.run {
|
||||
finishDeleted(guid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ServerScreen(
|
||||
guid: String,
|
||||
configType: EConfigType,
|
||||
initialConfig: ProfileItem,
|
||||
isRunning: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
onSave: (ProfileItem) -> Boolean,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val securityOptions = stringArrayResource(R.array.securitys).toList()
|
||||
val ssSecurityOptions = stringArrayResource(R.array.ss_securitys).toList()
|
||||
val flowOptions = stringArrayResource(R.array.flows).toList()
|
||||
val networkOptions = stringArrayResource(R.array.networks).toList()
|
||||
val tcpHeaderOptions = stringArrayResource(R.array.header_type_tcp).toList()
|
||||
val kcpHeaderOptions = stringArrayResource(R.array.header_type_kcp_and_quic).toList()
|
||||
val grpcModeOptions = stringArrayResource(R.array.mode_type_grpc).toList()
|
||||
val xhttpModeOptions = stringArrayResource(R.array.xhttp_mode).toList()
|
||||
val streamSecurityOptions = stringArrayResource(R.array.streamsecurityxs).toList()
|
||||
val uTlsOptions = stringArrayResource(R.array.streamsecurity_utls).toList()
|
||||
val alpnOptions = stringArrayResource(R.array.streamsecurity_alpn).toList()
|
||||
val browserDialerOptions = stringArrayResource(R.array.browser_dialer_mode_value).toList()
|
||||
|
||||
var remarks by rememberSaveable { mutableStateOf(initialConfig.remarks) }
|
||||
var address by rememberSaveable { mutableStateOf(initialConfig.server ?: "") }
|
||||
var port by rememberSaveable { mutableStateOf(initialConfig.serverPort ?: DEFAULT_PORT.toString()) }
|
||||
var password by rememberSaveable { mutableStateOf(initialConfig.password ?: "") }
|
||||
var method by rememberSaveable { mutableStateOf(initialConfig.method ?: "") }
|
||||
var flow by rememberSaveable { mutableStateOf(initialConfig.flow ?: "") }
|
||||
var encryption by rememberSaveable { mutableStateOf(initialConfig.method ?: "") }
|
||||
var username by rememberSaveable { mutableStateOf(initialConfig.username ?: "") }
|
||||
var secretKey by rememberSaveable { mutableStateOf(initialConfig.secretKey ?: "") }
|
||||
var publicKey by rememberSaveable { mutableStateOf(initialConfig.publicKey ?: "") }
|
||||
var preSharedKey by rememberSaveable { mutableStateOf(initialConfig.preSharedKey ?: "") }
|
||||
var reserved by rememberSaveable { mutableStateOf(initialConfig.reserved ?: "0,0,0") }
|
||||
var localAddress by rememberSaveable { mutableStateOf(initialConfig.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4) }
|
||||
var mtu by rememberSaveable { mutableStateOf(initialConfig.mtu?.toString() ?: WIREGUARD_LOCAL_MTU) }
|
||||
var obfsPassword by rememberSaveable { mutableStateOf(initialConfig.obfsPassword ?: "") }
|
||||
var portHopping by rememberSaveable { mutableStateOf(initialConfig.portHopping ?: "") }
|
||||
var portHoppingInterval by rememberSaveable { mutableStateOf(initialConfig.portHoppingInterval ?: "") }
|
||||
var bandwidthDown by rememberSaveable { mutableStateOf(initialConfig.bandwidthDown ?: "") }
|
||||
var bandwidthUp by rememberSaveable { mutableStateOf(initialConfig.bandwidthUp ?: "") }
|
||||
var network by rememberSaveable { mutableStateOf(initialConfig.network ?: NetworkType.TCP.type) }
|
||||
var headerType by rememberSaveable { mutableStateOf(initialConfig.headerType ?: "none") }
|
||||
var host by rememberSaveable { mutableStateOf(initialConfig.host ?: "") }
|
||||
var path by rememberSaveable { mutableStateOf(initialConfig.path ?: "") }
|
||||
var seed by rememberSaveable { mutableStateOf(initialConfig.seed ?: "") }
|
||||
var quicSecurity by rememberSaveable { mutableStateOf(initialConfig.quicSecurity ?: "") }
|
||||
var quicKey by rememberSaveable { mutableStateOf(initialConfig.quicKey ?: "") }
|
||||
var mode by rememberSaveable { mutableStateOf(initialConfig.mode ?: "") }
|
||||
var serviceName by rememberSaveable { mutableStateOf(initialConfig.serviceName ?: "") }
|
||||
var authority by rememberSaveable { mutableStateOf(initialConfig.authority ?: "") }
|
||||
var xhttpMode by rememberSaveable { mutableStateOf(initialConfig.xhttpMode ?: "") }
|
||||
var xhttpExtra by rememberSaveable { mutableStateOf(initialConfig.xhttpExtra ?: "") }
|
||||
var finalMask by rememberSaveable { mutableStateOf(initialConfig.finalMask ?: "") }
|
||||
var kcpMtu by rememberSaveable { mutableStateOf(initialConfig.kcpMtu?.toString() ?: "") }
|
||||
var kcpTti by rememberSaveable { mutableStateOf(initialConfig.kcpTti?.toString() ?: "") }
|
||||
var browserDialerMode by rememberSaveable { mutableStateOf(initialConfig.browserDialerMode ?: "") }
|
||||
var streamSecurity by rememberSaveable { mutableStateOf(initialConfig.security ?: "") }
|
||||
var sni by rememberSaveable { mutableStateOf(initialConfig.sni ?: "") }
|
||||
var allowInsecure by rememberSaveable { mutableStateOf(initialConfig.insecure == true) }
|
||||
var fingerPrint by rememberSaveable { mutableStateOf(initialConfig.fingerPrint ?: "") }
|
||||
var alpn by rememberSaveable { mutableStateOf(initialConfig.alpn ?: "") }
|
||||
var publicKeyReality by rememberSaveable { mutableStateOf(initialConfig.publicKey ?: "") }
|
||||
var shortId by rememberSaveable { mutableStateOf(initialConfig.shortId ?: "") }
|
||||
var spiderX by rememberSaveable { mutableStateOf(initialConfig.spiderX ?: "") }
|
||||
var mldsa65Verify by rememberSaveable { mutableStateOf(initialConfig.mldsa65Verify ?: "") }
|
||||
var echConfigList by rememberSaveable { mutableStateOf(initialConfig.echConfigList ?: "") }
|
||||
var verifyPeerCertByName by rememberSaveable { mutableStateOf(initialConfig.verifyPeerCertByName ?: "") }
|
||||
var pinnedCA256 by rememberSaveable { mutableStateOf(initialConfig.pinnedCA256 ?: "") }
|
||||
|
||||
val isVmess = configType == EConfigType.VMESS
|
||||
val isVless = configType == EConfigType.VLESS
|
||||
val isShadowsocks = configType == EConfigType.SHADOWSOCKS
|
||||
val isSocksOrHttp = configType == EConfigType.SOCKS || configType == EConfigType.HTTP
|
||||
val isTrojan = configType == EConfigType.TROJAN
|
||||
val isWireguard = configType == EConfigType.WIREGUARD
|
||||
val isHysteria2 = configType == EConfigType.HYSTERIA2
|
||||
|
||||
var showDeleteDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var isFetchingCert by rememberSaveable { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
fun buildProfileItem(): ProfileItem = initialConfig.copy(
|
||||
remarks = remarks,
|
||||
server = address,
|
||||
serverPort = port,
|
||||
password = password,
|
||||
method = when {
|
||||
isVmess || isShadowsocks -> method
|
||||
isVless -> encryption
|
||||
else -> null
|
||||
},
|
||||
flow = if (isVless) flow else null,
|
||||
username = if (isSocksOrHttp) username else null,
|
||||
secretKey = if (isWireguard) secretKey else null,
|
||||
publicKey = when {
|
||||
isWireguard -> publicKey
|
||||
streamSecurity == REALITY -> publicKeyReality
|
||||
else -> null
|
||||
},
|
||||
preSharedKey = if (isWireguard) preSharedKey else null,
|
||||
reserved = if (isWireguard) reserved else null,
|
||||
localAddress = if (isWireguard) localAddress else null,
|
||||
mtu = if (isWireguard) mtu.toIntOrNull() else null,
|
||||
obfsPassword = if (isHysteria2) obfsPassword else null,
|
||||
portHopping = if (isHysteria2) portHopping else null,
|
||||
portHoppingInterval = if (isHysteria2) portHoppingInterval else null,
|
||||
bandwidthDown = if (isHysteria2) bandwidthDown else null,
|
||||
bandwidthUp = if (isHysteria2) bandwidthUp else null,
|
||||
network = network,
|
||||
headerType = headerType,
|
||||
host = host,
|
||||
path = path,
|
||||
seed = seed,
|
||||
quicSecurity = quicSecurity,
|
||||
quicKey = quicKey,
|
||||
mode = mode,
|
||||
serviceName = serviceName,
|
||||
authority = authority,
|
||||
xhttpMode = xhttpMode,
|
||||
xhttpExtra = xhttpExtra.nullIfBlank(),
|
||||
finalMask = finalMask.nullIfBlank(),
|
||||
kcpMtu = kcpMtu.toIntOrNull(),
|
||||
kcpTti = kcpTti.toIntOrNull(),
|
||||
browserDialerMode = if (network in listOf(NetworkType.WS.type, NetworkType.XHTTP.type)) browserDialerMode.nullIfBlank() else null,
|
||||
security = streamSecurity,
|
||||
sni = sni,
|
||||
insecure = allowInsecure,
|
||||
fingerPrint = fingerPrint,
|
||||
alpn = alpn,
|
||||
shortId = shortId,
|
||||
spiderX = spiderX,
|
||||
mldsa65Verify = mldsa65Verify,
|
||||
echConfigList = echConfigList,
|
||||
verifyPeerCertByName = verifyPeerCertByName,
|
||||
pinnedCA256 = pinnedCA256
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0),
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = configType.toString(),
|
||||
onBackClick = onBackClick,
|
||||
actions = {
|
||||
if (guid.isNotEmpty() && !isRunning) {
|
||||
IconButton(onClick = { showDeleteDialog = true }) {
|
||||
Icon(painterResource(R.drawable.ic_delete_24dp), stringResource(R.string.acc_delete))
|
||||
}
|
||||
}
|
||||
IconButton(onClick = {
|
||||
onSave(buildProfileItem())
|
||||
}) {
|
||||
Icon(painterResource(R.drawable.ic_fab_check), stringResource(R.string.acc_save))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.consumeWindowInsets(innerPadding)
|
||||
.imePadding()
|
||||
.verticalScrollbar(listState),
|
||||
contentPadding = PaddingValues(bottom = 36.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item { FormTextField(stringResource(R.string.server_lab_remarks), remarks, { remarks = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_address), address, { address = it }) }
|
||||
if (configType != EConfigType.HYSTERIA2) item { FormTextField(stringResource(R.string.server_lab_port), port, { port = it }, keyboardType = KeyboardType.Number) }
|
||||
when {
|
||||
isVmess || isTrojan || isShadowsocks || isHysteria2 -> item {
|
||||
FormTextField(
|
||||
stringResource(
|
||||
when {
|
||||
isTrojan || isShadowsocks || isHysteria2 -> R.string.server_lab_id3
|
||||
else -> R.string.server_lab_id
|
||||
}
|
||||
), password, { password = it })
|
||||
}
|
||||
|
||||
isVless -> {
|
||||
item { FormTextField(stringResource(R.string.server_lab_id), password, { password = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_encryption), encryption, { encryption = it }) }
|
||||
item { FormDropdownField(stringResource(R.string.server_lab_flow), flow, flowOptions, { flow = it }) }
|
||||
}
|
||||
|
||||
isSocksOrHttp -> {
|
||||
item { FormTextField(stringResource(R.string.server_lab_security4), username, { username = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_id4), password, { password = it }) }
|
||||
}
|
||||
}
|
||||
if (isVmess) item { FormDropdownField(stringResource(R.string.server_lab_security), method, securityOptions, { method = it }) }
|
||||
if (isShadowsocks) item { FormDropdownField(stringResource(R.string.server_lab_security), method, ssSecurityOptions, { method = it }) }
|
||||
if (isWireguard) {
|
||||
item { FormTextField(stringResource(R.string.server_lab_secret_key), secretKey, { secretKey = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_public_key), publicKey, { publicKey = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_preshared_key), preSharedKey, { preSharedKey = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_reserved), reserved, { reserved = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_local_address), localAddress, { localAddress = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_local_mtu), mtu, { mtu = it }, keyboardType = KeyboardType.Number) }
|
||||
}
|
||||
if (isHysteria2) {
|
||||
item { FormTextField(stringResource(R.string.server_obfs_password), obfsPassword, { obfsPassword = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_port_hop), portHopping, { portHopping = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_port_hop_interval), portHoppingInterval, { portHoppingInterval = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_bandwidth_down), bandwidthDown, { bandwidthDown = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_bandwidth_up), bandwidthUp, { bandwidthUp = it }) }
|
||||
}
|
||||
item { FormDropdownField(stringResource(R.string.server_lab_network), network, networkOptions, { network = it }) }
|
||||
val headerOptions = when (network) {
|
||||
NetworkType.TCP.type -> tcpHeaderOptions
|
||||
NetworkType.KCP.type -> kcpHeaderOptions
|
||||
NetworkType.GRPC.type -> grpcModeOptions
|
||||
NetworkType.XHTTP.type -> xhttpModeOptions
|
||||
else -> listOf("---")
|
||||
}
|
||||
if (headerOptions.size > 1) {
|
||||
item {
|
||||
FormDropdownField(
|
||||
stringResource(
|
||||
when (network) {
|
||||
NetworkType.GRPC.type -> R.string.server_lab_mode_type; NetworkType.XHTTP.type -> R.string.server_lab_xhttp_mode; else -> R.string.server_lab_head_type
|
||||
}
|
||||
),
|
||||
headerType, headerOptions, { headerType = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
FormTextField(
|
||||
stringResource(
|
||||
when (network) {
|
||||
NetworkType.TCP.type, NetworkType.HTTP_UPGRADE.type, NetworkType.XHTTP.type, NetworkType.H2.type -> R.string.server_lab_request_host_http
|
||||
NetworkType.WS.type -> R.string.server_lab_request_host_ws
|
||||
NetworkType.GRPC.type -> R.string.server_lab_request_host_grpc
|
||||
else -> R.string.server_lab_request_host6
|
||||
}
|
||||
), host, { host = it })
|
||||
}
|
||||
item {
|
||||
FormTextField(
|
||||
stringResource(
|
||||
when (network) {
|
||||
NetworkType.KCP.type -> R.string.server_lab_path_kcp
|
||||
NetworkType.WS.type -> R.string.server_lab_path_ws
|
||||
NetworkType.HTTP_UPGRADE.type -> R.string.server_lab_path_httpupgrade
|
||||
NetworkType.XHTTP.type -> R.string.server_lab_path_xhttp
|
||||
NetworkType.H2.type -> R.string.server_lab_path_h2
|
||||
NetworkType.GRPC.type -> R.string.server_lab_path_grpc
|
||||
else -> R.string.server_lab_path
|
||||
}
|
||||
), path, { path = it })
|
||||
}
|
||||
if (network == NetworkType.XHTTP.type) {
|
||||
item { FormTextField(stringResource(R.string.server_lab_xhttp_extra), xhttpExtra, { xhttpExtra = it }) }
|
||||
}
|
||||
if (network == NetworkType.KCP.type) {
|
||||
item { FormTextField(stringResource(R.string.server_lab_kcp_mtu), kcpMtu, { kcpMtu = it }, keyboardType = KeyboardType.Number) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_kcp_tti), kcpTti, { kcpTti = it }, keyboardType = KeyboardType.Number) }
|
||||
}
|
||||
item { FormTextField(stringResource(R.string.server_lab_final_mask), finalMask, { finalMask = it }) }
|
||||
if (network == NetworkType.WS.type || network == NetworkType.XHTTP.type) {
|
||||
item { FormDropdownField(stringResource(R.string.server_lab_browser_dialer), browserDialerMode, browserDialerOptions, { browserDialerMode = it }) }
|
||||
}
|
||||
item { FormDropdownField(stringResource(R.string.server_lab_stream_security), streamSecurity, streamSecurityOptions, { streamSecurity = it }) }
|
||||
if (streamSecurity.isNotBlank()) {
|
||||
item { FormTextField(stringResource(R.string.server_lab_sni), sni, { sni = it }) }
|
||||
item { FormDropdownField(stringResource(R.string.server_lab_stream_fingerprint), fingerPrint, uTlsOptions, { fingerPrint = it }) }
|
||||
if (streamSecurity == TLS) {
|
||||
item { SettingsSwitchItem(title = stringResource(R.string.server_lab_allow_insecure), checked = allowInsecure, onCheckedChange = { allowInsecure = it }) }
|
||||
item { FormDropdownField(stringResource(R.string.server_lab_stream_alpn), alpn, alpnOptions, { alpn = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_ech_config_list), echConfigList, { echConfigList = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_verify_peer_cert_by_name), verifyPeerCertByName, { verifyPeerCertByName = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_pinned_ca256), pinnedCA256, { pinnedCA256 = it }) }
|
||||
item {
|
||||
Button(
|
||||
onClick = {
|
||||
if (address.isBlank()) {
|
||||
context.toast(R.string.server_lab_address); return@Button
|
||||
}
|
||||
if (configType != EConfigType.HYSTERIA2 && (port.toIntOrNull() ?: 0) <= 0) {
|
||||
context.toast(R.string.server_lab_port); return@Button
|
||||
}
|
||||
val temp = buildProfileItem()
|
||||
scope.launch {
|
||||
isFetchingCert = true
|
||||
try {
|
||||
val sha256 = withContext(Dispatchers.IO) { CertificateFingerprintManager.fetchForManualFill(temp) }
|
||||
if (sha256.isNullOrBlank()) context.toast(R.string.toast_fetch_cert_sha256_failed) else {
|
||||
pinnedCA256 = sha256
|
||||
context.toastSuccess(R.string.toast_fetch_cert_sha256_success)
|
||||
}
|
||||
} finally {
|
||||
isFetchingCert = false
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !isFetchingCert,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
) { Text(stringResource(R.string.pinned_ca256_action_fetch)) }
|
||||
}
|
||||
} else if (streamSecurity == REALITY) {
|
||||
item { FormTextField(stringResource(R.string.server_lab_public_key), publicKeyReality, { publicKeyReality = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_short_id), shortId, { shortId = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_spider_x), spiderX, { spiderX = it }) }
|
||||
item { FormTextField(stringResource(R.string.server_lab_mldsa65_verify), mldsa65Verify, { mldsa65Verify = it }) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showDeleteDialog) {
|
||||
DeleteConfirmDialog(
|
||||
message = stringResource(R.string.confirm_delete_profile),
|
||||
onConfirm = { showDeleteDialog = false; onDelete() },
|
||||
onDismiss = { showDeleteDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,6 @@ class ServerCustomConfigActivity : BaseComponentActivity() {
|
||||
content: String
|
||||
): Boolean {
|
||||
if (remarks.isBlank()) {
|
||||
toast(R.string.server_lab_remarks)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -207,6 +206,7 @@ fun ServerCustomConfigScreen(
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
var remarks by rememberSaveable { mutableStateOf(initialRemarks) }
|
||||
var isRemarksError by rememberSaveable { mutableStateOf(false) }
|
||||
val textFieldState = rememberTextFieldState(initialText = initialContent)
|
||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||
val showDelete = editGuid.isNotEmpty() && !isRunning
|
||||
@@ -325,7 +325,15 @@ fun ServerCustomConfigScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { onSave(remarks, textFieldState.text.toString()) }) {
|
||||
IconButton(onClick = {
|
||||
val remarksErr = remarks.isBlank()
|
||||
isRemarksError = remarksErr
|
||||
|
||||
val hasError = remarksErr
|
||||
if (!hasError) {
|
||||
onSave(remarks, textFieldState.text.toString())
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_fab_check),
|
||||
contentDescription = stringResource(R.string.acc_save)
|
||||
@@ -345,7 +353,8 @@ fun ServerCustomConfigScreen(
|
||||
FormTextField(
|
||||
label = stringResource(R.string.server_lab_remarks),
|
||||
value = remarks,
|
||||
onValueChange = { remarks = it }
|
||||
onValueChange = { remarks = it },
|
||||
isError = isRemarksError
|
||||
)
|
||||
|
||||
Box(
|
||||
@@ -486,6 +495,7 @@ fun ServerCustomConfigScreen(
|
||||
if (showDeleteConfirm) {
|
||||
DeleteConfirmDialog(
|
||||
message = stringResource(R.string.confirm_delete_profile),
|
||||
itemName = initialRemarks,
|
||||
onConfirm = {
|
||||
showDeleteConfirm = false
|
||||
onDelete()
|
||||
|
||||
@@ -116,7 +116,6 @@ class ServerGroupActivity : BaseComponentActivity() {
|
||||
fallbackTag: String,
|
||||
): Boolean {
|
||||
if (remarks.isBlank()) {
|
||||
toast(R.string.server_lab_remarks)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -227,6 +226,7 @@ fun ServerGroupScreen(
|
||||
val typeEntries = stringArrayResource(R.array.policy_group_type).toList()
|
||||
|
||||
var remarks by rememberSaveable { mutableStateOf(initialRemarks) }
|
||||
var isRemarksError by rememberSaveable { mutableStateOf(false) }
|
||||
var filter by rememberSaveable { mutableStateOf(initialFilter) }
|
||||
var typeValue by rememberSaveable { mutableStateOf(typeEntries.getOrNull(initialType).orEmpty()) }
|
||||
var subValue by rememberSaveable { mutableStateOf(subDisplay.getOrNull(initialSubIndex).orEmpty()) }
|
||||
@@ -250,9 +250,15 @@ fun ServerGroupScreen(
|
||||
}
|
||||
}
|
||||
IconButton(onClick = {
|
||||
val typeIdx = typeEntries.indexOf(typeValue).coerceAtLeast(0)
|
||||
val subIdx = subDisplay.indexOf(subValue).coerceAtLeast(0)
|
||||
onSave(remarks, filter, typeIdx, subIdx, testOutbounds, fallbackTag)
|
||||
val remarksErr = remarks.isBlank()
|
||||
isRemarksError = remarksErr
|
||||
|
||||
val hasError = remarksErr
|
||||
if (!hasError) {
|
||||
val typeIdx = typeEntries.indexOf(typeValue).coerceAtLeast(0)
|
||||
val subIdx = subDisplay.indexOf(subValue).coerceAtLeast(0)
|
||||
onSave(remarks, filter, typeIdx, subIdx, testOutbounds, fallbackTag)
|
||||
}
|
||||
}) {
|
||||
Icon(painterResource(R.drawable.ic_fab_check), contentDescription = stringResource(R.string.acc_save))
|
||||
}
|
||||
@@ -269,7 +275,12 @@ fun ServerGroupScreen(
|
||||
.padding(vertical = 8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
FormTextField(stringResource(R.string.server_lab_remarks), remarks, { remarks = it })
|
||||
FormTextField(
|
||||
label = stringResource(R.string.server_lab_remarks),
|
||||
value = remarks,
|
||||
onValueChange = { remarks = it },
|
||||
isError = isRemarksError
|
||||
)
|
||||
FormDropdownField(
|
||||
label = stringResource(R.string.title_policy_group_type),
|
||||
value = typeValue,
|
||||
@@ -306,6 +317,7 @@ fun ServerGroupScreen(
|
||||
if (showDeleteConfirm) {
|
||||
DeleteConfirmDialog(
|
||||
message = stringResource(R.string.confirm_delete_policy_group),
|
||||
itemName = initialRemarks,
|
||||
onConfirm = onDelete,
|
||||
onDismiss = { showDeleteConfirm = false }
|
||||
)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package com.v2ray.ang.ui.server
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.ui.compose.FormTextField
|
||||
import com.v2ray.ang.ui.compose.SettingsSwitchItem
|
||||
|
||||
@@ -18,7 +16,6 @@ class ServerHysteria2Activity : BaseServerActivity() {
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent() {
|
||||
val scope = rememberCoroutineScope()
|
||||
val uiState = rememberSaveable(saver = ServerUiState.Saver) {
|
||||
ServerUiState.from(
|
||||
initialConfig = initialConfig
|
||||
@@ -39,7 +36,6 @@ class ServerHysteria2Activity : BaseServerActivity() {
|
||||
|
||||
override fun validateProtocolConfig(config: ProfileItem): Boolean {
|
||||
if (config.password.isNullOrBlank()) {
|
||||
toast(R.string.server_lab_id3)
|
||||
return false
|
||||
}
|
||||
if (config.security.isNullOrBlank()) {
|
||||
@@ -51,9 +47,10 @@ class ServerHysteria2Activity : BaseServerActivity() {
|
||||
@Composable
|
||||
private fun Hysteria2ProtocolFields(state: ServerUiState) {
|
||||
FormTextField(
|
||||
stringResource(R.string.server_lab_id3),
|
||||
state.password,
|
||||
{ state.password = it }
|
||||
label = stringResource(R.string.server_lab_id3),
|
||||
value = state.password,
|
||||
onValueChange = { state.password = it },
|
||||
isError = state.isPasswordError
|
||||
)
|
||||
FormTextField(
|
||||
stringResource(R.string.server_obfs_password),
|
||||
|
||||
@@ -100,7 +100,6 @@ class ServerProxyChainActivity : BaseComponentActivity() {
|
||||
members: List<String>
|
||||
): Boolean {
|
||||
if (remarks.isBlank()) {
|
||||
toast(R.string.server_lab_remarks)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -200,10 +199,11 @@ fun ProxyChainScreen(
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
var remarks by rememberSaveable { mutableStateOf(initialRemarks) }
|
||||
var isRemarksError by rememberSaveable { mutableStateOf(false) }
|
||||
var members by rememberSaveable { mutableStateOf(initialMembers) }
|
||||
var memberKeys by rememberSaveable { mutableStateOf(List(initialMembers.size) { UUID.randomUUID().toString() }) }
|
||||
var showProfileDeleteConfirm by remember { mutableStateOf(false) }
|
||||
var memberToDeleteIndex by rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
var memberToDeleteKey by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val showDelete = editGuid.isNotEmpty() && !isRunning
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
@@ -233,7 +233,15 @@ fun ProxyChainScreen(
|
||||
Icon(painterResource(R.drawable.ic_delete_24dp), contentDescription = stringResource(R.string.acc_delete))
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { onSave(remarks, members) }) {
|
||||
IconButton(onClick = {
|
||||
val remarksErr = remarks.isBlank()
|
||||
isRemarksError = remarksErr
|
||||
|
||||
val hasError = remarksErr
|
||||
if (!hasError) {
|
||||
onSave(remarks, members)
|
||||
}
|
||||
}) {
|
||||
Icon(painterResource(R.drawable.ic_fab_check), contentDescription = stringResource(R.string.acc_save))
|
||||
}
|
||||
}
|
||||
@@ -272,7 +280,8 @@ fun ProxyChainScreen(
|
||||
FormTextField(
|
||||
label = stringResource(R.string.server_lab_remarks),
|
||||
value = remarks,
|
||||
onValueChange = { remarks = it }
|
||||
onValueChange = { remarks = it },
|
||||
isError = isRemarksError
|
||||
)
|
||||
}
|
||||
|
||||
@@ -285,7 +294,8 @@ fun ProxyChainScreen(
|
||||
}
|
||||
|
||||
itemsIndexed(items = members, key = { index, _ -> memberKeys[index] }) { index, member ->
|
||||
ReorderableItem(reorderableState, key = memberKeys[index]) { isDragging ->
|
||||
val memberKey = memberKeys[index]
|
||||
ReorderableItem(reorderableState, key = memberKey) { isDragging ->
|
||||
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
|
||||
Surface(shadowElevation = elevation) {
|
||||
Row(
|
||||
@@ -314,10 +324,11 @@ fun ProxyChainScreen(
|
||||
)
|
||||
IconButton(onClick = {
|
||||
if (member.isBlank()) {
|
||||
members = members.toMutableList().also { it.removeAt(index) }
|
||||
memberKeys = memberKeys.toMutableList().also { it.removeAt(index) }
|
||||
val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, memberKeys, memberKey)
|
||||
members = remainingMembers
|
||||
memberKeys = remainingKeys
|
||||
} else {
|
||||
memberToDeleteIndex = index
|
||||
memberToDeleteKey = memberKey
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
@@ -335,19 +346,22 @@ fun ProxyChainScreen(
|
||||
if (showProfileDeleteConfirm) {
|
||||
DeleteConfirmDialog(
|
||||
message = stringResource(R.string.confirm_delete_profile),
|
||||
itemName = initialRemarks,
|
||||
onConfirm = { showProfileDeleteConfirm = false; onDelete() },
|
||||
onDismiss = { showProfileDeleteConfirm = false }
|
||||
)
|
||||
}
|
||||
memberToDeleteIndex?.let { index ->
|
||||
memberToDeleteKey?.let { memberKey ->
|
||||
DeleteConfirmDialog(
|
||||
message = stringResource(R.string.confirm_delete_proxy_chain_member),
|
||||
itemName = members.getOrNull(memberKeys.indexOf(memberKey)).orEmpty(),
|
||||
onConfirm = {
|
||||
members = members.toMutableList().also { it.removeAt(index) }
|
||||
memberKeys = memberKeys.toMutableList().also { it.removeAt(index) }
|
||||
memberToDeleteIndex = null
|
||||
val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, memberKeys, memberKey)
|
||||
members = remainingMembers
|
||||
memberKeys = remainingKeys
|
||||
memberToDeleteKey = null
|
||||
},
|
||||
onDismiss = { memberToDeleteIndex = null }
|
||||
onDismiss = { memberToDeleteKey = null }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -52,9 +52,10 @@ class ServerShadowsocksActivity : BaseServerActivity() {
|
||||
methodOptions: List<String>
|
||||
) {
|
||||
FormTextField(
|
||||
stringResource(R.string.server_lab_id3),
|
||||
state.password,
|
||||
{ state.password = it }
|
||||
label = stringResource(R.string.server_lab_id3),
|
||||
value = state.password,
|
||||
onValueChange = { state.password = it },
|
||||
isError = state.isPasswordError
|
||||
)
|
||||
FormDropdownField(
|
||||
stringResource(R.string.server_lab_security),
|
||||
|
||||
@@ -7,7 +7,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.ui.compose.FormTextField
|
||||
|
||||
class ServerTrojanActivity : BaseServerActivity() {
|
||||
@@ -44,11 +43,9 @@ class ServerTrojanActivity : BaseServerActivity() {
|
||||
|
||||
override fun validateProtocolConfig(config: ProfileItem): Boolean {
|
||||
if (config.password.isNullOrBlank()) {
|
||||
toast(R.string.server_lab_id3)
|
||||
return false
|
||||
}
|
||||
if (config.security.isNullOrBlank()) {
|
||||
toast(R.string.server_lab_stream_security)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -57,9 +54,10 @@ class ServerTrojanActivity : BaseServerActivity() {
|
||||
@Composable
|
||||
private fun TrojanProtocolFields(state: ServerUiState) {
|
||||
FormTextField(
|
||||
stringResource(R.string.server_lab_id3),
|
||||
state.password,
|
||||
{ state.password = it }
|
||||
label = stringResource(R.string.server_lab_id3),
|
||||
value = state.password,
|
||||
onValueChange = { state.password = it },
|
||||
isError = state.isPasswordError
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.v2ray.ang.AppConfig.DEFAULT_PORT
|
||||
import com.v2ray.ang.AppConfig.REALITY
|
||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
|
||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_REMOTE_DNS
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.enums.NetworkType
|
||||
@@ -30,6 +31,7 @@ class ServerUiState(
|
||||
reserved: String = "0,0,0",
|
||||
localAddress: String = WIREGUARD_LOCAL_ADDRESS_V4,
|
||||
mtu: String = WIREGUARD_LOCAL_MTU,
|
||||
remoteDNS: String = WIREGUARD_LOCAL_REMOTE_DNS,
|
||||
obfsPassword: String = "",
|
||||
portHopping: String = "",
|
||||
portHoppingInterval: String = "",
|
||||
@@ -78,6 +80,7 @@ class ServerUiState(
|
||||
var reserved by mutableStateOf(reserved)
|
||||
var localAddress by mutableStateOf(localAddress)
|
||||
var mtu by mutableStateOf(mtu)
|
||||
var remoteDNS by mutableStateOf(remoteDNS)
|
||||
var obfsPassword by mutableStateOf(obfsPassword)
|
||||
var portHopping by mutableStateOf(portHopping)
|
||||
var portHoppingInterval by mutableStateOf(portHoppingInterval)
|
||||
@@ -111,6 +114,11 @@ class ServerUiState(
|
||||
var pinnedCA256 by mutableStateOf(pinnedCA256)
|
||||
var isFetchingCert by mutableStateOf(isFetchingCert)
|
||||
|
||||
var isRemarksError by mutableStateOf(false)
|
||||
var isAddressError by mutableStateOf(false)
|
||||
var isPortError by mutableStateOf(false)
|
||||
var isPasswordError by mutableStateOf(false)
|
||||
|
||||
fun toProfileItem(initialConfig: ProfileItem): ProfileItem {
|
||||
val isVmess = configType == EConfigType.VMESS
|
||||
val isVless = configType == EConfigType.VLESS
|
||||
@@ -142,6 +150,7 @@ class ServerUiState(
|
||||
reserved = if (isWireguard) reserved else null,
|
||||
localAddress = if (isWireguard) localAddress else null,
|
||||
mtu = if (isWireguard) mtu.toIntOrNull() else null,
|
||||
remoteDNS = if (isWireguard) remoteDNS else null,
|
||||
obfsPassword = if (isHysteria2) obfsPassword else null,
|
||||
portHopping = if (isHysteria2) portHopping else null,
|
||||
portHoppingInterval = if (isHysteria2) portHoppingInterval else null,
|
||||
@@ -199,6 +208,7 @@ class ServerUiState(
|
||||
reserved = initialConfig.reserved ?: "0,0,0",
|
||||
localAddress = initialConfig.localAddress ?: WIREGUARD_LOCAL_ADDRESS_V4,
|
||||
mtu = initialConfig.mtu?.toString() ?: WIREGUARD_LOCAL_MTU,
|
||||
remoteDNS = initialConfig.remoteDNS ?: WIREGUARD_LOCAL_REMOTE_DNS,
|
||||
obfsPassword = initialConfig.obfsPassword ?: "",
|
||||
portHopping = initialConfig.portHopping ?: "",
|
||||
portHoppingInterval = initialConfig.portHoppingInterval ?: "",
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.ui.compose.FormDropdownField
|
||||
import com.v2ray.ang.ui.compose.FormTextField
|
||||
|
||||
@@ -47,7 +46,6 @@ class ServerVlessActivity : BaseServerActivity() {
|
||||
|
||||
override fun validateProtocolConfig(config: ProfileItem): Boolean {
|
||||
if (config.password.isNullOrBlank()) {
|
||||
toast(R.string.server_lab_id)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -59,9 +57,10 @@ class ServerVlessActivity : BaseServerActivity() {
|
||||
flowOptions: List<String>
|
||||
) {
|
||||
FormTextField(
|
||||
stringResource(R.string.server_lab_id),
|
||||
state.password,
|
||||
{ state.password = it }
|
||||
label = stringResource(R.string.server_lab_id),
|
||||
value = state.password,
|
||||
onValueChange = { state.password = it },
|
||||
isError = state.isPasswordError
|
||||
)
|
||||
FormTextField(
|
||||
stringResource(R.string.server_lab_encryption),
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.entities.ProfileItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.ui.compose.FormDropdownField
|
||||
import com.v2ray.ang.ui.compose.FormTextField
|
||||
|
||||
@@ -47,7 +46,6 @@ class ServerVmessActivity : BaseServerActivity() {
|
||||
|
||||
override fun validateProtocolConfig(config: ProfileItem): Boolean {
|
||||
if (config.password.isNullOrBlank()) {
|
||||
toast(R.string.server_lab_id)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -59,9 +57,10 @@ class ServerVmessActivity : BaseServerActivity() {
|
||||
methodOptions: List<String>
|
||||
) {
|
||||
FormTextField(
|
||||
stringResource(R.string.server_lab_id),
|
||||
state.password,
|
||||
{ state.password = it }
|
||||
label = stringResource(R.string.server_lab_id),
|
||||
value = state.password,
|
||||
onValueChange = { state.password = it },
|
||||
isError = state.isPasswordError
|
||||
)
|
||||
FormDropdownField(
|
||||
stringResource(R.string.server_lab_security),
|
||||
|
||||
@@ -73,6 +73,12 @@ class ServerWireguardActivity : BaseServerActivity() {
|
||||
state.finalMask,
|
||||
{ state.finalMask = it }
|
||||
)
|
||||
|
||||
FormTextField(
|
||||
stringResource(R.string.server_lab_remote_dns),
|
||||
state.remoteDNS,
|
||||
{ state.remoteDNS = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.entities.SubscriptionItem
|
||||
import com.v2ray.ang.enums.EConfigType
|
||||
import com.v2ray.ang.extension.toLongEx
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsChangeManager
|
||||
@@ -78,26 +77,19 @@ class SubEditActivity : BaseComponentActivity() {
|
||||
}
|
||||
|
||||
private fun saveServer(subItem: SubscriptionItem): Boolean {
|
||||
|
||||
if (TextUtils.isEmpty(subItem.remarks)) {
|
||||
toast(R.string.sub_setting_remarks)
|
||||
return false
|
||||
}
|
||||
if (subItem.url.isNotEmpty()) {
|
||||
if (!Utils.isValidUrl(subItem.url)) {
|
||||
toast(R.string.toast_invalid_url)
|
||||
return false
|
||||
}
|
||||
if (!Utils.isValidSubUrl(subItem.url)) {
|
||||
toast(R.string.toast_insecure_url_protocol)
|
||||
if (!subItem.allowInsecureUrl) {
|
||||
return false
|
||||
}
|
||||
if (!Utils.isValidSubUrl(subItem.url) && !subItem.allowInsecureUrl) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (subItem.autoUpdate && subItem.updateInterval < AppConfig.SUBSCRIPTION_MIN_INTERVAL_MINUTES) {
|
||||
toast(R.string.toast_invalid_update_interval)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -132,13 +124,16 @@ fun SubEditScreen(
|
||||
) {
|
||||
//val context = LocalContext.current
|
||||
var remarks by rememberSaveable { mutableStateOf(initial.remarks.orEmpty()) }
|
||||
var isRemarksError by rememberSaveable { mutableStateOf(false) }
|
||||
var url by rememberSaveable { mutableStateOf(initial.url.orEmpty()) }
|
||||
var isUrlError by rememberSaveable { mutableStateOf(false) }
|
||||
var userAgent by rememberSaveable { mutableStateOf(initial.userAgent.orEmpty()) }
|
||||
var requestHeaders by rememberSaveable { mutableStateOf(initial.requestHeaders.orEmpty()) }
|
||||
var filter by rememberSaveable { mutableStateOf(initial.filter ?: "") }
|
||||
var enabled by rememberSaveable { mutableStateOf(initial.enabled) }
|
||||
var autoUpdate by rememberSaveable { mutableStateOf(initial.autoUpdate) }
|
||||
var updateInterval by rememberSaveable { mutableStateOf(initial.updateInterval.toString()) }
|
||||
var isUpdateIntervalError by rememberSaveable { mutableStateOf(false) }
|
||||
var allowInsecureUrl by rememberSaveable { mutableStateOf(initial.allowInsecureUrl) }
|
||||
var prevProfile by rememberSaveable { mutableStateOf(initial.prevProfile ?: "") }
|
||||
var nextProfile by rememberSaveable { mutableStateOf(initial.nextProfile ?: "") }
|
||||
@@ -177,7 +172,22 @@ fun SubEditScreen(
|
||||
Icon(painterResource(R.drawable.ic_delete_24dp), contentDescription = stringResource(R.string.acc_delete))
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { buildSubItem()?.let { onSave(it) } }) {
|
||||
IconButton(onClick = {
|
||||
val remarksErr = remarks.isBlank()
|
||||
val urlErr = url.isNotEmpty() && (
|
||||
!Utils.isValidUrl(url) || (!Utils.isValidSubUrl(url) && !allowInsecureUrl)
|
||||
)
|
||||
val intervalErr = autoUpdate && updateInterval.toLongEx() < AppConfig.SUBSCRIPTION_MIN_INTERVAL_MINUTES
|
||||
|
||||
isRemarksError = remarksErr
|
||||
isUrlError = urlErr
|
||||
isUpdateIntervalError = intervalErr
|
||||
|
||||
val hasError = remarksErr || urlErr || intervalErr
|
||||
if (!hasError) {
|
||||
onSave(buildSubItem())
|
||||
}
|
||||
}) {
|
||||
Icon(painterResource(R.drawable.ic_fab_check), contentDescription = stringResource(R.string.acc_save))
|
||||
}
|
||||
}
|
||||
@@ -195,8 +205,19 @@ fun SubEditScreen(
|
||||
.padding(vertical = 8.dp)
|
||||
.padding(bottom = 36.dp)
|
||||
) {
|
||||
FormTextField(stringResource(R.string.sub_setting_remarks), remarks, { remarks = it })
|
||||
FormTextField(stringResource(R.string.sub_setting_url), url, { url = it })
|
||||
FormTextField(
|
||||
label = stringResource(R.string.sub_setting_remarks),
|
||||
value = remarks,
|
||||
onValueChange = { remarks = it },
|
||||
isError = isRemarksError
|
||||
)
|
||||
FormTextField(
|
||||
label = stringResource(R.string.sub_setting_url),
|
||||
value = url,
|
||||
onValueChange = { url = it },
|
||||
isError = isUrlError,
|
||||
supportingText = if (isUrlError) stringResource(R.string.toast_invalid_url) else null
|
||||
)
|
||||
FormTextField(stringResource(R.string.sub_setting_user_agent), userAgent, { userAgent = it })
|
||||
FormTextField(stringResource(R.string.sub_setting_request_headers), requestHeaders, { requestHeaders = it })
|
||||
FormTextField(stringResource(R.string.sub_setting_filter), filter, { filter = it })
|
||||
@@ -213,8 +234,12 @@ fun SubEditScreen(
|
||||
)
|
||||
|
||||
FormTextField(
|
||||
stringResource(R.string.title_pref_auto_update_interval),
|
||||
updateInterval, { updateInterval = it }, keyboardType = KeyboardType.Number
|
||||
label = stringResource(R.string.title_pref_auto_update_interval),
|
||||
value = updateInterval,
|
||||
onValueChange = { updateInterval = it },
|
||||
keyboardType = KeyboardType.Number,
|
||||
isError = isUpdateIntervalError,
|
||||
supportingText = if (isUpdateIntervalError) stringResource(R.string.toast_invalid_update_interval) else null
|
||||
)
|
||||
|
||||
SettingsSwitchItem(
|
||||
@@ -247,6 +272,7 @@ fun SubEditScreen(
|
||||
if (showDeleteConfirm) {
|
||||
DeleteConfirmDialog(
|
||||
message = stringResource(R.string.confirm_delete_subscription_group),
|
||||
itemName = initial.remarks,
|
||||
onConfirm = onDelete,
|
||||
onDismiss = { showDeleteConfirm = false }
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.v2ray.ang.ui.subscription
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.StringRes
|
||||
@@ -53,7 +52,6 @@ import com.v2ray.ang.ui.compose.ReorderableListItem
|
||||
import com.v2ray.ang.ui.compose.SelectListDialog
|
||||
import com.v2ray.ang.ui.compose.SettingsSwitchItem
|
||||
import com.v2ray.ang.ui.compose.verticalScrollbar
|
||||
import com.v2ray.ang.util.QRCodeDecoder
|
||||
import com.v2ray.ang.util.Utils
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
@@ -63,6 +61,8 @@ private enum class SubscriptionShareAction(@StringRes val labelRes: Int) {
|
||||
Clipboard(R.string.share_subscription_clipboard)
|
||||
}
|
||||
|
||||
private data class SubscriptionDeleteTarget(val guid: String, val name: String)
|
||||
|
||||
class SubSettingActivity : BaseComponentActivity() {
|
||||
private val viewModel: SubscriptionsViewModel by viewModels()
|
||||
|
||||
@@ -83,7 +83,7 @@ class SubSettingActivity : BaseComponentActivity() {
|
||||
startActivity(Intent(this, SubEditActivity::class.java).putExtra("subId", subId))
|
||||
},
|
||||
onRemoveSub = { subId -> removeSub(subId) },
|
||||
onShareQRCode = { url -> QRCodeDecoder.createQRCode(url) },
|
||||
onShareQRCode = viewModel::shareQRCode,
|
||||
onShareClipboard = { url ->
|
||||
Utils.setClipboard(this, url)
|
||||
toast(getString(R.string.toast_success))
|
||||
@@ -110,16 +110,16 @@ fun SubSettingScreen(
|
||||
onSubUpdate: () -> Unit,
|
||||
onEditSub: (String) -> Unit,
|
||||
onRemoveSub: (String) -> Unit,
|
||||
onShareQRCode: (String) -> Bitmap?,
|
||||
onShareQRCode: (String) -> Unit,
|
||||
onShareClipboard: (String) -> Unit
|
||||
) {
|
||||
val subscriptions by viewModel.subsFlow.collectAsStateWithLifecycle()
|
||||
var showUpdateDialog by remember { mutableStateOf(false) }
|
||||
var removeTarget by remember { mutableStateOf<String?>(null) }
|
||||
var removeTarget by remember { mutableStateOf<SubscriptionDeleteTarget?>(null) }
|
||||
val confirmRemove = MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE, false)
|
||||
|
||||
var shareTarget by remember { mutableStateOf<Pair<String, String>?>(null) }
|
||||
var showQRCodeBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
val qrCodeBitmap by viewModel.qrCode.collectAsStateWithLifecycle()
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val reorderableState = rememberReorderableLazyListState(lazyListState) { from, to ->
|
||||
@@ -214,7 +214,7 @@ fun SubSettingScreen(
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
if (confirmRemove) removeTarget = subCache.guid
|
||||
if (confirmRemove) removeTarget = SubscriptionDeleteTarget(subCache.guid, subCache.subscription.remarks)
|
||||
else onRemoveSub(subCache.guid)
|
||||
}) {
|
||||
Icon(
|
||||
@@ -254,7 +254,7 @@ fun SubSettingScreen(
|
||||
onSelected = { action ->
|
||||
shareTarget = null
|
||||
when (action) {
|
||||
SubscriptionShareAction.QRCode -> showQRCodeBitmap = onShareQRCode(url)
|
||||
SubscriptionShareAction.QRCode -> onShareQRCode(url)
|
||||
SubscriptionShareAction.Clipboard -> onShareClipboard(url)
|
||||
}
|
||||
},
|
||||
@@ -263,19 +263,20 @@ fun SubSettingScreen(
|
||||
}
|
||||
|
||||
// QR Code Dialog
|
||||
if (showQRCodeBitmap != null) {
|
||||
if (qrCodeBitmap != null) {
|
||||
QRCodeDialog(
|
||||
bitmap = showQRCodeBitmap,
|
||||
onDismiss = { showQRCodeBitmap = null }
|
||||
bitmap = qrCodeBitmap,
|
||||
onDismiss = viewModel::dismissQRCode
|
||||
)
|
||||
}
|
||||
|
||||
if (removeTarget != null) {
|
||||
removeTarget?.let { target ->
|
||||
DeleteConfirmDialog(
|
||||
message = stringResource(R.string.confirm_delete_subscription_group),
|
||||
itemName = target.name,
|
||||
onConfirm = {
|
||||
onRemoveSub(removeTarget!!)
|
||||
removeTarget = null
|
||||
onRemoveSub(target.guid)
|
||||
},
|
||||
onDismiss = { removeTarget = null }
|
||||
)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.v2ray.ang.ui.subscription
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.Bitmap
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.SubscriptionUpdateMessage
|
||||
@@ -14,14 +16,20 @@ import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.helper.MessageHelper
|
||||
import com.v2ray.ang.ui.base.BaseViewModel
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.QRCodeDecoder
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SubscriptionsViewModel(application: Application) : BaseViewModel(application) {
|
||||
private var qrCodeJob: Job? = null
|
||||
private val _qrCode = MutableStateFlow<Bitmap?>(null)
|
||||
internal val qrCode = _qrCode.asStateFlow()
|
||||
private val subscriptions: MutableList<SubscriptionCache> =
|
||||
MmkvManager.decodeSubscriptions().toMutableList()
|
||||
|
||||
@@ -55,6 +63,21 @@ class SubscriptionsViewModel(application: Application) : BaseViewModel(applicati
|
||||
_subsFlow.value = subscriptions.toList()
|
||||
}
|
||||
|
||||
internal fun shareQRCode(url: String) {
|
||||
dismissQRCode()
|
||||
qrCodeJob = viewModelScope.launch {
|
||||
val bitmap = withContext(Dispatchers.Default) { QRCodeDecoder.createQRCode(url) }
|
||||
if (bitmap == null) toastError(R.string.toast_failure)
|
||||
_qrCode.value = bitmap
|
||||
}
|
||||
}
|
||||
|
||||
internal fun dismissQRCode() {
|
||||
qrCodeJob?.cancel()
|
||||
qrCodeJob = null
|
||||
_qrCode.value = null
|
||||
}
|
||||
|
||||
fun move(fromPosition: Int, toPosition: Int) {
|
||||
if (subscriptions.moveItem(fromPosition, toPosition)) {
|
||||
MmkvManager.encodeSubsList(subscriptions.mapTo(mutableListOf()) { it.guid })
|
||||
@@ -87,11 +110,20 @@ class SubscriptionsViewModel(application: Application) : BaseViewModel(applicati
|
||||
toast(R.string.title_update_subscription_no_subscription)
|
||||
|
||||
result.successCount > 0 && result.failureCount + result.skipCount == 0 ->
|
||||
toast(getString(R.string.title_update_config_count, result.configCount))
|
||||
toast(
|
||||
getQuantityString(
|
||||
R.plurals.title_update_config_count,
|
||||
result.configCount,
|
||||
result.configCount,
|
||||
)
|
||||
)
|
||||
|
||||
else ->
|
||||
toast(getString(R.string.title_update_subscription_result, result.configCount, result.successCount, result.failureCount, result.skipCount))
|
||||
}
|
||||
if (result.configCount > 0) {
|
||||
SettingsChangeManager.makeSetupGroupTab()
|
||||
}
|
||||
reload()
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.format.DateFormat
|
||||
import android.text.format.Formatter
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -34,6 +36,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -45,7 +49,6 @@ import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.entities.AssetUrlCache
|
||||
import com.v2ray.ang.dto.entities.AssetUrlItem
|
||||
import com.v2ray.ang.extension.toTrafficString
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastError
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
@@ -66,7 +69,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
private enum class AddAssetMenuAction(@StringRes val labelRes: Int) {
|
||||
@@ -219,7 +222,13 @@ class UserAssetActivity : HelperBaseComponentActivity() {
|
||||
viewModel.downloadGeoFiles(extDir, httpPort, proxyUsername, proxyPassword)
|
||||
}
|
||||
if (result.successCount > 0) {
|
||||
toast(getString(R.string.title_update_asset_count, result.successCount))
|
||||
toast(
|
||||
resources.getQuantityString(
|
||||
R.plurals.title_update_asset_count,
|
||||
result.successCount,
|
||||
result.successCount,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
toast(getString(R.string.toast_failure))
|
||||
}
|
||||
@@ -336,7 +345,8 @@ internal fun UserAssetScreen(
|
||||
|
||||
deleteTarget?.let { asset ->
|
||||
DeleteConfirmDialog(
|
||||
message = stringResource(R.string.confirm_delete_asset_file, asset.name),
|
||||
message = stringResource(R.string.confirm_delete_asset_file),
|
||||
itemName = asset.name,
|
||||
onConfirm = {
|
||||
deleteTarget = null
|
||||
onRemoveAsset(asset.guid, asset.name)
|
||||
@@ -353,11 +363,14 @@ private fun UserAssetItem(
|
||||
onEdit: () -> Unit,
|
||||
onDeleteClick: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val locale = LocalConfiguration.current.locales[0]
|
||||
val propertiesText = if (fileMetadata != null) {
|
||||
remember(fileMetadata) {
|
||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
||||
"${fileMetadata.length.toTrafficString()} • ${dateFormat.format(Date(fileMetadata.lastModified))}"
|
||||
}
|
||||
val fileSize = Formatter.formatFileSize(context, fileMetadata.length)
|
||||
val skeleton = if (DateFormat.is24HourFormat(context)) "yMMMdHm" else "yMMMdhm"
|
||||
val formattedDate = SimpleDateFormat(DateFormat.getBestDateTimePattern(locale, skeleton), locale)
|
||||
.format(Date(fileMetadata.lastModified))
|
||||
"$fileSize • $formattedDate"
|
||||
} else {
|
||||
stringResource(R.string.msg_file_not_found)
|
||||
}
|
||||
|
||||
@@ -4,15 +4,19 @@ import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -81,6 +85,15 @@ class UserAssetUrlActivity : BaseComponentActivity() {
|
||||
}
|
||||
|
||||
private fun saveServer(remarks: String, url: String): Boolean {
|
||||
val assetList = MmkvManager.decodeAssetUrls()
|
||||
if (assetList.any { it.assetUrl.remarks == remarks && it.guid != editAssetId }) {
|
||||
toast(R.string.msg_remark_is_duplicate)
|
||||
return false
|
||||
}
|
||||
if (TextUtils.isEmpty(remarks) || TextUtils.isEmpty(url)) {
|
||||
return false
|
||||
}
|
||||
|
||||
var assetItem = MmkvManager.decodeAsset(editAssetId)
|
||||
var assetId = editAssetId
|
||||
if (assetItem != null) {
|
||||
@@ -100,20 +113,6 @@ class UserAssetUrlActivity : BaseComponentActivity() {
|
||||
assetItem.remarks = remarks
|
||||
assetItem.url = url
|
||||
|
||||
val assetList = MmkvManager.decodeAssetUrls()
|
||||
if (assetList.any { it.assetUrl.remarks == assetItem.remarks && it.guid != assetId }) {
|
||||
toast(R.string.msg_remark_is_duplicate)
|
||||
return false
|
||||
}
|
||||
if (TextUtils.isEmpty(assetItem.remarks)) {
|
||||
toast(R.string.sub_setting_remarks)
|
||||
return false
|
||||
}
|
||||
if (TextUtils.isEmpty(assetItem.url)) {
|
||||
toast(R.string.title_url)
|
||||
return false
|
||||
}
|
||||
|
||||
MmkvManager.encodeAsset(assetId, assetItem)
|
||||
toastSuccess(R.string.toast_success)
|
||||
finish()
|
||||
@@ -138,9 +137,11 @@ fun UserAssetUrlScreen(
|
||||
onSave: (String, String) -> Boolean,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
var remarks by remember { mutableStateOf(initialRemarks) }
|
||||
var url by remember { mutableStateOf(initialUrl) }
|
||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||
var remarks by rememberSaveable(editAssetId, initialRemarks) { mutableStateOf(initialRemarks) }
|
||||
var isRemarksError by rememberSaveable(editAssetId) { mutableStateOf(false) }
|
||||
var url by rememberSaveable(editAssetId, initialUrl) { mutableStateOf(initialUrl) }
|
||||
var isUrlError by rememberSaveable(editAssetId) { mutableStateOf(false) }
|
||||
var showDeleteConfirm by rememberSaveable(editAssetId) { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0),
|
||||
@@ -157,7 +158,18 @@ fun UserAssetUrlScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { onSave(remarks, url) }) {
|
||||
IconButton(onClick = {
|
||||
val remarksErr = remarks.isBlank()
|
||||
val urlErr = url.isBlank()
|
||||
|
||||
isRemarksError = remarksErr
|
||||
isUrlError = urlErr
|
||||
|
||||
val hasError = remarksErr || urlErr
|
||||
if (!hasError) {
|
||||
onSave(remarks, url)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_fab_check),
|
||||
contentDescription = stringResource(R.string.acc_save)
|
||||
@@ -171,17 +183,22 @@ fun UserAssetUrlScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.consumeWindowInsets(innerPadding)
|
||||
.imePadding()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
FormTextField(
|
||||
label = stringResource(R.string.sub_setting_remarks),
|
||||
value = remarks,
|
||||
onValueChange = { remarks = it }
|
||||
onValueChange = { remarks = it },
|
||||
isError = isRemarksError
|
||||
)
|
||||
FormTextField(
|
||||
label = stringResource(R.string.title_url),
|
||||
value = url,
|
||||
onValueChange = { url = it }
|
||||
onValueChange = { url = it },
|
||||
isError = isUrlError
|
||||
)
|
||||
NavigationBarsSpacer()
|
||||
}
|
||||
@@ -190,6 +207,7 @@ fun UserAssetUrlScreen(
|
||||
if (showDeleteConfirm) {
|
||||
DeleteConfirmDialog(
|
||||
message = stringResource(R.string.confirm_delete_asset_source),
|
||||
itemName = initialRemarks,
|
||||
onConfirm = onDelete,
|
||||
onDismiss = { showDeleteConfirm = false }
|
||||
)
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.ui.base.BaseViewModel
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import com.v2ray.ang.util.LogUtil
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.ensureActive
|
||||
@@ -50,7 +49,7 @@ class UserAssetViewModel(application: Application) : BaseViewModel(application)
|
||||
}.also { reloadJob = it }
|
||||
}
|
||||
|
||||
private fun buildAssetList(
|
||||
internal fun buildAssetList(
|
||||
decodedAssets: List<AssetUrlCache>?,
|
||||
geoFilesSource: String
|
||||
): List<AssetUrlCache> {
|
||||
@@ -59,7 +58,8 @@ class UserAssetViewModel(application: Application) : BaseViewModel(application)
|
||||
.filter { geoFile -> savedAssets.none { it.assetUrl.remarks == geoFile } }
|
||||
.map {
|
||||
AssetUrlCache(
|
||||
Utils.getUuid(),
|
||||
// Built-in rows have no persisted GUID; keep their UI identity across reloads.
|
||||
"builtin:$it",
|
||||
AssetUrlItem(
|
||||
it,
|
||||
String.format(AppConfig.GITHUB_DOWNLOAD_URL, geoFilesSource).concatUrl(it),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.v2ray.ang.util
|
||||
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.fetch.DrawableResult
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.request.Options
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.decode.DataSource
|
||||
import coil3.fetch.FetchResult
|
||||
import coil3.fetch.Fetcher
|
||||
import coil3.fetch.ImageFetchResult
|
||||
import coil3.request.Options
|
||||
import com.v2ray.ang.AppConfig
|
||||
|
||||
class AppIconFetcher(
|
||||
@@ -25,8 +26,8 @@ class AppIconFetcher(
|
||||
null
|
||||
} ?: return null
|
||||
|
||||
return DrawableResult(
|
||||
drawable = drawable,
|
||||
return ImageFetchResult(
|
||||
image = drawable.asImage(),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.v2ray.ang.AppConfig.LOOPBACK
|
||||
import com.v2ray.ang.BuildConfig
|
||||
import com.v2ray.ang.dto.UrlContentRequest
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.File
|
||||
@@ -139,11 +140,15 @@ object HttpUtil {
|
||||
* @param url The URL to fetch content from.
|
||||
* @param timeout The timeout value in milliseconds.
|
||||
* @param httpPort The HTTP port to use.
|
||||
* @param onResponseHeaders Receives the headers of the successful response that produced the returned content.
|
||||
* @return The content of the URL as a string.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun getUrlContentWithUserAgent(request: UrlContentRequest): String {
|
||||
fun getUrlContentWithUserAgent(
|
||||
request: UrlContentRequest,
|
||||
onResponseHeaders: ((Headers) -> Unit)? = null
|
||||
): String {
|
||||
var currentUrl = request.url
|
||||
var redirects = 0
|
||||
val maxRedirects = 3
|
||||
@@ -193,6 +198,7 @@ object HttpUtil {
|
||||
}
|
||||
|
||||
response.isSuccessful -> {
|
||||
onResponseHeaders?.invoke(response.headers)
|
||||
return response.body?.string() ?: ""
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.google.gson.JsonParser
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
import com.google.gson.ToNumberPolicy
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.v2ray.ang.AppConfig
|
||||
import java.lang.reflect.Type
|
||||
@@ -64,12 +65,15 @@ object JsonUtil {
|
||||
val gsonPre = GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.disableHtmlEscaping()
|
||||
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
|
||||
.registerTypeAdapter( // custom serializer is needed here since JSON by default parse number as Double, core will fail to start
|
||||
object : TypeToken<Double>() {}.type,
|
||||
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? ->
|
||||
JsonPrimitive(
|
||||
src?.toInt()
|
||||
)
|
||||
when (src) {
|
||||
null -> null
|
||||
src.toLong().toDouble() -> JsonPrimitive(src.toLong())
|
||||
else -> JsonPrimitive(src)
|
||||
}
|
||||
}
|
||||
)
|
||||
.create()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
|
||||
</vector>
|
||||
@@ -1,10 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="20dp" />
|
||||
<padding
|
||||
android:bottom="0dp"
|
||||
android:left="0dp"
|
||||
android:right="0dp"
|
||||
android:top="0dp" />
|
||||
<solid android:color="#f97910" />
|
||||
</shape>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="23dp" />
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:endColor="#EA580C"
|
||||
android:startColor="#FB923C"
|
||||
android:type="linear" />
|
||||
<stroke
|
||||
android:width="1.5dp"
|
||||
android:color="#FFEDD5" />
|
||||
</shape>
|
||||
@@ -1,10 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="20dp" />
|
||||
<padding
|
||||
android:bottom="0dp"
|
||||
android:left="0dp"
|
||||
android:right="0dp"
|
||||
android:top="0dp" />
|
||||
<solid android:color="#9C9C9C" />
|
||||
</shape>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="23dp" />
|
||||
<solid android:color="#A0374151" />
|
||||
<stroke
|
||||
android:width="1.5dp"
|
||||
android:color="#40FFFFFF" />
|
||||
</shape>
|
||||
@@ -5,27 +5,38 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:padding="2dp">
|
||||
|
||||
<LinearLayout
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_background"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center">
|
||||
android:layout_width="46dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/ic_rounded_corner_inactive">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_switch"
|
||||
android:layout_width="45dp"
|
||||
android:layout_height="45dp"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:importantForAccessibility="no"
|
||||
android:padding="@dimen/padding_spacing_dp16"
|
||||
android:src="@drawable/ic_stat_name" />
|
||||
</LinearLayout>
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_play_24dp" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:shadowColor="#C0000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="1"
|
||||
android:shadowRadius="4"
|
||||
android:text="@string/app_name"
|
||||
android:textAppearance="@android:style/TextAppearance.Small"
|
||||
android:textColor="@android:color/white" />
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
@@ -14,6 +14,9 @@
|
||||
<string name="pull_down_to_refresh">اسحب لأسفل للتحديث!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_channel_service">خدمة v2rayNG</string>
|
||||
<string name="notification_channel_subscription_updates">تحديثات الاشتراكات</string>
|
||||
<string name="notification_channel_connection_checks">فحوص الاتصال</string>
|
||||
<string name="notification_action_stop_v2ray">إيقاف</string>
|
||||
<string name="toast_permission_denied">تعذر الحصول على الإذن</string>
|
||||
<string name="toast_permission_denied_notification">تعذر الحصول على إذن الإشعارات</string>
|
||||
@@ -50,7 +53,7 @@
|
||||
<string name="confirm_delete_proxy_chain_member">هل أنت متأكد من أنك تريد حذف هذا العضو من سلسلة الوكلاء؟</string>
|
||||
<string name="confirm_delete_routing_rule">هل أنت متأكد من أنك تريد حذف قاعدة التوجيه هذه؟</string>
|
||||
<string name="confirm_delete_subscription_group">هل أنت متأكد من أنك تريد حذف مجموعة الاشتراك هذه؟</string>
|
||||
<string name="confirm_delete_asset_file">هل أنت متأكد من أنك تريد حذف ملف الأصول \"%1$s\"؟</string>
|
||||
<string name="confirm_delete_asset_file">هل أنت متأكد من أنك تريد حذف ملف الأصول هذا؟</string>
|
||||
<string name="confirm_delete_asset_source">هل أنت متأكد من أنك تريد حذف مصدر الأصول هذا؟</string>
|
||||
<string name="server_lab_remarks">ملاحظات</string>
|
||||
<string name="server_lab_address">العنوان</string>
|
||||
@@ -100,6 +103,7 @@
|
||||
<string name="server_lab_reserved">Reserved (اختياري، مفصول بفواصل)</string>
|
||||
<string name="server_lab_local_address">العنوان المحلي (اختياري IPv4/IPv6، مفصولة بفواصل)</string>
|
||||
<string name="server_lab_local_mtu">MTU (اختياري، الافتراضي 1420)</string>
|
||||
<string name="server_lab_remote_dns">remoteDNS (اختياري، عناوين IPv4/IPv6 مفصولة بفواصل)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU (اختياري، الافتراضي 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI (اختياري، الافتراضي 50 مللي ثانية)</string>
|
||||
<string name="toast_success">نجاح</string>
|
||||
@@ -351,8 +355,22 @@
|
||||
<string name="title_del_config_count">حذف %d من التكوينات</string>
|
||||
<string name="title_import_config_count">استيراد %d من التكوينات</string>
|
||||
<string name="title_export_config_count">تصدير %d من التكوينات</string>
|
||||
<string name="title_update_config_count">تحديث %d من التكوينات</string>
|
||||
<string name="title_update_asset_count">تم تحديث %d من ملفات الأصول</string>
|
||||
<plurals name="title_update_config_count">
|
||||
<item quantity="zero">لم يتم تحديث أي تكوينات</item>
|
||||
<item quantity="one">تم تحديث تكوين واحد</item>
|
||||
<item quantity="two">تم تحديث تكوينين</item>
|
||||
<item quantity="few">تم تحديث %1$d تكوينات</item>
|
||||
<item quantity="many">تم تحديث %1$d تكوينًا</item>
|
||||
<item quantity="other">تم تحديث %1$d تكوين</item>
|
||||
</plurals>
|
||||
<plurals name="title_update_asset_count">
|
||||
<item quantity="zero">لم يتم تحديث أي ملفات أصول</item>
|
||||
<item quantity="one">تم تحديث ملف أصول واحد</item>
|
||||
<item quantity="two">تم تحديث ملفي أصول</item>
|
||||
<item quantity="few">تم تحديث %1$d ملفات أصول</item>
|
||||
<item quantity="many">تم تحديث %1$d ملف أصول</item>
|
||||
<item quantity="other">تم تحديث %1$d ملف أصول</item>
|
||||
</plurals>
|
||||
<string name="title_update_subscription_result">تم تحديث %1$d إعدادات (%2$d ناجحة، %3$d فاشلة، %4$d متخطاة)</string>
|
||||
<string name="title_update_subscription_no_subscription">لا توجد اشتراكات</string>
|
||||
<string name="toast_server_not_found_in_group">لم يُعثر على الخادم المحدد في المجموعة الحالية</string>
|
||||
@@ -550,5 +568,8 @@
|
||||
<string name="tasker_blurb_stop">إيقاف %1$s</string>
|
||||
<string name="connection_test_empty_message">لا تتوفر تفاصيل</string>
|
||||
<string name="value_unknown">غير معروف</string>
|
||||
<string name="sub_info_open_support">فتح رابط الدعم</string>
|
||||
<string name="sub_info_open_page">فتح صفحة معلومات الاشتراك</string>
|
||||
<string name="sub_info_unlimited">غير محدود</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,568 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">v2rayNG</string>
|
||||
<string name="app_widget_name">Keçid</string>
|
||||
<string name="app_tile_name">Keçid</string>
|
||||
<string name="app_tile_first_use">Bu funksiyanı istifadə etməzdən əvvəl tətbiqə profil əlavə edin.</string>
|
||||
<string name="navigation_drawer_open">Naviqasiya menyusunu açın</string>
|
||||
<string name="navigation_drawer_close">Naviqasiya menyusunu bağlayın</string>
|
||||
<string name="migration_success">Məlumatların köçürülməsi uğurla başa çatdı!</string>
|
||||
<string name="action_stop_service">Xidməti saxla</string>
|
||||
<string name="action_ok">Oldu</string>
|
||||
<string name="action_cancel">Ləğv et</string>
|
||||
<string name="migration_fail">Məlumatların köçürülməsi uğursuz oldu!</string>
|
||||
<string name="pull_down_to_refresh">Yeniləmək üçün aşağı çəkin!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_channel_service">v2rayNG xidməti</string>
|
||||
<string name="notification_channel_subscription_updates">Abunəlik yenilənmələri</string>
|
||||
<string name="notification_channel_connection_checks">Bağlantı yoxlamaları</string>
|
||||
<string name="notification_action_stop_v2ray">Dayandır</string>
|
||||
<string name="toast_permission_denied">İcazə rədd edildi</string>
|
||||
<string name="toast_permission_denied_notification">Bildiriş icazəsi rədd edildi</string>
|
||||
<string name="notification_action_more">Daha çoxu üçün toxunun</string>
|
||||
<string name="toast_services_start">Xidmət başladılır</string>
|
||||
<string name="toast_services_stop">Xidmət dayandırılır</string>
|
||||
<string name="toast_services_success">Xidmət uğurla başladıldı</string>
|
||||
<string name="toast_services_failure">Xidməti başlatmaq olmadı</string>
|
||||
|
||||
<!--ServerActivity-->
|
||||
<string name="title_server">Konfiqurasiya</string>
|
||||
<string name="menu_item_add_config">Konfiqurasiya əlavə et</string>
|
||||
<string name="menu_item_save_config">Konfiqurasiyanı saxla</string>
|
||||
<string name="menu_item_edit_config">Konfiqurasiyanı redaktə et</string>
|
||||
<string name="menu_item_del_config">Konfiqurasiyanı sil</string>
|
||||
<string name="menu_item_import_config_qrcode">QR koddan idxal et</string>
|
||||
<string name="menu_item_import_config_clipboard">Mübadilə buferindən idxal et</string>
|
||||
<string name="menu_item_import_config_local">Yerli fayldan idxal et</string>
|
||||
<string name="menu_item_import_config_policy_group">[Siyasət qrupu] əlavə et</string>
|
||||
<string name="menu_item_import_config_proxy_chain">[Proksi zənciri] əlavə et</string>
|
||||
<string name="menu_item_import_config_manually_vmess">[VMess] əlavə et</string>
|
||||
<string name="menu_item_import_config_manually_vless">[VLESS] əlavə et</string>
|
||||
<string name="menu_item_import_config_manually_ss">[Shadowsocks] əlavə et</string>
|
||||
<string name="menu_item_import_config_manually_socks">[SOCKS] əlavə et</string>
|
||||
<string name="menu_item_import_config_manually_http">[HTTP] əlavə et</string>
|
||||
<string name="menu_item_import_config_manually_trojan">[Trojan] əlavə et</string>
|
||||
<string name="menu_item_import_config_manually_wireguard">[WireGuard] əlavə et</string>
|
||||
<string name="menu_item_import_config_manually_hysteria2">[Hysteria2] əlavə et</string>
|
||||
<string name="confirm_delete_visible_profiles">Hazırda göstərilən bütün profilləri silmək istədiyinizə əminsiniz?</string>
|
||||
<string name="confirm_delete_duplicate_profiles">Hazırda göstərilən bütün təkrar profilləri silmək istədiyinizə əminsiniz?</string>
|
||||
<string name="confirm_delete_invalid_profiles">Zəhmət olmasa əvvəlcə profilləri sınaqdan keçirin. Hazırda göstərilən bütün yararsız profilləri silmək istədiyinizə əminsiniz?</string>
|
||||
<string name="confirm_delete_profile">Bu profili silmək istədiyinizə əminsiniz?</string>
|
||||
<string name="confirm_delete_policy_group">Bu siyasət qrupunu silmək istədiyinizə əminsiniz?</string>
|
||||
<string name="confirm_delete_proxy_chain_member">Zəncirin bu üzvünü silmək istədiyinizə əminsiniz?</string>
|
||||
<string name="confirm_delete_routing_rule">Bu marşrutlaşdırma qaydasını silmək istədiyinizə əminsiniz?</string>
|
||||
<string name="confirm_delete_subscription_group">Bu abunəlik qrupunu silmək istədiyinizə əminsiniz?</string>
|
||||
<string name="confirm_delete_asset_file">Bu resurs faylını silmək istədiyinizə əminsiniz?</string>
|
||||
<string name="confirm_delete_asset_source">Bu resurs mənbəyini silmək istədiyinizə əminsiniz?</string>
|
||||
<string name="server_lab_remarks">qeydlər</string>
|
||||
<string name="server_lab_address">ünvan</string>
|
||||
<string name="server_lab_port">port</string>
|
||||
<string name="server_lab_id">id</string>
|
||||
<string name="server_lab_alterid">alterId</string>
|
||||
<string name="server_lab_security">təhlükəsizlik</string>
|
||||
<string name="server_lab_network">Şəbəkə</string>
|
||||
<string name="server_lab_more_function">Nəqliyyat protokolu</string>
|
||||
<string name="server_lab_head_type">başlıq növü</string>
|
||||
<string name="server_lab_mode_type">gRPC rejimi</string>
|
||||
<string name="server_lab_request_host">host</string>
|
||||
<string name="server_lab_request_host_http">http host</string>
|
||||
<string name="server_lab_request_host_ws">ws host</string>
|
||||
<string name="server_lab_request_host_httpupgrade">httpupgrade host</string>
|
||||
<string name="server_lab_request_host_xhttp">xhttp host</string>
|
||||
<string name="server_lab_request_host_h2">h2 host</string>
|
||||
<string name="server_lab_request_host_quic">QUIC təhlükəsizliyi</string>
|
||||
<string name="server_lab_request_host_grpc">gRPC Authority</string>
|
||||
<string name="server_lab_path">yol</string>
|
||||
<string name="server_lab_path_ws">ws yolu</string>
|
||||
<string name="server_lab_path_httpupgrade">httpupgrade yolu</string>
|
||||
<string name="server_lab_path_xhttp">xhttp yolu</string>
|
||||
<string name="server_lab_path_h2">h2 yolu</string>
|
||||
<string name="server_lab_path_quic">QUIC açarı</string>
|
||||
<string name="server_lab_path_kcp">kcp seed</string>
|
||||
<string name="server_lab_path_grpc">gRPC serviceName</string>
|
||||
<string name="server_lab_stream_security" translatable="false">TLS</string>
|
||||
<string name="server_lab_stream_fingerprint">Barmaq izi</string>
|
||||
<string name="server_lab_stream_alpn" translatable="false">ALPN</string>
|
||||
<string name="server_lab_allow_insecure">allowInsecure</string>
|
||||
<string name="server_lab_sni" translatable="false">SNI</string>
|
||||
<string name="server_lab_address3">ünvan</string>
|
||||
<string name="server_lab_port3">port</string>
|
||||
<string name="server_lab_id3">şifrə</string>
|
||||
<string name="server_lab_security3">təhlükəsizlik</string>
|
||||
<string name="server_lab_id4">Şifrə (istəyə bağlı)</string>
|
||||
<string name="server_lab_security4">İstifadəçi (istəyə bağlı)</string>
|
||||
<string name="server_lab_encryption">şifrələmə</string>
|
||||
<string name="server_lab_flow">axın</string>
|
||||
<string name="server_lab_public_key">PublicKey</string>
|
||||
<string name="server_lab_preshared_key">PreSharedKey (istəyə bağlı)</string>
|
||||
<string name="server_lab_short_id">ShortId</string>
|
||||
<string name="server_lab_spider_x" translatable="false">SpiderX</string>
|
||||
<string name="server_lab_mldsa65_verify" translatable="false">mldsa65Verify</string>
|
||||
<string name="server_lab_secret_key">SecretKey</string>
|
||||
<string name="server_lab_reserved">Reserved (istəyə bağlı, vergüllə ayrılmış)</string>
|
||||
<string name="server_lab_local_address">Yerli ünvan (istəyə bağlı IPv4/IPv6, vergüllə ayrılmış)</string>
|
||||
<string name="server_lab_local_mtu">MTU (istəyə bağlı, standart 1420)</string>
|
||||
<string name="server_lab_remote_dns">remoteDNS (istəyə bağlı, vergüllə ayrılmış IPv4/IPv6)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU (istəyə bağlı, standart 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI (istəyə bağlı, standart 50 ms)</string>
|
||||
<string name="toast_success">Uğurlu</string>
|
||||
<string name="toast_failure">Uğursuz</string>
|
||||
<string name="toast_none_data">Əlçatan məlumat yoxdur</string>
|
||||
<string name="toast_incorrect_protocol">Yanlış protokol</string>
|
||||
<string name="toast_decoding_failed">Dekodlaşdırma uğursuz oldu</string>
|
||||
<string name="title_file_chooser">Konfiqurasiya seçin</string>
|
||||
<string name="toast_require_file_manager">Zəhmət olmasa fayl menecerini quraşdırın.</string>
|
||||
<string name="server_customize_config">Fərdi konfiqurasiya</string>
|
||||
<string name="toast_config_file_invalid">Yanlış konfiqurasiya</string>
|
||||
<string name="server_lab_content">Məzmun</string>
|
||||
<string name="toast_none_data_clipboard">Mübadilə buferində məlumat yoxdur</string>
|
||||
<string name="toast_invalid_url">Yanlış URL</string>
|
||||
<string name="toast_insecure_url_protocol">Təhlükəsiz olmayan HTTP abunəlik URL-indən istifadə etməyin</string>
|
||||
<string name="server_lab_need_inbound">Gələn portun tənzimləmələrə uyğun olduğundan əmin olun</string>
|
||||
<string name="toast_malformed_json">Yanlış konfiqurasiya.</string>
|
||||
<string name="server_lab_request_host6">Host (SNI) (istəyə bağlı)</string>
|
||||
<string name="toast_action_not_allowed">Fəaliyyətə icazə verilmir</string>
|
||||
<string name="server_obfs_password">Obfs şifrəsi</string>
|
||||
<string name="server_lab_port_hop">Port dəyişməsi (portu əvəzləyir)</string>
|
||||
<string name="server_lab_port_hop_interval">Port dəyişmə tezliyi (saniyə)</string>
|
||||
<string name="server_lab_bandwidth_down">Yükləmə zolaq genişliyi (dəstəklənən vahidlər: k/m/g/t)</string>
|
||||
<string name="server_lab_bandwidth_up">Ötürmə zolaq genişliyi (dəstəklənən vahidlər: k/m/g/t)</string>
|
||||
<string name="server_lab_xhttp_mode">XHTTP Rejimi</string>
|
||||
<string name="server_lab_xhttp_extra">XHTTP Əlavə xam JSON, format: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask xam JSON, format: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list" translatable="false">echConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Qarşı tərəf sertifikatını ada görə doğrulayın</string>
|
||||
<string name="server_lab_pinned_ca256">Sertifikatın barmaq izi (SHA-256)</string>
|
||||
<string name="server_lab_browser_dialer">Brauzer Yığcısını aktivləşdirin</string>
|
||||
<string name="server_lab_browser_dialer_tip">Yalnız xhttp (packet-up) və ws çıxışlarını dəstəkləyir; TLS ilə bağlı tənzimləmələr nəzərə alınmaya və ya ziddiyyət təşkil edə bilər</string>
|
||||
<string name="toast_allow_insecure_deprecated">Cari düyün şifrələnməmiş bağlantı istifadə edir. Ünsiyyətiniz avtoritar hökumətlər tərəfindən idarə olunan şəbəkə vasitəçiləri tərəfindən birbaşa izlənilə bilər.\nTəhlükəsizlik səbəbiylə, bu tür düyünlərə Xray core 26.2.6+ versiyası ilə qoşulmaq mümkün deyil.\nƏgər bu özünüzün yaratdığı düyündürsə, xahiş olunur TLS kimi təhlükəsizlik şifrələməsini aktivləşdirin və ya pinnedCA256 sertifikatını bərkidib istifadə edin.\nƏgər bu xidmət təminatçısının düyünüdürsə, texniki yenilənməni tamamlamaq üçün xidmət təminatçısı ilə əlaqə saxlayın. Xidmət təminatçısı əməkdaşlıq etməkdən imtina edərsə, istifadəçi təhlükəsizliyinə daha çox önəm verən başqa bir provayderə keçməyiniz tövsiyə olunur.\nƏtraflı məlumat üçün https://github.com/2dust/v2rayN/discussions/9460 ünvanına daxil olun</string>
|
||||
<string name="pinned_ca256_action_fetch">Sertifikatın barmaq izini əldə et</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Sertifikatın barmaq izi uğurla əldə edildi</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">Sertifikatın barmaq izini əldə etmək mümkün olmadı</string>
|
||||
|
||||
<!-- UserAssetActivity -->
|
||||
<string name="toast_asset_copy_failed">Fayl kopyalanması uğursuz oldu, zəhmət olmasa Fayl Menecerindən istifadə edin</string>
|
||||
<string name="menu_item_add_asset">Resurs əlavə et</string>
|
||||
<string name="menu_item_add_file">Fayllar əlavə et</string>
|
||||
<string name="menu_item_add_url">URL əlavə et</string>
|
||||
<string name="menu_item_scan_qrcode">QR kodu skanla</string>
|
||||
<string name="title_url" translatable="false">URL</string>
|
||||
<string name="menu_item_download_file">Faylları endir</string>
|
||||
<string name="title_user_asset_add_url">Resurs URL-i əlavə et</string>
|
||||
<string name="msg_file_not_found">Fayl tapılmadı</string>
|
||||
<string name="msg_remark_is_duplicate">Bu ad artıq mövcuddur</string>
|
||||
<string name="asset_geo_files_sources">Geo fayl mənbəyi (istəyə bağlı)</string>
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">Yüklənir</string>
|
||||
<string name="menu_item_search">Axtar</string>
|
||||
<string name="menu_item_select_all">Hamısını seç</string>
|
||||
<string name="menu_item_invert_selection">Seçimi tərsinə çevir</string>
|
||||
<string name="msg_enter_keywords">Açar sözləri daxil edin</string>
|
||||
<string name="switch_bypass_apps_mode">Yayınma Rejimi</string>
|
||||
<string name="menu_item_select_proxy_app">Proksi istifadə edəcək tətbiqləri avtomatik seç</string>
|
||||
<string name="msg_downloading_content">Məzmun endirilir</string>
|
||||
<string name="menu_item_export_proxy_app">Mübadilə buferinə ixrac et</string>
|
||||
<string name="menu_item_import_proxy_app">Mübadilə buferindən idxal et</string>
|
||||
<string name="per_app_proxy_settings">Tətbiq üzrə proksi tənzimləmələri</string>
|
||||
<string name="per_app_proxy_settings_enable">Tətbiq üzrə proksini aktivləşdir</string>
|
||||
<string name="app_picker_unknown_app">Bilinməyən tətbiq (UID tanınmadı)</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">Tənzimləmələr</string>
|
||||
<string name="title_advanced">Ətraflı Tənzimləmələr</string>
|
||||
<string name="title_core_settings">Nüvə Tənzimləmələri</string>
|
||||
<string name="title_vpn_settings">VPN Tənzimləmələri</string>
|
||||
<string name="title_pref_per_app_proxy">Tətbiq üzrə proksi</string>
|
||||
<string name="summary_pref_per_app_proxy">Ümumi: seçilmiş tətbiqlər proksidən istifadə edir, seçilməyənlər birbaşa qoşulur;\nYayınma rejimi: seçilmiş tətbiqlər birbaşa qoşulur, seçilməyənlər proksidən istifadə edir.\nProksi istifadə etməli olan tətbiqlərin avtomatik seçilməsi seçimi menyudadır.</string>
|
||||
<string name="title_pref_is_booted">Cihaz açılanda qoşul</string>
|
||||
<string name="summary_pref_is_booted">Cihaz işə düşdükdən sonra avtomatik olaraq seçilmiş serverə qoşulun. Android fonda başladılmanı məhdudlaşdıra bilər.</string>
|
||||
<string name="title_system_vpn_settings">Sistem VPN tənzimləmələri</string>
|
||||
<string name="summary_system_vpn_settings">Bu cihaz dəstəkləyirsə, Həmişə açıq VPN-i konfiqurasiya etmək üçün Android VPN tənzimləmələrini açın.</string>
|
||||
<string name="toast_system_vpn_settings_unavailable">Sistem VPN tənzimləmələri əlçatmazdır.</string>
|
||||
|
||||
|
||||
<string name="subscription_updater_job_tips">Tapşırıq arxa fonda icra olunacaq və gedişat məlumatı bildirişdə göstəriləcək.</string>
|
||||
<string name="title_pref_auto_test_after_update_subscription">Abunəlik yeniləndikdən sonra avtomatik sınaqdan keçir</string>
|
||||
<string name="summary_pref_auto_test_after_update_subscription">Avtomatlaşdırılmış sınaq uzun çəkir. Aktivləşdirilərsə, tapşırıq arxa fonda işləyəcək; əks halda, cari səhifədə icra olunacaq.</string>
|
||||
<string name="title_pref_auto_remove_invalid_after_test">Sınaqdan sonra yararsız konfiqurasiyaları avtomatik sil</string>
|
||||
<string name="summary_pref_auto_remove_invalid_after_test">Sınaq nəticələri dəqiq olmaya bilər; silinmiş konfiqurasiyanı bərpa etmək mümkün deyil.</string>
|
||||
<string name="title_pref_auto_sort_after_test">Sınaqdan sonra avtomatik çeşidlə</string>
|
||||
<string name="summary_pref_auto_sort_after_test">Sınaq nəticələri dəqiq olmaya bilər;</string>
|
||||
|
||||
<string name="title_mux_settings">Mux Tənzimləmələri</string>
|
||||
<string name="title_pref_mux_enabled">Mux-ı aktivləşdir</string>
|
||||
<string name="summary_pref_mux_enabled">Daha sürətlidir, lakin bağlantı qeyri-sabitliyinə səbəb ola bilər\nAşağıda TCP, UDP və QUIC idarəetməsini fərdiləşdirin</string>
|
||||
<string name="title_pref_mux_concurrency">TCP bağlantıları (diapazon -1 ilə 1024 arası)</string>
|
||||
<string name="title_pref_mux_xudp_concurrency">XUDP bağlantıları (diapazon -1 ilə 1024 arası)</string>
|
||||
<string name="title_pref_mux_xudp_quic">Mux tunelində QUIC idarə edilməsi</string>
|
||||
<string-array name="mux_xudp_quic_entries">
|
||||
<item>rədd et</item>
|
||||
<item>icazə ver</item>
|
||||
<item>ötür</item>
|
||||
</string-array>
|
||||
|
||||
<string name="title_pref_speed_enabled">Sürət göstəricisini aktivləşdir</string>
|
||||
<string name="summary_pref_speed_enabled">Cari sürəti bildiriş panelində göstərin.\nİkona proksi və ya birbaşa trafikin üstünlük təşkil etdiyini bildirir.</string>
|
||||
|
||||
<string name="title_pref_sniffing_enabled">Dinləməni aktivləşdir</string>
|
||||
<string name="summary_pref_sniffing_enabled">Trafikdən hədəf domenləri təyin edin (standart olaraq aktivdir).</string>
|
||||
<string name="title_pref_route_only_enabled">routeOnly aktivləşdir</string>
|
||||
<string name="summary_pref_route_only_enabled">Təyin edilmiş domen adını yalnız marşrutlaşdırma üçün istifadə edin və hədəf ünvanını IP ünvanı olaraq saxlayın.</string>
|
||||
|
||||
<string name="title_pref_local_dns_enabled">Yerli DNS-i aktivləşdir</string>
|
||||
<string name="summary_pref_local_dns_enabled">DNS nüvənin DNS modulu tərəfindən emal olunur (LAN və ölkə daxili ünvanları yan keçmək lazımdırsa tövsiyə olunur)</string>
|
||||
|
||||
<string name="title_pref_fake_dns_enabled">FakeDNS aktivləşdir</string>
|
||||
<string name="summary_pref_fake_dns_enabled">FakeDNS domen sorğuları üçün süni IP ünvanları qaytarır (daha sürətlidir, lakin bəzi tətbiqlərlə işləməyə bilər).</string>
|
||||
|
||||
<string name="title_pref_ipv6_enabled">IPv6-nı aktivləşdir</string>
|
||||
<string name="summary_pref_ipv6_enabled">VPN interfeysinə IPv6 ünvanı və marşrutlarını əlavə edin</string>
|
||||
|
||||
<string name="title_pref_prefer_ipv6">IPv6-ya üstünlük ver</string>
|
||||
<string name="summary_pref_prefer_ipv6">Domen adlarını həll edərkən IPv6 ünvanlarına üstünlük verin</string>
|
||||
|
||||
<string name="title_pref_remote_dns">Uzaq DNS (UDP/TCP/HTTPS/QUIC, istəyə bağlı)</string>
|
||||
<string name="summary_pref_remote_dns" translatable="false">DNS</string>
|
||||
|
||||
<string name="title_pref_vpn_dns">VPN DNS (yalnız IPv4/IPv6)</string>
|
||||
<string name="title_pref_vpn_bypass_lan">VPN rejimində LAN-ı yan keç</string>
|
||||
|
||||
<string name="title_pref_vpn_interface_address">VPN İnterfeys Ünvanı</string>
|
||||
|
||||
<string name="title_pref_vpn_mtu">VPN MTU (standart 1500)</string>
|
||||
|
||||
|
||||
<string name="title_pref_domestic_dns">Birbaşa DNS (istəyə bağlı)</string>
|
||||
<string name="summary_pref_domestic_dns" translatable="false">DNS</string>
|
||||
|
||||
<string name="title_pref_dns_hosts">DNS host mənsubiyyətləri (format: domen:ünvan,…)</string>
|
||||
<string name="summary_pref_dns_hosts">domen:ünvan,…</string>
|
||||
|
||||
<string name="title_pref_delay_test_url">Real gecikmə sınağı URL-i</string>
|
||||
<string name="summary_pref_delay_test_url" translatable="false">URL</string>
|
||||
<string name="title_pref_real_ping_concurrency">Eyni vaxtda aparılan real gecikmə sınaqları</string>
|
||||
|
||||
<string name="title_pref_ip_api_url">Cari bağlantı məlumatı sınağı URL-i</string>
|
||||
<string name="summary_pref_ip_api_url" translatable="false">URL</string>
|
||||
|
||||
<string name="title_pref_proxy_sharing_enabled">LAN qoşulmalarına icazə ver</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">Digər cihazlar yerli proksi vasitəsilə sizin IP ünvanınızla proksiyə qoşula bilər. İcazəsiz qoşulmaların qarşısını almaq üçün yalnız etibarlı şəbəkələrdə aktivləşdirin</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">LAN-dan qoşulmalara icazə verilir. Etibarlı şəbəkədə olduğunuzdan əmin olun</string>
|
||||
|
||||
<string name="title_pref_socks_port">Yerli proksi portu</string>
|
||||
<string name="title_pref_enable_local_proxy">Yerli proksini aktivləşdir</string>
|
||||
<string name="summary_pref_enable_local_proxy">Qapalı olduqda, SOCKS proksi əlçatan olmur, buna görə də abunəlik yenilənmələri və bənzər əməliyyatlar proksidən istifadə edə bilmir</string>
|
||||
<string name="title_pref_socks_enable_udp" translatable="false">SOCKS5 UDP</string>
|
||||
<string name="summary_pref_socks_enable_udp">UDP istifadə edərkən SOCKS5 doğrulama üsulu tam təhlükəsiz deyil.</string>
|
||||
<string name="summary_pref_socks_port">Yerli proksi portu</string>
|
||||
<string name="title_pref_dynamic_socks_port">Dinamik yerli proksi portunu aktivləşdir</string>
|
||||
<string name="summary_pref_dynamic_socks_port">Hər gələn bağlantı üçün təsadüfi yerli proksi portundan istifadə edin</string>
|
||||
<string name="title_pref_socks_username">Yerli proksi istifadəçi adı (istəyə bağlı)</string>
|
||||
<string name="summary_pref_socks_username">İstifadəçi adı</string>
|
||||
<string name="title_pref_socks_password">Yerli proksi şifrəsi (istəyə bağlı)</string>
|
||||
<string name="summary_pref_socks_password">Şifrə</string>
|
||||
|
||||
<string name="title_pref_local_dns_port">Yerli DNS portu</string>
|
||||
<string name="summary_pref_local_dns_port">Yerli DNS portu</string>
|
||||
|
||||
<string name="title_pref_confirm_remove">Konfiqurasiya silinməsi təsdiqi</string>
|
||||
<string name="summary_pref_confirm_remove">Konfiqurasiya silinərkən istifadəçidən ikinci təsdiqin tələb olunub-olunmaması</string>
|
||||
|
||||
|
||||
<string name="title_pref_append_http_proxy">VPN-ə HTTP proksi əlavə et</string>
|
||||
<string name="summary_pref_append_http_proxy">Brauzerlərə və dəstəklənən tətbiqlərə virtual şəbəkə interfeysi əvəzinə birbaşa HTTP proksidən istifadə etməyə icazə verin (Android 10+).</string>
|
||||
|
||||
<string name="title_pref_double_column_display">İki sütunlu düzülüşdən istifadə et</string>
|
||||
<string name="summary_pref_double_column_display">Ekrana daha çox məzmun sığdırmaq üçün profil siyahısını iki sütunda göstərin.</string>
|
||||
|
||||
<string name="title_pref_group_all_display">"Hamısı" qrup vərəqini göstər</string>
|
||||
<string name="summary_pref_group_all_display">Hər abunəlik qrupundakı profilləri bir vərəqdə birləşdirin.</string>
|
||||
|
||||
<!-- AboutActivity -->
|
||||
<string name="title_pref_feedback">Əks-əlaqə</string>
|
||||
<string name="summary_pref_feedback">GitHub-da təkliflərinizi bildirin və ya xətaları məruzə edin</string>
|
||||
<string name="summary_pref_tg_group">Telegram qrupuna qoşulun</string>
|
||||
<string name="toast_tg_app_not_found">Telegram tətbiqi tapılmadı</string>
|
||||
<string name="title_privacy_policy">Məxfilik siyasəti</string>
|
||||
<string name="title_qr_code">QR kod</string>
|
||||
<string name="title_about">Haqqında</string>
|
||||
<string name="title_source_code">Mənbə kodu</string>
|
||||
<string name="title_oss_license">Açıq mənbə lisenziyaları</string>
|
||||
<string name="title_translators">Tərcüməçilər</string>
|
||||
<string name="title_tg_channel">Telegram kanalı</string>
|
||||
|
||||
<string name="title_pref_promotion">Təşviqat</string>
|
||||
|
||||
<string name="title_pref_auto_update_subscription">Abunəlikləri avtomatik yenilə</string>
|
||||
<string name="summary_pref_auto_update_subscription">Abunəliklərinizi arxa fonda təyin olunmuş intervallarla avtomatik yeniləyin. Cihazdan asılı olaraq bu funksiya həmişə işləməyə bilər</string>
|
||||
<string name="title_pref_auto_update_interval">Avtomatik yenilənmə intervalı (dəqiqə; minimum 15)</string>
|
||||
|
||||
<string name="title_core_loglevel">Jurnal Səviyyəsi</string>
|
||||
<string name="title_outbound_domain_resolve_method">Çıxış domeninin həll edilməsi</string>
|
||||
<string name="title_mode">Rejim</string>
|
||||
<string name="title_mode_help">Ətraflı kömək üçün toxunun</string>
|
||||
<string name="title_language">Dil</string>
|
||||
<string name="title_ui_settings">İstifadəçi interfeysi tənzimləmələri</string>
|
||||
<string name="title_pref_ui_mode_night">İstifadəçi interfeysi rejim tənzimləmələri</string>
|
||||
<string name="title_pref_dynamic_color">Dinamik rəngi aktivləşdir</string>
|
||||
<string name="summary_pref_dynamic_color">Yalnız Android 12+ cihazlarında işləyir</string>
|
||||
<string name="title_pref_use_hev_tunnel">Hev TUN istifadə et</string>
|
||||
<string name="summary_pref_use_hev_tunnel">Aktiv edildikdə TUN hev-socks5-tunnel istifadə edəcək; əks halda xray-core istifadə olunacaq.</string>
|
||||
<string name="title_pref_hev_tunnel_loglevel">Hev TUN jurnal səviyyəsi</string>
|
||||
<string name="title_pref_hev_tunnel_rw_timeout">Hev TUN oxuma/yazma vaxt aşımı (saniyə; TCP/UDP standartları: 300/60)</string>
|
||||
<string name="title_mode_settings">Rejim Tənzimləmələri</string>
|
||||
<string name="title_root_mode_enabled">Root rejimini aktivləşdir (root istifadəçiləri)</string>
|
||||
<string name="summary_root_mode_enabled">Aşağı səviyyəli şəffaf proksiləmə üçün root icazəsindən istifadə edin. Root rejimi Android VPN xidmətindən istifadə etmək əvəzinə şəbəkə trafikini birbaşa idarə edir. Root rejimi aktiv olduqda standart rejim, tətbiq üzrə marşrutlaşdırma və VPN ilə bağlı tənzimləmələr tətbiq edilmir.</string>
|
||||
<string name="toast_root_required">Bu funksiya üçün Root icazəsi tələb olunur</string>
|
||||
<string name="title_root_lan_sharing">VPN-i LAN / modem rejimindəki cihazlarla paylaş (root istifadəçiləri)</string>
|
||||
<string name="summary_root_lan_sharing">Wi-Fi giriş nöqtəsi (hotspot) və USB modem rejiminə qoşulmuş cihazların trafikini VPN vasitəsilə yönləndirin.</string>
|
||||
|
||||
<string name="title_logcat" translatable="false">Logcat</string>
|
||||
<string name="logcat_copy">Kopyala</string>
|
||||
<string name="logcat_clear">Təmizlə</string>
|
||||
<string name="logcat_share">Jurnalı paylaş</string>
|
||||
<string name="title_service_restart">Xidməti yenidən başlat</string>
|
||||
<string name="title_del_all_config">Konfiqurasiyaları sil</string>
|
||||
<string name="title_del_duplicate_config">Təkrar konfiqurasiyaları sil</string>
|
||||
<string name="title_del_invalid_config">Yararsız konfiqurasiyaları sil</string>
|
||||
<string name="title_export_all">Konfiqurasiyaları mübadilə buferinə ixrac et</string>
|
||||
<string name="title_sub_setting">Abunəliklər</string>
|
||||
<string name="sub_setting_remarks">Qeydlər</string>
|
||||
<string name="sub_setting_url">İstəyə bağlı URL</string>
|
||||
<string name="sub_setting_user_agent" translatable="false">User Agent</string>
|
||||
<string name="sub_setting_request_headers">Sorğu Başlıqları (JSON formatında)</string>
|
||||
<string name="sub_setting_filter">Qeyd filtri (müntəzəm ifadə / regex)</string>
|
||||
<string name="sub_setting_enable">Yenilənməni aktivləşdir</string>
|
||||
<string name="sub_auto_update">Avtomatik yenilənməni aktivləşdir</string>
|
||||
<string name="sub_allow_insecure_url">Təhlükəsiz olmayan HTTP ünvanına icazə ver</string>
|
||||
<string name="sub_setting_pre_profile">Giriş proksisi</string>
|
||||
<string name="sub_setting_next_profile">Çıxış proksisi</string>
|
||||
<string name="sub_setting_pre_profile_tip">Konfiqurasiya qeydlərinin mövcud olduğundan və unikal olduğundan əmin olun</string>
|
||||
<string name="sub_setting_entry_proxy_tip">Seçilmiş proksi, bu abunəlikdəki hər bir profilin əvvəlinə həmin profil üçün proksi zəncirindəki giriş keçidi olaraq əlavə edilir.</string>
|
||||
<string name="sub_setting_exit_proxy_tip">Seçilmiş proksi, bu abunəlikdəki hər bir profilin sonuna həmin profil üçün proksi zəncirindəki çıxış keçidi olaraq əlavə edilir.</string>
|
||||
<string name="toast_invalid_update_interval">Yanlış yenilənmə intervalı. Minimum 15 dəqiqədir.</string>
|
||||
<string name="title_sub_update">Abunəliyi yenilə</string>
|
||||
<string name="title_ping_all_server">TCP gecikmələrini sınaqdan keçir (TCPing)</string>
|
||||
<string name="title_real_ping_all_server">Real gecikmələri sınaqdan keçir</string>
|
||||
<string name="title_user_asset_setting">Resurs faylları</string>
|
||||
<string name="title_sort_by_test_results">Sınaq nəticələrinə görə çeşidlə</string>
|
||||
<string name="title_filter_config">Konfiqurasiyaları filtrdən keçir</string>
|
||||
<string name="filter_config_all">Hamısı</string>
|
||||
<string name="summary_scan_qrcode">Kameranı açmaq və skan etmək üçün ekrana toxunun</string>
|
||||
<string name="title_del_duplicate_config_count">%d təkrar konfiqurasiya silinsin</string>
|
||||
<string name="title_del_config_count">%d konfiqurasiya silinsin</string>
|
||||
<string name="title_import_config_count">%d konfiqurasiya idxal edilsin</string>
|
||||
<string name="title_export_config_count">%d konfiqurasiya ixrac edilsin</string>
|
||||
<plurals name="title_update_config_count">
|
||||
<item quantity="one">%1$d konfiqurasiya yeniləndi</item>
|
||||
<item quantity="other">%1$d konfiqurasiya yeniləndi</item>
|
||||
</plurals>
|
||||
<plurals name="title_update_asset_count">
|
||||
<item quantity="one">%1$d resurs faylı yeniləndi</item>
|
||||
<item quantity="other">%1$d resurs faylı yeniləndi</item>
|
||||
</plurals>
|
||||
<string name="title_update_subscription_result">%1$d konfiqurasiya yeniləndi (%2$d uğurlu, %3$d uğursuz, %4$d ötürüldü)</string>
|
||||
<string name="title_update_subscription_no_subscription">Abunəlik yoxdur</string>
|
||||
<string name="toast_server_not_found_in_group">Seçilmiş server cari qrupda tapılmadı</string>
|
||||
<string name="title_locate_selected_config">Seçilmiş konfiqurasiyanı tapın</string>
|
||||
|
||||
<string name="tasker_start_service">Xidməti Başlat</string>
|
||||
<string name="tasker_setting_confirm">Təsdiqlə</string>
|
||||
|
||||
<!-- RoutingSettingActivity -->
|
||||
<string name="routing_settings_domain_strategy">Domen strategiyası</string>
|
||||
<string name="routing_settings_title">Marşrutlaşdırma Tənzimləmələri</string>
|
||||
<string name="routing_settings_tips">Qayda yalnız bütün konfiqurasiya edilmiş uyğunlaşma şərtləri yerinə yetirildikdə (VƏ) tətbiq olunur. Qaydalar yuxarıdan aşağıya doğru yoxlanılır; uyğun gələn ilk qayda tətbiq edilir.</string>
|
||||
<string name="routing_settings_comma_tip">Çoxsaylı daxiletmələri vergüllə (,) ayırın.</string>
|
||||
<string name="routing_settings_save">Saxla</string>
|
||||
<string name="routing_settings_delete">Təmizlə</string>
|
||||
<string name="routing_settings_rule_title">Marşrutlaşdırma Qaydası Tənzimləmələri</string>
|
||||
<string name="routing_settings_add_rule">Qayda əlavə et</string>
|
||||
<string name="routing_settings_import_predefined_rulesets">Əvvəlcədən təyin edilmiş qayda dəstlərini idxal et</string>
|
||||
<string name="routing_settings_import_rulesets_tip">Mövcud qayda dəstləri silinəcək. Davam edilsin?</string>
|
||||
<string name="routing_settings_import_rulesets_from_clipboard">Mübadilə buferindən qayda dəstini idxal et</string>
|
||||
<string name="routing_settings_import_rulesets_from_qrcode">QR koddan qayda dəstini idxal et</string>
|
||||
<string name="routing_settings_export_rulesets_to_clipboard">Qayda dəstini mübadilə buferinə ixrac et</string>
|
||||
<string name="routing_settings_locked">Kilidlənib; hazır tənzimləmələri idxal edərkən bu qaydanı saxlayın</string>
|
||||
<string name="routing_settings_domain" translatable="false">domain</string>
|
||||
<string name="routing_settings_ip" translatable="false">ip</string>
|
||||
<string name="routing_settings_process">proses (Paket Adı — yalnız Xray TUN istifadə edildikdə, routeOnly aktiv olduqda və Android 10+ versiyasında dəstəklənir)</string>
|
||||
<string name="routing_settings_process_select">Paket adını seçin</string>
|
||||
<string name="routing_settings_port" translatable="false">port</string>
|
||||
<string name="routing_settings_protocol" translatable="false">protocol</string>
|
||||
<string name="routing_settings_protocol_tip" translatable="false">[http,tls,bittorrent]</string>
|
||||
<string name="routing_settings_network" translatable="false">network</string>
|
||||
<string name="routing_settings_outbound_tag_hint">proxy / direct / block / <%1$s></string>
|
||||
<string name="routing_settings_outbound_tag" translatable="false">outboundTag</string>
|
||||
|
||||
<string name="connection_test_pending">Bağlantını yoxlayın</string>
|
||||
<string name="connection_test_testing">Yoxlanılır…</string>
|
||||
<string name="connection_test_testing_count">%d konfiqurasiya yoxlanılır…</string>
|
||||
<string name="connection_test_available">Bağlantı %1$s ərzində uğurla quruldu</string>
|
||||
<string name="server_test_delay_value">%1$d ms</string>
|
||||
<string name="connection_test_error">Bağlantı sınağı uğursuz oldu: %s</string>
|
||||
<string name="connection_test_fail">İnternet əlçatmazdır</string>
|
||||
<string name="connection_test_error_status_code">Xəta kodu: #%d</string>
|
||||
<string name="connection_connected">Qoşuldu. Bağlantını yoxlamaq üçün toxunun.</string>
|
||||
<string name="connection_not_connected">Qoşulmayıb</string>
|
||||
<string name="connection_running_task_left">İcra olunan sınaqlar: %s</string>
|
||||
|
||||
<string name="import_subscription_success">Abunəlik uğurla idxal edildi</string>
|
||||
<string name="import_subscription_failure">Abunəlik idxalı uğursuz oldu</string>
|
||||
<string name="title_fragment_settings">Fraqment Tənzimləmələri</string>
|
||||
<string name="title_pref_fragment_packets">Fraqment Paketləri</string>
|
||||
<string name="title_pref_fragment_length">Fraqment Uzunluğu (min-maks)</string>
|
||||
<string name="title_pref_fragment_interval">Fraqment İntervalı (min-maks)</string>
|
||||
<string name="title_pref_fragment_enabled">Fraqmenti aktivləşdir</string>
|
||||
<string name="title_pref_fragment_maxsplit">Fraqment Maksimum Bölünməsi</string>
|
||||
<string name="title_observatory_settings">Müşahidəçi Tənzimləmələri</string>
|
||||
<string name="title_pref_observatory_least_ping_interval">leastPing İntervalı</string>
|
||||
<string name="title_pref_observatory_least_load_interval">leastLoad İntervalı</string>
|
||||
<string name="title_pref_observatory_least_load_method">leastLoad HTTP metodu</string>
|
||||
<string name="title_pref_observatory_least_load_sampling">leastLoad Seçmələri</string>
|
||||
<string name="title_pref_observatory_least_load_timeout">leastLoad Vaxt Aşımı</string>
|
||||
<string name="toast_invalid_observatory_duration">Yanlış müddət. 3m və ya 30s kimi bir dəyərdən istifadə edin.</string>
|
||||
<string name="toast_invalid_observatory_sampling">Yanlış müddət. Müsbət ədəddən istifadə edin.</string>
|
||||
|
||||
<string name="update_check_for_update">Yenilənmələri yoxla</string>
|
||||
<string name="update_already_latest_version">Artıq ən son versiyadadır</string>
|
||||
<string name="update_new_version_found">Yeni versiya tapıldı: %s</string>
|
||||
<string name="update_now">İndi yenilə</string>
|
||||
<string name="update_check_pre_release">Sınaq versiyalarını yoxla</string>
|
||||
<string name="update_checking_for_update">Yenilənmələr yoxlanılır…</string>
|
||||
|
||||
<string name="title_policy_group_type">Siyasət qrupu növü</string>
|
||||
<string name="title_policy_group_subscription_id">Abunəlik qrupundan</string>
|
||||
<string name="title_policy_group_subscription_filter">Qeydlərin regex filtri</string>
|
||||
<string name="title_policy_group_test_outbounds">Çıxışları sınaqdan keçir</string>
|
||||
<string name="title_policy_group_fallback">Ehtiyat çıxışı (istəyə bağlı)</string>
|
||||
|
||||
<string name="server_proxy_chain_members">Proksi zənciri üzvləri</string>
|
||||
<string name="server_proxy_chain_pick_members">Üzv seçmək üçün bura toxunun</string>
|
||||
<string name="server_proxy_chain_member_unselected">Üzv seçin</string>
|
||||
<string name="server_proxy_chain_members_unselected">Zəhmət olmasa hər zəncir sətri üçün bir üzv seçin</string>
|
||||
<string name="server_proxy_chain_members_insufficient">Kifayət qədər üzv yoxdur</string>
|
||||
<string name="server_proxy_chain_members_invalid">Yanlış zəncir üzvləri: %1$s</string>
|
||||
|
||||
<!-- BackupActivity -->
|
||||
<string name="title_configuration_backup_restore">Ehtiyat Nüsxə & Bərpa</string>
|
||||
<string name="title_configuration_backup">Konfiqurasiyanın ehtiyat nüsxəsini çıxar</string>
|
||||
<string name="title_configuration_restore">Konfiqurasiyanı bərpa et</string>
|
||||
<string name="title_profile_storage_cleanup">Profil yaddaşını təmir et</string>
|
||||
<string name="summary_profile_storage_cleanup">İstifadə olunmayan profil məlumatlarını silin</string>
|
||||
<string name="message_profile_storage_cleanup">Artıq heç bir qrup tərəfindən istifadə edilməyən profil məlumatları silinsin? Seçilmiş profillər və təhlükəsiz təsnif edilə bilməyən məlumatlar saxlanılacaq.</string>
|
||||
<string name="toast_profile_storage_cleanup">İstifadə olunmayan profillər silindi: %1$d</string>
|
||||
<string name="toast_profile_storage_cleanup_skipped">Profil yaddaşı təhlükəsiz təsnif edilə bilmədi. Heç nə silinmədi.</string>
|
||||
<string name="title_configuration_share">Konfiqurasiyanı paylaş</string>
|
||||
<string name="title_webdav_config_setting">WebDAV Tənzimləmələri</string>
|
||||
<string name="title_webdav_config_setting_unknown">Zəhmət olmasa əvvəlcə WebDAV-ı konfiqurasiya edin.</string>
|
||||
<string name="title_webdav_url">WebDAV server URL-i</string>
|
||||
<string name="title_webdav_user">İstifadəçi adı</string>
|
||||
<string name="title_webdav_pass">Şifrə</string>
|
||||
<string name="title_webdav_remote_path">Uzaq qovluq (istəyə bağlı)</string>
|
||||
|
||||
<string name="share_method_qrcode">QR kod</string>
|
||||
<string name="share_method_clipboard">Profil keçidini kopyala</string>
|
||||
<string name="share_method_full_content">Tam konfiqurasiyanı kopyala</string>
|
||||
<string name="action_edit">Düzəliş et</string>
|
||||
<string name="action_delete">Sil</string>
|
||||
<string name="action_close">Bağla</string>
|
||||
|
||||
<string name="share_subscription_qrcode">QR kod</string>
|
||||
<string name="share_subscription_clipboard">Mübadilə buferinə ixrac et</string>
|
||||
|
||||
<string-array name="mode_entries">
|
||||
<item>VPN</item>
|
||||
<item>Yalnız proksi</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="browser_dialer_mode">
|
||||
<item>Qapat</item>
|
||||
<item>OkHttp</item>
|
||||
<item>WebView</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="ui_mode_night">
|
||||
<item>Sistem Standartı</item>
|
||||
<item>İşıqlı</item>
|
||||
<item>Qaranlıq</item>
|
||||
</string-array>
|
||||
|
||||
<string name="routing_preset_china_whitelist">Çin Ağ Siyahısı</string>
|
||||
<string name="routing_preset_china_blacklist">Çin Qara Siyahısı</string>
|
||||
<string name="routing_preset_global">Qlobal</string>
|
||||
<string name="routing_preset_iran_whitelist">İran Ağ Siyahısı</string>
|
||||
<string name="routing_preset_russia_whitelist">Rusiya Ağ Siyahısı</string>
|
||||
|
||||
<string-array name="vpn_bypass_lan">
|
||||
<item>Konfiqurasiyaya uyğunlaş</item>
|
||||
<item>Yan keç</item>
|
||||
<item>Yan keçmə</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="outbound_domain_resolve_method">
|
||||
<item>Həll etmə</item>
|
||||
<item>Həll et və DNS Hosts-a əlavə et</item>
|
||||
<item>Həll et və domeni əvəzlə</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="policy_group_type">
|
||||
<item>Ən az gecikmə</item>
|
||||
<item>Ən az yükləmə</item>
|
||||
<item>Təsadüfi</item>
|
||||
<item>Növbəli</item>
|
||||
</string-array>
|
||||
|
||||
<string name="backup_location_local">Yerli</string>
|
||||
<string name="backup_location_webdav" translatable="false">WebDAV</string>
|
||||
|
||||
<string name="acc_refresh">Jurnalı yenilə</string>
|
||||
<string name="toast_permission_denied_for">%1$s icazəsi rədd edildi</string>
|
||||
<string name="acc_add_subscription">Abunəlik əlavə et</string>
|
||||
<string name="toast_malformed_json_detail">Yanlış konfiqurasiya: %1$s</string>
|
||||
<string name="acc_add_asset">Resurs mənbəyi əlavə et</string>
|
||||
<string name="acc_download_file">Resurs fayllarını endir</string>
|
||||
<string name="acc_qr_code">QR kod</string>
|
||||
<string name="acc_copy_log">Jurnalı kopyala</string>
|
||||
<string name="acc_clear_log">Jurnalı təmizlə</string>
|
||||
<string name="acc_add_rule">Marşrutlaşdırma qaydası əlavə et</string>
|
||||
<string name="acc_start">Xidməti başlat</string>
|
||||
<string name="acc_stop">Xidməti dayandır</string>
|
||||
<string name="acc_back">Geri</string>
|
||||
<string name="acc_search">Axtar</string>
|
||||
<string name="acc_add">Konfiqurasiya əlavə et</string>
|
||||
<string name="acc_more">Əlavə seçimlər</string>
|
||||
<string name="acc_selected_server">Seçildi</string>
|
||||
<string name="acc_share_subscription">Abunəlik keçidini paylaş</string>
|
||||
<string name="acc_share_log">Jurnalı paylaş</string>
|
||||
<string name="acc_remove">Proksi zənciri üzvünü çıxar</string>
|
||||
<string name="acc_add_member">Proksi zənciri üzvü əlavə et</string>
|
||||
<string name="acc_start_scanner">QR skaneri başlat</string>
|
||||
<string name="acc_stop_scanner">QR skaneri dayandır</string>
|
||||
<string name="acc_turn_torch_on">Fənəri yandır</string>
|
||||
<string name="acc_turn_torch_off">Fənəri söndür</string>
|
||||
<string name="acc_select_image">Skan etmək üçün şəkil seçin</string>
|
||||
<string name="acc_locked">Hazır tənzimləmələri idxal edərkən bu qayda saxlanılır</string>
|
||||
<string name="acc_open_menu">Naviqasiya menyusunu açın</string>
|
||||
<string name="acc_edit">Düzəliş et</string>
|
||||
<string name="acc_delete">Sil</string>
|
||||
<string name="acc_save">Saxla</string>
|
||||
<string name="acc_update_subscriptions">Abunəlikləri yenilə</string>
|
||||
<string name="acc_per_app_proxy_information">Tətbiq üzrə proksi məlumatı</string>
|
||||
<string name="permission_camera">Kamera</string>
|
||||
<string name="permission_notification">Bildiriş</string>
|
||||
<string name="permission_local_network">Yerli şəbəkə</string>
|
||||
<string name="subscription_update_updating">Abunəlik yenilənir: %1$s</string>
|
||||
<string name="subscription_update_progress">%2$s sınaqdan keçirilir: %1$s</string>
|
||||
<string name="tasker_blurb_start">%1$s Başlat</string>
|
||||
<string name="tasker_blurb_stop">%1$s Dayandır</string>
|
||||
<string name="connection_test_empty_message">ətraflı məlumat yoxdur</string>
|
||||
<string name="value_unknown">naməlum</string>
|
||||
<string name="sub_info_open_support">Dəstək linkini aç</string>
|
||||
<string name="sub_info_open_page">Abunəlik səhifəsini aç</string>
|
||||
<string name="sub_info_traffic" translatable="false">%1$s / %2$s</string>
|
||||
<string name="sub_info_unlimited">Limitsiz</string>
|
||||
|
||||
</resources>
|
||||
@@ -14,6 +14,9 @@
|
||||
<string name="pull_down_to_refresh">রিফ্রেশ করতে নিচে টানুন!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_channel_service">v2rayNG সার্ভিস</string>
|
||||
<string name="notification_channel_subscription_updates">সাবস্ক্রিপশন আপডেট</string>
|
||||
<string name="notification_channel_connection_checks">সংযোগ পরীক্ষা</string>
|
||||
<string name="notification_action_stop_v2ray">বন্ধ করুন</string>
|
||||
<string name="toast_permission_denied">অনুমতি পাওয়া যাচ্ছে না</string>
|
||||
<string name="toast_permission_denied_notification">বিজ্ঞপ্তির অনুমতি পাওয়া যায়নি</string>
|
||||
@@ -50,7 +53,7 @@
|
||||
<string name="confirm_delete_proxy_chain_member">আপনি কি এই প্রক্সি চেইন সদস্যটিকে মুছে ফেলতে চান?</string>
|
||||
<string name="confirm_delete_routing_rule">আপনি কি এই রাউটিং নিয়মটি মুছে ফেলতে চান?</string>
|
||||
<string name="confirm_delete_subscription_group">আপনি কি এই সাবস্ক্রিপশন গ্রুপটি মুছে ফেলতে চান?</string>
|
||||
<string name="confirm_delete_asset_file">আপনি কি \"%1$s\" অ্যাসেট ফাইলটি মুছে ফেলতে চান?</string>
|
||||
<string name="confirm_delete_asset_file">আপনি কি এই অ্যাসেট ফাইলটি মুছে ফেলতে চান?</string>
|
||||
<string name="confirm_delete_asset_source">আপনি কি এই অ্যাসেট উৎসটি মুছে ফেলতে চান?</string>
|
||||
<string name="server_lab_remarks">মন্তব্য</string>
|
||||
<string name="server_lab_address">ঠিকানা</string>
|
||||
@@ -100,6 +103,7 @@
|
||||
<string name="server_lab_reserved">Reserved (ঐচ্ছিক, কমা দিয়ে পৃথক)</string>
|
||||
<string name="server_lab_local_address">স্থানীয় ঠিকানা (ঐচ্ছিক IPv4/IPv6, কমা দ্বারা পৃথক করা)</string>
|
||||
<string name="server_lab_local_mtu">MTU (ঐচ্ছিক, ডিফল্ট 1420)</string>
|
||||
<string name="server_lab_remote_dns">remoteDNS (ঐচ্ছিক, কমা দ্বারা পৃথক করা IPv4/IPv6 ঠিকানা)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU (ঐচ্ছিক, ডিফল্ট 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI (ঐচ্ছিক, ডিফল্ট 50 মিলিসেকেন্ড)</string>
|
||||
<string name="toast_success">সফল</string>
|
||||
@@ -351,8 +355,14 @@
|
||||
<string name="title_del_config_count">%dটি কনফিগারেশন মুছুন</string>
|
||||
<string name="title_import_config_count">%dটি কনফিগারেশন আমদানি করুন</string>
|
||||
<string name="title_export_config_count">%dটি কনফিগারেশন রপ্তানি করুন</string>
|
||||
<string name="title_update_config_count">%dটি কনফিগারেশন আপডেট করুন</string>
|
||||
<string name="title_update_asset_count">%dটি অ্যাসেট ফাইল আপডেট হয়েছে</string>
|
||||
<plurals name="title_update_config_count">
|
||||
<item quantity="one">%1$dটি কনফিগারেশন আপডেট হয়েছে</item>
|
||||
<item quantity="other">%1$dটি কনফিগারেশন আপডেট হয়েছে</item>
|
||||
</plurals>
|
||||
<plurals name="title_update_asset_count">
|
||||
<item quantity="one">%1$dটি অ্যাসেট ফাইল আপডেট হয়েছে</item>
|
||||
<item quantity="other">%1$dটি অ্যাসেট ফাইল আপডেট হয়েছে</item>
|
||||
</plurals>
|
||||
<string name="title_update_subscription_result">%1$dটি কনফিগ আপডেট হয়েছে (%2$d সফল, %3$d ব্যর্থ, %4$d এড়ানো)</string>
|
||||
<string name="title_update_subscription_no_subscription">কোনো সাবস্ক্রিপশন নেই</string>
|
||||
<string name="toast_server_not_found_in_group">বর্তমান গ্রুপে নির্বাচিত সার্ভারটি পাওয়া যায়নি</string>
|
||||
@@ -550,5 +560,8 @@
|
||||
<string name="tasker_blurb_stop">%1$s বন্ধ করুন</string>
|
||||
<string name="connection_test_empty_message">কোনো বিবরণ পাওয়া যায়নি</string>
|
||||
<string name="value_unknown">অজানা</string>
|
||||
<string name="sub_info_open_support">সাপোর্ট লিঙ্ক খুলুন</string>
|
||||
<string name="sub_info_open_page">সাবস্ক্রিপশন তথ্যের পাতা খুলুন</string>
|
||||
<string name="sub_info_unlimited">সীমাহীন</string>
|
||||
|
||||
</resources>
|
||||
@@ -3,17 +3,20 @@
|
||||
<string name="app_name" translatable="false">v2rayNG</string>
|
||||
<string name="app_widget_name">آلشت</string>
|
||||
<string name="app_tile_name">آلشت</string>
|
||||
<string name="app_tile_first_use">سی کرت ٱولی و کار بوردن ای ویژیی، ٱول ز من برنومه ی سرور اضاف کۊنین</string>
|
||||
<string name="app_tile_first_use">سی کرت ٱولی و کار بوردن ای ویژیی، ٱول ز من برنومه ی سرور ازاف کۊنین</string>
|
||||
<string name="navigation_drawer_open">گۊشیڌن نومگه کشاری</string>
|
||||
<string name="navigation_drawer_close">بستن نومگه کشاری</string>
|
||||
<string name="migration_success">جاگورویی داڌه یل وا مووفقیت ٱنجوم وابی!</string>
|
||||
<string name="migration_success">جاگورویی داده یل وا مووفقیت ٱنجوم وابی!</string>
|
||||
<string name="action_stop_service">واڌاشتن سرویس</string>
|
||||
<string name="action_ok">قوۊل</string>
|
||||
<string name="action_cancel">لغو</string>
|
||||
<string name="migration_fail">جاگورویی داڌه یل شکست خرد!</string>
|
||||
<string name="action_cancel">لقو</string>
|
||||
<string name="migration_fail">جاگورویی داده یل شکست خرد!</string>
|
||||
<string name="pull_down_to_refresh">سی وانۊ کردن، بلگه ن بلم بکشین!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_channel_service">سرویس v2rayNG</string>
|
||||
<string name="notification_channel_subscription_updates">ورۊ کردن اشتراکا</string>
|
||||
<string name="notification_channel_connection_checks">واجۊری منپیز</string>
|
||||
<string name="notification_action_stop_v2ray">واڌاشتن</string>
|
||||
<string name="toast_permission_denied">گرؽڌن موجوز مومکن نؽڌ</string>
|
||||
<string name="toast_permission_denied_notification">گرؽڌن موجوز وارسۊوی مومکن نؽڌ</string>
|
||||
@@ -31,7 +34,7 @@
|
||||
<string name="menu_item_del_config">پاک کردن کانفیگ</string>
|
||||
<string name="menu_item_import_config_qrcode">و من ٱووردن کانفیگ ز کود QR</string>
|
||||
<string name="menu_item_import_config_clipboard">و من ٱووردن کانفیگ ز کلیپ بورد</string>
|
||||
<string name="menu_item_import_config_local">و من ٱووردن کانفیگ ز محلی</string>
|
||||
<string name="menu_item_import_config_local">و من ٱووردن کانفیگ ز ویرگه مهلی</string>
|
||||
<string name="menu_item_import_config_policy_group">ٱووردن [بونکۊ سیاست]</string>
|
||||
<string name="menu_item_import_config_proxy_chain">ٱووردن [زنجیره پروکسی]</string>
|
||||
<string name="menu_item_import_config_manually_vmess">ٱووردن [VMess]</string>
|
||||
@@ -50,8 +53,8 @@
|
||||
<string name="confirm_delete_proxy_chain_member">ای عوزو زنجیره پروکسی پاک ابۊ، هنی هم اخۊی پاکس کۊنی؟</string>
|
||||
<string name="confirm_delete_routing_rule">ای قانووݩ تور جوستن پاک ابۊ، هنی هم اخۊی پاکس کۊنی؟</string>
|
||||
<string name="confirm_delete_subscription_group">ای بونکۊ اشتراک پاک ابۊ، هنی هم اخۊی پاکس کۊنی؟</string>
|
||||
<string name="confirm_delete_asset_file">فایل بونچک جغرافیایی «%1$s» پاک ابۊ، هنی هم اخۊی پاکس کۊنی؟</string>
|
||||
<string name="confirm_delete_asset_source">ای بونچک فایلا جغرافیایی پاک ابۊ، هنی هم اخۊی پاکس کۊنی؟</string>
|
||||
<string name="confirm_delete_asset_file">فایل بونچک جوقرافیایی «%1$s» پاک ابۊ، هنی هم اخۊی پاکس کۊنی؟</string>
|
||||
<string name="confirm_delete_asset_source">ای بونچک فایل جوقرافیایی پاک ابۊ، هنی هم اخۊی پاکس کۊنی؟</string>
|
||||
<string name="server_lab_remarks">نیشتنا</string>
|
||||
<string name="server_lab_address">نشۊوی</string>
|
||||
<string name="server_lab_port">پورت</string>
|
||||
@@ -61,7 +64,7 @@
|
||||
<string name="server_lab_network">شبکه</string>
|
||||
<string name="server_lab_more_function">جاگورو</string>
|
||||
<string name="server_lab_head_type">نوع هدر</string>
|
||||
<string name="server_lab_mode_type">حالت gRPC</string>
|
||||
<string name="server_lab_mode_type">هالت gRPC</string>
|
||||
<string name="server_lab_request_host">هاست</string>
|
||||
<string name="server_lab_request_host_http">هاست http</string>
|
||||
<string name="server_lab_request_host_ws">هاست ws</string>
|
||||
@@ -81,7 +84,7 @@
|
||||
<string name="server_lab_stream_security" translatable="false">TLS</string>
|
||||
<string name="server_lab_stream_fingerprint">جا کلک</string>
|
||||
<string name="server_lab_stream_alpn" translatable="false">ALPN</string>
|
||||
<string name="server_lab_allow_insecure">تئیڌ گوواهی ن ناڌیڌه بگرین (allowInsecure)</string>
|
||||
<string name="server_lab_allow_insecure">تئیڌ گوواهی ن نیڌه بگرین (allowInsecure)</string>
|
||||
<string name="server_lab_sni" translatable="false">SNI</string>
|
||||
<string name="server_lab_address3">نشۊوی</string>
|
||||
<string name="server_lab_port3">پورت</string>
|
||||
@@ -98,41 +101,42 @@
|
||||
<string name="server_lab_mldsa65_verify" translatable="false">mldsa65Verify</string>
|
||||
<string name="server_lab_secret_key">کیلیت سیخومی</string>
|
||||
<string name="server_lab_reserved">Reserved(اختیاری، وا کاما (,) ز یک جوڌا ابۊن)</string>
|
||||
<string name="server_lab_local_address">نشۊوی محلی (اختیاری IPv4/IPv6، وا کاما (,) ز یک جوڌا ابۊن)</string>
|
||||
<string name="server_lab_local_mtu">MTU(اختیاری، پؽش فرض 1420)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU (اختیاری، پؽش فرض 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI (اختیاری، پؽش فرض 50 میلیثانیه)</string>
|
||||
<string name="server_lab_local_address">نشۊوی مهلی (اختیاری IPv4/IPv6، وا کاما (,) ز یک جوڌا ابۊن)</string>
|
||||
<string name="server_lab_local_mtu">MTU(اختیاری، پؽش فرز 1420)</string>
|
||||
<string name="server_lab_remote_dns">remoteDNS (اختياری، نشۊویا IPv4/IPv6 جوڌا وابیڌه وا کاما، یا «local» سی و کار گرؽڌن DNS دسگا)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU (اختیاری، پؽش فرز 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI (اختیاری، پؽش فرز 50 میلیسانیه)</string>
|
||||
<string name="toast_success">وا مووفقیت ٱنجوم وابی</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_decoding_failed">رزم گوشایی ٱنجوم نوابی</string>
|
||||
<string name="title_file_chooser">پسند فایل کانفیگ</string>
|
||||
<string name="toast_require_file_manager">ی برنومه دؽوۉداری فایل بپۊرنین.</string>
|
||||
<string name="server_customize_config">کانفیگ سفارشی</string>
|
||||
<string name="toast_config_file_invalid">کانفیگ نا موئتبر هڌ</string>
|
||||
<string name="server_lab_content">محتوا</string>
|
||||
<string name="toast_none_data_clipboard">هیچ داڌه ای من کلیپ بورد وۊجۊڌ نڌاره</string>
|
||||
<string name="server_lab_content">موئتوا</string>
|
||||
<string name="toast_none_data_clipboard">هیچ داده ای من کلیپ بورد وۊجۊڌ نڌاره</string>
|
||||
<string name="toast_invalid_url">نشۊوی اینترنتی نا موئتبر هڌ</string>
|
||||
<string name="toast_insecure_url_protocol">نشۊوی اشتراک پوروتوکول نا ٱمن HTTP ن و کار مبرین</string>
|
||||
<string name="server_lab_need_inbound">موطمئن بۊین ک پورت وۊرۊڌی وا سامووا ی جۊر هڌ</string>
|
||||
<string name="server_lab_need_inbound">موتمعن بۊین ک پورت وۊرۊڌی وا سامووا ی جۊر هڌ</string>
|
||||
<string name="toast_malformed_json">کانفیگ نا موئتبر هڌ</string>
|
||||
<string name="server_lab_request_host6">هاست(SNI)(اختیاری)</string>
|
||||
<string name="toast_action_not_allowed">ای کار ممنۊع هڌ</string>
|
||||
<string name="server_obfs_password">رزم obfs</string>
|
||||
<string name="server_lab_port_hop">پورت گوم (درگا سرورن ز نۊ هؽل اکونه)</string>
|
||||
<string name="server_lab_port_hop_interval">فاصله پورت گوم (ثانیه)</string>
|
||||
<string name="server_lab_port_hop">گوم پورت (پورت سرورن ز نۊ هؽل اکونه)</string>
|
||||
<string name="server_lab_port_hop_interval">فاسله گوم پورت (سانیه)</string>
|
||||
<string name="server_lab_bandwidth_down">ب لم ٱووڌن پئنا باند (واهڌ)</string>
|
||||
<string name="server_lab_bandwidth_up">وا روء رئڌن پئنا باند (واهڌ)</string>
|
||||
<string name="server_lab_xhttp_mode">حالت XHTTP</string>
|
||||
<string name="server_lab_xhttp_mode">هالت XHTTP</string>
|
||||
<string name="server_lab_xhttp_extra">XHTTP Extra خام JSON، قالوو: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask خام JSON، قالوو: { FinalMaskObject }</string>
|
||||
<string name="server_lab_ech_config_list" translatable="false">echConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">تئیڌ گوواهی هومتا و ری نوم</string>
|
||||
<string name="server_lab_pinned_ca256">جا کلک گوواهی (SHA-256)</string>
|
||||
<string name="server_lab_browser_dialer">Browser Dialer ن ره وستین</string>
|
||||
<string name="server_lab_browser_dialer_tip">تینا وۊرۊیا xhttp (packet-up) وو ws ن پوشتؽووی اکونه؛ گاشڌ سامووا وابسته و TLS ناڌیڌه گرفته ابۊن یا وا همن جۊر نین</string>
|
||||
<string name="toast_allow_insecure_deprecated">ای گر ز منپیز بی رمزنگاری کار اگیره. گاشڌ منپیزا شمو و دست دسگایل میووݩ شبکه ک و دست دولتا استبداڌی کنتورول ابۊن موستقیمن دیڌه ابۊ.\nسی ٱمنیت، ای جۊر گر یل وا هسته Xray نوسخه 26.2.6+ منپیز نابۊن.\nٱر گر خوتووݩ هڌ، رمزنگاری ٱمنی وری TLS ن ره وستین یا اثر انگشت گوواهی pinnedCA256 ن چفت کۊنین.\nٱر گر مال سرویس ڌهنده هڌ، ز سرویس ڌهنده بخواین ٱرتقا فنی ن ٱنجوم بڌه. ٱر همکاری نکونه، بئتره سرویس ڌهنده ی ک ب ٱمنیت کارورووݩ بؽتر اهمیت اڌه پسند کۊنین.\nسی دووسته بؽتر ورین و https://github.com/2dust/v2rayN/discussions/9460</string>
|
||||
<string name="server_lab_browser_dialer">Browser Dialer ره وندن</string>
|
||||
<string name="server_lab_browser_dialer_tip">تینا خۊرۊجیا xhttp (packet-up) وو ws لادراری ابۊن؛ سامووا وابسته و TLS گاشڌ نیڌه گرؽڌه بۊن یا تداخول داشته بۊ</string>
|
||||
<string name="toast_allow_insecure_deprecated">گره هیم سکویی منپیز رزم نگاری نوابیڌه ن و کار اگره. زؽرساختا منجا شبکه ک من کونتورول هۊکۊمتا اقتدارگرا هڌن گاشڌ ارتباتا ایسا ن موستقیمن بنیرن.\nسی دلیلا ٱمنیتی، منپیز و ای گره هایی ک وا هسته Xray نوسخه 26.2.6 وو روء تر امکووݩ پزیر نؽڌ.\nٱر گره ن خوتووݩ ره وندین، رزم نگاری ٱمنی جۊر TLS ن ره ونین یا جا کلک گوواهی pinnedCA256 ن پین کۊنین.\nٱر گره ز ارائه کون خدمات هڌ، سی ارتقای فنی بسووݩ زنگ بزݩ. ٱر ارائه کون واتووݩ هیاری نکرد، پؽشنهاڌ ابۊ و ارائه کونی ریوین ک سی ٱمنیت منتورووݩ ٱهمیت قلوه ای اده.\nسی دووسمندیا قلوه و https://github.com/2dust/v2rayN/discussions/9460 سر بزنین.</string>
|
||||
<string name="pinned_ca256_action_fetch">گرؽڌن وو پور کردن جا کلک گوواهی</string>
|
||||
<string name="toast_fetch_cert_sha256_success">جا کلک گوواهی وا مووفقیت گرؽڌه وابی</string>
|
||||
<string name="toast_fetch_cert_sha256_failed">گرؽڌن جا کلک گوواهی مووفق نبی</string>
|
||||
@@ -145,10 +149,10 @@
|
||||
<string name="menu_item_scan_qrcode">اسکن کود QR</string>
|
||||
<string name="title_url" translatable="false">URL</string>
|
||||
<string name="menu_item_download_file">دانلود فایلا</string>
|
||||
<string name="title_user_asset_add_url">نشۊوی اینترنتی بونچک ن اضاف کۊنین</string>
|
||||
<string name="title_user_asset_add_url">نشۊوی اینترنتی بونچک ن ازاف کۊنین</string>
|
||||
<string name="msg_file_not_found">فایلن نجوست</string>
|
||||
<string name="msg_remark_is_duplicate">ای نوم ز زیتر بیڌس</string>
|
||||
<string name="asset_geo_files_sources">بونچک فایلا جغرافیایی (اختیاری)</string>
|
||||
<string name="asset_geo_files_sources">بونچک فایلا جوقرافیایی (اختیاری)</string>
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">هونی بار ونی ابۊ…</string>
|
||||
@@ -156,14 +160,14 @@
|
||||
<string name="menu_item_select_all">پسند پوی</string>
|
||||
<string name="menu_item_invert_selection">چواسه کردن پسندا</string>
|
||||
<string name="msg_enter_keywords">کلمه یل کلیڌی ن بزنین</string>
|
||||
<string name="switch_bypass_apps_mode">حالت دور زیڌن</string>
|
||||
<string name="switch_bypass_apps_mode">هالت دور زیڌن</string>
|
||||
<string name="menu_item_select_proxy_app">پسند خوتکار برنومه یلی ک پروکسی اخون</string>
|
||||
<string name="msg_downloading_content">محتوا هونی دانلود ابۊن…</string>
|
||||
<string name="msg_downloading_content">موئتوا هونی دانلود ابۊ…</string>
|
||||
<string name="menu_item_export_proxy_app">و در کشیڌن من کلیپ بورد</string>
|
||||
<string name="menu_item_import_proxy_app">و من ٱووردن ز کلیپ بورد</string>
|
||||
<string name="per_app_proxy_settings">سامووا ب تفکیک برنومه</string>
|
||||
<string name="per_app_proxy_settings_enable">ره وندن ب تفکیک برنومه</string>
|
||||
<string name="app_picker_unknown_app">برنومه ناناس (UID ناساڌه)</string>
|
||||
<string name="app_picker_unknown_app">برنومه ناشناس (UID شناسایی نوابیڌه)</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">سامووا</string>
|
||||
@@ -179,9 +183,9 @@
|
||||
<string name="toast_system_vpn_settings_unavailable">سامووا VPN سیستوم من دسرس نین.</string>
|
||||
|
||||
|
||||
<string name="subscription_updater_job_tips">وزیفه من پس زمینه کار اکونه وو ایلاعات پؽشرفت من اعلان نشووݩ داڌه ابۊ.</string>
|
||||
<string name="title_pref_auto_test_after_update_subscription">تست خوتکار بئڌ و رۊز کۊنی اشتراک</string>
|
||||
<string name="summary_pref_auto_test_after_update_subscription">تست خوتکار زمووݩ زیاڌی ابره. ٱر ره وست بۊ، وزیفه من پس زمینه کار اکونه؛ وگرنه من همین بلگه کار اکونه.</string>
|
||||
<string name="subscription_updater_job_tips">ای کار من پس زمینه ره اوۊفته وو پؽش رئڌن هو منه وارسۊوی نشووݩ داڌه ابۊ.</string>
|
||||
<string name="title_pref_auto_test_after_update_subscription">تست خوتکار بئڌ ورۊ رسۊوی کردن اشتراک</string>
|
||||
<string name="summary_pref_auto_test_after_update_subscription">تست خوتکار زمووݩ زیاڌی ابره. ٱر ره وسته بۊوه، وزیفه من پس زمینه کار اکونه؛ ٱندی من هیم بلگه کار اکونه.</string>
|
||||
<string name="title_pref_auto_remove_invalid_after_test">پاک کردن خوتکار کانفیگا نا موئتبر بئڌ پینگ</string>
|
||||
<string name="summary_pref_auto_remove_invalid_after_test">نتیجه یل پینگ گاشڌ دییق نبۊن؛ کانفیگا پاک وابیڌه ن نتری وورگنی.</string>
|
||||
<string name="title_pref_auto_sort_after_test">ترتیب خوتکار بئڌ پینگ گرؽڌن</string>
|
||||
@@ -189,7 +193,7 @@
|
||||
|
||||
<string name="title_mux_settings">سامووا Mux</string>
|
||||
<string name="title_pref_mux_enabled">ره وندن Mux</string>
|
||||
<string name="summary_pref_mux_enabled">زل تر، ٱما گاشڌ منپیز زی قط بۊ\nدؽوۉداری TCP، UDP وو QUIC ن ای لم سفارشی کۊنین.</string>
|
||||
<string name="summary_pref_mux_enabled">زل تر، ٱما گاشڌ منپیز زی قت بۊ\nدؽوۉداری TCP ،UDP وو QUIC ن ای لم سفارشی کۊنین.</string>
|
||||
<string name="title_pref_mux_concurrency">منپیزا TCP (تلایه منجا -1 تا 1024)</string>
|
||||
<string name="title_pref_mux_xudp_concurrency">منپیزا XUDP (تلایه منجا -1 تا 1024)</string>
|
||||
<string name="title_pref_mux_xudp_quic">دؽوۉداری QUIC من تۊنل mux</string>
|
||||
@@ -200,21 +204,21 @@
|
||||
</string-array>
|
||||
|
||||
<string name="title_pref_speed_enabled">ره وندن نشووݩ داڌن سورعت</string>
|
||||
<string name="summary_pref_speed_enabled">سورعت هیم سکویی من وارسۊوی نشووݩ داڌه ابۊ.\nنماڌ نشووݩ اڌه ک ترافیک پروکسی یا موستقیم بؽتره.</string>
|
||||
<string name="summary_pref_speed_enabled">سورعت هیم سکویی من وارسۊوی نشووݩ داڌه ابۊ.\nنماڌ نشووݩ اده ک ترافیک پروکسی قلوه هڌ یا ترافیک موستقیم.</string>
|
||||
|
||||
<string name="title_pref_sniffing_enabled">ره وندن در کشیڌن نوم دامنه موورد نظر (Sniffing)</string>
|
||||
<string name="summary_pref_sniffing_enabled">قپ ریت سی و در کشیڌن نوم دامنه واقعی ز کتنا (پؽش فرض رۊشن هڌ)</string>
|
||||
<string name="title_pref_sniffing_enabled">ره وندن و در کشیڌن نوم دامنه موورد نزر (Sniffing)</string>
|
||||
<string name="summary_pref_sniffing_enabled">قپ ریت سی و در کشیڌن نوم دامنه واقعی ز کتنا (پؽش فرز رۊشن هڌ)</string>
|
||||
<string name="title_pref_route_only_enabled">ره وندن تینا تور جوستن routeOnly</string>
|
||||
<string name="summary_pref_route_only_enabled">نوم دامنه sniffed ن تینا سی تور جوستن و کار بگیرین وو نشۊوی موورد نظرن و عونوان نشۊوی IP ووردارین.</string>
|
||||
<string name="summary_pref_route_only_enabled">نوم دامنه sniffed ن تینا سی تور جوستن و کار بگیرین وو نشۊوی موورد نزرن و عونوان نشۊوی IP ووردارین.</string>
|
||||
|
||||
<string name="title_pref_local_dns_enabled">ره وندن DNS محلی</string>
|
||||
<string name="summary_pref_local_dns_enabled">درخواستا DNS و هسته و من ایان وو و دست ماژول DNS پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین ره ونده بۊ)</string>
|
||||
<string name="title_pref_local_dns_enabled">ره وندن DNS مهلی</string>
|
||||
<string name="summary_pref_local_dns_enabled">درخاستا DNS و هسته و من ایان وو و دست ماژول DNS پردازشت ابۊن (پؽشنهاڌ ابۊ ٱر لنگ تور جوستن سی دور زیڌن نشۊویا LAN وو وولات ٱسلی هڌین ره ونده بۊ)</string>
|
||||
|
||||
<string name="title_pref_fake_dns_enabled">ره وندن DNS جئلی (FakeDNS)</string>
|
||||
<string name="summary_pref_fake_dns_enabled">FakeDNS نشۊویا IP جئلی ن سی نوم دامنه اوورگنه (زل تر، ٱما گاشڌ من ی قرده ز برنومه یل کار نکونه).</string>
|
||||
|
||||
<string name="title_pref_ipv6_enabled">ره وندن IPv6</string>
|
||||
<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="summary_pref_prefer_ipv6">مجال هل وو فسل نوم دامنه، نشۊویا IPv6 ن ترجی بڌین</string>
|
||||
@@ -223,11 +227,11 @@
|
||||
<string name="summary_pref_remote_dns" translatable="false">DNS</string>
|
||||
|
||||
<string name="title_pref_vpn_dns">VPN DNS (تینا IPv4/IPv6)</string>
|
||||
<string name="title_pref_vpn_bypass_lan">VPN ز شبکه محلی اگوڌرته؟</string>
|
||||
<string name="title_pref_vpn_bypass_lan">VPN ز شبکه مهلی اگوڌرته؟</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>
|
||||
@@ -243,74 +247,74 @@
|
||||
<string name="title_pref_ip_api_url">نشۊوی اینترنتی آزمایش دووسمندیا منپیز هیم سکویی</string>
|
||||
<string name="summary_pref_ip_api_url" translatable="false">URL</string>
|
||||
|
||||
<string name="title_pref_proxy_sharing_enabled">یک رسۊوی پروکسی من شبکه محلی LAN</string>
|
||||
<string name="title_pref_proxy_sharing_enabled">یک رسۊوی پروکسی من شبکه مهلی LAN</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">پوی دسگایل ترن وا نشۊوی IP ایسا، ز ره socks/http و پروکسی منپیز بۊن، تینا من شبکه قابل ائتماڌ ره ونده بۊ تا ز منپیز غیر موجاز جلو گری بۊ.</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">یک رسۊوی من شبکه محلی ره وست؛ موطمئن بۊین ک من ی شبکه ٱمن هڌین.</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">یک رسۊوی من شبکه مهلی ره وست؛ موتمعن بۊین ک من ی شبکه ٱمن هڌین.</string>
|
||||
|
||||
<string name="title_pref_socks_port">پورت پروکسی محلی</string>
|
||||
<string name="title_pref_enable_local_proxy">ره وندن پروکسی محلی</string>
|
||||
<string name="title_pref_socks_port">پورت پروکسی مهلی</string>
|
||||
<string name="title_pref_enable_local_proxy">ره وندن پروکسی مهلی</string>
|
||||
<string name="summary_pref_enable_local_proxy">هرسا ره نوسته بۊ، هیچ پروکسی SOCKS من دسرس نؽ، سی دل هیمو کارایی جۊر ورۊ رسۊوی یل اشتراک نترن پروکسی ن و کار گرن.</string>
|
||||
<string name="title_pref_socks_enable_udp" translatable="false">SOCKS5 UDP</string>
|
||||
<string name="summary_pref_socks_enable_udp">ائراز هۊویت SOCKS5 مجال استفاڌه ز UDP کاملن ایمن نؽ.</string>
|
||||
<string name="summary_pref_socks_port">پورت پروکسی محلی</string>
|
||||
<string name="title_pref_dynamic_socks_port">وورکل پورت موتغیر سی پروکسی محلی</string>
|
||||
<string name="summary_pref_dynamic_socks_port">هر کرت ک و من ٱووڌنی وورکل ابۊ، ی پورت پروکسی محلی تساڌۊفی وورکل اکونه.</string>
|
||||
<string name="title_pref_socks_username">نوم منتوری پروکسی محلی (اختیاری)</string>
|
||||
<string name="summary_pref_socks_port">پورت پروکسی مهلی</string>
|
||||
<string name="title_pref_dynamic_socks_port">وورکل پورت موتغیر سی پروکسی مهلی</string>
|
||||
<string name="summary_pref_dynamic_socks_port">هر کرت ک و من ٱووڌنی وورکل ابۊ، ی پورت پروکسی مهلی تساڌۊفی وورکل اکونه.</string>
|
||||
<string name="title_pref_socks_username">نوم منتوری پروکسی مهلی (اختیاری)</string>
|
||||
<string name="summary_pref_socks_username">نوم منتوری</string>
|
||||
<string name="title_pref_socks_password">رزم پروکسی محلی (اختیاری)</string>
|
||||
<string name="title_pref_socks_password">رزم پروکسی مهلی (اختیاری)</string>
|
||||
<string name="summary_pref_socks_password">رزم</string>
|
||||
|
||||
<string name="title_pref_local_dns_port">پورت DNS محلی</string>
|
||||
<string name="summary_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="title_pref_confirm_remove">قوۊل کردن پاک کردن کانفیگ</string>
|
||||
<string name="summary_pref_confirm_remove">سی پاک وابیڌن فایل کانفیگ، لنگ قوۊل کردن دووارته ز سمت منتور هڌ.</string>
|
||||
|
||||
|
||||
<string name="title_pref_append_http_proxy">پروکسی HTTP ن و VPN اضاف کۊنین</string>
|
||||
<string name="title_pref_append_http_proxy">پروکسی HTTP ن و VPN ازاف کۊنین</string>
|
||||
<string name="summary_pref_append_http_proxy">پروکسی HTTP ن موستقیمن ز (مۊرۊرگر/ی قرد ز برنومه یل لادراری بیڌه)، بؽ استفاڌه ز دسگا NIC مجازی (Android 10+) استفاڌه ابۊ.</string>
|
||||
|
||||
<string name="title_pref_double_column_display">ره وندن نشووݩ داڌن دو سۊتۊنی</string>
|
||||
<string name="summary_pref_double_column_display">نومگه کانفیگا من دو ستۊݩ نشووݩ داڌه ابۊ تا محتوا بؽتری من بلگه جا بگره.</string>
|
||||
<string name="summary_pref_double_column_display">نومگه کانفیگا من دو سۊتۊن نشووݩ داڌه ابۊ تا موئتوا قلوه ای من بلگه جا بگره.</string>
|
||||
|
||||
<string name="title_pref_group_all_display">نشووݩ داڌن پوی بونکۊ یل ن ره ونین</string>
|
||||
<string name="summary_pref_group_all_display">ی بلگه «پوی بونکۊ یل کانفیگ» اضاف کۊنین</string>
|
||||
<string name="title_pref_group_all_display">نشووݩ داڌن بلگه «پوی»</string>
|
||||
<string name="summary_pref_group_all_display">نشووݩ داڌن پوروفایلا پوی بونکۊیل اشتراک من ی بلگه.</string>
|
||||
|
||||
<!-- AboutActivity -->
|
||||
<string name="title_pref_feedback">فشناڌن منشڌ</string>
|
||||
<string name="summary_pref_feedback">فشناڌن منشڌ یا گوزارش موشکلا من GitHub</string>
|
||||
<string name="summary_pref_tg_group">ٱووڌن من بونکۊ تلگرام</string>
|
||||
<string name="toast_tg_app_not_found">برنومه تلگرامن نجوست</string>
|
||||
<string name="title_privacy_policy">حریم سیخومی</string>
|
||||
<string name="title_privacy_policy">هریم سیخومی</string>
|
||||
<string name="title_qr_code">کود QR</string>
|
||||
<string name="title_about">زبار</string>
|
||||
<string name="title_source_code">کود بونچک</string>
|
||||
<string name="title_oss_license">موجوزا کود بونچک</string>
|
||||
<string name="title_translators">ترجومهگرا</string>
|
||||
<string name="title_translators">ولرنی کارووݩ</string>
|
||||
<string name="title_tg_channel">تورگه تلگرام</string>
|
||||
|
||||
<string name="title_pref_promotion">تبلیغات</string>
|
||||
<string name="title_pref_promotion">تبلیقات</string>
|
||||
|
||||
<string name="title_pref_auto_update_subscription">ورۊ کردن خوتکار اشتراکا</string>
|
||||
<string name="summary_pref_auto_update_subscription">اشتراکا خوتووݩ ن و تۉر خوتکار وا فاصله زمۊوی من پس زمینه ورۊ کۊنین. ای ویژیی من پوی دسگایل گاشڌ همیشه کار نکونه</string>
|
||||
<string name="title_pref_auto_update_interval">فاصله ورۊ کردن خوتکار (اقلن وا 15 دؽقه بۊ)</string>
|
||||
<string name="summary_pref_auto_update_subscription">اشتراکا خوتووݩ ن و تۉر خوتکار وا فاسله زمۊوی من پس زمینه ورۊ کۊنین. ای ویژیی من پوی دسگایل گاشڌ هی کار نکونه</string>
|
||||
<string name="title_pref_auto_update_interval">فاسله ورۊ کردن خوتکار (ٱقلن وا 15 دؽقه بۊ)</string>
|
||||
|
||||
<string name="title_core_loglevel">سئت گوزارشا</string>
|
||||
<string name="title_outbound_domain_resolve_method">بارت پؽش هل دامنه دری (outbound)</string>
|
||||
<string name="title_mode">حالت</string>
|
||||
<string name="title_mode">هالت</string>
|
||||
<string name="title_mode_help">سی دووسمندیا وو هیاری قلوه، ری ای هؽل بزݩ</string>
|
||||
<string name="title_language">زووݩ</string>
|
||||
<string name="title_ui_settings">سامووا رابط منتوری</string>
|
||||
<string name="title_pref_ui_mode_night">سامووا حالت رابط منتوری</string>
|
||||
<string name="title_pref_dynamic_color">فعالسازی رنگ پویا</string>
|
||||
<string name="summary_pref_dynamic_color">فقط روی اندروید ۱۲+ کار میکند</string>
|
||||
<string name="title_ui_settings">سامووا رابت منتوری</string>
|
||||
<string name="title_pref_ui_mode_night">سامووا هالت رابت منتوری</string>
|
||||
<string name="title_pref_dynamic_color">ره وندن رنگ پۊیا</string>
|
||||
<string name="summary_pref_dynamic_color">فقت ری ٱندروید 12+ کار اکونه</string>
|
||||
<string name="title_pref_use_hev_tunnel">و کار گرؽڌن ویژیی Hev TUN</string>
|
||||
<string name="summary_pref_use_hev_tunnel">مجالی ک ره ونده بۊ، TUN ایا hev-socks5-tunnel ن و کار اگره؛ ٱر ره نوۊفته بۊ، همو xray-core ن و کار اگره.</string>
|
||||
<string name="title_pref_hev_tunnel_loglevel">Hev TUN سئت گوزارشا</string>
|
||||
<string name="title_pref_hev_tunnel_rw_timeout">زمووݩ مندیر بیڌن خوندن/هؽل کردن Hev TUN (ثانیه؛ پؽشفرض TCP/UDP: 300/60)</string>
|
||||
<string name="title_mode_settings">سامووا حالت</string>
|
||||
<string name="title_root_mode_enabled">حالت رۊت ره وست (منتورووݩ رۊت)</string>
|
||||
<string name="summary_root_mode_enabled">ره وستن پروکسی شفاف سئت بلم وا امتیازا ریشه. ای حالت، VPN استاندارد سیستوم ٱندروید ن دور ازنه تا موستقیمن پوی ترافیک شبکه لمی ن وا دست بگره. بئڌ ره وندن، حالت معمۊلی ٱسلی، تور جۊری هر برنومه وو سامووا VPN مربۊته کاملن نا موئتبر ابۊن.</string>
|
||||
<string name="title_pref_hev_tunnel_loglevel">سئت گوزارشا Hev TUN</string>
|
||||
<string name="title_pref_hev_tunnel_rw_timeout">زمووݩ مندیر بیڌن خوندن/هؽل کردن Hev TUN (سانیه؛ پؽشفرز TCP/UDP: 300/60)</string>
|
||||
<string name="title_mode_settings">سامووا هالت</string>
|
||||
<string name="title_root_mode_enabled">هالت رۊت ره وست (منتورووݩ رۊت)</string>
|
||||
<string name="summary_root_mode_enabled">ره وستن پروکسی شفاف سئت بلم وا امتیازا ریشه. ای هالت، VPN استاندارد سیستوم ٱندروید ن دور ازنه تا موستقیمن پوی ترافیک شبکه لمی ن وا دست بگره. بئڌ ره وندن، هالت معمۊلی ٱسلی، تور جۊری هر برنومه وو سامووا VPN مربۊته کاملن نا موئتبر ابۊن.</string>
|
||||
<string name="toast_root_required">دسرسی رۊت سی ای ویژیی لازوم هڌ</string>
|
||||
<string name="title_root_lan_sharing">یک رسۊوی LAN / تترینگ (منتورووݩ رۊت وابیڌه)</string>
|
||||
<string name="summary_root_lan_sharing">ترافیک هاتاسپات وایفای وو دسگا یل منپیز وابیڌه وا USB (تترینگ) ن ز تور VPN بفشنین.</string>
|
||||
@@ -328,31 +332,37 @@
|
||||
<string name="sub_setting_remarks">نیشتنا</string>
|
||||
<string name="sub_setting_url">نشۊوی اینترنتی اختیاری</string>
|
||||
<string name="sub_setting_user_agent" translatable="false">User Agent</string>
|
||||
<string name="sub_setting_request_headers">سرآیندا درخواست (قالوو JSON)</string>
|
||||
<string name="sub_setting_request_headers">هدر درخاست (قالوو JSON)</string>
|
||||
<string name="sub_setting_filter">فیلتر نیشتنا وا regex</string>
|
||||
<string name="sub_setting_enable">ره وندن ورۊ کردن</string>
|
||||
<string name="sub_auto_update">ره وندن ورۊ کردن خوتکار</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_pre_profile_tip">موطمئن بۊ ک نیشتنا هڌس وو جۊرس نی</string>
|
||||
<string name="sub_setting_entry_proxy_tip">ای پروکسی پؽش ز هر کانفیگ من ای اشتراک، وری ٱول عوزو زنجیره پروکسی اضاف ابۊ.</string>
|
||||
<string name="sub_setting_exit_proxy_tip">ای پروکسی بئڌ ز هر کانفیگ من ای اشتراک، وری آخر عوزو زنجیره پروکسی اضاف ابۊ.</string>
|
||||
<string name="toast_invalid_update_interval">فاصله ورۊ کردن نا موئتبر هڌ. حداقل مقدار 15 دؽقه هڌ.</string>
|
||||
<string name="sub_setting_pre_profile_tip">موتمعن بۊ ک نوم موستعار ن هڌس وو جۊرس نی</string>
|
||||
<string name="sub_setting_entry_proxy_tip">پروکسی پسند وابیڌه پؽش ز هر کانفیگ من ای اشتراک، و عونوان عوزو نیایی زنجیره پروکسی ازاف ابۊ.</string>
|
||||
<string name="sub_setting_exit_proxy_tip">پروکسی پسند وابیڌه بئڌ ز هر کانفیگ من ای اشتراک، و عونوان عوزو دیندایی زنجیره پروکسی ازاف ابۊ.</string>
|
||||
<string name="toast_invalid_update_interval">فاسله ورۊ کردن نا موئتبر هڌ. حداقل مقدار 15 دؽقه هڌ.</string>
|
||||
<string name="title_sub_update">ورۊ کردن اشتراک بونکۊ سکویی</string>
|
||||
<string name="title_ping_all_server">آزمایش تئخیر TCP (TCPing)</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_filter_config">فیلتر کردن کانفیگا</string>
|
||||
<string name="filter_config_all">پوی</string>
|
||||
<string name="summary_scan_qrcode">سی وا کردن دوربیݩ وو ٱسکووݩ، و ری بلگه بزنین</string>
|
||||
<string name="summary_scan_qrcode">سی گۊشیڌن شؽواتگر وو اسکن، و ری بلگه بزنین</string>
|
||||
<string name="title_del_duplicate_config_count">پاک کردن %d کانفیگ تکراری</string>
|
||||
<string name="title_del_config_count">پاک کردن %d کانفیگ</string>
|
||||
<string name="title_import_config_count">و من ٱووردن %d کانفیگ</string>
|
||||
<string name="title_export_config_count">و در کشیڌن %d کانفیگ</string>
|
||||
<string name="title_update_config_count">ورۊ کردن %d کانفیگ</string>
|
||||
<string name="title_update_asset_count">%d فایل بونچک جغرافیایی ورۊ وابی</string>
|
||||
<plurals name="title_update_config_count">
|
||||
<item quantity="one">%1$d کانفیگ ورۊ وابی</item>
|
||||
<item quantity="other">%1$d کانفیگ ورۊ وابی</item>
|
||||
</plurals>
|
||||
<plurals name="title_update_asset_count">
|
||||
<item quantity="one">%1$d فایل بونچک جوقرافیایی ورۊ وابی</item>
|
||||
<item quantity="other">%1$d فایل بونچک جوقرافیایی ورۊ وابی</item>
|
||||
</plurals>
|
||||
<string name="title_update_subscription_result">%1$d کانفیگ ورۊ وابی (مووفق: %2$d، شکست خرده: %3$d، رڌ وابیڌه: %4$d)</string>
|
||||
<string name="title_update_subscription_no_subscription">بؽ اشتراک</string>
|
||||
<string name="toast_server_not_found_in_group">سرور پسند وابیڌه من بونکۊ سکویی نجۊرست</string>
|
||||
@@ -364,7 +374,7 @@
|
||||
<!-- RoutingSettingActivity -->
|
||||
<string name="routing_settings_domain_strategy">نشقه دامنه</string>
|
||||
<string name="routing_settings_title">سامووا تور جوستن</string>
|
||||
<string name="routing_settings_tips">پوی شرطا سئت بیڌه بایڌ هوم زمووی جور ابۊن (AND). قانووݩ یل ز سر و ته وارس ابۊن؛ ٱولی قانووݩ جور وابیڌه و کار ابسته ابۊ.</string>
|
||||
<string name="routing_settings_tips">ی قانووݩ تینا زمووی ائمال ابۊ ک پوی شرتا ی جۊر بیڌه و من ٱووڌه هوم زمووݩ برقرار بۊن (AND). قانووا ز روء و بلم واجۊری ابۊن وو ٱولین قانووݩ ی جۊر، ائمال ابۊ.</string>
|
||||
<string name="routing_settings_comma_tip">چنتا مقدار ن وا کاما (,) ز یک جوڌا کۊنین.</string>
|
||||
<string name="routing_settings_save">زفت کردن</string>
|
||||
<string name="routing_settings_delete">روفتن</string>
|
||||
@@ -374,47 +384,47 @@
|
||||
<string name="routing_settings_import_rulesets_tip">قانووایی ک هیم سکو هڌسووݩ پاک ابۊن، هنی هم اخۊی پاکسووݩ کۊنی؟</string>
|
||||
<string name="routing_settings_import_rulesets_from_clipboard">و من ٱووردن قانووا ز کلیپ بورد</string>
|
||||
<string name="routing_settings_import_rulesets_from_qrcode">و من ٱووردن قانووا ز کود QR</string>
|
||||
<string name="routing_settings_export_rulesets_to_clipboard">و در کشیڌن قانووا وو زفت من کلیپ بورد</string>
|
||||
<string name="routing_settings_export_rulesets_to_clipboard">و در کشیڌن قانووا وو زفت کردن من کلیپ بورد</string>
|
||||
<string name="routing_settings_locked">چفت هڌ، ای قانووݩ ن مجال و من ٱووردن ز پؽش سامووا زفت کۊنین</string>
|
||||
<string name="routing_settings_domain" translatable="false">domain</string>
|
||||
<string name="routing_settings_ip" translatable="false">ip</string>
|
||||
<string name="routing_settings_process">process (نوم بسته — تینا مجال کار بوردن Xray TUN، ره بیڌن routeOnly وو Android 10+ پوشتؽووی ابۊ)</string>
|
||||
<string name="routing_settings_process">process (نوم بسته — تینا مجال و کار گرؽڌن Xray TUN، ره وسته بیڌن routeOnly وو ٱندروید 10+ لادراری ابۊ)</string>
|
||||
<string name="routing_settings_process_select">نوم بسته ن پسند کۊنین</string>
|
||||
<string name="routing_settings_port" translatable="false">port</string>
|
||||
<string name="routing_settings_protocol" translatable="false">protocol</string>
|
||||
<string name="routing_settings_protocol_tip" translatable="false">[http,tls,bittorrent]</string>
|
||||
<string name="routing_settings_network" translatable="false">network</string>
|
||||
<string name="routing_settings_outbound_tag_hint">proxy / direct / block / <%1$s></string>
|
||||
<string name="routing_settings_outbound_tag_hint" translatable="false">proxy / direct / block / <%1$s></string>
|
||||
<string name="routing_settings_outbound_tag" translatable="false">outboundTag</string>
|
||||
|
||||
<string name="connection_test_pending">منپیزن واجۊری کوݩ</string>
|
||||
<string name="connection_test_testing">هونی آزمایش ابۊ…</string>
|
||||
<string name="connection_test_testing_count">%d کانفیگ هونی آزمایش ابۊ…</string>
|
||||
<string name="connection_test_available">مووفق بی: منپیز %1$s طۊل کشی</string>
|
||||
<string name="server_test_delay_value">%1$d میلیثانیه</string>
|
||||
<string name="connection_test_available">مووفق بی: منپیز %1$s تۊل کشی</string>
|
||||
<string name="server_test_delay_value">%1$d میلیسانیه</string>
|
||||
<string name="connection_test_error">منپیز و اینترنتن نجوست: %s</string>
|
||||
<string name="connection_test_fail">اینترنت من دسرس نؽ</string>
|
||||
<string name="connection_test_error_status_code">کود خطا: #%d</string>
|
||||
<string name="connection_test_error_status_code">کود ختا: #%d</string>
|
||||
<string name="connection_connected">منپیز هڌ. سی واجۊری کیلیک کوݩ.</string>
|
||||
<string name="connection_not_connected">منپیز نؽڌ</string>
|
||||
<string name="connection_running_task_left">آزمایشا هونی ٱنجوم ابۊن: %s</string>
|
||||
|
||||
<string name="import_subscription_success">اشتراک وا مووفقیت و من ٱووڌه وابی</string>
|
||||
<string name="import_subscription_success">اشتراک وا مووفقیت و من ٱوورده وابی</string>
|
||||
<string name="import_subscription_failure">و من ٱووردن اشتراک مووفق نبی</string>
|
||||
<string name="title_fragment_settings">سامووا فرگمنت (Fragment)</string>
|
||||
<string name="title_pref_fragment_packets">کتنا فرگمنت</string>
|
||||
<string name="title_pref_fragment_length">طۊل کتنا فرگمنت (حداقل-حداکثر)</string>
|
||||
<string name="title_pref_fragment_interval">فاصله منجا کتنا فرگمنت (حداقل-حداکثر)</string>
|
||||
<string name="title_pref_fragment_length">تۊل کتنا فرگمنت (هدقل-هدکسر)</string>
|
||||
<string name="title_pref_fragment_interval">فاسله منجا کتنا فرگمنت (هدقل-هدکسر)</string>
|
||||
<string name="title_pref_fragment_enabled">ره وندن فرگمنت</string>
|
||||
<string name="title_pref_fragment_maxsplit">حداکثر تقسیم فرگمنت</string>
|
||||
<string name="title_pref_fragment_maxsplit">هدکسر تقسیم فرگمنت</string>
|
||||
<string name="title_observatory_settings">سامووا واجۊری منپیز (Observatory)</string>
|
||||
<string name="title_pref_observatory_least_ping_interval">فاصله leastPing</string>
|
||||
<string name="title_pref_observatory_least_load_interval">فاصله leastLoad</string>
|
||||
<string name="title_pref_observatory_least_load_method">روش HTTP سی leastLoad</string>
|
||||
<string name="title_pref_observatory_least_load_sampling">نمونه گیری leastLoad</string>
|
||||
<string name="title_pref_observatory_least_load_timeout">مهلت leastLoad</string>
|
||||
<string name="toast_invalid_observatory_duration">زمووݩ نا موئتبر هڌ. ارزشی وری 3m یا 30s ن و کار بگیرین.</string>
|
||||
<string name="toast_invalid_observatory_sampling">نمونه گیری نا موئتبر هڌ. شوماره ی مثبت ن و کار بگیرین.</string>
|
||||
<string name="title_pref_observatory_least_ping_interval">فاسله leastPing</string>
|
||||
<string name="title_pref_observatory_least_load_interval">فاسله leastLoad</string>
|
||||
<string name="title_pref_observatory_least_load_method">بارت HTTP سی leastLoad</string>
|
||||
<string name="title_pref_observatory_least_load_sampling">نمووه گیری leastLoad</string>
|
||||
<string name="title_pref_observatory_least_load_timeout">موهلت leastLoad</string>
|
||||
<string name="toast_invalid_observatory_duration">زمووݩ نا موئتبر هڌ. ٱرزشی و ری 3m یا 30s ن و کار بگیرین.</string>
|
||||
<string name="toast_invalid_observatory_sampling">نمووه گیری نا موئتبر هڌ. شوماره ای موسبت ن و کار بگیرین.</string>
|
||||
|
||||
<string name="update_check_for_update">واجۊری سی ورۊ رسۊوی</string>
|
||||
<string name="update_already_latest_version">سکو نوسخه دیندایی پۊرنیڌه هڌ</string>
|
||||
@@ -427,12 +437,12 @@
|
||||
<string name="title_policy_group_subscription_id">ز بونکۊ اشتراک</string>
|
||||
<string name="title_policy_group_subscription_filter">فیلتر نیشتنا وا regex</string>
|
||||
<string name="title_policy_group_test_outbounds">آزمایش بارتا (outbounds)</string>
|
||||
<string name="title_policy_group_fallback">بارت جایگزین (outbound، اختیاری)</string>
|
||||
<string name="title_policy_group_fallback">بارت جایگۊزین (outbound، اختیاری)</string>
|
||||
|
||||
<string name="server_proxy_chain_members">ٱئزا زنجیره پروکسی</string>
|
||||
<string name="server_proxy_chain_pick_members">سی پسند عوزو ایچو بزنین</string>
|
||||
<string name="server_proxy_chain_member_unselected">ی عوزو پسند کۊنین</string>
|
||||
<string name="server_proxy_chain_members_unselected">سی هر ردیف زنجیره ی عوزو پسند کۊنین</string>
|
||||
<string name="server_proxy_chain_members_unselected">سی هر ردیف زنجیره، ی عوزو پسند کۊنین</string>
|
||||
<string name="server_proxy_chain_members_insufficient">تعداڌ ٱئزا بس نؽ</string>
|
||||
<string name="server_proxy_chain_members_invalid">ٱئزا نا موئتبر زنجیره: %1$s</string>
|
||||
|
||||
@@ -440,11 +450,11 @@
|
||||
<string name="title_configuration_backup_restore">لادراری گرؽڌن & وورگندن</string>
|
||||
<string name="title_configuration_backup">لادراری گرؽڌن ز کانفیگ</string>
|
||||
<string name="title_configuration_restore">وورگندن کانفیگ</string>
|
||||
<string name="title_profile_storage_cleanup">جۊر کردن داڌه یل کانفیگا</string>
|
||||
<string name="summary_profile_storage_cleanup">پاک کردن داڌه یل کانفیگا ک دی و کار نؽن</string>
|
||||
<string name="message_profile_storage_cleanup">داڌه یل کانفیگا ک دی من هیچ بونکۊی و کار نؽن پاک ابۊن، هنی هم اخۊی پاکسووݩ کۊنی؟ کانفیگا پسند وابیڌه وو داڌه یلی ک نتری وا موطمئنی بفهمی دی و کار نؽن پاک نابۊن.</string>
|
||||
<string name="toast_profile_storage_cleanup">کانفیگا ک دی و کار نؽن پاک وابیڌن: %1$d</string>
|
||||
<string name="toast_profile_storage_cleanup_skipped">نتری وا موطمئنی بفهمی ک کۊی داڌه یل کانفیگا دی و کار نؽن. هیچ داڌه ای پاک نوابی.</string>
|
||||
<string name="title_profile_storage_cleanup">ترمیم جاگه زفت کردن پوروفایلا</string>
|
||||
<string name="summary_profile_storage_cleanup">پاک کردن داده یل پوروفایلایی ک دی و کار گرؽڌه نؽبۊن</string>
|
||||
<string name="message_profile_storage_cleanup">داده یل پوروفایلایی ک دی من هیچ بونکۊیی و کار گرؽڌه نؽبۊن پاک ابۊن، هنی هم اخۊی پاکسووݩ کۊنی؟ پوروفایل پسند وابیڌه وو داده یلی ک نتری وا موتمعنی بفهمی دی و کار گرؽڌه نؽبۊن پاک نؽبۊن.</string>
|
||||
<string name="toast_profile_storage_cleanup">پوروفایلایی ک دی و کار گرؽڌه نؽبۊن پاک وابیڌن: %1$d</string>
|
||||
<string name="toast_profile_storage_cleanup_skipped">نترست وا موتمعنی بفهمه ک کۊیکی ز داده یل پوروفایلا دی و کار گرؽڌه نؽبۊن. هیچ داده ای پاک نوابی.</string>
|
||||
<string name="title_configuration_share">یک رسۊوی کانفیگ</string>
|
||||
<string name="title_webdav_config_setting">WebDAV سامووا</string>
|
||||
<string name="title_webdav_config_setting_unknown">ٱول WebDAV ن کانفیگ کۊنین</string>
|
||||
@@ -454,7 +464,7 @@
|
||||
<string name="title_webdav_remote_path">دایرکتوری ره دیر (اختیاری)</string>
|
||||
|
||||
<string name="share_method_qrcode">کود QR</string>
|
||||
<string name="share_method_clipboard">لف گیری لینک کانفیگ</string>
|
||||
<string name="share_method_clipboard">لف گیری لینگ کانفیگ</string>
|
||||
<string name="share_method_full_content">لف گیری پوی کانفیگ</string>
|
||||
<string name="action_edit">آلشتکاری</string>
|
||||
<string name="action_delete">پاک کردن</string>
|
||||
@@ -469,7 +479,7 @@
|
||||
</string-array>
|
||||
|
||||
<string-array name="browser_dialer_mode">
|
||||
<item>خامۊش</item>
|
||||
<item>کۊر</item>
|
||||
<item>OkHttp</item>
|
||||
<item>WebView</item>
|
||||
</string-array>
|
||||
@@ -487,15 +497,15 @@
|
||||
<string name="routing_preset_russia_whitelist">نومگه اسبؽڌ رۊسیه</string>
|
||||
|
||||
<string-array name="vpn_bypass_lan">
|
||||
<item>پؽش فرض کانفیگ</item>
|
||||
<item>پؽش فرز کانفیگ</item>
|
||||
<item>دور زیڌه بۊ</item>
|
||||
<item>دور زیڌه نبۊ</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="outbound_domain_resolve_method">
|
||||
<item>هل وو فسل مکۊنین</item>
|
||||
<item>هل وو ٱووردن و میزبووݩ یل دامنه DNS</item>
|
||||
<item>هل وو جایونی دامنه</item>
|
||||
<item>هل وو فسل وو ٱووردن و هاستا دامنه DNS</item>
|
||||
<item>هل وو فسل وو جایگۊزینی دامنه</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="policy_group_type">
|
||||
@@ -505,35 +515,35 @@
|
||||
<item>گردشی (roundRobin)</item>
|
||||
</string-array>
|
||||
|
||||
<string name="backup_location_local">محلی</string>
|
||||
<string name="backup_location_local">مهلی</string>
|
||||
<string name="backup_location_webdav" translatable="false">WebDAV</string>
|
||||
|
||||
<string name="acc_refresh">وانۊ کردن گوزارش</string>
|
||||
<string name="toast_permission_denied_for">گرؽڌن موجوز مومکن نؽڌ: %1$s</string>
|
||||
<string name="acc_add_subscription">ٱووردن اشتراک</string>
|
||||
<string name="toast_malformed_json_detail">کانفیگ نا موئتبر هڌ: %1$s</string>
|
||||
<string name="acc_add_asset">ٱووردن بونچک فایلا جغرافیایی</string>
|
||||
<string name="acc_download_file">دانلود فایلا بونچک جغرافیایی</string>
|
||||
<string name="acc_add_asset">ٱووردن بونچک فایلا جوقرافیایی</string>
|
||||
<string name="acc_download_file">دانلود فایلا بونچک جوقرافیایی</string>
|
||||
<string name="acc_qr_code">کود QR</string>
|
||||
<string name="acc_copy_log">لف گیری گوزارش</string>
|
||||
<string name="acc_clear_log">روفتن گوزارش</string>
|
||||
<string name="acc_add_rule">ٱووردن قانووݩ</string>
|
||||
<string name="acc_start">ره وستن سرویس</string>
|
||||
<string name="acc_stop">واڌاشتن سرویس</string>
|
||||
<string name="acc_back">برگشت</string>
|
||||
<string name="acc_back">وورگشتن</string>
|
||||
<string name="acc_search">پیتینیڌن</string>
|
||||
<string name="acc_add">ٱووردن کانفیگ</string>
|
||||
<string name="acc_more">امکووݩا بؽتر</string>
|
||||
<string name="acc_selected_server">Selected</string>
|
||||
<string name="acc_share_subscription">یک رسۊوی لینک اشتراک</string>
|
||||
<string name="acc_more">گۊزینه یل قلوه</string>
|
||||
<string name="acc_selected_server">پسند وابیڌه</string>
|
||||
<string name="acc_share_subscription">یک رسۊوی لینگ اشتراک</string>
|
||||
<string name="acc_share_log">یک رسۊوی گوزارش</string>
|
||||
<string name="acc_remove">پاک کردن عوزو زنجیره پروکسی</string>
|
||||
<string name="acc_add_member">ٱووردن عوزو زنجیره پروکسی</string>
|
||||
<string name="acc_start_scanner">ره وستن اسکنر QR</string>
|
||||
<string name="acc_stop_scanner">واڌاشتن اسکنر QR</string>
|
||||
<string name="acc_turn_torch_on">روشن کردن چراغ</string>
|
||||
<string name="acc_turn_torch_off">خاموش کردن چراغ</string>
|
||||
<string name="acc_select_image">پسند ٱکس سی اسکن</string>
|
||||
<string name="acc_turn_torch_on">رۊشن کردن لیت</string>
|
||||
<string name="acc_turn_torch_off">کۊر کردن لیت</string>
|
||||
<string name="acc_select_image">پسند شؽوات سی اسکن</string>
|
||||
<string name="acc_locked">ای قانووݩ مجال و من ٱووردن ز پؽش سامووا زفت ابۊ</string>
|
||||
<string name="acc_open_menu">گۊشیڌن نومگه کشاری</string>
|
||||
<string name="acc_edit">آلشتکاری</string>
|
||||
@@ -541,14 +551,17 @@
|
||||
<string name="acc_save">زفت کردن</string>
|
||||
<string name="acc_update_subscriptions">ورۊ کردن اشتراکا</string>
|
||||
<string name="acc_per_app_proxy_information">دووسمندی پروکسی و ری برنومه</string>
|
||||
<string name="permission_camera">دوربیݩ</string>
|
||||
<string name="permission_camera">شؽواتگر</string>
|
||||
<string name="permission_notification">وارسۊوی</string>
|
||||
<string name="permission_local_network">شبکه محلی</string>
|
||||
<string name="permission_local_network">شبکه مهلی</string>
|
||||
<string name="subscription_update_updating">اشتراک %1$s هونی ورۊ ابۊ</string>
|
||||
<string name="subscription_update_progress">%2$s هونی آزمایش ابۊ: %1$s</string>
|
||||
<string name="tasker_blurb_start">ره وستن %1$s</string>
|
||||
<string name="tasker_blurb_stop">واڌاشتن %1$s</string>
|
||||
<string name="connection_test_empty_message">دووسمندی نؽ</string>
|
||||
<string name="value_unknown">نا مؽخص</string>
|
||||
<string name="value_unknown">نا موشخس</string>
|
||||
<string name="sub_info_open_support">واز کردن لینگ پشتیوانی</string>
|
||||
<string name="sub_info_open_page">واز کردن صفحه اطلاعات اشتراک</string>
|
||||
<string name="sub_info_unlimited">بیحد</string>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
@@ -14,6 +14,9 @@
|
||||
<string name="pull_down_to_refresh">برای بروزرسانی، صفحه را به پایین بکشید!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_channel_service">سرویس v2rayNG</string>
|
||||
<string name="notification_channel_subscription_updates">بهروزرسانی اشتراکها</string>
|
||||
<string name="notification_channel_connection_checks">بررسی اتصال</string>
|
||||
<string name="notification_action_stop_v2ray">توقف</string>
|
||||
<string name="toast_permission_denied">امکان دریافت مجوز وجود ندارد</string>
|
||||
<string name="toast_permission_denied_notification">امکان دریافت مجوز ارسال اعلان وجود ندارد</string>
|
||||
@@ -50,7 +53,7 @@
|
||||
<string name="confirm_delete_proxy_chain_member">آیا مطمئن هستید که میخواهید این عضو زنجیره پراکسی را حذف کنید؟</string>
|
||||
<string name="confirm_delete_routing_rule">آیا مطمئن هستید که میخواهید این قانون مسیریابی را حذف کنید؟</string>
|
||||
<string name="confirm_delete_subscription_group">آیا مطمئن هستید که میخواهید این گروه اشتراک را حذف کنید؟</string>
|
||||
<string name="confirm_delete_asset_file">آیا مطمئن هستید که میخواهید فایل دارایی \"%1$s\" را حذف کنید؟</string>
|
||||
<string name="confirm_delete_asset_file">آیا مطمئن هستید که میخواهید این فایل دارایی را حذف کنید؟</string>
|
||||
<string name="confirm_delete_asset_source">آیا مطمئن هستید که میخواهید این منبع دارایی را حذف کنید؟</string>
|
||||
<string name="server_lab_remarks">ملاحظات</string>
|
||||
<string name="server_lab_address">نشانی</string>
|
||||
@@ -100,6 +103,7 @@
|
||||
<string name="server_lab_reserved">Reserved (اختیاری، جدا شده با کاما)</string>
|
||||
<string name="server_lab_local_address">آدرس محلی (IPv4/IPv6 اختیاری، جدا شده با کاما)</string>
|
||||
<string name="server_lab_local_mtu">MTU (اختیاری، پیشفرض 1420)</string>
|
||||
<string name="server_lab_remote_dns">remoteDNS (اختیاری، آدرسهای IPv4/IPv6 جدا شده با کاما)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU (اختیاری، پیشفرض 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI (اختیاری، پیشفرض 50 میلیثانیه)</string>
|
||||
<string name="toast_success">موفقیتآمیز</string>
|
||||
@@ -351,8 +355,14 @@
|
||||
<string name="title_del_config_count">حذف %d کانفیگ</string>
|
||||
<string name="title_import_config_count">وارد کردن %d کانفیگ</string>
|
||||
<string name="title_export_config_count">صادر کردن %d کانفیگ</string>
|
||||
<string name="title_update_config_count">آپدیت کردن %d کانفیگ</string>
|
||||
<string name="title_update_asset_count">%d فایل منبع بروزرسانی شد</string>
|
||||
<plurals name="title_update_config_count">
|
||||
<item quantity="one">%1$d کانفیگ بهروزرسانی شد</item>
|
||||
<item quantity="other">%1$d کانفیگ بهروزرسانی شد</item>
|
||||
</plurals>
|
||||
<plurals name="title_update_asset_count">
|
||||
<item quantity="one">%1$d فایل منبع بهروزرسانی شد</item>
|
||||
<item quantity="other">%1$d فایل منبع بهروزرسانی شد</item>
|
||||
</plurals>
|
||||
<string name="title_update_subscription_result">بروزرسانی %1$d کانفیگ انجام شد (%2$d موفق، %3$d شکست خورده، %4$d نادیده گرفته شده)</string>
|
||||
<string name="title_update_subscription_no_subscription">هیچ اشتراکی وجود ندارد</string>
|
||||
<string name="toast_server_not_found_in_group">سرور انتخابشده در گروه فعلی پیدا نشد</string>
|
||||
@@ -550,5 +560,8 @@
|
||||
<string name="tasker_blurb_stop">توقف %1$s</string>
|
||||
<string name="connection_test_empty_message">جزئیاتی در دسترس نیست</string>
|
||||
<string name="value_unknown">ناشناخته</string>
|
||||
<string name="sub_info_open_support">باز کردن لینک پشتیبانی</string>
|
||||
<string name="sub_info_open_page">باز کردن صفحه اطلاعات اشتراک</string>
|
||||
<string name="sub_info_unlimited">نامحدود</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="AppThemeBase" parent="AppThemeBaseCommon">
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,11 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Night theme: override window background and status bar flags for dark mode -->
|
||||
<style name="AppThemeBase" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<style name="AppThemeBaseCommon" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
<item name="android:windowBackground">@color/window_background_dark</item>
|
||||
</style>
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
<string name="pull_down_to_refresh">Потяните вниз для обновления!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_channel_service">Служба v2rayNG</string>
|
||||
<string name="notification_channel_subscription_updates">Обновления подписок</string>
|
||||
<string name="notification_channel_connection_checks">Проверки соединения</string>
|
||||
<string name="notification_action_stop_v2ray">Остановить</string>
|
||||
<string name="toast_permission_denied">Разрешение не получено</string>
|
||||
<string name="toast_permission_denied_notification">Разрешение на отображение уведомлений не получено</string>
|
||||
@@ -46,11 +49,11 @@
|
||||
<string name="confirm_delete_duplicate_profiles">Удалить все отображаемые дубликаты профилей?</string>
|
||||
<string name="confirm_delete_invalid_profiles">Выполните проверку перед удалением! Удалить все отображаемые нерабочие профили?</string>
|
||||
<string name="confirm_delete_profile">Удалить этот профиль?</string>
|
||||
<string name="confirm_delete_policy_group">Удалить эту политику группы?</string>
|
||||
<string name="confirm_delete_policy_group">Удалить эту группу политик?</string>
|
||||
<string name="confirm_delete_proxy_chain_member">Удалить этого участника цепочки прокси?</string>
|
||||
<string name="confirm_delete_routing_rule">Удалить это правило маршрутизации?</string>
|
||||
<string name="confirm_delete_subscription_group">Удалить эту подписку?</string>
|
||||
<string name="confirm_delete_asset_file">Удалить файл ресурсов «%1$s»?</string>
|
||||
<string name="confirm_delete_asset_file">Удалить этот файл ресурсов?</string>
|
||||
<string name="confirm_delete_asset_source">Удалить этот источник ресурсов?</string>
|
||||
<string name="server_lab_remarks">Название</string>
|
||||
<string name="server_lab_address">Адрес</string>
|
||||
@@ -81,7 +84,7 @@
|
||||
<string name="server_lab_stream_security" translatable="false">TLS</string>
|
||||
<string name="server_lab_stream_fingerprint">Отпечаток</string>
|
||||
<string name="server_lab_stream_alpn" translatable="false">ALPN</string>
|
||||
<string name="server_lab_allow_insecure">Пропускать проверку сертификата (allowInsecure)</string>
|
||||
<string name="server_lab_allow_insecure">Не проверять сертификат (allowInsecure)</string>
|
||||
<string name="server_lab_sni" translatable="false">SNI</string>
|
||||
<string name="server_lab_address3">Адрес</string>
|
||||
<string name="server_lab_port3">Порт</string>
|
||||
@@ -100,6 +103,7 @@
|
||||
<string name="server_lab_reserved">Reserved (необязательно, через запятую)</string>
|
||||
<string name="server_lab_local_address">Локальный адрес (необязательно, IPv4/IPv6 через запятую)</string>
|
||||
<string name="server_lab_local_mtu">MTU (необязательно, по умолчанию 1420)</string>
|
||||
<string name="server_lab_remote_dns">remoteDNS (необязательно, IPv4/IPv6-адреса через запятую)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU (необязательно, по умолчанию 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI (необязательно, по умолчанию 50 мс)</string>
|
||||
<string name="toast_success">Успешно</string>
|
||||
@@ -116,12 +120,12 @@
|
||||
<string name="toast_invalid_url">Неправильный URL</string>
|
||||
<string name="toast_insecure_url_protocol">Не используйте небезопасный HTTP-протокол в адресе подписки</string>
|
||||
<string name="server_lab_need_inbound">Убедитесь, что входящий порт соответствует настройкам</string>
|
||||
<string name="toast_malformed_json">Некорректная конфигурация.</string>
|
||||
<string name="toast_malformed_json">Профиль повреждён</string>
|
||||
<string name="server_lab_request_host6">Узел (SNI) (необязательно)</string>
|
||||
<string name="toast_action_not_allowed">Это действие запрещено</string>
|
||||
<string name="server_obfs_password">Пароль obfs</string>
|
||||
<string name="server_lab_port_hop">Смена портов (переопределяет порт)</string>
|
||||
<string name="server_lab_port_hop_interval">Интервал смены портов (секунды)</string>
|
||||
<string name="server_lab_port_hop_interval">Интервал смены портов (сек.)</string>
|
||||
<string name="server_lab_bandwidth_down">Входящая пропускная способность (допускаются: k/m/g/t)</string>
|
||||
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string>
|
||||
<string name="server_lab_xhttp_mode">Режим XHTTP</string>
|
||||
@@ -130,8 +134,8 @@
|
||||
<string name="server_lab_ech_config_list" translatable="false">echConfigList</string>
|
||||
<string name="server_lab_verify_peer_cert_by_name">Проверять сертификат узла по имени</string>
|
||||
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
|
||||
<string name="server_lab_browser_dialer">Включить Browser Dialer</string>
|
||||
<string name="server_lab_browser_dialer_tip">Поддерживаются только исходящие соединения XHTTP (packet-up) и WS; настройки, связанные с TLS, могут быть проигнорированы или конфликтовать</string>
|
||||
<string name="server_lab_browser_dialer">Использовать Browser Dialer</string>
|
||||
<string name="server_lab_browser_dialer_tip">Поддерживаются только исходящие соединения XHTTP (packet-up) и WS; настройки, связанные с TLS, могут быть проигнорированы или конфликтовать.</string>
|
||||
<string name="toast_allow_insecure_deprecated">Этот узел использует незашифрованное соединение. Ваши данные могут быть перехвачены и просмотрены государственными структурами через промежуточное сетевое оборудование.\nВ целях безопасности такие узлы больше не поддерживаются ядром Xray версии 26.2.6+.\nДля исправления: включите TLS (или другое шифрование) на своём сервере или используйте отпечаток сертификата (pinnedCA256).\nЕсли вы используете платный сервис, обратитесь в поддержку для обновления. Если провайдер отказывается, рекомендуется сменить его на более безопасный.\nПодробнее: https://github.com/2dust/v2rayN/discussions/9460</string>
|
||||
<string name="pinned_ca256_action_fetch">Получить отпечаток сертификата</string>
|
||||
<string name="toast_fetch_cert_sha256_success">Отпечаток сертификата успешно получен</string>
|
||||
@@ -156,13 +160,13 @@
|
||||
<string name="menu_item_select_all">Выбрать все</string>
|
||||
<string name="menu_item_invert_selection">Инвертировать выбор</string>
|
||||
<string name="msg_enter_keywords">Введите ключевое слово</string>
|
||||
<string name="switch_bypass_apps_mode">Режим обхода</string>
|
||||
<string name="switch_bypass_apps_mode">Обход</string>
|
||||
<string name="menu_item_select_proxy_app">Автовыбор проксируемых приложений</string>
|
||||
<string name="msg_downloading_content">Загрузка данных</string>
|
||||
<string name="menu_item_export_proxy_app">Экспорт в буфер обмена</string>
|
||||
<string name="menu_item_import_proxy_app">Импорт из буфера обмена</string>
|
||||
<string name="per_app_proxy_settings">Выбор приложений</string>
|
||||
<string name="per_app_proxy_settings_enable">Использовать выбор приложений</string>
|
||||
<string name="per_app_proxy_settings_enable">Использовать выбор</string>
|
||||
<string name="app_picker_unknown_app">Неизвестное приложение (неопознанный UID)</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
@@ -171,24 +175,24 @@
|
||||
<string name="title_core_settings">Настройки ядра</string>
|
||||
<string name="title_vpn_settings">Настройки VPN</string>
|
||||
<string name="title_pref_per_app_proxy">Прокси для выбранных приложений</string>
|
||||
<string name="summary_pref_per_app_proxy">Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nВ меню можно автоматически выбрать приложения, которые должны использовать прокси.</string>
|
||||
<string name="title_pref_is_booted">Подключаться после загрузки устройства</string>
|
||||
<string name="summary_pref_per_app_proxy">Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nОбход: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nВ меню можно автоматически выбрать приложения, которые должны использовать прокси.</string>
|
||||
<string name="title_pref_is_booted">Автоподключение при загрузке устройства</string>
|
||||
<string name="summary_pref_is_booted">Автоматически подключаться к выбранному серверу после загрузки устройства. Android может ограничивать запуск в фоне.</string>
|
||||
<string name="title_system_vpn_settings">Системные настройки VPN</string>
|
||||
<string name="summary_system_vpn_settings">Открыть настройки VPN в Android для включения постоянного VPN-подключения, если устройство его поддерживает.</string>
|
||||
<string name="summary_system_vpn_settings">Открыть настройки VPN в Android для включения постоянного VPN-подключения, если устройство его поддерживает</string>
|
||||
<string name="toast_system_vpn_settings_unavailable">Системные настройки VPN недоступны.</string>
|
||||
|
||||
|
||||
<string name="subscription_updater_job_tips">Задача будет выполняться в фоновом режиме, информация о ходе выполнения будет отображаться в уведомлении.</string>
|
||||
<string name="title_pref_auto_test_after_update_subscription">Автопроверка после обновления подписки</string>
|
||||
<string name="summary_pref_auto_test_after_update_subscription">Автоматическая проверка занимает много времени. Если параметр включён, задача выполняется в фоновом режиме; иначе — на текущей странице.</string>
|
||||
<string name="summary_pref_auto_test_after_update_subscription">Автоматическая проверка занимает много времени. Если включено, задача выполняется в фоновом режиме; иначе — для текущей вкладки.</string>
|
||||
<string name="title_pref_auto_remove_invalid_after_test">Автоудаление нерабочих профилей после проверки</string>
|
||||
<string name="summary_pref_auto_remove_invalid_after_test">Результаты проверки могут быть неточными, восстановление удалённых профилей невозможно</string>
|
||||
<string name="title_pref_auto_sort_after_test">Автосортировка профилей</string>
|
||||
<string name="summary_pref_auto_sort_after_test">Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string>
|
||||
|
||||
<string name="title_mux_settings">Настройки мультиплексирования (Mux)</string>
|
||||
<string name="title_pref_mux_enabled">Использовать мультиплексирование (Mux)</string>
|
||||
<string name="title_mux_settings">Настройки мультиплексирования</string>
|
||||
<string name="title_pref_mux_enabled">Использовать мультиплексирование</string>
|
||||
<string name="summary_pref_mux_enabled">Быстрее, но это может привести к нестабильному соединению.\nНиже можно настроить обработку TCP, UDP и QUIC.</string>
|
||||
<string name="title_pref_mux_concurrency">TCP-соединения (диапазон от -1 до 1024)</string>
|
||||
<string name="title_pref_mux_xudp_concurrency">XUDP-соединения (диапазон от -1 до 1024)</string>
|
||||
@@ -202,24 +206,24 @@
|
||||
<string name="title_pref_speed_enabled">Показывать скорость</string>
|
||||
<string name="summary_pref_speed_enabled">Показывать текущую скорость в уведомлении.\nЗначок указывает, какой трафик преобладает: через прокси или напрямую.</string>
|
||||
|
||||
<string name="title_pref_sniffing_enabled">Определять домены в трафике (Sniffing)</string>
|
||||
<string name="summary_pref_sniffing_enabled">Определять домены назначения в трафике (по умолчанию включено).</string>
|
||||
<string name="title_pref_route_only_enabled">Включить routeOnly</string>
|
||||
<string name="summary_pref_route_only_enabled">Использовать доменное имя только для маршрутизации и сохранять целевой адрес в виде IP.</string>
|
||||
<string name="title_pref_sniffing_enabled">Анализировать трафик (sniffing)</string>
|
||||
<string name="summary_pref_sniffing_enabled">Определять домены назначения в трафике (по умолчанию включено)</string>
|
||||
<string name="title_pref_route_only_enabled">Домен только для маршрутизации (routeOnly)</string>
|
||||
<string name="summary_pref_route_only_enabled">Использовать доменное имя только для маршрутизации и сохранять целевой адрес в виде IP</string>
|
||||
|
||||
<string name="title_pref_local_dns_enabled">Использовать локальный DNS</string>
|
||||
<string name="title_pref_local_dns_enabled">Использовать локальный сервер DNS</string>
|
||||
<string name="summary_pref_local_dns_enabled">Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая»)</string>
|
||||
|
||||
<string name="title_pref_fake_dns_enabled">Использовать виртуальный DNS (FakeDNS)</string>
|
||||
<string name="summary_pref_fake_dns_enabled">FakeDNS возвращает виртуальные IP-адреса при разрешении доменов (быстрее, но может не работать с некоторыми приложениями).</string>
|
||||
<string name="title_pref_fake_dns_enabled">Использовать виртуальный сервер DNS (FakeDNS)</string>
|
||||
<string name="summary_pref_fake_dns_enabled">FakeDNS возвращает виртуальные IP-адреса при определении доменов (быстрее, но может не работать с некоторыми приложениями)</string>
|
||||
|
||||
<string name="title_pref_ipv6_enabled">Включить IPv6</string>
|
||||
<string name="title_pref_ipv6_enabled">Использовать IPv6</string>
|
||||
<string name="summary_pref_ipv6_enabled">Добавить IPv6-адрес и маршруты в VPN-интерфейс</string>
|
||||
|
||||
<string name="title_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" translatable="false">DNS</string>
|
||||
|
||||
<string name="title_pref_vpn_dns">VPN DNS (только IPv4/IPv6)</string>
|
||||
@@ -236,15 +240,15 @@
|
||||
<string name="title_pref_dns_hosts">Сопоставления DNS (формат: domain:address,…)</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" translatable="false">URL</string>
|
||||
<string name="title_pref_real_ping_concurrency">Параллельная проверка задержки</string>
|
||||
<string name="title_pref_real_ping_concurrency">Параллельные тесты реальной задержки</string>
|
||||
|
||||
<string name="title_pref_ip_api_url">Сервис проверки текущего соединения</string>
|
||||
<string name="summary_pref_ip_api_url" translatable="false">URL</string>
|
||||
|
||||
<string name="title_pref_proxy_sharing_enabled">Разрешать соединения из LAN</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети</string>
|
||||
|
||||
<string name="title_pref_socks_port">Порт локального прокси</string>
|
||||
@@ -254,27 +258,27 @@
|
||||
<string name="summary_pref_socks_enable_udp">Аутентификация SOCKS5 не полностью безопасна при использовании UDP</string>
|
||||
<string name="summary_pref_socks_port">Порт локального прокси</string>
|
||||
<string name="title_pref_dynamic_socks_port">Динамически менять порт локального прокси</string>
|
||||
<string name="summary_pref_dynamic_socks_port">Использовать случайный порт локального прокси для каждого входящего подключения (inbound)</string>
|
||||
<string name="summary_pref_dynamic_socks_port">Использовать случайный порт локального прокси для каждого входящего подключения</string>
|
||||
<string name="title_pref_socks_username">Пользователь локального прокси (необязательно)</string>
|
||||
<string name="summary_pref_socks_username">Имя пользователя</string>
|
||||
<string name="title_pref_socks_password">Пароль локального прокси (необязательно)</string>
|
||||
<string name="summary_pref_socks_password">Пароль</string>
|
||||
|
||||
<string name="title_pref_local_dns_port">Порт локального DNS</string>
|
||||
<string name="summary_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="title_pref_confirm_remove">Подтверждать удаление профиля</string>
|
||||
<string name="summary_pref_confirm_remove">Обязательное подтверждение удаления профиля</string>
|
||||
|
||||
|
||||
<string name="title_pref_append_http_proxy">Добавить HTTP-прокси в VPN</string>
|
||||
<string name="summary_pref_append_http_proxy">Браузеры и поддерживаемые приложения смогут использовать HTTP-прокси напрямую, минуя виртуальный сетевой адаптер (Android 10+).</string>
|
||||
<string name="title_pref_append_http_proxy">Дополнительный HTTP-прокси</string>
|
||||
<string name="summary_pref_append_http_proxy">Браузеры и поддерживаемые приложения смогут использовать HTTP-прокси напрямую, минуя виртуальный сетевой адаптер (Android 10+)</string>
|
||||
|
||||
<string name="title_pref_double_column_display">Профили в два столбца</string>
|
||||
<string name="summary_pref_double_column_display">Список профилей отображается двумя столбцами, что позволяет показать больше информации на экране.</string>
|
||||
<string name="summary_pref_double_column_display">Список профилей отображается двумя столбцами, что позволяет показать больше информации на экране</string>
|
||||
|
||||
<string name="title_pref_group_all_display">Показывать вкладку «Все»</string>
|
||||
<string name="summary_pref_group_all_display">Объединять профили из всех групп подписок на одной вкладке.</string>
|
||||
<string name="summary_pref_group_all_display">Вкладка, объединяющая профили из всех групп</string>
|
||||
|
||||
<!-- AboutActivity -->
|
||||
<string name="title_pref_feedback">Обратная связь</string>
|
||||
@@ -296,24 +300,24 @@
|
||||
<string name="title_pref_auto_update_interval">Интервал автообновления (минут, не менее 15)</string>
|
||||
|
||||
<string name="title_core_loglevel">Подробность ведения журнала</string>
|
||||
<string name="title_outbound_domain_resolve_method">Разрешение доменов исходящих подключений (outbound)</string>
|
||||
<string name="title_outbound_domain_resolve_method">Определение доменов исходящих подключений</string>
|
||||
<string name="title_mode">Режим</string>
|
||||
<string name="title_mode_help">Нажмите для получения дополнительной информации</string>
|
||||
<string name="title_language">Язык</string>
|
||||
<string name="title_ui_settings">Настройки интерфейса</string>
|
||||
<string name="title_pref_ui_mode_night">Тема интерфейса</string>
|
||||
<string name="title_pref_dynamic_color">Включить динамические цвета</string>
|
||||
<string name="title_pref_dynamic_color">Динамические цвета</string>
|
||||
<string name="summary_pref_dynamic_color">Работает только на Android 12+</string>
|
||||
<string name="title_pref_use_hev_tunnel">Использовать Hev TUN</string>
|
||||
<string name="summary_pref_use_hev_tunnel">Если включено, TUN будет использовать hev-socks5-tunnel; иначе будет использован xray-core</string>
|
||||
<string name="title_pref_hev_tunnel_loglevel">Уровень журналирования Hev TUN</string>
|
||||
<string name="title_pref_hev_tunnel_rw_timeout">Тайм-аут чтения/записи Hev TUN (секунды; значения TCP/UDP по умолчанию: 300/60)</string>
|
||||
<string name="title_pref_hev_tunnel_rw_timeout">Время ожидания чтения/записи Hev TUN (секунды; значения TCP/UDP по умолчанию: 300/60)</string>
|
||||
<string name="title_mode_settings">Настройки режима</string>
|
||||
<string name="title_root_mode_enabled">Использовать root-режим</string>
|
||||
<string name="summary_root_mode_enabled">Использовать root-доступ для низкоуровневого прозрачного проксирования. Root-режим управляет сетевым трафиком напрямую вместо службы VPN Android. Пока он включён, обычный режим, маршрутизация отдельных приложений и связанные настройки VPN не действуют.</string>
|
||||
<string name="toast_root_required">Для этой функции требуется доступ с правами root</string>
|
||||
<string name="title_root_lan_sharing">Доступ к VPN для устройств LAN/точки доступа (root)</string>
|
||||
<string name="summary_root_lan_sharing">Направлять через VPN трафик устройств, подключённых к точке доступа Wi-Fi или по USB.</string>
|
||||
<string name="summary_root_lan_sharing">Направлять через VPN трафик устройств, подключённых к точке доступа Wi-Fi или по USB</string>
|
||||
|
||||
<string name="title_logcat" translatable="false">Logcat</string>
|
||||
<string name="logcat_copy">Копировать</string>
|
||||
@@ -323,24 +327,24 @@
|
||||
<string name="title_del_all_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>
|
||||
<string name="title_export_all">Экспорт профилей в буфер обмена</string>
|
||||
<string name="title_sub_setting">Группы</string>
|
||||
<string name="sub_setting_remarks">Название</string>
|
||||
<string name="sub_setting_url">URL (необязательно)</string>
|
||||
<string name="sub_setting_user_agent" translatable="false">User Agent</string>
|
||||
<string name="sub_setting_request_headers">Заголовки запроса (формат JSON)</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_allow_insecure_url">Разрешать незащищённые HTTP-адреса</string>
|
||||
<string name="sub_setting_pre_profile">Входной прокси</string>
|
||||
<string name="sub_setting_next_profile">Выходной прокси</string>
|
||||
<string name="sub_setting_pre_profile_tip">Убедитесь, что профиль с таким названием существует и его название уникально</string>
|
||||
<string name="sub_setting_entry_proxy_tip">Выбранный прокси добавляется перед каждым профилем подписки и становится входным звеном цепочки прокси для этого профиля.</string>
|
||||
<string name="sub_setting_exit_proxy_tip">Выбранный прокси добавляется после каждого профиля подписки и становится выходным звеном цепочки прокси для этого профиля.</string>
|
||||
<string name="sub_setting_pre_profile_tip">Убедитесь, что профиль с таким названием существует и его название уникально.</string>
|
||||
<string name="sub_setting_entry_proxy_tip">Выбранный прокси добавляется перед каждым профилем подписки и становится входным участником цепочки прокси для этого профиля.</string>
|
||||
<string name="sub_setting_exit_proxy_tip">Выбранный прокси добавляется после каждого профиля подписки и становится выходным участником цепочки прокси для этого профиля.</string>
|
||||
<string name="toast_invalid_update_interval">Неправильный интервал обновления. Минимум 15 минут.</string>
|
||||
<string name="title_sub_update">Обновить подписку</string>
|
||||
<string name="title_ping_all_server">Проверить TCP-задержку (TCPing)</string>
|
||||
<string name="title_ping_all_server">Проверить TCP-задержку</string>
|
||||
<string name="title_real_ping_all_server">Проверить реальную задержку</string>
|
||||
<string name="title_user_asset_setting">Файлы ресурсов</string>
|
||||
<string name="title_sort_by_test_results">Сортировать по результатам теста</string>
|
||||
@@ -351,8 +355,18 @@
|
||||
<string name="title_del_config_count">Удалено профилей: %d</string>
|
||||
<string name="title_import_config_count">Импортировано профилей: %d</string>
|
||||
<string name="title_export_config_count">Экспортировано профилей: %d</string>
|
||||
<string name="title_update_config_count">Обновлено профилей: %d</string>
|
||||
<string name="title_update_asset_count">Обновлено файлов ресурсов: %d</string>
|
||||
<plurals name="title_update_config_count">
|
||||
<item quantity="one">Обновлён %1$d профиль</item>
|
||||
<item quantity="few">Обновлено %1$d профиля</item>
|
||||
<item quantity="many">Обновлено %1$d профилей</item>
|
||||
<item quantity="other">Обновлено %1$d профиля</item>
|
||||
</plurals>
|
||||
<plurals name="title_update_asset_count">
|
||||
<item quantity="one">Обновлён %1$d файл ресурсов</item>
|
||||
<item quantity="few">Обновлено %1$d файла ресурсов</item>
|
||||
<item quantity="many">Обновлено %1$d файлов ресурсов</item>
|
||||
<item quantity="other">Обновлено %1$d файла ресурсов</item>
|
||||
</plurals>
|
||||
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибки: %3$d, пропущено: %4$d)</string>
|
||||
<string name="title_update_subscription_no_subscription">Нет подписок</string>
|
||||
<string name="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string>
|
||||
@@ -364,7 +378,7 @@
|
||||
<!-- RoutingSettingActivity -->
|
||||
<string name="routing_settings_domain_strategy">Доменная стратегия</string>
|
||||
<string name="routing_settings_title">Маршрутизация</string>
|
||||
<string name="routing_settings_tips">Правило применяется, только если одновременно выполняются все заданные условия сопоставления (AND). Правила проверяются сверху вниз; применяется первое совпавшее правило.</string>
|
||||
<string name="routing_settings_tips">Правило применяется, только если одновременно выполняются все заданные условия сопоставления (логический оператор «И»). Правила проверяются сверху вниз; применяется первое совпавшее правило.</string>
|
||||
<string name="routing_settings_comma_tip">Разделяйте несколько значений запятыми (,).</string>
|
||||
<string name="routing_settings_save">Сохранить</string>
|
||||
<string name="routing_settings_delete">Очистить</string>
|
||||
@@ -378,7 +392,7 @@
|
||||
<string name="routing_settings_locked">Постоянное (сохранится при импорте правил)</string>
|
||||
<string name="routing_settings_domain" translatable="false">domain</string>
|
||||
<string name="routing_settings_ip" translatable="false">ip</string>
|
||||
<string name="routing_settings_process">process (имя пакета — поддерживается только при использовании Xray TUN, включённом routeOnly и Android 10+)</string>
|
||||
<string name="routing_settings_process">process (имя пакета — поддерживается только при использовании Xray TUN, включённой функции «Домен только для маршрутизации (routeOnly)» и Android 10+)</string>
|
||||
<string name="routing_settings_process_select">Выбор названия пакета</string>
|
||||
<string name="routing_settings_port" translatable="false">port</string>
|
||||
<string name="routing_settings_protocol" translatable="false">protocol</string>
|
||||
@@ -401,13 +415,13 @@
|
||||
|
||||
<string name="import_subscription_success">Подписка импортирована</string>
|
||||
<string name="import_subscription_failure">Невозможно импортировать подписку</string>
|
||||
<string name="title_fragment_settings">Настройки фрагментирования (Fragment)</string>
|
||||
<string name="title_fragment_settings">Настройки фрагментирования</string>
|
||||
<string name="title_pref_fragment_packets">Фрагментирование пакетов</string>
|
||||
<string name="title_pref_fragment_length">Длина фрагмента (от — до)</string>
|
||||
<string name="title_pref_fragment_interval">Интервал фрагментов (от — до)</string>
|
||||
<string name="title_pref_fragment_enabled">Использовать фрагментирование</string>
|
||||
<string name="title_pref_fragment_maxsplit">Максимальное число фрагментов</string>
|
||||
<string name="title_observatory_settings">Настройки мониторинга соединений (Observatory)</string>
|
||||
<string name="title_observatory_settings">Настройки контроля соединений</string>
|
||||
<string name="title_pref_observatory_least_ping_interval">Интервал leastPing</string>
|
||||
<string name="title_pref_observatory_least_load_interval">Интервал leastLoad</string>
|
||||
<string name="title_pref_observatory_least_load_method">HTTP-метод leastLoad</string>
|
||||
@@ -426,8 +440,8 @@
|
||||
<string name="title_policy_group_type">Тип политики группы</string>
|
||||
<string name="title_policy_group_subscription_id">Из группы подписки</string>
|
||||
<string name="title_policy_group_subscription_filter">Фильтр примечаний (регулярное выражение)</string>
|
||||
<string name="title_policy_group_test_outbounds">Тестировать исходящие подключения (outbounds)</string>
|
||||
<string name="title_policy_group_fallback">Резервное исходящее подключение (outbound, необязательно)</string>
|
||||
<string name="title_policy_group_test_outbounds">Тестировать исходящие подключения</string>
|
||||
<string name="title_policy_group_fallback">Резервное исходящее подключение (необязательно)</string>
|
||||
|
||||
<string name="server_proxy_chain_members">Участники цепочки прокси</string>
|
||||
<string name="server_proxy_chain_pick_members">Нажмите здесь, чтобы выбрать участника</string>
|
||||
@@ -437,8 +451,8 @@
|
||||
<string name="server_proxy_chain_members_invalid">Недопустимые участники цепочки: %1$s</string>
|
||||
|
||||
<!-- BackupActivity -->
|
||||
<string name="title_configuration_backup_restore">Резервное копирование и восстановление</string>
|
||||
<string name="title_configuration_backup">Создать резервную копию</string>
|
||||
<string name="title_configuration_backup_restore">Резервирование и восстановление</string>
|
||||
<string name="title_configuration_backup">Резервирование конфигурации</string>
|
||||
<string name="title_configuration_restore">Восстановление конфигурации</string>
|
||||
<string name="title_profile_storage_cleanup">Исправить хранилище профилей</string>
|
||||
<string name="summary_profile_storage_cleanup">Удалить данные неиспользуемых профилей</string>
|
||||
@@ -461,7 +475,7 @@
|
||||
<string name="action_close">Закрыть</string>
|
||||
|
||||
<string name="share_subscription_qrcode">QR-код</string>
|
||||
<string name="share_subscription_clipboard">Экспорт в буфер обмена</string>
|
||||
<string name="share_subscription_clipboard">Копировать ссылку</string>
|
||||
|
||||
<string-array name="mode_entries">
|
||||
<item>VPN</item>
|
||||
@@ -482,7 +496,7 @@
|
||||
|
||||
<string name="routing_preset_china_whitelist">Белый список Китая</string>
|
||||
<string name="routing_preset_china_blacklist">Чёрный список Китая</string>
|
||||
<string name="routing_preset_global">Глобальная</string>
|
||||
<string name="routing_preset_global">Общие</string>
|
||||
<string name="routing_preset_iran_whitelist">Белый список Ирана</string>
|
||||
<string name="routing_preset_russia_whitelist">Белый список России</string>
|
||||
|
||||
@@ -493,9 +507,9 @@
|
||||
</string-array>
|
||||
|
||||
<string-array name="outbound_domain_resolve_method">
|
||||
<item>Не разрешать</item>
|
||||
<item>Разрешать и добавлять в DNS Hosts</item>
|
||||
<item>Разрешать и заменять домен</item>
|
||||
<item>Не определять</item>
|
||||
<item>Определять и добавлять к узлам DNS</item>
|
||||
<item>Определять и заменять домен</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="policy_group_type">
|
||||
@@ -524,7 +538,7 @@
|
||||
<string name="acc_search">Поиск</string>
|
||||
<string name="acc_add">Добавить профиль</string>
|
||||
<string name="acc_more">Дополнительные параметры</string>
|
||||
<string name="acc_selected_server">Selected</string>
|
||||
<string name="acc_selected_server">Выбран</string>
|
||||
<string name="acc_share_subscription">Поделиться ссылкой на подписку</string>
|
||||
<string name="acc_share_log">Поделиться журналом</string>
|
||||
<string name="acc_remove">Удалить участника цепочки прокси</string>
|
||||
@@ -532,9 +546,9 @@
|
||||
<string name="acc_start_scanner">Запустить сканер QR-кодов</string>
|
||||
<string name="acc_stop_scanner">Остановить сканер QR-кодов</string>
|
||||
<string name="acc_turn_torch_on">Включить фонарик</string>
|
||||
<string name="acc_turn_torch_off">Выключить фонарик</string>
|
||||
<string name="acc_turn_torch_off">Отключить фонарик</string>
|
||||
<string name="acc_select_image">Выбрать изображение для сканирования</string>
|
||||
<string name="acc_locked">Правило сохраняется при импорте предустановок</string>
|
||||
<string name="acc_locked">Правило сохраняется при импорте набора</string>
|
||||
<string name="acc_open_menu">Открыть меню навигации</string>
|
||||
<string name="acc_edit">Изменить</string>
|
||||
<string name="acc_delete">Удалить</string>
|
||||
@@ -550,5 +564,8 @@
|
||||
<string name="tasker_blurb_stop">Остановить %1$s</string>
|
||||
<string name="connection_test_empty_message">подробности отсутствуют</string>
|
||||
<string name="value_unknown">неизвестно</string>
|
||||
<string name="sub_info_open_support">Открыть ссылку поддержки</string>
|
||||
<string name="sub_info_open_page">Открыть страницу подписки</string>
|
||||
<string name="sub_info_unlimited">Без ограничений</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="AppThemeBase" parent="AppThemeBaseCommon">
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -14,6 +14,9 @@
|
||||
<string name="pull_down_to_refresh">Kéo xuống để làm mới!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_channel_service">Dịch vụ v2rayNG</string>
|
||||
<string name="notification_channel_subscription_updates">Cập nhật gói đăng ký</string>
|
||||
<string name="notification_channel_connection_checks">Kiểm tra kết nối</string>
|
||||
<string name="notification_action_stop_v2ray">Ngắt kết nối v2rayNG</string>
|
||||
<string name="toast_permission_denied">Vui lòng cấp các quyền cần thiết cho v2rayNG. Bạn có thể đã từ chối quyền truy cập máy ảnh hoặc bộ nhớ.</string>
|
||||
<string name="toast_permission_denied_notification">Không thể nhận quyền thông báo</string>
|
||||
@@ -50,7 +53,7 @@
|
||||
<string name="confirm_delete_proxy_chain_member">Bạn có chắc muốn xóa thành viên này khỏi chuỗi proxy không?</string>
|
||||
<string name="confirm_delete_routing_rule">Bạn có chắc muốn xóa quy tắc định tuyến này không?</string>
|
||||
<string name="confirm_delete_subscription_group">Bạn có chắc muốn xóa nhóm đăng ký này không?</string>
|
||||
<string name="confirm_delete_asset_file">Bạn có chắc muốn xóa tệp tài nguyên \"%1$s\" không?</string>
|
||||
<string name="confirm_delete_asset_file">Bạn có chắc muốn xóa tệp tài nguyên này không?</string>
|
||||
<string name="confirm_delete_asset_source">Bạn có chắc muốn xóa nguồn tài nguyên này không?</string>
|
||||
<string name="server_lab_remarks">Tên cấu hình</string>
|
||||
<string name="server_lab_address">Địa chỉ</string>
|
||||
@@ -100,6 +103,7 @@
|
||||
<string name="server_lab_reserved">Reserved (không bắt buộc, phân cách bằng dấu phẩy)</string>
|
||||
<string name="server_lab_local_address">Địa chỉ cục bộ (IPv4/IPv6 không bắt buộc, phân cách bằng dấu phẩy)</string>
|
||||
<string name="server_lab_local_mtu">MTU (không bắt buộc, mặc định 1420)</string>
|
||||
<string name="server_lab_remote_dns">remoteDNS (tùy chọn, các địa chỉ IPv4/IPv6 cách nhau bằng dấu phẩy)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU (không bắt buộc, mặc định 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI (không bắt buộc, mặc định 50 ms)</string>
|
||||
<string name="toast_success">Thành công!</string>
|
||||
@@ -351,8 +355,12 @@
|
||||
<string name="title_del_config_count">Xóa %d cấu hình</string>
|
||||
<string name="title_import_config_count">Nhập %d cấu hình</string>
|
||||
<string name="title_export_config_count">Xuất %d cấu hình</string>
|
||||
<string name="title_update_config_count">Cập nhật %d cấu hình</string>
|
||||
<string name="title_update_asset_count">Đã cập nhật %d tệp tài nguyên</string>
|
||||
<plurals name="title_update_config_count">
|
||||
<item quantity="other">Đã cập nhật %1$d cấu hình</item>
|
||||
</plurals>
|
||||
<plurals name="title_update_asset_count">
|
||||
<item quantity="other">Đã cập nhật %1$d tệp tài nguyên</item>
|
||||
</plurals>
|
||||
<string name="title_update_subscription_result">Đã cập nhật %1$d cấu hình (%2$d thành công, %3$d thất bại, %4$d bỏ qua)</string>
|
||||
<string name="title_update_subscription_no_subscription">Không có đăng ký</string>
|
||||
<string name="toast_server_not_found_in_group">Không tìm thấy máy chủ đã chọn trong nhóm hiện tại</string>
|
||||
@@ -550,5 +558,8 @@
|
||||
<string name="tasker_blurb_stop">Dừng %1$s</string>
|
||||
<string name="connection_test_empty_message">không có thông tin chi tiết</string>
|
||||
<string name="value_unknown">không xác định</string>
|
||||
<string name="sub_info_open_support">Mở liên kết hỗ trợ</string>
|
||||
<string name="sub_info_open_page">Mở trang thông tin đăng ký</string>
|
||||
<string name="sub_info_unlimited">Không giới hạn</string>
|
||||
|
||||
</resources>
|
||||
@@ -14,6 +14,9 @@
|
||||
<string name="pull_down_to_refresh">请下拉刷新!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_channel_service">v2rayNG 服务</string>
|
||||
<string name="notification_channel_subscription_updates">订阅更新</string>
|
||||
<string name="notification_channel_connection_checks">连接检查</string>
|
||||
<string name="notification_action_stop_v2ray">停止</string>
|
||||
<string name="toast_permission_denied">无法取得权限</string>
|
||||
<string name="toast_permission_denied_notification">无法取得通知权限</string>
|
||||
@@ -50,7 +53,7 @@
|
||||
<string name="confirm_delete_proxy_chain_member">确定要删除此代理链成员吗?</string>
|
||||
<string name="confirm_delete_routing_rule">确定要删除此路由规则吗?</string>
|
||||
<string name="confirm_delete_subscription_group">确定要删除此订阅组吗?</string>
|
||||
<string name="confirm_delete_asset_file">确定要删除资源文件“%1$s”吗?</string>
|
||||
<string name="confirm_delete_asset_file">确定要删除此资源文件吗?</string>
|
||||
<string name="confirm_delete_asset_source">确定要删除此资源来源吗?</string>
|
||||
<string name="server_lab_remarks">别名 (remarks)</string>
|
||||
<string name="server_lab_address">地址 (address)</string>
|
||||
@@ -100,6 +103,7 @@
|
||||
<string name="server_lab_reserved">Reserved (可选,逗号隔开)</string>
|
||||
<string name="server_lab_local_address">本地地址 (可选 IPv4/IPv6,逗号隔开)</string>
|
||||
<string name="server_lab_local_mtu">MTU(可选,默认 1420)</string>
|
||||
<string name="server_lab_remote_dns">remoteDNS(可选,多个 IPv4/IPv6 地址用逗号分隔)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU(可选,默认 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI(可选,默认 50 毫秒)</string>
|
||||
<string name="toast_success">成功</string>
|
||||
@@ -351,8 +355,12 @@
|
||||
<string name="title_del_config_count">删除 %d 个配置</string>
|
||||
<string name="title_import_config_count">导入 %d 个配置</string>
|
||||
<string name="title_export_config_count">导出 %d 个配置</string>
|
||||
<string name="title_update_config_count">更新 %d 个配置</string>
|
||||
<string name="title_update_asset_count">已更新 %d 个资源文件</string>
|
||||
<plurals name="title_update_config_count">
|
||||
<item quantity="other">已更新 %1$d 个配置</item>
|
||||
</plurals>
|
||||
<plurals name="title_update_asset_count">
|
||||
<item quantity="other">已更新 %1$d 个资源文件</item>
|
||||
</plurals>
|
||||
<string name="title_update_subscription_result">更新了 %1$d 个配置(%2$d 个成功,%3$d 个失败,%4$d 个跳过)</string>
|
||||
<string name="title_update_subscription_no_subscription">无订阅</string>
|
||||
<string name="toast_server_not_found_in_group">当前分组中未找到选中的服务器</string>
|
||||
@@ -550,5 +558,8 @@
|
||||
<string name="tasker_blurb_stop">停止 %1$s</string>
|
||||
<string name="connection_test_empty_message">无详细信息</string>
|
||||
<string name="value_unknown">未知</string>
|
||||
<string name="sub_info_open_support">打开支持链接</string>
|
||||
<string name="sub_info_open_page">打开订阅信息页面</string>
|
||||
<string name="sub_info_unlimited">无限制</string>
|
||||
|
||||
</resources>
|
||||
@@ -14,6 +14,9 @@
|
||||
<string name="pull_down_to_refresh">請下拉重新整理!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_channel_service">v2rayNG 服務</string>
|
||||
<string name="notification_channel_subscription_updates">訂閱更新</string>
|
||||
<string name="notification_channel_connection_checks">連線檢查</string>
|
||||
<string name="notification_action_stop_v2ray">停止</string>
|
||||
<string name="toast_permission_denied">無法取得此權限</string>
|
||||
<string name="toast_permission_denied_notification">無法取得此通知權限</string>
|
||||
@@ -50,7 +53,7 @@
|
||||
<string name="confirm_delete_proxy_chain_member">確定要刪除此代理鏈成員嗎?</string>
|
||||
<string name="confirm_delete_routing_rule">確定要刪除此路由規則嗎?</string>
|
||||
<string name="confirm_delete_subscription_group">確定要刪除此訂閱群組嗎?</string>
|
||||
<string name="confirm_delete_asset_file">確定要刪除資源檔案「%1$s」嗎?</string>
|
||||
<string name="confirm_delete_asset_file">確定要刪除此資源檔案嗎?</string>
|
||||
<string name="confirm_delete_asset_source">確定要刪除此資源來源嗎?</string>
|
||||
<string name="server_lab_remarks">備註</string>
|
||||
<string name="server_lab_address">位址</string>
|
||||
@@ -100,6 +103,7 @@
|
||||
<string name="server_lab_reserved">Reserved (可選,逗號隔開)</string>
|
||||
<string name="server_lab_local_address">本機位址 (可選 IPv4/IPv6,逗號隔開)</string>
|
||||
<string name="server_lab_local_mtu">MTU(可選,預設 1420)</string>
|
||||
<string name="server_lab_remote_dns">remoteDNS(選填,多個 IPv4/IPv6 位址以逗號分隔)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU(可選,預設 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI(可選,預設 50 毫秒)</string>
|
||||
<string name="toast_success">成功</string>
|
||||
@@ -351,8 +355,12 @@
|
||||
<string name="title_del_config_count">刪除 %d 個設定</string>
|
||||
<string name="title_import_config_count">匯入 %d 個設定</string>
|
||||
<string name="title_export_config_count">匯出 %d 個設定</string>
|
||||
<string name="title_update_config_count">更新 %d 個設定</string>
|
||||
<string name="title_update_asset_count">已更新 %d 個資源檔案</string>
|
||||
<plurals name="title_update_config_count">
|
||||
<item quantity="other">已更新 %1$d 個設定</item>
|
||||
</plurals>
|
||||
<plurals name="title_update_asset_count">
|
||||
<item quantity="other">已更新 %1$d 個資源檔案</item>
|
||||
</plurals>
|
||||
<string name="title_update_subscription_result">更新了 %1$d 個設定(%2$d 個成功,%3$d 個失敗,%4$d 個跳過)</string>
|
||||
<string name="title_update_subscription_no_subscription">無訂閱</string>
|
||||
<string name="toast_server_not_found_in_group">目前群組中找不到所選伺服器</string>
|
||||
@@ -550,5 +558,8 @@
|
||||
<string name="tasker_blurb_stop">停止 %1$s</string>
|
||||
<string name="connection_test_empty_message">沒有詳細資訊</string>
|
||||
<string name="value_unknown">未知</string>
|
||||
<string name="sub_info_open_support">開啟支援連結</string>
|
||||
<string name="sub_info_open_page">開啟訂閱資訊頁面</string>
|
||||
<string name="sub_info_unlimited">無限制</string>
|
||||
|
||||
</resources>
|
||||
@@ -146,6 +146,7 @@
|
||||
<item>Русский</item>
|
||||
<item>فارسی</item>
|
||||
<item>عربي</item>
|
||||
<item>Azərbaycanca</item>
|
||||
<item>বাংলা</item>
|
||||
<item>لۊری بختیاری</item>
|
||||
</string-array>
|
||||
@@ -160,6 +161,7 @@
|
||||
<item>ru</item>
|
||||
<item>fa</item>
|
||||
<item>ar</item>
|
||||
<item>az</item>
|
||||
<item>bn</item>
|
||||
<item>bqi-rIR</item>
|
||||
</string-array>
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
<string name="pull_down_to_refresh">Please pull down to refresh!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_channel_service">v2rayNG service</string>
|
||||
<string name="notification_channel_subscription_updates">Subscription updates</string>
|
||||
<string name="notification_channel_connection_checks">Connection checks</string>
|
||||
<string name="notification_action_stop_v2ray">Stop</string>
|
||||
<string name="toast_permission_denied">Permission denied</string>
|
||||
<string name="toast_permission_denied_notification">Notification permission denied</string>
|
||||
@@ -50,7 +53,7 @@
|
||||
<string name="confirm_delete_proxy_chain_member">Are you sure you want to delete this chain member?</string>
|
||||
<string name="confirm_delete_routing_rule">Are you sure you want to delete this routing rule?</string>
|
||||
<string name="confirm_delete_subscription_group">Are you sure you want to delete this subscription group?</string>
|
||||
<string name="confirm_delete_asset_file">Are you sure you want to delete the asset file \"%1$s\"?</string>
|
||||
<string name="confirm_delete_asset_file">Are you sure you want to delete this asset file?</string>
|
||||
<string name="confirm_delete_asset_source">Are you sure you want to delete this asset source?</string>
|
||||
<string name="server_lab_remarks">remarks</string>
|
||||
<string name="server_lab_address">address</string>
|
||||
@@ -100,6 +103,7 @@
|
||||
<string name="server_lab_reserved">Reserved (optional, separated by commas)</string>
|
||||
<string name="server_lab_local_address">Local address (optional IPv4/IPv6, separated by commas)</string>
|
||||
<string name="server_lab_local_mtu">MTU (optional, default 1420)</string>
|
||||
<string name="server_lab_remote_dns">remoteDNS (optional, comma-separated IPv4/IPv6)</string>
|
||||
<string name="server_lab_kcp_mtu">mKCP MTU (optional, default 1350)</string>
|
||||
<string name="server_lab_kcp_tti">mKCP TTI (optional, default 50 ms)</string>
|
||||
<string name="toast_success">Success</string>
|
||||
@@ -351,8 +355,14 @@
|
||||
<string name="title_del_config_count">Delete %d configs</string>
|
||||
<string name="title_import_config_count">Import %d configs</string>
|
||||
<string name="title_export_config_count">Export %d configs</string>
|
||||
<string name="title_update_config_count">Update %d configs</string>
|
||||
<string name="title_update_asset_count">Updated %d asset files</string>
|
||||
<plurals name="title_update_config_count">
|
||||
<item quantity="one">Updated %1$d config</item>
|
||||
<item quantity="other">Updated %1$d configs</item>
|
||||
</plurals>
|
||||
<plurals name="title_update_asset_count">
|
||||
<item quantity="one">Updated %1$d asset file</item>
|
||||
<item quantity="other">Updated %1$d asset files</item>
|
||||
</plurals>
|
||||
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
||||
<string name="title_update_subscription_no_subscription">No subscriptions</string>
|
||||
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
|
||||
@@ -550,5 +560,9 @@
|
||||
<string name="tasker_blurb_stop">Stop %1$s</string>
|
||||
<string name="connection_test_empty_message">no details available</string>
|
||||
<string name="value_unknown">unknown</string>
|
||||
<string name="sub_info_open_support">Open support link</string>
|
||||
<string name="sub_info_open_page">Open subscription page</string>
|
||||
<string name="sub_info_traffic" translatable="false">%1$s / %2$s</string>
|
||||
<string name="sub_info_unlimited">Unlimited</string>
|
||||
|
||||
</resources>
|
||||
@@ -2,14 +2,16 @@
|
||||
<color name="window_background_light">#FFFFFFFF</color>
|
||||
<color name="window_background_dark">#FF1C1B1F</color>
|
||||
|
||||
<style name="AppThemeBase" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<style name="AppThemeBaseCommon" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
<item name="android:windowBackground">@color/window_background_light</item>
|
||||
</style>
|
||||
|
||||
<!-- API 27+ adds navigation bar icon appearance in the versioned base theme. -->
|
||||
<style name="AppThemeBase" parent="AppThemeBaseCommon" />
|
||||
|
||||
<style name="AppThemeDayNight" parent="AppThemeBase" />
|
||||
|
||||
<style name="AppThemeDayNight.NoActionBar" parent="AppThemeBase" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.v2ray.ang
|
||||
|
||||
import com.v2ray.ang.util.HttpUtil
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class HttpUtilTest {
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.v2ray.ang
|
||||
|
||||
import com.v2ray.ang.util.Utils
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.v2ray.ang.extension
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ListExtTest {
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.v2ray.ang.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.net.URI
|
||||
|
||||
class Hysteria2FmtTest {
|
||||
|
||||
@Test
|
||||
fun hysteria2WithoutPortUses443() {
|
||||
val profile = Hysteria2Fmt.parse("hysteria2://test@example.com/")
|
||||
|
||||
assertEquals("example.com", profile.server)
|
||||
assertEquals("443", profile.serverPort)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hy2WithoutPortUses443() {
|
||||
val profile = Hysteria2Fmt.parse("hy2://test@example.com/")
|
||||
|
||||
assertEquals("example.com", profile.server)
|
||||
assertEquals("443", profile.serverPort)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uriWithoutCredentialsUses443() {
|
||||
val profile = Hysteria2Fmt.parse("hysteria2://example.com/")
|
||||
|
||||
assertEquals("example.com", profile.server)
|
||||
assertEquals("443", profile.serverPort)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun omittedPortPreservesTlsParameters() {
|
||||
val profile = Hysteria2Fmt.parse(
|
||||
"hy2://test@example.com/?sni=example.org&insecure=0&pinSHA256=test-pin#example"
|
||||
)
|
||||
|
||||
assertEquals("test", profile.password)
|
||||
assertEquals("example", profile.remarks)
|
||||
assertEquals(AppConfig.TLS, profile.security)
|
||||
assertEquals("example.org", profile.sni)
|
||||
assertEquals(false, profile.insecure)
|
||||
assertEquals("test-pin", profile.pinnedCA256)
|
||||
assertEquals("443", profile.serverPort)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ipv6WithoutPortUses443() {
|
||||
val profile = Hysteria2Fmt.parse("hysteria2://test@[2001:db8::1]/")
|
||||
|
||||
assertEquals("2001:db8::1", profile.server)
|
||||
assertEquals("443", profile.serverPort)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explicitPortsArePreserved() {
|
||||
for (scheme in listOf("hysteria2", "hy2")) {
|
||||
// Port range validation is separate from choosing a missing-port default.
|
||||
for (port in listOf(0, 443, 8443, 65535, 65536)) {
|
||||
val profile = Hysteria2Fmt.parse("$scheme://test@example.com:$port/")
|
||||
|
||||
assertEquals("example.com", profile.server)
|
||||
assertEquals(port.toString(), profile.serverPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonnumericPortDoesNotAcquireAHost() {
|
||||
val profile = Hysteria2Fmt.parse("hysteria2://test@example.com:invalid/")
|
||||
|
||||
assertEquals("", profile.server)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultPortSurvivesUriRoundTrip() {
|
||||
val profile = Hysteria2Fmt.parse("hy2://test@example.com/?sni=example.org")
|
||||
val exported = "hysteria2://${Hysteria2Fmt.toUri(profile)}"
|
||||
val reparsed = Hysteria2Fmt.parse(exported)
|
||||
|
||||
assertEquals(443, URI(exported).port)
|
||||
assertEquals("443", reparsed.serverPort)
|
||||
assertEquals(profile.server, reparsed.server)
|
||||
assertEquals(profile.password, reparsed.password)
|
||||
assertEquals(profile.sni, reparsed.sni)
|
||||
}
|
||||
}
|
||||
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user