Skip to content

Theorem 1.6.0

Latest

Choose a tag to compare

@github-actions github-actions released this 26 Oct 02:18
· 89 commits to main since this release

Smart Shelves & Sub-Shelves

  • Smart Shelves (#132) — Introduced dynamic, automated smart shelves powered by native filter rules (reading status, tags, series, authors, and formats). Smart shelves dynamically recompute membership on library changes without manual bookkeeping, preserving upstream layout and custom shelf sorting.
  • Hierarchical Sub-Shelves (#138) — Added one-level nested sub-shelves to organize large libraries hierarchically. Create and navigate sub-shelves with breadcrumbs, dedicated parent-child navigation, and unified shelf action controls.

Library & Organization

  • Preserve Source Folder Structure (#133) — Ingesting folders now preserves folder hierarchy with an integrated Source Folder browser chip strip. Users can toggle between preserving directory hierarchy or flat importing, filter the library view by source folder, and keep library organizations in sync with external storage trees.
  • Bulk Selection UX Overhaul (#134) — Completely redesigned filtered bulk selection. "Select All" now selects all filtered items across virtualized rows; selections automatically clear on filter shifts to prevent accidental batch operations. Features a solid full-width bottom toolbar with explicit icon+label buttons, mobile-optimized single-row control layout, Esc key dismissal, and 1-click Undo toast protection on batch mark read/unread.
  • Dynamic Read/Unread Toggles — Replaced dual "Mark Read" and "Mark Unread" buttons with an intelligent, dynamic toggle across both the bulk selection toolbar and right-click book context menus. If all selected items are completed, the action displays "Mark Unread"; otherwise "Mark Read".
  • Bookmarks Are a Workbench Tab — Bookmarks now open inside the Workbench like Highlights, Notes and Words: same title, same tab row with the source and sort menus beside it, and no jump to a separate Bookmarks page. Ctrl+7 still opens the Bookmarks tab directly.
  • Shelves Layout & Header Unification — Standardized Shelves page layout, header spacing, and toolbar button styling (TOOLBAR_BUTTON_PRIMARY, TOOLBAR_BUTTON_BASE, TOOLBAR_ICON_BUTTON) with Library viewport standards. Action buttons, search, and back navigation remain permanently accessible across empty and populated states.

Reader & Comics

  • Continuous Strip Comic Reading (#135) — Added an optional continuous vertical strip view for CBZ/CBR comic books with bounded DOM image virtualization and adaptive preloading, eliminating page-turn friction for webtoons and vertical-scrolling comics.
  • Streamed Native Comic Ingestion (#136) — Streaming archive decompression in native Rust avoids whole-archive memory buffers, dramatically speeding up multi-gigabyte comic imports and preserving CBR binary responses.

Shell, Stability & Remote Announcements

  • Log files & Export Logs — Theorem now keeps a rotating log file (theorem.log in the platform log folder, at most ~10 MB) with native errors, every Rust panic (location and message, quoted text redacted) and errors caught by the app's error screens. Settings → About → Export Logs saves them as a zip with the app version and platform to attach to a bug report; the home folder is written as ~, and no books, notes or device keys are included.
  • No web inspector in release builds — Shipped builds no longer include the webview developer tools; pnpm dev:* and debug Android builds still have them (cargo feature devtools).
  • Fewer native crashes — A panic on one sync or dictionary thread no longer leaves its lock poisoned and crashes every later sync and lookup; the app recovers the lock and carries on. A system without a usable TLS backend now gets a "could not start the network client" error on feeds, articles and catalogs instead of a crash.
  • Android Crash Prevention & Memory Management (#137) — Resolved startup crash loops and out-of-memory crashes on Android by deduplicating native runtime initialization, adding aggressive webview memory trimming on backgrounding, and safely validating book files before reopening last-read state.
  • Remote Announcements Banner — Added a non-intrusive, zero-HTML remote announcement system (src/ui/AnnouncementBar.tsx) delivering project updates, advisories, and release notes to all installations from a dedicated edge Cloudflare Worker (announcements.fundaments.work). Announcements are fetched asynchronously with a 24-hour local cache, fail-open resiliency, persistent local dismissal, and responsive mobile/desktop layout with severity badges (Notice, Advisory, Alert).
  • Zero-HTML AST Parser & Linkifier — Pure AST linkification (src/core/lib/announcements.ts) that strictly validates https:// URLs without credentials, trims balanced punctuation and parentheses, and converts **bold** markdown syntax into React elements without touching dangerouslySetInnerHTML.
  • Reader Isolation — Announcement banners are strictly isolated from the reading view (ReaderPage), preserving a distraction-free, zero-chrome reading experience.

Monetization & Creator Support

  • Buy Me Momo Integration — Added a native support link for Nepal's creator-funding platform Buy Me Momo (https://buymemomo.com/usefundaments), allowing domestic (eSewa, Khalti, Connect IPS) and international supporters to fund Theorem's development.
  • Monthly Cadence Engine — A recurring 30-day cadence model (src/core/lib/support-prompt.ts) keeps the support prompt gentle and unobtrusive: visible on first run, dismissed or clicked prompts hide for 30 days locally without polluting P2P sync CRDT state.
  • Support Placements — Added an authentic crescent-dumpling MomoIcon to the desktop sidebar footer (both expanded and collapsed layouts), the mobile titlebar header cluster, and a permanent entry in Settings → Links.
  • Safe External Opener — Unified external link launching (src/core/lib/open-external-url.ts) dynamically invoking @tauri-apps/plugin-opener on native desktop/mobile platforms with fallback to window.open in browser environments.

Backup & Data Safety

  • Startup integrity guard — Once a day, and on every app update before migrations run, Theorem checks its database with PRAGMA quick_check and keeps a rolling copy of it (db-snapshots/, the two newest). A damaged database or a failed migration is no longer only logged while the app starts with an empty library: it is reported to the app, which can put a snapshot back. The check costs about 0.4 ms per MB of database (25 ms for a 56 MB library, 210 ms for 566 MB, warm cache). After an update the copy is made before migrations, as their rollback point; on the daily check it is made in the background once the window is open, so it no longer delays that start (0.7–3 s on a 400 MB library).
  • Streaming backups — Backups are now one .theorem-backup file (a zip: manifest, a consistent SQLite snapshot, book files, generated audiobooks and, optionally, dictionaries) written entry by entry in Rust, so large libraries no longer run out of memory. A backup never carries this device's sync identity (device id, paired devices and their sync tickets) or OPDS catalog passwords. On Android the file is streamed into Downloads/Theorem.
  • Backup & Restore in Settings — Settings → Data & Storage now has Create Backup File (with an option to include dictionaries), Restore from Backup (shows when the backup was made and what it holds, then asks before replacing the library; the previous database is kept as theorem.db.pre-restore) and the automatic copies with a Restore button each. The old JSON "backup bundle", which loaded every book into memory and could never be restored, is removed.
  • Recovery screen — When the startup check finds the database damaged or an update cannot migrate it, Theorem opens a recovery screen (restore an automatic copy, restore a backup file, or continue anyway) with all writes paused, instead of starting with an empty library.

Discover & OPDS Catalogs

  • A new Discover page, curated on our server — Discover is now a front page fed by our own OPDS catalog (catalog.fundaments.work), so its shelves, subjects and search change without an app update: a large search over all ~78,000 Gutenberg books (your language first) with recent searches kept on this device, the newest Standard Ebooks editions as hero cards, hand-picked shelves, subjects as tiles with their book counts, and a "Bring your library" card. The page is kept on the device for offline use; catalogs you add are never stored.
  • Your catalogs as tabs — Catalogs you add (your school, university or library, or your own Calibre, Kavita or Komga) appear as tabs next to Discover; with none added there are no tabs at all. The in-app storefront and the built-in Project Gutenberg and Standard Ebooks tabs are gone (their books are on the Discover page); one you had signed in to is kept.
  • Browse any OPDS catalog — Discover is now one row of sources: the curated storefront, Project Gutenberg, Standard Ebooks and every catalog you add. Catalogs added in earlier versions were saved but never browsable (the full browser was unmounted); they all work now. Sections, shelves with See all, a trail with Back, server search with a local-filter fallback, sort/language/format filters, series and covers, and books that load as you scroll on a virtualized grid.
  • OPDS 2.0 and real-world Atom — A single Rust parser (theorem-core::opds) reads OPDS 1.0–1.2 Atom, OPDS 2.0 JSON, OpenSearch descriptions, HTML pages that advertise a feed, facets, indirect acquisition, prices and authentication documents. It runs natively on desktop and Android and through WebAssembly in the web build, so every platform behaves the same. Tested on real Gutenberg, Standard Ebooks and OPDS 2.0 reference documents plus Calibre, Komga/Kavita and Feedbooks-style output.
  • Sign in to private catalogs — HTTP Basic sign-in using the server's own labels; credentials stay on the device, are never synced, and are saved only after the server accepts them. Adding a catalog checks the address first and explains what is wrong (opds:// links, host:port, LAN addresses and HTML discovery are accepted).
  • Honest downloads — Purchases, loans, subscriptions and DRM-locked files (Adobe ADEPT, Readium LCP) are labelled instead of offered as downloads. Downloads stream with progress, go through the same import as "Add Books", and a login page that answers where a book should be is refused.
  • Native network layer — opds_net.rs replaces the old fetcher: content negotiation, redirect-aware relative links, typed errors (sign-in, 403/404/429, timeout, size), 25 MB catalog cap. The CLI's opds commands use it too.
  • Gutenberg covers — Gutenberg search results now show real covers and open straight to the book's downloads.

Phones & Navigation

  • One navigation order everywhere — The desktop rail, the phone bottom bar and Ctrl+1–5 now share one order from one list (nav-items.ts): Library · Shelves · Discover · Feeds · Workbench (your books, new reading, your notes). Before, each had its own order and the desktop called Feeds "Snapshots". Statistics and Settings sit together at the foot of the rail on desktop (top bar on phones). Shortcuts follow the screen: Discover is now Ctrl+3 (was 6), Feeds Ctrl+4 (was 3), Workbench Ctrl+5 (was 4), Statistics Ctrl+6 (was 5); Bookmarks stays Ctrl+7.
  • Discover and Bookmarks on phones — The 2026-02 removal of the mobile drawer left both without an entry point. The bottom bar is now Library, Shelves, Discover, Workbench, Feeds; Settings moved to the top bar beside Statistics; Bookmarks is a Workbench tab (with Highlights, Notes and Words). The tab row never scrolls away on small phones: below sm only the active tab shows its label.
  • One Bookmarks entry point on desktop — The sidebar no longer has a separate Bookmarks icon; Bookmarks is a Workbench tab on every screen size, and the Workbench icon stays lit while you are on it. Ctrl+7 still opens it.
  • Markdown export hidden where it cannot run — Export only works in the desktop app, but Android and the browser still showed the whole Markdown Export section with a disabled folder picker. It is now hidden there, the Settings tab is called Devices, the reader's annotations panel shows the export status only when export is set up on desktop, and onboarding no longer promises export on phones.
  • Keyboard shortcuts follow the platform — Ctrl in a shortcut now means Command on macOS and Control elsewhere; labels show ⌘/⇧/⌥ on a Mac. Ctrl+6 opens Discover (it duplicated Workbench).

Accessibility & Responsiveness

  • Audited with axe-core on every screen at 1280, 390 and 320px (see docs/accessibility.md): the final run reports no violations, no horizontal overflow and no target under 24px.
  • Dialogs and reader panels are named; closed panels are inert — Modal was labelled by an empty heading, and the reader's closed panels stayed tabbable and announced. Reader sliders, "more actions" buttons and the retention select now have names; Settings toggles report which choice is selected; annotation cards no longer nest controls inside a button.
  • Shelf and pairing controls are named — the Create Shelf and Add to Shelf dialogs were announced only as "Dialog" and now carry their titles (Create Shelf gained a visible title and a Close button); the Markdown export folder field, the new-shelf and shelf-search fields, and the send button for a pasted pairing code had no name beyond a placeholder or an icon.
  • Contrast — muted text on muted surfaces (3.9:1) and the reading themes' muted/secondary text (3.3:1) now reach 4.5:1; text is no longer dimmed with opacity.
  • Phones — the page title is always in the accessibility tree, the title bar is a header landmark, the reader has a main region, and sliders, swatches, breadcrumbs and toolbar buttons meet minimum target sizes.
  • Library and Shelves toolbars use the phone row — Add stretches across the row and the icon buttons are 44px, instead of a small desktop cluster pushed to the right.
  • No more overflow from long paths — a long folder path in Settings (the Neural Voice "N MB at /home/…/tts" line) wrapped past the screen; setting descriptions now wrap anywhere.

Highlights

  • Cross-page highlights stay one highlight — A selection that overlaps or touches existing highlights (the second half of a sentence on the next page) merges into one: the oldest keeps its id and notes, its range becomes the union, and the others are removed with sync tombstones. Merging never crosses chapters.
  • Touch selection — The toolbar anchors to a visible rect (not the last rect on the next page), waits for a handle drag to settle, and touch selections never turn the page.

Sync Security

  • Only your approved devices can connect — Before, any device on the same network could connect to Theorem's sync, download any file the app could read (including its database and private sync key, by asking for a "book" named with ../), and pair itself without a secret. Now every sync connection, incoming and outgoing, is refused unless the other device is paired, checked by its cryptographic identity, not its address (an approach like KDE Connect's and Syncthing's).
  • Pairing needs the QR code and your approval — The QR code carries a one-time secret that works once, for 10 minutes, and stops working when the dialog closes or after five wrong attempts. The device showing it then asks "Pair this device?", and both screens show the same 6-digit code to compare, so a photographed QR code cannot pair a stranger's device unnoticed. Pairing codes from older versions are refused with a request to update.
  • Book transfers send only books — A request names a book, never a path; only book files are sent, links are resolved first, and downloads always land in Theorem's own book cache.
  • Synced data cannot plant paths or settings — File paths in synced books are ignored, odd book IDs are skipped, and only the settings that sync can be written by another device.
  • Unpairing takes effect at once, on both devices — The device can no longer connect, even over a connection that was already open, and the data shared with it is removed from this device. The other device is told and removes this one from its list too; if it was off, it does so (and says "… removed this device") the next time it tries to sync.

Sync

  • Sync conflicts you can see and undo — When the same book's details or a note were changed on two devices before they synced, the newer change used to win silently. Settings → Device Sync now lists such conflicts with both versions, and "Use the replaced version" brings the other one back. Changes made one after the other are never reported.
  • Devices have real names — Instead of both being "Theorem Device", a device is named after the computer, or the phone's maker and model, and can be renamed in Device Sync; the other device picks up the new name on its next sync.
  • An offline device is noticed in seconds — A sync with a device that is off now reports it after about 6 seconds instead of 30, and Device Sync shows it as offline within about 15 seconds.
  • Deleted items stay deleted, even for a device away for months — Deletions are now kept in the sync data permanently, so a device that comes back after more than 90 days drops what was deleted meanwhile instead of restoring it.
  • A deletion is not undone by a device that still had the item — When a device that had been away came back still holding an item deleted meanwhile, the sync engine could put that older copy in place of the deletion on the device that deleted it, so the item reappeared there and, after deletion records expired, stayed. Each device now notes the deletions it makes and puts one back, exactly as it was, when an older copy replaces it.
  • An edit is no longer lost behind a device in the middle — With three devices where one sits between the others (A and B paired, B and C paired), B passing on a book it had received could hide a newer edit made on A in the meantime, so the edit never reached B or C until the book was edited again. The newest change now wins, whichever copy was written last.
  • A sync cut off midway finishes — If the connection dropped (or the app closed) after the items arrived but before their contents did, those items never appeared on that device, however often it synced again. Their contents are now fetched on the next sync, and such a sync no longer stalls for minutes waiting for them.
  • A large library's first sync is not "Peer offline" — The first sync of thousands of books and highlights takes minutes; after 30 seconds the round used to give up and call the other device offline while the two were still exchanging. It now waits as long as items keep arriving or the other device still answers.
  • A large library's first sync is five times faster — Highlights and notes now travel as one sync entry per book instead of one per highlight. A library of 5,000 books with 50,000 highlights is 10,000 entries instead of 55,000: its first sync with a newly paired device took 62 seconds instead of 5.7 minutes, and each device's sync data shrank from about 90 MiB to under 40 MiB. Edits to different highlights of the same book on two devices both survive, and a highlight deleted anywhere stays deleted, even on a device away for months. Devices still on an older version keep getting one entry per highlight until they update; pairings made before keep their existing entries, which are still read.
  • Each change syncs on its own — Notes, shelves, words, feeds, articles and deletions are now sent one item at a time instead of as whole lists every round. Devices still on an older version keep getting the lists until they update.

Interface Polish

  • Sync button that looks like sync — The titlebar's sync button had a sort-like arrow icon. It now shows a refresh icon that turns while syncing, and with no paired device it opens Settings → Devices instead of doing nothing.
  • Library sort and filter in plain words — "ASC/DESC" became "A to Z" or "Newest first" depending on what is sorted, statuses read "Unread / Reading / Finished", "Unshelved" became "Not on a shelf", and the duplicate "Recent" button is gone. The phone sheet and the desktop popover are now one component, so they no longer drift (the desktop one said "Read" where the phone said "Reading"), and the phone sheet no longer cuts off its last section.
  • Onboarding that holds still — Every step now has the same height, so Back and Next stay under the pointer, and sync is described as it works: directly between devices on any network, not "over LAN" (Settings → Devices said the same).
  • Empty states that lead somewhere — An empty library names the formats it takes and links to Discover for free books; the Workbench's empty state says "No Highlights or Notes Yet" instead of the internal word "annotations".
  • Books open in the dark reading theme on new installs — matching the dark interface instead of flashing a white page. A theme you already chose is kept.
  • Text sizes on buttons and inputs finally apply — A global style outside Tailwind's layers overrode the size, weight and line height set on every button, input and select since February, so chips, tabs and toggles rendered at body size. Element defaults now sit in the base layer, and links take the colour they are given.
  • Workbench tabs are labelled on phones — All, Highlights, Notes, Words and Bookmarks each show their name under the icon instead of icons that had to be guessed.
  • "Last read" sort is complete — Books never opened are ordered by when they were added instead of arbitrarily, and an unreadable date no longer scrambles the order. The old "Recent" button, which only re-selected the default sort and so seemed to do nothing, is gone.
  • Reading settings show their effect live — The settings panel no longer greys out the page, so a new font, size or theme can be judged while choosing it. Clicking the page still closes the panel.
  • Every Settings tab visible on phones — The tabs were a sideways-scrolling row of wide buttons that hid four of six; they now wrap into a grid. About names the maker correctly ("Fundaments") and drops the developer-only stack line.
  • Room above the first line on desktop — The reader toolbar sits over the top of the page, and the first line of text nearly touched it; the page now leaves a proper margin above and below the text.
  • Feed names fit, and feeds open from the keyboard — A feed's refresh and remove buttons kept their space while hidden, cutting names to "The Pub…"; they now take no room until the row is hovered or focused. Each feed in the sidebar is a real button, reachable with Tab and opened with Enter or Space.
  • Workbench speaks its tabs' language — Its subtitle read "4 annotations • 6 terms"; it now says "3 highlights, 1 note from 1 book · 6 words", matching the Highlights, Notes and Words tabs.
  • Shelf cards keep a readable size on wide screens — The shelf overview picked its column count from the window width while its content stops widening, so a 1920px window made six narrow cards and cut names to "Adven…". Columns now follow the content width, at least 14rem each.
  • A calm progress bar in long books — The reader's progress bar drew a tick for every chapter, so books with many short chapters showed a dense row of marks. A chapter's tick now appears only when it has room around it: a book with a handful of chapters keeps them all, one with hundreds shows a plain bar. Hovering the bar with a mouse still names the chapter under the pointer; touch only drags.
  • One search engine everywhere — Searching highlights, notes, words, bookmarks, shelves and a catalog's loaded list now uses the same matcher as the Library (typo-tolerant, best match first), instead of a second, simpler one that ranked differently. Word search also tolerates typos now rather than needing an exact fragment.

Command line

  • The theorem command runs without a display — It built a hidden app window just to find the library folder, so on a server, over SSH or in a container every command crashed. It now finds the folder itself and opens the database directly. theorem sync still needs the app's sync engine and says so instead of crashing when there is no graphical session.
  • The command line sees the library the app keeps — Since 1.6.0 the app stores books and highlights in SQLite, but the CLI still read and wrote the old list in the app's settings copy, so it showed an empty library, and books it imported were missing from the app or rejected highlights with a FOREIGN KEY error. It now lists, edits and removes the same books and highlights as the app, registers imports the way the app does (book row, file, cover, search entry) and stamps edits so sync can tell them apart. Shelf changes run in one write transaction, so two commands at once no longer lose each other's changes.
  • library add and library import say what happened to every file — Files that fail are listed with the reason and the command exits with 1; files already in the library (same content) are skipped and named; paths are stored as absolute paths; series and volume number are kept; a folder that links back to itself no longer loops forever; and a big import is written in one transaction (3,000 books took 21 seconds).
  • theorem storage cleanup goes by the library's records — It compared the leftovers against the very table it was cleaning, so it never found any, and when that read failed it treated the library as empty and deleted every book file and cover. It now keeps whatever has a library record, books synced from other devices included, removes the rest, and stops with an error when it cannot read the library.
  • The command line no longer writes behind the running app — The app keeps the library in memory and wrote its copy over changes made with theorem while it was open, and sync then spread the old copy. Commands that change the library now stop with exit code 5 and a message while the app runs (--force overrides), reading commands still work, and the app waits for a running command to finish before it starts.
  • theorem read and the text interface print readable text — Chapters came out as one line with the page title, style sheets and scripts mixed in and entities such as é left as written. Headings and paragraphs are now separated by blank lines, list items and line breaks start new lines, preformatted text keeps its spacing, and only the visible text is printed. In-book search positions are unchanged.
  • theorem help works, and a mistyped command no longer opens the app — theorem help and theorem help <command> print help. A misspelt command, a lone option such as theorem --json or a file that does not exist used to start the app window instead (or crash without a display); they now print a usage error and exit with code 2. Only theorem alone, or files and links to open, start the app. On Windows, theorem commands print in the terminal they were run from.
  • A damaged or old library copy no longer crashes theorem — Commands stopped with a Rust panic when the app's saved library copy had an unexpected shape. They now print what is wrong and exit with an error.
  • theorem feeds works with the app's feeds — Feeds were written to an old copy the 1.6.0 app no longer reads, so feeds added from the command line never appeared, and the CLI's own parser named a feed after its first post, took an Atom podcast's audio file as the article link, kept no dates and garbled accented text in CDATA. The CLI now uses the app's feed parser and its feed tables: the feed's own title, the article link, publication dates, and readable text. A web page that is not a feed is refused, feeds refresh reports each feed that failed, and removing a feed also removes it on your other devices.
  • theorem opds download downloads like the app — It saved whatever the server answered, a sign-in or "buy" page included, under an extension guessed from the catalog, with no size limit. It now uses the app's download: web pages and empty files are refused, the file is named by its real type, the size cap applies, and the book goes through the same import as library add. A failed download exits with code 4, an entry that is not in the catalog with 3.
  • theorem stats shows the right reading time — The app counts reading time in minutes, but the CLI divided it by 60 again, so 10 hours showed as 10 minutes. Reading time is now shown in hours and minutes, streaks in days and the goal in minutes a day, and a library with no reading yet shows zeros instead of an error.
  • theorem search takes any text — A quote, a hyphen or a word like AND in the search failed with a database syntax error, and only whole words matched. Every word you type now has to appear in the title or author, as a whole word or the start of one, and --limit sets how many results are printed (50 by default).
  • Exit codes and errors scripts can use — theorem now exits with 2 for a command line that does not make sense, 3 when a book, shelf, highlight, feed or catalog entry does not exist, 4 when the network failed and 5 when the app is running; other errors stay 1. With --json, errors are printed on standard error as {"error": …, "exitCode": …}. Setting NO_COLOR turns colors off.
  • The text interface starts with the first book selected and stays idle — Enter did nothing until you moved the selection, and the screen was redrawn five times a second while nothing happened. The first book is now selected, and the screen is drawn only after a key press or a resize.

Under the Hood

  • End-to-end tests against the real app — A new suite (pnpm e2e, docs/testing-e2e.md) drives the desktop app the way a person does: it clicks, types, picks files and reads the screen, in an isolated data folder. It found the reading-position bugs fixed above, and it is the behaviour spec the 2.0 interface must pass unchanged.
  • Sync tested between real devices — Scenario tests run real sync nodes with their own data: three devices with one in the middle, devices away for months, a sync cut off midway, a relay-only connection, older app versions, and a 5,000-book / 50,000-highlight library. They found the sync fixes listed above.
  • Typed errors — Every native command error now has a kind (busy, damaged, constraint, not found, invalid input, I/O, network, internal). The text the app receives is unchanged; the command line picks its exit code from the kind.
  • Smaller modules — The database, the command line and the app setup were split into one module per area, with no change in behaviour.
  • Dependencies — One version each of reqwest and rand; rodio 0.23, rars 0.11, React 19.3.1 and current Tauri plugins.

Fixed

  • The window moves again (#163) — Dragging the title bar never moved Theorem's window (the permission it needs was missing), which showed once the window was no longer maximized, and a double-click on the title toggled maximize twice so nothing happened. Both title bars now drag and double-click to maximize, and the reader window drags from its whole bar.
  • A save that fails says why — When the library could not be saved because the database was busy or damaged, the message blamed full browser storage. It now says the library is busy and to try again in a moment, or that the database is damaged and to restore a snapshot in Settings → Data & Storage.
  • Reopening a book no longer forgets your place — While the reader restored your position, a page-change event that did not exactly match the saved spot reset the book to page 1 and erased the saved position (seen in about one reopen in three by the new end-to-end tests). A mismatch now only waits for the restore to finish.
  • Pages you turn right after opening a book are remembered, and it reopens on the same page — Opening a book held back position saves for a second; pages turned in that second were never saved (and a stale timer could keep the hold on for the whole visit), so the book reopened on page 1. A page turn now ends the hold at once, and no longer gets undone by the jump to the start that a new book makes a moment after opening (a page turned in that moment snapped back to page 1). Separately, re-applying the reader's styles dropped foliate's base sheet (line height, orphans/widows) from chapters loaded afterwards, so the same chapter could break into pages differently on the next open and the book came back one page early.
  • Highlights load in one step, and unreadable ones are reported — At startup every stored highlight and note was sent to the app as a separate JSON string and parsed one by one, and a damaged one vanished without a trace. They now arrive parsed and checked in one payload (with 50,000 annotations: webview work about 105 → 72 ms, payload 19.7 → 18.2 MB), and a damaged one is logged with its id (never its text).
  • Android source folders show their name — The Source folders list showed an Android folder as its raw content:// address. It now shows the folder's name, and where it is only when two folders share a name.
  • Browser build keeps libraries over 5 MB — The browser build saved its library, feeds and settings in localStorage, whose ~5 MB limit a dozen books already exceed; saving then stopped without a word and the books were gone after a reload. Stores now live in IndexedDB (existing data moves over on first start), and a save that still fails shows a notice instead of failing silently.
  • Highlights in feed articles are saved — The database enforces that a highlight belongs to a stored book, and an article is not one, so highlights and notes made in an RSS article were refused when saved and gone after a restart. They are now stored, and Storage cleanup no longer treats them as leftovers of a deleted book.
  • Browser build: feed unread counts after a reload — The browser build keeps only recent articles, but saved each feed's unread count from before that cut, so a feed with years of items showed 100 unread above a handful of articles. Counts are now taken from the saved articles.
  • Removing a favourite, a tag or a rating now syncs — Book details were merged as a union, so undoing something on one device was merged back from the other, and an older copy could undo a rename. Edits are now timestamped and the newer one wins, removals included.
  • Deleted books came back after 90 days — Deleting an item left its sync entries behind, and once the deletion record expired every device restored the item from them. Deleting now removes them.
  • Sync rounds report what really happened — An unreachable device showed "Already in sync" and was treated as synced; "last synced" changed even when nothing was exchanged; the status could stay on "syncing"; unpairing a device could make the next book download fail with "peer not available".
  • Sync never overwrites newer edits — Merges now run one at a time and apply onto the current library, an older note, word or settings copy no longer replaces a newer one, a peer's edit to something you changed seconds before is no longer dropped, and re-reading a book from the start keeps progress and position together. With several devices, the newest copy wins instead of a random one.
  • Sync data survives a slow start — Theorem deleted its sync databases when the sync engine took more than 15 seconds to start, as it can on a phone waking up. A damaged list of paired devices is now kept aside instead of being replaced by an empty one.
  • Books and deletions received between syncs are kept — A book or a deletion that arrived from another device while both were open showed in the library but was not saved, so a restart undid it until the next full sync.
  • Pairing codes work without internet — The QR code now lists addresses the other device can reach directly on the same network, plus the relay.
  • Faster sync rounds — Each round now reads only what changed since the last one instead of every entry (covers included), and no longer rewrites unchanged books. Quick sync covers every paired device, and Settings shows which devices are online.
  • The daily reminder never reminded anyone on a phone — It was a timer inside the app's webview, which Android pauses as soon as Theorem leaves the screen, and even then it only showed an in-app toast. Android now keeps the reminder itself (native code, not the webview): it arrives with the app closed, is scheduled again after a restart, an app update or a clock change, skips a day once its goal is met, and follows the settings as soon as they change. Settings shows when the next reminder is due. It has its own "Reading reminders" notification channel and Theorem's icon. Settings says when notifications are off or Android may deliver the reminder up to an hour late (no "Alarms & reminders" permission, the default from Android 14), with one tap to fix each. On a computer the reminder still shows while Theorem is open.
  • Android 15: nothing under the status or gesture bar — Android's WebView only reports the camera cutout as a safe area, so with edge-to-edge (always on from Android 15) onboarding's Skip sat under the status bar where it could not be tapped, and the gesture bar ran across the bottom bar's labels. Theorem now reads Android's real system-bar insets at start and on every rotation, and every screen pads by them.
  • Dialogs on phones — Every dialog (new shelf, add catalog, book details, edit book) ran edge to edge; it now keeps a 16px margin and stays clear of the status and navigation bars.
  • No Shortcuts tab on phones and tablets — Settings no longer lists keyboard shortcuts where there is no keyboard.
  • EPUBs with a publisher no longer crash the reader — Opening an EPUB whose package lists a publisher (every Standard Ebooks title, for one) failed with "Objects are not valid as a React child": the book-info panel, mounted as soon as a book opens, rendered foliate's publisher object. Reader metadata is now flattened to plain text (contributor names, language maps and language lists), and the panel only ever shows text.
  • Ctrl/Cmd+A now selects all — In the Library and inside a shelf it enters selection mode and selects every book in the current filtered view (previously it only toggled the mode, and did nothing on Bookmarks, which has no selection mode). In text fields it keeps its normal meaning.
  • Windows MSI installer — Releases now also build a WiX .msi next to the NSIS .exe, for Group Policy / Intune / msiexec deployments. The in-app updater keeps using the NSIS installer.
  • Faster startup (about 53% less JavaScript before first paint) — The app shell loaded 1.34 MB of JavaScript; it now loads 0.63 MB. PDF.js (430 kB) was being preloaded on every launch because a bundler chunk group had absorbed Vite's shared dynamic-import helper; the note-template engine (knap, 74 kB), i18next (about 50 kB, not yet used by any screen) and the Radix menu/dialog libraries (about 100 kB) also loaded eagerly. They now load on first use. pnpm build and CI fail if the startup bundle exceeds 700 kB or a lazy library creeps back in (scripts/check-startup-bundle.mjs).
  • Faster CI — Superseded runs are cancelled, the Rust check caches its build, pnpm uses setup-node's store cache, the Build job no longer repeats the Typecheck job, and jobs have timeouts.
  • Audio reliability pass (Immersion Reading, companion audiobooks, Save as Audiobook)
    • Speed changes no longer freeze neural narration — changing the speed while the native player held its lock called back into the same lock and deadlocked the player.
    • Replaying a page can't double-start narration — a stale run for the same text could take over after stop + replay; runs are now tracked by session. Stopping while playback is starting no longer leaves audio running, blank pages complete instead of erroring, and the end of the book stops instead of re-reading the last page.
    • OS voices (macOS, Windows, Linux) hardened — book text was spliced into a PowerShell script on Windows (an apostrophe broke narration, and crafted text could run commands), and a page starting with - was parsed as an option by say/spd-say. Text now travels via stdin or after --; Stop kills only Theorem's own process instead of every powershell/say on the machine; the chosen speed is applied.
    • Attaching audiobooks — MP3s without an ID3 tag attach instead of failing, MP3 duration is read from Xing/VBRI/bitrate instead of being 0, and a corrupt MP4 box can no longer wrap a size.
    • Audiobook player — closing before the file loaded no longer resets your position to 0; an unplayable or missing file shows a message instead of a dead Play button; finished recordings restart; end-of-chapter sleep stops at the real boundary at any speed; the scrubber supports dragging and the keyboard.
    • Save as Audiobook — the Ogg Opus OpusTags packet was missing its comment count (strict decoders reject it), pages could overflow the 255-segment limit or lack an end-of-stream page, text was always normalised as English, cancel only took effect between chapters (and there was no Cancel button), progress events from other books leaked in, partial files were left behind, and generated files were never deleted on detach or book delete.
  • Neural narration plays at the speed you chose — At 1.5× every page after the first played at about 2.25×, because the speed was applied twice: once when the voice was generated and again by the player, which also kept it from the last page (after a restart the same setting played at 1.5× again). The voice is now always generated at normal speed and the player applies your speed once, so changing it is instant and needs no new synthesis. Changing speed in the middle of a sentence no longer jumps to another part of the page.
  • Neural narration splits sentences where a reader would — Closing quotes and brackets were cut off into chunks of their own ("Hello. then "), an ellipsis could become a chunk of nothing but dots, abbreviations such as Gen., Lt., Col., p.m., Jan. or U.S. and initials like J. R. R. ended sentences, Chinese and Japanese text was never split at 。!?, every comma of a long sentence became its own clipped chunk with a comma added to it, and a heading ran into the paragraph below it. Sentences now keep their quotes, an ellipsis is one stop, long sentences are divided only as much as needed, and headings and paragraph breaks end a sentence. Save as Audiobook now uses the same, shorter limit for Japanese and Korean.
  • Read-aloud no longer reads other languages as English — Before a page was spoken its numbers and abbreviations were always expanded in English, so a French book read "En 1984" as "En one thousand nine hundred eighty-four" and a German "3,50 €" as "three,fifty €". These rules now apply only to English books; other languages are spoken as written. In English, "1990s" is now "nineteen nineties" (it was "nineteen ninetyies"), ISBNs and phone numbers are no longer read as sums ("555-1234" was "five hundred fifty-five-one thousand…"), and paragraph breaks survive so narration pauses between paragraphs.
  • Read-aloud says why it stopped — When narration failed (no system voice installed, a damaged neural voice, no audio output) the play button simply went back to idle without a word. A message now explains what went wrong and what to do, and when the neural voice fails it offers Use system voice to carry on reading with the computer's own voice.
  • Generated audiobooks have real chapter names — Save as Audiobook named every chapter "Section 1", "Section 2"… and also narrated the cover, the table of contents and pop-up notes. Chapters now take their names from the book's table of contents (or the chapter heading), a chapter split over several files stays one chapter, and the cover, contents page, image-only pages and notes outside the reading order are skipped. Narration also pauses between paragraphs. The neural voice's audio cache now keeps the sentences you replayed most recently rather than the newest ones (its size limit is 150 MB, as the docs now say).
  • Android read-aloud follows your speed and the book's language — The speed setting was ignored by the phone's speech engine, every book was read in the phone's own language (a French book with an English voice), and a page longer than the engine accepts could fail. Each page now uses your speed and the book's language (with a note if no voice for that language is installed), long pages are read in pieces, and a page you stopped or skipped can no longer end or interrupt the next one.
  • Native PDFium RFC withdrawn — The proposal to replace PDF.js with a native PDFium engine was removed from the docs; PDF.js stays the PDF engine.
  • Maintenance — The Buy Me Momo content-drift check (pnpm check:content and its CI job) was removed; the two URL literals are now kept in sync by hand. CodeQL runs on a schedule rather than on every push, and the landing-page hook also fires on documentation changes.
  • Tauri IPC access control check on memory trim — Added wildcard window capability in src-tauri/capabilities/default.json and allowed IPC localhost origins in CSP, resolving Tauri v2 IPC ACL denials (trim_memory) on sub-windows and reloaded webviews.
  • PDF Viewer Stability & Zoom Convergence — Eliminated PDF zoom jumping, dropped TOC jumps, and page-pill layout squeezing on document load.
  • Discover no longer crashes on an old cache — The cached storefront from before the OPDS rewrite (theorem_discover_cache_v2) held entries the new UI cannot render and took the page down (entry.authors undefined). The cache is versioned by entry shape (v3), old caches are deleted, anything that is not in today's shape is ignored, and the helpers tolerate malformed entries.
  • Repeated phrases no longer edit the wrong highlight — Existing-highlight matching used the selected text (or a substring of it) across the whole book; it now uses range containment.
  • Neural voice offered where it cannot work — Intel Macs and Windows/Linux on ARM were offered a download for a runtime that does not exist (the "supported" flag was always true). The flag is real now and the installer refuses to run.
  • Untrusted URLs are allowlisted — Feed links, extracted article markup, book covers and the Library's HTML preview now pass through src/core/lib/url-safety.ts: only http(s), mailto and tel links, only image-type data: URIs as image sources, and control-character tricks such as java\tscript: are normalized before checking. The Library's regex sanitizer was replaced by the existing DOMPurify sanitizeHtml.
  • Lower memory on desktop — PDF.js and Foliate are no longer pre-warmed at startup (they load when a book of that format opens), the PDF offscreen page cache dropped from 6 pages / ~96 MB to 4 pages / ~48 MB and is cleared on unmount, and session Replay was removed from Sentry.
  • Sentry bundle — Named imports let the bundler drop session Replay: the Sentry chunk is 158 KB instead of 446 KB (54 KB instead of 149 KB gzipped).
  • Library search no longer emptied at every launch — The startup search-index check compared the index with the library list in the settings blob, which the desktop and Android apps have kept empty since books moved to their own table, so every indexed book was removed and the app then skipped re-indexing. The check now reads the books table, never clears the index because the library looks empty, and tells the app to re-index after any repair.
  • RSS retention deletes old articles again — Articles store their fetch time in milliseconds, but the cleanup compared it with the current time in seconds, so no article was ever old enough to remove and feeds grew without limit. The cutoff is now computed in milliseconds (and can use the index), the article and its cached content go together in one transaction, and times that older versions stored in seconds are converted at startup.
  • Saving highlights can no longer erase them — Saving a book's highlights deleted the stored ones and then inserted the new list; if any insert failed (two highlights with the same id was enough) the book was left with none. The save is now one transaction that rolls back on error, and a repeated id updates the highlight instead of failing (the last copy wins). Deleting a feed and clearing all storage are transactions too.
  • Deleted books leave nothing behind — The database relied on cascading deletes that were never switched on for the app's connections, so removing a book kept its cover, metadata and highlights (and "Clear all storage" kept every book's metadata, so books came back). Book removal, in the app and in theorem library remove, now deletes the cover, cache record, metadata, highlights and search row in one transaction; deleting an RSS article removes its stored content in the same transaction.
  • Sync deletions are all or nothing — Deletions received from another device ignored database errors, so a failure could leave a book half-deleted while sync reported success. A sync batch is now applied in one transaction that rolls back on any failed delete (and is retried on the next sync), and a book deleted on another device also loses its cover, cache record and cached file here.
  • Feed changes that fail to save are reported — Marking an article read, favouriting, saving, reading progress, extracted text and feed add/remove/refresh wrote to SQLite with every error discarded, so a failed write was only noticed when the change was gone after a restart. Each failure is now logged with the operation that failed; a failure in one article no longer hides the others.
  • Upgrading from an old version can no longer lose books, highlights or words — When books (before v8), annotations (before v7) and vocabulary (before v6) moved from the saved app state into their own SQLite tables, the saved copy was emptied without waiting for SQLite, so a failed write lost them. The saved copy now keeps them until SQLite holds every one, a failed move is retried on the next launch (never overwriting a newer row, never bringing back a deleted one), and they stay visible meanwhile. A failed vocabulary read no longer empties the word list either.
  • Auto sync stops when it should — Turning auto sync off (or finishing onboarding) while sync was still starting left its timer running in the background, because the stop handle was only kept once the start had finished. Startup failures of sync, the window close handler and the tray Quit handler are now logged instead of ignored, and a close handler set up after its window effect was gone is removed.
  • Phones reopen your book after Android closes Theorem in the background — Android ends a backgrounded app whenever it needs the memory, and coming back used to land on the Library. If you were reading when Theorem went to the background, and come back within 6 hours, the same book now opens at your place. Leaving the reader first, or a longer break, still starts on the Library, and a book that crashed the app while reopening is not reopened again.
  • Sync catches up when you return to the app — Coming back to Theorem now runs a sync round when auto sync is on and the last round is more than 30 seconds old, also on phones where returning does not count as the page becoming visible again. Both signals arriving together start one round, not two at once.
  • Large libraries open faster — Before the window appeared, every start also converted old covers, moved books and dictionaries out of the database, repaired "not downloaded" flags, checked the search index and could VACUUM the whole file: about 10 seconds on the first start after upgrading a 20,000-book library, and 130–390 ms on every ordinary start. Only the integrity check and the migrations the interface needs still run first (an ordinary start now spends about 1–4 ms on the database); the rest runs in the background a few seconds later, never overwrites a change you make meanwhile, and VACUUM waits until the app has been idle. Dictionaries left inside the database by old versions were never moved out, because that step read a column that does not exist; they now are.
  • Duplicate imports no longer return after a restart — When a batch of imported books held a copy of a book already in the library, the copy was dropped from the library but still saved to the database, along with its search entry. Only the books that are kept are saved now.
  • Scan Folder reports every book it could not import — On the desktop, books that failed to import vanished without a word, a failed copy still added a book with no file, folders it could not open and broken links were skipped silently, and a link that pointed back up the tree could make the command-line import run forever. Each file is now imported or listed with the reason (empty, damaged, unreadable, gone), the scan follows links without looping, skips hidden folders such as .Trash, and fb2.zip and .fbz files are recognised as FB2.
  • Rescanning a folder is quick and adds nothing twice — Every scan copied and hashed every file again and only then dropped the duplicates. Books already imported from the same place are now skipped without being read, a moved or renamed book is recognised before it is copied (and followed to its new place), and two copies of a book in one folder are imported once.
  • Folder imports show progress and can be stopped — Scanning a large folder showed only a spinning icon and could not be stopped, and each book was added to the library on its own, which made the window stutter. The library now shows how many books are done, a Stop button ends the scan or import at once (finished books stay), books are added in batches, and one name that cannot be kept as a folder no longer stops the whole folder import.
  • Faster folder imports — Making cover thumbnails was most of the time a large import took: the image code ran at the release build's size-first optimisation and used a slow resizer and JPEG encoder. Covers are now made two to four times faster (an integer box filter, the jpeg-encoder crate, image decoders built for speed), and 2,000 books import in about 5 s instead of 11 s on a 12-thread desktop.
  • Scan Folder on Android — A damaged EPUB is reported instead of being added as a book that cannot be opened; copies of a book are counted as "already in your library" instead of "imported"; problems name the file by its folder instead of a content:// address; a rescan no longer copies second copies of a book out of the folder again; each book is copied once instead of twice; and no temporary copy is left behind when a file cannot be imported.
  • Neural voice audio is never cut short or made twice — A sentence could be played while it was still being saved, and the next page's audio was sometimes made twice when reading ahead and reading aloud asked for it at the same moment. Audio is now saved whole before it can be played, each sentence is made once, and saved audio is kept apart per language and voice model.
  • Computer voices finish each page before turning it — On Windows, macOS and Linux, read-aloud with the system voice guessed how long a page would take, so it cut speech off mid-sentence or sat in silence before the next page. The page now turns when the voice actually stops speaking, and on Linux the book's language is passed to the voice.

Download & install

Platform Download
Windows (x64) Theorem_1.6.0_x64-setup.exe
Windows (x64, MSI) Theorem_1.6.0_x64_en-US.msi
macOS (Apple Silicon) Theorem_1.6.0_aarch64.dmg
macOS (Intel) Theorem_1.6.0_x64.dmg
Linux (Debian/Ubuntu) Theorem_1.6.0_amd64.deb
Linux (any) Theorem_1.6.0_amd64.AppImage
Android (arm64, most phones) app-arm64-release.apk
Android (32-bit arm) app-arm-release.apk
Android (x86_64 emulator) app-x86_64-release.apk
Android (x86 emulator) app-x86-release.apk

Linux one-liner: curl -fsSL https://raw.githubusercontent.com/Fundaments-Work/Theorem/main/scripts/install-linux.sh | bash

Desktop builds auto-update in place (Settings → About → Check for Updates). Android updates are manual unless installed via F-Droid.