Skip to main content
Written on 31/10/2026 from the code on the chat+ai branch of LMS_Frontend (up to Update 180). Keywords for search: SunEditor, flutter_quill, Quill Delta, Fleather, HtmlWidget, AppHtmlContent, RichNoteEditor, YouTube, Error 153, WebView, Referer.
In one sentence. Lessons and classrooms are written in HTML (SunEditor on the web), every other rich-text field is a Quill Delta JSON string written with flutter_quill, and every authored HTML is shown through AppHtmlContent, the one viewer that knows how to play YouTube and Google Drive embeds on mobile.

At a glance

“Native apps” means tablets in practice. Lessons, classrooms, explanations, instructor profiles and order notes are all edited by staff (instructor, content manager, admin). The app signs every staff role out on a phone (“this role is not supported on this device”), so the native editors only ever run on an Android tablet or an iPad, where a staff user can sign in. Students on phones only see the viewers.

1. Editors

1.1 Lesson and classroom content: HtmlContentEditor

One widget, two engines, chosen by platform. Code: lib/ui/widgets/rich_text_editor/.
  • SunEditor 3.4 (MIT), self-hosted in web/suneditor/ (no CDN): suneditor.min.js / .css, lms_editor.html (the page), lms_editor.js (the config, the app’s buttons and the bridge), lms_editor.css, suneditor-dark.css, and langs/ar.js (our own Arabic file: SunEditor ships none).
  • Loaded in an <iframe> by SunEditorFrame (sun_editor_frame_web.dart, a Flutter HtmlElementView). The page is same-origin, so the app reads the HTML synchronously (window.LmsEditor.getHtml()) and the save paths stay synchronous.
  • Toolbar: styles, colors, lists, tables, links, image / video / iframe embeds, HTML code view, plus the app’s upload a PDF button and the host’s own buttons (the classroom’s Google Calendar).
  • Every file goes to Cloudflare R2, never base64: images (dialog, paste, drop) and PDFs are sent to Flutter, which uploads them with POST /upload_file (type 3), naming the host’s training (and saved lesson) or classroom by id; the backend puts the file in that owner’s folder (files/all-lessons/{training-slug} or files/all-classrooms/{slug}, see R2 Storage Paths), then the app inserts the <img> or the PDF <iframe>. Other file types are refused.
  • While a dialog covers the editor, the iframe gets pointer-events: none, or it would take the dialog’s clicks.
  • In use since Update 153 (14/10/2026). To upgrade SunEditor, follow the comment at the top of lms_editor.html and bump kSunEditorVersion (3.4.0-1 today).
Shared rules
  • Unedited content comes back byte for byte. HtmlContentEditorController.html returns the loaded string until the user edits something, so opening and saving never rewrites a lesson.
  • Hosts: the lesson editor add_lessons_content.dart (admin, instructor, content manager) and the classroom editor classroom_content_widget.dart.
  • Guarded by test/html_content_editor_test.dart and test/html_quill_codec_test.dart.

1.2 Question explanations: QuestionExplanationDialog

  • lib/ui/widgets/quiz/question/question_explanation_dialog.dart, opened from the quiz / exam editor’s questions step (questions_create_widget.dart).
  • flutter_quill + flutter_quill_extensions (embed builders). Saved in question.explanation as a Quill Delta JSON string, for example [{"insert":"Because "},{"insert":"PMP","attributes":{"bold":true}},{"insert":" is right.\n"}].
  • The video / image toolbar buttons are off, so a Quill explanation never carries a video. An explanation can still be HTML (imported quizzes, older rows): see the viewer in §2.2.

1.3 Instructor descriptions and order notes: RichNoteEditor

Since Update 180 (31/10/2026). These two fields used Fleather, a fork of Quill; the app now uses flutter_quill for them too, and fleather is no longer a dependency.
  • Code: lib/ui/widgets/rich_text_editor/rich_note_editor.dart (RichNoteEditor, RichNoteView) and rich_note_document.dart (RichNoteDocument.parse / encode / plainText).
  • Hosts:
  • Toolbar (one row in the dialogs): undo / redo, bold, italic, underline, strikethrough, text color, background color, clear format, left / center / right alignment, text direction, heading, numbered and bulleted lists, indent, link.
  • Saved as Quill Delta JSON, like question explanations.
Fleather saved a Delta too, but with its own attribute names. RichNoteDocument.parse converts them when the row is loaded; the row is stored in the Quill format the next time someone saves it. Nothing is migrated on the server.Guarded by test/rich_note_editor_test.dart.

2. Viewers

2.1 AppHtmlContent: the one viewer for authored HTML

lib/ui/widgets/shared/app_html_content.dart (since Update 60). It is HtmlWidget from flutter_widget_from_html plus the rules for the embeds the stock renderer gets wrong, in one place (AppHtmlContent.embedFor): It also removes the 40px indent of SunEditor’s <figure> wrappers (images, videos, tables).
Never give AppHtmlContent (or the HtmlWidget inside) a UniqueKey. Every rebuild would parse the HTML again and recreate the embeds, and a rotation is a rebuild: a video would restart while going full screen. Key it by the content’s identity (the course player uses the lesson id).

2.2 Question explanations: QuestionExplanationView

lib/ui/widgets/quiz/question/question_explanation_view.dart, used by the exam and training-quiz review “tips” cards on the web (and tablets). The two phone review screens (quiz_review_question_tips_mobile.dart, training_quiz_review_question_tips_mobile_widget.dart) and the staff exam answer-key preview (admin_exam_preview_answers_widget.dart) build their own read-only QuillEditor instead. A Quill explanation looks the same there; an HTML explanation shows as raw markup on phones (known issue 12, see §4.4).

2.3 Instructor descriptions and order notes

  • RichNoteView draws a description read-only (nothing for an empty one). It is used by instructor_profile_item.dart, so every list of educations / work experiences shows it: the instructor’s own profile pages and the public instructor profile students open (web and phone).
  • The orders table shows notes as plain text (RichNoteDocument.plainText, two lines with a tooltip): no editor per row. The same helper finds the Konnect payment link in the notes.

2.4 Other HTML on screen


3. Packages


4. YouTube embeds and “Error 153”

4.1 The problem

On the mobile app, a YouTube video placed in a lesson as an <iframe> showed “Video player configuration error – Error 153” instead of playing. The web app was fine. Cause. YouTube now requires every embedded player to identify the app or site that embeds it, through the HTTP Referer header (“embedded player client identity”). The browser sends it on the web. On mobile:
  • HtmlWidget renders an <iframe> in a WebView that opens youtube.com/embed/<id> directly, with no Referer → Error 153;
  • youtube_player_iframe loads its player with https://www.youtube.com as base URL → Error 152.
Measured with the same video:

4.2 The fix: AppYoutubeEmbed

lib/ui/widgets/shared/video_player/app_youtube_embed.dart (Update 50, 02/10/2026):
  • loads YouTube’s standard embed in a WebView page whose base URL is the app ID, kYoutubeEmbedReferrer = 'https://com.example.strived', so YouTube receives a valid Referer;
  • 16:9 frame with YouTube’s own controls; keeps the start / t time of a pasted iframe;
  • full screen goes landscape on Android (Back or YouTube’s exit button returns to portrait); iOS plays inline (allowsInlineMediaPlayback) and uses its native full screen;
  • “Watch on YouTube” and other links open in the YouTube app or the browser;
  • setTextZoom(100) on Android, so the system font size does not distort the player.
mobileYoutubeIframeEmbed(src) returns that player for a YouTube <iframe> on any native device (phone or tablet) and null on the web, where the browser’s iframe already works. WebYouTubePlayer uses AppYoutubeEmbed on mobile and youtube_player_iframe on the web.
kYoutubeEmbedReferrer must stay equal to the Android applicationId / iOS bundle ID. The iOS bundle ID is still the com.example.strived placeholder: if it changes, change the constant too, or every mobile video goes back to Error 153.

4.3 How the fix reached every screen

1

Update 50 · 02/10/2026 — lessons

