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

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