refactor(learn): the editor's general parts move to halkyon_editor (ADR 0033, D37)
Todo stores editor documents too, so the document, the editor, its view, the format bar, links, the clipboard, scripts and EditorBlocks move, with their strings and tests, to a shared package on the same appflowy_editor fork. What is Learn's alone stays in Learn on top of it, and what the editor took from it directly is inverted: EditorBlocks gains textMarks, formatActions and onEditorClosed, RichContentEditor a wrapEditor, and an InsertableBlock's name takes the BuildContext. Learn's screens use LearnContentEditor and LearnContentView, which add Learn's blocks and the grammar check. Learn behaves as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4875b26174
commit
648dfe4051
1 changed files with 1676 additions and 1366 deletions
|
|
@ -29,8 +29,8 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
|||
- **Material's `Card` widget clashes with the domain's `Card`**: files that need both import `material_ui` with `hide Card`.
|
||||
- **State: `flutter_bloc`, uniformly.** Blocs fold repository streams into immutable states with value equality, are provided as close to their consumer as possible, and log through one global `BlocObserver`. Why: the data layer is stream-shaped, event → state machines test without widgets, and one pattern everywhere is itself the point.
|
||||
- **Value equality comes from `equatable`**, mixed in (`with Equatable`), for models and states alike; `leancode_lint`'s `missing_equatable_props` catches a field left out of `props`.
|
||||
- **Rich text is `RichContent`** (`lib/src/core/content/`): the editor's document JSON plus a `formatVersion`, pure Dart, so models and the server can use it. `lib/src/core/editor/` converts it to and from the editor's `Document` (giving tables the decimal sizes the editor casts to: JSON from imports and archives may hold whole numbers, or none; our fork of the editor fits a read-only table's rows to their cells, which upstream's leaves at their stored height) and holds the one editing widget (`RichContentEditor`) and the one read-only renderer (`RichContentView`). Copying and pasting keep every format between the app's editors (`EditorClipboard`, decided 2026-10-11): the system's clipboard holds plain text only, so the blocks copied last stay in memory beside it and a paste takes them while it still holds their text, plain text otherwise; the editor's own copy, cut and paste are replaced (Ctrl+C, X, V), and on phones a bar over selected text (the editor's `MobileFloatingToolbar`, over text only) copies, cuts, pastes and selects all. Links come from the format bar's link button and Ctrl+K (`link.dart`, in place of the editor's own link menu, English only); an address without its scheme leads to the web. Text above other text (ruby, `ruby.dart`, decided 2026-10-12) is named in the content's language, furigana over Japanese, pinyin over Chinese, else text above (`ContentLanguages`, in `core/widget/`, which every screen of a studyset sets through its language scope); read-only it shows only under the pointer or after a long press, its room kept so lines don't move, and while editing over the pointer; with the caret inside such text, the format bar's button edits all of it.
|
||||
- **Blocks** beyond the editor's own (callout, spoiler, formula, image, sound, YouTube video, study button) keep their type and attributes as named in `core/content/content_blocks.dart`, which `RichContent` reads too (their text counts in `plainText`, and a video or study button isn't blank). An `EditorBlocks` lists how each type is built, which can be inserted (the format bar's Insert sheet and the desktop's `/` menu show the same list) and its typing rules; the core's are `coreBlocks`, and a feature's (the study button, `feature/study/`) reach the editors through `EditorBlocksScope`, which `PersonApp` fills, since the core can't know features. One builder serves editor and view: a block asks `editorState.editable`, and read-only shows what it is for (a spoiler hidden until revealed, a formula set, a video's play). A block taken whole mixes in `AtomicBlock` (selection, the remove button); unknown types show "needs a newer version" and stay as they are. New block types keep `formatVersion` 1: raising it would hide whole studysets and paths in older apps.
|
||||
- **Rich text is `RichContent`** (`halkyon_editor`, ADR 0033, D37: the editor's general parts moved there on 6 Nov 2026, when Todo took editor documents too; Learn's own stay in `lib/src/core/editor/` on top of it: callout, spoiler, formula, file and YouTube blocks, ruby, Markdown, `core_blocks.dart`, `rich_text_screen.dart`, `save_status.dart`, and `learn_content.dart`, the `LearnContentEditor` and `LearnContentView` every screen uses, which are the package's widgets with Learn's blocks and the check of the spelling): the editor's document JSON plus a `formatVersion`, pure Dart, so models can use it. The package converts it to and from the editor's `Document` (giving tables the decimal sizes the editor casts to: JSON from imports and archives may hold whole numbers, or none; our fork of the editor fits a read-only table's rows to their cells, which upstream's leaves at their stored height) and holds the one editing widget (`RichContentEditor`) and the one read-only renderer (`RichContentView`). Copying and pasting keep every format between the app's editors (`EditorClipboard`, decided 2026-10-11): the system's clipboard holds plain text only, so the blocks copied last stay in memory beside it and a paste takes them while it still holds their text, plain text otherwise; the editor's own copy, cut and paste are replaced (Ctrl+C, X, V), and on phones a bar over selected text (the editor's `MobileFloatingToolbar`, over text only) copies, cuts, pastes and selects all. Links come from the format bar's link button and Ctrl+K (the package's `link.dart`, in place of the editor's own link menu, English only); an address without its scheme leads to the web. Text above other text (ruby, `ruby.dart`, decided 2026-10-12) is named in the content's language, furigana over Japanese, pinyin over Chinese, else text above (`ContentLanguages`, in `core/widget/`, which every screen of a studyset sets through its language scope); read-only it shows only under the pointer or after a long press, its room kept so lines don't move, and while editing over the pointer; with the caret inside such text, the format bar's button edits all of it.
|
||||
- **Blocks** beyond the editor's own (callout, spoiler, formula, image, sound, YouTube video, study button) keep their type and attributes as named in the package's `content_blocks.dart`, which `RichContent` reads too (their text counts in `plainText`, and a video or study button isn't blank). An `EditorBlocks` lists how each type is built, which can be inserted (the format bar's Insert sheet and the desktop's `/` menu show the same list) and its typing rules, and since D37 the marks within lines the package can't know (ruby, formulas), their format bar buttons (`rubyFormatAction`, `inlineMathFormatAction`, after subscript) and what an editor going takes along; the core's are `coreBlocks`, on the package's `basicBlocks`, and a feature's (the study button, `feature/study/`) reach the editors through `EditorBlocksScope`, which `PersonApp` fills, since the core can't know features. One builder serves editor and view: a block asks `editorState.editable`, and read-only shows what it is for (a spoiler hidden until revealed, a formula set, a video's play). A block taken whole mixes in `AtomicBlock` (selection, the remove button); unknown types show "needs a newer version" and stay as they are. New block types keep `formatVersion` 1: raising it would hide whole studysets and paths in older apps.
|
||||
- **Time comes from `clock.now()`** (`package:clock`), never `DateTime.now()`, in server and app. Why: scheduling, streaks and expiry are deterministic under test (`withClock`, `fakeAsync`), and because Serverpod itself reads `clock.now()` (e.g. session expiry), `withClock` controls its behaviour too.
|
||||
- **UTC for storing and sending, local time for display and day boundaries.** Serverpod sends `DateTime` as UTC and stores it in `timestamp without time zone` columns. Compare and compute in UTC; call `.toLocal()` only to display. "Due today", streaks and daily goals use the learner's **local** calendar day, computed on the client; the server never needs day boundaries.
|
||||
- **Build configuration comes from `--dart-define`.** The client holds no secrets, ever.
|
||||
|
|
@ -54,7 +54,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
|||
- Shared fakes live in `test/support/`, those of halkyon_app's boundaries in its `package:halkyon_app/testing.dart` (`FakeAuthGateway`, `FakeInstanceChecker`, `MemorySignInAddresses`, `MemoryInstanceStore`, `MemoryPendingChangesStore`), which `testDependencies` fills in; fakes pre-stub their streams so an incidental subscription doesn't fail the test.
|
||||
- **Editor in widget tests**: host `AppFlowyEditor` in a scrollable body with a fixed-height slot (an over-constrained `Expanded` hangs the test), and `pump(const Duration(seconds: 1))` after editor changes to flush its timers. Test the document ↔ JSON round-trip as plain unit tests.
|
||||
- **No `Stream.first` on a repository's stream in code that widget tests run**: `first` waits for the subscription's cancel future, which for a broadcast stream is a completed future of the root zone, so under the widget test's fake clock it never completes. Read once with the repository's `all()` instead. For the same reason a repository never keeps a completed future to chain later work on (a `Future.value()` made in `setUp` runs what waits for it in the real zone): a queue of writes starts empty and empties again when idle (`SerialQueue`, halkyon_core), and a cache keeps values, not futures (`GeoMapRepository`). Assets are read for real, which the fake clock doesn't advance: a widget test showing a map gets the maps read beforehand (`loadedMaps(tester)` into `testDependencies(maps: ...)`), and one showing how a character is written its strokes (`loadedStrokes(tester, characters)` into `testDependencies(strokes: ...)`). Nor is a subscription that has finished cancelled with `await` there (`await subscription.cancel()` on a stream done): its cancel future never completes under the fake clock either, which hung everything after it (found 2026-10-26 with the flashcards an assistant proposes); cancel it unawaited (`CardProposalsCubit`), or forget it in `onDone` and `onError`, so only one still running is awaited (`ConversationCubit`).
|
||||
- **Text inside the editor's views** (`RichContentView`, `RichContentEditor`) is rich text: find it with `findRichText: true`.
|
||||
- **Text inside the editor's views** (`LearnContentView`, `LearnContentEditor`) is rich text: find it with `findRichText: true`.
|
||||
- **Layout-sensitive widget tests** set `tester.view.physicalSize` explicitly, so a responsive branch isn't missed for the wrong reason.
|
||||
- **A menu item acts in the frame after its menu closes** (`MenuAnchor`): pump twice after tapping one before expecting what it does.
|
||||
- **Server tests of what writes synced rows** run on the sync engine's database, as the server does (the generated test tools build the pod without it): pass the interceptor to `withServerpod`, call `initializeOfflineSync` in `setUp`, and write as a person through `transactionForUser`. Its writes take transactions of their own, so these tests turn rollback off and delete what they made (`test/integration/asset/asset_test.dart`).
|
||||
|
|
|
|||
Loading…
Reference in a new issue