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>
2026-10-01 00:18:31 +02:00
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/app/person_app.dart';
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/auth/widget/session_gate.dart';
|
feat(studyset): Material, Progress and Study plan in place of the tabs
The studyset's four tabs become three parts, as Vaia has them, chosen
with a switch in the header that ticks as the navigation bar does: below
expanded windows in a line of its own under the title, above the path
and Topics; from there on at the end of the path's line, which the
topics leave free there.
- Material: the flashcards, notes and files one below the other, the
first three of each with Show all; Browse, the tag filter (where all
cards show, or while they are narrowed) and Reorder in each section's
header; an outlined box where a section is empty.
- Progress, once anything in the studyset was studied (a card rated, a
path finished): how much is learnt (mature cards), how many studied
cards are due today, and how long it was studied this week on this
device, with a line of the seven days (StudysetActivityCubit, from the
reviews' durations).
- Study plan: a ring of the paths finished, Next up (study the cards due,
go on with the first path not finished), then the paths.
It opens on the part it was left on; a tab kept before counts as its
part. Opened for the first time, it opens on the study plan while a path
is unfinished, else on the material. Material added lands in its part.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-12 13:48:11 +02:00
|
|
|
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/content_tab_cubit.dart';
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_parts.dart';
|
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>
2026-10-01 00:18:31 +02:00
|
|
|
import 'package:material_ui/material_ui.dart';
|
|
|
|
|
|
|
|
|
|
/// Pumps the whole app as a person starts it, on [dependencies] (usually
|
|
|
|
|
/// `testDependencies(...)`): signed in, anonymously unless the auth fake says
|
|
|
|
|
/// otherwise, and opened at [location]. Settles, so that the person's data is
|
|
|
|
|
/// open and the first screen shows; the app's spinner animates without end,
|
|
|
|
|
/// so settling needs the fakes to have answered.
|
|
|
|
|
///
|
|
|
|
|
/// Every person's app opens at [location], also after a change of person.
|
|
|
|
|
Future<void> pumpApp(
|
|
|
|
|
WidgetTester tester,
|
|
|
|
|
Dependencies dependencies, {
|
|
|
|
|
String location = RoutePaths.home,
|
|
|
|
|
}) async {
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
DependenciesScope(
|
|
|
|
|
dependencies: dependencies,
|
|
|
|
|
child: SessionGate(
|
|
|
|
|
app: (person) => PersonApp(person: person, initialLocation: location),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The tab labelled [label] in the app's navigation, rail or bar. Its label
|
|
|
|
|
/// alone could also be a screen's title.
|
|
|
|
|
Finder tab(String label) => find.descendant(
|
|
|
|
|
of: find.byWidgetPredicate(
|
|
|
|
|
(widget) => widget is NavigationRail || widget is NavigationBar,
|
|
|
|
|
),
|
|
|
|
|
matching: find.text(label),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/// Chooses the tab labelled [label] and settles.
|
|
|
|
|
Future<void> openTab(WidgetTester tester, String label) async {
|
|
|
|
|
await tester.tap(tab(label));
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
}
|
feat(studyset): Material, Progress and Study plan in place of the tabs
The studyset's four tabs become three parts, as Vaia has them, chosen
with a switch in the header that ticks as the navigation bar does: below
expanded windows in a line of its own under the title, above the path
and Topics; from there on at the end of the path's line, which the
topics leave free there.
- Material: the flashcards, notes and files one below the other, the
first three of each with Show all; Browse, the tag filter (where all
cards show, or while they are narrowed) and Reorder in each section's
header; an outlined box where a section is empty.
- Progress, once anything in the studyset was studied (a card rated, a
path finished): how much is learnt (mature cards), how many studied
cards are due today, and how long it was studied this week on this
device, with a line of the seven days (StudysetActivityCubit, from the
reviews' durations).
- Study plan: a ring of the paths finished, Next up (study the cards due,
go on with the first path not finished), then the paths.
It opens on the part it was left on; a tab kept before counts as its
part. Opened for the first time, it opens on the study plan while a path
is unfinished, else on the material. Material added lands in its part.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-12 13:48:11 +02:00
|
|
|
|
|
|
|
|
/// The part of the studyset screen shown.
|
|
|
|
|
StudysetPart shownPart(WidgetTester tester) =>
|
|
|
|
|
tester.widget<StudysetPartSwitch>(find.byType(StudysetPartSwitch)).current;
|
|
|
|
|
|
|
|
|
|
/// Shows the studyset screen's part [label] ("Material", "Study plan") and
|
|
|
|
|
/// settles.
|
|
|
|
|
Future<void> openPart(WidgetTester tester, String label) async {
|
|
|
|
|
await tester.tap(
|
|
|
|
|
find.descendant(
|
|
|
|
|
of: find.byType(StudysetPartSwitch),
|
|
|
|
|
matching: find.text(label),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
}
|