Image library: - "Clean up duplicates" finds byte-identical images by md5, keeps one file per group (preferring the most-used, then the oldest), merges the tags/descriptions and captions of the copies, repoints slides in open decks and in .md presentations on disk, and deletes the copies after a confirmation that lists every group. - A header toggle filters to images without tags/description, so it is easy to see which ones still need attention. - The delete warning now also lists presentations on disk that still reference the image (marked "not open"), next to the open decks. Editor and accessibility (already in tree): - Interface text scaling up to 200%, keyboard-operable panel divider, keyboard-first add-slide dialog, and screen-reader improvements. - Paste a spreadsheet/CSV/markdown selection into a table cell to fill the whole grid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.8 KiB
OciDeck — Architecture
A high-level map of how OciDeck is put together, for contributors. For how files
are stored on disk, see FILE_FORMAT.md.
Stack
- Flutter desktop app (macOS, Windows, Linux), Dart 3.12+.
- State: Riverpod.
- Storage: standard Marp Markdown (
.md) as the single source of truth, with sidecars for anything that isn't plain Marp.
Module layout
lib/
models/ # Deck, Slide, Settings/ThemeProfile, Chart, Annotation
services/ # markdown, file, export, image, caption, description,
# image_dedup (md5 duplicates), image_reference (.md rewrites),
# recovery, rasterizer, marp_html, annotation_codec
state/ # Riverpod providers: deck, editor, settings, tabs, clipboard
widgets/ # app shell, panels, dialogs, per-type editors, slides, presenter
l10n/ # AppLocalizations (8 languages)
theme/ # app theming
utils/ # small shared helpers (clipboard table parsing, URL launching)
Data model
Deckholds metadata, a list of **Slide**s, the activeThemeProfile, the deck-wide TLP level, and an in-memory annotation layer (Map<slideId, List<InkStroke>>) that is never serialized into the Markdown.Slideis a single immutable value with aSlideTypeand typed fields. A few types reusecustomMarkdownfor their payload: free-Markdown (raw),code(the source), andchart(the JSON spec).- Slide ids are regenerated on every parse, so they are stable only within a session. Anything persisted that must survive a reload (annotations) re-anchors by slide order + a content fingerprint rather than by id.
Markdown round-trip
MarkdownService is the contract:
generateDeck/generateSlidewrite Marp Markdown. OciDeck extras live in front-matter keys and<!-- … -->comments that Marp ignores.parseDeck/_parseBlockread it back.codeandchartslides are detected by their_classand parsed separately (their fenced block would otherwise confuse the generic line parser).
This service is heavily covered by the round-trip tests — treat it as the
source-of-truth for the file format and keep FILE_FORMAT.md in sync.
The two rendering worlds
Charts, diagrams, and slides are rendered in two independent places, which is the key thing to understand before touching rendering:
- In-app —
widgets/slides/slide_preview.dart(SlidePreviewWidget) renders a slide as Flutter widgets. The same widget is used for the editor preview, thumbnails, the fullscreen presenter, and — viaservices/slide_rasterizer.dart— the PDF and PPTX exports (rasterized to images). So anything that must appear in PDF/PPTX must render here. Charts usefl_chart. - HTML export —
services/marp_html_service.dartproduces a single self-contained.htmlthat renders in a browser using inlined JavaScript (marked, highlight.js, mermaid, MathJax). Charts are pre-rendered to inline SVG in Dart here (no JS chart library). Fidelity differs from the in-app renderer by design.
Presenter
widgets/presentation/fullscreen_presenter.dart drives presenting:
- Keyboard navigation, presenter view, blank screen, grid overview, auto-advance, and the annotation tools (pen/highlighter/eraser/laser).
- Neighbour slide images are precached and
gaplessPlaybackis on, so slide changes never flash black (important for screen recording).
Dual-screen mode
When a second display is present (shouldUseDualScreen), the presenter runs in
two OS windows:
- The laptop window shows the presenter view.
- A borderless audience window (
audience_window.dart) fills the external screen with the slide. - They sync over method channels (
ocideck/audience,ocideck/presenter): current index, blank state, ink strokes, and the laser pointer. Media plays only on the beamer to avoid double audio.
This needs a real second window, which window_manager (single-window) can't do,
hence the vendored multi-window fork below.
Sidecars (separate layers)
To keep the .md pure Marp, four kinds of data live beside it (see
FILE_FORMAT.md §6):
- Captions —
.ocideck_captions.json(per image, inimages/). - Descriptions/tags —
.ocideck_descriptions.json(searchable image metadata, used by the library's search and the untagged filter). - Annotations —
<name>.ink.json(services/annotation_codec.dart). - Linked chart data —
data/*.csv(the living source for a chart).
Vendored forks
Two upstream plugins are forked into third_party/ and wired via pubspec.yaml
(path dependency / dependency_overrides):
desktop_multi_window(MixinNetwork) — published 0.3.0 dropped the native window-geometry API. The fork adds macOSwindow_setFrame,window_coverScreen(borderless fill of a chosen screen), andwindow_close, exposed onWindowController. It also tracks the mouse for non-key windows (matched bymacos/Runner/MainFlutterWindow.swiftfor the main window): macOS only delivers mouse-moved events to the key window by default, and the borderless audience window deliberately never becomes key, so chart tooltips and hover states would otherwise never appear on the beamer. This is what makes the dual-screen audience window possible.screen_retriever_macos(leanflutter) — a packaging fix for recent Xcode/CocoaPods.
If you bump either upstream, re-apply the local changes (they're small and documented in the diff) and re-test the dual-screen presenter.
Localization
l10n/app_localizations.dart holds Dutch source strings (d('…')) and
translation maps for en/it/de/fr/es/fy/pap. A test enforces that every literal
.d('…') has a translation in every language — add new strings to all maps.