5 Commits
Author SHA1 Message Date
OneWay c7c080939c Release 1.6.0
- The app starts faster
- 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
- Download progress notifications are more reliable
- Search results clear instantly when the query is erased
- Leftover files from cancelled downloads are now removed automatically
- Improved app stability
- Faster dictionary indexing
- 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
2026-09-24 13:00:00 +08:00
OneWay 2c8668dab3 build: update core to 1.19.1 2026-09-24 12:00:00 +08:00
OneWay e4e50db08e fix(ui): clear article title selection on any tap
The title SelectionContainer covered only the text, so a tap on the empty bar area or the article body never cleared the highlight. A tap anywhere on the article screen now recreates the container and drops the selection.
2026-09-24 10:00:00 +08:00
OneWay 98ec38f272 fix(ui): remap dark article colors in dark theme
Colors set by dictionaries such as navy, dark red and brown are nearly invisible on the dark background. Swap the dark named colors for lighter tones when rendering in the dark theme; the light theme keeps the original colors.
2026-09-24 09:00:00 +08:00
OneWay 6040ca8db5 fix(ui): merge article blocks to fix scroll jank
Link-heavy articles split every line into its own list item, so a
fast fling composed a dozen items in one frame and froze. Merge small
blocks into multi-paragraph items: indent via em space and a uniform
line pitch via LineHeightStyle(Proportional, Trim.Both).
2026-09-23 09:30:00 +08:00
2 changed files with 25 additions and 53 deletions

No files matched your search

@@ -14,7 +14,6 @@ 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.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
@@ -22,12 +21,9 @@ 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.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.text.style.LineHeightStyle
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
@@ -60,17 +56,8 @@ fun ArticleRoute(
private const val MERGED_BLOCK_MAX_CHARS = 1500 private const val MERGED_BLOCK_MAX_CHARS = 1500
private const val MERGED_BLOCK_MAX_ITEMS = 12 private const val MERGED_BLOCK_MAX_ITEMS = 12
private fun mergeDslBlocks( private fun mergeDslBlocks(blocks: List<DslBlock>): List<DslBlock> {
blocks: List<DslBlock>,
density: Density,
baseStyle: TextStyle,
): List<DslBlock> {
if (blocks.size <= MERGED_BLOCK_MAX_ITEMS) return blocks 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) val merged = ArrayList<DslBlock>(blocks.size / 4)
var i = 0 var i = 0
while (i < blocks.size) { while (i < blocks.size) {
@@ -93,15 +80,6 @@ private fun mergeDslBlocks(
val more = j + 1 < blocks.size && val more = j + 1 < blocks.size &&
(j + 1 - i) < MERGED_BLOCK_MAX_ITEMS && (j + 1 - i) < MERGED_BLOCK_MAX_ITEMS &&
builder.length < MERGED_BLOCK_MAX_CHARS 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) { if (more) {
builder.append('\n') builder.append('\n')
} }
@@ -138,7 +116,11 @@ fun ArticleContent(
is ArticleState.Success -> { is ArticleState.Success -> {
val articleSaveKey = "${articleState.dictionaryPath}:${articleState.title}" val articleSaveKey = "${articleState.dictionaryPath}:${articleState.title}"
val baseTextStyle = MaterialTheme.typography.bodyLarge.copy( val baseTextStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Proportional,
trim = LineHeightStyle.Trim.Both,
),
) )
val indentStep = 16.dp val indentStep = 16.dp
@@ -198,16 +180,14 @@ 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 darkTheme = MaterialTheme.colorScheme.background.luminance() < 0.5f
val displayBlocks = remember( val displayBlocks = remember(
articleState.dslContent, articleState.dslContent,
density,
baseTextStyle, baseTextStyle,
darkTheme darkTheme
) { ) {
val merged = val merged =
mergeDslBlocks(articleState.dslContent, density, baseTextStyle) mergeDslBlocks(articleState.dslContent)
if (darkTheme) { if (darkTheme) {
merged.map { merged.map {
it.copy(text = DslAnnotatedParser.remapForDarkTheme(it.text)) it.copy(text = DslAnnotatedParser.remapForDarkTheme(it.text))
@@ -233,32 +213,24 @@ fun ArticleContent(
}, },
contentType = { _, _ -> "article_block" } contentType = { _, _ -> "article_block" }
) { index, block -> ) { index, block ->
val followedByContinuation = Box(
displayBlocks.getOrNull(index + 1)?.continuesPrevious == true modifier = Modifier
val textContent = @Composable { .padding(start = Spacing.contentStart + indentStep * block.indent)
Box( ) {
ArticleLinkText(
block = block,
linksEnabled = true,
preferDirectHitTesting = isHeavyArticle,
style = baseTextStyle,
onRefClick = refClickHandler,
onTextLayout = {},
modifier = Modifier modifier = Modifier
.padding( .testTag("article_block_$index")
start = Spacing.contentStart + indentStep * block.indent .semantics {
) testTagsAsResourceId = true
.padding(bottom = if (followedByContinuation) 0.dp else 8.dp) }
) { )
ArticleLinkText(
block = block,
linksEnabled = true,
preferDirectHitTesting = isHeavyArticle,
style = baseTextStyle,
onRefClick = refClickHandler,
onTextLayout = {},
modifier = Modifier
.testTag("article_block_$index")
.semantics {
testTagsAsResourceId = true
}
)
}
} }
textContent()
} }
} }
} }
+1 -1
View File
@@ -3,7 +3,7 @@ aboutlibraries = "15.2.0"
activity_compose = "1.13.0" activity_compose = "1.13.0"
agp = "9.4.1" agp = "9.4.1"
compose_bom = "2026.09.00" compose_bom = "2026.09.00"
core = "1.19.0" core = "1.19.1"
datastore_preferences = "1.2.1" datastore_preferences = "1.2.1"
documentfile = "1.1.0" documentfile = "1.1.0"
kotlin = "2.4.20" kotlin = "2.4.20"