Anki and Duolingo had a baby: a spaced-repetition app for learning hiragana and katakana in pairs, plus essential & tech Japanese vocabulary — with Anki's exact SM-2 scheduling under the hood and a playful, dark-mode-first UI on top. No decks, no accounts.
| Target | Where | How |
|---|---|---|
| Phone / tablet / Google TV | app/ |
one APK; TV gets a leanback launcher entry + D-pad focus |
| Wear OS watch | wear/ |
standalone watch app (Wear 3+), shares the same engine |
| Linux desktop (any distro, incl. Hyprland) | desktop/ |
Compose Desktop app image with bundled JRE |
- Anki-faithful SM-2 scheduler — learning steps (1m/10m), Again/Hard/Good/Easy with live interval previews, ease factor, lapses, leech suspension, 4 AM day cutover.
- Duolingo feel — streak flame, daily XP goal, chunky 3D buttons, session celebrations.
- GitHub-style activity heatmap and daily reminder notifications.
- Add your own words — type romaji or kana; an offline IME-style transliterator (っ/ん/long-vowel aware) fills in the other side.
- Account-free Wi-Fi sync across all devices: cards, your words, scheduling progress, deletions, review history — and therefore streak & XP — converge peer-to-peer (mDNS discovery + manual address entry). Last change wins per card; deletions carry tombstones; logs are unioned.
- Missing built-in cards are detected and restorable in one tap.
# phone + Google TV APK
./gradlew :app:assembleDebug
# Wear OS APK
./gradlew :wear:assembleDebug
# Linux desktop (self-contained app image)
cd desktop && ../gradlew createDistributable
# → desktop/build/compose/binaries/main/app/kana-pairs/
# install with desktop/install.sh, see desktop/README.md
# tests (scheduler, romaji converter, sync merge)
./gradlew :app:testDebugUnitTestToolchain: AGP 9 (built-in Kotlin 2.2), compileSdk 37, Room + KSP,
Compose everywhere. gradle.properties sets
android.disallowKotlinSourceSets=false for KSP compatibility.
app/src/main/java/.../domain/is pure Kotlin (scheduler, dates, romaji, seed data, sync engine + TCP protocol) — shared as source by all three targets.- The Wear module reuses the phone app's
domain/data/syncpackages viasourceSets(seewear/build.gradle.kts); the desktop build sharesdomain/only and keeps its own SQLite store. - Sync is a single TCP round trip of full JSON snapshots, merged
deterministically on both sides (
domain/SyncModel.kt, unit-tested).