The course player and the lesson details screen route YouTube iframes to AppYoutubeEmbed. WebYouTubePlayer switches to it on mobile, which also fixes the training preview page videos.
2

Update 60 · 03/10/2026 — classrooms, one renderer

Classroom content still showed Error 153: its viewer had its own copy of the rules, without YouTube. Instead of a third copy, the rules moved into AppHtmlContent, and the classroom viewer, the course player and lesson details all use it. Any later fix to embedFor reaches every viewer at once.
3

Update 180 · 31/10/2026 — question explanations

Known issue 12: an HTML explanation was still drawn by a bare HtmlWidget, so a YouTube iframe in it would show Error 153 in the native app. QuestionExplanationView now uses AppHtmlContent for HTML. Guarded by a YouTube case in test/question_explanation_view_test.dart.

4.4 What is left

Known issue 12 (reviewed 31/10/2026):
  • the training preview page’s lesson preview dialog works (it plays through WebYouTubePlayer) but keeps its own copy of the embed rules (Drive PDF at 60 % instead of 70 %): move it to AppHtmlContent;
  • the two phone review screens draw explanations with their own QuillEditor, so an HTML explanation shows as markup there: use QuestionExplanationView.

5. Rules for new code

  • Show authored HTML with AppHtmlContent, never a bare HtmlWidget, and add any new embed fix to AppHtmlContent.embedFor so every screen gets it.
  • Short rich-text fields: use RichNoteEditor / RichNoteView and save with RichNoteDocument.encode. Read with RichNoteDocument.parse, which accepts Quill, Fleather and plain text. Don’t add Fleather back.
  • Import flutter_widget_from_html with show HtmlWidget next to flutter_quill: both export a DefaultStyles.
  • Don’t swap HtmlQuillCodec for flutter_quill_delta_from_html: it loses iframes, tables and paragraph breaks.
  • Lesson and classroom HTML that was only opened must be saved unchanged (the controller returns the loaded string until an edit).

6. Writing HTML that looks the same in the editor and on the learners’ page

Found on 05/11/2026 while building the content of the TEST classroom class - 10 (a Google Meet card, a weekly plan table, a callout) with SunEditor 3.4 and flutter_widget_from_html 0.17.4. The card’s HTML, ready to paste in the code view, is in the admin and instructor guides on the user guide site (Classrooms and Chatrooms → The content editor). What SunEditor keeps (strict mode, the default tagStyles plus the iframe / image additions of lms_editor.js): padding is dropped everywhere (except on SunEditor’s own <figure>), so a styled <div>, a padded box or a button-styled link loses its look as soon as the editor loads it. What the viewer does with it
  • A table’s width moves to SunEditor’s <figure> wrapper, and cell widths move to <col style="96px"> (SunEditor 3.4 writes them without the property name). The viewer ignores both: a table is as wide as its content, up to the column. It fills the column only when its text needs the room (a two-line description does it).
  • Tables get a 2px border-spacing unless they have border-collapse: collapse. With collapse, a cell’s wider border wins over the table’s own border. SunEditor drops border-collapse: separate on the first load and border-spacing on the second, so don’t rely on either.
  • An image inside SunEditor’s image component ignores its margin (AppHtmlContent.editorFigureStyles sets the figure’s margin to 0), and an image in a cell with a border-top is squashed. Space an image with spacer cells (<td style="border: 0"><p style="margin: 0 10px">&nbsp;</p></td>) and a spacer line above it (<p style="margin: 0; line-height: 18px"><br></p>). Cell borders in the background color work in the viewer but break the card’s outline in the editor.
  • Links are always underlined; a <span style="color: …"> inside the link sets their color.
  • A nested one-cell table with a background-color is a button (SunEditor keeps nested tables, in their own <figure>).
How to check a design. Load the HTML in web/suneditor/lms_editor.html (LmsEditor.start, then LmsEditor.getHtml()), feed the result back once more, and save the second result: that is what a later edit in the editor would save, so the page stays the same after someone opens it. Then look at it on ⋯ → Preview. The classroom endpoint PUT /classroom/update/{id} with only content changes nothing else, which makes trying several versions quick on a test platform.