Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc3fa85b9d | ||
|
|
457bbe43b6 | ||
|
|
f751ac0d36 |
@@ -95,15 +95,15 @@ The main search screen remains usable as long as at least one indexed dictionary
|
|||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- Kotlin 2.4.10
|
- Kotlin 2.4.10
|
||||||
- Jetpack Compose (BOM 2026.06.01)
|
- Jetpack Compose (BOM 2026.08.00)
|
||||||
- Material 3
|
- Material 3
|
||||||
- Coroutines and Flow 1.11.0
|
- Coroutines and Flow 1.11.0
|
||||||
- DataStore Preferences 1.2.1
|
- DataStore Preferences 1.2.1
|
||||||
- Paging 3.5.0
|
- Paging 3.5.1
|
||||||
- OkHttp 5.4.0
|
- OkHttp 5.4.0
|
||||||
- kotlinx.serialization 1.11.0
|
- kotlinx.serialization 1.11.0
|
||||||
- AboutLibraries 15.0.4 metadata generation
|
- AboutLibraries 15.0.4 metadata generation
|
||||||
- Android Gradle Plugin 9.3.0
|
- Android Gradle Plugin 9.3.1
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
|||||||
@@ -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 = 7
|
versionCode = 8
|
||||||
versionName = "1.4.0"
|
versionName = "1.5.0"
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ object DslHeadwordParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (trimmed.indexOf('{') == -1 && trimmed.indexOf('[') == -1) {
|
if (trimmed.indexOf('{') == -1 && trimmed.indexOf('[') == -1) {
|
||||||
|
val unescaped = unescape(trimmed)
|
||||||
return ParsedHeadword(
|
return ParsedHeadword(
|
||||||
simplified = trimmed,
|
simplified = unescaped,
|
||||||
displayText = trimmed,
|
displayText = unescaped,
|
||||||
searchableText = trimmed.lowercase()
|
searchableText = unescaped.lowercase()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,12 +61,12 @@ object DslHeadwordParser {
|
|||||||
val searchableText = createSearchableText(trimmed)
|
val searchableText = createSearchableText(trimmed)
|
||||||
|
|
||||||
return ParsedHeadword(
|
return ParsedHeadword(
|
||||||
simplified = simplified,
|
simplified = unescape(simplified),
|
||||||
displayText = displayText,
|
displayText = unescape(displayText),
|
||||||
searchableText = searchableText
|
searchableText = unescape(searchableText)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDisplayText(
|
private fun buildDisplayText(
|
||||||
reconstructedPrefix: String,
|
reconstructedPrefix: String,
|
||||||
simplified: String,
|
simplified: String,
|
||||||
@@ -96,7 +97,7 @@ object DslHeadwordParser {
|
|||||||
.orEmpty()
|
.orEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSearchableText(originalHeadword: String): String {
|
private fun createSearchableText(originalHeadword: String): String {
|
||||||
return removeFormattingTags(
|
return removeFormattingTags(
|
||||||
removeCurlyBraceMatches(originalHeadword, removeEmpty = true)
|
removeCurlyBraceMatches(originalHeadword, removeEmpty = true)
|
||||||
@@ -129,8 +130,17 @@ object DslHeadwordParser {
|
|||||||
if (value.indexOf('{') == -1 && value.indexOf('}') == -1) return value
|
if (value.indexOf('{') == -1 && value.indexOf('}') == -1) return value
|
||||||
|
|
||||||
return buildString(value.length) {
|
return buildString(value.length) {
|
||||||
value.forEach { char ->
|
var index = 0
|
||||||
if (char != '{' && char != '}') append(char)
|
while (index < value.length) {
|
||||||
|
val char = value[index]
|
||||||
|
if (char == '\\' && index + 1 < value.length) {
|
||||||
|
append(char)
|
||||||
|
append(value[index + 1])
|
||||||
|
index += 2
|
||||||
|
} else {
|
||||||
|
if (char != '{' && char != '}') append(char)
|
||||||
|
index++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,6 +148,10 @@ object DslHeadwordParser {
|
|||||||
private fun firstFormattingTagIndex(value: String): Int {
|
private fun firstFormattingTagIndex(value: String): Int {
|
||||||
var index = 0
|
var index = 0
|
||||||
while (index < value.length) {
|
while (index < value.length) {
|
||||||
|
if (value[index] == '\\' && index + 1 < value.length) {
|
||||||
|
index += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (value[index] == '[' && formattingTagEnd(value, index) > index) {
|
if (value[index] == '[' && formattingTagEnd(value, index) > index) {
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
@@ -149,8 +163,12 @@ object DslHeadwordParser {
|
|||||||
private fun firstCurlyContent(value: String): String? {
|
private fun firstCurlyContent(value: String): String? {
|
||||||
var index = 0
|
var index = 0
|
||||||
while (index < value.length) {
|
while (index < value.length) {
|
||||||
|
if (value[index] == '\\' && index + 1 < value.length) {
|
||||||
|
index += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (value[index] == '{') {
|
if (value[index] == '{') {
|
||||||
val end = value.indexOf('}', startIndex = index + 1)
|
val end = matchingCurlyEnd(value, index + 1)
|
||||||
if (end > index + 1) {
|
if (end > index + 1) {
|
||||||
return value.substring(index + 1, end)
|
return value.substring(index + 1, end)
|
||||||
}
|
}
|
||||||
@@ -161,7 +179,7 @@ object DslHeadwordParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun substringBeforeFirstBracket(value: String): String {
|
private fun substringBeforeFirstBracket(value: String): String {
|
||||||
val bracketIndex = value.indexOf('[')
|
val bracketIndex = indexOfUnescaped(value, '[')
|
||||||
return if (bracketIndex >= 0) value.substring(0, bracketIndex) else value
|
return if (bracketIndex >= 0) value.substring(0, bracketIndex) else value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +189,12 @@ object DslHeadwordParser {
|
|||||||
var index = 0
|
var index = 0
|
||||||
|
|
||||||
while (index < value.length) {
|
while (index < value.length) {
|
||||||
|
if (value[index] == '\\' && index + 1 < value.length) {
|
||||||
|
index += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (value[index] == '{') {
|
if (value[index] == '{') {
|
||||||
val end = value.indexOf('}', startIndex = index + 1)
|
val end = matchingCurlyEnd(value, index + 1)
|
||||||
if (end >= 0 && (removeEmpty || end > index + 1)) {
|
if (end >= 0 && (removeEmpty || end > index + 1)) {
|
||||||
if (builder == null) {
|
if (builder == null) {
|
||||||
builder = StringBuilder(value.length)
|
builder = StringBuilder(value.length)
|
||||||
@@ -197,6 +219,10 @@ object DslHeadwordParser {
|
|||||||
var index = 0
|
var index = 0
|
||||||
|
|
||||||
while (index < value.length) {
|
while (index < value.length) {
|
||||||
|
if (value[index] == '\\' && index + 1 < value.length) {
|
||||||
|
index += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (value[index] == '[') {
|
if (value[index] == '[') {
|
||||||
val tagEnd = formattingTagEnd(value, index)
|
val tagEnd = formattingTagEnd(value, index)
|
||||||
if (tagEnd > index) {
|
if (tagEnd > index) {
|
||||||
@@ -217,6 +243,56 @@ object DslHeadwordParser {
|
|||||||
}?.toString() ?: value
|
}?.toString() ?: value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun matchingCurlyEnd(value: String, startIndex: Int): Int {
|
||||||
|
var index = startIndex
|
||||||
|
while (index < value.length) {
|
||||||
|
when {
|
||||||
|
value[index] == '\\' && index + 1 < value.length -> index += 2
|
||||||
|
value[index] == '{' -> return -1
|
||||||
|
value[index] == '}' -> return index
|
||||||
|
else -> index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun indexOfUnescaped(
|
||||||
|
value: String,
|
||||||
|
target: Char,
|
||||||
|
startIndex: Int = 0,
|
||||||
|
): Int {
|
||||||
|
var index = startIndex.coerceAtLeast(0)
|
||||||
|
while (index < value.length) {
|
||||||
|
if (value[index] == '\\' && index + 1 < value.length) {
|
||||||
|
index += 2
|
||||||
|
} else if (value[index] == target) {
|
||||||
|
return index
|
||||||
|
} else {
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unescape(value: String): String {
|
||||||
|
val firstEscape = value.indexOf('\\')
|
||||||
|
if (firstEscape < 0) return value
|
||||||
|
|
||||||
|
return buildString(value.length) {
|
||||||
|
append(value, 0, firstEscape)
|
||||||
|
var index = firstEscape
|
||||||
|
while (index < value.length) {
|
||||||
|
if (value[index] == '\\' && index + 1 < value.length) {
|
||||||
|
append(value[index + 1])
|
||||||
|
index += 2
|
||||||
|
} else {
|
||||||
|
append(value[index])
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun formattingTagEnd(value: String, openBracketIndex: Int): Int {
|
private fun formattingTagEnd(value: String, openBracketIndex: Int): Int {
|
||||||
val tokenStart = openBracketIndex + 1
|
val tokenStart = openBracketIndex + 1
|
||||||
if (tokenStart >= value.length) return -1
|
if (tokenStart >= value.length) return -1
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ object DslAnnotatedParser {
|
|||||||
"br", "p", "b", "i", "c", "t", "m", "m0", "m1", "m2", "m3", "m4", "m5",
|
"br", "p", "b", "i", "c", "t", "m", "m0", "m1", "m2", "m3", "m4", "m5",
|
||||||
"ref", "ex", "e", "trn", "com", "lang", "sup", "'"
|
"ref", "ex", "e", "trn", "com", "lang", "sup", "'"
|
||||||
)
|
)
|
||||||
private val ESCAPED_CHARS = setOf('[', ']', '(', ')')
|
|
||||||
|
|
||||||
data class ColorScheme(
|
data class ColorScheme(
|
||||||
val secondaryText: Color,
|
val secondaryText: Color,
|
||||||
@@ -101,15 +100,13 @@ object DslAnnotatedParser {
|
|||||||
'\\' -> {
|
'\\' -> {
|
||||||
if (i + 1 < length) {
|
if (i + 1 < length) {
|
||||||
val next = dsl[i + 1]
|
val next = dsl[i + 1]
|
||||||
if (next in ESCAPED_CHARS) {
|
builder.append(next)
|
||||||
builder.append(next)
|
if (refStack.isNotEmpty()) {
|
||||||
if (refStack.isNotEmpty()) {
|
refStack.last().second.append(next)
|
||||||
refStack.last().second.append(next)
|
|
||||||
}
|
|
||||||
lastChar = next
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
lastChar = next
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
builder.append(char)
|
builder.append(char)
|
||||||
if (refStack.isNotEmpty()) {
|
if (refStack.isNotEmpty()) {
|
||||||
@@ -119,7 +116,7 @@ object DslAnnotatedParser {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
'[' -> {
|
'[' -> {
|
||||||
val end = dsl.indexOf(']', i + 1)
|
val end = tagCloseIndex(dsl, i + 1)
|
||||||
if (end != -1) {
|
if (end != -1) {
|
||||||
val tagStart = i + 1
|
val tagStart = i + 1
|
||||||
var tagEnd = end
|
var tagEnd = end
|
||||||
@@ -228,6 +225,19 @@ object DslAnnotatedParser {
|
|||||||
splitOversizedBlocks(result)
|
splitOversizedBlocks(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun tagCloseIndex(value: String, startIndex: Int): Int {
|
||||||
|
var index = startIndex
|
||||||
|
while (index < value.length) {
|
||||||
|
when {
|
||||||
|
value[index] == '\\' && index + 1 < value.length -> index += 2
|
||||||
|
value[index] == '[' -> return -1
|
||||||
|
value[index] == ']' -> return index
|
||||||
|
else -> index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
private fun splitOversizedBlocks(blocks: List<DslBlock>): List<DslBlock> {
|
private fun splitOversizedBlocks(blocks: List<DslBlock>): List<DslBlock> {
|
||||||
if (blocks.none { it.text.length > MAX_BLOCK_TEXT_LENGTH }) return blocks
|
if (blocks.none { it.text.length > MAX_BLOCK_TEXT_LENGTH }) return blocks
|
||||||
return blocks.flatMap { block ->
|
return blocks.flatMap { block ->
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ class SettingsViewModel(
|
|||||||
private var isDownloadInProgress = false
|
private var isDownloadInProgress = false
|
||||||
|
|
||||||
private var cancelRefreshPending = false
|
private var cancelRefreshPending = false
|
||||||
|
private var errorStateHandled = false
|
||||||
|
private var statusBeforeDownload: DictionaryStatus = DictionaryStatus.Unknown
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setupStateObservation()
|
setupStateObservation()
|
||||||
@@ -147,32 +149,37 @@ class SettingsViewModel(
|
|||||||
is DownloadState.Loading, is DownloadState.Extracting -> {
|
is DownloadState.Loading, is DownloadState.Extracting -> {
|
||||||
isDownloadInProgress = true
|
isDownloadInProgress = true
|
||||||
cancelRefreshPending = false
|
cancelRefreshPending = false
|
||||||
|
errorStateHandled = false
|
||||||
}
|
}
|
||||||
is DownloadState.Success -> {
|
is DownloadState.Success -> {
|
||||||
pendingSourceUrls.clear()
|
pendingSourceUrls.clear()
|
||||||
}
|
}
|
||||||
is DownloadState.Error -> {
|
is DownloadState.Error -> {
|
||||||
isDownloadInProgress = false
|
isDownloadInProgress = false
|
||||||
dictionaryStatus.value = if (dictionaryState.dictionaries.isEmpty()) {
|
if (!errorStateHandled) {
|
||||||
DictionaryStatus.Empty
|
errorStateHandled = true
|
||||||
} else {
|
dictionaryStatus.value = if (dictionaryState.dictionaries.isEmpty()) {
|
||||||
DictionaryStatus.UpToDate
|
DictionaryStatus.Empty
|
||||||
}
|
} else when (statusBeforeDownload) {
|
||||||
if (pendingSourceUrls.isNotEmpty()) {
|
DictionaryStatus.Unknown, DictionaryStatus.Checking -> DictionaryStatus.UpToDate
|
||||||
pendingSourceUrls.forEach { urlTemplate ->
|
else -> statusBeforeDownload
|
||||||
val source = dictionaryState.dictionarySources.find {
|
}
|
||||||
it.urlTemplate == urlTemplate
|
if (pendingSourceUrls.isNotEmpty()) {
|
||||||
}
|
pendingSourceUrls.forEach { urlTemplate ->
|
||||||
if (source != null) {
|
val source = dictionaryState.dictionarySources.find {
|
||||||
val hasDictionary = dictionaryState.dictionaries.any { dictionary ->
|
it.urlTemplate == urlTemplate
|
||||||
DictionarySourceFileMatcher.matches(source, dictionary)
|
|
||||||
}
|
}
|
||||||
if (!hasDictionary) {
|
if (source != null) {
|
||||||
manageDictionarySourcesUseCase.removeSource(source.id)
|
val hasDictionary = dictionaryState.dictionaries.any { dictionary ->
|
||||||
|
DictionarySourceFileMatcher.matches(source, dictionary)
|
||||||
|
}
|
||||||
|
if (!hasDictionary) {
|
||||||
|
manageDictionarySourcesUseCase.removeSource(source.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pendingSourceUrls.clear()
|
||||||
}
|
}
|
||||||
pendingSourceUrls.clear()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is DownloadState.Cancelled -> {
|
is DownloadState.Cancelled -> {
|
||||||
@@ -412,6 +419,7 @@ class SettingsViewModel(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusBeforeDownload = dictionaryStatus.value
|
||||||
dictionaryStatus.value = DictionaryStatus.Checking
|
dictionaryStatus.value = DictionaryStatus.Checking
|
||||||
isDownloadInProgress = true
|
isDownloadInProgress = true
|
||||||
|
|
||||||
@@ -435,6 +443,7 @@ class SettingsViewModel(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusBeforeDownload = dictionaryStatus.value
|
||||||
dictionaryStatus.value = DictionaryStatus.Checking
|
dictionaryStatus.value = DictionaryStatus.Checking
|
||||||
|
|
||||||
val installedSources = installedEnabledSources(
|
val installedSources = installedEnabledSources(
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- The dictionary update card now returns after a failed download
|
||||||
|
- Fixed headwords and articles with escaped characters being displayed incorrectly
|
||||||
|
- Fixed slow rendering of articles containing many bracket characters
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
- Карточка обновления словарей теперь появляется снова после неудачной загрузки
|
||||||
|
- Исправлено отображение заголовков и статей с экранированными символами
|
||||||
|
- Исправлено медленное отображение статей с большим количеством скобок
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
aboutlibraries = "15.0.4"
|
aboutlibraries = "15.0.4"
|
||||||
activity_compose = "1.13.0"
|
activity_compose = "1.13.0"
|
||||||
agp = "9.3.1"
|
agp = "9.3.1"
|
||||||
compose_bom = "2026.06.01"
|
compose_bom = "2026.08.00"
|
||||||
core = "1.19.0"
|
core = "1.19.0"
|
||||||
datastore_preferences = "1.2.1"
|
datastore_preferences = "1.2.1"
|
||||||
documentfile = "1.1.0"
|
documentfile = "1.1.0"
|
||||||
kotlin = "2.4.10"
|
kotlin = "2.4.10"
|
||||||
lifecycle_runtime_ktx = "2.11.0"
|
lifecycle_runtime_ktx = "2.11.0"
|
||||||
okhttp = "5.4.0"
|
okhttp = "5.4.0"
|
||||||
paging = "3.5.0"
|
paging = "3.5.1"
|
||||||
profileinstaller = "1.4.1"
|
profileinstaller = "1.4.1"
|
||||||
kotlinx_coroutines = "1.11.0"
|
kotlinx_coroutines = "1.11.0"
|
||||||
kotlinx_serialization = "1.11.0"
|
kotlinx_serialization = "1.11.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user