feat(learn): a studyset's subject, picked from a fixed list (ADR 0041)

A top studyset is filed under one of 44 subjects in six groups (the
developer's decisions of 9 and 10 Nov 2026): languages as one, computing
in five, Other / not sure last. Each has a Lucide icon and a default
colour, which picking it gives the studyset, its own to change from then
on. The studyset's sheet picks it (pickSubject); a sub-studyset shows its
top studyset's (subjectOf) and offers none. The payload keeps it as
`subject` (schema 8), so archives and published studysets carry it; an
ID this version doesn't know is kept and reads as none.

Whats-New: A studyset can now be filed under a subject, which gives it that subject's colour.
Whats-New-De: Ein Lernset lässt sich jetzt einem Fach zuordnen, das ihm dessen Farbe gibt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-10 18:37:17 +01:00
commit 9868dc27c6
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 1766 additions and 10 deletions

View file

@ -14,6 +14,7 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/deleted_studyset_card.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_card.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/subject_picker.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
@ -74,6 +75,7 @@ void main() {
DateTime? archivedAt,
DateTime? starredAt,
String? publishedAt,
String? subject,
bool private = false,
}) => gateway.receiveRemoteChange(
id,
@ -86,6 +88,7 @@ void main() {
? null
: const PublishedRef(publication: 'p', item: 'i', version: 1),
publishedAt: publishedAt,
subject: subject,
),
),
archivedAt: archivedAt,
@ -267,6 +270,109 @@ void main() {
);
});
group("a studyset's subject (the developer's decisions of 9 and 10 Nov "
'2026)', () {
Future<void> pickSubject(WidgetTester tester, String name) async {
await tester.tap(find.text('Choose a subject'));
await tester.pumpAndSettle();
final row = find.widgetWithText(ListTile, name);
await tester.scrollUntilVisible(
row,
200,
scrollable: find
.descendant(
of: find.byType(SheetScaffold).last,
matching: find.byType(Scrollable),
)
.first,
);
await tester.pumpAndSettle();
await tester.tap(row);
await tester.pumpAndSettle();
}
/// The subject the sheet's field shows.
Finder subjectShown(String name) => find.descendant(
of: find.ancestor(
of: find.byType(SubjectBadge),
matching: find.byType(ListTile),
),
matching: find.text(name),
);
testWidgets('is picked from the subjects in their groups, and gives the '
'studyset its colour', (tester) async {
final semantics = tester.ensureSemantics();
await showLibrary(tester);
await tester.tap(find.text('Create studyset'));
await tester.pumpAndSettle();
await tester.enterText(find.widgetWithText(TextField, 'Title'), 'Kanji');
await tester.tap(find.text('Choose a subject'));
await tester.pumpAndSettle();
expect(find.text('Sciences and health'), findsOneWidget);
await tester.tap(find.byTooltip('Close').last);
await tester.pumpAndSettle();
await pickSubject(tester, 'Languages');
expect(subjectShown('Languages'), findsOneWidget);
expect(find.bySemanticsLabel('Color: Cerise'), findsOneWidget);
await tester.tap(find.text('Create'));
await tester.pumpAndSettle();
final content = decodeStudysetPayload(gateway.writes.single.$2);
expect(content.subject, 'languages');
expect(content.colorName, 'cerise');
semantics.dispose();
});
testWidgets("its colour stays the studyset's to change", (tester) async {
final semantics = tester.ensureSemantics();
await showLibrary(tester);
await tester.tap(find.text('Create studyset'));
await tester.pumpAndSettle();
await tester.enterText(find.widgetWithText(TextField, 'Title'), 'Rust');
await pickSubject(tester, 'Software engineering');
await tester.tap(find.bySemanticsLabel('Color: Enzian'));
await tester.pumpAndSettle();
await tester.tap(find.bySemanticsLabel('Fern'));
await tester.pumpAndSettle();
await tester.tap(find.text('Create'));
await tester.pumpAndSettle();
final content = decodeStudysetPayload(gateway.writes.single.$2);
expect(content.subject, 'software-engineering');
expect(content.colorName, 'fern');
semantics.dispose();
});
testWidgets('editing shows the one chosen, and keeps one this app '
'version does not know', (tester) async {
stored('origami', 'Origami', subject: 'paper-folding');
stored('japanese', 'Japanese', subject: 'languages');
await showLibrary(tester);
await choose(tester, 'Japanese', 'Edit');
expect(subjectShown('Languages'), findsOneWidget);
await tester.tap(find.byTooltip('Close'));
await tester.pumpAndSettle();
await choose(tester, 'Origami', 'Edit');
expect(find.text('Choose a subject'), findsOneWidget);
await tester.enterText(
find.widgetWithText(TextField, 'Origami'),
'Paper',
);
await tester.pump();
await tester.tap(find.text('Save'));
await tester.pumpAndSettle();
expect(
decodeStudysetPayload(gateway.writes.last.$2).subject,
'paper-folding',
);
});
});
testWidgets('creating waits for a title', (tester) async {
await showLibrary(tester);
await tester.tap(find.text('Create studyset'));

View file

@ -396,6 +396,11 @@ void main() {
tester.getRect(find.text('Inherit')).left,
greaterThan(tester.getRect(find.byType(PaletteColorPicker)).right),
);
expect(
find.text('Choose a subject'),
findsNothing,
reason: "a sub-studyset's subject is its top studyset's",
);
await tester.enterText(find.widgetWithText(TextField, 'Title'), 'Kana');
await tester.pump();
await tester.tap(find.text('Create'));