2.10.1: Storage cleanup tier 2+3 — flat schema, gzip-compressed definitions, in-memory history
v2.10.1 — Storage cleanup + reader & dictionary UX overhaul
This is a large release. Two themes dominate: a fundamental rework of how dictionary data is stored on disk (cutting install size by more than half), and a broad set of UX improvements to the reader and dictionary lookup flows that have been accumulating for several weeks.
It will delete your data: all installed dictionaries!!!
This is intentional, the dictionaries need to be imported cleanly — to be saved in the new format. On the first launch after installing 2.10.1 will perform a one-time migration — it wipes the existing Isar dictionary database and the app_flutter/dictionaryResources/ directory, then prompts you to re-import your dictionaries. Your books, cards, Anki mappings, reader history, and preferences are not touched. Plan for ~5–15 minutes of re-import time depending on how many dictionaries you use.
🗜 Storage cleanup (tier 2 + 3)
Building on v2.9.18's tier-1 work (dropping raw bank JSONs, excluding WebView caches from backup), this release completes the refactor of dictionary storage that v2.9.18 only began.
What changed under the hood
DictionaryHeadingremoved from Isar. Headings are now reconstructed in memory on demand from the flattened entry table. The Isar-sidelinks:overhead — which turned out to be a significant contributor to database bloat — is gone.DictionaryEntryflattened. What used to be a tree of linked objects is now a single indexed row per entry, carryingterm,reading,dictionaryId,termLength,popularity,entryTagsRaw,headingTagsRaw, and the definitions blob inline.- Definitions are gzip-compressed. Per-entry payloads ≥64 bytes are stored as gzipped bytes with a single magic-byte sentinel (
0x47) for backward compatibility. Shorter payloads stay uncompressed to avoid wasting CPU. The codec lives atlib/src/dictionary/definition_codec.dart, wrapped in a Future return type so a future swap to async zstd (blocked on newer Flutter) needs no API changes. DictionaryPitchandDictionaryFrequencyflattened with compositeterm+dictionaryIdindices for fast lookup.DictionarySearchResultremoved from Isar. Search history moves into a bounded in-memoryLinkedHashMapLRU cache. This also means search history is no longer persisted across app restarts — a deliberate trade for the storage savings and for not writing to Isar on every lookup.
Measured impact (clean install, same dictionary set, same device)
| Metric | 2.9.18 | 2.10.1 | Change |
|---|---|---|---|
Isar files/ directory |
995 MB | 505 MB | −49% |
| Total app install size | 1.50 GB | 581 MB | −62% |
| Search latency (average) | 158 ms | 263 ms | +66% |
| Search latency (p95) | 424 ms | 581 ms | +37% |
| Search latency (p99) | 445 ms | 717 ms | +61% |
The latency regression is the cost of gzip-decoding definitions inline during the post-search result-building batch. It is on the list for the next release — the plan is to defer decode until the result is actually rendered (lazy per-entry), rather than decoding everything up front. See Known issues below.
Why gzip and not zstd?
Zstd would give both smaller payloads and faster decode. The Dart zstd packages that are both stable and well-maintained require Flutter ≥3.22; this project is on Flutter 3.13.5 for unrelated reasons (AGP + Kotlin stdlib pinning). Swapping to zstd is a one-line change in DefinitionCodec and will land in a later release alongside the Flutter bump.
📖 Reader improvements
Split-screen translation book
The split-screen reader (primary book + translation book side-by-side, introduced earlier in 2.9.x) has received a round of fixes and per-book controls:
- Independent font-size gestures. Vertical drag on the left edge now routes to the correct book based on where on the edge you started the drag — upper half adjusts the primary book, lower half the translation book. In single-book mode the full edge still controls the only book.
- Independent appearance settings. Each book in split mode now has its own persisted font family, weight, margins, line/paragraph spacing, and colors. The bottom-toolbar menu now shows two entries in split mode — Primary book appearance and Translation book appearance — instead of one shared dialog.
- Secondary URL sanitization. A trailing
#fragment on the translation book URL no longer breaks the per-book settings key.
Reader appearance is now a page
The "Reader appearance" form (font family/weight, colors, margins, spacing) was previously a showDialog overlay. It's now pushed as a full-screen page with its own Scaffold and AppBar.
This was forced by a focus-handling bug that could not be resolved within the dialog approach: tapping any text field in the dialog would silently fail to request the IME, and only a specific sequence of interactions (opening the Font Weight dropdown and dismissing it) would "wake up" the focus scope enough for text fields to accept input. Converting to a pushed route, combined with an explicit FocusManager.instance.primaryFocus?.unfocus() before the push and a post-frame unfocus() in the page's initState, resolves it fully — text fields accept input on the first tap.
Font picker with bundled font presets
The "Font family (serif)" and "Font family (sans-serif)" fields gained a third quick-pick icon (beside the existing "load TTF/OTF" and "reset" buttons) that opens a dialog listing the fonts bundled with the embedded ttu reader — Genei Koburi Mincho v5, Klee One, Klee One SemiBold, Noto Sans JP, Noto Serif JP, and Shippori Mincho. Tapping a preset drops its exact CSS name into the field without needing to type it.
Yellow-on-black reader menu
The reader bottom-toolbar menu bottom sheet previously rendered with Material light-theme defaults (white text on grey surface) in contexts where the parent theme was light. It now force-themes itself to yellow-on-black to match the rest of the reader UI regardless of parent theme — except for destructive items, which stay red.
clearReaderCaches fix for Huawei devices
The previous cache-clearing logic only touched app_webview/. Huawei handsets use app_hws_webview/ instead. It now walks both paths identically, and — critically — only targets the named cache subdirectories (Cache, Code Cache, GPU Cache, GPUCache, CacheStorage). IndexedDB and other persistent WebView data the reader needs are preserved on both stock Android and Huawei.
🔍 Dictionary lookup
Full-screen-on-tap mode (new, optional)
Some workflows are best served by the existing half-screen popup that appears over the reader when you tap a word. Others — particularly deep-dive research, where you want the full search bar, the segmentation controls, and the creator button immediately available — want the full recursive dictionary page right away.
A new toggle, Open dictionary full-screen on tap, lives in Dictionary settings (home screen → Dictionary tab → gear icon, right below Auto-search). When enabled:
- Tapping a word in the reader, player, or browser skips the half-screen popup entirely and pushes the full-screen recursive dictionary page.
- The search term is truncated to the actually-matched word (via the language's
getFinalHighlightLength) — tapping into the middle of a Japanese sentence gives you just the deinflected word, not the whole rest of the sentence. Same logic for space-delimited languages. - The "show more" expansion path that exists inside the half-screen popup is unaffected.
Quick-toggle icons in both views
You don't have to walk back to settings every time you want to flip the preference:
- In the half-screen popup, a new
⛶(fullscreen) icon sits next to the existing edit-pencil. Tapping it flips the preference to on and re-opens the current search in full-screen immediately. - In the full-screen page's top toolbar, a new
⛶(fullscreen-exit) icon sits left of the segment/creator/search actions. Tapping it flips the preference to off and pops the page back to wherever you came from.
In short: the two views are reversible from themselves with a single tap.
🐛 Bug fixes
- Huawei IndexedDB wipe. The reader-cache cleanup was wiping IndexedDB on Huawei because it was walking
app_webview/*blindly instead of targeting cache subdirectories. Fixed by restricting cleanup to named cache directories and walking bothapp_webview/andapp_hws_webview/. - Reader appearance text-field focus. Text fields in the reader appearance form didn't accept taps until a dropdown was opened and dismissed first. Fixed by converting the form from a dialog to a pushed page plus explicit focus-scope management.
- Split-screen edge gestures. In split-screen mode, the left-edge font-size drag was applying to the primary book regardless of where on the edge the drag started. Now routes correctly to whichever book's half of the edge was touched.
- Split-screen per-book settings. In split-screen mode, opening the appearance dialog was only editing the primary book's settings. Each book now has its own persisted settings accessible via two menu entries.
- Reader menu color palette. The reader bottom-toolbar menu could render as white-on-grey in light-theme contexts. Now force-themed yellow-on-black to match the rest of the reader UI.
⚠ Known issues
Deliberately shipped as-is; tracked for a follow-up release:
- Search latency regression (~+60%). Gzip decode of definitions happens inline during the post-search result-building batch. The fix is lazy per-entry decode at render time; this is scheduled for the next release.
- Back button in full-screen dictionary requires two presses. The embedded
FloatingSearchBarconsumes the first Back to collapse itself before the route's pop handler is reached. Attempted fixes using bothWillPopScopeandBackButtonListenereither did not work or caused other regressions; this needs a deeper refactor and was scoped out of this release. - Back button in reader with half-screen dictionary shown. Currently prompts "Exit media?" as if no dictionary were open. The natural behaviour would be for Back to dismiss the popup only. Deferred for the same reason as above.
🛠 For contributors
A few orientation notes if you're building or patching on top of this release:
- Codec location.
lib/src/dictionary/definition_codec.dart. The magic byte sentinel is0x47('G'); the 64-byte threshold is there because below that the gzip header outweighs the payload savings on typical definitions. - Migration gate. Controlled by the Hive flag
dictionary_schema_v2_initialized. Only Isar andapp_flutter/dictionaryResources/are wiped; user content (books, cards, mappings, preferences) is untouched. - Search history persistence. Intentionally in-memory only as of this release. See the
LinkedHashMapLRU in the recursive dictionary result pipeline. - AGP / Flutter version pinning. Still on AGP 7.1.2 and Flutter 3.13.5. The zstd swap, the async-codec path, and replacements for two abandoned plugins (
external_app_launcher,flutter_exit_app) are all blocked on the Flutter bump, which is blocked on the AGP bump. That's the next major plumbing task.