Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 42e7f95e4f | |||
| 0a52a79fa0 | |||
| 9dc1b1f2cf | |||
| b61cad5a5f | |||
| 66cd746ad2 | |||
| ae4a92b66b | |||
| 55c0ccf1b0 | |||
| deedd26fac | |||
| e6c8307b96 | |||
| d6d5753283 | |||
| 014d510774 | |||
| 42f89e69a7 | |||
| d5a0997775 | |||
| af7db6f86b | |||
| 7b7f43e12e | |||
| 8dd1df1451 | |||
| d4a73493b5 | |||
| 9fce03d0ed | |||
| a12ca3ea04 | |||
| 4e9df84d3e | |||
| d3118b53f2 | |||
| cd45b51487 | |||
| 6aa26d2621 | |||
| 0b452707c3 | |||
| 303b642ade |
@@ -22,7 +22,7 @@ jobs:
|
|||||||
fetch-depth: '0'
|
fetch-depth: '0'
|
||||||
|
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v3.2.0
|
uses: android-actions/setup-android@v4.0.0
|
||||||
with:
|
with:
|
||||||
log-accepted-android-sdk-licenses: false
|
log-accepted-android-sdk-licenses: false
|
||||||
cmdline-tools-version: '13114758'
|
cmdline-tools-version: '13114758'
|
||||||
|
|||||||
+1
-1
Submodule AndroidLibXrayLite updated: c0a0231614...880725442c
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 714
|
versionCode = 718
|
||||||
versionName = "2.0.14"
|
versionName = "2.0.18"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ data class ProfileItem(
|
|||||||
var authority: String? = null,
|
var authority: String? = null,
|
||||||
var xhttpMode: String? = null,
|
var xhttpMode: String? = null,
|
||||||
var xhttpExtra: String? = null,
|
var xhttpExtra: String? = null,
|
||||||
|
var finalMask: String? = null,
|
||||||
var security: String? = null,
|
var security: String? = null,
|
||||||
var sni: String? = null,
|
var sni: String? = null,
|
||||||
var alpn: String? = null,
|
var alpn: String? = null,
|
||||||
|
|||||||
@@ -68,8 +68,6 @@ data class V2rayConfig(
|
|||||||
) {
|
) {
|
||||||
data class OutSettingsBean(
|
data class OutSettingsBean(
|
||||||
var vnext: List<VnextBean>? = null,
|
var vnext: List<VnextBean>? = null,
|
||||||
var fragment: FragmentBean? = null,
|
|
||||||
var noises: List<NoiseBean>? = null,
|
|
||||||
var servers: List<ServersBean>? = null,
|
var servers: List<ServersBean>? = null,
|
||||||
/*Blackhole*/
|
/*Blackhole*/
|
||||||
var response: Response? = null,
|
var response: Response? = null,
|
||||||
@@ -108,18 +106,6 @@ data class V2rayConfig(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FragmentBean(
|
|
||||||
var packets: String? = null,
|
|
||||||
var length: String? = null,
|
|
||||||
var interval: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class NoiseBean(
|
|
||||||
var type: String? = null,
|
|
||||||
var packet: String? = null,
|
|
||||||
var delay: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ServersBean(
|
data class ServersBean(
|
||||||
var address: String = "",
|
var address: String = "",
|
||||||
var method: String? = null,
|
var method: String? = null,
|
||||||
@@ -162,7 +148,7 @@ data class V2rayConfig(
|
|||||||
var realitySettings: TlsSettingsBean? = null,
|
var realitySettings: TlsSettingsBean? = null,
|
||||||
var grpcSettings: GrpcSettingsBean? = null,
|
var grpcSettings: GrpcSettingsBean? = null,
|
||||||
var hysteriaSettings: HysteriaSettingsBean? = null,
|
var hysteriaSettings: HysteriaSettingsBean? = null,
|
||||||
var finalmask: FinalMaskBean? = null,
|
var finalmask: Any? = null,
|
||||||
val dsSettings: Any? = null,
|
val dsSettings: Any? = null,
|
||||||
var sockopt: SockoptBean? = null
|
var sockopt: SockoptBean? = null
|
||||||
) {
|
) {
|
||||||
@@ -292,28 +278,50 @@ data class V2rayConfig(
|
|||||||
|
|
||||||
data class HysteriaSettingsBean(
|
data class HysteriaSettingsBean(
|
||||||
var version: Int,
|
var version: Int,
|
||||||
var auth: String? = null,
|
var auth: String? = null
|
||||||
var up: String? = null,
|
)
|
||||||
var down: String? = null,
|
|
||||||
var udphop: HysteriaUdpHopBean? = null
|
|
||||||
) {
|
|
||||||
data class HysteriaUdpHopBean(
|
|
||||||
var port: String? = null,
|
|
||||||
var interval: Int? = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
//https://xtls.github.io/config/transport.html#finalmaskobject
|
||||||
data class FinalMaskBean(
|
data class FinalMaskBean(
|
||||||
var tcp: List<MaskBean>? = null,
|
var tcp: List<MaskBean>? = null,
|
||||||
var udp: List<MaskBean>? = null
|
var udp: List<MaskBean>? = null,
|
||||||
|
var quicParams: QuicParamsBean? = null
|
||||||
) {
|
) {
|
||||||
data class MaskBean(
|
data class MaskBean(
|
||||||
var type: String,
|
var type: String,
|
||||||
var settings: MaskSettingsBean? = null
|
var settings: MaskSettingsBean? = null
|
||||||
) {
|
) {
|
||||||
data class MaskSettingsBean(
|
data class MaskSettingsBean(
|
||||||
var password: String? = null,
|
val password: String? = null,
|
||||||
var domain: String? = null
|
val domain: String? = null,
|
||||||
|
// fragment
|
||||||
|
val packets: String? = null,
|
||||||
|
val length: String? = null,
|
||||||
|
val delay: String? = null,
|
||||||
|
// val maxSplit: String? = null,
|
||||||
|
// noise
|
||||||
|
val reset: Int? = null,
|
||||||
|
val noise: List<NoiseMaskBean>? = null
|
||||||
|
) {
|
||||||
|
data class NoiseMaskBean(
|
||||||
|
val rand: String? = null,
|
||||||
|
// val randRange: String? = null,
|
||||||
|
// val type: String? = null,
|
||||||
|
// val packet: String? = null,
|
||||||
|
val delay: String? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class QuicParamsBean(
|
||||||
|
var congestion: String? = null,
|
||||||
|
var brutalUp: String? = null,
|
||||||
|
var brutalDown: String? = null,
|
||||||
|
var udpHop: UdpHopBean? = null,
|
||||||
|
) {
|
||||||
|
// Nested data class for the udpHop JSON object
|
||||||
|
data class UdpHopBean(
|
||||||
|
var ports: String? = null,
|
||||||
|
var interval: String? = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,4 +189,15 @@ fun String.concatUrl(vararg paths: String): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return builder.toString()
|
return builder.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to match text either by Regex or literal string.
|
||||||
|
*/
|
||||||
|
fun String.matchesPattern(regex: Regex?, keyword: String?, ignoreCase: Boolean = true): Boolean {
|
||||||
|
if (keyword.isNullOrEmpty()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return regex?.containsMatchIn(this)
|
||||||
|
?: this.contains(keyword, ignoreCase = ignoreCase)
|
||||||
}
|
}
|
||||||
@@ -67,6 +67,7 @@ open class FmtBase {
|
|||||||
config.authority = queryParam["authority"]
|
config.authority = queryParam["authority"]
|
||||||
config.xhttpMode = queryParam["mode"]
|
config.xhttpMode = queryParam["mode"]
|
||||||
config.xhttpExtra = queryParam["extra"]
|
config.xhttpExtra = queryParam["extra"]
|
||||||
|
config.finalMask = queryParam["fm"]
|
||||||
|
|
||||||
config.security = queryParam["security"]
|
config.security = queryParam["security"]
|
||||||
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
|
if (config.security != AppConfig.TLS && config.security != AppConfig.REALITY) {
|
||||||
@@ -110,6 +111,7 @@ open class FmtBase {
|
|||||||
config.spiderX?.nullIfBlank()?.let { dicQuery["spx"] = it }
|
config.spiderX?.nullIfBlank()?.let { dicQuery["spx"] = it }
|
||||||
config.mldsa65Verify?.nullIfBlank()?.let { dicQuery["pqv"] = it }
|
config.mldsa65Verify?.nullIfBlank()?.let { dicQuery["pqv"] = it }
|
||||||
config.flow?.nullIfBlank()?.let { dicQuery["flow"] = it }
|
config.flow?.nullIfBlank()?.let { dicQuery["flow"] = it }
|
||||||
|
config.finalMask?.nullIfBlank()?.let { dicQuery["fm"] = it }
|
||||||
// Add two keys for compatibility: "insecure" and "allowInsecure"
|
// Add two keys for compatibility: "insecure" and "allowInsecure"
|
||||||
if (config.security == AppConfig.TLS) {
|
if (config.security == AppConfig.TLS) {
|
||||||
val insecureFlag = if (config.insecure == true) "1" else "0"
|
val insecureFlag = if (config.insecure == true) "1" else "0"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.v2ray.ang.fmt
|
|||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
import com.v2ray.ang.dto.V2rayConfig.OutboundBean.StreamSettingsBean.FinalMaskBean
|
|
||||||
import com.v2ray.ang.enums.EConfigType
|
import com.v2ray.ang.enums.EConfigType
|
||||||
import com.v2ray.ang.enums.NetworkType
|
import com.v2ray.ang.enums.NetworkType
|
||||||
import com.v2ray.ang.extension.idnHost
|
import com.v2ray.ang.extension.idnHost
|
||||||
@@ -73,7 +72,37 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
dicQuery["mport"] = config.portHopping.orEmpty()
|
dicQuery["mport"] = config.portHopping.orEmpty()
|
||||||
}
|
}
|
||||||
if (config.portHoppingInterval.isNotNullEmpty()) {
|
if (config.portHoppingInterval.isNotNullEmpty()) {
|
||||||
dicQuery["mportHopInt"] = config.portHoppingInterval.orEmpty()
|
val rawInterval = config.portHoppingInterval?.trim().nullIfBlank()
|
||||||
|
val interval = if (rawInterval == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
val singleValue = rawInterval.toIntOrNull()
|
||||||
|
if (singleValue != null) {
|
||||||
|
if (singleValue < 5) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
rawInterval
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val parts = rawInterval.split('-')
|
||||||
|
if (parts.size == 2) {
|
||||||
|
val start = parts[0].trim().toIntOrNull()
|
||||||
|
val end = parts[1].trim().toIntOrNull()
|
||||||
|
if (start != null && end != null) {
|
||||||
|
val minStart = maxOf(5, start)
|
||||||
|
val minEnd = maxOf(minStart, end)
|
||||||
|
(minStart + minEnd) / 2
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (interval != null) {
|
||||||
|
dicQuery["mportHopInt"] = interval.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (config.pinnedCA256.isNotNullEmpty()) {
|
if (config.pinnedCA256.isNotNullEmpty()) {
|
||||||
dicQuery["pinSHA256"] = config.pinnedCA256.orEmpty()
|
dicQuery["pinSHA256"] = config.pinnedCA256.orEmpty()
|
||||||
@@ -107,18 +136,6 @@ object Hysteria2Fmt : FmtBase() {
|
|||||||
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
V2rayConfigManager.populateTlsSettings(it, profileItem, sni)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profileItem.obfsPassword.isNotNullEmpty()) {
|
|
||||||
outboundBean.streamSettings?.finalmask = FinalMaskBean(
|
|
||||||
udp = listOf(
|
|
||||||
FinalMaskBean.MaskBean(
|
|
||||||
type = "salamander",
|
|
||||||
settings = FinalMaskBean.MaskBean.MaskSettingsBean(
|
|
||||||
password = profileItem.obfsPassword
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return outboundBean
|
return outboundBean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ object AngConfigManager {
|
|||||||
count = parseBatchConfig(server, subid, append)
|
count = parseBatchConfig(server, subid, append)
|
||||||
}
|
}
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
count = parseCustomConfigServer(server, subid)
|
count = parseCustomConfigServer(server, subid, append)
|
||||||
}
|
}
|
||||||
|
|
||||||
var countSub = parseBatchSubscription(server)
|
var countSub = parseBatchSubscription(server)
|
||||||
@@ -221,21 +221,14 @@ object AngConfigManager {
|
|||||||
if (servers == null) {
|
if (servers == null) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
val removedSelectedServer =
|
// Find the currently selected server that matches the subscription ID
|
||||||
if (!TextUtils.isEmpty(subid) && !append) {
|
val removedSelected = if (subid.isNotBlank() && !append) {
|
||||||
MmkvManager.decodeServerConfig(
|
MmkvManager.getSelectServer()
|
||||||
MmkvManager.getSelectServer().orEmpty()
|
.takeIf { it?.isNotBlank() == true }
|
||||||
)?.let {
|
?.let { MmkvManager.decodeServerConfig(it) }
|
||||||
if (it.subscriptionId == subid) {
|
?.takeIf { it.subscriptionId == subid }
|
||||||
return@let it
|
} else {
|
||||||
}
|
null
|
||||||
return@let null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (!append) {
|
|
||||||
MmkvManager.removeServerViaSubid(subid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subid)
|
val subItem = MmkvManager.decodeSubscription(subid)
|
||||||
@@ -254,18 +247,12 @@ object AngConfigManager {
|
|||||||
|
|
||||||
// Batch save all parsed configs (only one serverList read/write)
|
// Batch save all parsed configs (only one serverList read/write)
|
||||||
if (configs.isNotEmpty()) {
|
if (configs.isNotEmpty()) {
|
||||||
val keys = batchSaveConfigs(configs, subid)
|
if (!append) {
|
||||||
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
// Handle removed selected server
|
|
||||||
removedSelectedServer?.let { removed ->
|
|
||||||
val matchKey = keys.find { key ->
|
|
||||||
val savedConfig = MmkvManager.decodeServerConfig(key)
|
|
||||||
savedConfig != null &&
|
|
||||||
savedConfig.server == removed.server &&
|
|
||||||
savedConfig.serverPort == removed.serverPort
|
|
||||||
}
|
|
||||||
matchKey?.let { MmkvManager.setSelectServer(it) }
|
|
||||||
}
|
}
|
||||||
|
val keyToProfile = batchSaveConfigs(configs, subid)
|
||||||
|
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
||||||
|
matchKey?.let { MmkvManager.setSelectServer(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return configs.size
|
return configs.size
|
||||||
@@ -281,10 +268,10 @@ object AngConfigManager {
|
|||||||
*
|
*
|
||||||
* @param configs The list of ProfileItem to save.
|
* @param configs The list of ProfileItem to save.
|
||||||
* @param subid The subscription ID.
|
* @param subid The subscription ID.
|
||||||
* @return The list of generated keys.
|
* @return Map of generated keys to their corresponding ProfileItem.
|
||||||
*/
|
*/
|
||||||
private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String): List<String> {
|
private fun batchSaveConfigs(configs: List<ProfileItem>, subid: String): Map<String, ProfileItem> {
|
||||||
val keys = mutableListOf<String>()
|
val keyToProfile = mutableMapOf<String, ProfileItem>()
|
||||||
|
|
||||||
// Read serverList once
|
// Read serverList once
|
||||||
val serverList = MmkvManager.decodeServerList(subid)
|
val serverList = MmkvManager.decodeServerList(subid)
|
||||||
@@ -302,12 +289,67 @@ object AngConfigManager {
|
|||||||
needSetSelected = false
|
needSetSelected = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keys.add(key)
|
keyToProfile[key] = config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write serverList once
|
// Write serverList once
|
||||||
MmkvManager.encodeServerList(serverList, subid)
|
MmkvManager.encodeServerList(serverList, subid)
|
||||||
return keys
|
return keyToProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a matched profile key from the given key-profile map using multi-level matching.
|
||||||
|
* Matching priority (from highest to lowest):
|
||||||
|
* 1. Exact match: server + port + password
|
||||||
|
* 2. Match by remarks (exact match)
|
||||||
|
* 3. Match by server + port
|
||||||
|
* 4. Match by server only
|
||||||
|
*
|
||||||
|
* @param keyToProfile Map of server keys to their ProfileItem
|
||||||
|
* @param target Target profile to match
|
||||||
|
* @return Matched key or null
|
||||||
|
*/
|
||||||
|
private fun findMatchedProfileKey(keyToProfile: Map<String, ProfileItem>, target: ProfileItem?): String? {
|
||||||
|
if (keyToProfile.isEmpty() || target == null) return null
|
||||||
|
|
||||||
|
// Level 1: Match by remarks
|
||||||
|
if (target.remarks.isNotBlank()) {
|
||||||
|
keyToProfile.entries.firstOrNull { (_, saved) ->
|
||||||
|
isSameText(saved.remarks, target.remarks)
|
||||||
|
}?.key?.let { return it }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 2: Exact match (server + port + password)
|
||||||
|
keyToProfile.entries.firstOrNull { (_, saved) ->
|
||||||
|
isSameText(saved.server, target.server) &&
|
||||||
|
isSameText(saved.serverPort, target.serverPort) &&
|
||||||
|
isSameText(saved.password, target.password)
|
||||||
|
}?.key?.let { return it }
|
||||||
|
|
||||||
|
// Level 3: Match by server + port
|
||||||
|
keyToProfile.entries.firstOrNull { (_, saved) ->
|
||||||
|
isSameText(saved.server, target.server) &&
|
||||||
|
isSameText(saved.serverPort, target.serverPort)
|
||||||
|
}?.key?.let { return it }
|
||||||
|
|
||||||
|
// Level 4: Match by server only
|
||||||
|
keyToProfile.entries.firstOrNull { (_, saved) ->
|
||||||
|
isSameText(saved.server, target.server)
|
||||||
|
}?.key?.let { return it }
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case-insensitive trimmed string comparison.
|
||||||
|
*
|
||||||
|
* @param left First string
|
||||||
|
* @param right Second string
|
||||||
|
* @return True if both are non-empty and equal (case-insensitive, trimmed)
|
||||||
|
*/
|
||||||
|
private fun isSameText(left: String?, right: String?): Boolean {
|
||||||
|
if (left.isNullOrBlank() || right.isNullOrBlank()) return false
|
||||||
|
return left.trim().equals(right.trim(), ignoreCase = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -315,9 +357,10 @@ object AngConfigManager {
|
|||||||
*
|
*
|
||||||
* @param server The server string.
|
* @param server The server string.
|
||||||
* @param subid The subscription ID.
|
* @param subid The subscription ID.
|
||||||
|
* @param append Whether to append the configurations.
|
||||||
* @return The number of configurations parsed.
|
* @return The number of configurations parsed.
|
||||||
*/
|
*/
|
||||||
private fun parseCustomConfigServer(server: String?, subid: String): Int {
|
private fun parseCustomConfigServer(server: String?, subid: String, append: Boolean): Int {
|
||||||
if (server == null) {
|
if (server == null) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -330,6 +373,9 @@ object AngConfigManager {
|
|||||||
JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
|
JsonUtil.fromJson(server, Array<Any>::class.java) ?: arrayOf()
|
||||||
|
|
||||||
if (serverList.isNotEmpty()) {
|
if (serverList.isNotEmpty()) {
|
||||||
|
if (!append) {
|
||||||
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
|
}
|
||||||
var count = 0
|
var count = 0
|
||||||
for (srv in serverList.reversed()) {
|
for (srv in serverList.reversed()) {
|
||||||
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
val config = CustomFmt.parse(JsonUtil.toJson(srv)) ?: continue
|
||||||
@@ -350,6 +396,9 @@ object AngConfigManager {
|
|||||||
val config = CustomFmt.parse(server) ?: return 0
|
val config = CustomFmt.parse(server) ?: return 0
|
||||||
config.subscriptionId = subid
|
config.subscriptionId = subid
|
||||||
config.description = generateDescription(config)
|
config.description = generateDescription(config)
|
||||||
|
if (!append) {
|
||||||
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
|
}
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
return 1
|
return 1
|
||||||
@@ -361,6 +410,9 @@ object AngConfigManager {
|
|||||||
try {
|
try {
|
||||||
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
|
val config = WireguardFmt.parseWireguardConfFile(server) ?: return R.string.toast_incorrect_protocol
|
||||||
config.description = generateDescription(config)
|
config.description = generateDescription(config)
|
||||||
|
if (!append) {
|
||||||
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
|
}
|
||||||
val key = MmkvManager.encodeServerConfig("", config)
|
val key = MmkvManager.encodeServerConfig("", config)
|
||||||
MmkvManager.encodeServerRaw(key, server)
|
MmkvManager.encodeServerRaw(key, server)
|
||||||
return 1
|
return 1
|
||||||
@@ -533,7 +585,7 @@ object AngConfigManager {
|
|||||||
count = parseBatchConfig(server, subid, append)
|
count = parseBatchConfig(server, subid, append)
|
||||||
}
|
}
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
count = parseCustomConfigServer(server, subid)
|
count = parseCustomConfigServer(server, subid, append)
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -710,8 +710,6 @@ object V2rayConfigManager {
|
|||||||
} else {
|
} else {
|
||||||
v2rayConfig.outbounds.add(outbound)
|
v2rayConfig.outbounds.add(outbound)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOutboundFragment(v2rayConfig)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -955,68 +953,80 @@ object V2rayConfigManager {
|
|||||||
*
|
*
|
||||||
* Configures packet fragmentation for TLS and REALITY protocols if enabled.
|
* Configures packet fragmentation for TLS and REALITY protocols if enabled.
|
||||||
*
|
*
|
||||||
* @param v2rayConfig The V2ray configuration object to be modified
|
* @param streamSettings The streamSettings object to be modified
|
||||||
* @return true if fragment configuration was successful, false otherwise
|
* @return true if fragment configuration was successful, false otherwise
|
||||||
*/
|
*/
|
||||||
private fun updateOutboundFragment(v2rayConfig: V2rayConfig): Boolean {
|
private fun updateOutboundFragment(streamSettings: StreamSettingsBean): Boolean {
|
||||||
try {
|
try {
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.TLS
|
if (streamSettings.security != AppConfig.TLS
|
||||||
&& v2rayConfig.outbounds[0].streamSettings?.security != AppConfig.REALITY
|
&& streamSettings.security != AppConfig.REALITY
|
||||||
) {
|
) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if (streamSettings.sockopt?.dialerProxy.isNotNullEmpty()) {
|
||||||
val fragmentOutbound =
|
return true
|
||||||
OutboundBean(
|
}
|
||||||
protocol = AppConfig.PROTOCOL_FREEDOM,
|
|
||||||
tag = AppConfig.TAG_FRAGMENT,
|
|
||||||
mux = null
|
|
||||||
)
|
|
||||||
|
|
||||||
var packets =
|
var packets =
|
||||||
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
|
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
|
||||||
if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.REALITY
|
if (streamSettings.security == AppConfig.REALITY
|
||||||
&& packets == "tlshello"
|
&& packets == "tlshello"
|
||||||
) {
|
) {
|
||||||
packets = "1-3"
|
packets = "1-3"
|
||||||
} else if (v2rayConfig.outbounds[0].streamSettings?.security == AppConfig.TLS
|
} else if (streamSettings.security == AppConfig.TLS
|
||||||
&& packets != "tlshello"
|
&& packets != "tlshello"
|
||||||
) {
|
) {
|
||||||
packets = "tlshello"
|
packets = "tlshello"
|
||||||
}
|
}
|
||||||
|
|
||||||
fragmentOutbound.settings = OutSettingsBean(
|
val fragmentMask = StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
fragment = OutSettingsBean.FragmentBean(
|
type = "fragment",
|
||||||
|
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
packets = packets,
|
packets = packets,
|
||||||
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
|
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
|
||||||
?: "50-100",
|
?: "50-100",
|
||||||
interval = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
delay = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
||||||
?: "10-20"
|
?: "10-20"
|
||||||
),
|
)
|
||||||
noises = listOf(
|
)
|
||||||
OutSettingsBean.NoiseBean(
|
val noiseMask = StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
type = "rand",
|
type = "noise",
|
||||||
packet = "10-20",
|
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
delay = "10-16",
|
noise = listOf(
|
||||||
|
StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean.NoiseMaskBean(
|
||||||
|
rand = "10-20",
|
||||||
|
delay = "10-16",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
),
|
|
||||||
)
|
|
||||||
fragmentOutbound.streamSettings = StreamSettingsBean(
|
|
||||||
sockopt = StreamSettingsBean.SockoptBean(
|
|
||||||
TcpNoDelay = true,
|
|
||||||
mark = 255
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
v2rayConfig.outbounds.add(fragmentOutbound)
|
|
||||||
|
|
||||||
//proxy chain
|
val finalMaskObj = streamSettings.finalmask?.let { existingFinalMask ->
|
||||||
v2rayConfig.outbounds[0].streamSettings?.sockopt =
|
JsonUtil.parseString(JsonUtil.toJson(existingFinalMask))
|
||||||
StreamSettingsBean.SockoptBean(
|
} ?: com.google.gson.JsonObject()
|
||||||
dialerProxy = AppConfig.TAG_FRAGMENT
|
|
||||||
)
|
// finalmask.tcp / finalmask.udp are arrays; prepend mask at index 0.
|
||||||
|
fun prependMask(scope: String, mask: StreamSettingsBean.FinalMaskBean.MaskBean) {
|
||||||
|
val current = finalMaskObj.get(scope)
|
||||||
|
if (current != null && current.isJsonArray && current.asJsonArray.size() > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val newArray = JsonArray()
|
||||||
|
newArray.add(JsonUtil.parseString(JsonUtil.toJson(mask)))
|
||||||
|
|
||||||
|
if (current != null && current.isJsonArray) {
|
||||||
|
current.asJsonArray.forEach { newArray.add(it) }
|
||||||
|
}
|
||||||
|
finalMaskObj.add(scope, newArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
prependMask("tcp", fragmentMask)
|
||||||
|
prependMask("udp", noiseMask)
|
||||||
|
streamSettings.finalmask = finalMaskObj
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to update outbound fragment", e)
|
Log.e(AppConfig.TAG, "Failed to update outbound fragment", e)
|
||||||
return false
|
return false
|
||||||
@@ -1174,7 +1184,7 @@ object V2rayConfigManager {
|
|||||||
val authority = profileItem.authority
|
val authority = profileItem.authority
|
||||||
val xhttpMode = profileItem.xhttpMode
|
val xhttpMode = profileItem.xhttpMode
|
||||||
val xhttpExtra = profileItem.xhttpExtra
|
val xhttpExtra = profileItem.xhttpExtra
|
||||||
|
val finalMask = profileItem.finalMask
|
||||||
var sni: String? = null
|
var sni: String? = null
|
||||||
streamSettings.network = transport.ifEmpty { NetworkType.TCP.type }
|
streamSettings.network = transport.ifEmpty { NetworkType.TCP.type }
|
||||||
when (streamSettings.network) {
|
when (streamSettings.network) {
|
||||||
@@ -1290,21 +1300,69 @@ object V2rayConfigManager {
|
|||||||
val hysteriaSetting = StreamSettingsBean.HysteriaSettingsBean(
|
val hysteriaSetting = StreamSettingsBean.HysteriaSettingsBean(
|
||||||
version = 2,
|
version = 2,
|
||||||
auth = profileItem.password.orEmpty(),
|
auth = profileItem.password.orEmpty(),
|
||||||
up = profileItem.bandwidthUp?.ifEmpty { "0" }.orEmpty(),
|
|
||||||
down = profileItem.bandwidthDown?.ifEmpty { "0" }.orEmpty(),
|
|
||||||
udphop = null
|
|
||||||
)
|
)
|
||||||
|
val quicParams = StreamSettingsBean.FinalMaskBean.QuicParamsBean(
|
||||||
|
brutalUp = profileItem.bandwidthUp?.nullIfBlank(),
|
||||||
|
brutalDown = profileItem.bandwidthDown?.nullIfBlank(),
|
||||||
|
)
|
||||||
|
quicParams.congestion = if (quicParams.brutalUp != null || quicParams.brutalDown != null) "brutal" else null
|
||||||
if (profileItem.portHopping.isNotNullEmpty()) {
|
if (profileItem.portHopping.isNotNullEmpty()) {
|
||||||
hysteriaSetting.udphop = StreamSettingsBean.HysteriaSettingsBean.HysteriaUdpHopBean(
|
val rawInterval = profileItem.portHoppingInterval?.trim().nullIfBlank()
|
||||||
port = profileItem.portHopping,
|
val interval = if (rawInterval == null) {
|
||||||
interval = profileItem.portHoppingInterval
|
"30"
|
||||||
?.trim()
|
} else {
|
||||||
?.toIntOrNull()
|
val singleValue = rawInterval.toIntOrNull()
|
||||||
?.takeIf { it >= 5 }
|
if (singleValue != null) {
|
||||||
?: 30
|
if (singleValue < 5) {
|
||||||
|
"30"
|
||||||
|
} else {
|
||||||
|
rawInterval
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val parts = rawInterval.split('-')
|
||||||
|
if (parts.size == 2) {
|
||||||
|
val start = parts[0].trim().toIntOrNull()
|
||||||
|
val end = parts[1].trim().toIntOrNull()
|
||||||
|
if (start != null && end != null) {
|
||||||
|
val minStart = maxOf(5, start)
|
||||||
|
val minEnd = maxOf(minStart, end)
|
||||||
|
"$minStart-$minEnd"
|
||||||
|
} else {
|
||||||
|
"30"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"30"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
quicParams.udpHop = StreamSettingsBean.FinalMaskBean.QuicParamsBean.UdpHopBean(
|
||||||
|
ports = profileItem.portHopping,
|
||||||
|
interval = interval
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val finalmask = StreamSettingsBean.FinalMaskBean(
|
||||||
|
quicParams = quicParams
|
||||||
|
)
|
||||||
|
if (profileItem.obfsPassword.isNotNullEmpty()) {
|
||||||
|
finalmask.udp = listOf(
|
||||||
|
StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
|
type = "salamander",
|
||||||
|
settings = StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
|
password = profileItem.obfsPassword.orEmpty()
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
streamSettings.hysteriaSettings = hysteriaSetting
|
streamSettings.hysteriaSettings = hysteriaSetting
|
||||||
|
streamSettings.finalmask = finalmask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalMask?.let {
|
||||||
|
val parsedFinalMask = JsonUtil.parseString(finalMask)
|
||||||
|
if (parsedFinalMask != null) {
|
||||||
|
streamSettings.finalmask = parsedFinalMask
|
||||||
|
} else {
|
||||||
|
Log.w("V2rayConfigManager", "Invalid finalMask JSON, keeping previously generated finalmask")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sni
|
return sni
|
||||||
@@ -1354,6 +1412,10 @@ object V2rayConfigManager {
|
|||||||
streamSettings.tlsSettings = null
|
streamSettings.tlsSettings = null
|
||||||
streamSettings.realitySettings = tlsSetting
|
streamSettings.realitySettings = tlsSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profileItem.finalMask.isNullOrEmpty()) {
|
||||||
|
updateOutboundFragment(streamSettings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.contracts.MainAdapterListener
|
import com.v2ray.ang.contracts.MainAdapterListener
|
||||||
@@ -28,7 +29,8 @@ import com.v2ray.ang.viewmodel.MainViewModel
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
||||||
|
SwipeRefreshLayout.OnRefreshListener {
|
||||||
private val ownerActivity: MainActivity
|
private val ownerActivity: MainActivity
|
||||||
get() = requireActivity() as MainActivity
|
get() = requireActivity() as MainActivity
|
||||||
private val mainViewModel: MainViewModel by activityViewModels()
|
private val mainViewModel: MainViewModel by activityViewModels()
|
||||||
@@ -68,6 +70,11 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter, allowSwipe = false))
|
itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter, allowSwipe = false))
|
||||||
itemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
itemTouchHelper?.attachToRecyclerView(binding.recyclerView)
|
||||||
|
|
||||||
|
binding.refreshLayout.isEnabled = false
|
||||||
|
// binding.refreshLayout.setOnRefreshListener(this)
|
||||||
|
// // Set the distance to trigger sync to 160dp
|
||||||
|
// binding.refreshLayout.setDistanceToTriggerSync((160 * resources.displayMetrics.density).toInt())
|
||||||
|
|
||||||
mainViewModel.updateListAction.observe(viewLifecycleOwner) { index ->
|
mainViewModel.updateListAction.observe(viewLifecycleOwner) { index ->
|
||||||
if (mainViewModel.subscriptionId != subId) {
|
if (mainViewModel.subscriptionId != subId) {
|
||||||
return@observe
|
return@observe
|
||||||
@@ -142,7 +149,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
* @param guid The server unique identifier
|
* @param guid The server unique identifier
|
||||||
*/
|
*/
|
||||||
private fun shareFullContent(guid: String) {
|
private fun shareFullContent(guid: String) {
|
||||||
ownerActivity.lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val result = AngConfigManager.shareFullContent2Clipboard(ownerActivity, guid)
|
val result = AngConfigManager.shareFullContent2Clipboard(ownerActivity, guid)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
@@ -212,7 +219,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
* @param position The position in the list
|
* @param position The position in the list
|
||||||
*/
|
*/
|
||||||
private fun removeServerSub(guid: String, position: Int) {
|
private fun removeServerSub(guid: String, position: Int) {
|
||||||
ownerActivity.mainViewModel.removeServer(guid)
|
mainViewModel.removeServer(guid)
|
||||||
adapter.removeServerSub(guid, position)
|
adapter.removeServerSub(guid, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,4 +278,43 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
shareServer(guid, profile, position, shareOptions, skip)
|
shareServer(guid, profile, position, shareOptions, skip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRefresh() {
|
||||||
|
ownerActivity.importConfigViaSub()
|
||||||
|
//binding.refreshLayout.isRefreshing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls to the currently selected server in the RecyclerView
|
||||||
|
*/
|
||||||
|
fun scrollToSelectedServer() {
|
||||||
|
val selectedGuid = MmkvManager.getSelectServer()
|
||||||
|
if (selectedGuid.isNullOrEmpty()) {
|
||||||
|
ownerActivity.toast(R.string.title_file_chooser)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the position of the selected server
|
||||||
|
val serversCache = mainViewModel.serversCache
|
||||||
|
val position = serversCache.indexOfFirst { it.guid == selectedGuid }
|
||||||
|
val recyclerView = binding.recyclerView
|
||||||
|
|
||||||
|
if (position >= 0) {
|
||||||
|
// Get the layout manager
|
||||||
|
val layoutManager = recyclerView.layoutManager as? GridLayoutManager
|
||||||
|
|
||||||
|
if (layoutManager != null) {
|
||||||
|
// Scroll to position with offset to center it on screen
|
||||||
|
// First scroll to position, then adjust to center
|
||||||
|
recyclerView.post {
|
||||||
|
layoutManager.scrollToPositionWithOffset(position, recyclerView.height / 3)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to smooth scroll if layout manager is not GridLayoutManager
|
||||||
|
recyclerView.smoothScrollToPosition(position)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ownerActivity.toast(R.string.toast_server_not_found_in_group)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -339,6 +339,10 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.locate_selected_config -> {
|
||||||
|
locateSelectedServer()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
@@ -433,7 +437,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
/**
|
/**
|
||||||
* import config from sub
|
* import config from sub
|
||||||
*/
|
*/
|
||||||
private fun importConfigViaSub(): Boolean {
|
fun importConfigViaSub(): Boolean {
|
||||||
showLoading()
|
showLoading()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
@@ -569,6 +573,47 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locates and scrolls to the currently selected server.
|
||||||
|
* If the selected server is in a different group, automatically switches to that group first.
|
||||||
|
*/
|
||||||
|
private fun locateSelectedServer() {
|
||||||
|
val targetSubscriptionId = mainViewModel.findSubscriptionIdBySelect()
|
||||||
|
if (targetSubscriptionId.isNullOrEmpty()) {
|
||||||
|
toast(R.string.title_file_chooser)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetGroupIndex = groupPagerAdapter.groups.indexOfFirst { it.id == targetSubscriptionId }
|
||||||
|
if (targetGroupIndex < 0) {
|
||||||
|
toast(R.string.toast_server_not_found_in_group)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to target group if needed, then scroll to the server
|
||||||
|
if (binding.viewPager.currentItem != targetGroupIndex) {
|
||||||
|
binding.viewPager.setCurrentItem(targetGroupIndex, true)
|
||||||
|
binding.viewPager.postDelayed({ scrollToSelectedServer(targetGroupIndex) }, 1000)
|
||||||
|
} else {
|
||||||
|
scrollToSelectedServer(targetGroupIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls to the selected server in the specified fragment.
|
||||||
|
* @param groupIndex The index of the group/fragment to scroll in
|
||||||
|
*/
|
||||||
|
private fun scrollToSelectedServer(groupIndex: Int) {
|
||||||
|
val itemId = groupPagerAdapter.getItemId(groupIndex)
|
||||||
|
val fragment = supportFragmentManager.findFragmentByTag("f$itemId") as? GroupServerFragment
|
||||||
|
|
||||||
|
if (fragment?.isAdded == true && fragment.view != null) {
|
||||||
|
fragment.scrollToSelectedServer()
|
||||||
|
} else {
|
||||||
|
toast(R.string.toast_fragment_not_available)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) {
|
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_BUTTON_B) {
|
||||||
moveTaskToBack(false)
|
moveTaskToBack(false)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.v2ray.ang.dto.ProfileItem
|
|||||||
import com.v2ray.ang.enums.EConfigType
|
import com.v2ray.ang.enums.EConfigType
|
||||||
import com.v2ray.ang.enums.NetworkType
|
import com.v2ray.ang.enums.NetworkType
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.extension.nullIfBlank
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
@@ -132,6 +133,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) }
|
private val et_bandwidth_down: EditText? by lazy { findViewById(R.id.et_bandwidth_down) }
|
||||||
private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) }
|
private val et_bandwidth_up: EditText? by lazy { findViewById(R.id.et_bandwidth_up) }
|
||||||
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
|
private val et_extra: EditText? by lazy { findViewById(R.id.et_extra) }
|
||||||
|
private val et_fm: EditText? by lazy { findViewById(R.id.et_fm) }
|
||||||
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
|
private val layout_extra: LinearLayout? by lazy { findViewById(R.id.layout_extra) }
|
||||||
private val et_ech_config_list: EditText? by lazy { findViewById(R.id.et_ech_config_list) }
|
private val et_ech_config_list: EditText? by lazy { findViewById(R.id.et_ech_config_list) }
|
||||||
private val container_ech_config_list: LinearLayout? by lazy { findViewById(R.id.lay_ech_config_list) }
|
private val container_ech_config_list: LinearLayout? by lazy { findViewById(R.id.lay_ech_config_list) }
|
||||||
@@ -241,6 +243,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
else -> null
|
else -> null
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
)
|
)
|
||||||
|
et_fm?.text = Utils.getEditable(config?.finalMask)
|
||||||
|
|
||||||
layout_extra?.visibility =
|
layout_extra?.visibility =
|
||||||
when (networks[position]) {
|
when (networks[position]) {
|
||||||
@@ -489,6 +492,13 @@ class ServerActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (et_fm?.text?.toString().isNotNullEmpty()) {
|
||||||
|
if (JsonUtil.parseString(et_fm?.text?.toString()) == null) {
|
||||||
|
toast(R.string.server_lab_final_mask)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveCommon(config)
|
saveCommon(config)
|
||||||
saveStreamSettings(config)
|
saveStreamSettings(config)
|
||||||
saveTls(config)
|
saveTls(config)
|
||||||
@@ -533,7 +543,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
} else if (config.configType == EConfigType.HYSTERIA2) {
|
} else if (config.configType == EConfigType.HYSTERIA2) {
|
||||||
config.obfsPassword = et_obfs_password?.text?.toString()
|
config.obfsPassword = et_obfs_password?.text?.toString()
|
||||||
config.portHopping = et_port_hop?.text?.toString()
|
config.portHopping = et_port_hop?.text?.toString()
|
||||||
config.portHoppingInterval = et_port_hop_interval?.text?.toString()
|
config.portHoppingInterval = et_port_hop_interval?.text?.toString()?.trim()
|
||||||
config.bandwidthDown = et_bandwidth_down?.text?.toString()
|
config.bandwidthDown = et_bandwidth_down?.text?.toString()
|
||||||
config.bandwidthUp = et_bandwidth_up?.text?.toString()
|
config.bandwidthUp = et_bandwidth_up?.text?.toString()
|
||||||
}
|
}
|
||||||
@@ -557,7 +567,8 @@ class ServerActivity : BaseActivity() {
|
|||||||
profileItem.serviceName = path
|
profileItem.serviceName = path
|
||||||
profileItem.authority = requestHost
|
profileItem.authority = requestHost
|
||||||
profileItem.xhttpMode = transportTypes(networks[network])[type]
|
profileItem.xhttpMode = transportTypes(networks[network])[type]
|
||||||
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim()
|
profileItem.xhttpExtra = et_extra?.text?.toString()?.trim().nullIfBlank()
|
||||||
|
profileItem.finalMask = et_fm?.text?.toString()?.trim()?.nullIfBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveTls(config: ProfileItem) {
|
private fun saveTls(config: ProfileItem) {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
hideLoading()
|
hideLoading()
|
||||||
|
refreshData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.v2ray.ang.dto.ServersCache
|
|||||||
import com.v2ray.ang.dto.SubscriptionCache
|
import com.v2ray.ang.dto.SubscriptionCache
|
||||||
import com.v2ray.ang.dto.SubscriptionUpdateResult
|
import com.v2ray.ang.dto.SubscriptionUpdateResult
|
||||||
import com.v2ray.ang.dto.TestServiceMessage
|
import com.v2ray.ang.dto.TestServiceMessage
|
||||||
|
import com.v2ray.ang.extension.matchesPattern
|
||||||
import com.v2ray.ang.extension.serializable
|
import com.v2ray.ang.extension.serializable
|
||||||
import com.v2ray.ang.extension.toastError
|
import com.v2ray.ang.extension.toastError
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
@@ -35,6 +36,7 @@ import kotlinx.coroutines.cancelChildren
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
import java.util.regex.PatternSyntaxException
|
||||||
|
|
||||||
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private var serverList = mutableListOf<String>() // MmkvManager.decodeServerList()
|
private var serverList = mutableListOf<String>() // MmkvManager.decodeServerList()
|
||||||
@@ -117,7 +119,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
fun updateCache() {
|
fun updateCache() {
|
||||||
serversCache.clear()
|
serversCache.clear()
|
||||||
val kw = keywordFilter.trim().lowercase()
|
val kw = keywordFilter.trim()
|
||||||
|
val searchRegex = try {
|
||||||
|
if (kw.isNotEmpty()) Regex(kw, setOf(RegexOption.IGNORE_CASE)) else null
|
||||||
|
} catch (e: PatternSyntaxException) {
|
||||||
|
null // Fallback to literal search if regex is invalid
|
||||||
|
}
|
||||||
for (guid in serverList) {
|
for (guid in serverList) {
|
||||||
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
|
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
|
||||||
if (kw.isEmpty()) {
|
if (kw.isEmpty()) {
|
||||||
@@ -125,11 +132,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val remarks = profile.remarks.lowercase()
|
val remarks = profile.remarks
|
||||||
val description = profile.description.orEmpty().lowercase()
|
val description = profile.description.orEmpty()
|
||||||
val server = profile.server.orEmpty().lowercase()
|
val server = profile.server.orEmpty()
|
||||||
|
val protocol = profile.configType.name
|
||||||
if (remarks.contains(kw) || description.contains(kw) || server.contains(kw)) {
|
if (remarks.matchesPattern(searchRegex, kw)
|
||||||
|
|| description.matchesPattern(searchRegex, kw)
|
||||||
|
|| server.matchesPattern(searchRegex, kw)
|
||||||
|
|| protocol.matchesPattern(searchRegex, kw)
|
||||||
|
) {
|
||||||
serversCache.add(ServersCache(guid, profile))
|
serversCache.add(ServersCache(guid, profile))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,6 +413,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
reloadServerList()
|
reloadServerList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun findSubscriptionIdBySelect(): String? {
|
||||||
|
// Get the selected server GUID
|
||||||
|
val selectedGuid = MmkvManager.getSelectServer()
|
||||||
|
if (selectedGuid.isNullOrEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val config = MmkvManager.decodeServerConfig(selectedGuid)
|
||||||
|
return config?.subscriptionId
|
||||||
|
}
|
||||||
|
|
||||||
fun onTestsFinished() {
|
fun onTestsFinished() {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_REMOVE_INVALID_AFTER_TEST)) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_AUTO_REMOVE_INVALID_AFTER_TEST)) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
tools:context=".ui.LogcatActivity">
|
tools:context=".ui.LogcatActivity">
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/refreshLayout"
|
android:id="@+id/refresh_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom|end"
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginBottom="-16dp"
|
||||||
android:layout_marginEnd="@dimen/padding_spacing_dp16">
|
android:layout_marginEnd="@dimen/padding_spacing_dp16">
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
@@ -108,6 +109,7 @@
|
|||||||
android:nextFocusLeft="@+id/layout_test"
|
android:nextFocusLeft="@+id/layout_test"
|
||||||
android:src="@drawable/ic_play_24dp"
|
android:src="@drawable/ic_play_24dp"
|
||||||
app:tint="@color/colorWhite"
|
app:tint="@color/colorWhite"
|
||||||
|
app:useCompatPadding="true"
|
||||||
app:layout_anchorGravity="bottom|right|end" />
|
app:layout_anchorGravity="bottom|right|end" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@@ -85,8 +85,7 @@
|
|||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/et_port_hop_interval"
|
android:id="@+id/et_port_hop_interval"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content" />
|
||||||
android:inputType="number" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,17 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/recycler_view"
|
android:id="@+id/refresh_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:scrollbars="vertical" />
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="vertical" />
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@@ -118,4 +118,25 @@
|
|||||||
android:minLines="4" />
|
android:minLines="4" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_fm"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/padding_spacing_dp16"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/server_lab_final_mask" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_fm"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="top"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:maxLines="20"
|
||||||
|
android:minLines="4" />
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -98,6 +98,10 @@
|
|||||||
android:id="@+id/sort_by_test_results"
|
android:id="@+id/sort_by_test_results"
|
||||||
android:title="@string/title_sort_by_test_results"
|
android:title="@string/title_sort_by_test_results"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/locate_selected_config"
|
||||||
|
android:title="@string/title_locate_selected_config"
|
||||||
|
app:showAsAction="never" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/sub_update"
|
android:id="@+id/sub_update"
|
||||||
android:title="@string/title_sub_update"
|
android:title="@string/title_sub_update"
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||||
|
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||||
@@ -299,6 +300,10 @@
|
|||||||
<string name="title_update_config_count">Update %d configurations</string>
|
<string name="title_update_config_count">Update %d configurations</string>
|
||||||
<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_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="title_update_subscription_no_subscription">No subscriptions</string>
|
||||||
|
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
|
||||||
|
<string name="toast_fragment_not_available">Unable to locate current view</string>
|
||||||
|
<string name="title_locate_selected_config">Locate the selected config</string>
|
||||||
|
|
||||||
<string name="tasker_start_service">بدء الخدمة</string>
|
<string name="tasker_start_service">بدء الخدمة</string>
|
||||||
<string name="tasker_setting_confirm">تأكيد</string>
|
<string name="tasker_setting_confirm">تأكيد</string>
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||||
|
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||||
@@ -298,6 +299,10 @@
|
|||||||
<string name="title_update_config_count">Update %d configurations</string>
|
<string name="title_update_config_count">Update %d configurations</string>
|
||||||
<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_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="title_update_subscription_no_subscription">No subscriptions</string>
|
||||||
|
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
|
||||||
|
<string name="toast_fragment_not_available">Unable to locate current view</string>
|
||||||
|
<string name="title_locate_selected_config">Locate the selected config</string>
|
||||||
|
|
||||||
<string name="tasker_start_service">সার্ভিস শুরু করুন</string>
|
<string name="tasker_start_service">সার্ভিস শুরু করুন</string>
|
||||||
<string name="tasker_setting_confirm">নিশ্চিত করুন</string>
|
<string name="tasker_setting_confirm">নিশ্চিত করুন</string>
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">وا روء رئڌن پئنا باند (واهڌ)</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_xhttp_extra">XHTTP Extra خام JSON، قالوو: { XHTTPObject }</string>
|
||||||
|
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||||
<string name="server_lab_ech_config_list">نومگه کانفیگ Ech</string>
|
<string name="server_lab_ech_config_list">نومگه کانفیگ Ech</string>
|
||||||
<string name="server_lab_ech_force_query">پورس وو جۊ اجباری Ech</string>
|
<string name="server_lab_ech_force_query">پورس وو جۊ اجباری Ech</string>
|
||||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||||
@@ -299,6 +300,10 @@
|
|||||||
<string name="title_update_config_count">ورۊ کردن %d کانفیگ</string>
|
<string name="title_update_config_count">ورۊ کردن %d کانفیگ</string>
|
||||||
<string name="title_update_subscription_result">%1$d کانفیگ ورۊ وابی (مووفق: %2$d، شکست خرده: %3$d، رڌ وابیڌه: %4$d)</string>
|
<string name="title_update_subscription_result">%1$d کانفیگ ورۊ وابی (مووفق: %2$d، شکست خرده: %3$d، رڌ وابیڌه: %4$d)</string>
|
||||||
<string name="title_update_subscription_no_subscription">بؽ اشتراک</string>
|
<string name="title_update_subscription_no_subscription">بؽ اشتراک</string>
|
||||||
|
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
|
||||||
|
<string name="toast_fragment_not_available">Unable to locate current view</string>
|
||||||
|
<string name="title_locate_selected_config">Locate the selected config</string>
|
||||||
|
|
||||||
<string name="tasker_start_service">ره وندن خدمات</string>
|
<string name="tasker_start_service">ره وندن خدمات</string>
|
||||||
<string name="tasker_setting_confirm">قوۊل</string>
|
<string name="tasker_setting_confirm">قوۊل</string>
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">افزایش پهنای باند (واحد)</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">خام JSON XHTTP Extra، قالب: { XHTTPObject }</string>
|
<string name="server_lab_xhttp_extra">خام JSON XHTTP Extra، قالب: { XHTTPObject }</string>
|
||||||
|
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||||
<string name="server_lab_ech_config_list">لیست کانفیگ Ech</string>
|
<string name="server_lab_ech_config_list">لیست کانفیگ Ech</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||||
@@ -296,6 +297,10 @@
|
|||||||
<string name="title_update_config_count">آپدیت کردن %d کانفیگ</string>
|
<string name="title_update_config_count">آپدیت کردن %d کانفیگ</string>
|
||||||
<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_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="title_update_subscription_no_subscription">No subscriptions</string>
|
||||||
|
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
|
||||||
|
<string name="toast_fragment_not_available">Unable to locate current view</string>
|
||||||
|
<string name="title_locate_selected_config">Locate the selected config</string>
|
||||||
|
|
||||||
<string name="tasker_start_service">شروع خدمات</string>
|
<string name="tasker_start_service">شروع خدمات</string>
|
||||||
<string name="tasker_setting_confirm">تایید</string>
|
<string name="tasker_setting_confirm">تایید</string>
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">Режим XHTTP</string>
|
<string name="server_lab_xhttp_mode">Режим XHTTP</string>
|
||||||
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
|
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
|
||||||
|
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
|
||||||
@@ -297,6 +298,9 @@
|
|||||||
<string name="title_update_config_count">Обновлено профилей: %d</string>
|
<string name="title_update_config_count">Обновлено профилей: %d</string>
|
||||||
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
|
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
|
||||||
<string name="title_update_subscription_no_subscription">Нет подписок</string>
|
<string name="title_update_subscription_no_subscription">Нет подписок</string>
|
||||||
|
<string name="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string>
|
||||||
|
<string name="toast_fragment_not_available">Фрагмент недоступен</string>
|
||||||
|
<string name="title_locate_selected_config">Найти выбранный профиль</string>
|
||||||
|
|
||||||
<string name="tasker_start_service">Запуск службы</string>
|
<string name="tasker_start_service">Запуск службы</string>
|
||||||
<string name="tasker_setting_confirm">Подтвердить</string>
|
<string name="tasker_setting_confirm">Подтвердить</string>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||||
|
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||||
@@ -299,6 +300,10 @@
|
|||||||
<string name="title_update_config_count">Update %d configurations</string>
|
<string name="title_update_config_count">Update %d configurations</string>
|
||||||
<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_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="title_update_subscription_no_subscription">No subscriptions</string>
|
||||||
|
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
|
||||||
|
<string name="toast_fragment_not_available">Unable to locate current view</string>
|
||||||
|
<string name="title_locate_selected_config">Locate the selected config</string>
|
||||||
|
|
||||||
<string name="tasker_start_service">Khởi động v2rayNG</string>
|
<string name="tasker_start_service">Khởi động v2rayNG</string>
|
||||||
<string name="tasker_setting_confirm">Xác nhận</string>
|
<string name="tasker_setting_confirm">Xác nhận</string>
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">带宽上行 (支持的单位 k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">带宽上行 (支持的单位 k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">XHTTP 模式</string>
|
<string name="server_lab_xhttp_mode">XHTTP 模式</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</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">EchConfigList</string>
|
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">证书指纹 (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">证书指纹 (SHA-256)</string>
|
||||||
@@ -297,6 +298,10 @@
|
|||||||
<string name="title_update_config_count">更新 %d 个配置</string>
|
<string name="title_update_config_count">更新 %d 个配置</string>
|
||||||
<string name="title_update_subscription_result">更新了 %1$d 个配置(%2$d 个成功,%3$d 个失败,%4$d 个跳过)</string>
|
<string name="title_update_subscription_result">更新了 %1$d 个配置(%2$d 个成功,%3$d 个失败,%4$d 个跳过)</string>
|
||||||
<string name="title_update_subscription_no_subscription">无订阅</string>
|
<string name="title_update_subscription_no_subscription">无订阅</string>
|
||||||
|
<string name="toast_server_not_found_in_group">当前分组中未找到选中的服务器</string>
|
||||||
|
<string name="toast_fragment_not_available">无法定位当前视图</string>
|
||||||
|
<string name="title_locate_selected_config">定位所选配置</string>
|
||||||
|
|
||||||
<string name="tasker_start_service">启动服务</string>
|
<string name="tasker_start_service">启动服务</string>
|
||||||
<string name="tasker_setting_confirm">确定</string>
|
<string name="tasker_setting_confirm">确定</string>
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">頻寬上行 (支持的單位 k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">頻寬上行 (支持的單位 k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">XHTTP 模式</string>
|
<string name="server_lab_xhttp_mode">XHTTP 模式</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra 原始 JSON,格式: { XHTTPObject }</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">EchConfigList</string>
|
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">證書指紋 (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">證書指紋 (SHA-256)</string>
|
||||||
@@ -297,6 +298,10 @@
|
|||||||
<string name="title_update_config_count">更新 %d 個配置</string>
|
<string name="title_update_config_count">更新 %d 個配置</string>
|
||||||
<string name="title_update_subscription_result">更新了 %1$d 個配置(%2$d 個成功,%3$d 個失敗,%4$d 個跳過)</string>
|
<string name="title_update_subscription_result">更新了 %1$d 個配置(%2$d 個成功,%3$d 個失敗,%4$d 個跳過)</string>
|
||||||
<string name="title_update_subscription_no_subscription">無訂閱</string>
|
<string name="title_update_subscription_no_subscription">無訂閱</string>
|
||||||
|
<string name="toast_server_not_found_in_group">當前分組中未找到選中的伺服器</string>
|
||||||
|
<string name="toast_fragment_not_available">定位所選配置</string>
|
||||||
|
<string name="title_locate_selected_config">Locate the selected config</string>
|
||||||
|
|
||||||
<string name="tasker_start_service">啟動服務</string>
|
<string name="tasker_start_service">啟動服務</string>
|
||||||
<string name="tasker_setting_confirm">確定</string>
|
<string name="tasker_setting_confirm">確定</string>
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,6 @@
|
|||||||
<dimen name="view_height_dp36">36dp</dimen>
|
<dimen name="view_height_dp36">36dp</dimen>
|
||||||
<dimen name="view_height_dp48">48dp</dimen>
|
<dimen name="view_height_dp48">48dp</dimen>
|
||||||
<dimen name="view_height_dp64">64dp</dimen>
|
<dimen name="view_height_dp64">64dp</dimen>
|
||||||
|
<dimen name="view_height_dp120">120dp</dimen>
|
||||||
<dimen name="view_height_dp160">160dp</dimen>
|
<dimen name="view_height_dp160">160dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -113,6 +113,7 @@
|
|||||||
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string>
|
||||||
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
<string name="server_lab_xhttp_mode">XHTTP Mode</string>
|
||||||
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||||
|
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||||
<string name="server_lab_ech_config_list">EchConfigList</string>
|
<string name="server_lab_ech_config_list">EchConfigList</string>
|
||||||
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
<string name="server_lab_ech_force_query">EchForceQuery</string>
|
||||||
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string>
|
||||||
@@ -303,6 +304,9 @@
|
|||||||
<string name="title_update_config_count">Update %d configs</string>
|
<string name="title_update_config_count">Update %d configs</string>
|
||||||
<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_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="title_update_subscription_no_subscription">No subscriptions</string>
|
||||||
|
<string name="toast_server_not_found_in_group">Selected server not found in current group</string>
|
||||||
|
<string name="toast_fragment_not_available">Unable to locate current view</string>
|
||||||
|
<string name="title_locate_selected_config">Locate the selected config</string>
|
||||||
|
|
||||||
<string name="tasker_start_service">Start Service</string>
|
<string name="tasker_start_service">Start Service</string>
|
||||||
<string name="tasker_setting_confirm">Confirm</string>
|
<string name="tasker_setting_confirm">Confirm</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user