feat(subscription): add per-subscription update status tracking

This commit is contained in:
zarazaex69
2026-04-11 12:29:14 +03:00
parent 9af6472f8b
commit 153c545400
8 changed files with 159 additions and 21 deletions
@@ -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
)
@@ -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)
""
@@ -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
@@ -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
@@ -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<Boolean>(false)
val subscriptionStatuses = MutableLiveData<Map<String, SubscriptionStatus>>(emptyMap())
fun getAll(): List<SubscriptionCache> = 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 {
@@ -168,9 +168,26 @@
<TextView
android:id="@+id/tv_last_updated"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"/>
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleSmall"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="@dimen/padding_spacing_dp8"
android:visibility="gone"/>
<TextView
android:id="@+id/tv_update_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"/>
android:layout_marginStart="@dimen/padding_spacing_dp8"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:visibility="gone"/>
</LinearLayout>
@@ -298,6 +298,7 @@
<string name="title_import_config_count">Импортировано профилей: %d</string>
<string name="title_export_config_count">Экспортировано профилей: %d</string>
<string name="title_update_config_count">Обновлено профилей: %d</string>
<string name="title_updating">Обновление…</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="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string>
@@ -304,6 +304,7 @@
<string name="title_import_config_count">Import %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_updating">Updating…</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="toast_server_not_found_in_group">Selected server not found in current group</string>