feat(learn): Add material no longer offers flashcards from a text (ADR 0021, D56 amended)

The developer's review of 4 Nov 2026. The entry goes from a studyset's
Add material, and with it the sheet for pasting a text
(proposeCardsFromText, _TextSheet), its three texts and proposeCards'
fromStudyset, which only it set to false. A note's and a PDF page's
flashcards stay; their tests now drive the proposals sheet from a note.

Whats-New: Add material no longer offers Flashcards from a text; a note's or a PDF page's flashcards stay.
Whats-New-De: Material hinzufügen bietet keine Karteikarten aus einem Text mehr an; die aus einer Notiz oder einer PDF-Seite bleiben.

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-04 21:19:30 +01:00
commit bda09a2959
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
8 changed files with 35 additions and 158 deletions

View file

@ -1875,9 +1875,6 @@
"proposalsFromNote": "Karteikarten aus dieser Notiz",
"proposalsFromPage": "Aus dieser Seite",
"proposalsFromSelection": "Aus der Auswahl",
"proposalsFromText": "Karteikarten aus einem Text",
"proposalsTextHint": "Füg den Text ein, aus dem Karteikarten werden sollen",
"proposalsPropose": "Karteikarten vorschlagen",
"settingsSearchEngine": "Suchmaschine",
"searchEngineDevice": "Suche des Geräts",
"searchEngineCustom": "Eigene Adresse",

View file

@ -2248,10 +2248,6 @@
"@proposalsFromPage": {"description": "In a PDF's flashcards: has an assistant propose cards from the page shown (D56)."},
"proposalsFromSelection": "From the selection",
"@proposalsFromSelection": {"description": "In a PDF's flashcards: has an assistant propose cards from the text selected (D56)."},
"proposalsFromText": "Flashcards from a text",
"@proposalsFromText": {"description": "Add material: has an assistant propose cards from text pasted (D56)."},
"proposalsTextHint": "Paste the text to make flashcards from",
"proposalsPropose": "Propose flashcards",
"settingsSearchEngine": "Search engine",
"searchEngineDevice": "The device's search",
"searchEngineCustom": "Your own address",

View file

@ -11090,24 +11090,6 @@ abstract class AppLocalizations {
/// **'From the selection'**
String get proposalsFromSelection;
/// Add material: has an assistant propose cards from text pasted (D56).
///
/// In en, this message translates to:
/// **'Flashcards from a text'**
String get proposalsFromText;
/// No description provided for @proposalsTextHint.
///
/// In en, this message translates to:
/// **'Paste the text to make flashcards from'**
String get proposalsTextHint;
/// No description provided for @proposalsPropose.
///
/// In en, this message translates to:
/// **'Propose flashcards'**
String get proposalsPropose;
/// No description provided for @settingsSearchEngine.
///
/// In en, this message translates to:

View file

@ -7054,16 +7054,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get proposalsFromSelection => 'Aus der Auswahl';
@override
String get proposalsFromText => 'Karteikarten aus einem Text';
@override
String get proposalsTextHint =>
'Füg den Text ein, aus dem Karteikarten werden sollen';
@override
String get proposalsPropose => 'Karteikarten vorschlagen';
@override
String get settingsSearchEngine => 'Suchmaschine';

View file

@ -6991,15 +6991,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get proposalsFromSelection => 'From the selection';
@override
String get proposalsFromText => 'Flashcards from a text';
@override
String get proposalsTextHint => 'Paste the text to make flashcards from';
@override
String get proposalsPropose => 'Propose flashcards';
@override
String get settingsSearchEngine => 'Search engine';

View file

@ -26,13 +26,12 @@ bool proposesCards(BuildContext context) =>
/// studyset [studysetId] (ADR 0021, D55), in a sheet where the person
/// chooses them, changes them and adds them, tied to [source] where they
/// come from a PDF's page; once [readyToAsk], [text] counting as the
/// studyset's content where it is [fromStudyset] (a note, a page), so a
/// private one goes only once agreed to. Says how many were added.
/// studyset's content (a note, a page), so a private one goes only once
/// agreed to. Says how many were added.
Future<void> proposeCards(
BuildContext context, {
required String studysetId,
required String text,
bool fromStudyset = true,
PdfPageRef? source,
}) async {
final choice = nextAssistant(context);
@ -41,7 +40,7 @@ Future<void> proposeCards(
}
final scope = AssistantScope(
studysetId: studysetId,
partsFrom: fromStudyset ? {studysetId} : const {},
partsFrom: {studysetId},
);
if (!await readyToAsk(context, choice: choice, scope: scope) ||
!context.mounted) {
@ -73,27 +72,6 @@ Future<void> proposeCards(
}
}
/// Asks for a text to make flashcards of, pasted or typed, then has them
/// proposed for [studysetId] (D56): the text is the person's, not the
/// studyset's, so nothing private is said to go.
Future<void> proposeCardsFromText(
BuildContext context, {
required String studysetId,
}) async {
final text = await showAdaptiveSheet<String>(
context,
builder: (context) => const _TextSheet(),
);
if (text != null && context.mounted) {
await proposeCards(
context,
studysetId: studysetId,
text: text,
fromStudyset: false,
);
}
}
/// The cards [name] proposes, each chosen or not, changed in a sheet of
/// its own, and Add for those chosen, which closes with how many.
class const _ProposalsSheet({required final String name})
@ -276,50 +254,3 @@ class _EditSheetState() extends State<_EditSheet> {
);
}
}
/// A text to make flashcards of; closes with it.
class const _TextSheet() extends StatefulWidget {
@override
State<_TextSheet> createState() => _TextSheetState();
}
class _TextSheetState() extends State<_TextSheet> {
final _text = TextEditingController();
@override
void dispose() {
_text.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return SheetScaffold(
title: l10n.proposalsFromText,
body: TextField(
controller: _text,
autofocus: true,
// As much as goes to the assistant.
maxLength: maxProposalSource,
buildCounter: nearLimitCounter,
minLines: 6,
maxLines: 14,
decoration: InputDecoration(
hintText: l10n.proposalsTextHint,
border: const OutlineInputBorder(),
),
),
footer: ListenableBuilder(
listenable: _text,
builder: (context, _) => FilledButton.icon(
icon: const Icon(LucideIcons.sparkles),
label: Text(l10n.proposalsPropose),
onPressed: _text.text.trim().isEmpty
? null
: () => Navigator.of(context).pop(_text.text),
),
),
);
}
}

