fix(settings): keep the update badge across PROCESS_TEXT relaunches

Persist NeedsUpdate with its source ids and restore today's persisted
status — UpToDate and NeedsUpdate alike — through one shared helper used
by both startup paths, falling back to a fresh evaluation when the
persisted status can no longer be trusted (stale date, removed sources,
or the update already installed).
This commit is contained in:
OneWay committed 2026-10-14 23:33:51 +08:00
1 parent 731340fd13
commit e20af9c1ff
1 file changed
+88 -26
@@ -37,6 +37,16 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
private const val STATUS_UP_TO_DATE = "UpToDate"
private const val STATUS_NEEDS_UPDATE = "NeedsUpdate"
private const val STATUS_EMPTY = "Empty"
private data class PersistedDictionaryStatus(
val name: String,
val date: String,
val sourceIds: Set<String>,
)
private data class DictionaryStateInputs( private data class DictionaryStateInputs(
val theme: AppTheme, val theme: AppTheme,
val dictionarySources: List<DictionarySource>, val dictionarySources: List<DictionarySource>,
@@ -115,14 +125,18 @@ class SettingsViewModel(
private fun persistDictionaryStatus() { private fun persistDictionaryStatus() {
viewModelScope.launch { viewModelScope.launch {
dictionaryStatus.collect { status -> dictionaryStatus.collect { status ->
val persistedName = when (status) { val persisted = when (status) {
DictionaryStatus.UpToDate -> "UpToDate" DictionaryStatus.UpToDate ->
is DictionaryStatus.NeedsUpdate -> "NeedsUpdate" "$STATUS_UP_TO_DATE|${DateUtils.getCurrentDateString()}"
DictionaryStatus.Empty -> "Empty" is DictionaryStatus.NeedsUpdate ->
"$STATUS_NEEDS_UPDATE|${DateUtils.getCurrentDateString()}|" +
status.sourceIds.joinToString(",")
DictionaryStatus.Empty ->
"$STATUS_EMPTY|${DateUtils.getCurrentDateString()}"
else -> null else -> null
} }
if (persistedName != null) { if (persisted != null) {
preferencesManager.saveDictionaryStatus("$persistedName|${DateUtils.getCurrentDateString()}") preferencesManager.saveDictionaryStatus(persisted)
} }
} }
} }
@@ -283,7 +297,7 @@ class SettingsViewModel(
scanCompleted = if (localDictionaryRepository.dictionaries.value.isEmpty()) { scanCompleted = if (localDictionaryRepository.dictionaries.value.isEmpty()) {
scanDirectoryInternal(preferencesManager.dictionaryPath) is OperationResult.Success scanDirectoryInternal(preferencesManager.dictionaryPath) is OperationResult.Success
} else { } else {
dictionaryStatus.value = DictionaryStatus.UpToDate restoreOrEvaluateDictionaryStatus()
false false
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -551,6 +565,71 @@ class SettingsViewModel(
): List<DictionarySource> = ): List<DictionarySource> =
DictionarySourceFileMatcher.installedSources(sources, dictionaries) DictionarySourceFileMatcher.installedSources(sources, dictionaries)
private suspend fun restoreOrEvaluateDictionaryStatus() {
val restored = restorePersistedDictionaryStatus()
if (restored != null) {
dictionaryStatus.value = restored
return
}
dictionaryStatus.value = DictionaryStatus.Checking
dictionaryStatus.value = withContext(Dispatchers.IO) { evaluateDictionaryStatus() }
}
private suspend fun restorePersistedDictionaryStatus(): DictionaryStatus? {
val persisted = readPersistedDictionaryStatus() ?: return null
if (persisted.date != DateUtils.getCurrentDateString()) return null
return when (persisted.name) {
STATUS_UP_TO_DATE ->
if (withContext(Dispatchers.IO) { installedDictionariesAreCurrent() }) {
DictionaryStatus.UpToDate
} else {
null
}
STATUS_NEEDS_UPDATE -> {
val updatableIds = withContext(Dispatchers.IO) { sourcesMissingTodaysFile() }
val restorableIds = persisted.sourceIds intersect updatableIds
if (restorableIds.isNotEmpty()) DictionaryStatus.NeedsUpdate(restorableIds) else null
}
else -> null
}
}
private suspend fun readPersistedDictionaryStatus(): PersistedDictionaryStatus? {
val raw = preferencesManager.dictionaryStatus() ?: return null
val parts = raw.split('|')
if (parts.isEmpty() || parts.first().isEmpty()) return null
return PersistedDictionaryStatus(
name = parts.first(),
date = parts.getOrElse(1) { "" },
sourceIds = parts.getOrElse(2) { "" }
.split(',')
.map(String::trim)
.filter(String::isNotEmpty)
.toSet(),
)
}
private suspend fun sourcesMissingTodaysFile(): Set<String> {
val sources = preferencesManager.dictionarySources.first()
val dictionaries = localDictionaryRepository.dictionaries.first()
val datedEnabledSources = installedEnabledSources(sources, dictionaries)
.filter { DictionarySource.hasDatePlaceholder(it.urlTemplate) }
if (datedEnabledSources.isEmpty()) return emptySet()
val fileNames = localDictionaryRepository.listDictionaryPayloadFileNames(
preferencesManager.dictionaryPath
) ?: return emptySet()
val today = DateUtils.getCurrentDateString()
return datedEnabledSources
.filterNot { source -> fileNames.any { it.startsWith(fileBaseNameFor(source, today)) } }
.mapTo(mutableSetOf(), DictionarySource::id)
}
private fun fileBaseNameFor(source: DictionarySource, date: String): String =
DictionarySource.extractFileName(source.urlTemplate, date)
.removeSuffix(".gz")
.removeSuffix(".dsl")
.removeSuffix(".dz")
private suspend fun installedDictionariesAreCurrent(): Boolean { private suspend fun installedDictionariesAreCurrent(): Boolean {
val sources = preferencesManager.dictionarySources.first() val sources = preferencesManager.dictionarySources.first()
val dictionaries = localDictionaryRepository.dictionaries.first() val dictionaries = localDictionaryRepository.dictionaries.first()
@@ -562,11 +641,7 @@ class SettingsViewModel(
) ?: return true ) ?: return true
val today = DateUtils.getCurrentDateString() val today = DateUtils.getCurrentDateString()
return datedEnabledSources.all { source -> return datedEnabledSources.all { source ->
val baseName = DictionarySource.extractFileName(source.urlTemplate, today) fileNames.any { it.startsWith(fileBaseNameFor(source, today)) }
.removeSuffix(".gz")
.removeSuffix(".dsl")
.removeSuffix(".dz")
fileNames.any { it.startsWith(baseName) }
} }
} }
@@ -622,20 +697,7 @@ class SettingsViewModel(
dictionaryStatus.value = DictionaryStatus.UpToDate dictionaryStatus.value = DictionaryStatus.UpToDate
isDownloadInProgress = false isDownloadInProgress = false
} else { } else {
val savedStatus = preferencesManager.dictionaryStatus() restoreOrEvaluateDictionaryStatus()
val savedStatusName = savedStatus?.substringBefore('|')
val savedStatusDate = savedStatus?.substringAfter('|', "")
val restoredStatus = if (savedStatusName == "UpToDate") DictionaryStatus.UpToDate else null
if (restoredStatus != null &&
savedStatusDate == DateUtils.getCurrentDateString() &&
withContext(Dispatchers.IO) { installedDictionariesAreCurrent() }
) {
dictionaryStatus.value = restoredStatus
} else {
dictionaryStatus.value = DictionaryStatus.Checking
val status = withContext(Dispatchers.IO) { evaluateDictionaryStatus() }
dictionaryStatus.value = status
}
} }
if (result.data > 0) { if (result.data > 0) {