Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0265265f3 | |||
| 2d6dd33a7b | |||
| 3a32373abe | |||
| befb2937d1 | |||
| 0a375bec67 | |||
| 029f8b67e7 | |||
| 7475d1757b | |||
| 5a16614a7b | |||
| edb06b466a | |||
| 30b5391895 | |||
| 6cd04b831f |
@@ -99,21 +99,21 @@ jobs:
|
|||||||
./gradlew assembleRelease -Pandroid.injected.signing.store.file=${{ steps.android_keystore.outputs.filePath }} -Pandroid.injected.signing.store.password=${{ secrets.APP_KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.APP_KEYSTORE_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.APP_KEY_PASSWORD }}
|
./gradlew assembleRelease -Pandroid.injected.signing.store.file=${{ steps.android_keystore.outputs.filePath }} -Pandroid.injected.signing.store.password=${{ secrets.APP_KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.APP_KEYSTORE_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.APP_KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: Upload arm64-v8a APK
|
- name: Upload arm64-v8a APK
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
with:
|
with:
|
||||||
name: arm64-v8a
|
name: arm64-v8a
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk
|
||||||
|
|
||||||
- name: Upload armeabi-v7a APK
|
- name: Upload armeabi-v7a APK
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
with:
|
with:
|
||||||
name: armeabi-v7a
|
name: armeabi-v7a
|
||||||
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk
|
||||||
|
|
||||||
- name: Upload x86 APK
|
- name: Upload x86 APK
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v7
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
with:
|
with:
|
||||||
name: x86-apk
|
name: x86-apk
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 711
|
versionCode = 713
|
||||||
versionName = "2.0.11"
|
versionName = "2.0.13"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ object AppConfig {
|
|||||||
|
|
||||||
/** Cache keys. */
|
/** Cache keys. */
|
||||||
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
||||||
const val CACHE_KEYWORD_FILTER = "cache_keyword_filter"
|
|
||||||
|
|
||||||
/** Protocol identifiers. */
|
/** Protocol identifiers. */
|
||||||
const val PROTOCOL_FREEDOM = "freedom"
|
const val PROTOCOL_FREEDOM = "freedom"
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of subscription update operation
|
||||||
|
*/
|
||||||
|
data class SubscriptionUpdateResult(
|
||||||
|
val configCount: Int = 0, // Total configs updated
|
||||||
|
val successCount: Int = 0, // Subscriptions updated successfully
|
||||||
|
val failureCount: Int = 0, // Subscriptions failed to update
|
||||||
|
val skipCount: Int = 0 // Subscriptions skipped (disabled)
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Combine two results by adding their counts
|
||||||
|
*/
|
||||||
|
operator fun plus(other: SubscriptionUpdateResult): SubscriptionUpdateResult {
|
||||||
|
return SubscriptionUpdateResult(
|
||||||
|
configCount = this.configCount + other.configCount,
|
||||||
|
successCount = this.successCount + other.successCount,
|
||||||
|
failureCount = this.failureCount + other.failureCount,
|
||||||
|
skipCount = this.skipCount + other.skipCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class TestServiceMessage(
|
||||||
|
val key: Int,
|
||||||
|
val subscriptionId: String = "",
|
||||||
|
val serverGuids: List<String> = emptyList()
|
||||||
|
) : Serializable
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ import com.v2ray.ang.R
|
|||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.SubscriptionCache
|
import com.v2ray.ang.dto.SubscriptionCache
|
||||||
import com.v2ray.ang.dto.SubscriptionItem
|
import com.v2ray.ang.dto.SubscriptionItem
|
||||||
|
import com.v2ray.ang.dto.SubscriptionUpdateResult
|
||||||
import com.v2ray.ang.enums.EConfigType
|
import com.v2ray.ang.enums.EConfigType
|
||||||
import com.v2ray.ang.extension.isNotNullEmpty
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
import com.v2ray.ang.fmt.CustomFmt
|
import com.v2ray.ang.fmt.CustomFmt
|
||||||
@@ -433,45 +434,48 @@ object AngConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Updates the configuration via all subscriptions.
|
* Updates the configuration via all subscriptions.
|
||||||
*
|
*
|
||||||
* @return The number of configurations updated.
|
* @return Detailed result of the subscription update operation.
|
||||||
*/
|
*/
|
||||||
fun updateConfigViaSubAll(): Int {
|
fun updateConfigViaSubAll(): SubscriptionUpdateResult {
|
||||||
var count = 0
|
return try {
|
||||||
try {
|
val subscriptions = MmkvManager.decodeSubscriptions()
|
||||||
MmkvManager.decodeSubscriptions().forEach {
|
subscriptions.fold(SubscriptionUpdateResult()) { acc, subscription ->
|
||||||
count += updateConfigViaSub(it)
|
acc + updateConfigViaSub(subscription)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
|
Log.e(AppConfig.TAG, "Failed to update config via all subscriptions", e)
|
||||||
return 0
|
SubscriptionUpdateResult()
|
||||||
}
|
}
|
||||||
return count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the configuration via a subscription.
|
* Updates the configuration via a subscription.
|
||||||
*
|
*
|
||||||
* @param it The subscription item.
|
* @param it The subscription item.
|
||||||
* @return The number of configurations updated.
|
* @return Subscription update result.
|
||||||
*/
|
*/
|
||||||
fun updateConfigViaSub(it: SubscriptionCache): Int {
|
fun updateConfigViaSub(it: SubscriptionCache): SubscriptionUpdateResult {
|
||||||
try {
|
try {
|
||||||
|
// Check if disabled
|
||||||
|
if (!it.subscription.enabled) {
|
||||||
|
return SubscriptionUpdateResult(skipCount = 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate subscription info
|
||||||
if (TextUtils.isEmpty(it.guid)
|
if (TextUtils.isEmpty(it.guid)
|
||||||
|| TextUtils.isEmpty(it.subscription.remarks)
|
|| TextUtils.isEmpty(it.subscription.remarks)
|
||||||
|| TextUtils.isEmpty(it.subscription.url)
|
|| TextUtils.isEmpty(it.subscription.url)
|
||||||
) {
|
) {
|
||||||
return 0
|
return SubscriptionUpdateResult(skipCount = 1)
|
||||||
}
|
|
||||||
if (!it.subscription.enabled) {
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val url = HttpUtil.toIdnUrl(it.subscription.url)
|
val url = HttpUtil.toIdnUrl(it.subscription.url)
|
||||||
if (!Utils.isValidUrl(url)) {
|
if (!Utils.isValidUrl(url)) {
|
||||||
return 0
|
return SubscriptionUpdateResult(failureCount = 1)
|
||||||
}
|
}
|
||||||
if (!it.subscription.allowInsecureUrl) {
|
if (!it.subscription.allowInsecureUrl) {
|
||||||
if (!Utils.isValidSubUrl(url)) {
|
if (!Utils.isValidSubUrl(url)) {
|
||||||
return 0
|
return SubscriptionUpdateResult(failureCount = 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(AppConfig.TAG, url)
|
Log.i(AppConfig.TAG, url)
|
||||||
@@ -493,18 +497,25 @@ object AngConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (configText.isEmpty()) {
|
if (configText.isEmpty()) {
|
||||||
return 0
|
return SubscriptionUpdateResult(failureCount = 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
val count = parseConfigViaSub(configText, it.guid, false)
|
val count = parseConfigViaSub(configText, it.guid, false)
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
it.subscription.lastUpdated = System.currentTimeMillis()
|
it.subscription.lastUpdated = System.currentTimeMillis()
|
||||||
MmkvManager.encodeSubscription(it.guid, it.subscription)
|
MmkvManager.encodeSubscription(it.guid, it.subscription)
|
||||||
Log.i(AppConfig.TAG, "Subscription updated: ${it.subscription.remarks}, $count configs")
|
Log.i(AppConfig.TAG, "Subscription updated: ${it.subscription.remarks}, $count configs")
|
||||||
|
return SubscriptionUpdateResult(
|
||||||
|
configCount = count,
|
||||||
|
successCount = 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Got response but no valid configs parsed
|
||||||
|
return SubscriptionUpdateResult(failureCount = 1)
|
||||||
}
|
}
|
||||||
return count
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to update config via subscription", e)
|
Log.e(AppConfig.TAG, "Failed to update config via subscription", e)
|
||||||
return 0
|
return SubscriptionUpdateResult(failureCount = 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ object MmkvManager {
|
|||||||
|
|
||||||
//region private
|
//region private
|
||||||
|
|
||||||
//private const val ID_PROFILE_CONFIG = "PROFILE_CONFIG"
|
|
||||||
private const val ID_MAIN = "MAIN"
|
private const val ID_MAIN = "MAIN"
|
||||||
private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG"
|
private const val ID_PROFILE_FULL_CONFIG = "PROFILE_FULL_CONFIG"
|
||||||
private const val ID_SERVER_RAW = "SERVER_RAW"
|
private const val ID_SERVER_RAW = "SERVER_RAW"
|
||||||
@@ -33,7 +32,6 @@ object MmkvManager {
|
|||||||
private const val KEY_SUB_IDS = "SUB_IDS"
|
private const val KEY_SUB_IDS = "SUB_IDS"
|
||||||
private const val KEY_WEBDAV_CONFIG = "WEBDAV_CONFIG"
|
private const val KEY_WEBDAV_CONFIG = "WEBDAV_CONFIG"
|
||||||
|
|
||||||
//private val profileStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
|
||||||
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
private val mainStorage by lazy { MMKV.mmkvWithID(ID_MAIN, MMKV.MULTI_PROCESS_MODE) }
|
||||||
private val profileFullStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_FULL_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
private val profileFullStorage by lazy { MMKV.mmkvWithID(ID_PROFILE_FULL_CONFIG, MMKV.MULTI_PROCESS_MODE) }
|
||||||
private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
|
private val serverRawStorage by lazy { MMKV.mmkvWithID(ID_SERVER_RAW, MMKV.MULTI_PROCESS_MODE) }
|
||||||
@@ -56,13 +54,6 @@ object MmkvManager {
|
|||||||
return mainStorage.decodeString(KEY_ANG_CONFIGS)
|
return mainStorage.decodeString(KEY_ANG_CONFIGS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Removes the legacy KEY_ANG_CONFIGS after migration.
|
|
||||||
// * This method is for migration purposes only.
|
|
||||||
// */
|
|
||||||
// fun removeLegacyServerListKey() {
|
|
||||||
// mainStorage.remove(KEY_ANG_CONFIGS)
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the selected server GUID.
|
* Gets the selected server GUID.
|
||||||
@@ -95,13 +86,6 @@ object MmkvManager {
|
|||||||
mainStorage.encode(key, JsonUtil.toJson(serverList))
|
mainStorage.encode(key, JsonUtil.toJson(serverList))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy method for compatibility
|
|
||||||
// TODO: Remove after migration and update all callers
|
|
||||||
/*
|
|
||||||
fun encodeServerList(serverList: MutableList<String>) {
|
|
||||||
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes the server list for a given subscription.
|
* Decodes the server list for a given subscription.
|
||||||
@@ -139,18 +123,6 @@ object MmkvManager {
|
|||||||
return allServers
|
return allServers
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy method for compatibility - reads all servers
|
|
||||||
// TODO: Remove after migration and update all callers
|
|
||||||
/*
|
|
||||||
fun decodeServerList(): MutableList<String> {
|
|
||||||
val json = mainStorage.decodeString(KEY_ANG_CONFIGS)
|
|
||||||
return if (json.isNullOrBlank()) {
|
|
||||||
mutableListOf()
|
|
||||||
} else {
|
|
||||||
JsonUtil.fromJson(json, Array<String>::class.java)?.toMutableList() ?: mutableListOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes the server configuration.
|
* Decodes the server configuration.
|
||||||
@@ -169,16 +141,6 @@ object MmkvManager {
|
|||||||
return JsonUtil.fromJson(json, ProfileItem::class.java)
|
return JsonUtil.fromJson(json, ProfileItem::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun decodeProfileConfig(guid: String): ProfileLiteItem? {
|
|
||||||
// if (guid.isBlank()) {
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
// val json = profileStorage.decodeString(guid)
|
|
||||||
// if (json.isNullOrBlank()) {
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
// return JsonUtil.fromJson(json, ProfileLiteItem::class.java)
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes the server configuration.
|
* Encodes the server configuration.
|
||||||
@@ -203,25 +165,6 @@ object MmkvManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy code - keep for reference during migration
|
|
||||||
/*
|
|
||||||
val serverList = decodeServerList()
|
|
||||||
if (!serverList.contains(key)) {
|
|
||||||
serverList.add(0, key)
|
|
||||||
encodeServerList(serverList)
|
|
||||||
if (getSelectServer().isNullOrBlank()) {
|
|
||||||
mainStorage.encode(KEY_SELECTED_SERVER, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
// val profile = ProfileLiteItem(
|
|
||||||
// configType = config.configType,
|
|
||||||
// subscriptionId = config.subscriptionId,
|
|
||||||
// remarks = config.remarks,
|
|
||||||
// server = config.getProxyOutbound()?.getServerAddress(),
|
|
||||||
// serverPort = config.getProxyOutbound()?.getServerPort(),
|
|
||||||
// )
|
|
||||||
// profileStorage.encode(key, JsonUtil.toJson(profile))
|
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,22 +197,11 @@ object MmkvManager {
|
|||||||
serverList.remove(guid)
|
serverList.remove(guid)
|
||||||
encodeServerList(serverList, subId)
|
encodeServerList(serverList, subId)
|
||||||
|
|
||||||
// Legacy code - keep for reference during migration
|
|
||||||
/*
|
|
||||||
if (getSelectServer() == guid) {
|
|
||||||
mainStorage.remove(KEY_SELECTED_SERVER)
|
|
||||||
}
|
|
||||||
val serverList = decodeServerList()
|
|
||||||
serverList.remove(guid)
|
|
||||||
encodeServerList(serverList)
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Clean up storage
|
// Clean up storage
|
||||||
if (getSelectServer() == guid) {
|
if (getSelectServer() == guid) {
|
||||||
mainStorage.remove(KEY_SELECTED_SERVER)
|
mainStorage.remove(KEY_SELECTED_SERVER)
|
||||||
}
|
}
|
||||||
profileFullStorage.remove(guid)
|
profileFullStorage.remove(guid)
|
||||||
//profileStorage.remove(guid)
|
|
||||||
serverAffStorage.remove(guid)
|
serverAffStorage.remove(guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,17 +225,6 @@ object MmkvManager {
|
|||||||
|
|
||||||
serverList.clear()
|
serverList.clear()
|
||||||
encodeServerList(serverList, subId)
|
encodeServerList(serverList, subId)
|
||||||
|
|
||||||
// Legacy code - keep for reference during migration
|
|
||||||
/*
|
|
||||||
profileFullStorage.allKeys()?.forEach { key ->
|
|
||||||
decodeServerConfig(key)?.let { config ->
|
|
||||||
if (config.subscriptionId == subid) {
|
|
||||||
removeServer(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -361,7 +282,6 @@ object MmkvManager {
|
|||||||
val count = profileFullStorage.allKeys()?.count() ?: 0
|
val count = profileFullStorage.allKeys()?.count() ?: 0
|
||||||
mainStorage.clearAll()
|
mainStorage.clearAll()
|
||||||
profileFullStorage.clearAll()
|
profileFullStorage.clearAll()
|
||||||
//profileStorage.clearAll()
|
|
||||||
serverAffStorage.clearAll()
|
serverAffStorage.clearAll()
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -502,8 +502,6 @@ object SettingsManager {
|
|||||||
MmkvManager.encodeServerList(serverGuids, subId)
|
MmkvManager.encodeServerList(serverGuids, subId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove legacy KEY_ANG_CONFIGS data
|
|
||||||
// MmkvManager.removeLegacyServerListKey()
|
|
||||||
|
|
||||||
// Mark migration as complete
|
// Mark migration as complete
|
||||||
MmkvManager.encodeSettings(migrationKey, true)
|
MmkvManager.encodeSettings(migrationKey, true)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import android.os.IBinder
|
|||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
|
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
|
||||||
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
|
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
|
||||||
|
import com.v2ray.ang.dto.TestServiceMessage
|
||||||
|
import com.v2ray.ang.extension.serializable
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.handler.V2RayNativeManager
|
import com.v2ray.ang.handler.V2RayNativeManager
|
||||||
import com.v2ray.ang.util.MessageUtil
|
import com.v2ray.ang.util.MessageUtil
|
||||||
@@ -52,13 +54,15 @@ class V2RayTestService : Service() {
|
|||||||
* @return The start mode.
|
* @return The start mode.
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
when (intent?.getIntExtra("key", 0)) {
|
val message = intent?.serializable<TestServiceMessage>("content") ?: return super.onStartCommand(intent, flags, startId)
|
||||||
|
when (message.key) {
|
||||||
MSG_MEASURE_CONFIG -> {
|
MSG_MEASURE_CONFIG -> {
|
||||||
val subscriptionId = intent.getStringExtra("content").orEmpty()
|
val guidsList = if (message.serverGuids.isNotEmpty()) {
|
||||||
val guidsList = if (subscriptionId.isEmpty()) {
|
message.serverGuids
|
||||||
MmkvManager.decodeAllServerList()
|
} else if (message.subscriptionId.isNotEmpty()) {
|
||||||
|
MmkvManager.decodeServerList(message.subscriptionId)
|
||||||
} else {
|
} else {
|
||||||
MmkvManager.decodeServerList(subscriptionId)
|
MmkvManager.decodeAllServerList()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (guidsList.isNotEmpty()) {
|
if (guidsList.isNotEmpty()) {
|
||||||
|
|||||||
@@ -437,14 +437,23 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
showLoading()
|
showLoading()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val count = mainViewModel.updateConfigViaSubAll()
|
val result = mainViewModel.updateConfigViaSubAll()
|
||||||
delay(500L)
|
delay(500L)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
if (count > 0) {
|
if (result.successCount + result.failureCount + result.skipCount == 0) {
|
||||||
toast(getString(R.string.title_update_config_count, count))
|
toast(R.string.title_update_subscription_no_subscription)
|
||||||
mainViewModel.reloadServerList()
|
} else if (result.successCount > 0 && result.failureCount + result.skipCount == 0) {
|
||||||
|
toast(getString(R.string.title_update_config_count, result.configCount))
|
||||||
} else {
|
} else {
|
||||||
toastError(R.string.toast_failure)
|
toast(
|
||||||
|
getString(
|
||||||
|
R.string.title_update_subscription_result,
|
||||||
|
result.configCount, result.successCount, result.failureCount, result.skipCount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (result.configCount > 0) {
|
||||||
|
mainViewModel.reloadServerList()
|
||||||
}
|
}
|
||||||
hideLoading()
|
hideLoading()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import com.v2ray.ang.contracts.BaseAdapterListener
|
|||||||
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
import com.v2ray.ang.databinding.ActivitySubSettingBinding
|
||||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||||
import com.v2ray.ang.extension.toast
|
import com.v2ray.ang.extension.toast
|
||||||
import com.v2ray.ang.extension.toastError
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
@@ -77,14 +75,20 @@ class SubSettingActivity : BaseActivity() {
|
|||||||
showLoading()
|
showLoading()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val count = AngConfigManager.updateConfigViaSubAll()
|
val result = AngConfigManager.updateConfigViaSubAll()
|
||||||
delay(500L)
|
delay(500L)
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
if (count > 0) {
|
if (result.successCount + result.failureCount + result.skipCount == 0) {
|
||||||
toastSuccess(R.string.toast_success)
|
toast(R.string.title_update_subscription_no_subscription)
|
||||||
refreshData()
|
} else if (result.successCount > 0 && result.failureCount + result.skipCount == 0) {
|
||||||
|
toast(getString(R.string.title_update_config_count, result.configCount))
|
||||||
} else {
|
} else {
|
||||||
toastError(R.string.toast_failure)
|
toast(
|
||||||
|
getString(
|
||||||
|
R.string.title_update_subscription_result,
|
||||||
|
result.configCount, result.successCount, result.failureCount, result.skipCount
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
hideLoading()
|
hideLoading()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.TestServiceMessage
|
||||||
import com.v2ray.ang.service.V2RayTestService
|
import com.v2ray.ang.service.V2RayTestService
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
@@ -37,15 +38,13 @@ object MessageUtil {
|
|||||||
* Sends a message to the test service.
|
* Sends a message to the test service.
|
||||||
*
|
*
|
||||||
* @param ctx The context.
|
* @param ctx The context.
|
||||||
* @param what The message identifier.
|
* @param message The test service message containing key, subscriptionId, and serverGuids.
|
||||||
* @param content The message content.
|
|
||||||
*/
|
*/
|
||||||
fun sendMsg2TestService(ctx: Context, what: Int, content: Serializable) {
|
fun sendMsg2TestService(ctx: Context, message: TestServiceMessage) {
|
||||||
try {
|
try {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.component = ComponentName(ctx, V2RayTestService::class.java)
|
intent.component = ComponentName(ctx, V2RayTestService::class.java)
|
||||||
intent.putExtra("key", what)
|
intent.putExtra("content", message)
|
||||||
intent.putExtra("content", content)
|
|
||||||
ctx.startService(intent)
|
ctx.startService(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(AppConfig.TAG, "Failed to send message to test service", e)
|
Log.e(AppConfig.TAG, "Failed to send message to test service", e)
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import com.v2ray.ang.R
|
|||||||
import com.v2ray.ang.dto.GroupMapItem
|
import com.v2ray.ang.dto.GroupMapItem
|
||||||
import com.v2ray.ang.dto.ServersCache
|
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.TestServiceMessage
|
||||||
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
|
||||||
@@ -37,8 +39,6 @@ import java.util.Collections
|
|||||||
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()
|
||||||
var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
|
var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
|
||||||
|
|
||||||
//var keywordFilter: String = MmkvManager.MmkvManager.decodeSettingsString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
|
|
||||||
var keywordFilter = ""
|
var keywordFilter = ""
|
||||||
val serversCache = mutableListOf<ServersCache>()
|
val serversCache = mutableListOf<ServersCache>()
|
||||||
val isRunning by lazy { MutableLiveData<Boolean>() }
|
val isRunning by lazy { MutableLiveData<Boolean>() }
|
||||||
@@ -95,38 +95,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Appends a custom configuration server.
|
|
||||||
// * @param server The server configuration to append.
|
|
||||||
// * @return True if the server was successfully appended, false otherwise.
|
|
||||||
// */
|
|
||||||
// fun appendCustomConfigServer(server: String): Boolean {
|
|
||||||
// if (server.contains("inbounds")
|
|
||||||
// && server.contains("outbounds")
|
|
||||||
// && server.contains("routing")
|
|
||||||
// ) {
|
|
||||||
// try {
|
|
||||||
// val config = CustomFmt.parse(server) ?: return false
|
|
||||||
// config.subscriptionId = subscriptionId
|
|
||||||
// val key = MmkvManager.encodeServerConfig("", config)
|
|
||||||
// MmkvManager.encodeServerRaw(key, server)
|
|
||||||
// serverList.add(0, key)
|
|
||||||
//// val profile = ProfileLiteItem(
|
|
||||||
//// configType = config.configType,
|
|
||||||
//// subscriptionId = config.subscriptionId,
|
|
||||||
//// remarks = config.remarks,
|
|
||||||
//// server = config.getProxyOutbound()?.getServerAddress(),
|
|
||||||
//// serverPort = config.getProxyOutbound()?.getServerPort(),
|
|
||||||
//// )
|
|
||||||
// serversCache.add(0, ServersCache(key, config))
|
|
||||||
// return true
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// e.printStackTrace()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Swaps the positions of two servers.
|
* Swaps the positions of two servers.
|
||||||
* @param fromPosition The initial position of the server.
|
* @param fromPosition The initial position of the server.
|
||||||
@@ -149,26 +117,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
fun updateCache() {
|
fun updateCache() {
|
||||||
serversCache.clear()
|
serversCache.clear()
|
||||||
|
val kw = keywordFilter.trim().lowercase()
|
||||||
for (guid in serverList) {
|
for (guid in serverList) {
|
||||||
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
|
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
|
||||||
// var profile = MmkvManager.decodeProfileConfig(guid)
|
if (kw.isEmpty()) {
|
||||||
// if (profile == null) {
|
serversCache.add(ServersCache(guid, profile))
|
||||||
// val config = MmkvManager.decodeServerConfig(guid) ?: continue
|
continue
|
||||||
// profile = ProfileLiteItem(
|
}
|
||||||
// configType = config.configType,
|
|
||||||
// subscriptionId = config.subscriptionId,
|
|
||||||
// remarks = config.remarks,
|
|
||||||
// server = config.getProxyOutbound()?.getServerAddress(),
|
|
||||||
// serverPort = config.getProxyOutbound()?.getServerPort(),
|
|
||||||
// )
|
|
||||||
// MmkvManager.encodeServerConfig(guid, config)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (subscriptionId.isNotEmpty() && subscriptionId != profile.subscriptionId) {
|
val remarks = profile.remarks.lowercase()
|
||||||
// continue
|
val description = profile.description.orEmpty().lowercase()
|
||||||
// }
|
val server = profile.server.orEmpty().lowercase()
|
||||||
|
|
||||||
if (keywordFilter.isEmpty() || profile.remarks.lowercase().contains(keywordFilter.lowercase())) {
|
if (remarks.contains(kw) || description.contains(kw) || server.contains(kw)) {
|
||||||
serversCache.add(ServersCache(guid, profile))
|
serversCache.add(ServersCache(guid, profile))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,13 +137,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the configuration via subscription for all servers.
|
* Updates the configuration via subscription for all servers.
|
||||||
* @return The number of updated configurations.
|
* @return Detailed result of the subscription update operation.
|
||||||
*/
|
*/
|
||||||
fun updateConfigViaSubAll(): Int {
|
fun updateConfigViaSubAll(): SubscriptionUpdateResult {
|
||||||
if (subscriptionId.isEmpty()) {
|
if (subscriptionId.isEmpty()) {
|
||||||
return AngConfigManager.updateConfigViaSubAll()
|
return AngConfigManager.updateConfigViaSubAll()
|
||||||
} else {
|
} else {
|
||||||
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return 0
|
val subItem = MmkvManager.decodeSubscription(subscriptionId) ?: return SubscriptionUpdateResult()
|
||||||
return AngConfigManager.updateConfigViaSub(SubscriptionCache(subscriptionId, subItem))
|
return AngConfigManager.updateConfigViaSub(SubscriptionCache(subscriptionId, subItem))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,7 +197,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
* Tests the real ping for all servers.
|
* Tests the real ping for all servers.
|
||||||
*/
|
*/
|
||||||
fun testAllRealPing() {
|
fun testAllRealPing() {
|
||||||
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "")
|
MessageUtil.sendMsg2TestService(
|
||||||
|
getApplication(),
|
||||||
|
TestServiceMessage(key = AppConfig.MSG_MEASURE_CONFIG_CANCEL)
|
||||||
|
)
|
||||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||||
updateListAction.value = -1
|
updateListAction.value = -1
|
||||||
|
|
||||||
@@ -244,7 +208,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
if (serversCache.isEmpty()) {
|
if (serversCache.isEmpty()) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, subscriptionId)
|
MessageUtil.sendMsg2TestService(
|
||||||
|
getApplication(),
|
||||||
|
TestServiceMessage(
|
||||||
|
key = AppConfig.MSG_MEASURE_CONFIG,
|
||||||
|
subscriptionId = subscriptionId,
|
||||||
|
serverGuids = if (keywordFilter.isNotEmpty()) serversCache.map { it.guid } else emptyList()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +401,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
keywordFilter = keyword
|
keywordFilter = keyword
|
||||||
MmkvManager.encodeSettings(AppConfig.CACHE_KEYWORD_FILTER, keywordFilter)
|
|
||||||
reloadServerList()
|
reloadServerList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -297,6 +297,8 @@
|
|||||||
<string name="title_import_config_count">Import %d configurations</string>
|
<string name="title_import_config_count">Import %d configurations</string>
|
||||||
<string name="title_export_config_count">Export %d configurations</string>
|
<string name="title_export_config_count">Export %d configurations</string>
|
||||||
<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_no_subscription">No subscriptions</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>
|
||||||
|
|
||||||
|
|||||||
@@ -296,6 +296,8 @@
|
|||||||
<string name="title_import_config_count">Import %d configurations</string>
|
<string name="title_import_config_count">Import %d configurations</string>
|
||||||
<string name="title_export_config_count">Export %d configurations</string>
|
<string name="title_export_config_count">Export %d configurations</string>
|
||||||
<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_no_subscription">No subscriptions</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>
|
||||||
|
|
||||||
|
|||||||
@@ -234,8 +234,8 @@
|
|||||||
<string name="title_pref_double_column_display">ره وندن نشووݩ داڌن دو سۊتۊنی</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">Enable show all groups</string>
|
<string name="title_pref_group_all_display">نشووݩ داڌن پوی بونکۊ یل ن فعال کۊنین</string>
|
||||||
<string name="summary_pref_group_all_display">Add an extra "All Tabs" page</string>
|
<string name="summary_pref_group_all_display">ی بلگه «پوی بلگه یل» ازاف کۊنین</string>
|
||||||
<!-- AboutActivity -->
|
<!-- AboutActivity -->
|
||||||
<string name="title_pref_feedback">فشناڌن منشڌ</string>
|
<string name="title_pref_feedback">فشناڌن منشڌ</string>
|
||||||
<string name="summary_pref_feedback">فشناڌن منشڌ یا گوزارش موشکلا من Github</string>
|
<string name="summary_pref_feedback">فشناڌن منشڌ یا گوزارش موشکلا من Github</string>
|
||||||
@@ -297,6 +297,8 @@
|
|||||||
<string name="title_import_config_count">و من ٱووردن %d کانفیگ</string>
|
<string name="title_import_config_count">و من ٱووردن %d کانفیگ</string>
|
||||||
<string name="title_export_config_count">و در کشیڌن %d کانفیگ</string>
|
<string name="title_export_config_count">و در کشیڌن %d کانفیگ</string>
|
||||||
<string name="title_update_config_count">ورۊ کردن %d کانفیگ</string>
|
<string name="title_update_config_count">ورۊ کردن %d کانفیگ</string>
|
||||||
|
<string name="title_update_subscription_result">%1$d کانفیگ ورۊ وابی (مووفق: %2$d، شکست خرده: %3$d، رڌ وابیڌه: %4$d)</string>
|
||||||
|
<string name="title_update_subscription_no_subscription">بؽ اشتراک</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>
|
||||||
|
|
||||||
@@ -417,7 +419,7 @@
|
|||||||
<string-array name="policy_group_type">
|
<string-array name="policy_group_type">
|
||||||
<item>کم ترین پینگ</item>
|
<item>کم ترین پینگ</item>
|
||||||
<item>کم ترین بار(لود)</item>
|
<item>کم ترین بار(لود)</item>
|
||||||
<item>تصادفی</item>
|
<item>تساڌۊفی</item>
|
||||||
<item>گردشی</item>
|
<item>گردشی</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
|||||||
@@ -294,6 +294,8 @@
|
|||||||
<string name="title_import_config_count">وارد کردن %d کانفیگ</string>
|
<string name="title_import_config_count">وارد کردن %d کانفیگ</string>
|
||||||
<string name="title_export_config_count">صادر کردن %d کانفیگ</string>
|
<string name="title_export_config_count">صادر کردن %d کانفیگ</string>
|
||||||
<string name="title_update_config_count">آپدیت کردن %d کانفیگ</string>
|
<string name="title_update_config_count">آپدیت کردن %d کانفیگ</string>
|
||||||
|
<string name="title_update_subscription_result">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="tasker_start_service">شروع خدمات</string>
|
<string name="tasker_start_service">شروع خدمات</string>
|
||||||
<string name="tasker_setting_confirm">تایید</string>
|
<string name="tasker_setting_confirm">تایید</string>
|
||||||
|
|
||||||
|
|||||||
@@ -233,8 +233,8 @@
|
|||||||
<string name="title_pref_double_column_display">Профили в два столбца</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">Enable show all groups</string>
|
<string name="title_pref_group_all_display">Общая вкладка групп</string>
|
||||||
<string name="summary_pref_group_all_display">Add an extra "All Tabs" page</string>
|
<string name="summary_pref_group_all_display">Показывать дополнительную вкладку со всеми профилями групп</string>
|
||||||
<!-- AboutActivity -->
|
<!-- AboutActivity -->
|
||||||
<string name="title_pref_feedback">Обратная связь</string>
|
<string name="title_pref_feedback">Обратная связь</string>
|
||||||
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string>
|
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string>
|
||||||
@@ -295,6 +295,8 @@
|
|||||||
<string name="title_import_config_count">Импортировано профилей: %d</string>
|
<string name="title_import_config_count">Импортировано профилей: %d</string>
|
||||||
<string name="title_export_config_count">Экспортировано профилей: %d</string>
|
<string name="title_export_config_count">Экспортировано профилей: %d</string>
|
||||||
<string name="title_update_config_count">Обновлено профилей: %d</string>
|
<string name="title_update_config_count">Обновлено профилей: %d</string>
|
||||||
|
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
|
||||||
|
<string name="title_update_subscription_no_subscription">Нет подписок</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>
|
||||||
@@ -358,7 +360,7 @@
|
|||||||
<string name="title_configuration_restore">Восстановление конфигурации</string>
|
<string name="title_configuration_restore">Восстановление конфигурации</string>
|
||||||
<string name="title_configuration_share">Поделиться конфигурацией</string>
|
<string name="title_configuration_share">Поделиться конфигурацией</string>
|
||||||
<string name="title_webdav_config_setting">Настройки WebDAV</string>
|
<string name="title_webdav_config_setting">Настройки WebDAV</string>
|
||||||
<string name="title_webdav_config_setting_unknown">Необходимо настроить WebDAV.</string>
|
<string name="title_webdav_config_setting_unknown">Необходимо настроить WebDAV</string>
|
||||||
<string name="title_webdav_url">URL сервера</string>
|
<string name="title_webdav_url">URL сервера</string>
|
||||||
<string name="title_webdav_user">Пользователь</string>
|
<string name="title_webdav_user">Пользователь</string>
|
||||||
<string name="title_webdav_pass">Пароль</string>
|
<string name="title_webdav_pass">Пароль</string>
|
||||||
|
|||||||
@@ -297,6 +297,8 @@
|
|||||||
<string name="title_import_config_count">Import %d configurations</string>
|
<string name="title_import_config_count">Import %d configurations</string>
|
||||||
<string name="title_export_config_count">Export %d configurations</string>
|
<string name="title_export_config_count">Export %d configurations</string>
|
||||||
<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_no_subscription">No subscriptions</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>
|
||||||
|
|
||||||
|
|||||||
@@ -295,6 +295,8 @@
|
|||||||
<string name="title_import_config_count">导入 %d 个配置</string>
|
<string name="title_import_config_count">导入 %d 个配置</string>
|
||||||
<string name="title_export_config_count">导出 %d 个配置</string>
|
<string name="title_export_config_count">导出 %d 个配置</string>
|
||||||
<string name="title_update_config_count">更新 %d 个配置</string>
|
<string name="title_update_config_count">更新 %d 个配置</string>
|
||||||
|
<string name="title_update_subscription_result">更新了 %1$d 个配置(%2$d 个成功,%3$d 个失败,%4$d 个跳过)</string>
|
||||||
|
<string name="title_update_subscription_no_subscription">无订阅</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>
|
||||||
|
|
||||||
|
|||||||
@@ -295,6 +295,8 @@
|
|||||||
<string name="title_import_config_count">匯入 %d 個配置</string>
|
<string name="title_import_config_count">匯入 %d 個配置</string>
|
||||||
<string name="title_export_config_count">匯出 %d 個配置</string>
|
<string name="title_export_config_count">匯出 %d 個配置</string>
|
||||||
<string name="title_update_config_count">更新 %d 個配置</string>
|
<string name="title_update_config_count">更新 %d 個配置</string>
|
||||||
|
<string name="title_update_subscription_result">更新了 %1$d 個配置(%2$d 個成功,%3$d 個失敗,%4$d 個跳過)</string>
|
||||||
|
<string name="title_update_subscription_no_subscription">無訂閱</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>
|
||||||
|
|
||||||
|
|||||||
@@ -301,6 +301,8 @@
|
|||||||
<string name="title_import_config_count">Import %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_export_config_count">Export %d configs</string>
|
||||||
<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_no_subscription">No subscriptions</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