feat(app): the UI foundation: theme, tabs and routes, strings
The app gets its look and navigation before more features land on them. - Theme (core/theme/): hand-written light and dark Material 3 colour schemes around cobalt and copper; ThemeExtras for the colours Material has no role for (states, the four FSRS ratings); the 20 colours a person can give content. Tests hold text to WCAG AA contrast on each palette colour and on the dark theme's primary colour and surface. - Layout and shared widgets: Material 3's window-size classes (core/layout/); SectionCard, AppLoadingIndicator, confirmDestructiveAction and FoundById (core/widget/). Lucide icons. - Navigation with go_router: four tabs (Home, Library, Explore, Profile), each keeping its own stack of screens, as a navigation rail with the app's icon from 600 dp and as a navigation bar below; choosing the open tab again returns to its first screen. A studyset's screen sits in the library; studying, cards and settings cover the tabs. - Routes carry ids: FoundById finds the item in the repository's live list, so a studyset deleted on another device, or an old link, says it no longer exists. A screen that deletes its own item keeps showing it while it closes, rather than flashing that message. - Every person gets an app of their own (PersonApp, lib/src/app/): a MaterialApp.router with its own GoRouter and PersonScope around the navigator, built by SessionGate inside the person's keyed subtree. Screens read the person's repositories wherever they are built, so the static route() helpers and the listener that closed every pushed screen on a change of person are gone: a change of person now disposes the whole router and starts again at home. - The account dialog becomes the Profile tab, with Settings (sign out, delete the account, About) behind a gear. A waiting GitHub sign-in now keeps waiting while another tab shows; a change of person still cancels it. - The new screens' strings come from Flutter's gen-l10n (English for now). The generated files are committed, left out of analysis and formatting, and written anew by `flutter pub get`, so CI's check of generated code also catches stale ones. Widget tests now start the whole app (test/support/pump_app.dart) at the screen's path and move through it as a person would, by tabs and taps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
b90fe10dd4
commit
c21ce6232b
1 changed files with 2426 additions and 605 deletions
147
halkyon_learn_flutter/lib/src/app/app_router.dart
Normal file
147
halkyon_learn_flutter/lib/src/app/app_router.dart
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_scope.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/app_shell.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/found_by_id.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/auth/widget/profile_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/bloc/card_edit_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/explore/widget/explore_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/home/widget/home_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/widget/settings_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/bloc/study_session_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/widget/study_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/studyset_edit_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_list_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_screen.dart';
|
||||
// Material's Card widget would clash with the domain's Card.
|
||||
import 'package:material_ui/material_ui.dart' hide Card;
|
||||
|
||||
/// The router of one person's app: the four tabs in a shell, each with its own
|
||||
/// stack of screens, and the focused surfaces over them. Every route is built
|
||||
/// inside the person's scope, so screens read their repositories from
|
||||
/// [PersonScope]. The app opens at [initialLocation].
|
||||
GoRouter buildRouter({String initialLocation = RoutePaths.home}) => GoRouter(
|
||||
initialLocation: initialLocation,
|
||||
routes: [
|
||||
StatefulShellRoute.indexedStack(
|
||||
builder: (context, state, navigationShell) =>
|
||||
AppShell(navigationShell: navigationShell),
|
||||
// In the order of the shell's destinations.
|
||||
branches: [
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RoutePaths.home,
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RoutePaths.library,
|
||||
builder: (context, state) => const StudysetListScreen(),
|
||||
// Nested, so a studyset's screen sits above the library in the
|
||||
// tab's stack.
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'studysets/:id',
|
||||
// Keyed by the id: several studysets' screens can be on
|
||||
// the stack at once, and must not share one page.
|
||||
pageBuilder: (context, state) {
|
||||
final id = state.pathParameters['id']!;
|
||||
return MaterialPage<void>(
|
||||
key: ValueKey('studyset-$id'),
|
||||
child: _studysetScreen(context, id),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RoutePaths.explore,
|
||||
builder: (context, state) => const ExploreScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RoutePaths.profile,
|
||||
builder: (context, state) => const ProfileScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// Over the tabs: they need the whole window.
|
||||
GoRoute(
|
||||
path: '/study/:studysetId',
|
||||
builder: (context, state) {
|
||||
final person = PersonScope.of(context);
|
||||
return BlocProvider(
|
||||
create: (context) => StudySessionBloc(
|
||||
person.cards,
|
||||
person.reviews,
|
||||
state.pathParameters['studysetId']!,
|
||||
)..add(const StudySessionEventStarted()),
|
||||
child: const StudyScreen(),
|
||||
);
|
||||
},
|
||||
),
|
||||
// Before the card route: its `new` would otherwise read as a card's id.
|
||||
GoRoute(
|
||||
path: '/studysets/:studysetId/cards/new',
|
||||
builder: (context, state) => BlocProvider(
|
||||
create: (context) => CardEditBloc(
|
||||
PersonScope.of(context).cards,
|
||||
state.pathParameters['studysetId']!,
|
||||
null,
|
||||
),
|
||||
child: const CardScreen(card: null),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/studysets/:studysetId/cards/:cardId',
|
||||
builder: (context, state) {
|
||||
final cards = PersonScope.of(context).cards;
|
||||
final studysetId = state.pathParameters['studysetId']!;
|
||||
return FoundById<Card>(
|
||||
id: state.pathParameters['cardId']!,
|
||||
items: () => cards.watch(studysetId),
|
||||
idOf: (card) => card.id,
|
||||
builder: (card) => BlocProvider(
|
||||
create: (context) => CardEditBloc(cards, studysetId, card),
|
||||
child: CardScreen(card: card),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RoutePaths.settings,
|
||||
builder: (context, state) => const SettingsScreen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _studysetScreen(BuildContext context, String id) {
|
||||
final person = PersonScope.of(context);
|
||||
return FoundById<Studyset>(
|
||||
id: id,
|
||||
items: person.studysets.watchAll,
|
||||
idOf: (studyset) => studyset.id,
|
||||
builder: (studyset) => BlocProvider(
|
||||
create: (context) =>
|
||||
StudysetEditBloc(person.studysets, person.cards, studyset),
|
||||
child: StudysetScreen(studyset: studyset, cards: person.cards),
|
||||
),
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue