mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60ce213f67 | |||
| d0161826e2 | |||
| 641fcb943d | |||
| 1fbb4f2bd3 | |||
| d3a5a1af9c | |||
| 33971a576a | |||
| a04c53a045 | |||
| 278095015b |
@@ -159,10 +159,10 @@ func (x *CoreController) QueryStats(tag string, direct string) int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MeasureDelay measures network latency to a specified URL through the current core instance
|
// MeasureDelay measures network latency to a specified URL through the current core instance
|
||||||
// Uses a 12-second timeout context and returns the round-trip time in milliseconds
|
// Uses a 6-second timeout context and returns the round-trip time in milliseconds
|
||||||
// An error is returned if the connection fails or returns an unexpected status
|
// An error is returned if the connection fails or returns an unexpected status
|
||||||
func (x *CoreController) MeasureDelay(url string) (int64, error) {
|
func (x *CoreController) MeasureDelay(url string) (int64, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
return measureInstDelay(ctx, x.coreInstance, url)
|
return measureInstDelay(ctx, x.coreInstance, url)
|
||||||
@@ -307,11 +307,11 @@ func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int
|
|||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: tr,
|
Transport: tr,
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 6 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
if url == "" {
|
if url == "" {
|
||||||
url = "https://www.google.com/generate_204"
|
url = "https://api.ipify.org"
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ object AppConfig {
|
|||||||
const val PREF_HEV_TUNNEL_LOGLEVEL = "pref_hev_tunnel_loglevel"
|
const val PREF_HEV_TUNNEL_LOGLEVEL = "pref_hev_tunnel_loglevel"
|
||||||
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2"
|
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2"
|
||||||
const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
|
const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
|
||||||
|
const val PREF_SHOW_COPY_BUTTON = "pref_show_copy_button"
|
||||||
|
|
||||||
/** Cache keys. */
|
/** Cache keys. */
|
||||||
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
||||||
@@ -119,7 +120,7 @@ object AppConfig {
|
|||||||
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
|
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
|
||||||
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
||||||
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
||||||
const val DELAY_TEST_URL = "https://icanhazip.com"
|
const val DELAY_TEST_URL = "https://api.ipify.org"
|
||||||
const val DELAY_TEST_URL2 = "https://api64.ipify.org"
|
const val DELAY_TEST_URL2 = "https://api64.ipify.org"
|
||||||
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
||||||
const val IP_API_URL = "https://api.ip.sb/geoip"
|
const val IP_API_URL = "https://api.ip.sb/geoip"
|
||||||
@@ -164,6 +165,7 @@ object AppConfig {
|
|||||||
const val MSG_MEASURE_CONFIG_CANCEL = 72
|
const val MSG_MEASURE_CONFIG_CANCEL = 72
|
||||||
const val MSG_MEASURE_CONFIG_NOTIFY = 73
|
const val MSG_MEASURE_CONFIG_NOTIFY = 73
|
||||||
const val MSG_MEASURE_CONFIG_FINISH = 74
|
const val MSG_MEASURE_CONFIG_FINISH = 74
|
||||||
|
const val MSG_MEASURE_CONFIG_BATCH = 75
|
||||||
|
|
||||||
/** Notification channel IDs and names. */
|
/** Notification channel IDs and names. */
|
||||||
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
|
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ interface MainAdapterListener :BaseAdapterListener {
|
|||||||
|
|
||||||
fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean)
|
fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean)
|
||||||
|
|
||||||
|
fun onCopyToClipboard(guid: String)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package xyz.zarazaex.olc.dto
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class PingResultItem(
|
||||||
|
val guid: String,
|
||||||
|
val delay: Long
|
||||||
|
) : Serializable
|
||||||
|
|
||||||
|
data class PingProgressUpdate(
|
||||||
|
val results: ArrayList<PingResultItem>,
|
||||||
|
val finished: Int,
|
||||||
|
val total: Int
|
||||||
|
) : Serializable
|
||||||
@@ -128,4 +128,39 @@ data class ProfileItem(
|
|||||||
&& this.pinnedCA256 == obj.pinnedCA256
|
&& this.pinnedCA256 == obj.pinnedCA256
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = server?.hashCode() ?: 0
|
||||||
|
result = 31 * result + (serverPort?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (password?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (method?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (flow?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (username?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (network?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (headerType?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (host?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (path?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (seed?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (quicSecurity?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (quicKey?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (mode?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (serviceName?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (authority?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (xhttpMode?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (security?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (sni?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (alpn?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (fingerPrint?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (publicKey?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (shortId?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (secretKey?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (localAddress?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (reserved?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (mtu ?: 0)
|
||||||
|
result = 31 * result + (obfsPassword?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (portHopping?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (portHoppingInterval?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (pinnedCA256?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -241,8 +241,8 @@ object AngConfigManager {
|
|||||||
|
|
||||||
val subItem = MmkvManager.decodeSubscription(subid)
|
val subItem = MmkvManager.decodeSubscription(subid)
|
||||||
|
|
||||||
val oldPingData = if (!append) {
|
val oldServerData = if (!append) {
|
||||||
saveOldPingData(subid)
|
saveOldServerData(subid)
|
||||||
} else {
|
} else {
|
||||||
emptyMap()
|
emptyMap()
|
||||||
}
|
}
|
||||||
@@ -263,7 +263,7 @@ object AngConfigManager {
|
|||||||
MmkvManager.removeServerViaSubid(subid)
|
MmkvManager.removeServerViaSubid(subid)
|
||||||
}
|
}
|
||||||
val keyToProfile = batchSaveConfigs(configs, subid, append)
|
val keyToProfile = batchSaveConfigs(configs, subid, append)
|
||||||
restoreOldPingData(keyToProfile, oldPingData)
|
restoreOldServerData(keyToProfile, oldServerData)
|
||||||
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
val matchKey = findMatchedProfileKey(keyToProfile, removedSelected)
|
||||||
matchKey?.let { MmkvManager.setSelectServer(it) }
|
matchKey?.let { MmkvManager.setSelectServer(it) }
|
||||||
}
|
}
|
||||||
@@ -330,31 +330,39 @@ object AngConfigManager {
|
|||||||
return keyToProfile
|
return keyToProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveOldPingData(subid: String): Map<ProfileItem, Long> {
|
private fun saveOldServerData(subid: String): Map<ProfileItem, Pair<Long, Boolean>> {
|
||||||
val pingData = mutableMapOf<ProfileItem, Long>()
|
val serverData = mutableMapOf<ProfileItem, Pair<Long, Boolean>>()
|
||||||
val serverList = MmkvManager.decodeServerList(subid)
|
val serverList = MmkvManager.decodeServerList(subid)
|
||||||
|
|
||||||
serverList.forEach { guid ->
|
serverList.forEach { guid ->
|
||||||
val profile = MmkvManager.decodeServerConfig(guid)
|
val profile = MmkvManager.decodeServerConfig(guid)
|
||||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
if (profile != null) {
|
||||||
if (profile != null && aff != null && aff.testDelayMillis > 0) {
|
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||||
pingData[profile] = aff.testDelayMillis
|
val delay = aff?.testDelayMillis ?: 0L
|
||||||
|
if (delay > 0 || profile.isFavorite) {
|
||||||
|
serverData[profile] = Pair(delay, profile.isFavorite)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pingData
|
return serverData
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreOldPingData(keyToProfile: Map<String, ProfileItem>, oldPingData: Map<ProfileItem, Long>) {
|
private fun restoreOldServerData(keyToProfile: Map<String, ProfileItem>, oldServerData: Map<ProfileItem, Pair<Long, Boolean>>) {
|
||||||
if (oldPingData.isEmpty()) return
|
if (oldServerData.isEmpty()) return
|
||||||
|
|
||||||
keyToProfile.forEach { (key, newProfile) ->
|
keyToProfile.forEach { (key, newProfile) ->
|
||||||
val oldPing = oldPingData.entries.firstOrNull { (oldProfile, _) ->
|
val oldData = oldServerData[newProfile]
|
||||||
oldProfile == newProfile
|
|
||||||
}?.value
|
|
||||||
|
|
||||||
if (oldPing != null && oldPing > 0) {
|
if (oldData != null) {
|
||||||
MmkvManager.encodeServerTestDelayMillis(key, oldPing)
|
val (oldPing, isFavorite) = oldData
|
||||||
|
if (oldPing > 0) {
|
||||||
|
MmkvManager.encodeServerTestDelayMillis(key, oldPing)
|
||||||
|
}
|
||||||
|
if (isFavorite) {
|
||||||
|
newProfile.isFavorite = true
|
||||||
|
MmkvManager.encodeServerConfig(key, newProfile)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ object SpeedtestManager {
|
|||||||
var result: String
|
var result: String
|
||||||
var elapsed = -1L
|
var elapsed = -1L
|
||||||
|
|
||||||
val testUrl = "https://icanhazip.com"
|
val testUrl = "https://api.ipify.org"
|
||||||
val conn = HttpUtil.createProxyConnection(testUrl, port, 15000, 15000) ?: return Pair(elapsed, "")
|
val conn = HttpUtil.createProxyConnection(testUrl, port, 15000, 15000) ?: return Pair(elapsed, "")
|
||||||
try {
|
try {
|
||||||
val start = SystemClock.elapsedRealtime()
|
val start = SystemClock.elapsedRealtime()
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ import kotlinx.coroutines.CoroutineName
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import xyz.zarazaex.olc.AppConfig
|
import xyz.zarazaex.olc.AppConfig
|
||||||
|
import xyz.zarazaex.olc.dto.PingProgressUpdate
|
||||||
|
import xyz.zarazaex.olc.dto.PingResultItem
|
||||||
import xyz.zarazaex.olc.handler.SettingsManager
|
import xyz.zarazaex.olc.handler.SettingsManager
|
||||||
import xyz.zarazaex.olc.handler.V2RayNativeManager
|
import xyz.zarazaex.olc.handler.V2RayNativeManager
|
||||||
import xyz.zarazaex.olc.handler.V2rayConfigManager
|
import xyz.zarazaex.olc.handler.V2rayConfigManager
|
||||||
@@ -29,27 +35,42 @@ class RealPingWorkerService(
|
|||||||
|
|
||||||
private val totalCount = AtomicInteger(guids.size)
|
private val totalCount = AtomicInteger(guids.size)
|
||||||
private val finishedCount = AtomicInteger(0)
|
private val finishedCount = AtomicInteger(0)
|
||||||
|
private val pendingResults = ArrayList<PingResultItem>()
|
||||||
|
private val pendingLock = Any()
|
||||||
|
|
||||||
private val delayTestUrl = SettingsManager.getDelayTestUrl()
|
private val delayTestUrl = SettingsManager.getDelayTestUrl()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val RESULT_BATCH_SIZE = 32
|
||||||
|
private const val FLUSH_INTERVAL_MS = 1000L
|
||||||
|
}
|
||||||
|
|
||||||
data class PingItem(val guid: String, val config: String)
|
data class PingItem(val guid: String, val config: String)
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
while (isActive) {
|
||||||
|
delay(FLUSH_INTERVAL_MS)
|
||||||
|
flushPendingResults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
// Prepare configurations for batch test and shuffle for better async feel
|
// Prepare configurations in parallel for faster startup
|
||||||
val items =
|
val shuffledGuids = guids.shuffled()
|
||||||
guids.shuffled().mapNotNull { guid ->
|
val deferredItems = shuffledGuids.map { guid ->
|
||||||
val configResult =
|
async(Dispatchers.IO) {
|
||||||
V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||||
if (configResult.status) {
|
if (configResult.status) {
|
||||||
PingItem(guid, configResult.content)
|
PingItem(guid, configResult.content)
|
||||||
} else {
|
} else {
|
||||||
// Notify failure immediately for invalid configs
|
reportResult(guid, -1L)
|
||||||
reportResult(guid, -1L)
|
null
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val items = deferredItems.awaitAll().filterNotNull()
|
||||||
|
|
||||||
if (items.isNotEmpty()) {
|
if (items.isNotEmpty()) {
|
||||||
val configsJson = JsonUtil.toJson(items)
|
val configsJson = JsonUtil.toJson(items)
|
||||||
@@ -67,8 +88,10 @@ class RealPingWorkerService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flushPendingResults()
|
||||||
onFinish("0")
|
onFinish("0")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
flushPendingResults()
|
||||||
onFinish("-1")
|
onFinish("-1")
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
@@ -78,14 +101,42 @@ class RealPingWorkerService(
|
|||||||
|
|
||||||
private fun reportResult(guid: String, delay: Long) {
|
private fun reportResult(guid: String, delay: Long) {
|
||||||
val finished = finishedCount.incrementAndGet()
|
val finished = finishedCount.incrementAndGet()
|
||||||
val total = guids.size
|
var readyBatch: PingProgressUpdate? = null
|
||||||
|
synchronized(pendingLock) {
|
||||||
|
pendingResults.add(PingResultItem(guid, delay))
|
||||||
|
if (pendingResults.size >= RESULT_BATCH_SIZE || finished >= totalCount.get()) {
|
||||||
|
readyBatch = createProgressUpdateLocked(finished)
|
||||||
|
pendingResults.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readyBatch?.let(::sendBatchUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
// Notify UI about the individual result
|
private fun flushPendingResults() {
|
||||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, delay))
|
val finished = finishedCount.get()
|
||||||
|
val update =
|
||||||
|
synchronized(pendingLock) {
|
||||||
|
if (pendingResults.isEmpty()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
createProgressUpdateLocked(finished).also { pendingResults.clear() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update?.let(::sendBatchUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
// Notify UI about progress
|
private fun createProgressUpdateLocked(finished: Int): PingProgressUpdate {
|
||||||
val left = total - finished
|
return PingProgressUpdate(
|
||||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / $total")
|
results = ArrayList(pendingResults),
|
||||||
|
finished = finished,
|
||||||
|
total = totalCount.get()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendBatchUpdate(update: PingProgressUpdate) {
|
||||||
|
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_BATCH, update)
|
||||||
|
val left = (update.total - update.finished).coerceAtLeast(0)
|
||||||
|
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "$left / ${update.total}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel() {
|
fun cancel() {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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 androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import xyz.zarazaex.olc.AppConfig
|
import xyz.zarazaex.olc.AppConfig
|
||||||
import xyz.zarazaex.olc.R
|
import xyz.zarazaex.olc.R
|
||||||
import xyz.zarazaex.olc.contracts.MainAdapterListener
|
import xyz.zarazaex.olc.contracts.MainAdapterListener
|
||||||
@@ -137,9 +138,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
|||||||
* @param guid The server unique identifier
|
* @param guid The server unique identifier
|
||||||
*/
|
*/
|
||||||
private fun share2Clipboard(guid: String) {
|
private fun share2Clipboard(guid: String) {
|
||||||
if (AngConfigManager.share2Clipboard(ownerActivity, guid) == 0) {
|
if (AngConfigManager.share2Clipboard(ownerActivity, guid) != 0) {
|
||||||
ownerActivity.toastSuccess(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
ownerActivity.toastError(R.string.toast_failure)
|
ownerActivity.toastError(R.string.toast_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,6 +263,10 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
|||||||
setSelectServer(guid)
|
setSelectServer(guid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCopyToClipboard(guid: String) {
|
||||||
|
share2Clipboard(guid)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean) {
|
override fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean) {
|
||||||
val isCustom = profile.configType == EConfigType.CUSTOM || profile.configType == EConfigType.POLICYGROUP
|
val isCustom = profile.configType == EConfigType.CUSTOM || profile.configType == EConfigType.POLICYGROUP
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
@@ -84,6 +86,12 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
toggle.syncState()
|
toggle.syncState()
|
||||||
binding.navView.setNavigationItemSelectedListener(this)
|
binding.navView.setNavigationItemSelectedListener(this)
|
||||||
|
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(binding.drawerContentLayout) { v, insets ->
|
||||||
|
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
v.setPadding(0, systemBars.top, 0, systemBars.bottom)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
findViewById<android.widget.TextView>(R.id.drawer_settings)?.setOnClickListener {
|
findViewById<android.widget.TextView>(R.id.drawer_settings)?.setOnClickListener {
|
||||||
requestActivityLauncher.launch(Intent(this, SettingsActivity::class.java))
|
requestActivityLauncher.launch(Intent(this, SettingsActivity::class.java))
|
||||||
binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START)
|
binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START)
|
||||||
@@ -258,8 +266,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
|
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val result = mainViewModel.updateConfigViaSubAll()
|
val result = mainViewModel.updateConfigViaSubAll()
|
||||||
delay(500L)
|
withContext(Dispatchers.Main) {
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (result.configCount > 0) {
|
if (result.configCount > 0) {
|
||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
showStatus("Обновлено ${result.configCount} профилей. Запуск теста...")
|
showStatus("Обновлено ${result.configCount} профилей. Запуск теста...")
|
||||||
@@ -268,7 +275,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
|||||||
}
|
}
|
||||||
hideLoading()
|
hideLoading()
|
||||||
|
|
||||||
delay(500L)
|
|
||||||
showStatus("Выполняется замер задержки. Ожидаем завершения...")
|
showStatus("Выполняется замер задержки. Ожидаем завершения...")
|
||||||
mainViewModel.testAllRealPing()
|
mainViewModel.testAllRealPing()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class MainRecyclerAdapter(
|
|||||||
|
|
||||||
private val doubleColumnDisplay =
|
private val doubleColumnDisplay =
|
||||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
|
MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
|
||||||
|
private val showCopyButton =
|
||||||
|
MmkvManager.decodeSettingsBool(AppConfig.PREF_SHOW_COPY_BUTTON, false)
|
||||||
private var data: MutableList<ServersCache> = mutableListOf()
|
private var data: MutableList<ServersCache> = mutableListOf()
|
||||||
private var recyclerView: RecyclerView? = null
|
private var recyclerView: RecyclerView? = null
|
||||||
|
|
||||||
@@ -200,6 +202,11 @@ class MainRecyclerAdapter(
|
|||||||
mainViewModel.reloadServerList()
|
mainViewModel.reloadServerList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.itemMainBinding.ivCopy.visibility = if (showCopyButton) View.VISIBLE else View.GONE
|
||||||
|
holder.itemMainBinding.ivCopy.setOnClickListener {
|
||||||
|
adapterListener?.onCopyToClipboard(guid)
|
||||||
|
}
|
||||||
|
|
||||||
holder.itemMainBinding.infoContainer.setOnClickListener {
|
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||||
adapterListener?.onSelectServer(guid)
|
adapterListener?.onSelectServer(guid)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import xyz.zarazaex.olc.AngApplication
|
|||||||
import xyz.zarazaex.olc.AppConfig
|
import xyz.zarazaex.olc.AppConfig
|
||||||
import xyz.zarazaex.olc.R
|
import xyz.zarazaex.olc.R
|
||||||
import xyz.zarazaex.olc.dto.GroupMapItem
|
import xyz.zarazaex.olc.dto.GroupMapItem
|
||||||
|
import xyz.zarazaex.olc.dto.PingProgressUpdate
|
||||||
import xyz.zarazaex.olc.dto.ServersCache
|
import xyz.zarazaex.olc.dto.ServersCache
|
||||||
import xyz.zarazaex.olc.dto.SubscriptionCache
|
import xyz.zarazaex.olc.dto.SubscriptionCache
|
||||||
import xyz.zarazaex.olc.dto.SubscriptionUpdateResult
|
import xyz.zarazaex.olc.dto.SubscriptionUpdateResult
|
||||||
@@ -628,6 +629,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
updateListAction.value = getPosition(resultPair.first)
|
updateListAction.value = getPosition(resultPair.first)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppConfig.MSG_MEASURE_CONFIG_BATCH -> {
|
||||||
|
val update = intent.serializable<PingProgressUpdate>("content") ?: return
|
||||||
|
update.results.forEach { result ->
|
||||||
|
MmkvManager.encodeServerTestDelayMillis(result.guid, result.delay)
|
||||||
|
}
|
||||||
|
updateListAction.value = -1
|
||||||
|
}
|
||||||
|
|
||||||
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {
|
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {
|
||||||
val content = intent.getStringExtra("content")
|
val content = intent.getStringExtra("content")
|
||||||
updateTestResultAction.value =
|
updateTestResultAction.value =
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF000000"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" />
|
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/drawer_layout"
|
android:id="@+id/drawer_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -150,6 +151,7 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/drawer_content_layout"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="start"
|
android:layout_gravity="start"
|
||||||
|
|||||||
@@ -99,16 +99,35 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageView
|
<LinearLayout
|
||||||
android:id="@+id/iv_favorite"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_vertical"
|
android:orientation="vertical"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:gravity="end">
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
<ImageView
|
||||||
android:padding="@dimen/padding_spacing_dp8"
|
android:id="@+id/iv_favorite"
|
||||||
android:src="@drawable/ic_star_empty" />
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:padding="@dimen/padding_spacing_dp8"
|
||||||
|
android:src="@drawable/ic_star_empty" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_copy"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:padding="@dimen/padding_spacing_dp8"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:srcCompat="@drawable/ic_copy"
|
||||||
|
app:tint="?attr/colorAccent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -236,6 +236,9 @@
|
|||||||
|
|
||||||
<string name="title_pref_group_all_display">Общая вкладка групп</string>
|
<string name="title_pref_group_all_display">Общая вкладка групп</string>
|
||||||
<string name="summary_pref_group_all_display">Показывать дополнительную вкладку со всеми профилями групп</string>
|
<string name="summary_pref_group_all_display">Показывать дополнительную вкладку со всеми профилями групп</string>
|
||||||
|
|
||||||
|
<string name="title_pref_show_copy_button">Кнопка копирования</string>
|
||||||
|
<string name="summary_pref_show_copy_button">Показывать кнопку копирования конфигурации сервера в буфер обмена</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>
|
||||||
|
|||||||
@@ -160,6 +160,9 @@
|
|||||||
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string>
|
||||||
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||||
|
|
||||||
|
<string name="title_pref_show_copy_button">Show copy button</string>
|
||||||
|
<string name="summary_pref_show_copy_button">Show button to copy server configuration to clipboard</string>
|
||||||
|
|
||||||
<string name="title_mux_settings">Mux Settings</string>
|
<string name="title_mux_settings">Mux Settings</string>
|
||||||
<string name="title_pref_mux_enabled">Enable Mux</string>
|
<string name="title_pref_mux_enabled">Enable Mux</string>
|
||||||
<string name="summary_pref_mux_enabled">Faster, but it may cause unstable connectivity\nCustomize how to handle TCP, UDP and QUIC below</string>
|
<string name="summary_pref_mux_enabled">Faster, but it may cause unstable connectivity\nCustomize how to handle TCP, UDP and QUIC below</string>
|
||||||
|
|||||||
@@ -28,6 +28,12 @@
|
|||||||
android:summary="@string/summary_pref_group_all_display"
|
android:summary="@string/summary_pref_group_all_display"
|
||||||
android:title="@string/title_pref_group_all_display" />
|
android:title="@string/title_pref_group_all_display" />
|
||||||
|
|
||||||
|
<CheckBoxPreference
|
||||||
|
android:key="pref_show_copy_button"
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:summary="@string/summary_pref_show_copy_button"
|
||||||
|
android:title="@string/title_pref_show_copy_button" />
|
||||||
|
|
||||||
<ListPreference
|
<ListPreference
|
||||||
android:defaultValue="auto"
|
android:defaultValue="auto"
|
||||||
android:entries="@array/language_select"
|
android:entries="@array/language_select"
|
||||||
|
|||||||
Reference in New Issue
Block a user