feat(learn): a split of Topics, Introduction and Bibliography beside the material
As Tomorrow University's challenges have it (the developer's decisions of 10 Nov 2026): beside a studyset's material, one at a time, its topics, its introduction (the description, whole; left out where there is none) and its bibliography, read and changed in place. In the side column from expanded windows on, in the topics' sheet below; it opens on Topics, at every level. The description no longer stands above the material, nor the bibliography's button at the foot of the topics. The pill between the material's parts becomes PillSwitch, shared by both. Whats-New: A studyset's topics, introduction and bibliography now share one panel beside its material, a tap apart. Whats-New-De: Themen, Einführung und Literatur eines Lernsets teilen sich jetzt ein Feld neben dem Material, einen Tipp voneinander entfernt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
a3b124ddcb
commit
bed39dc2a2
2 changed files with 369 additions and 363 deletions
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart
|
|||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_side_panel.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
|
|
@ -138,6 +139,14 @@ void main() {
|
|||
studyButtonBackFirstKey: true,
|
||||
});
|
||||
await showApp(tester, RoutePaths.studyset('lesson'));
|
||||
// The description is the split's Introduction (decided 10 Nov 2026).
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byKey(StudysetSidePanel.switchKey),
|
||||
matching: find.text('Introduction'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Verbs'));
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -150,6 +159,14 @@ void main() {
|
|||
testWidgets('a button whose studyset is gone says so', (tester) async {
|
||||
describedWithButton({studyButtonStudysetKey: 'gone'});
|
||||
await showApp(tester, RoutePaths.studyset('lesson'));
|
||||
// The description is the split's Introduction (decided 10 Nov 2026).
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byKey(StudysetSidePanel.switchKey),
|
||||
matching: find.text('Introduction'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Study'));
|
||||
await tester.pump();
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_navig
|
|||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_parts.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_rail.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_side_panel.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
|
@ -152,6 +153,19 @@ void main() {
|
|||
|
||||
/// Opens the studyset titled [title] from the library, so its screen has
|
||||
/// the library below it.
|
||||
/// The part [name] of the split beside the material (Topics,
|
||||
/// Introduction, Bibliography).
|
||||
Finder sidePart(String name) => find.descendant(
|
||||
of: find.byKey(StudysetSidePanel.switchKey),
|
||||
matching: find.text(name),
|
||||
);
|
||||
|
||||
/// Shows the part [name] of the split beside the material.
|
||||
Future<void> chooseSide(WidgetTester tester, String name) async {
|
||||
await tester.tap(sidePart(name));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> openFromLibrary(
|
||||
WidgetTester tester,
|
||||
String title, {
|
||||
|
|
@ -349,19 +363,26 @@ void main() {
|
|||
expect(step('Japanese'), findsOneWidget);
|
||||
expect(step('Kana'), findsOneWidget);
|
||||
expect(step('›'), findsNWidgets(2));
|
||||
// What it is about is a part of the split beside the material, which
|
||||
// opens on the topics (the developer's decisions of 10 Nov 2026).
|
||||
expect(
|
||||
find.textContaining('The two syllabaries.', findRichText: true),
|
||||
findsOneWidget,
|
||||
findsNothing,
|
||||
);
|
||||
// Short, it shows whole.
|
||||
expect(find.text('Read more'), findsNothing);
|
||||
expect(find.text('Topics'), findsOneWidget);
|
||||
expect(sidePart('Topics'), findsOneWidget);
|
||||
expect(find.text('Hiragana'), findsOneWidget);
|
||||
expect(find.text('All topics'), findsOneWidget);
|
||||
// What the topic and all topics hold: nothing.
|
||||
expect(find.text('Empty'), findsNWidgets(2));
|
||||
expect(find.text('Flashcards'), findsOneWidget);
|
||||
expect(find.text('No flashcards yet'), findsOneWidget);
|
||||
|
||||
await chooseSide(tester, 'Introduction');
|
||||
expect(
|
||||
find.textContaining('The two syllabaries.', findRichText: true),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Hiragana'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a step of the way leads up there, and back leads further '
|
||||
|
|
@ -618,11 +639,11 @@ void main() {
|
|||
expect(find.byTooltip('Copy'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('the bibliography is a button at the foot of the topics, not '
|
||||
"in the app bar: in the side bar, and on a phone in the topics' sheet", (
|
||||
tester,
|
||||
) async {
|
||||
stored('japanese', 'Japanese');
|
||||
testWidgets('beside the material, a split of Topics, Introduction and '
|
||||
"Bibliography, as Tomorrow University's challenges (the developer's "
|
||||
'decisions of 10 Nov 2026): in the side column, and on a phone in the '
|
||||
"topics' sheet; not in the app bar", (tester) async {
|
||||
stored('japanese', 'Japanese', description: paragraph('Learn it all.'));
|
||||
await openFromLibrary(tester, 'Japanese');
|
||||
expect(
|
||||
find.descendant(
|
||||
|
|
@ -631,50 +652,69 @@ void main() {
|
|||
),
|
||||
findsNothing,
|
||||
);
|
||||
for (final name in ['Topics', 'Introduction', 'Bibliography']) {
|
||||
expect(sidePart(name), findsOneWidget);
|
||||
}
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(StudysetRail),
|
||||
matching: find.byTooltip('Bibliography'),
|
||||
),
|
||||
findsOneWidget,
|
||||
tester.getTopLeft(sidePart('Topics')).dx,
|
||||
lessThan(tester.getTopLeft(sidePart('Introduction')).dx),
|
||||
);
|
||||
expect(find.byType(StudysetRail), findsOneWidget, reason: 'on Topics');
|
||||
|
||||
await showApp(
|
||||
tester,
|
||||
location: RoutePaths.studyset('japanese'),
|
||||
width: 400,
|
||||
);
|
||||
expect(find.byTooltip('Bibliography'), findsNothing);
|
||||
expect(find.byKey(StudysetSidePanel.switchKey), findsNothing);
|
||||
expect(
|
||||
find.textContaining('Learn it all.', findRichText: true),
|
||||
findsNothing,
|
||||
reason: 'no longer above the material',
|
||||
);
|
||||
await tester.tap(find.byTooltip('Show topics'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(BottomSheet),
|
||||
matching: find.byTooltip('Bibliography'),
|
||||
matching: find.byKey(StudysetSidePanel.switchKey),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
await chooseSide(tester, 'Introduction');
|
||||
expect(
|
||||
find.textContaining('Learn it all.', findRichText: true),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('in the side bar, the bibliography shares the foot of the '
|
||||
'topics with New sub-studyset: that at the start, the bibliography at '
|
||||
'the end', (tester) async {
|
||||
testWidgets('without a description, the split leaves Introduction out', (
|
||||
tester,
|
||||
) async {
|
||||
stored('japanese', 'Japanese');
|
||||
stored('kana', 'Kana', parent: 'japanese');
|
||||
await showApp(tester, location: RoutePaths.studyset('japanese'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final rail = tester.getRect(find.byType(StudysetRail));
|
||||
final create = tester.getRect(
|
||||
find.ancestor(
|
||||
of: find.text('New sub-studyset'),
|
||||
matching: find.byWidgetPredicate((widget) => widget is FilledButton),
|
||||
),
|
||||
expect(sidePart('Topics'), findsOneWidget);
|
||||
expect(sidePart('Introduction'), findsNothing);
|
||||
expect(sidePart('Bibliography'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a topic shows the split too: its own introduction, and the '
|
||||
'bibliography it keeps', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
stored(
|
||||
'kana',
|
||||
'Kana',
|
||||
parent: 'japanese',
|
||||
description: paragraph('The two syllabaries.'),
|
||||
);
|
||||
final bibliography = tester.getRect(find.byTooltip('Bibliography'));
|
||||
expect(bibliography.center.dy, moreOrLessEquals(create.center.dy));
|
||||
expect(create.left, moreOrLessEquals(rail.left + 16));
|
||||
expect(bibliography.right, moreOrLessEquals(rail.right - 16));
|
||||
await showApp(tester, location: RoutePaths.studyset('kana'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
for (final name in ['Topics', 'Introduction', 'Bibliography']) {
|
||||
expect(sidePart(name), findsOneWidget);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('in the side bar, New sub-studyset shows whole', (
|
||||
|
|
@ -693,8 +733,8 @@ void main() {
|
|||
expect(label.didExceedMaxLines, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('without topics, the foot is the same: one New sub-studyset '
|
||||
'at its start, the bibliography at its end', (tester) async {
|
||||
testWidgets('without topics, New sub-studyset stands below the empty '
|
||||
'list, the bibliography no longer at the foot', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showApp(tester, location: RoutePaths.studyset('japanese'));
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -707,40 +747,13 @@ void main() {
|
|||
matching: find.byWidgetPredicate((widget) => widget is FilledButton),
|
||||
),
|
||||
);
|
||||
final bibliography = tester.getRect(find.byTooltip('Bibliography'));
|
||||
expect(bibliography.center.dy, moreOrLessEquals(create.center.dy));
|
||||
expect(find.byTooltip('Bibliography'), findsNothing);
|
||||
expect(
|
||||
create.top,
|
||||
greaterThan(tester.getRect(find.text('No sub-studysets yet.')).bottom),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('on a phone, the bibliography shares the foot of the topics '
|
||||
'with New sub-studyset: that at the start, the bibliography at the '
|
||||
'end', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
stored('kana', 'Kana', parent: 'japanese');
|
||||
await showApp(
|
||||
tester,
|
||||
location: RoutePaths.studyset('japanese'),
|
||||
width: 400,
|
||||
);
|
||||
await tester.tap(find.byTooltip('Show topics'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final sheet = tester.getRect(find.byType(StudysetRail));
|
||||
final create = tester.getRect(
|
||||
find.ancestor(
|
||||
of: find.text('New sub-studyset'),
|
||||
matching: find.byWidgetPredicate((widget) => widget is FilledButton),
|
||||
),
|
||||
);
|
||||
final bibliography = tester.getRect(find.byTooltip('Bibliography'));
|
||||
expect(bibliography.center.dy, moreOrLessEquals(create.center.dy));
|
||||
expect(create.left, moreOrLessEquals(sheet.left + 16));
|
||||
expect(bibliography.right, moreOrLessEquals(sheet.right - 16));
|
||||
});
|
||||
|
||||
testWidgets('with two paths or more, Reorder beside the tabs lets them be '
|
||||
'dragged, until Done', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
|
|
@ -1161,10 +1174,8 @@ void main() {
|
|||
});
|
||||
|
||||
group('the bibliography', () {
|
||||
Future<void> open(WidgetTester tester) async {
|
||||
await tester.tap(find.byTooltip('Bibliography'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
Future<void> open(WidgetTester tester) =>
|
||||
chooseSide(tester, 'Bibliography');
|
||||
|
||||
testWidgets('starts empty, ready for a first source, and keeps what is '
|
||||
'added, cited as the device cites', (tester) async {
|
||||
|
|
@ -1696,8 +1707,8 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
testWidgets('a long description is cut short, the whole of it a tap on '
|
||||
'Read more away', (tester) async {
|
||||
testWidgets('a long description shows whole in Introduction, scrolling '
|
||||
'there', (tester) async {
|
||||
stored(
|
||||
'grammar',
|
||||
'Grammar',
|
||||
|
|
@ -1707,15 +1718,21 @@ void main() {
|
|||
);
|
||||
await showApp(tester, location: RoutePaths.studyset('grammar'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Read more'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(SheetScaffold),
|
||||
matching: find.text('Grammar'),
|
||||
),
|
||||
findsOneWidget,
|
||||
await chooseSide(tester, 'Introduction');
|
||||
|
||||
expect(find.text('Read more'), findsNothing);
|
||||
final text = find.textContaining('Line 40 of', findRichText: true);
|
||||
await tester.scrollUntilVisible(
|
||||
text,
|
||||
200,
|
||||
scrollable: find
|
||||
.descendant(
|
||||
of: find.byType(StudysetSidePanel),
|
||||
matching: find.byType(Scrollable),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
expect(text, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("the material's tabs say how many each holds, nothing on an "
|
||||
|
|
|
|||
Loading…
Reference in a new issue