A reading list that keeps itself, ranked by what you actually read.
An AI agent checks your inbox and files what's worth keeping into Notion;
blogs you follow add their own posts the same way.
See every screen · How it fits together · Run it
Monochrome by default. One accent, spent on purpose.
Newsletters and blogs file themselves into one Notion database — ranked by what you actually read, not by when it arrived — then you set a twenty-five minute timer and read, on Paper Black or Ink, the same page with the colour drained out.
- Curated through Notion. Claude files inbox newsletters straight into
the reading list; blogs with a working feed are fetched by the app and
never touch Notion. Either way, only rows ticked
Savedreach the phone — see docs/architecture.md for the schema and sync rules. Following a blog writes it back to Notion too, so it's there to edit next time you open it. - Save it however it reaches you — the Chrome share sheet, or the paste field on Saved behind Add.
- Reads offline. Every screen renders from local storage; a feed with full-content RSS is cached whole at sync time, so most articles were already on the phone before you tapped them.
- Saved is a queue and a record. Unread leads; what you've read stays under its own heading. Filter to either, or search a title, host or topic.
- On-device summaries and reading aloud. Long articles get a one-tap summary via Gemini Nano; the phone can also read one aloud with its own text-to-speech. Both are absent on hardware that can't run them, and a read that can't happen says why instead of staying silent.
- A focus timer that behaves like an alarm, not a widget — a real system notification and vibration when the interval ends, phone face-down.
Android is the app, built and tested day to day. Desktop, web and iOS compile from the same source to prove it travels.
git clone https://github.com/MKS-01/duskread.git && cd duskread
./gradlew :androidApp:installDebug # ~5 s once warmThat's the whole setup — Notion is optional, and connecting it is one pasted token in Settings.
| JDK | 17 or newer |
| Android | 12 and up (minSdk 31, compile/target 36) |
| Gradle | 9.3.1, via the wrapper |
| For summaries | A phone with AICore — Pixel 9+, Galaxy S24+ and similar. No emulator has it |
| For iOS | Xcode, plus XcodeGen |
Desktop, web and iOS
./gradlew :composeApp:runDistributable # desktop — use this, not `run`
./gradlew :composeApp:wasmJsBrowserDevelopmentRun # web, on :8080
cd iosApp && xcodegen generate && cd .. # iOS needs an Xcode host
open iosApp/iosApp.xcodeprojrunDistributable, not run, for desktop — the embedded Chromium needs the
packaged .app layout, or it segfaults on init. First iOS build is ten
minutes or more; incremental after that.
One commonMain source set feeds Android, desktop, web and iOS; storage,
audio, the summariser and the HTTP client are expect/actual pairs behind
it. UI is Compose Multiplatform
1.11 with Haze for the floating-bar
blur, on Kotlin 2.3 and Ktor
3.1. Summaries run on-device via ML Kit GenAI
/ Gemini Nano; reading aloud is Android's own TextToSpeech; the optional
Readback tab queries readback's
library.db read-only. No navigation library, no ViewModel, no DI, no
database — a ceiling chosen on purpose.
./gradlew ktlintCheck # several rules deliberately off, see .editorconfig
./gradlew ktlintFormatNo tests yet — a build that succeeds is the start of checking a change, not the end of it.
The design system — Paper Black and Ink, and every token behind them. docs/architecture.md — how the pieces connect, the Notion schema, and the end-to-end flows.
MIT — see LICENSE.
A personal project, built for learning and for its own author to use
Built agent-first with Claude Code
The synced-audio Readback tab is a separate, optional open-source project: readback
