Files
tiennm99 e314ae01c8 feat(android)!: replace the Capacitor wrapper with a native Kotlin/Compose app
Fresh Kotlin 2.2 + Jetpack Compose app at feature parity with web/
(game logic, settings contract, voice semantics ported one-to-one with
twinned tests; 104 unit + 9 instrumentation tests). Fully offline:
VIBRATE is the only permission; Media3's ACCESS_NETWORK_STATE is
stripped. Voice clips mount from web/static/audio at build time.

Same applicationId, signing env contract, and Play alpha track;
versionCode 7 / 0.2.0. CI Android job is Gradle-only (lint, test,
debug + R8 release APK) and the release workflow fails fast on an
unbumped versionCode.

BREAKING CHANGE: WebView localStorage (settings, in-progress round)
is not migrated; wrapper users start clean.
2026-08-31 13:28:48 +07:00

83 lines
3.1 KiB
Markdown

# Lô Tô
*Lô tô hội chợ* — Vietnamese fairground bingo. One number caller, two shipping
targets: a static web app and a fully-offline native Android app.
Live: [tiennm99.github.io/loto](https://tiennm99.github.io/loto)
## Layout
| Path | What it is |
|------|------------|
| [`web/`](web) | SvelteKit static app — the game itself. npm, Vitest, deployed to GitHub Pages and Firebase Hosting. |
| [`android/`](android) | Native Kotlin + Jetpack Compose app at feature parity with `web/`. Pure Gradle — no Node. |
The two apps share the voice clips: the Android build mounts
`web/static/audio/` straight into the APK's assets, so the MP3s exist once in
the repo. Game logic is hand-ported — a behavior change in `web/src/lib/`
needs a matching Kotlin change (the web app is the behavioral spec).
## Quick start
```bash
# web
cd web && npm install && npm run dev
# android debug APK (requires JDK 21 + Android SDK)
cd android && ./gradlew :app:assembleDebug
```
Per-project detail lives in [`web/README.md`](web/README.md) and
[`android/README.md`](android/README.md).
## CI
Workflows live at the repository root in `.github/workflows/`. Because both
subprojects ship from the same commit, ordinary CI is a single `ci.yml`;
only the tag-driven release stands apart.
| Workflow | Trigger | Result |
|----------|---------|--------|
| `ci` | push to `main` or PR touching `web/` or `android/` | see the job graph below |
| `android-release` | tag `v*.*.*` | versionCode guard, lint + tests, then a signed AAB + APK on the GH Release, plus Play Store closed-testing upload |
`ci` builds the web app exactly twice — once per base path — and the Android
job builds independently from the same checkout:
```
test (npm test)
└── build (root, base "") ── deploy-firebase push to main
│ └─ preview-firebase PR from this repo
└── build (gh, base /loto) ── deploy-pages push to main
android-debug (gradle lint + test + assembleDebug) ── unsigned APK artifact
```
Nothing deploys unless `test` is green. Deploy jobs are skipped on pull
requests, and the Firebase preview is skipped for PRs from forks, which
cannot read the service-account secret.
Shared toolchain setup lives in `.github/actions/setup-web` (Node) and
`.github/actions/setup-android` (JDK 21 + Gradle caching) — composite actions
used by both workflows.
Release/secret setup for the Play Store pipeline is documented in
[`docs/play-store-publishing.md`](docs/play-store-publishing.md).
## History
This repository is the merge of the former `tiennm99/loto` (now `web/`) and
`tiennm99/loto-android` (now `android/`). Both histories are preserved commit
for commit and interleaved in commit-time order under a single root, so
`git log` reads as one timeline and `git log -- web/` or `git log -- android/`
each resolve on the current paths.
`android/` has lived three lives: a native Kotlin/Compose port (retired
2026-05-10), a Capacitor WebView wrapper around the web app, and — since
2026-08-31 — a fresh native Kotlin/Compose rewrite that replaced the wrapper.
All three are preserved in history.
## License
Apache-2.0 — see [`LICENSE`](LICENSE).