A failed update left DownloadState.Error in the state flow forever:
settings replayed it as a snackbar on every recomposition and search
stayed locked until the next app start.
A download error stayed in the state flow forever: the dictionaries
section replayed it as a snackbar on every recomposition and screens
created later picked it up long after the failure, even while a newer
download was already running. Error states now decay to idle after the
same grace period as success, unless a newer state has replaced them.
Errors of downloads this screen did not start no longer touch the
status; when the prior status is unknown or still unresolved, today's
persisted status is restored or a fresh evaluation runs instead of
fabricating UpToDate.
The search pipeline ranked and deduplicated matches twice: inside the
searcher per dictionary and again after merging across dictionaries.
The extra pass ran before the required ordering and duplicated the
dedup work. The searcher now returns article-deduplicated matches and
the paging source applies a single ordering pass: display length
ascending, then natural numeric-aware order. Paging configuration
knobs that a full-page source cannot use are dropped.
Persist NeedsUpdate with its source ids and restore today's persisted
status — UpToDate and NeedsUpdate alike — through one shared helper used
by both startup paths, falling back to a fresh evaluation when the
persisted status can no longer be trusted (stale date, removed sources,
or the update already installed).
- Search loads all prefix matches; broad prefix searches are much faster
- Fixed indexing progress freezing on large dictionaries
- Large articles open faster
- Numbered results sort in numeric order
- UI improvements
- Updated libraries and build tooling
Report which sources have a newer file instead of a single flag, and
show a Material badge at the top-end corner of each such dictionary's
icon. The section icon carries the badge while the section is
collapsed.
With the search bar at the bottom, offset the results list so the
first result sits on the article title row and the back button
lands under the finger, in single- and two-pane layouts alike.
The source check, folder access and request ran before the first
byte with no visible feedback. Enter the loading state on tap and
show an indeterminate wavy indicator until download progress is
known. File import is unchanged. An early cancel deletes only files
created by the current download, so installed dictionaries survive.
Needed for the expressive wavy progress indicator. Move the sheet
states to rememberBottomSheetState, drop opt-ins for APIs that are
now stable, and read the window height from LocalWindowInfo.
Rows were read via peek(), which gives Paging no access hints, so
prefix matches beyond the first page never loaded. Drop the head/tail
paging: a query reads the whole prefix range of every active
dictionary in one scan, merges and ranks it, and hands Paging a single
page. The append error row and its strings go away; the prefix scan
reuses search() with substring matches disabled. FileSource reads
through a buffer, cutting per-entry syscall overhead.
The parse phase estimated progress from a line-count formula that hit
its ceiling early, so the percentage froze on large dictionaries.
Estimate it from the position in the decompressed stream against the
uncompressed size instead.
A fast download reposted the ongoing notification several times per
second. Publish stage changes immediately and rate-limit intermediate
updates to the latest snapshot.
Cover the startup path, per-keystroke search with index reads and
ranking, the dictzip article read, block merge and text warm-up,
and dictionary indexing; they previously ran as JIT-warmed or
interpreted code.
The screen merges small parser blocks into larger render blocks,
while the warm-up measured the unmerged parser blocks. Merge once in
the view model and keep the merged blocks in the article state, so
the screen and the warm-up share exactly what will be drawn.
The warm-up ran only above a block-count threshold, so an article
that parses into one large block never qualified. Trigger on the
total character count as well.
The final ranking tie-break compared headwords lexicographically,
so entries numbered with digits or CJK numerals sorted by text
instead of by number. Compare runs of digits or numeral characters
by their numeric value, saturating on very long runs.
- Added search history
- The search bar can be placed at the top or bottom of the screen
- Fixed headwords with stress marks displaying truncated in search results
- Improved download, indexing and navigation stability
- The About screen opens faster
- Updated build tooling and code cleanup
HistoryEntryClicked in wide mode only called SearchAndLoadArticle,
which never updates searchQuery, so the field stayed empty and the
results pager took the query.isBlank() branch with PagingData.empty().
Now dispatch onSearchQueryChanged first, then SearchAndLoadArticle.
The display form was truncated at the first formatting tag, so headwords
with a mid-word stress mark displayed as their prefix only. Build the
display from the full text with tags stripped and render stress marks as
a combining acute accent. Bump the index version so existing indexes
rebuild.
Drop the v prefix so the version shows as a plain version name, and
align it by baseline with the app name instead of the bottom edge of
the headline line box.
Pasting several URLs separated by spaces, newlines or commas into one
field now distributes them across separate fields when every token is a
valid source template. Previously the joined text passed validation as a
single URL and failed at download time.
Remove unused imports and an unused spacing constant, drop redundant if
checks and redundant opt-in markers, replace loop variables with
destructuring, convert a string concatenation to a template, use
Configuration.screenHeightDp instead of LocalWindowInfo.containerSize, and
opt in to ExperimentalSerializationApi for decodeFromStream.
Background metadata loading rewrote the dictionaries flow several times
after startup, and every emission recreated the Pager: the results list
visibly collapsed and refilled. Distinct on the index paths that paging
actually uses, and seed the state synchronously instead of an empty list.
A second activity instance (PROCESS_TEXT) ran the full directory scan
again, and the main screen stayed blank until the scan finished. The
repository is a process-wide singleton: when it already holds
dictionaries, mark them up to date and load preferences immediately.
handleExternalSearch auto-opened the article on an exact match, so the main
screen with the search bar flashed before the article covered it. Only set
the query; the article now opens from the results list.
TextStyle.copy() returned a new instance on every recomposition, so the
remember() cache for the rendered blocks never hit. A text selection re-ran
block merging and recreated all article text nodes, rebuilding their
StaticLayouts repeatedly. Hoist the theme values and remember the style.
Stream the bundled license catalog straight into the JSON decoder
instead of buffering the whole resource as a string first, halving
parse time and peak allocation when the About screen loads.
A racing SettingsViewModel whose scan short-circuits with an in-progress
error persisted hadNoDictionaries from a still-empty list, forcing the
Settings screen on the next cold start despite existing dictionaries.
With many fields, the Add Source sheet could keep re-triggering its
settle animation: animated window insets reached the container while
its height followed the content. Keep only the static status bar
inset on the sheet container; the content already pads itself for
the navigation bar and IME.
Record queries on explicit search and keep up to 100 entries.
Show them in a bottom sheet opened from the search bar, with
per-entry removal and clear-all.
Top-align the title row in compact layouts, align the back button
with the first title line using the shared back button, make the
title start padding depend on whether the back button is shown,
and drop the spacing constant the old arrow buttons used.
Read the theme, locale, empty-library seed and section expansion
once in the application and collect them as state, instead of
blocking the UI thread with synchronous reads before setContent.
On restart the update-available state was restored purely from the
saved date. Also compare the installed payload file names against
dated sources, and re-check when they no longer match.
An already-consumed down change is part of another gesture, not a
reason to cancel the link press. Only treat movement past touch
slop as cancellation.
A just-deleted index file can still be held by the system, so
createNewFile() may fail right after deletion. Retry a few times
with a short delay, waiting for the old file to disappear first.
The platform blocks cleartext traffic by default, so an http:// source
passed validation but could never download and failed with a misleading
network error. The validator now rejects http:// upfront.
areDictionariesUpToDate() mapped every exception to NeedsUpdate, and
the post-update recheck ran after the first indexed source of a batch,
before later sources finished downloading; the interim NeedsUpdate
persisted for the day. Treat unexpected errors as up to date, rethrow
cancellation, and recheck once when the download batch completes.
Unnecessary safe call on a non-null File receiver; unused catch parameters renamed to _; constructor parameter used only in init no longer held as a property; deprecated Configuration.screenHeightDp replaced with LocalWindowInfo.containerSize; redundant filterNotNull on Paging items; deprecated invisibleToUser() replaced with hideFromAccessibility(); redundant alias variable inlined; redundant else in an exhaustive when removed; paging snapshot read moved into the SideEffect that consumes it.
On back navigation the article state was cleared before the exit animation ran, so the panel animated out empty except for the back arrow. The overlay now retains the last shown article state until the exit transition completes.