fix(app): study faces centred in the card
The read-only view passed its alignment to the text alone, which the editor lays out only as wide as itself, so a centred face stayed at the start. It now aligns top-level paragraphs and headings that have no alignment of their own; lists, quotes and the rest keep the start, and an alignment chosen in the format bar wins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
43b2d86fc0
commit
7c00abcc05
4 changed files with 135 additions and 22 deletions
|
|
@ -88,31 +88,21 @@ class const EditorBlocksScope({
|
||||||
/// English.
|
/// English.
|
||||||
Map<String, BlockComponentBuilder> buildersWithUnknown(
|
Map<String, BlockComponentBuilder> buildersWithUnknown(
|
||||||
EditorBlocks blocks,
|
EditorBlocks blocks,
|
||||||
AppLocalizations l10n, {
|
AppLocalizations l10n,
|
||||||
TextAlign? textAlign,
|
) => {
|
||||||
}) => {
|
|
||||||
for (final MapEntry(key: type, value: builder) in blocks.builders.entries)
|
for (final MapEntry(key: type, value: builder) in blocks.builders.entries)
|
||||||
type: _withPlaceholder(builder, l10n, textAlign),
|
type: _withPlaceholder(builder, l10n),
|
||||||
errorBlockComponentBuilderKey: _UnknownBlockBuilder(),
|
errorBlockComponentBuilderKey: _UnknownBlockBuilder(),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// [builder] alike, but saying what its block is in [l10n]'s language while
|
/// [builder] alike, but saying what its block is in [l10n]'s language while
|
||||||
/// empty, for the editor's own text blocks, and aligning their text as
|
/// empty, for the editor's own text blocks; others as they are.
|
||||||
/// [textAlign] says where the text doesn't say otherwise; others as they
|
|
||||||
/// are.
|
|
||||||
BlockComponentBuilder _withPlaceholder(
|
BlockComponentBuilder _withPlaceholder(
|
||||||
BlockComponentBuilder builder,
|
BlockComponentBuilder builder,
|
||||||
AppLocalizations l10n,
|
AppLocalizations l10n,
|
||||||
TextAlign? textAlign,
|
|
||||||
) {
|
) {
|
||||||
BlockComponentConfiguration saying(String Function(Node node) text) =>
|
BlockComponentConfiguration saying(String Function(Node node) text) =>
|
||||||
builder.configuration.copyWith(
|
builder.configuration.copyWith(placeholderText: text);
|
||||||
placeholderText: text,
|
|
||||||
textAlign: switch (textAlign) {
|
|
||||||
final align? => (_) => align,
|
|
||||||
null => null,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return switch (builder) {
|
return switch (builder) {
|
||||||
final ParagraphBlockComponentBuilder paragraph =>
|
final ParagraphBlockComponentBuilder paragraph =>
|
||||||
ParagraphBlockComponentBuilder(
|
ParagraphBlockComponentBuilder(
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ class const RichContentView({
|
||||||
/// The text's style; the theme's `bodyLarge` without.
|
/// The text's style; the theme's `bodyLarge` without.
|
||||||
final TextStyle? textStyle,
|
final TextStyle? textStyle,
|
||||||
|
|
||||||
/// How the text aligns where it doesn't say itself; at the start without.
|
/// How paragraphs and headings align where the editor gave them no
|
||||||
|
/// alignment of their own; at the start without. Lists, quotes and the
|
||||||
|
/// other blocks keep the start: centred, a list's markers drift apart.
|
||||||
final TextAlign? textAlign,
|
final TextAlign? textAlign,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends StatefulWidget {
|
}) extends StatefulWidget {
|
||||||
|
|
@ -33,16 +35,17 @@ class const RichContentView({
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RichContentViewState() extends State<RichContentView> {
|
class _RichContentViewState() extends State<RichContentView> {
|
||||||
late EditorState _editorState = _readOnly(widget.content);
|
late EditorState _editorState = _readOnly(widget.content, widget.textAlign);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(RichContentView oldWidget) {
|
void didUpdateWidget(RichContentView oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.content != oldWidget.content) {
|
if (widget.content != oldWidget.content ||
|
||||||
|
widget.textAlign != oldWidget.textAlign) {
|
||||||
// The old editor is still attached until this frame is built.
|
// The old editor is still attached until this frame is built.
|
||||||
final previous = _editorState;
|
final previous = _editorState;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => previous.dispose());
|
WidgetsBinding.instance.addPostFrameCallback((_) => previous.dispose());
|
||||||
_editorState = _readOnly(widget.content);
|
_editorState = _readOnly(widget.content, widget.textAlign);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,7 +71,6 @@ class _RichContentViewState() extends State<RichContentView> {
|
||||||
blockComponentBuilders: buildersWithUnknown(
|
blockComponentBuilders: buildersWithUnknown(
|
||||||
blocksOf(context),
|
blocksOf(context),
|
||||||
AppLocalizations.of(context),
|
AppLocalizations.of(context),
|
||||||
textAlign: widget.textAlign,
|
|
||||||
),
|
),
|
||||||
editable: false,
|
editable: false,
|
||||||
disableSelectionService: true,
|
disableSelectionService: true,
|
||||||
|
|
@ -78,5 +80,25 @@ class _RichContentViewState() extends State<RichContentView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EditorState _readOnly(RichContent content) =>
|
/// [content] to show, its top-level paragraphs and headings aligned as
|
||||||
EditorState(document: content.toDocument())..editable = false;
|
/// [textAlign] says where they don't say themselves. The editor lays a text
|
||||||
|
/// block's text out only as wide as it is, so aligning the text alone moves
|
||||||
|
/// nothing: the block's own `align` places it. The document is never saved.
|
||||||
|
EditorState _readOnly(RichContent content, TextAlign? textAlign) {
|
||||||
|
final document = content.toDocument();
|
||||||
|
final align = switch (textAlign) {
|
||||||
|
TextAlign.center => 'center',
|
||||||
|
TextAlign.right => 'right',
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
if (align != null) {
|
||||||
|
for (final node in document.root.children) {
|
||||||
|
if ((node.type == ParagraphBlockKeys.type ||
|
||||||
|
node.type == HeadingBlockKeys.type) &&
|
||||||
|
node.attributes[blockComponentAlign] == null) {
|
||||||
|
node.updateAttributes({blockComponentAlign: align});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EditorState(document: document)..editable = false;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,4 +74,83 @@ void main() {
|
||||||
expect(tapped, 1);
|
expect(tapped, 1);
|
||||||
expect(tester.testTextInput.hasAnyClients, isFalse);
|
expect(tester.testTextInput.hasAnyClients, isFalse);
|
||||||
}, variant: TargetPlatformVariant.all());
|
}, variant: TargetPlatformVariant.all());
|
||||||
|
|
||||||
|
group('asked to centre', () {
|
||||||
|
Map<String, Object?> block(
|
||||||
|
String type,
|
||||||
|
String text, [
|
||||||
|
Map<String, Object?> data = const {},
|
||||||
|
]) => {
|
||||||
|
'type': type,
|
||||||
|
'data': {
|
||||||
|
...data,
|
||||||
|
'delta': [
|
||||||
|
{'insert': text},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<void> showCentred(
|
||||||
|
WidgetTester tester,
|
||||||
|
List<Map<String, Object?>> blocks,
|
||||||
|
) async {
|
||||||
|
tester.view.physicalSize = const Size(400, 800);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
home: Scaffold(
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
RichContentView(
|
||||||
|
content: RichContent({
|
||||||
|
'document': {'type': 'page', 'children': blocks},
|
||||||
|
}),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
double centreOf(WidgetTester tester, String text) =>
|
||||||
|
tester.getCenter(find.text(text, findRichText: true)).dx;
|
||||||
|
double leftOf(WidgetTester tester, String text) =>
|
||||||
|
tester.getTopLeft(find.text(text, findRichText: true)).dx;
|
||||||
|
|
||||||
|
testWidgets('centres paragraphs and headings', (tester) async {
|
||||||
|
await showCentred(tester, [
|
||||||
|
block('heading', 'Greetings', {'level': 2}),
|
||||||
|
block('paragraph', 'hola'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(centreOf(tester, 'Greetings'), moreOrLessEquals(200, epsilon: 1));
|
||||||
|
expect(centreOf(tester, 'hola'), moreOrLessEquals(200, epsilon: 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('leaves a list at the start: centred, its markers would '
|
||||||
|
'drift apart', (tester) async {
|
||||||
|
await showCentred(tester, [
|
||||||
|
block('bulleted_list', 'hola'),
|
||||||
|
block('bulleted_list', 'buenos días'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(leftOf(tester, 'hola'), lessThan(60));
|
||||||
|
expect(leftOf(tester, 'buenos días'), leftOf(tester, 'hola'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('keeps the alignment a paragraph was given in the editor', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await showCentred(tester, [
|
||||||
|
block('paragraph', 'hola', {'align': 'left'}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(leftOf(tester, 'hola'), lessThan(40));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,28 @@ void main() {
|
||||||
expect(find.textContaining('cat', findRichText: true), findsNothing);
|
expect(find.textContaining('cat', findRichText: true), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('the faces are centred in the card, and a tap on their text '
|
||||||
|
'shows and hides the answer too', (tester) async {
|
||||||
|
await cards.create(
|
||||||
|
'japanese',
|
||||||
|
CardContent(front: paragraph('猫'), back: paragraph('cat')),
|
||||||
|
);
|
||||||
|
await study(tester, width: 400);
|
||||||
|
final card = tester.getRect(find.byType(StudyCardSurface));
|
||||||
|
double centreOf(String text) =>
|
||||||
|
tester.getCenter(find.text(text, findRichText: true)).dx;
|
||||||
|
|
||||||
|
expect(centreOf('猫'), moreOrLessEquals(card.center.dx, epsilon: 1));
|
||||||
|
|
||||||
|
await tester.tap(find.text('猫', findRichText: true));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(centreOf('cat'), moreOrLessEquals(card.center.dx, epsilon: 1));
|
||||||
|
|
||||||
|
await tester.tap(find.text('cat', findRichText: true));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.textContaining('cat', findRichText: true), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('on a phone, a card swiped right is rated Good, one let go '
|
testWidgets('on a phone, a card swiped right is rated Good, one let go '
|
||||||
'early springs back', (tester) async {
|
'early springs back', (tester) async {
|
||||||
await cards.create(
|
await cards.create(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue