Compare commits
4
Commits
c7c080939c
...
586852073e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
586852073e | ||
|
|
10a7ff06c1 | ||
|
|
ec28d63656 | ||
|
|
c9b4a958bf |
No files matched your search
@@ -5,7 +5,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="#screenshots">Screenshots</a> •
|
<a href="#screenshots">Screenshots</a> •
|
||||||
<a href="#features">Features</a> •
|
<a href="#features">Features</a> •
|
||||||
<a href="#architecture">Architecture</a> •
|
|
||||||
<a href="#dictionary-pipeline">Pipeline</a> •
|
<a href="#dictionary-pipeline">Pipeline</a> •
|
||||||
<a href="#build">Build</a>
|
<a href="#build">Build</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -23,31 +22,17 @@ The app stores dictionaries as DictZip files and builds compact binary indexes f
|
|||||||
|
|
||||||
- Fast headword search across multiple DSL dictionaries.
|
- Fast headword search across multiple DSL dictionaries.
|
||||||
- Import local `.dsl`, `.dsl.gz`, and `.dsl.dz` dictionary files.
|
- Import local `.dsl`, `.dsl.gz`, and `.dsl.dz` dictionary files.
|
||||||
- Download dictionaries from configured URLs.
|
- Download dictionaries from configured URLs, with an optional `{DATE}` placeholder for dated releases.
|
||||||
- Automatic extraction, DictZip compression, and indexing.
|
- Automatic extraction, DictZip compression, and indexing.
|
||||||
|
- Faithful article rendering with DSL indentation, margins, and named colors.
|
||||||
- Offline search after dictionaries are indexed.
|
- Offline search after dictionaries are indexed.
|
||||||
- Random-access article loading from DictZip dictionaries.
|
- Random-access article loading from DictZip dictionaries.
|
||||||
- Background processing with progress shown in Settings and notifications.
|
- Background processing with progress shown in Settings and notifications; downloads are cancelled with a button on the progress notification.
|
||||||
- Dictionary management with enable, disable, update, and delete actions.
|
- Dictionary management with enable, disable, and update actions; deleting a dictionary is done with a swipe.
|
||||||
- Material 3 interface with dynamic color, light, dark, and system themes.
|
- Material 3 interface with dynamic color, light, dark, and system themes.
|
||||||
- English and Russian localization.
|
- English and Russian localization.
|
||||||
- About screen with open source license information.
|
- About screen with open source license information.
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
ReSearch is an Android app written in Kotlin and Jetpack Compose.
|
|
||||||
|
|
||||||
```text
|
|
||||||
com.example.research/
|
|
||||||
├── common/ # shared progress, UI, file, archive, notification utilities
|
|
||||||
├── core/ # domain models, source validation, operation results
|
|
||||||
├── data/ # DictZip, DSL parsing, indexing, paging, repositories
|
|
||||||
├── feature/ # search, dictionary download, local import workflows
|
|
||||||
└── ui/ # Compose screens, article view, settings, navigation
|
|
||||||
```
|
|
||||||
|
|
||||||
State is exposed through Kotlin Flow and collected by Compose screens. Long-running work uses coroutines and foreground services where Android requires visible progress.
|
|
||||||
|
|
||||||
## Dictionary Pipeline
|
## Dictionary Pipeline
|
||||||
|
|
||||||
ReSearch supports both downloaded and locally imported dictionaries.
|
ReSearch supports both downloaded and locally imported dictionaries.
|
||||||
@@ -94,16 +79,16 @@ 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.20
|
||||||
- Jetpack Compose (BOM 2026.08.00)
|
- Jetpack Compose (BOM 2026.09.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.1
|
- Paging 3.5.1
|
||||||
- OkHttp 5.4.0
|
- OkHttp 5.5.0
|
||||||
- kotlinx.serialization 1.11.0
|
- kotlinx.serialization 1.11.0
|
||||||
- AboutLibraries 15.0.4 metadata generation
|
- AboutLibraries 15.2.0 metadata generation
|
||||||
- Android Gradle Plugin 9.3.1
|
- Android Gradle Plugin 9.4.1
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -111,7 +96,7 @@ The main search screen remains usable as long as at least one indexed dictionary
|
|||||||
./gradlew :app:assembleRelease
|
./gradlew :app:assembleRelease
|
||||||
```
|
```
|
||||||
|
|
||||||
The release APK targets `arm64-v8a`. English and Russian resources are packaged together.
|
The release APK targets `arm64-v8a`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -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 = 8
|
versionCode = 9
|
||||||
versionName = "1.5.0"
|
versionName = "1.6.0"
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.example.research.ui.article
|
package com.example.research.ui.article
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -11,14 +12,22 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.style.BaselineShift
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.isUnspecified
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.example.research.R
|
import com.example.research.R
|
||||||
@@ -48,6 +57,72 @@ fun ArticleRoute(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val MERGED_BLOCK_MAX_CHARS = 1500
|
||||||
|
private const val MERGED_BLOCK_MAX_ITEMS = 12
|
||||||
|
|
||||||
|
private fun mergeDslBlocks(
|
||||||
|
blocks: List<DslBlock>,
|
||||||
|
density: Density,
|
||||||
|
baseStyle: TextStyle,
|
||||||
|
): List<DslBlock> {
|
||||||
|
if (blocks.size <= MERGED_BLOCK_MAX_ITEMS) return blocks
|
||||||
|
val gapShift = with(density) {
|
||||||
|
val fontPx =
|
||||||
|
if (baseStyle.fontSize.isUnspecified) 0f else baseStyle.fontSize.toPx()
|
||||||
|
if (fontPx > 0f) 8.dp.toPx() / (0.534f * fontPx) else 0.94f
|
||||||
|
}
|
||||||
|
val merged = ArrayList<DslBlock>(blocks.size / 4)
|
||||||
|
var i = 0
|
||||||
|
while (i < blocks.size) {
|
||||||
|
val builder = AnnotatedString.Builder()
|
||||||
|
val refs = ArrayList<DslAnnotatedParser.LazyRef>()
|
||||||
|
var j = i
|
||||||
|
while (j < blocks.size) {
|
||||||
|
val block = blocks[j]
|
||||||
|
repeat(block.indent) { builder.append(' ') }
|
||||||
|
val paraStart = builder.length
|
||||||
|
builder.append(block.text)
|
||||||
|
block.lazyRefs.forEach { ref ->
|
||||||
|
refs.add(
|
||||||
|
ref.copy(
|
||||||
|
startIndex = ref.startIndex + paraStart,
|
||||||
|
endIndex = ref.endIndex + paraStart
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val more = j + 1 < blocks.size &&
|
||||||
|
(j + 1 - i) < MERGED_BLOCK_MAX_ITEMS &&
|
||||||
|
builder.length < MERGED_BLOCK_MAX_CHARS
|
||||||
|
if (more && !blocks[j + 1].continuesPrevious) {
|
||||||
|
val shiftPos = builder.length
|
||||||
|
builder.append('')
|
||||||
|
builder.addStyle(
|
||||||
|
SpanStyle(baselineShift = BaselineShift(-gapShift)),
|
||||||
|
shiftPos,
|
||||||
|
shiftPos + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (more) {
|
||||||
|
builder.append('\n')
|
||||||
|
}
|
||||||
|
j++
|
||||||
|
if (!more) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merged.add(
|
||||||
|
DslBlock(
|
||||||
|
text = builder.toAnnotatedString(),
|
||||||
|
indent = 0,
|
||||||
|
lazyRefs = refs,
|
||||||
|
continuesPrevious = blocks[i].continuesPrevious
|
||||||
|
)
|
||||||
|
)
|
||||||
|
i = j
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArticleContent(
|
fun ArticleContent(
|
||||||
articleState: ArticleState?,
|
articleState: ArticleState?,
|
||||||
@@ -123,6 +198,24 @@ fun ArticleContent(
|
|||||||
val isHeavyArticle =
|
val isHeavyArticle =
|
||||||
articleState.dslContent.size > HEAVY_ARTICLE_BLOCK_COUNT
|
articleState.dslContent.size > HEAVY_ARTICLE_BLOCK_COUNT
|
||||||
|
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val darkTheme = MaterialTheme.colorScheme.background.luminance() < 0.5f
|
||||||
|
val displayBlocks = remember(
|
||||||
|
articleState.dslContent,
|
||||||
|
density,
|
||||||
|
baseTextStyle,
|
||||||
|
darkTheme
|
||||||
|
) {
|
||||||
|
val merged =
|
||||||
|
mergeDslBlocks(articleState.dslContent, density, baseTextStyle)
|
||||||
|
if (darkTheme) {
|
||||||
|
merged.map {
|
||||||
|
it.copy(text = DslAnnotatedParser.remapForDarkTheme(it.text))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
merged
|
||||||
|
}
|
||||||
|
}
|
||||||
val articleList: @Composable (Modifier) -> Unit = { listModifier ->
|
val articleList: @Composable (Modifier) -> Unit = { listModifier ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
@@ -134,15 +227,14 @@ fun ArticleContent(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = articleState.dslContent,
|
items = displayBlocks,
|
||||||
key = { index, _ ->
|
key = { index, _ ->
|
||||||
// Use stable key combining article save key and index
|
|
||||||
"$articleSaveKey:$index"
|
"$articleSaveKey:$index"
|
||||||
},
|
},
|
||||||
contentType = { _, _ -> "article_block" }
|
contentType = { _, _ -> "article_block" }
|
||||||
) { index, block ->
|
) { index, block ->
|
||||||
val followedByContinuation =
|
val followedByContinuation =
|
||||||
articleState.dslContent.getOrNull(index + 1)?.continuesPrevious == true
|
displayBlocks.getOrNull(index + 1)?.continuesPrevious == true
|
||||||
val textContent = @Composable {
|
val textContent = @Composable {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -213,6 +305,7 @@ fun ArticleTitleBar(
|
|||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
compact: Boolean = false,
|
compact: Boolean = false,
|
||||||
|
selectionReset: Int = 0,
|
||||||
) {
|
) {
|
||||||
val title = when (articleState) {
|
val title = when (articleState) {
|
||||||
is ArticleState.Loading -> articleState.title
|
is ArticleState.Loading -> articleState.title
|
||||||
@@ -258,18 +351,20 @@ fun ArticleTitleBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectionContainer {
|
key(selectionReset) {
|
||||||
Text(
|
SelectionContainer {
|
||||||
text = title,
|
Text(
|
||||||
style = MaterialTheme.typography.dictionaryTitleLarge,
|
text = title,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
style = MaterialTheme.typography.dictionaryTitleLarge,
|
||||||
maxLines = 1,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
overflow = TextOverflow.Ellipsis,
|
maxLines = 1,
|
||||||
modifier = Modifier
|
overflow = TextOverflow.Ellipsis,
|
||||||
.padding(start = if (showBackButton) Spacing.searchTextStart - Spacing.iconStart - 24.dp else 16.dp)
|
modifier = Modifier
|
||||||
.testTag("article_title")
|
.padding(start = if (showBackButton) Spacing.searchTextStart - Spacing.iconStart - 24.dp else 16.dp)
|
||||||
.semantics { testTagsAsResourceId = true }
|
.testTag("article_title")
|
||||||
)
|
.semantics { testTagsAsResourceId = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,22 +378,31 @@ internal fun ArticleScreen(
|
|||||||
onRefClick: ((String) -> Unit)? = null,
|
onRefClick: ((String) -> Unit)? = null,
|
||||||
onScrollPositionChanged: ((Int, Int) -> Unit)? = null,
|
onScrollPositionChanged: ((Int, Int) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
val titleSelectionReset = remember { mutableIntStateOf(0) }
|
||||||
topBar = {
|
Box(
|
||||||
ArticleTitleBar(
|
modifier = Modifier.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null
|
||||||
|
) { titleSelectionReset.intValue++ }
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
ArticleTitleBar(
|
||||||
|
articleState = articleState,
|
||||||
|
showBackButton = true,
|
||||||
|
onNavigateBack = onNavigateBack,
|
||||||
|
selectionReset = titleSelectionReset.intValue
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentWindowInsets = AppWindowInsets.contentWithNavigation
|
||||||
|
) { padding ->
|
||||||
|
ArticleContent(
|
||||||
articleState = articleState,
|
articleState = articleState,
|
||||||
showBackButton = true,
|
onNavigateBack = onNavigateBack,
|
||||||
onNavigateBack = onNavigateBack
|
onRefClick = onRefClick,
|
||||||
|
onScrollPositionChanged = onScrollPositionChanged,
|
||||||
|
modifier = Modifier.padding(padding)
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
contentWindowInsets = AppWindowInsets.contentWithNavigation
|
|
||||||
) { padding ->
|
|
||||||
ArticleContent(
|
|
||||||
articleState = articleState,
|
|
||||||
onNavigateBack = onNavigateBack,
|
|
||||||
onRefClick = onRefClick,
|
|
||||||
onScrollPositionChanged = onScrollPositionChanged,
|
|
||||||
modifier = Modifier.padding(padding)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,6 +411,35 @@ object DslAnnotatedParser {
|
|||||||
linkText = Color(0xFF1E88E5),
|
linkText = Color(0xFF1E88E5),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val DARK_THEME_COLORS: Map<Color, Color> = mapOf(
|
||||||
|
Color(0xFF000000) to Color(0xFFE8E6E3),
|
||||||
|
Color(0xFF000080) to Color(0xFF8FA8E0),
|
||||||
|
Color(0xFF00008B) to Color(0xFF8FA8E0),
|
||||||
|
Color(0xFF0000FF) to Color(0xFF7C9CE8),
|
||||||
|
Color(0xFF006400) to Color(0xFF66BB6A),
|
||||||
|
Color(0xFF008000) to Color(0xFF66BB6A),
|
||||||
|
Color(0xFF8B0000) to Color(0xFFE57373),
|
||||||
|
Color(0xFF800000) to Color(0xFFE57373),
|
||||||
|
Color(0xFF800080) to Color(0xFFBA68C8),
|
||||||
|
Color(0xFFA52A2A) to Color(0xFFBCAAA4),
|
||||||
|
Color(0xFF008080) to Color(0xFF4DB6AC),
|
||||||
|
Color(0xFF808000) to Color(0xFFC0CA33),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun remapForDarkTheme(text: AnnotatedString): AnnotatedString {
|
||||||
|
if (text.spanStyles.none { it.item.color in DARK_THEME_COLORS }) return text
|
||||||
|
val builder = AnnotatedString.Builder(text.text)
|
||||||
|
text.spanStyles.forEach { range ->
|
||||||
|
val style = DARK_THEME_COLORS[range.item.color]
|
||||||
|
?.let { range.item.copy(color = it) } ?: range.item
|
||||||
|
builder.addStyle(style, range.start, range.end)
|
||||||
|
}
|
||||||
|
text.paragraphStyles.forEach { range ->
|
||||||
|
builder.addStyle(range.item, range.start, range.end)
|
||||||
|
}
|
||||||
|
return builder.toAnnotatedString()
|
||||||
|
}
|
||||||
|
|
||||||
// Allow external cache clearing (e.g., when dictionaries change)
|
// Allow external cache clearing (e.g., when dictionaries change)
|
||||||
fun clearRefCache() {
|
fun clearRefCache() {
|
||||||
refCache.evictAll()
|
refCache.evictAll()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.example.research.ui.main
|
package com.example.research.ui.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -90,9 +92,14 @@ fun AdaptiveMainScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
detailPane = {
|
detailPane = {
|
||||||
|
val titleSelectionReset = remember { mutableIntStateOf(0) }
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null
|
||||||
|
) { titleSelectionReset.intValue++ }
|
||||||
.padding(bottom = 16.dp)
|
.padding(bottom = 16.dp)
|
||||||
) {
|
) {
|
||||||
ArticleTitleBar(
|
ArticleTitleBar(
|
||||||
@@ -100,6 +107,7 @@ fun AdaptiveMainScreen(
|
|||||||
showBackButton = isShowingDetail && articleState != null,
|
showBackButton = isShowingDetail && articleState != null,
|
||||||
onNavigateBack = onNavigateBackFromArticle,
|
onNavigateBack = onNavigateBackFromArticle,
|
||||||
compact = true,
|
compact = true,
|
||||||
|
selectionReset = titleSelectionReset.intValue,
|
||||||
modifier = Modifier.windowInsetsPadding(
|
modifier = Modifier.windowInsetsPadding(
|
||||||
WindowInsets.safeDrawing.only(WindowInsetsSides.Top)
|
WindowInsets.safeDrawing.only(WindowInsetsSides.Top)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
- The app starts faster
|
- The app starts faster
|
||||||
- Smooth screen transition animations
|
- Smooth screen transition animations
|
||||||
|
- Articles render DSL line breaks, indentation and named colors correctly
|
||||||
|
- Tabs and extra spaces inside articles no longer break the layout
|
||||||
|
- The article title can be copied, tapping the article clears the selection
|
||||||
|
- Source URLs accept host:port addresses
|
||||||
- Dictionary downloads retry automatically when the network switches
|
- Dictionary downloads retry automatically when the network switches
|
||||||
- Download progress notifications are more reliable
|
- Download progress notifications are more reliable
|
||||||
- Search results clear instantly when the query is erased
|
- Search results clear instantly when the query is erased
|
||||||
- Leftover files from cancelled downloads are now removed automatically
|
- Leftover files from cancelled downloads are now removed automatically
|
||||||
|
- Improved app stability
|
||||||
|
- Faster dictionary indexing
|
||||||
- Updated build tooling and documentation
|
- Updated build tooling and documentation
|
||||||
|
- Long link-heavy articles no longer freeze while scrolling
|
||||||
|
- Large search result lists render faster
|
||||||
|
- Dark article colors are readable in the dark theme
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
- Приложение запускается быстрее
|
- Приложение запускается быстрее
|
||||||
- Плавные анимации переходов между экранами
|
- Плавные анимации переходов между экранами
|
||||||
|
- Статьи корректно отображают переносы строк, отступы и цвета из DSL-разметки
|
||||||
|
- Табуляции и лишние пробелы в статьях больше не ломают вёрстку
|
||||||
|
- Заголовок статьи можно скопировать, тап по статье сбрасывает выделение
|
||||||
|
- Адреса источников принимают формат host:port
|
||||||
- Загрузка словарей автоматически повторяется при переключении сети
|
- Загрузка словарей автоматически повторяется при переключении сети
|
||||||
- Уведомления о прогрессе загрузки стали надёжнее
|
- Уведомления о прогрессе загрузки стали надёжнее
|
||||||
- Результаты поиска очищаются мгновенно при стирании запроса
|
- Результаты поиска очищаются мгновенно при стирании запроса
|
||||||
- Остаточные файлы после отменённых загрузок удаляются автоматически
|
- Остаточные файлы после отменённых загрузок удаляются автоматически
|
||||||
|
- Повышена стабильность приложения
|
||||||
|
- Ускорена индексация словарей
|
||||||
- Обновлены тулинг сборки и документация
|
- Обновлены тулинг сборки и документация
|
||||||
|
- Длинные статьи со ссылками больше не подвисают при прокрутке
|
||||||
|
- Большие списки результатов поиска отображаются быстрее
|
||||||
|
- Тёмные цвета статей читаемы в тёмной теме
|
||||||
Reference in new issue
Block a user