feat(app): editor blocks: callout, spoiler, formulas, files, video, study button
All checks were successful
CI / check (push) Successful in 8m34s
CI / deploy (push) Successful in 1m1s

Blocks beyond the editor's own, each built once for editor and view:
- callout: eight kinds in theme colours, kind, title and "folded at
  first" from its header; written in place, left with Enter on its
  empty last line; never inside another
- spoiler: written in plain sight, hidden behind Reveal read-only
- formula on its own line, written on its own screen with a preview
  and snippet buttons, and formulas within a line (Σ in the format
  bar); set by flutter_math_fork behind MathView, LaTeX's numbered
  environments read as their aligned forms
- image and sound picked from the device, kept as synced files
- YouTube video from its link, with its start and a caption: a card
  until played, nothing fetched before; played in place where a web
  view exists (youtube_player_iframe, privacy-enhanced), else opened
  in the browser, behind YoutubeVideos in Dependencies
- study button: a studyset, a tag or all cards, with or without its
  topics, back first; its StudyFocus rides on the study route and
  narrows that session only
- tables, already in the editor, now insertable everywhere

An Insert button opens on the format bar, so blocks can be inserted on
phones too; the desktop's / menu lists the same blocks, translated.
Card faces now dock the format bar while written, so cards get blocks
and formatting on phones as well. The blocks are listed in
EditorBlocks: the core's, plus the features' through
EditorBlocksScope. Blocks of a type the app doesn't know say they need
a newer version and stay as they are, so formatVersion stays 1.

Their types and keys are in content_blocks.dart, which RichContent
reads: a block's text counts as text, and a video or study button
isn't blank. The path import now makes formulas real ones.

integration_test/youtube_player_test.dart plays a real video through
the app's player on a device, which no widget test can: YouTube
refuses embeds that don't say where they are embedded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-03 12:20:41 +02:00
commit ec34bb2334
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1683,6 +1683,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get commonCancel => 'Cancel';
@override
String get commonSave => 'Save';
@override
String get commonClose => 'Close';
@ -1730,6 +1733,278 @@ class AppLocalizationsEn extends AppLocalizations {
String get fileSoundUnplayable =>
'This sound can\'t be played. Its file may be damaged, or in a format this device can\'t play.';
@override
String get fileImages => 'Images';
@override
String get fileSounds => 'Sounds';
@override
String get fileNotAnImage => 'This file isn\'t an image the app can show.';
@override
String get fileNotASound => 'This file isn\'t a sound the app can play.';
@override
String fileTooLarge(int megabytes) {
return 'This file is larger than $megabytes MB.';
}
@override
String get fileNotKept => 'The file couldn\'t be kept on this device.';
@override
String get fileRemoveImage => 'Remove image';
@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 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';
@override
String get blockSpoiler => 'Spoiler';
@override
String get calloutNote => 'Note';
@override
String get calloutInfo => 'Info';
@override
String get calloutTip => 'Tip';
@override
String get calloutSuccess => 'Success';
@override
String get calloutQuestion => 'Question';
@override
String get calloutWarning => 'Warning';
@override
String get calloutDanger => 'Danger';
@override
String get calloutQuote => 'Quote';
@override
String get calloutOptions => 'Callout options';
@override
String get calloutEditTitle => 'Edit title…';
@override
String get calloutFoldedAtFirst => 'Folded at first';
@override
String get calloutRemove => 'Remove callout';
@override
String get calloutShow => 'Show';
@override
String get calloutHide => 'Hide';
@override
String get calloutWriteInside => 'Write inside';
@override
String get calloutTitleDialog => 'Callout title';
@override
String get calloutTitleLabel => 'Title';
@override
String get calloutTitleHelper => 'Leave it empty to show the kind\'s name.';
@override
String get spoilerReveal => 'Reveal';
@override
String get spoilerPlaceholder => 'Hidden text, revealed on a tap';
@override
String get blockFormula => 'Formula';
@override
String get formatInlineMath => 'Formula in the line';
@override
String get formulaTitle => 'Formula';
@override
String get formulaInLineTitle => 'Formula in the line';
@override
String get formulaPreview => 'Preview';
@override
String get formulaPreviewEmpty => 'The formula shows here';
@override
String get formulaLatex => 'LaTeX';
@override
String get formulaLatexHelper =>
'\\\\ starts a new line in aligned, cases or a matrix.';
@override
String get formulaEmpty => 'An empty formula. Tap to write it.';
@override
String get formulaEdit => 'Edit formula';
@override
String get formulaRemove => 'Remove formula';
@override
String get blockYoutube => 'YouTube video';
@override
String get youtubeLink => 'YouTube link';
@override
String get youtubeLinkHelper => 'Paste a link from YouTube.';
@override
String get youtubeNotFound => 'No YouTube video in this link.';
@override
String get youtubeFound => 'Found the video.';
@override
String youtubeFoundFrom(String time) {
return 'Found the video, starting at $time.';
}
@override
String get youtubeCaption => 'Caption';
@override
String get youtubeCaptionHelper =>
'Shown in place of the video until it plays.';
@override
String get youtubeVideo => 'YouTube video';
@override
String get youtubeSource => 'YouTube';
@override
String youtubeFrom(String time) {
return 'YouTube · from $time';
}
@override
String get youtubeEdit => 'Edit video';
@override
String get youtubePlay => 'Play video';
@override
String get youtubeOpen => 'Open on YouTube';
@override
String get youtubeRemove => 'Remove video';
@override
String get youtubeBroken => 'This video\'s link can\'t be read.';
@override
String get youtubeNotOpened => 'Couldn\'t open YouTube.';
@override
String get blockStudyButton => 'Study button';
@override
String get studyButtonDefaultLabel => 'Study';
@override
String get studyButtonStudyset => 'Studyset to study';
@override
String get studyButtonTag => 'Cards';
@override
String get studyButtonAllCards => 'All cards';
@override
String get studyButtonLabel => 'Label';
@override
String get studyButtonTopics => 'With its topics';
@override
String get studyButtonBackFirst => 'Back first';
@override
String get studyButtonBackFirstHelper =>
'Front/back and vocabulary cards ask their back.';
@override
String studyButtonWithTag(String tag) {
return 'Tagged $tag';
}
@override
String get studyButtonAlone => 'Without its topics';
@override
String get studyButtonEdit => 'Edit study button';
@override
String get studyButtonRemove => 'Remove study button';
@override
String get studyButtonBroken =>
'This study button doesn\'t say what to study.';
@override
String get studyButtonGone => 'That studyset no longer exists.';
@override
String get ankiImportMenu => 'Import from Anki';