Release 1.4.0
- Fixed matching source URLs to installed dictionary files, so deleted dictionaries can be downloaded again from the same URL
This commit is contained in:
1 parent
9d18d0c927
commit
910bc8313c
10 files changed
+200
-129
No files matched your search
@@ -11,6 +11,7 @@ import com.example.research.core.domain.model.AppTheme
|
||||
import com.example.research.core.domain.model.Dictionary
|
||||
import com.example.research.core.domain.model.DictionarySource
|
||||
import com.example.research.core.domain.model.IndexingProgress
|
||||
import com.example.research.core.domain.usecase.DictionarySourceFileMatcher
|
||||
import com.example.research.core.domain.usecase.DictionarySourceValidator
|
||||
import com.example.research.core.domain.usecase.ManageDictionarySourcesUseCase
|
||||
import com.example.research.core.util.OperationResult
|
||||
@@ -30,7 +31,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
private data class DictionaryStateInputs(
|
||||
val theme: AppTheme,
|
||||
@@ -79,7 +80,7 @@ class SettingsViewModel(
|
||||
private val language = MutableStateFlow("system")
|
||||
private val hasCompletedStartupScan = MutableStateFlow(false)
|
||||
|
||||
private val pendingSourceUrls = mutableSetOf<String>()
|
||||
private val pendingSourceUrls = ConcurrentHashMap.newKeySet<String>()
|
||||
private var isDownloadInProgress = false
|
||||
|
||||
private var cancelRefreshPending = false
|
||||
@@ -158,19 +159,14 @@ class SettingsViewModel(
|
||||
DictionaryStatus.UpToDate
|
||||
}
|
||||
if (pendingSourceUrls.isNotEmpty()) {
|
||||
|
||||
pendingSourceUrls.forEach { urlTemplate ->
|
||||
val source = dictionaryState.dictionarySources.find {
|
||||
it.urlTemplate == urlTemplate
|
||||
}
|
||||
if (source != null) {
|
||||
val hasDictionary = dictionaryState.dictionaries.any { dict ->
|
||||
DictionarySource.matchesDictionaryFile(
|
||||
urlTemplate,
|
||||
File(dict.path).name
|
||||
)
|
||||
val hasDictionary = dictionaryState.dictionaries.any { dictionary ->
|
||||
DictionarySourceFileMatcher.matches(source, dictionary)
|
||||
}
|
||||
|
||||
if (!hasDictionary) {
|
||||
manageDictionarySourcesUseCase.removeSource(source.id)
|
||||
}
|
||||
@@ -264,26 +260,35 @@ class SettingsViewModel(
|
||||
private suspend fun evaluateDictionaryStatus(): DictionaryStatus {
|
||||
return try {
|
||||
val actualDictionaries = localDictionaryRepository.dictionaries.first()
|
||||
val hasDictionaries = actualDictionaries.isNotEmpty()
|
||||
var sources = preferencesManager.dictionarySources.first()
|
||||
if (pendingSourceUrls.isEmpty() &&
|
||||
downloadManager.downloadState.value !is DownloadState.Loading &&
|
||||
downloadManager.downloadState.value !is DownloadState.Extracting
|
||||
) {
|
||||
val dictionaryFileNames = localDictionaryRepository.listDictionaryPayloadFileNames(
|
||||
preferencesManager.dictionaryPath
|
||||
)
|
||||
if (dictionaryFileNames != null) {
|
||||
val installedSources = DictionarySourceFileMatcher.installedSourcesForFileNames(
|
||||
sources,
|
||||
dictionaryFileNames,
|
||||
)
|
||||
val installedSourceIds = installedSources
|
||||
.mapTo(mutableSetOf(), DictionarySource::id)
|
||||
val staleSourceIds = sources
|
||||
.filterNot { it.id in installedSourceIds }
|
||||
.map(DictionarySource::id)
|
||||
if (staleSourceIds.isNotEmpty()) {
|
||||
preferencesManager.removeDictionarySources(staleSourceIds)
|
||||
sources = installedSources
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDictionaries) {
|
||||
if (actualDictionaries.isEmpty()) {
|
||||
return DictionaryStatus.Empty
|
||||
}
|
||||
|
||||
var sources = preferencesManager.dictionarySources.first()
|
||||
if (downloadManager.downloadState.value !is DownloadState.Loading &&
|
||||
downloadManager.downloadState.value !is DownloadState.Extracting
|
||||
) {
|
||||
val installedSources = installedSources(sources, actualDictionaries)
|
||||
val installedSourceIds = installedSources.mapTo(mutableSetOf(), DictionarySource::id)
|
||||
val staleSourceIds = sources
|
||||
.filterNot { it.id in installedSourceIds }
|
||||
.map(DictionarySource::id)
|
||||
if (staleSourceIds.isNotEmpty()) {
|
||||
preferencesManager.removeDictionarySources(staleSourceIds)
|
||||
sources = installedSources
|
||||
}
|
||||
}
|
||||
val enabledSources = installedEnabledSources(sources, actualDictionaries)
|
||||
|
||||
if (enabledSources.isEmpty()) {
|
||||
@@ -338,45 +343,62 @@ class SettingsViewModel(
|
||||
|
||||
private fun deleteDictionary(dictionary: Dictionary) {
|
||||
viewModelScope.launch {
|
||||
localDictionaryRepository.deleteDictionary(dictionary)
|
||||
|
||||
manageDictionarySourcesUseCase.removeSourceForDictionary(dictionary)
|
||||
|
||||
val remainingDictionaries = localDictionaryRepository.dictionaries.first()
|
||||
|
||||
if (remainingDictionaries.isEmpty()) {
|
||||
dictionaryStatus.value = DictionaryStatus.Empty
|
||||
when (localDictionaryRepository.deleteDictionary(dictionary)) {
|
||||
is OperationResult.Success -> {
|
||||
val remainingDictionaryFileNames =
|
||||
localDictionaryRepository.listDictionaryPayloadFileNames(
|
||||
preferencesManager.dictionaryPath
|
||||
)
|
||||
manageDictionarySourcesUseCase.removeSourceForDictionary(
|
||||
dictionary = dictionary,
|
||||
remainingDictionaryFileNames = remainingDictionaryFileNames,
|
||||
)
|
||||
dictionaryStatus.value = withContext(Dispatchers.IO) {
|
||||
evaluateDictionaryStatus()
|
||||
}
|
||||
}
|
||||
is OperationResult.Error -> {
|
||||
effectChannel.send(
|
||||
getApplication<Application>().getString(R.string.error_delete_dictionary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addDictionarySources(urlTemplates: List<String>) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val validUrls = mutableListOf<String>()
|
||||
val validUrls = mutableListOf<String>()
|
||||
|
||||
urlTemplates.forEach { urlTemplate ->
|
||||
val trimmed = urlTemplate.trim()
|
||||
if (trimmed.isEmpty() || trimmed.length > 2048) {
|
||||
return@forEach
|
||||
}
|
||||
urlTemplates.forEach { urlTemplate ->
|
||||
val trimmed = urlTemplate.trim()
|
||||
if (trimmed.isEmpty() || trimmed.length > 2048) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val normalizedUrl = DictionarySource.normalizeTemplate(trimmed)
|
||||
if (!pendingSourceUrls.add(normalizedUrl)) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
try {
|
||||
when (manageDictionarySourcesUseCase.addSource(trimmed)) {
|
||||
is ManageDictionarySourcesUseCase.AddSourceResult.Success -> {
|
||||
pendingSourceUrls.add(DictionarySource.normalizeTemplate(trimmed))
|
||||
validUrls.add(trimmed)
|
||||
}
|
||||
is ManageDictionarySourcesUseCase.AddSourceResult.ValidationFailed -> {
|
||||
pendingSourceUrls.remove(normalizedUrl)
|
||||
// Validation failed, skip this source
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
pendingSourceUrls.remove(normalizedUrl)
|
||||
android.util.Log.e("SettingsViewModel", "Error adding dictionary source", e)
|
||||
}
|
||||
}
|
||||
|
||||
if (validUrls.isNotEmpty()) {
|
||||
startDownloadForSources(validUrls)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("SettingsViewModel", "Error adding dictionary sources", e)
|
||||
if (validUrls.isNotEmpty()) {
|
||||
startDownloadForSources(validUrls)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -448,14 +470,8 @@ class SettingsViewModel(
|
||||
private fun installedSources(
|
||||
sources: List<DictionarySource>,
|
||||
dictionaries: List<Dictionary>
|
||||
): List<DictionarySource> {
|
||||
val installedFileNames = dictionaries.map { File(it.path).name }
|
||||
return sources.filter { source ->
|
||||
installedFileNames.any { fileName ->
|
||||
DictionarySource.matchesDictionaryFile(source.urlTemplate, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
): List<DictionarySource> =
|
||||
DictionarySourceFileMatcher.installedSources(sources, dictionaries)
|
||||
|
||||
private fun cancelDownload() {
|
||||
// Cancel the ViewModel-side scan job so no further status recomputes
|
||||
@@ -501,17 +517,17 @@ class SettingsViewModel(
|
||||
is OperationResult.Success -> {
|
||||
val dictionaries = localDictionaryRepository.dictionaries.first()
|
||||
if (dictionaries.isEmpty()) {
|
||||
dictionaryStatus.value = DictionaryStatus.Empty
|
||||
dictionaryStatus.value = withContext(Dispatchers.IO) {
|
||||
evaluateDictionaryStatus()
|
||||
}
|
||||
isDownloadInProgress = false
|
||||
} else if (isDownloadInProgress) {
|
||||
dictionaryStatus.value = DictionaryStatus.UpToDate
|
||||
isDownloadInProgress = false
|
||||
} else {
|
||||
if (isDownloadInProgress) {
|
||||
dictionaryStatus.value = DictionaryStatus.UpToDate
|
||||
isDownloadInProgress = false
|
||||
} else {
|
||||
dictionaryStatus.value = DictionaryStatus.Checking
|
||||
val status = withContext(Dispatchers.IO) { evaluateDictionaryStatus() }
|
||||
dictionaryStatus.value = status
|
||||
}
|
||||
dictionaryStatus.value = DictionaryStatus.Checking
|
||||
val status = withContext(Dispatchers.IO) { evaluateDictionaryStatus() }
|
||||
dictionaryStatus.value = status
|
||||
}
|
||||
|
||||
if (result.data > 0) {
|
||||
|
||||
Reference in new issue
Block a user