feat(learn): the library on shelves by recency, narrowed to a subject (ADR 0041, D8)

The library as Vaia's (the developer's decisions of 10 Nov 2026): the
starred on a shelf of their own, then those studied or changed in the
last 7 days, the last month and before, each under its heading, the
archive last. Recent is the later of a card's last rating
(StudyProgress.lastStudied) and the studyset's own last change. A
dropdown narrows the library to one of the subjects in use, kept on the
device (DeviceSettings.librarySubject). A card names its subject under
its title, with its icon, and a language course its language beside.

Whats-New: The library groups your studysets by when you last used them, and narrows them to one subject.
Whats-New-De: Die Bibliothek ordnet deine Lernsets danach, wann du sie zuletzt genutzt hast, und zeigt auf Wunsch nur ein Fach.

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 19:08:02 +01:00
commit a3b124ddcb
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 786 additions and 26 deletions

View file

@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/testing.dart';
import 'package:halkyon_editor/halkyon_editor.dart';
import 'package:halkyon_learn_flutter/src/core/content/published_ref.dart';
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
@ -22,6 +23,7 @@ import 'package:material_ui/material_ui.dart';
import '../../../support/fake_card_gateway.dart';
import '../../../support/fake_publish_gateway.dart';
import '../../../support/fake_studyset_gateway.dart';
import '../../../support/memory_device_settings_store.dart';
import '../../../support/memory_published_store.dart';
import '../../../support/pump_app.dart';
import '../../../support/test_dependencies.dart';
@ -76,6 +78,7 @@ void main() {
DateTime? starredAt,
String? publishedAt,
String? subject,
DateTime? updatedAt,
bool private = false,
}) => gateway.receiveRemoteChange(
id,
@ -93,6 +96,7 @@ void main() {
),
archivedAt: archivedAt,
starredAt: starredAt,
updatedAt: updatedAt,
isPrivate: private,
);
@ -373,6 +377,133 @@ void main() {
});
});
group("shelved and narrowed as Vaia's library (ADR 0041, D8, the "
"developer's decisions of 10 Nov 2026)", () {
final now = DateTime.now().toUtc();
DateTime daysAgo(int days) => now.subtract(Duration(days: days));
testWidgets('the starred on top, then the last 7 days, the last month '
'and before, each under its heading; the archive last', (
tester,
) async {
stored('old', 'Old', updatedAt: daysAgo(90));
stored('recent', 'Recent', updatedAt: daysAgo(1));
stored('weeks', 'Weeks', updatedAt: daysAgo(14));
stored('star', 'Star', updatedAt: daysAgo(200), starredAt: daysAgo(1));
stored('rest', 'Rest', updatedAt: daysAgo(2), archivedAt: daysAgo(2));
await showLibrary(tester);
double topOf(Finder finder) => tester.getTopLeft(finder).dy;
final order = [
'Starred',
'Star',
'Last 7 days',
'Recent',
'Last month',
'Weeks',
'Older',
'Old',
'Archived',
];
for (final (before, after) in [
for (var i = 1; i < order.length; i++) (order[i - 1], order[i]),
]) {
expect(
topOf(find.text(before).first),
lessThan(topOf(find.text(after).first)),
reason: '$before above $after',
);
}
});
testWidgets('the dropdown offers the subjects in use, narrows the '
'library to one, and the device keeps it', (tester) async {
final settings = MemoryDeviceSettingsStore();
stored('rust', 'Rust', subject: 'software-engineering');
stored('japanese', 'Japanese', subject: 'languages');
stored('notes', 'Notes');
tester.view.physicalSize = const Size(1200, 1600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await pumpApp(
tester,
testDependencies(
studysetGateway: gateway,
cardGateway: cardGateway,
deviceSettingsStore: settings,
),
location: RoutePaths.library,
);
await tester.tap(find.text('All subjects'));
await tester.pumpAndSettle();
expect(
find.widgetWithText(MenuItemButton, 'Software engineering'),
findsOneWidget,
);
expect(
find.widgetWithText(MenuItemButton, 'Other / not sure'),
findsOneWidget,
);
expect(find.text('Biology'), findsNothing, reason: 'not in use');
await tester.tap(find.widgetWithText(MenuItemButton, 'Languages'));
await tester.pumpAndSettle();
expect(find.widgetWithText(StudysetCard, 'Japanese'), findsOneWidget);
expect(find.widgetWithText(StudysetCard, 'Rust'), findsNothing);
expect(find.widgetWithText(StudysetCard, 'Notes'), findsNothing);
expect(settings.writes.last.librarySubject, 'languages');
await tester.tap(find.byTooltip('Show one subject'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(MenuItemButton, 'Other / not sure'));
await tester.pumpAndSettle();
expect(find.widgetWithText(StudysetCard, 'Notes'), findsOneWidget);
expect(find.widgetWithText(StudysetCard, 'Japanese'), findsNothing);
});
testWidgets('opens on the subject kept', (
tester,
) async {
stored('rust', 'Rust', subject: 'software-engineering');
stored('japanese', 'Japanese', subject: 'languages');
tester.view.physicalSize = const Size(1200, 1600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await pumpApp(
tester,
testDependencies(
studysetGateway: gateway,
cardGateway: cardGateway,
deviceSettings: const DeviceSettings(librarySubject: 'languages'),
),
location: RoutePaths.library,
);
expect(find.widgetWithText(StudysetCard, 'Rust'), findsNothing);
expect(find.widgetWithText(StudysetCard, 'Japanese'), findsOneWidget);
});
testWidgets('a subject kept but no longer in use shows all', (
tester,
) async {
stored('rust', 'Rust', subject: 'software-engineering');
tester.view.physicalSize = const Size(1200, 1600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await pumpApp(
tester,
testDependencies(
studysetGateway: gateway,
cardGateway: cardGateway,
deviceSettings: const DeviceSettings(librarySubject: 'biology'),
),
location: RoutePaths.library,
);
expect(find.widgetWithText(StudysetCard, 'Rust'), findsOneWidget);
expect(find.text('All subjects'), findsOneWidget);
});
});
testWidgets('creating waits for a title', (tester) async {
await showLibrary(tester);
await tester.tap(find.text('Create studyset'));
@ -482,9 +613,10 @@ void main() {
await showLibrary(tester);
// On a shelf of their own above (ADR 0041, D8).
expect(
tester.getTopLeft(find.text('German')).dx,
lessThan(tester.getTopLeft(find.text('Japanese')).dx),
tester.getTopLeft(find.text('German')).dy,
lessThan(tester.getTopLeft(find.text('Japanese')).dy),
);
});

View file

@ -7,6 +7,7 @@ import 'package:halkyon_learn_flutter/src/core/theme/learn_theme.dart';
import 'package:halkyon_learn_flutter/src/feature/study/model/study_progress.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/subject.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_card.dart';
import 'package:material_ui/material_ui.dart';
@ -42,7 +43,7 @@ void main() {
body: Center(
child: SizedBox(
width: 360,
height: 188,
height: 212,
child: StudysetCard(
studyset: studyset ?? vocabulary,
progress: progress,
@ -77,6 +78,47 @@ void main() {
expect(find.text('52 flashcards'), findsOneWidget);
});
group("names its subject under the title (ADR 0041, D8, the developer's "
'decisions of 10 Nov 2026)', () {
Studyset filed(String? subject, {String? front}) => Studyset(
id: 'set',
content: StudysetContent(
title: 'Japanese',
description: RichContent.empty(),
subject: subject,
frontLanguage: front,
),
);
testWidgets('with its icon', (tester) async {
await show(tester, studyset: filed('software-engineering'));
final name = find.text('Software engineering');
expect(name, findsOneWidget);
expect(
tester.getTopLeft(name).dy,
greaterThan(tester.getBottomLeft(find.text('Japanese')).dy - 1),
);
expect(find.byIcon(Subject.softwareEngineering.icon), findsOneWidget);
});
testWidgets('a language course with its language', (tester) async {
await show(tester, studyset: filed('languages', front: 'ja'));
expect(find.text('Languages · Japanese'), findsOneWidget);
});
testWidgets('none, nor one this app does not know: nothing', (
tester,
) async {
await show(tester, studyset: filed(null));
expect(find.byIcon(Subject.other.icon), findsNothing);
await show(tester, studyset: filed('paper-folding'));
expect(find.byIcon(Subject.other.icon), findsNothing);
});
});
testWidgets('says when a studyset holds nothing yet', (tester) async {
await show(
tester,