# App Shell & Navigation ## Purpose App bootstrap, the `go_router` typed route tree, and the stateful navigation shell (NavigationRail on tablet/desktop, bottom NavigationBar on phone). ## Key files | Path | Responsibility | |---|---| | `lib/main.dart` | Bootstrap: Hive init, auth migration, auth preload, `ProviderContainer` setup, `runApp` | | `lib/src/tsumiru_app.dart` | Root widget `TsumiruApp`; wires `GraphQLProvider`, `MaterialApp.router`, theming, locale, `ReauthBannerHost` | | `lib/src/routes/router_config.dart` | `@riverpod routerConfig` `GoRouter`; typed `ShellRoute`/`GoRoute` defs; `Routes` constants; navigator keys | | `lib/src/routes/router_config.g.dart` | Generated `$appRoutes` (build_runner) | | `lib/src/routes/sub_routes/*.dart` | `part` files, one per nav section | | `lib/src/widgets/shell/navigation_shell_screen.dart` | `NavigationShellScreen` — dispatches to big/small nav bar; runs update check on first mount | | `lib/src/widgets/shell/big_screen_navigation_bar.dart` | `BigScreenNavigationBar` — `NavigationRail` (extended on desktop, logo in leading) | | `lib/src/widgets/shell/small_screen_navigation_bar.dart` | `SmallScreenNavigationBar` — M3 bottom `NavigationBar` | | `lib/src/constants/navigation_bar_data.dart` | `NavigationBarData` — `phoneNavList`/`tabletNavList`, `getNavList(context)` | ## Bootstrap sequence (`main()`) 1. `WidgetsFlutterBinding.ensureInitialized()`, `PackageInfo.fromPlatform()`, `SharedPreferences.getInstance()`, `initHiveForFlutter()`. 2. `GoRouter.optionURLReflectsImperativeAPIs = true`. 3. `ProviderContainer` with three pre-seeded overrides: `packageInfoProvider`, `sharedPreferencesProvider`, `hiveStoreProvider`. 4. `migrateBasicAuthCredentials` — one-shot legacy basic-auth → secure storage migration (non-fatal). 5. `Future.wait([authCredentialsStoreProvider.future, credentialsProvider.future])` — both auth providers loaded **before first frame** (prevents tokenless image requests caching as 401s). 6. `container.read(authCoordinatorProvider.notifier)` — eagerly construct so the proactive-refresh listener is live before any image request. 7. `runApp(UncontrolledProviderScope(container, child: TsumiruApp()))`. Initial location: `/library/0`. `TsumiruApp.build()` wraps: `GraphQLProvider` → `MaterialApp.router` → `FToastBuilder` → `ReauthBannerHost`. ## Routing `go_router` with typed routes (`@TypedShellRoute` / `@TypedGoRoute` / `@TypedStatefulShellRoute`) generated into `router_config.g.dart`. The `GoRouter` itself is a `@riverpod` provider (`routerConfigProvider`). Route tree (outer → inner): ``` QuickSearchRoute (ShellRouteData — wraps everything in SearchStackScreen) NavigationShellRoute (StatefulShellRouteData — NavigationShellScreen) LibraryBranch → /library/:categoryId UpdatesBranch → /updates HistoryBranch → /history BrowserBranch → BrowseShellRoute (nested StatefulShellRoute: Sources / Extensions) DownloadsBranch → /downloads MoreBranch → /more → settings, about, ... MangaRoute → /manga/:mangaId (outside the shell) ReaderRoute → /manga/:mangaId/chapter/:chapterId GlobalSearchRoute, UpdateStatusRoute, Migration*Route ``` Navigator keys: `rootNavigatorKey`, `_quickOpenNavigatorKey` (also `$parentNavigatorKey` for `ReaderRoute`/`GlobalSearchRoute` → they present full-screen above the shell), `_shellNavigatorKey`, `_browseNavigatorKey`. Navigation is always typed: `const FooRoute(...).go(context)` / `.push(context)` — no string-based navigation. In wide layouts the navigation and detail shells wrap only the content column in `Scaffold`. Banners and snackbars render there, right of the rail and below the top bar. ## Screen-size split - **Phone** (< 600): `SmallScreenNavigationBar` (M3 bottom bar), 5 items (History omitted — lives under More). - **Tablet** (≥ 600, `context.isTablet`): `BigScreenNavigationBar` (`NavigationRail`), 6 items incl. History. - **Desktop** (≥ 1200, `context.isDesktop`): rail is **extended** (256px) with logo + title in `leading`. ## Desktop page frame `lib/src/widgets/desktop/desktop_page_frame.dart` supplies the shared page frame. `DesktopPageLayout.standard` adds a 16 px start (left in LTR) and top inset; `header` keeps the start inset and lets `DesktopPageHeader` add the 16 px gap below the header; `fullBleed` adds neither inset. Page-line helpers align content within these variants. Page scroll views reserve a global 14 px scrollbar gutter (10 px thumb plus 4 px clearance). Full-bleed scroll views keep edge-to-edge backgrounds and apply the clearance to their content instead. `desktop_top_bar.dart` keeps search and actions on one row. Search has a shared 200 px minimum and 320 px preferred width; icon actions form a group of 40 px buttons, with 10 px gaps between the search and action groups. ## Gotchas - **History index offset on phone.** The stateful shell always has 6 branches (0–5); phone shows 5 (History hidden). `NavigationShellScreen` has `getAdjustedIndex`/`getReverseAdjustedIndex` that ±1 for indices ≥ 2 on phone. Changing the nav item count must update these or the wrong branch is selected. - **Reader presents above the shell** via `_quickOpenNavigatorKey` as `$parentNavigatorKey` — nav bars are fully hidden while reading. - **`MangaRoute` is outside the stateful shell** (inside `QuickSearchRoute`) — manga detail renders without a nav bar. - **`NavigationBarData.navList`** (static field) is a stale legacy alias for `phoneNavList`; always use `getNavList(context)`. - **Browse is a tab-within-a-tab** — a nested `StatefulShellRoute` (Sources / Extensions) with its own navigator. - **`isTablet` uses `width`, not shortestSide** despite a misleading comment — a landscape phone ≥ 600px wide gets the rail layout.