Words come to you.
Ledor is Portuguese for someone who reads aloud to another person — and that's what this app does: instead of your eyes chasing the words, the words come to your eyes.
An open-source RSVP reader for EPUB and web articles, built with Flutter and running on Android, Linux desktop, and iOS. Words are displayed one at a time at a configurable WPM, with the Optimal Recognition Point (ORP) highlighted so your eye doesn't need to saccade — letting you read faster while preserving comprehension. Formerly known as RSVP Reader.
- Android APK and Linux tarball: grab the latest from Releases.
- iOS: build from source (no App Store release yet).
- Try the interactive RSVP demo on the website: ledor.app.
- Three reading modes
rsvp— single-word display with ORP highlight, active during playbackereader— traditional continuous reading without highlights or controlstts— text-to-speech narration with word-synced highlighting; keeps playing in the background with lockscreen media controls- The last mode used is remembered per book
- Pause into the full text — pausing RSVP or TTS drops you into a scrollable full-text view with the current word highlighted; tap any word to seek and continue from there
- TTS that feels like an audiobook player — system voices with human-friendly names ("English (UK) · Female 1" instead of
en-gb-x-fis-network), engine and voice pickers, and playback speed from 0.5× to 3× independent of the RSVP WPM. On Linux it speaks throughspeech-dispatcher(e.g. Piper voices) - Two content sources, one pipeline — import EPUB files or save any web article by URL (or share sheet on Android); both end up as the same tokenized read
- Bookmarks — long-press any word in any mode to bookmark it with a context snippet; synced across devices
- Smart timing — longer pauses on punctuation, paragraph starts, chapter starts, and long words (all pre-computed at import, zero work in the hot loop)
- Ramp-up — starts at 70% of target WPM and accelerates over the first 30 words to help your eyes warm up
- Chapter-aware progress slider with visual chapter markers and title tooltips while dragging
- Velocity-based scroll tracking — in the paused full-text view, slow scrolling moves word-by-word while faster scrolling steps by sentence or paragraph
- Lock + recenter overlay in the full-text view — pin the highlighted word in place so the engine keeps advancing without your hand reaching for it; tap to recenter when you scrolled away
- Light + dark editorial themes — warm "ink on paper" palettes with a system/light/dark toggle
- Fully customizable reader — colors, fonts (Google Fonts), sizes, layout positions, and a configurable focus line, all with live preview
- Reading stats — local per-session tracking feeds a weekly/monthly dashboard with words-per-day, time, and WPM trend charts
- Shareable monthly recap — last.fm-style 9:16 image with finished + in-progress books, exported as PNG via share sheet
- Book completion celebration — automatic recap on the last word of a book with time/words/sessions/WPM stats, a 0–5 star rating, and an optional shareable card. Also reachable manually from the library (long-press a finished book) and from the reader's "Finish book" button for in-progress books where the tail is acknowledgements
- Bilingual UI — English and Brazilian Portuguese (PT-BR)
- Offline-first — books and progress stored locally in SQLite via Drift
- Google Drive sync (optional) — library, reading progress, bookmarks, display settings, ratings, and reading sessions sync across devices through a user-owned
Ledor/folder on Drive (drive.filescope, so the app only sees files it created). Sharded manifest pushes only the shards that changed. Available on Android and Linux desktop - Linux desktop support — full GTK build with drag-and-drop for EPUB/URL imports and keyboard shortcuts (
Space/←→/↑↓/Esc) - Unicode-aware ORP calculation — handles Portuguese accents and punctuation correctly
| Library | RSVP mode | Paused reader | Settings |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
- Flutter SDK
^3.10.1 - Android Studio / Xcode for device builds
- On Linux,
lldmust be installed to run tests (sudo apt install lld)
flutter pub get
dart run build_runner build --delete-conflicting-outputs # generate Drift code
flutter gen-l10n # generate i18n bindings
flutter run # run on device/emulatorflutter analyze # static analysis
flutter test test/ # unit tests
flutter test --coverage test/ && python3 tool/check_coverage.py --min 72 # coverage (CI gate)
flutter build apk --release # release Android build
flutter build linux --release # release Linux buildReleases are cut by pushing a vX.Y.Z tag — CI builds the signed APK and the Linux tar.gz.
The app syncs the library manifest, reading progress, bookmarks, display settings,
ratings, and per-session reading stats through a Ledor/ folder it creates in your
own Google Drive. The reading features all work without it — this is only if
you want the sync button on the Settings screen to actually connect.
Available on Android (via google_sign_in + Google Play Services) and on
Linux desktop (via a loopback OAuth flow opened in the system browser).
Both platforms hit the same Drive folder when wired to the same OAuth client.
Because Google OAuth ties the credentials to a specific Cloud project + Android
signing key, if you clone this repo and try to sign in, you'll get
PlatformException(sign_in_failed, …ApiException: 10) (DEVELOPER_ERROR). Fix by
provisioning your own OAuth client:
- Create a project at https://console.cloud.google.com/ and enable the Google Drive API.
- OAuth consent screen → External → Testing. Add yourself as a Test User.
- Credentials → Create OAuth 2.0 Client ID → Web application. Add
http://127.0.0.1to Authorized redirect URIs (no port — Google ignores it for loopback IPs). This client_id is what desktop uses directly, and what Android passes togoogle_sign_inasserverClientIdso both platforms see the same Drive folder. - Credentials → Create OAuth 2.0 Client ID → Android (in the same project):
- Package name:
com.pimenta.ledor(or your fork'sapplicationIdfromandroid/app/build.gradle.kts). - SHA-1: your debug keystore fingerprint. Get it with:
keytool -list -v -keystore ~/.android/debug.keystore \ -alias androiddebugkey -storepass android -keypass android | grep SHA1
- No client secret needed.
- Package name:
- For Linux desktop: copy
.env.example→.envand paste the Web client_id + client secret. The file is gitignored;flutter_dotenvloads it at startup. Without both values present, the Sync section is hidden in Settings.
Scope used: https://www.googleapis.com/auth/drive.file (the app only sees
files it created — never your other Drive contents).
Android needs Google Play Services on the device/emulator (use a Play-image emulator, not AOSP). See docs/linux-desktop.md for the desktop-side details and docs/library-sync.md for the sync pipeline + merge rules.
Feature-based Clean Architecture with Riverpod state management. Each feature owns its own domain/, data/, and presentation/ folders.
lib/
core/ # theme, routing, constants, utils, platform_capabilities, share handlers
database/ # Drift/SQLite: books, reading_progress, reading_session, cached_tokens,
# bookmarks, sync_import_failures
features/
book_library/ # book grid + import FAB, master-detail on tablet landscape
epub_import/ # EPUB parsing pipeline -> WordToken[] cached in SQLite
article_import/ # URL fetch -> readability -> WordToken[] (same persistence as EPUB)
library_sync/ # Drive sync gateway, auth backends, sharded manifest service
rsvp_reader/ # RSVP engine (Ticker), TTS player + backends, display widgets,
# controls, bookmarks, settings sheet
reading_stats/ # session aggregations, weekly/monthly dashboards, share cards
settings/ # full-screen settings wrapping the shared display panel
l10n/ # ARB files (en, pt) + generated
Key concept — WordToken: every word of every book is pre-processed at import time with its ORP index and timing multiplier already calculated. The RSVP engine does no computation inside the per-word tick, keeping playback smooth at 600+ WPM.
See docs/architecture.md and docs/rsvp-engine.md for detailed documentation on the data flow, state management, ORP math, smart timing multipliers, ramp-up, and velocity-based scroll stepping. docs/tts-mode.md covers the cross-platform TTS pipeline (flutter_tts + a speech-dispatcher socket backend on Linux). docs/reading-stats.md covers the reading-session model, stats dashboard, and the monthly recap + completion share pipelines. docs/library-sync.md documents the Drive sync pipeline — manifest format, merge rules, tombstone handling, and the DateTime / cache invariants. docs/linux-desktop.md covers the desktop build, shortcuts, and drag-and-drop.
- Flutter 3.x / Dart
- Riverpod 2 (state, no codegen — avoids conflict with
drift_dev/source_gen) - Drift over SQLite for persistent storage
- SharedPreferences for display/theme settings
- epub_pro for EPUB parsing
- go_router for navigation
- flutter_tts + a speech-dispatcher (SSIP socket) backend for TTS, audio_service for background playback
- google_sign_in / googleapis (+ loopback OAuth on desktop) for Drive sync
- fl_chart for the stats dashboards, share_plus for PNG export
- google_fonts + flex_color_picker for theming
- RSVP and ORP concepts are based on established speed-reading research (e.g. Spritz-style presentation). This project implements them with its own ORP lookup table, tokenizer, and timing heuristics tuned for Portuguese and English.
- Open-source libraries listed in pubspec.yaml.
Contributions are welcome. See CONTRIBUTING.md for setup, conventions, and PR guidelines.
MIT © 2026 Daniel Pimenta



