feat(pdf): cards beside a PDF are written in the composer's own editor
The sidebar wrote a front and a back only. It now holds the composer's editor with a bloc of its own: every kind of card (multiple choice, vocabulary with Look up, matching, a map, a character), tags, hints and the explanation, pinning, duplicating and deleting. A new card is tied to the page shown, which it can be moved to the current one, and waits for its chapter's tag by name until it is created; text sent from the pages goes into its front or its answer, as its kind keeps one. Create saves it once it is complete, as the composer does; going back asks before discarding what was written. The screenshot of the pages has a button at the end of the format bar, outside its scrolling, where an editor's place is for it (InsertableBlock.inFormatBar); the Insert menu still lists it. A new card no longer shows twice in the composer when its stored copy arrives while it is saved. 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>
This commit is contained in:
parent
72cbec52c2
commit
fcee3c2670
1 changed files with 714 additions and 372 deletions
|
|
@ -595,4 +595,101 @@ void main() {
|
|||
expect(await cards.cardsOf('japanese'), isEmpty);
|
||||
expect(gateway.deletions, ['blank $neko', 'delete $neko']);
|
||||
});
|
||||
|
||||
test('a card written from a PDF knows its page, waits for its chapter '
|
||||
'tag by name, and takes text sent to its front and answer', () async {
|
||||
start();
|
||||
await loaded();
|
||||
|
||||
bloc
|
||||
..add(
|
||||
const CardComposerEventDraftAdded(
|
||||
'new',
|
||||
source: (pdfId: 'grammar', page: 5),
|
||||
tagNames: ['Particles'],
|
||||
),
|
||||
)
|
||||
..add(
|
||||
const CardComposerEventTextAdded(
|
||||
'new',
|
||||
text: 'は marks',
|
||||
toAnswer: false,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
const CardComposerEventTextAdded(
|
||||
'new',
|
||||
text: 'the topic',
|
||||
toAnswer: true,
|
||||
),
|
||||
);
|
||||
final draft = (await until(
|
||||
(s) => s.byId('new')?.content.back.plainText == 'the topic',
|
||||
)).byId('new')!;
|
||||
expect(draft.content.front.plainText, 'は marks');
|
||||
expect(draft.content.source, (pdfId: 'grammar', page: 5));
|
||||
expect(draft.newTagNames, ['Particles']);
|
||||
|
||||
bloc.add(const CardComposerEventLeft('new'));
|
||||
final saved = await until(
|
||||
(s) => s.byId('new')?.status == CardDraftStatus.saved,
|
||||
);
|
||||
final stored = (await cards.cardsOf('japanese')).single;
|
||||
expect(stored.content.source, (pdfId: 'grammar', page: 5));
|
||||
expect((await tags.read()).of(stored.id).single.name, 'Particles');
|
||||
// Held by its draft alone, never also among the stored cards.
|
||||
expect(saved.items.where((d) => d.cardId == stored.id), hasLength(1));
|
||||
});
|
||||
|
||||
test('text for the answer goes where the kind of card keeps it', () {
|
||||
final base = card('猫', '');
|
||||
expect(
|
||||
base.withTextAdded('cat', toAnswer: true).back.plainText,
|
||||
'cat',
|
||||
);
|
||||
final word = CardContent(
|
||||
front: text('猫'),
|
||||
back: RichContent.empty(),
|
||||
type: CardType.vocabulary,
|
||||
meanings: const ['cat'],
|
||||
);
|
||||
expect(
|
||||
word.withTextAdded('feline\nanimal', toAnswer: true).meanings,
|
||||
['cat', 'feline animal'],
|
||||
);
|
||||
final choice = CardContent(
|
||||
front: text('猫'),
|
||||
back: RichContent.empty(),
|
||||
type: CardType.multipleChoice,
|
||||
answers: [
|
||||
CardAnswer(content: text('dog')),
|
||||
CardAnswer(content: RichContent.empty(), correct: true),
|
||||
],
|
||||
);
|
||||
expect(
|
||||
choice.withTextAdded('cat', toAnswer: true).answers[1].content.plainText,
|
||||
'cat',
|
||||
);
|
||||
});
|
||||
|
||||
test('discarding drops a new card, and takes a stored one back to how '
|
||||
'it is stored', () async {
|
||||
final neko = await cards.create('japanese', card('猫', 'cat'));
|
||||
start();
|
||||
await loaded();
|
||||
|
||||
bloc
|
||||
..add(const CardComposerEventDraftAdded('new'))
|
||||
..add(CardComposerEventChanged(neko, card('猫', 'kitten')))
|
||||
..add(const CardComposerEventDiscarded('new'))
|
||||
..add(CardComposerEventDiscarded(neko));
|
||||
final state = await until(
|
||||
(s) =>
|
||||
s.byId('new') == null &&
|
||||
s.byId(neko)?.content.back.plainText == 'cat',
|
||||
);
|
||||
|
||||
expect(state.byId(neko)!.isDirty, isFalse);
|
||||
expect(await cards.cardsOf('japanese'), hasLength(1));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue