Documentation & licensing: - Add the EUPL-1.2 licence (LICENSE.md) and set the project licence; refresh the README (name origin wink, updated feature list, documentation index). - Add CONTRIBUTING, SECURITY, CODE_OF_CONDUCT, CHANGELOG, AUTHORS, and THIRD_PARTY_NOTICES, plus docs/ (ARCHITECTURE, BUILD, USER_GUIDE, SHORTCUTS, LICENSE_COMPLIANCE) and .github/ (CI workflow, issue/PR templates). - Bring docs/FILE_FORMAT.md in line with current behaviour (code & chart slides, per-slide TLP comment, annotation .ink.json sidecar, chart data/ CSVs). Open-source compliance: - Add tool/check_licenses.dart and a `make licenses` target (wired into check-full and CI) that verifies every resolved dependency uses a recognised open-source licence. A scan of all 151 packages and bundled assets found only OSI-approved licences. Charts (Fase 1.1): - Replace the chart CSV textarea with an in-app editable data grid (editable series/labels/values, add/remove row & column, read-only when linked). - Centralize the linked-CSV directory name (`data/`) in a shared constant. Also normalize formatting repo-wide with `dart format` and fix one curly-braces lint, so `make check` and CI are green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.2 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,
# 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
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, three kinds of data live beside it (see
FILE_FORMAT.md §6):
- Captions —
.ocideck_captions.json(per image, inimages/). - 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. 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.