Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
910bc8313c |
@@ -92,8 +92,8 @@ android {
|
|||||||
applicationId = "com.example.research"
|
applicationId = "com.example.research"
|
||||||
minSdk = project.property("minSdk").toString().toInt()
|
minSdk = project.property("minSdk").toString().toInt()
|
||||||
targetSdk = project.property("targetSdk").toString().toInt()
|
targetSdk = project.property("targetSdk").toString().toInt()
|
||||||
versionCode = 6
|
versionCode = 7
|
||||||
versionName = "1.3.0"
|
versionName = "1.4.0"
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package com.example.research.core.domain.usecase
|
||||||
|
|
||||||
|
import com.example.research.core.domain.model.Dictionary
|
||||||
|
import com.example.research.core.domain.model.DictionarySource
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object DictionarySourceFileMatcher {
|
||||||
|
fun matches(source: DictionarySource, fileName: String): Boolean =
|
||||||
|
DictionarySource.matchesDictionaryFile(source.urlTemplate, fileName)
|
||||||
|
|
||||||
|
fun matches(source: DictionarySource, dictionary: Dictionary): Boolean =
|
||||||
|
matches(source, File(dictionary.path).name)
|
||||||
|
|
||||||
|
fun installedSources(
|
||||||
|
sources: List<DictionarySource>,
|
||||||
|
dictionaries: List<Dictionary>,
|
||||||
|
): List<DictionarySource> = sources.filter { source ->
|
||||||
|
dictionaries.any { dictionary -> matches(source, dictionary) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installedSourcesForFileNames(
|
||||||
|
sources: List<DictionarySource>,
|
||||||
|
fileNames: Collection<String>,
|
||||||
|
): List<DictionarySource> = sources.filter { source ->
|
||||||
|
fileNames.any { fileName -> matches(source, fileName) }
|
||||||
|
}
|
||||||
|
}
|
||||||
-8
@@ -43,12 +43,4 @@ class DictionarySourceValidator {
|
|||||||
return ValidationResult.Valid
|
return ValidationResult.Valid
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findMatchingSourceWithPrecomputedPrefixes(
|
|
||||||
dictionaryPrefix: String,
|
|
||||||
sourcesWithPrefixes: List<Pair<DictionarySource, String?>>
|
|
||||||
): DictionarySource? {
|
|
||||||
return sourcesWithPrefixes.find { (_, sourcePrefix) ->
|
|
||||||
sourcePrefix != null && dictionaryPrefix.equals(sourcePrefix, ignoreCase = true)
|
|
||||||
}?.first
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-10
@@ -30,17 +30,23 @@ class ManageDictionarySourcesUseCase(
|
|||||||
preferencesManager.removeDictionarySource(sourceId)
|
preferencesManager.removeDictionarySource(sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeSourceForDictionary(dictionary: Dictionary) {
|
suspend fun removeSourceForDictionary(
|
||||||
val dictionaryPrefix = dictionary.name
|
dictionary: Dictionary,
|
||||||
val sources = preferencesManager.dictionarySources.first()
|
remainingDictionaryFileNames: Collection<String>?,
|
||||||
val sourcesWithPrefixes = sources.map { source ->
|
) {
|
||||||
source to DictionarySource.extractPrefix(source.urlTemplate)
|
if (remainingDictionaryFileNames == null) return
|
||||||
}
|
|
||||||
|
|
||||||
val matchingSource = validator.findMatchingSourceWithPrecomputedPrefixes(dictionaryPrefix, sourcesWithPrefixes)
|
val sources = preferencesManager.dictionarySources.first()
|
||||||
if (matchingSource != null) {
|
val sourceIdsToRemove = sources
|
||||||
preferencesManager.removeDictionarySource(matchingSource.id)
|
.filter { source ->
|
||||||
}
|
DictionarySourceFileMatcher.matches(source, dictionary) &&
|
||||||
|
remainingDictionaryFileNames.none { fileName ->
|
||||||
|
DictionarySourceFileMatcher.matches(source, fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map(DictionarySource::id)
|
||||||
|
|
||||||
|
preferencesManager.removeDictionarySources(sourceIdsToRemove)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class AddSourceResult {
|
sealed class AddSourceResult {
|
||||||
|
|||||||
+72
-46
@@ -123,6 +123,24 @@ class LocalDictionaryRepository(
|
|||||||
|
|
||||||
fun isIndexingInProgress(): Boolean = currentIndexingJob?.isActive == true
|
fun isIndexingInProgress(): Boolean = currentIndexingJob?.isActive == true
|
||||||
|
|
||||||
|
suspend fun listDictionaryPayloadFileNames(path: String): List<String>? =
|
||||||
|
withContext(ioDispatcher) {
|
||||||
|
try {
|
||||||
|
val directory = File(path)
|
||||||
|
when {
|
||||||
|
!directory.exists() -> {
|
||||||
|
if (directory.parentFile?.isDirectory == true) emptyList() else null
|
||||||
|
}
|
||||||
|
!directory.isDirectory -> null
|
||||||
|
else -> directory.listFiles { file -> isDictionaryPayloadFile(file) }
|
||||||
|
?.map(File::getName)
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(TAG, "Failed to list dictionary files: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun scanDirectory(pathOrUri: String): OperationResult<Int> = withContext(ioDispatcher) {
|
suspend fun scanDirectory(pathOrUri: String): OperationResult<Int> = withContext(ioDispatcher) {
|
||||||
if (isIndexingInProgress()) {
|
if (isIndexingInProgress()) {
|
||||||
return@withContext OperationResult.Error(
|
return@withContext OperationResult.Error(
|
||||||
@@ -371,9 +389,7 @@ class LocalDictionaryRepository(
|
|||||||
val dir = File(path)
|
val dir = File(path)
|
||||||
if (!dir.exists() || !dir.isDirectory) return emptyList()
|
if (!dir.exists() || !dir.isDirectory) return emptyList()
|
||||||
|
|
||||||
return dir.listFiles { f ->
|
return dir.listFiles { file -> isDictionaryPayloadFile(file) }?.map { file ->
|
||||||
f.isFile && (f.name.endsWith(".dsl") || f.name.endsWith(".dsl.dz") || f.name.endsWith(".dsl.gz"))
|
|
||||||
}?.map { file ->
|
|
||||||
DiscoveredFile(
|
DiscoveredFile(
|
||||||
name = file.name,
|
name = file.name,
|
||||||
localPath = file.absolutePath,
|
localPath = file.absolutePath,
|
||||||
@@ -382,6 +398,13 @@ class LocalDictionaryRepository(
|
|||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isDictionaryPayloadFile(file: File): Boolean =
|
||||||
|
file.isFile && (
|
||||||
|
file.name.endsWith(".dsl") ||
|
||||||
|
file.name.endsWith(".dsl.dz") ||
|
||||||
|
file.name.endsWith(".dsl.gz")
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun search(query: String): OperationResult<List<IndexEntry>> = withContext(defaultDispatcher) {
|
suspend fun search(query: String): OperationResult<List<IndexEntry>> = withContext(defaultDispatcher) {
|
||||||
if (query.isBlank()) return@withContext OperationResult.Success(emptyList())
|
if (query.isBlank()) return@withContext OperationResult.Success(emptyList())
|
||||||
|
|
||||||
@@ -460,53 +483,56 @@ class LocalDictionaryRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteDictionary(dictionary: Dictionary): OperationResult<Unit> {
|
suspend fun deleteDictionary(dictionary: Dictionary): OperationResult<Unit> {
|
||||||
try {
|
return try {
|
||||||
dictionaryStateMutex.withLock { removeDictionaryFromState(dictionary.path) }
|
val (failure, cleanupErrors) = withContext(ioDispatcher) {
|
||||||
|
val dictFile = File(dictionary.path)
|
||||||
launch(ioDispatcher) {
|
if (dictFile.exists() && !dictFile.delete()) {
|
||||||
try {
|
return@withContext Pair(
|
||||||
val deleteErrors = mutableListOf<String>()
|
"Failed to delete dictionary file: ${dictFile.absolutePath}",
|
||||||
|
emptyList<String>(),
|
||||||
val dictFile = File(dictionary.path)
|
)
|
||||||
if (dictFile.exists()) {
|
|
||||||
if (!dictFile.delete()) {
|
|
||||||
deleteErrors.add("Failed to delete dictionary file: ${dictFile.absolutePath}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val indexFile = if (dictionary.indexPath.endsWith(".idx", ignoreCase = true)) {
|
|
||||||
File(dictionary.indexPath)
|
|
||||||
} else {
|
|
||||||
File("${dictionary.indexPath}.idx")
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteIndexFiles(indexFile, deleteErrors)
|
|
||||||
|
|
||||||
if (!dictFile.exists()) {
|
|
||||||
dictionaryStateMutex.withLock {
|
|
||||||
preferencesManager.removeDictionaryActiveState(dictionary.path)
|
|
||||||
removeDictionaryFromState(dictionary.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
engine.getIndexSearcher().trimMemory(80)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Failed to trim memory after deletion: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deleteErrors.isNotEmpty()) {
|
|
||||||
val message = deleteErrors.joinToString("; ")
|
|
||||||
Log.w(TAG, "Dictionary deletion completed with errors: $message")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Background deletion failed: ${e.message}", e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val deleteErrors = mutableListOf<String>()
|
||||||
|
val indexFile = if (dictionary.indexPath.endsWith(".idx", ignoreCase = true)) {
|
||||||
|
File(dictionary.indexPath)
|
||||||
|
} else {
|
||||||
|
File("${dictionary.indexPath}.idx")
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteIndexFiles(indexFile, deleteErrors)
|
||||||
|
Pair<String?, List<String>>(null, deleteErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
return OperationResult.Success(Unit)
|
if (failure != null) {
|
||||||
|
return OperationResult.Error(failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
dictionaryStateMutex.withLock {
|
||||||
|
try {
|
||||||
|
preferencesManager.removeDictionaryActiveState(dictionary.path)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to clear dictionary active state: ${e.message}")
|
||||||
|
}
|
||||||
|
removeDictionaryFromState(dictionary.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
engine.getIndexSearcher().trimMemory(80)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to trim memory after deletion: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanupErrors.isNotEmpty()) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Dictionary deletion completed with errors: ${cleanupErrors.joinToString("; ")}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OperationResult.Success(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return OperationResult.Error("Failed to delete dictionary", e)
|
OperationResult.Error("Failed to delete dictionary", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.Dictionary
|
||||||
import com.example.research.core.domain.model.DictionarySource
|
import com.example.research.core.domain.model.DictionarySource
|
||||||
import com.example.research.core.domain.model.IndexingProgress
|
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.DictionarySourceValidator
|
||||||
import com.example.research.core.domain.usecase.ManageDictionarySourcesUseCase
|
import com.example.research.core.domain.usecase.ManageDictionarySourcesUseCase
|
||||||
import com.example.research.core.util.OperationResult
|
import com.example.research.core.util.OperationResult
|
||||||
@@ -30,7 +31,7 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
private data class DictionaryStateInputs(
|
private data class DictionaryStateInputs(
|
||||||
val theme: AppTheme,
|
val theme: AppTheme,
|
||||||
@@ -79,7 +80,7 @@ class SettingsViewModel(
|
|||||||
private val language = MutableStateFlow("system")
|
private val language = MutableStateFlow("system")
|
||||||
private val hasCompletedStartupScan = MutableStateFlow(false)
|
private val hasCompletedStartupScan = MutableStateFlow(false)
|
||||||
|
|
||||||
private val pendingSourceUrls = mutableSetOf<String>()
|
private val pendingSourceUrls = ConcurrentHashMap.newKeySet<String>()
|
||||||
private var isDownloadInProgress = false
|
private var isDownloadInProgress = false
|
||||||
|
|
||||||
private var cancelRefreshPending = false
|
private var cancelRefreshPending = false
|
||||||
@@ -158,19 +159,14 @@ class SettingsViewModel(
|
|||||||
DictionaryStatus.UpToDate
|
DictionaryStatus.UpToDate
|
||||||
}
|
}
|
||||||
if (pendingSourceUrls.isNotEmpty()) {
|
if (pendingSourceUrls.isNotEmpty()) {
|
||||||
|
|
||||||
pendingSourceUrls.forEach { urlTemplate ->
|
pendingSourceUrls.forEach { urlTemplate ->
|
||||||
val source = dictionaryState.dictionarySources.find {
|
val source = dictionaryState.dictionarySources.find {
|
||||||
it.urlTemplate == urlTemplate
|
it.urlTemplate == urlTemplate
|
||||||
}
|
}
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
val hasDictionary = dictionaryState.dictionaries.any { dict ->
|
val hasDictionary = dictionaryState.dictionaries.any { dictionary ->
|
||||||
DictionarySource.matchesDictionaryFile(
|
DictionarySourceFileMatcher.matches(source, dictionary)
|
||||||
urlTemplate,
|
|
||||||
File(dict.path).name
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasDictionary) {
|
if (!hasDictionary) {
|
||||||
manageDictionarySourcesUseCase.removeSource(source.id)
|
manageDictionarySourcesUseCase.removeSource(source.id)
|
||||||
}
|
}
|
||||||
@@ -264,26 +260,35 @@ class SettingsViewModel(
|
|||||||
private suspend fun evaluateDictionaryStatus(): DictionaryStatus {
|
private suspend fun evaluateDictionaryStatus(): DictionaryStatus {
|
||||||
return try {
|
return try {
|
||||||
val actualDictionaries = localDictionaryRepository.dictionaries.first()
|
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
|
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)
|
val enabledSources = installedEnabledSources(sources, actualDictionaries)
|
||||||
|
|
||||||
if (enabledSources.isEmpty()) {
|
if (enabledSources.isEmpty()) {
|
||||||
@@ -338,45 +343,62 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
private fun deleteDictionary(dictionary: Dictionary) {
|
private fun deleteDictionary(dictionary: Dictionary) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
localDictionaryRepository.deleteDictionary(dictionary)
|
when (localDictionaryRepository.deleteDictionary(dictionary)) {
|
||||||
|
is OperationResult.Success -> {
|
||||||
manageDictionarySourcesUseCase.removeSourceForDictionary(dictionary)
|
val remainingDictionaryFileNames =
|
||||||
|
localDictionaryRepository.listDictionaryPayloadFileNames(
|
||||||
val remainingDictionaries = localDictionaryRepository.dictionaries.first()
|
preferencesManager.dictionaryPath
|
||||||
|
)
|
||||||
if (remainingDictionaries.isEmpty()) {
|
manageDictionarySourcesUseCase.removeSourceForDictionary(
|
||||||
dictionaryStatus.value = DictionaryStatus.Empty
|
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>) {
|
private fun addDictionarySources(urlTemplates: List<String>) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
val validUrls = mutableListOf<String>()
|
||||||
val validUrls = mutableListOf<String>()
|
|
||||||
|
|
||||||
urlTemplates.forEach { urlTemplate ->
|
urlTemplates.forEach { urlTemplate ->
|
||||||
val trimmed = urlTemplate.trim()
|
val trimmed = urlTemplate.trim()
|
||||||
if (trimmed.isEmpty() || trimmed.length > 2048) {
|
if (trimmed.isEmpty() || trimmed.length > 2048) {
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val normalizedUrl = DictionarySource.normalizeTemplate(trimmed)
|
||||||
|
if (!pendingSourceUrls.add(normalizedUrl)) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
when (manageDictionarySourcesUseCase.addSource(trimmed)) {
|
when (manageDictionarySourcesUseCase.addSource(trimmed)) {
|
||||||
is ManageDictionarySourcesUseCase.AddSourceResult.Success -> {
|
is ManageDictionarySourcesUseCase.AddSourceResult.Success -> {
|
||||||
pendingSourceUrls.add(DictionarySource.normalizeTemplate(trimmed))
|
|
||||||
validUrls.add(trimmed)
|
validUrls.add(trimmed)
|
||||||
}
|
}
|
||||||
is ManageDictionarySourcesUseCase.AddSourceResult.ValidationFailed -> {
|
is ManageDictionarySourcesUseCase.AddSourceResult.ValidationFailed -> {
|
||||||
|
pendingSourceUrls.remove(normalizedUrl)
|
||||||
// Validation failed, skip this source
|
// Validation failed, skip this source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
pendingSourceUrls.remove(normalizedUrl)
|
||||||
|
android.util.Log.e("SettingsViewModel", "Error adding dictionary source", e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (validUrls.isNotEmpty()) {
|
if (validUrls.isNotEmpty()) {
|
||||||
startDownloadForSources(validUrls)
|
startDownloadForSources(validUrls)
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
android.util.Log.e("SettingsViewModel", "Error adding dictionary sources", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,14 +470,8 @@ class SettingsViewModel(
|
|||||||
private fun installedSources(
|
private fun installedSources(
|
||||||
sources: List<DictionarySource>,
|
sources: List<DictionarySource>,
|
||||||
dictionaries: List<Dictionary>
|
dictionaries: List<Dictionary>
|
||||||
): List<DictionarySource> {
|
): List<DictionarySource> =
|
||||||
val installedFileNames = dictionaries.map { File(it.path).name }
|
DictionarySourceFileMatcher.installedSources(sources, dictionaries)
|
||||||
return sources.filter { source ->
|
|
||||||
installedFileNames.any { fileName ->
|
|
||||||
DictionarySource.matchesDictionaryFile(source.urlTemplate, fileName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelDownload() {
|
private fun cancelDownload() {
|
||||||
// Cancel the ViewModel-side scan job so no further status recomputes
|
// Cancel the ViewModel-side scan job so no further status recomputes
|
||||||
@@ -501,17 +517,17 @@ class SettingsViewModel(
|
|||||||
is OperationResult.Success -> {
|
is OperationResult.Success -> {
|
||||||
val dictionaries = localDictionaryRepository.dictionaries.first()
|
val dictionaries = localDictionaryRepository.dictionaries.first()
|
||||||
if (dictionaries.isEmpty()) {
|
if (dictionaries.isEmpty()) {
|
||||||
dictionaryStatus.value = DictionaryStatus.Empty
|
dictionaryStatus.value = withContext(Dispatchers.IO) {
|
||||||
|
evaluateDictionaryStatus()
|
||||||
|
}
|
||||||
|
isDownloadInProgress = false
|
||||||
|
} else if (isDownloadInProgress) {
|
||||||
|
dictionaryStatus.value = DictionaryStatus.UpToDate
|
||||||
isDownloadInProgress = false
|
isDownloadInProgress = false
|
||||||
} else {
|
} else {
|
||||||
if (isDownloadInProgress) {
|
dictionaryStatus.value = DictionaryStatus.Checking
|
||||||
dictionaryStatus.value = DictionaryStatus.UpToDate
|
val status = withContext(Dispatchers.IO) { evaluateDictionaryStatus() }
|
||||||
isDownloadInProgress = false
|
dictionaryStatus.value = status
|
||||||
} else {
|
|
||||||
dictionaryStatus.value = DictionaryStatus.Checking
|
|
||||||
val status = withContext(Dispatchers.IO) { evaluateDictionaryStatus() }
|
|
||||||
dictionaryStatus.value = status
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.data > 0) {
|
if (result.data > 0) {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
</plurals>
|
</plurals>
|
||||||
<string name="dictionary_not_indexed">• Не индексирован</string>
|
<string name="dictionary_not_indexed">• Не индексирован</string>
|
||||||
<string name="dictionary_delete">Удалить словарь</string>
|
<string name="dictionary_delete">Удалить словарь</string>
|
||||||
|
<string name="error_delete_dictionary">Не удалось удалить словарь</string>
|
||||||
<string name="dictionaries_tap_to_update">Нажмите для обновления</string>
|
<string name="dictionaries_tap_to_update">Нажмите для обновления</string>
|
||||||
<string name="notification_import_title">Импорт словарей</string>
|
<string name="notification_import_title">Импорт словарей</string>
|
||||||
<string name="notification_import_success_title">Импорт завершён</string>
|
<string name="notification_import_success_title">Импорт завершён</string>
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
</plurals>
|
</plurals>
|
||||||
<string name="dictionary_not_indexed">• Not indexed</string>
|
<string name="dictionary_not_indexed">• Not indexed</string>
|
||||||
<string name="dictionary_delete">Delete dictionary</string>
|
<string name="dictionary_delete">Delete dictionary</string>
|
||||||
|
<string name="error_delete_dictionary">Failed to delete dictionary</string>
|
||||||
<string name="dictionaries_tap_to_update">Tap to update all dictionaries</string>
|
<string name="dictionaries_tap_to_update">Tap to update all dictionaries</string>
|
||||||
<string name="notification_import_title">Importing dictionaries</string>
|
<string name="notification_import_title">Importing dictionaries</string>
|
||||||
<string name="notification_import_success_title">Import completed</string>
|
<string name="notification_import_success_title">Import completed</string>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
- Fixed matching source URLs to installed dictionary files, so deleted dictionaries can be downloaded again from the same URL
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- Исправлено сопоставление URL-источников с установленными файлами — удалённый словарь теперь можно повторно скачать по тому же URL
|
||||||
Reference in New Issue
Block a user