Track the anime you watch — from the tablet you actually watch it on.
An offline-first Android companion for Autoreas Desktop, built to sit next to your video player in split-screen and stay out of your way.
Overview · Features · Screenshots · Install · Stack · Getting started · Structure · Docs
Autoreas Desktop is a Chapter Tracking System (SCC) for anime with 800+ records and years of active use. It is legacy software — Electron 7, last released in 2020 — and it will not be updated again. Meanwhile, the way it gets used moved from the desktop to the tablet.
Autoreas Bridge solves half of that: a service on the PC that watches animes.dat, exposes a
REST + WebSocket API, and enables bidirectional sync over the local Wi-Fi network.
Autoreas Mobile is the other half — the client that makes those records usable from the couch.
The concrete workflow it replaces: watching an episode on the tablet in split-screen, then writing
the chapter number into a scratch note (AnimeName[DayCode]Chapters) to transcribe into the desktop
app later. Error-prone, duplicated work.
With Autoreas Mobile, the video player sits on the left and Autoreas on the right (~360–400dp wide). When an episode ends, you tap Cap+ without pausing the video. That is the whole interaction.
| Principle | What it means in practice |
|---|---|
| Offline-first | The catalogue lives in local SQLite. Cap+ works with the Bridge unreachable; changes replay on reconnect. |
| Invisible sync | Silent on success, loud only on failure or conflict. |
| Optimistic ignorance | A remote WebSocket event never blindly overwrites a pending local mutation. |
| Split-screen native | The layout targets ~320dp width. No horizontal scroll, Cap+/Cap- always one tap away. |
Note
Scope. This is not a replacement for Autoreas Desktop. It does not add, edit or delete anime, show statistics, or manage backups. It does not display cover art (the desktop stores Windows local paths). Sync conflicts are surfaced here but resolved in the Bridge web UI.
Weekday rail with the Estrenos pseudo-days (Sin ver / Ver hoy / Visto), one-tap chapter controls, and a live Bridge connection banner.
Pairing details plus a runtime snapshot of background sync — execution mode, last attempt, last success, backlog read, and whether the persistent foreground service is actually alive. (Host and device ID are masked in this screenshot.)
- Weekday-based lists — Monday through Sunday, matching the desktop broadcast-day model.
- Estrenos workflow — move new-season titles across the Sin ver → Ver hoy → Visto pseudo-days.
- One-tap chapter tracking — Cap+ / Cap- always visible on the card, never behind a submenu.
- Status machine — Viendo, Finalizado, No me gustó, En pausa. Reaching the last chapter
(
nrocapvisto == totalcap) auto-transitions the anime to Finalizado in the same transaction, while a manual status change persists and syncs only the status: it never rewrites watched chapters, the last-viewed date or the first-cycle metadata. - Season ratings — rate active-season titles; ratings queue and sync like any other mutation.
- Local operation log — every mutation writes to
animesandoperation_login one atomic SQLite transaction. Both succeed or both fail. - REST reconciler — pending operations batch to
POST /api/sync/reconcile; only a confirmed response marks themsyncedand advanceslast_changelog_id. - WebSocket lifecycle —
WS /wsis bound to React Native'sAppState: disconnect on background, reconnect on active. Remote events trigger reconciliation instead of blind writes. - Continuous background sync — an Android foreground service with a persistent notification,
backed by a repository-owned Kotlin Expo module (
modules/foreground-sync-ticker), with Expo Background Task as the best-effort fallback. - Log retention — the operation log is pruned automatically once entries are confirmed.
- Manual pairing — IP, port and token validated against
POST /api/devices/pair. - QR scan —
expo-camerareads the pairing code emitted by the Bridge. - Deep link —
autoreas-mobile://pair?v=1pre-fills the form with ip, port and token. - Instant boot —
bridge_configis read synchronously from SQLite in_layout.tsx, so a cold start goes straight to the list with no white flash and no setup detour. - Local mode — with no Bridge reachable, the app keeps serving the local catalogue copy.
You do not have to build anything. Every release publishes a ready-to-install APK:
⬇ Download the latest release ·
autoreas-mobile-X.Y.Z-android.apk, about 132 MiB
-
Download the APK and
SHA256SUMS-android.txtfrom the release page. -
Verify it — the checksum file is published for exactly this reason:
sha256sum -c SHA256SUMS-android.txt
-
Copy the APK to your Android device and open it. Android will ask you to allow installation from unknown sources, and Play Protect will scan it: the app is signed, but it is not distributed through Google Play, so both prompts are expected.
-
Open the app and pair it with your Bridge.
Note
Android only, and a Bridge is required. There is no iOS build and none is planned. The app is a companion to Autoreas Desktop — without a reachable Bridge on the same Wi-Fi network it starts in local mode with an empty catalogue.
The rest of this README is for working on the app, not using it. How a release is produced and published is documented in Deployment.
| Layer | Choice | Why |
|---|---|---|
| Runtime | Expo SDK 55 · React Native 0.83 · React 19 | Reuses the React knowledge behind the Bridge web UI; one toolchain for native builds. |
| Language | TypeScript 5.9 | Contract safety across the sync boundary. |
| Navigation | Expo Router | File-based routes; src/app/** stays a thin delivery layer. |
| UI | HeroUI Native · Uniwind · Tailwind CSS 4 | Native primitives with design tokens shared with the Bridge web UI. |
| Local database | Expo SQLite · Drizzle ORM | Typed schema, generated migrations, live queries. |
| Server state | TanStack Query | Bridge fetch and cache lifecycle. |
| Client state | Zustand | Small in-memory global state with no boot cost. |
| Validation | Zod 4 | Coerces the legacy animes.dat wire format at the boundary. |
| Native | Kotlin Expo module · react-native-notify-kit | Foreground service and persistent notification for continuous sync. |
| Testing | Jest · jest-expo · Testing Library · Stryker | TDD is mandatory here; mutation testing guards the tests themselves. |
| Quality | ESLint 9 · dharness · dlinter-ts-react · fallow · Lefthook |
Architecture rules enforced at commit time, not by review etiquette. |
| Package manager | Bun | The only supported package manager for this repo. |
Autoreas Mobile is an offline-first peer, not a thin client. It owns a full local copy of the catalogue plus a log of pending operations, and reconciles with the Bridge when it can.
flowchart LR
subgraph PC["🖥️ Windows PC"]
DAT[("animes.dat<br/>Autoreas Desktop")]
BRIDGE["<b>Autoreas Bridge</b><br/>REST + WebSocket"]
DAT <--> BRIDGE
end
subgraph TABLET["📱 Android Tablet"]
subgraph APP["<b>Autoreas Mobile</b>"]
UI["<b>src/app/**</b><br/>Expo Router · dumb UI"]
FEAT["<b>src/features/**</b><br/>hooks · pure helpers"]
CLIENT["<b>BridgeClient</b><br/>the only transport door"]
DB[("<b>SQLite + Drizzle</b><br/>animes · operation_log")]
FGS["<b>foreground-sync-ticker</b><br/>Kotlin Expo module"]
end
end
UI --> FEAT
FEAT --> DB
FEAT --> CLIENT
FGS -.->|wakes| FEAT
CLIENT <==>|"Wi-Fi LAN · REST + WS"| BRIDGE
classDef door fill:#0d47a1,stroke:#64b5f6,color:#ffffff
class CLIENT,DB door
Two boundaries are load-bearing:
- Bridge Boundary — all HTTP and WebSocket traffic goes through
BridgeClientinsrc/infrastructure/api. Feature code never callsfetch()ornew WebSocket(). - Write Door — every SQLite write routes through
withLocalWrite, so a mutation and its operation-log entry stay atomic.
Decisions are recorded as ADRs in docs/adr/; the full ruleset lives in
ARCHITECTURE.md.
- Bun — the only supported package manager
- Node.js 22+
- An Android device or emulator
- Docker Desktop for local Android APK builds
.env.localwithEXPO_TOKENfor the Docker build (EAS fetches signing credentials from Expo)- A reachable Autoreas Bridge instance on the same Wi-Fi network
Important
Expo Go is not enough. This app uses native expo-sqlite, a custom Kotlin module, and
cleartext HTTP to a LAN address. Plain Expo Go produces the SQLite-unavailable fallback or
Cannot find native module 'ExpoSQLite'. You need a development build.
git clone git@github.com:Disble/autoreas-mobile.git
cd autoreas-mobile
bun installbun install also installs the Git hooks through Lefthook's postinstall. If hooks go missing on an
already-installed tree, repair them with npx lefthook install — see
Git hooks.
docker compose -f docker-compose.eas.yml run --rm eas-build labThe debuggable lab APK for device diagnosis lands in dist/android/; never distribute it.
Install or upgrade it with adb install -r <path-to-lab-apk> to preserve app data. Do not
uninstall after a signer mismatch without first backing up or accepting the loss of local data.
For a development client that attaches to Metro, run the same Docker command with development
as the final argument. The local build contacts Expo for signing credentials; see
Local Android build with Docker for prerequisites and profiles.
Tagged releases are published through GitHub Actions; see Deployment.
bun run start # Metro bundler (clears cache)
bun run android # Metro + open Android
bun run ios # Metro + open iOS
bun run web # Metro + open WebIf using the development build, open it on the device and let it attach to Metro.
- Start the Bridge on your PC and note its LAN IP and port.
- Open Autoreas Mobile — a fresh install lands on the setup screen.
- Enter IP, port and token, or scan the Bridge QR code, or open the
autoreas-mobile://pair?v=1deep link. - On success the config is written synchronously to SQLite, and every later launch boots straight into your list.
autoreas-mobile/
├── src/
│ ├── app/ # Expo Router routes — delivery layer ONLY
│ │ ├── _layout.tsx # SQLiteProvider · migrations · boot gate
│ │ ├── (tabs)/ # index (catalogue) · settings
│ │ └── setup/ # pairing screen
│ ├── features/ # all business logic, one folder per domain
│ │ ├── animes/ # catalogue, Cap+/Cap-, status machine, seasons
│ │ ├── settings/ # bridge config + background-sync diagnostics
│ │ ├── setup/ # device pairing (manual · QR · deep link)
│ │ ├── startup/ # boot readiness gate
│ │ ├── sync/ # operation log · reconciler · foreground service
│ │ └── ws/ # WebSocket lifecycle
│ └── infrastructure/
│ ├── api/bridge-client/ # the ONLY transport adapter (HTTP + WS)
│ ├── db/ # Drizzle schema · migrations · write door
│ ├── store/ # Zustand stores
│ └── validation/ # Zod schemas for the legacy wire format
├── modules/
│ └── foreground-sync-ticker/ # local Android Expo module (Kotlin)
├── plugins/ # Expo config plugins (foreground service, Gradle memory)
├── tests/ # Jest suites — mirrors src/features/**
├── scripts/ # feature scaffolding, staged mutation runner
├── docs/ # ADRs · specs · postmortems · images
└── openspec/ # historical specifications and change evidence (not an execution contract)
Every non-trivial feature folder is self-contained and follows the same shape:
src/features/<feature>/ui/<Component>/
├── index.ts # public contract — the only import surface
├── <Component>.tsx # JSX only: HeroUI Native primitives + cn()
├── use-<component>.ts # all logic, in the mandated 10-step hook order
├── <component>.helpers.ts # pure functions
├── <component>.types.ts
└── <component>.constants.ts
Never create these by hand — scaffold them:
npm run generate:feature <name>| Command | What it does |
|---|---|
bun run start |
Start Metro with a cleared cache |
bun run android / ios / web |
Start Metro and open the target platform |
bun run lint |
ESLint across the repo |
bun run typecheck |
tsc --noEmit |
bun run test |
Jest suite |
bun run test:watch |
Jest in watch mode |
bun run test:coverage |
Jest with coverage |
bun run validate |
lint + typecheck + test |
bun run audit |
fallow dead-code, complexity and duplication audit |
bun run doctor:react |
React Doctor analysis |
bun run generate:feature <name> |
Scaffold a new feature folder |
bun run test:mutation:staged |
Stryker mutation run over staged files |
bun run sqlite:lab |
SQLite concurrency lab harness |
Tests are not colocated with source. Jest only picks up suites under tests/, mirroring the
feature path:
src/features/animes/anime-season.helpers.ts
└── tests/features/animes/__tests__/anime-season.helpers.test.ts
The cycle is RED → GREEN → MUTATE → REFACTOR. A green test proves nothing until you delete the guard it claims to cover and watch it fail. See ADR 003 — Testing policy.
Drizzle config lives in drizzle.config.ts. Migrations are generated into
src/infrastructure/db/migrations and applied at runtime from src/app/_layout.tsx.
bunx drizzle-kit generateThis repository enforces its architecture mechanically. Read ARCHITECTURE.md
before opening a PR — the short version:
- Dumb UI —
.tsxfiles return JSX and nothing else. NouseEffect, no business logic, no database access. - Hook anatomy —
use-*.tsfiles follow a fixed 10-step order: refs → state → context → queries → derived → callbacks → effects → return. - Strict colocation — a feature is a folder with
index.ts,.tsx,use-*.tsand*.helpers.ts. Usegenerate:feature; never hand-roll it. - TDD is mandatory — no helper or hook change without its test under
tests/first. - 500-line rule — any file crossing 500 lines gets refactored on the spot.
- Bridge Boundary — transport belongs to
src/infrastructure/apiand nowhere else. - Reference feature — when in doubt, copy the shape of
src/features/animes.
The pre-commit hook lints whole staged files, not your diff. Touching a file makes its existing lint findings yours to fix. That is deliberate: it is how the standing debt gets paid down incrementally instead of never.
| Document | Contents |
|---|---|
ARCHITECTURE.md |
The complete, enforced ruleset |
docs/adr/ |
Architecture decision records |
docs/specs/ |
Historical functional specifications (SDD-00 → SDD-07) |
docs/deployment.md |
How a release ships: version bump, tag, CI pipeline and its guards |
docs/build-and-release.md |
EAS and Docker builds, Git hooks, local build paths |
docs/local-android-build.md |
Building an APK locally with Docker: quick start, profiles, ABI configuration, troubleshooting |
docs/logbooks/ |
Append-only measurement records; build performance of the Docker build and the CI release |
docs/troubleshooting.md |
Native module, cleartext HTTP and Expo Router issues |
docs/postmortems/ |
Incident write-ups |
docs/Autoreas_mobile_design_doc.md |
The original RFC |
Licensed under the Apache License, Version 2.0. See LICENSE for the full text.
Copyright 2026 Disble
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
package.json still sets private: true — that is deliberate. It blocks accidental publication to
the npm registry and has nothing to do with the source license; this is an application, not a
distributable package.
Built by Disble · "apoyando la vagancia desde tiempos inmemoriales"