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
211 changed files with 1676 additions and 1366 deletions
|
|
@ -11,6 +11,7 @@ Halkyon's apps and the packages they share, in one repository and one pub worksp
|
|||
- `halkyon_ui`: Halkyon's look, the theme around cobalt and copper, Noto Sans bundled, the window-size classes and the widgets every screen is built of (the floating bar and rail, `FoldingTabs`' tab row folding into the title), with texts of its own (English, German).
|
||||
- `halkyon_base_server`, `halkyon_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (a password with TOTP and the resets of ADR 0017, SimpleLogin on `halkyon_oidc`, email codes proving an address; no anonymous accounts since ADR 0020, D25, nor GitHub since D36), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics, people's handles and public profiles, who may sign up (`SIGN_UP`: open, approval, closed) with the accounts waiting for approval and the instance's operators who approve them (Halkyon Learn's ADR 0021, step 1), the operator's assistants forwarded on halkyon_llm with their grants, each person's budget, the efforts they allow and counted figures, and the instance's privacy page made from its settings (step 2), and the instance's register (ADR 0016): one account per person across an operator's services, held by one service (Learn's server on learn.halkyon.eu) and asked by the others, its members (`ACCOUNT_REGISTER_URL`), all sharing a secret (`INSTANCE_SECRET`); its README says how.
|
||||
- `halkyon_app`: every app's shell on the module's client (ADR 0014): the instances a device keeps accounts on and the first start, signing in and switching person, the gates building an app's dependencies per instance and per person, the sync service and where it stands, the account section of an app's settings (`AccountTiles`), every app's Profile (`HalkyonProfile`, decided 2026-11-02: the person's own tab, the same in every app, with their account, the ways they sign in and their public profile, one for all apps, then the app's own tabs) and people's public profiles (`PublicProfileRepository`, `PublicProfileSection`, `showProfileOf`), the wait screen of an account waiting for approval and the operator's card, badge and screen (ADR 0021), the assistants of an app's settings (`AssistantsCard`: granted, by provider, agreed to, what is left of the person's budget, used, and the person's own keys) and the operator's grants from that screen (`AssistantGrantsScreen`: several assistants to several people at once, each with one budget, D57 to D63), with the repositories asking them through the module, at an effort the operator allows, or a person's own key directly (step 2) and what an app's warning before a private studyset goes names (`providerTermsOf`, `OwnAssistant.onThisMachine`, step 3), About with the instance's source link (`showAppAbout`), the device's own speech recognition (`SpeechInput`, `DeviceSpeechInput` on `speech_to_text`, `FakeSpeechInput` in `testing.dart`; none on Linux) and dictation into a text at its cursor, listening again after each pause (`Dictation`, Meditate's ADR 0038, D15; an app that listens declares the microphone and its usage texts itself), what's new after an update (`WhatsNew`, `WhatsNewOnUpdate`, `WhatsNewTile`, from the app's `assets/whats_new/`, Train's ADR 0030, D61), the device's appearance and language, chosen in Settings and applied before anyone signs in (`Look`, `FileLookStore`, `LookCubit`, `DeviceLookCard`; Learn keeps them among its own device settings and shows `LookSettingsCard`; ADR 0014's amendment of 4 Nov 2026), the language every app picks (`resolveAppLocale`) and countries' and subdivisions' names (Unicode CLDR's, bundled), with texts that name the app.
|
||||
- `halkyon_editor` (ADR 0033, D37, 6 Nov 2026): rich text, the general parts of Learn's editor on our fork of `appflowy_editor`: `RichContent` (the editor's document JSON, which Learn and Todo store), `RichContentEditor`, `RichContentView`, the format bar, links, the clipboard keeping formats, superscript and subscript, and `EditorBlocks`, through which an app adds blocks, marks within lines and their buttons (`basicBlocks` without any), with texts of its own (English, German). What is Learn's alone stays in Learn on top of it.
|
||||
- `halkyon_core` (low-level primitives in pure Dart: ids, a logger, `SerialQueue`, `CoalescingRunner`) and `halkyon_interop` (contracts between apps, pure Dart).
|
||||
- `halkyon_keys` (pure Dart on libsodium, ADR 0017): an account's keys on the device, the password's Argon2id and SRP's x, the key pairs and their wrapping, the recovery phrase, data keys, content sealed and signed, row IDs keyed by the account (`keyedRowId`). SRP-6a's two halves are the module's (`halkyon_base_server`'s `base.dart`, `halkyon_base_client`'s `srp.dart`), as are TOTP and the recovery codes. `sodium`'s build hook compiles libsodium, so every machine that builds or tests needs a C compiler and `make`.
|
||||
- `halkyon_llm` (pure Dart, Halkyon Learn's ADR 0021, D15 and D46): language models' APIs streamed over plain HTTP, no SDKs: Anthropic's Messages API, OpenAI's chat completions (each known host's fields, D45) and its Responses API, behind one interface (`LlmClient`), with `FakeLlmClient` for tests; tested on replies written from the providers' documentation and on those `tool/record_llm.dart` records with a key (D36). The module forwards the operator's assistants with it, and the apps call a person's own provider. BSD-3-Clause as `halkyon_oidc`, `publish_to: none`.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ The repository's packages carry different licences, each package's full text in
|
|||
| `apps/learn/halkyon_learn_curator`, turning dictionary downloads into dictionary files | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` |
|
||||
| `packages/halkyon_base_server`, the Serverpod module every Halkyon server includes | Server Side Public License, version 1 | `SSPL-1.0` |
|
||||
| `packages/halkyon_base_client`, generated from the module's models, with SRP's client half | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` |
|
||||
| `packages/halkyon_app`, `packages/halkyon_ui` | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` |
|
||||
| `packages/halkyon_app`, `packages/halkyon_editor`, `packages/halkyon_ui` | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` |
|
||||
| `packages/halkyon_keys`, an account's keys on the device | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` |
|
||||
| `packages/halkyon_core`, `packages/halkyon_interop`; BIP-39's English word list in halkyon_core under the MIT License (`BIP39-LICENSE`) | GNU Affero General Public License, version 3 or any later version, as halkyon_shared, where they began, licensed them | `AGPL-3.0-or-later` |
|
||||
| `apps/todo/halkyon_todo_flutter`, Todo's app | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` |
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ Halkyon Meditate is to follow.
|
|||
[`halkyon_base`](packages/halkyon_base_server/README.md) (what every server has, as a
|
||||
Serverpod module with its generated client `halkyon_base_client`: signing in, the
|
||||
account and its deletion, the pruning, the instance's basics),
|
||||
[`halkyon_editor`](packages/halkyon_editor/README.md) (rich text: the editor, its view
|
||||
and the document they keep),
|
||||
[`halkyon_core`](packages/halkyon_core/README.md) (low-level primitives) and
|
||||
[`halkyon_interop`](packages/halkyon_interop/README.md) (contracts between apps).
|
||||
- `deploy/`: what runs an instance (Docker Compose: Caddy, the server, PostgreSQL),
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ other apps and the packages they share ([the repository's README](../../README.m
|
|||
| `halkyon_learn_curator` | Turns dictionary sources into the files the server loads | AGPL-3.0-or-later |
|
||||
|
||||
It builds on the shared packages in `packages/`: `halkyon_base` (sign-in, accounts, the
|
||||
instance's basics; a Serverpod module), `halkyon_app` (instances, sign-in, sync) and
|
||||
`halkyon_ui` (the look).
|
||||
instance's basics; a Serverpod module), `halkyon_app` (instances, sign-in, sync),
|
||||
`halkyon_editor` (rich text) and `halkyon_ui` (the look).
|
||||
|
||||
## Setting up
|
||||
|
||||
|
|
|
|||
|
|
@ -775,24 +775,9 @@
|
|||
"composerDiscardConfirm": "Verwerfen",
|
||||
"composerConflict": "Diese Karteikarte wurde auf zwei Arten zugleich geändert: auf zwei deiner Geräte oder von dir und durch eine Korrektur im Wörterbuch oder eine neue Veröffentlichung dessen, wovon es kopiert ist. Wähle unten für jeden Teil die Version, die du behalten möchtest.",
|
||||
"composerKeepThis": "Diese behalten",
|
||||
"formatAlignLeft": "Linksbündig",
|
||||
"formatAlignCenter": "Zentriert",
|
||||
"formatAlignRight": "Rechtsbündig",
|
||||
"formatBold": "Fett",
|
||||
"formatItalic": "Kursiv",
|
||||
"formatUnderline": "Unterstrichen",
|
||||
"formatSuperscript": "Hochgestellt",
|
||||
"formatSubscript": "Tiefgestellt",
|
||||
"formatRuby": "Text darüber (Aussprache, Übersetzung)",
|
||||
"formatRubyFurigana": "Furigana (eine Lesung über dem Text)",
|
||||
"formatRubyPinyin": "Pinyin (über den Schriftzeichen)",
|
||||
"formatLink": "Link",
|
||||
"linkTitle": "Link",
|
||||
"linkText": "Text",
|
||||
"linkAddress": "Adresse",
|
||||
"linkAddressHint": "https://…",
|
||||
"linkSave": "Speichern",
|
||||
"linkRemove": "Link entfernen",
|
||||
"rubyTitle": "Text darüber",
|
||||
"rubyTitleFurigana": "Furigana",
|
||||
"rubyTitlePinyin": "Pinyin",
|
||||
|
|
@ -1100,22 +1085,6 @@
|
|||
"fileNotKept": "Die Datei konnte nicht auf diesem Gerät gespeichert werden.",
|
||||
"fileRemoveImage": "Bild entfernen",
|
||||
"fileRemoveSound": "Audio entfernen",
|
||||
"editorInsert": "Einfügen",
|
||||
"blockGroupText": "Text",
|
||||
"blockGroupBlocks": "Blöcke",
|
||||
"blockGroupMedia": "Medien",
|
||||
"blockHeading": "Überschrift {level}",
|
||||
"blockBulletedList": "Aufzählung",
|
||||
"blockNumberedList": "Nummerierte Liste",
|
||||
"blockChecklist": "Checkliste",
|
||||
"blockQuote": "Zitat",
|
||||
"blockListItem": "Listenpunkt",
|
||||
"blockChecklistItem": "Aufgabe",
|
||||
"editorSlashHint": "Tippe / für einen Block oder schreib einfach los",
|
||||
"blockDivider": "Trennlinie",
|
||||
"blockTable": "Tabelle",
|
||||
"blockRemove": "Entfernen",
|
||||
"blockUnknown": "Dieser Teil braucht eine neuere Version der App.",
|
||||
"blockCallout": "Hinweisbox",
|
||||
"blockSpoiler": "Spoiler",
|
||||
"calloutNote": "Notiz",
|
||||
|
|
|
|||
|
|
@ -885,24 +885,9 @@
|
|||
"composerDiscardConfirm": "Discard",
|
||||
"composerConflict": "This flashcard was changed in two ways at once: on two of your devices, or by you and by a correction from the dictionary or a new release of what it was copied from. Choose the version to keep of each part below.",
|
||||
"composerKeepThis": "Keep this",
|
||||
"formatAlignLeft": "Align left",
|
||||
"formatAlignCenter": "Centre",
|
||||
"formatAlignRight": "Align right",
|
||||
"formatBold": "Bold",
|
||||
"formatItalic": "Italic",
|
||||
"formatUnderline": "Underline",
|
||||
"formatSuperscript": "Superscript",
|
||||
"formatSubscript": "Subscript",
|
||||
"formatRuby": "Text above (a pronunciation, a translation)",
|
||||
"formatRubyFurigana": "Furigana (a reading above the text)",
|
||||
"formatRubyPinyin": "Pinyin (above the characters)",
|
||||
"formatLink": "Link",
|
||||
"linkTitle": "Link",
|
||||
"linkText": "Text",
|
||||
"linkAddress": "Address",
|
||||
"linkAddressHint": "https://…",
|
||||
"linkSave": "Save",
|
||||
"linkRemove": "Remove link",
|
||||
"rubyTitle": "Text above",
|
||||
"rubyTitleFurigana": "Furigana",
|
||||
"rubyTitlePinyin": "Pinyin",
|
||||
|
|
@ -1253,27 +1238,6 @@
|
|||
"fileNotKept": "The file couldn't be kept on this device.",
|
||||
"fileRemoveImage": "Remove image",
|
||||
"fileRemoveSound": "Remove sound",
|
||||
"editorInsert": "Insert",
|
||||
"blockGroupText": "Text",
|
||||
"blockGroupBlocks": "Blocks",
|
||||
"blockGroupMedia": "Media",
|
||||
"blockHeading": "Heading {level}",
|
||||
"@blockHeading": {
|
||||
"placeholders": {
|
||||
"level": {"type": "int"}
|
||||
}
|
||||
},
|
||||
"blockBulletedList": "Bulleted list",
|
||||
"blockNumberedList": "Numbered list",
|
||||
"blockChecklist": "Checklist",
|
||||
"blockQuote": "Quote",
|
||||
"blockListItem": "List item",
|
||||
"blockChecklistItem": "To-do",
|
||||
"editorSlashHint": "Type / to insert a block, or just start typing",
|
||||
"blockDivider": "Divider",
|
||||
"blockTable": "Table",
|
||||
"blockRemove": "Remove",
|
||||
"blockUnknown": "This part needs a newer version of the app.",
|
||||
"blockCallout": "Callout",
|
||||
"blockSpoiler": "Spoiler",
|
||||
"calloutNote": "Note",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
"studysetSheetColor": "Color",
|
||||
"tagSheetColor": "Color",
|
||||
"formatAlignCenter": "Center",
|
||||
"homeCustomise": "Customize",
|
||||
"exploreLicense": "License",
|
||||
"exploreLicenseChosen": "License: {count}",
|
||||
|
|
|
|||
|
|
@ -4564,54 +4564,6 @@ abstract class AppLocalizations {
|
|||
/// **'Keep this'**
|
||||
String get composerKeepThis;
|
||||
|
||||
/// No description provided for @formatAlignLeft.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Align left'**
|
||||
String get formatAlignLeft;
|
||||
|
||||
/// No description provided for @formatAlignCenter.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Centre'**
|
||||
String get formatAlignCenter;
|
||||
|
||||
/// No description provided for @formatAlignRight.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Align right'**
|
||||
String get formatAlignRight;
|
||||
|
||||
/// No description provided for @formatBold.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bold'**
|
||||
String get formatBold;
|
||||
|
||||
/// No description provided for @formatItalic.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Italic'**
|
||||
String get formatItalic;
|
||||
|
||||
/// No description provided for @formatUnderline.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Underline'**
|
||||
String get formatUnderline;
|
||||
|
||||
/// No description provided for @formatSuperscript.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Superscript'**
|
||||
String get formatSuperscript;
|
||||
|
||||
/// No description provided for @formatSubscript.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Subscript'**
|
||||
String get formatSubscript;
|
||||
|
||||
/// No description provided for @formatRuby.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -4630,48 +4582,6 @@ abstract class AppLocalizations {
|
|||
/// **'Pinyin (above the characters)'**
|
||||
String get formatRubyPinyin;
|
||||
|
||||
/// No description provided for @formatLink.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Link'**
|
||||
String get formatLink;
|
||||
|
||||
/// No description provided for @linkTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Link'**
|
||||
String get linkTitle;
|
||||
|
||||
/// No description provided for @linkText.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Text'**
|
||||
String get linkText;
|
||||
|
||||
/// No description provided for @linkAddress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Address'**
|
||||
String get linkAddress;
|
||||
|
||||
/// No description provided for @linkAddressHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'https://…'**
|
||||
String get linkAddressHint;
|
||||
|
||||
/// No description provided for @linkSave.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save'**
|
||||
String get linkSave;
|
||||
|
||||
/// No description provided for @linkRemove.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove link'**
|
||||
String get linkRemove;
|
||||
|
||||
/// No description provided for @rubyTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -6478,102 +6388,6 @@ abstract class AppLocalizations {
|
|||
/// **'Remove sound'**
|
||||
String get fileRemoveSound;
|
||||
|
||||
/// No description provided for @editorInsert.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Insert'**
|
||||
String get editorInsert;
|
||||
|
||||
/// No description provided for @blockGroupText.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Text'**
|
||||
String get blockGroupText;
|
||||
|
||||
/// No description provided for @blockGroupBlocks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Blocks'**
|
||||
String get blockGroupBlocks;
|
||||
|
||||
/// No description provided for @blockGroupMedia.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Media'**
|
||||
String get blockGroupMedia;
|
||||
|
||||
/// No description provided for @blockHeading.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Heading {level}'**
|
||||
String blockHeading(int level);
|
||||
|
||||
/// No description provided for @blockBulletedList.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bulleted list'**
|
||||
String get blockBulletedList;
|
||||
|
||||
/// No description provided for @blockNumberedList.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Numbered list'**
|
||||
String get blockNumberedList;
|
||||
|
||||
/// No description provided for @blockChecklist.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Checklist'**
|
||||
String get blockChecklist;
|
||||
|
||||
/// No description provided for @blockQuote.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Quote'**
|
||||
String get blockQuote;
|
||||
|
||||
/// No description provided for @blockListItem.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'List item'**
|
||||
String get blockListItem;
|
||||
|
||||
/// No description provided for @blockChecklistItem.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'To-do'**
|
||||
String get blockChecklistItem;
|
||||
|
||||
/// No description provided for @editorSlashHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Type / to insert a block, or just start typing'**
|
||||
String get editorSlashHint;
|
||||
|
||||
/// No description provided for @blockDivider.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Divider'**
|
||||
String get blockDivider;
|
||||
|
||||
/// No description provided for @blockTable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Table'**
|
||||
String get blockTable;
|
||||
|
||||
/// No description provided for @blockRemove.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove'**
|
||||
String get blockRemove;
|
||||
|
||||
/// No description provided for @blockUnknown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This part needs a newer version of the app.'**
|
||||
String get blockUnknown;
|
||||
|
||||
/// No description provided for @blockCallout.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -2837,30 +2837,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get composerKeepThis => 'Diese behalten';
|
||||
|
||||
@override
|
||||
String get formatAlignLeft => 'Linksbündig';
|
||||
|
||||
@override
|
||||
String get formatAlignCenter => 'Zentriert';
|
||||
|
||||
@override
|
||||
String get formatAlignRight => 'Rechtsbündig';
|
||||
|
||||
@override
|
||||
String get formatBold => 'Fett';
|
||||
|
||||
@override
|
||||
String get formatItalic => 'Kursiv';
|
||||
|
||||
@override
|
||||
String get formatUnderline => 'Unterstrichen';
|
||||
|
||||
@override
|
||||
String get formatSuperscript => 'Hochgestellt';
|
||||
|
||||
@override
|
||||
String get formatSubscript => 'Tiefgestellt';
|
||||
|
||||
@override
|
||||
String get formatRuby => 'Text darüber (Aussprache, Übersetzung)';
|
||||
|
||||
|
|
@ -2870,27 +2846,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get formatRubyPinyin => 'Pinyin (über den Schriftzeichen)';
|
||||
|
||||
@override
|
||||
String get formatLink => 'Link';
|
||||
|
||||
@override
|
||||
String get linkTitle => 'Link';
|
||||
|
||||
@override
|
||||
String get linkText => 'Text';
|
||||
|
||||
@override
|
||||
String get linkAddress => 'Adresse';
|
||||
|
||||
@override
|
||||
String get linkAddressHint => 'https://…';
|
||||
|
||||
@override
|
||||
String get linkSave => 'Speichern';
|
||||
|
||||
@override
|
||||
String get linkRemove => 'Link entfernen';
|
||||
|
||||
@override
|
||||
String get rubyTitle => 'Text darüber';
|
||||
|
||||
|
|
@ -4045,57 +4000,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get fileRemoveSound => 'Audio entfernen';
|
||||
|
||||
@override
|
||||
String get editorInsert => 'Einfügen';
|
||||
|
||||
@override
|
||||
String get blockGroupText => 'Text';
|
||||
|
||||
@override
|
||||
String get blockGroupBlocks => 'Blöcke';
|
||||
|
||||
@override
|
||||
String get blockGroupMedia => 'Medien';
|
||||
|
||||
@override
|
||||
String blockHeading(int level) {
|
||||
return 'Überschrift $level';
|
||||
}
|
||||
|
||||
@override
|
||||
String get blockBulletedList => 'Aufzählung';
|
||||
|
||||
@override
|
||||
String get blockNumberedList => 'Nummerierte Liste';
|
||||
|
||||
@override
|
||||
String get blockChecklist => 'Checkliste';
|
||||
|
||||
@override
|
||||
String get blockQuote => 'Zitat';
|
||||
|
||||
@override
|
||||
String get blockListItem => 'Listenpunkt';
|
||||
|
||||
@override
|
||||
String get blockChecklistItem => 'Aufgabe';
|
||||
|
||||
@override
|
||||
String get editorSlashHint =>
|
||||
'Tippe / für einen Block oder schreib einfach los';
|
||||
|
||||
@override
|
||||
String get blockDivider => 'Trennlinie';
|
||||
|
||||
@override
|
||||
String get blockTable => 'Tabelle';
|
||||
|
||||
@override
|
||||
String get blockRemove => 'Entfernen';
|
||||
|
||||
@override
|
||||
String get blockUnknown => 'Dieser Teil braucht eine neuere Version der App.';
|
||||
|
||||
@override
|
||||
String get blockCallout => 'Hinweisbox';
|
||||
|
||||
|
|
|
|||
|
|
@ -2813,30 +2813,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get composerKeepThis => 'Keep this';
|
||||
|
||||
@override
|
||||
String get formatAlignLeft => 'Align left';
|
||||
|
||||
@override
|
||||
String get formatAlignCenter => 'Centre';
|
||||
|
||||
@override
|
||||
String get formatAlignRight => 'Align right';
|
||||
|
||||
@override
|
||||
String get formatBold => 'Bold';
|
||||
|
||||
@override
|
||||
String get formatItalic => 'Italic';
|
||||
|
||||
@override
|
||||
String get formatUnderline => 'Underline';
|
||||
|
||||
@override
|
||||
String get formatSuperscript => 'Superscript';
|
||||
|
||||
@override
|
||||
String get formatSubscript => 'Subscript';
|
||||
|
||||
@override
|
||||
String get formatRuby => 'Text above (a pronunciation, a translation)';
|
||||
|
||||
|
|
@ -2846,27 +2822,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get formatRubyPinyin => 'Pinyin (above the characters)';
|
||||
|
||||
@override
|
||||
String get formatLink => 'Link';
|
||||
|
||||
@override
|
||||
String get linkTitle => 'Link';
|
||||
|
||||
@override
|
||||
String get linkText => 'Text';
|
||||
|
||||
@override
|
||||
String get linkAddress => 'Address';
|
||||
|
||||
@override
|
||||
String get linkAddressHint => 'https://…';
|
||||
|
||||
@override
|
||||
String get linkSave => 'Save';
|
||||
|
||||
@override
|
||||
String get linkRemove => 'Remove link';
|
||||
|
||||
@override
|
||||
String get rubyTitle => 'Text above';
|
||||
|
||||
|
|
@ -4009,57 +3964,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get fileRemoveSound => 'Remove sound';
|
||||
|
||||
@override
|
||||
String get editorInsert => 'Insert';
|
||||
|
||||
@override
|
||||
String get blockGroupText => 'Text';
|
||||
|
||||
@override
|
||||
String get blockGroupBlocks => 'Blocks';
|
||||
|
||||
@override
|
||||
String get blockGroupMedia => 'Media';
|
||||
|
||||
@override
|
||||
String blockHeading(int level) {
|
||||
return 'Heading $level';
|
||||
}
|
||||
|
||||
@override
|
||||
String get blockBulletedList => 'Bulleted list';
|
||||
|
||||
@override
|
||||
String get blockNumberedList => 'Numbered list';
|
||||
|
||||
@override
|
||||
String get blockChecklist => 'Checklist';
|
||||
|
||||
@override
|
||||
String get blockQuote => 'Quote';
|
||||
|
||||
@override
|
||||
String get blockListItem => 'List item';
|
||||
|
||||
@override
|
||||
String get blockChecklistItem => 'To-do';
|
||||
|
||||
@override
|
||||
String get editorSlashHint =>
|
||||
'Type / to insert a block, or just start typing';
|
||||
|
||||
@override
|
||||
String get blockDivider => 'Divider';
|
||||
|
||||
@override
|
||||
String get blockTable => 'Table';
|
||||
|
||||
@override
|
||||
String get blockRemove => 'Remove';
|
||||
|
||||
@override
|
||||
String get blockUnknown => 'This part needs a newer version of the app.';
|
||||
|
||||
@override
|
||||
String get blockCallout => 'Callout';
|
||||
|
||||
|
|
@ -8046,9 +7950,6 @@ class AppLocalizationsEnUs extends AppLocalizationsEn {
|
|||
@override
|
||||
String get tagSheetColor => 'Color';
|
||||
|
||||
@override
|
||||
String get formatAlignCenter => 'Center';
|
||||
|
||||
@override
|
||||
String get homeCustomise => 'Customize';
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/app/app_router.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
||||
|
|
@ -10,7 +11,6 @@ import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.
|
|||
import 'package:halkyon_learn_flutter/src/core/device_settings/bloc/device_settings_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/device_settings/widget/device_settings_scope.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/core_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/editor_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/theme/learn_theme.dart';
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ import 'dart:async';
|
|||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/editor_blocks.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
|
@ -17,7 +16,7 @@ import 'package:material_ui/material_ui.dart';
|
|||
/// one opens and closes with the header's button, for that view alone.
|
||||
/// A callout can't go inside another.
|
||||
final calloutBlock = InsertableBlock(
|
||||
name: (l10n) => l10n.blockCallout,
|
||||
name: (context) => AppLocalizations.of(context).blockCallout,
|
||||
icon: LucideIcons.messageSquareWarning,
|
||||
group: InsertGroup.blocks,
|
||||
keywords: const ['callout', 'note', 'info', 'tip', 'warning', 'admonition'],
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/callout_block.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/editor_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/file_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/math_block.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/ruby.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/spoiler_block.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/youtube_block.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// The blocks every editor knows: the editor's own, and the core's.
|
||||
/// The blocks every editor of Learn knows: halkyon_editor's, and the core's.
|
||||
final coreBlocks = EditorBlocks(
|
||||
builders: {
|
||||
...standardBlockComponentBuilderMap,
|
||||
...basicBlocks.builders,
|
||||
...fileBlockBuilders,
|
||||
...calloutBlockBuilders,
|
||||
...spoilerBlockBuilders,
|
||||
|
|
@ -20,68 +18,7 @@ final coreBlocks = EditorBlocks(
|
|||
...youtubeBlockBuilders,
|
||||
},
|
||||
insertable: [
|
||||
for (final level in [1, 2, 3])
|
||||
_textBlock(
|
||||
name: (l10n) => l10n.blockHeading(level),
|
||||
icon: const [
|
||||
LucideIcons.heading1,
|
||||
LucideIcons.heading2,
|
||||
LucideIcons.heading3,
|
||||
][level - 1],
|
||||
keywords: ['heading', 'h$level', 'title'],
|
||||
node: () => headingNode(level: level),
|
||||
),
|
||||
_textBlock(
|
||||
name: (l10n) => l10n.blockBulletedList,
|
||||
icon: LucideIcons.list,
|
||||
keywords: const ['bulleted list', 'bullet', 'unordered'],
|
||||
node: bulletedListNode,
|
||||
),
|
||||
_textBlock(
|
||||
name: (l10n) => l10n.blockNumberedList,
|
||||
icon: LucideIcons.listOrdered,
|
||||
keywords: const ['numbered list', 'ordered', 'number'],
|
||||
node: numberedListNode,
|
||||
),
|
||||
_textBlock(
|
||||
name: (l10n) => l10n.blockChecklist,
|
||||
icon: LucideIcons.listTodo,
|
||||
keywords: const ['checklist', 'todo', 'checkbox', 'task'],
|
||||
node: () => todoListNode(checked: false),
|
||||
),
|
||||
_textBlock(
|
||||
name: (l10n) => l10n.blockQuote,
|
||||
icon: LucideIcons.textQuote,
|
||||
keywords: const ['quote', 'citation'],
|
||||
node: quoteNode,
|
||||
),
|
||||
InsertableBlock(
|
||||
name: (l10n) => l10n.blockDivider,
|
||||
icon: LucideIcons.separatorHorizontal,
|
||||
group: InsertGroup.blocks,
|
||||
keywords: const ['divider', 'horizontal rule', 'line', 'separator'],
|
||||
insert: (context, editorState, at) =>
|
||||
insertBlock(editorState, dividerNode(), at: at),
|
||||
),
|
||||
InsertableBlock(
|
||||
name: (l10n) => l10n.blockTable,
|
||||
icon: LucideIcons.table,
|
||||
group: InsertGroup.blocks,
|
||||
keywords: const ['table', 'grid', 'rows', 'columns'],
|
||||
insideCallouts: false,
|
||||
// Two rows of two cells, the caret in the first.
|
||||
insert: (context, editorState, at) => insertBlock(
|
||||
editorState,
|
||||
TableNode.fromList([
|
||||
['', ''],
|
||||
['', ''],
|
||||
]).node,
|
||||
at: at,
|
||||
caretIn: const [0, 0],
|
||||
lineAfter: true,
|
||||
insideCallouts: false,
|
||||
),
|
||||
),
|
||||
...basicBlocks.insertable,
|
||||
calloutBlock,
|
||||
spoilerBlock,
|
||||
mathBlock,
|
||||
|
|
@ -90,24 +27,12 @@ final coreBlocks = EditorBlocks(
|
|||
youtubeBlock,
|
||||
],
|
||||
shortcuts: [leaveCalloutOnEnter],
|
||||
textMarks: [withRuby, withInlineMath],
|
||||
formatActions: [rubyFormatAction, inlineMathFormatAction],
|
||||
onEditorClosed: [hideHoverReading],
|
||||
);
|
||||
|
||||
/// The blocks of the editors below [context]: the core's, and the
|
||||
/// features' too where the app gives them ([EditorBlocksScope]).
|
||||
EditorBlocks blocksOf(BuildContext context) =>
|
||||
EditorBlocksScope.maybeOf(context) ?? coreBlocks;
|
||||
|
||||
/// A line of text of a kind, which the caret goes into.
|
||||
InsertableBlock _textBlock({
|
||||
required String Function(AppLocalizations l10n) name,
|
||||
required IconData icon,
|
||||
required List<String> keywords,
|
||||
required Node Function() node,
|
||||
}) => InsertableBlock(
|
||||
name: name,
|
||||
icon: icon,
|
||||
group: InsertGroup.text,
|
||||
keywords: keywords,
|
||||
insert: (context, editorState, at) =>
|
||||
insertBlock(editorState, node(), at: at, caretIn: const []),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,18 +3,15 @@ import 'dart:developer';
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/bloc/sound_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/media_type.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/widget/stored_file_image.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/widget/studyset_files.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/atomic_block.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/editor_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
|
@ -60,7 +57,7 @@ final Map<String, BlockComponentBuilder> fileBlockBuilders = {
|
|||
|
||||
/// An image from the person's device, kept as a file of theirs.
|
||||
final imageBlock = InsertableBlock(
|
||||
name: (l10n) => l10n.fileImage,
|
||||
name: (context) => AppLocalizations.of(context).fileImage,
|
||||
icon: LucideIcons.image,
|
||||
group: InsertGroup.media,
|
||||
keywords: const ['image', 'picture', 'photo'],
|
||||
|
|
@ -70,7 +67,7 @@ final imageBlock = InsertableBlock(
|
|||
|
||||
/// A sound from the person's device, kept as a file of theirs.
|
||||
final soundBlock = InsertableBlock(
|
||||
name: (l10n) => l10n.fileSound,
|
||||
name: (context) => AppLocalizations.of(context).fileSound,
|
||||
icon: LucideIcons.volume2,
|
||||
group: InsertGroup.media,
|
||||
keywords: const ['sound', 'audio', 'pronunciation'],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/core_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_host.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// halkyon_editor's [RichContentEditor] with Learn's blocks ([blocksOf]) and,
|
||||
/// with [grammar], spelling and grammar checked as the text is written, in
|
||||
/// that language ([GrammarHost], ADR 0024).
|
||||
class const LearnContentEditor({
|
||||
/// The content the editor opens with; later changes to it are ignored.
|
||||
required final RichContent initialContent,
|
||||
|
||||
/// Called with the whole content after every change to the document.
|
||||
required final ValueChanged<RichContent> onChanged,
|
||||
|
||||
/// Whether the editor takes the focus as it opens, its cursor at the end.
|
||||
final bool autofocus = false,
|
||||
|
||||
/// The editor's focus, to follow or move it; one of its own otherwise.
|
||||
final FocusNode? focusNode,
|
||||
|
||||
/// Whether a [FormatBar] shows below the editor while it has the focus.
|
||||
final bool formatBar = false,
|
||||
|
||||
/// The room around the text, within the editor's scrolling.
|
||||
final EdgeInsets padding = EdgeInsets.zero,
|
||||
|
||||
/// The language the text is checked in as it is written; null: it isn't.
|
||||
final GrammarLanguage? grammar,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => RichContentEditor(
|
||||
initialContent: initialContent,
|
||||
onChanged: onChanged,
|
||||
autofocus: autofocus,
|
||||
focusNode: focusNode,
|
||||
formatBar: formatBar,
|
||||
padding: padding,
|
||||
blocks: blocksOf(context),
|
||||
wrapEditor: grammar == null ? null : _checked,
|
||||
);
|
||||
|
||||
Widget _checked(
|
||||
BuildContext context,
|
||||
EditorState editorState,
|
||||
FocusNode focusNode,
|
||||
Widget editor,
|
||||
) => switch (grammar) {
|
||||
// Checked for the person signed in; nobody is outside their app.
|
||||
final language? when PersonDependencies.maybeOf(context) != null =>
|
||||
GrammarHost(
|
||||
editorState: editorState,
|
||||
focusNode: focusNode,
|
||||
language: language,
|
||||
child: editor,
|
||||
),
|
||||
_ => editor,
|
||||
};
|
||||
}
|
||||
|
||||
/// halkyon_editor's [RichContentView] with Learn's blocks ([blocksOf]).
|
||||
class const LearnContentView({
|
||||
/// The content to show.
|
||||
required final RichContent content,
|
||||
|
||||
/// The text's style; the theme's `bodyLarge` without.
|
||||
final TextStyle? textStyle,
|
||||
|
||||
/// How paragraphs and headings align where the editor gave them no
|
||||
/// alignment of their own; at the start without.
|
||||
final TextAlign? textAlign,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => RichContentView(
|
||||
content: content,
|
||||
textStyle: textStyle,
|
||||
textAlign: textAlign,
|
||||
blocks: blocksOf(context),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/atomic_block.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/editor_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/formula_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/math_view.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
|
@ -12,7 +10,7 @@ import 'package:material_ui/material_ui.dart';
|
|||
/// ([editFormula]) and inserted once saved; a tap on it in the editor opens
|
||||
/// it again.
|
||||
final mathBlock = InsertableBlock(
|
||||
name: (l10n) => l10n.blockFormula,
|
||||
name: (context) => AppLocalizations.of(context).blockFormula,
|
||||
icon: LucideIcons.sigma,
|
||||
group: InsertGroup.blocks,
|
||||
keywords: const ['formula', 'math', 'equation', 'latex', 'tex'],
|
||||
|
|
@ -38,6 +36,18 @@ final Map<String, BlockComponentBuilder> mathBlockBuilders = {
|
|||
mathBlockType: _MathBlockBuilder(),
|
||||
};
|
||||
|
||||
/// The format bar's button for formulas within lines (Σ).
|
||||
final inlineMathFormatAction = FormatAction(
|
||||
icon: LucideIcons.sigma,
|
||||
tooltip: (context) => AppLocalizations.of(context).formatInlineMath,
|
||||
active: (editorState) =>
|
||||
editorState.selection != null &&
|
||||
(editorState.getDeltaAttributeValueInSelection<bool>(inlineMathKey) ??
|
||||
false),
|
||||
run: toggleInlineMath,
|
||||
refocus: true,
|
||||
);
|
||||
|
||||
/// Makes the selection of [editorState] a formula within its line, or
|
||||
/// plain text again if it is one; at a caret, writes a new formula on its
|
||||
/// own screen and puts it there. A formula within a line is text marked
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_document.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
|
||||
/// What Markdown has no words of its own for, in the person's language: a
|
||||
/// sound's link, a video's link without a caption, a spoiler's summary, a
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/widget/studyset_files.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/learn_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/grammar/grammar_host.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
|
@ -84,7 +84,7 @@ class _RichTextEditorState() extends State<_RichTextEditor> {
|
|||
children: [
|
||||
SizedBox(
|
||||
height: math.max(320, constraints.maxHeight - 32),
|
||||
child: RichContentEditor(
|
||||
child: LearnContentEditor(
|
||||
initialContent: widget.initial,
|
||||
onChanged: (text) => _text = text,
|
||||
formatBar: true,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/content_languages.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
// Text above (ruby): text set small above text within a line, kept on the
|
||||
|
|
@ -49,6 +50,16 @@ String rubyButtonName(BuildContext context) {
|
|||
};
|
||||
}
|
||||
|
||||
/// The format bar's button for text above.
|
||||
final rubyFormatAction = FormatAction(
|
||||
icon: LucideIcons.languages,
|
||||
tooltip: rubyButtonName,
|
||||
active: (editorState) =>
|
||||
editorState.selection != null &&
|
||||
editorState.getDeltaAttributeValueInSelection<String>(rubyKey) != null,
|
||||
run: editRuby,
|
||||
);
|
||||
|
||||
/// Gives the text selected in [editorState] text above, asked for starting
|
||||
/// from what it has; empty takes it away. With the caret in text that has
|
||||
/// text above (or right after it), that whole text is selected and edited,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue