# Overview
## Tech stack
- **Flutter 3.44.6** (pinned via `.fvmrc`; Dart SDK ≥3.9).
- **Riverpod** (`hooks_riverpod`) + `riverpod_generator` (`@riverpod`) for state/DI.
- **flutter_hooks** for local widget state.
- **graphql_flutter** + **graphql_codegen** for the Suwayomi GraphQL API.
- **freezed** + **json_serializable** for data classes.
- **go_router** + **go_router_builder** for typed routing.
- **Curated named-theme system** (no third-party theming lib): explicit Material 3 `ColorScheme`s built from theme-kit tokens + a brand component layer for gradients/glow — see [theming-l10n.md](theming-l10n.md).
- **scrollable_positioned_list** (a pinned fork) for the multi-image readers.
- **cached_network_image** + **flutter_cache_manager** for image fetches.
- **flutter_secure_storage** for credentials; **shared_preferences** for settings; **Hive** for the GraphQL cache store.
After adding `@riverpod` annotations or `.graphql` files: `dart run build_runner build --delete-conflicting-outputs`.
## Layered structure
```
lib/
main.dart # bootstrap: Hive, prefs, auth preload, runApp
src/
tsumiru_app.dart # root widget: GraphQLProvider + MaterialApp.router + theming
routes/ # go_router typed route tree (+ sub_routes/ part files)
global_providers/ # GraphQL clients, SharedPreferences, HiveStore, auth-type
graphql/ # shared schema + fragments + codegen output
constants/ # DBKeys (prefs registry), enums, urls, endpoints, sizes, gen assets
utils/ # context extensions, the SharedPreference mixins, AsyncValue helpers
widgets/ # shared widgets (ServerImage, manga covers, async buttons, shell nav)
l10n/ # ARB files + generated localizations
features// # per-feature: data/ (repository + .graphql), domain/ (typedefs), presentation/ (screens + controllers)
```
Each feature follows the same internal shape: `data/` (a repository class + a `@riverpod` factory + `.graphql` operations), `domain/` (mostly `typedef`s over GraphQL codegen fragments), `presentation/` (screens + `controller/` providers + `widgets/`).
## Conventions reused everywhere
These appear across every feature — learn them once:
- **Persisted setting:** add a case to `DBKeys` (with its default), declare a `@riverpod` notifier that mixes in `SharedPreferenceClientMixin` (or `SharedPreferenceEnumClientMixin` for enums), call `initialize(DBKeys.yourKey)` in `build()`. Read/persist is automatic via `ref.listenSelf`. See [shared-infrastructure.md](shared-infrastructure.md).
- **Repository:** a plain class taking a `GraphQLClient`, calling codegen extension methods (`client.query$Foo()`, `client.mutate$Bar(...)`), plus a `@riverpod` factory injecting `graphQlClientProvider`. See [data-layer.md](data-layer.md).
- **Domain types are `typedef`s** over generated fragments (`typedef MangaDto = Fragment$MangaDto`), not bespoke classes (except a few `freezed` ones).
- **Async UI:** `asyncValue.showUiWhenData(context, builder)` renders a shimmer while loading and an `Emoticons` error widget on error — skip custom loading/error handling.
- **Async mutations:** `AppUtils.guard(future, toast)` runs a future, surfaces errors as a toast, returns `T?`.
- **Strings:** all user-facing text goes through `context.l10n.someKey` (ARB/l10n). No hardcoded strings.
- **Sizes:** use `KEdgeInsets.X.size` / `KBorderRadius.X.radius` tokens, never hardcoded padding/radii.
- **Responsive:** `context.isDesktop` (width ≥ 1200), `context.isTablet` (≥ 600), `context.showNavbar` (> 800) — distinct thresholds, don't conflate.
## The big cross-cutting gotchas
- **`SharedPreferenceEnumClientMixin` stores enums by index**, not name — reordering enum cases silently corrupts stored prefs (`ReaderMode`, `MangaSort`, `DisplayMode`, etc.).
- **`DBKeys` key string = the enum case `.name`** — renaming a case is a breaking migration.
- **Two GraphQL clients** (HTTP + WebSocket). WebSocket auth must be on the socket (`initialPayload`/`handshakeHeaders`), never a `Link`. See [auth.md](auth.md) / [data-layer.md](data-layer.md).
- **Several "fetch" operations are mutations** (`fetchChapters`, `FetchExtensionList`) — calling them triggers a server-side source refresh, not just a DB read.