Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 = 717
|
||||||
versionName = "2.0.14"
|
versionName = "2.0.17"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,10 @@ 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.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 +148,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 +218,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 +277,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -94,6 +94,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
})
|
})
|
||||||
|
|
||||||
binding.fab.setOnClickListener { handleFabAction() }
|
binding.fab.setOnClickListener { handleFabAction() }
|
||||||
|
binding.fabLocate.setOnClickListener { locateSelectedServer() }
|
||||||
binding.layoutTest.setOnClickListener { handleLayoutTestClick() }
|
binding.layoutTest.setOnClickListener { handleLayoutTestClick() }
|
||||||
|
|
||||||
setupGroupTab()
|
setupGroupTab()
|
||||||
@@ -433,7 +434,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 +570,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)
|
||||||
|
|||||||
@@ -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,8 +94,22 @@
|
|||||||
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
|
||||||
|
android:id="@+id/fab_locate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginBottom="@dimen/view_height_dp120"
|
||||||
|
android:clickable="true"
|
||||||
|
android:contentDescription="@string/title_server"
|
||||||
|
android:focusable="true"
|
||||||
|
android:src="@android:drawable/ic_menu_mylocation"
|
||||||
|
app:useCompatPadding="true"
|
||||||
|
app:layout_anchorGravity="bottom|right|end" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/fab"
|
android:id="@+id/fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -108,6 +122,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -299,6 +299,8 @@
|
|||||||
<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="tasker_start_service">بدء الخدمة</string>
|
<string name="tasker_start_service">بدء الخدمة</string>
|
||||||
<string name="tasker_setting_confirm">تأكيد</string>
|
<string name="tasker_setting_confirm">تأكيد</string>
|
||||||
|
|
||||||
|
|||||||
@@ -298,6 +298,8 @@
|
|||||||
<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="tasker_start_service">সার্ভিস শুরু করুন</string>
|
<string name="tasker_start_service">সার্ভিস শুরু করুন</string>
|
||||||
<string name="tasker_setting_confirm">নিশ্চিত করুন</string>
|
<string name="tasker_setting_confirm">নিশ্চিত করুন</string>
|
||||||
|
|
||||||
|
|||||||
@@ -299,6 +299,8 @@
|
|||||||
<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="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_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="tasker_start_service">شروع خدمات</string>
|
<string name="tasker_start_service">شروع خدمات</string>
|
||||||
<string name="tasker_setting_confirm">تایید</string>
|
<string name="tasker_setting_confirm">تایید</string>
|
||||||
|
|
||||||
|
|||||||
@@ -297,6 +297,8 @@
|
|||||||
<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="tasker_start_service">Запуск службы</string>
|
<string name="tasker_start_service">Запуск службы</string>
|
||||||
<string name="tasker_setting_confirm">Подтвердить</string>
|
<string name="tasker_setting_confirm">Подтвердить</string>
|
||||||
|
|||||||
@@ -299,6 +299,8 @@
|
|||||||
<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="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>
|
||||||
|
|
||||||
|
|||||||
@@ -297,6 +297,8 @@
|
|||||||
<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="tasker_start_service">启动服务</string>
|
<string name="tasker_start_service">启动服务</string>
|
||||||
<string name="tasker_setting_confirm">确定</string>
|
<string name="tasker_setting_confirm">确定</string>
|
||||||
|
|
||||||
|
|||||||
@@ -297,6 +297,8 @@
|
|||||||
<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="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>
|
||||||
|
|||||||
@@ -303,6 +303,8 @@
|
|||||||
<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="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