diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/dto/SubscriptionUpdateStatus.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/dto/SubscriptionUpdateStatus.kt new file mode 100644 index 00000000..acae5279 --- /dev/null +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/dto/SubscriptionUpdateStatus.kt @@ -0,0 +1,15 @@ +package xyz.zarazaex.olc.dto + +enum class SubscriptionUpdateStatus { + IDLE, + LOADING, + SUCCESS, + FAILED, + SKIPPED +} + +data class SubscriptionStatus( + val guid: String, + val status: SubscriptionUpdateStatus = SubscriptionUpdateStatus.IDLE, + val configCount: Int = 0 +) diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/handler/AngConfigManager.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/handler/AngConfigManager.kt index 5fb3a795..18925d02 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/handler/AngConfigManager.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/handler/AngConfigManager.kt @@ -576,16 +576,18 @@ object AngConfigManager { Log.i(AppConfig.TAG, url) val userAgent = it.subscription.userAgent + val timeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 15000 + var configText = try { val httpPort = SettingsManager.getHttpPort() - HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort) + HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout, httpPort) } catch (e: Exception) { Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e) "" } if (configText.isEmpty()) { configText = try { - HttpUtil.getUrlContentWithUserAgent(url, userAgent) + HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout) } catch (e: Exception) { Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e) "" diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/SubSettingActivity.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/SubSettingActivity.kt index e5521860..6dfdc9c4 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/SubSettingActivity.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/SubSettingActivity.kt @@ -56,6 +56,10 @@ class SubSettingActivity : BaseActivity() { viewModel.isUpdating.observe(this) { isUpdating -> adapter.setUpdating(isUpdating) } + + viewModel.subscriptionStatuses.observe(this) { statuses -> + adapter.notifyDataSetChanged() + } } override fun onResume() { @@ -86,27 +90,77 @@ class SubSettingActivity : BaseActivity() { showLoading() viewModel.isUpdating.value = true + viewModel.subscriptionStatuses.value = emptyMap() - lifecycleScope.launch(Dispatchers.IO) { - val result = AngConfigManager.updateConfigViaSubAll() - delay(500L) - launch(Dispatchers.Main) { - viewModel.isUpdating.value = false - if (result.successCount + result.failureCount + result.skipCount == 0) { - toast(R.string.title_update_subscription_no_subscription) - } else if (result.successCount > 0 && result.failureCount + result.skipCount == 0) { - toast(getString(R.string.title_update_config_count, result.configCount)) - } else { - toast( - getString( - R.string.title_update_subscription_result, - result.configCount, result.successCount, result.failureCount, result.skipCount - ) - ) + lifecycleScope.launch(Dispatchers.Main) { + val subscriptions = viewModel.getAll() + var totalConfigCount = 0 + var successCount = 0 + var failureCount = 0 + var skipCount = 0 + + val jobs = subscriptions.map { subscription -> + launch(Dispatchers.IO) { + val subId = subscription.guid + + launch(Dispatchers.Main) { + viewModel.updateSubscriptionStatus(subId, xyz.zarazaex.olc.dto.SubscriptionUpdateStatus.LOADING) + } + + val result = AngConfigManager.updateConfigViaSub(subscription) + + launch(Dispatchers.Main) { + when { + result.successCount > 0 -> { + viewModel.updateSubscriptionStatus( + subId, + xyz.zarazaex.olc.dto.SubscriptionUpdateStatus.SUCCESS, + result.configCount + ) + } + result.skipCount > 0 -> { + viewModel.updateSubscriptionStatus( + subId, + xyz.zarazaex.olc.dto.SubscriptionUpdateStatus.SKIPPED + ) + } + else -> { + viewModel.updateSubscriptionStatus( + subId, + xyz.zarazaex.olc.dto.SubscriptionUpdateStatus.FAILED + ) + } + } + } + + synchronized(this@SubSettingActivity) { + totalConfigCount += result.configCount + successCount += result.successCount + failureCount += result.failureCount + skipCount += result.skipCount + } } - hideLoading() - refreshData() } + + jobs.forEach { it.join() } + + delay(500L) + viewModel.isUpdating.value = false + + if (successCount + failureCount + skipCount == 0) { + toast(R.string.title_update_subscription_no_subscription) + } else if (successCount > 0 && failureCount + skipCount == 0) { + toast(getString(R.string.title_update_config_count, totalConfigCount)) + } else { + toast( + getString( + R.string.title_update_subscription_result, + totalConfigCount, successCount, failureCount, skipCount + ) + ) + } + hideLoading() + refreshData() } true diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/SubSettingRecyclerAdapter.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/SubSettingRecyclerAdapter.kt index 83107435..38869134 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/SubSettingRecyclerAdapter.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/SubSettingRecyclerAdapter.kt @@ -8,8 +8,10 @@ import android.view.ViewGroup import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import xyz.zarazaex.olc.AppConfig +import xyz.zarazaex.olc.R import xyz.zarazaex.olc.contracts.BaseAdapterListener import xyz.zarazaex.olc.databinding.ItemRecyclerSubSettingBinding +import xyz.zarazaex.olc.dto.SubscriptionUpdateStatus import xyz.zarazaex.olc.helper.ItemTouchHelperAdapter import xyz.zarazaex.olc.helper.ItemTouchHelperViewHolder import xyz.zarazaex.olc.util.Utils @@ -41,6 +43,38 @@ class SubSettingRecyclerAdapter( holder.itemSubSettingBinding.tvLastUpdated.text = Utils.formatTimestamp(subItem.lastUpdated) holder.itemView.setBackgroundColor(Color.TRANSPARENT) + val subStatus = viewModel.getSubscriptionStatus(subId) + when (subStatus?.status) { + SubscriptionUpdateStatus.LOADING -> { + holder.itemSubSettingBinding.progressBar.visibility = View.VISIBLE + holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.VISIBLE + holder.itemSubSettingBinding.tvUpdateStatus.text = holder.itemView.context.getString(R.string.title_updating) + holder.itemSubSettingBinding.tvUpdateStatus.setTextColor(Color.GRAY) + } + SubscriptionUpdateStatus.SUCCESS -> { + holder.itemSubSettingBinding.progressBar.visibility = View.GONE + holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.VISIBLE + holder.itemSubSettingBinding.tvUpdateStatus.text = "✓ ${subStatus.configCount}" + holder.itemSubSettingBinding.tvUpdateStatus.setTextColor(Color.parseColor("#4CAF50")) + } + SubscriptionUpdateStatus.FAILED -> { + holder.itemSubSettingBinding.progressBar.visibility = View.GONE + holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.VISIBLE + holder.itemSubSettingBinding.tvUpdateStatus.text = "✗" + holder.itemSubSettingBinding.tvUpdateStatus.setTextColor(Color.parseColor("#F44336")) + } + SubscriptionUpdateStatus.SKIPPED -> { + holder.itemSubSettingBinding.progressBar.visibility = View.GONE + holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.VISIBLE + holder.itemSubSettingBinding.tvUpdateStatus.text = "—" + holder.itemSubSettingBinding.tvUpdateStatus.setTextColor(Color.GRAY) + } + else -> { + holder.itemSubSettingBinding.progressBar.visibility = View.GONE + holder.itemSubSettingBinding.tvUpdateStatus.visibility = View.GONE + } + } + val isEnabled = !isUpdating holder.itemSubSettingBinding.layoutEdit.isClickable = isEnabled diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/viewmodel/SubscriptionsViewModel.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/viewmodel/SubscriptionsViewModel.kt index 970f00fe..a3c316fb 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/viewmodel/SubscriptionsViewModel.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/viewmodel/SubscriptionsViewModel.kt @@ -4,6 +4,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import xyz.zarazaex.olc.dto.SubscriptionCache import xyz.zarazaex.olc.dto.SubscriptionItem +import xyz.zarazaex.olc.dto.SubscriptionStatus +import xyz.zarazaex.olc.dto.SubscriptionUpdateStatus import xyz.zarazaex.olc.handler.MmkvManager import xyz.zarazaex.olc.handler.SettingsChangeManager import xyz.zarazaex.olc.handler.SettingsManager @@ -13,12 +15,24 @@ class SubscriptionsViewModel : ViewModel() { MmkvManager.decodeSubscriptions().toMutableList() val isUpdating = MutableLiveData(false) + val subscriptionStatuses = MutableLiveData>(emptyMap()) fun getAll(): List = subscriptions.toList() fun reload() { subscriptions.clear() subscriptions.addAll(MmkvManager.decodeSubscriptions()) + subscriptionStatuses.value = emptyMap() + } + + fun updateSubscriptionStatus(guid: String, status: SubscriptionUpdateStatus, configCount: Int = 0) { + val currentStatuses = subscriptionStatuses.value?.toMutableMap() ?: mutableMapOf() + currentStatuses[guid] = SubscriptionStatus(guid, status, configCount) + subscriptionStatuses.postValue(currentStatuses) + } + + fun getSubscriptionStatus(guid: String): SubscriptionStatus? { + return subscriptionStatuses.value?.get(guid) } fun remove(subId: String): Boolean { diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml index 9323157c..287c5fd1 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_sub_setting.xml @@ -168,9 +168,26 @@ + + + + + android:layout_marginStart="@dimen/padding_spacing_dp8" + android:textAppearance="@style/TextAppearance.AppCompat.Caption" + android:visibility="gone"/> diff --git a/V2rayNG/app/src/main/res/values-ru/strings.xml b/V2rayNG/app/src/main/res/values-ru/strings.xml index 4c824314..636d87b2 100644 --- a/V2rayNG/app/src/main/res/values-ru/strings.xml +++ b/V2rayNG/app/src/main/res/values-ru/strings.xml @@ -298,6 +298,7 @@ Импортировано профилей: %d Экспортировано профилей: %d Обновлено профилей: %d + Обновление… Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d) Нет подписок Выбранный профиль не найден в текущей группе diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml index ad720140..bc3cfb01 100644 --- a/V2rayNG/app/src/main/res/values/strings.xml +++ b/V2rayNG/app/src/main/res/values/strings.xml @@ -304,6 +304,7 @@ Import %d configs Export %d configs Update %d configs + Updating… Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped) No subscriptions Selected server not found in current group