View file

@ -14,7 +14,6 @@ import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_importer.dart';
import 'package:halkyon_learn_flutter/src/feature/anki/widget/anki_import_sheet.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/model/assistant_topic.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/assistant_field.dart';
import 'package:halkyon_learn_flutter/src/feature/assistant/widget/card_proposals_sheet.dart';
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_list.dart';
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_text_import_sheet.dart';
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
@ -1394,16 +1393,6 @@ class _AddMaterialState() extends State<_AddMaterial> {
onPressed: () => context.push(RoutePaths.jellyfin(studysetId)),
child: Text(l10n.studysetWatch),
),
// Only where an assistant can be asked (D56).
if (proposesCards(context))
MenuItemButton(
leadingIcon: const Icon(LucideIcons.sparkles),
onPressed: () {
context.read<ContentTabCubit>().show(StudysetTab.flashcards);
unawaited(proposeCardsFromText(context, studysetId: studysetId));
},
child: Text(l10n.proposalsFromText),
),
// Only for a language the instance has a dictionary of.
if (dictionary != null)
MenuItemButton(

View file

@ -144,27 +144,28 @@ void main() {
];
}
testWidgets('Add material proposes flashcards from a text pasted: every '
'one chosen, changed or left out, and those chosen added', (
tester,
) async {
await showApp(tester, RoutePaths.studyset('japanese'));
/// Shows a note of Japanese's holding [text], for its flashcards.
Future<void> showNote(WidgetTester tester, String text) async {
final notes = NoteRepository(noteGateway);
addTearDown(notes.dispose);
final (:noteId, revisionId: _) = await notes.create(
'japanese',
NoteContent(title: 'Verbs', body: RichContent.plain(text)),
);
await showApp(tester, RoutePaths.note('japanese', noteId));
}
testWidgets("a note's proposed flashcards: every one chosen, changed or "
'left out, and those chosen added', (tester) async {
await showNote(tester, '食べる means to eat.');
assistants.answers.add(
_proposes([('食べる', 'to eat'), ('飲む', 'to drink'), ('見る', 'to see')]),
);
await tester.tap(find.text('Add material'));
await tester.pumpAndSettle();
await tapAndSettle(tester, find.text('Flashcards from a text'));
await tester.enterText(
find.byType(TextField).last,
'食べる means to eat.',
);
await tester.pump();
await tapAndSettle(tester, find.text('Propose flashcards'));
await tapAndSettle(tester, find.byTooltip('Flashcards from this note'));
expect(find.text('Flashcards by Claude'), findsOneWidget);
expect(_sent(assistants.asked.single.$2), '食べる means to eat.');
expect(_sent(assistants.asked.single.$2), 'Verbs\n\n食べる means to eat.');
expect(find.text('Add 3 flashcards'), findsOneWidget);
await tester.tap(find.text('飲む'));
@ -196,7 +197,7 @@ void main() {
testWidgets('none proposed says so, and Try again asks once more', (
tester,
) async {
await showApp(tester, RoutePaths.studyset('japanese'));
await showNote(tester, 'Some text.');
assistants.answers
..add([
LlmFinished(
@ -206,12 +207,7 @@ void main() {
),
])
..add(_proposes([('Q', 'A')]));
await tester.tap(find.text('Add material'));
await tester.pumpAndSettle();
await tapAndSettle(tester, find.text('Flashcards from a text'));
await tester.enterText(find.byType(TextField).last, 'Some text.');
await tester.pump();
await tapAndSettle(tester, find.text('Propose flashcards'));
await tapAndSettle(tester, find.byTooltip('Flashcards from this note'));
expect(find.text('Claude proposed no flashcards.'), findsOneWidget);
await tapAndSettle(tester, find.text('Try again'));
@ -282,6 +278,16 @@ void main() {
);
});
testWidgets('Add material offers no flashcards from a text (the '
"developer's review of 4 Nov 2026): a note's, a page's are enough", (
tester,
) async {
await showApp(tester, RoutePaths.studyset('japanese'));
await tester.tap(find.text('Add material'));
await tester.pumpAndSettle();
expect(find.text('Flashcards from a text'), findsNothing);
});
testWidgets('nothing offers flashcards without an assistant', (
tester,
) async {
@ -292,13 +298,6 @@ void main() {
'japanese',
NoteContent(title: 'Kana', body: RichContent.plain('あ is a.')),
);
await showApp(tester, RoutePaths.studyset('japanese'));
await tester.tap(find.text('Add material'));
await tester.pumpAndSettle();
expect(find.text('Flashcards from a text'), findsNothing);
await tester.tapAt(Offset.zero);
await tester.pumpAndSettle();
await showApp(tester, RoutePaths.note('japanese', noteId));
expect(find.byTooltip('Flashcards from this note'), findsNothing);
});