Theorem v1.0.7
Pre-releaseFixed
-
Production Tauri: book shows "1-2 lines and white" — Two interrelated fixes:
1. Invalid iframe sandbox flag — The reader iframe had
allow-clipboard-writein thesandboxattribute, which is not a valid sandbox token (it's a Permissions-Policy directive). WebKitGTK (Tauri's production WebView on Linux) rejects the entire sandbox attribute when it encounters an invalid flag, dropping the iframe into maximum sandboxing. This broke foliate-js page-positioning logic that relies onallow-same-originto read iframe content dimensions and set up CSS columns. Reverted toallow-same-origin allow-scripts.2. CSP
'unsafe-inline'ignored due to Tauri style nonce injection — An inline<style>tag inindex.html(loading spinner, added in v1.0.7) triggered Tauri's CSP processing pipeline: the build system injects__TAURI_STYLE_NONCE__into<style>elements, and at runtime replaces it with a real nonce + adds'nonce-xxx'tostyle-src. Per CSP spec, when a nonce source is present in a directive,'unsafe-inline'is ignored — so all dynamic inline styles (including foliate-js's<style>elements inside the reader iframe that set column-width, height, padding) were blocked. The loading spinner CSS is now insrc/index.css(loaded via<link>from'self'), eliminating the inline<style>element and keeping'unsafe-inline'intact.3. Tauri IPC blocked by CSP — The CSP
connect-srcwas missingipc:andhttp://ipc.localhost, causing Tauri's custom IPC protocol to fail and fall back to postMessage. Addedipc: http://ipc.localhosttoconnect-src. -
Production builds must not ship with devtools — Removed
devtoolsfeature flag fromtauridependency inCargo.toml.
Added
- On-demand download progress — Reader shows a real progress bar with file size (e.g.,
12.5 MB / 45.3 MB (28%)) instead of an indeterminate spinner when downloading synced books. Progress events emitted from Rust in 1MB chunks, throttled to percentage changes. - Memorized reader state — Reader now persists
downloadingBookIdanddownloadProgressacross renders. "Try Again" button on all error screens.
Changed
- File transfer: streaming writes with no IPC —
download_book_filewrites received data directly tobook-cache/{id}.bookfrom Rust in 1MB chunks. Zero bytes pass through the Tauri IPC bridge. Previously, the entire file was returned via IPC as JSONnumber[], causing 360MB+ allocations that crashed Android with OOM (heap limit 256MB). - File transfer: 120s timeouts — All I/O operations (connect, open_bi, read_line, read_exact) now have 120s timeouts. Previously,
request_book_filecould hang indefinitely on unreachable peers. - File transfer: SQLite fallback —
FileTransferHandlerfalls back to reading from SQLitebookstable when the file is not inbook-cache. Previously only served frombook-cache, meaning locally-imported books (stored in SQLite) were unavailable to peers. - Sync: gossip-based bidirectional auto-sync —
NeighborUpevents now properly triggerrunDeviceSyncon the peer viadocs-peer-online. Theiroh_node_idfield was missing fromPairedDeviceInfo, making the peer-matchingd.irohNodeId === nodeIdalways fail. Auto-sync when a peer comes online was completely dead. - Sync: settle threshold fixed —
signalSettle()checks>= 2instead of>= 3. Only 2 events (docs-pending-content-ready+docs-sync-finished) ever call signalSettle, so the threshold of 3 was never reached — every sync round waited the full 30s timeout. - Sync: event subscription on doc recovery —
subscribe_doc_eventsis now called after re-importing a sync doc from ticket indocs_sync_now. Previously, doc subscriptions were lost during recovery, causing no live events to flow. - Sync:
_bridgePausedguard for all stores — Vocabulary, RSS, and settings store subscriptions now check_bridgePausedduring provisioning, preventing writes during initial data sync. - Reader: shortcuts useEffect moved before early returns — The keyboard shortcuts registration useEffect is now called before all early-return guards (downloading, error, syncedWithoutFile). Previously, the shortcuts useEffect was duplicated — one copy before and one after the early returns — causing "Rendered more hooks than during the previous render" errors when transitioning from the download screen to the reader.
- Reader: polling loop timeout — The
while (syncedWithoutFile)polling loop now has a 120-second timeout. Previously it hung forever if the download failed. - Annotations page: fully responsive card view — Touch swipe navigation, icon-based mobile toolbars, clipped dot indicators,
touch-manipulationon all interactive elements.
Performance
- Memory: eliminate unnecessary Blob copy — When the MIME type already matches, use the blob directly instead of
new Blob([blob], type), which duplicated the entire book in JS memory. Saves one full copy (50-100MB for large EPUBs). - Memory: call EPUB.destroy() on engine teardown —
FoliateEngine.destroy()now callsEPUB.destroy(), which revokes all blob URLs created for sections, CSS, images, and fonts. Previously, blob URLs accumulated until page refresh. - Memory: unload previous fixed-layout spreads —
goToSpread()now callsunload()on the previous spread's sections. Fixed-layout books (children's books, comics) were leaking all visited spreads' blob URLs. - Memory: cover extraction early-return before file re-read — The
hasRealCover && coverExtractionDonecheck now runs BEFOREgetBookData(), avoiding a full re-read of the book from storage when cover is already extracted. - Memory: metadata-only EPUB prefetch — Rust
prefetch_zip_metadatano longer pre-decodes all EPUB section HTML files (up to 100 sections, ~3-6MB of JS strings). Only metadata files (container.xml, OPF, NAV, NCX, encryption) are prefetched. Sections load lazily via zip.js. - Binary size: removed unused Rust dependencies —
read_zip_entrywrapper removed after section prefetch elimination.
Fixed (Android)
- ndk_context restore — Re-added JNI
initNdkContextandndk-contextdependency. iroh'snetwatch→netdev→ndk_context::android_context()panics if the Android JVM/context are not initialized before any iroh networking operations run. Without this, the app crashed on Android 15 at startup. - ndk_context init after super.onCreate —
initNdkContext(applicationContext)moved aftersuper.onCreate()to ensureapplicationContextis non-null when the JNI call fires. - CBR/RAR desktop-only —
unrar-ng(bundled UnRAR C++ source) useslutimes()which was removed from Android's bionic libc in NDK 28. CBR conversion works on desktop; Android returns a clear "not supported" error to avoid compilation failure. - accept loop gate — When no paired devices exist, the accept loop registers only pairing + file_transfer handlers, skipping gossip/docs creation. This prevents the
ndk_context::android_context()panic on fresh Android installs with no paired devices. - Top-bar single-device sync — Sync button now syncs only the first paired device instead of iterating all, preventing sequential timeouts on offline peers.
- Mobile safe-area padding — All pages (Settings, Statistics, Bookmarks, Annotations, Feeds, Vocabulary, Shelves) now use
pb-[calc(var(--spacing-2xl)+env(safe-area-inset-bottom))]to clear the bottom navigation on notched devices.
Fixed (Sync)
- Duplicate pairing by iroh_node_id — The dedup check only matched by
fingerprint, which is empty on Android (and often empty on desktop). Addediroh_node_id-based dedup in bothhandle_pair_reqandsubmit_pairing_code. - Frontend duplicate device list —
DeviceSync.tsxfiltered bydeviceIdbefore appending, preventing duplicate entries in the local React state.
Fixed (General)
- Stuck "Downloading Book" screen — Added 120s timeout, clears
downloadingBookIdon timeout, shows "Book download timed out" error with retry. - Broken reader state after download failure — New guard renders "Book File Not Available" screen when
syncedWithoutFileis true but not currently downloading, instead of showing a broken reader view. - "Rendered more hooks than during the previous render" — Removed duplicate shortcuts useEffect that was left after the move. When transitioning from "Downloading" screen (early return fired, 1 shortcuts hook) to reader (no early return, 2 shortcuts hooks), React detected the mismatch and threw.