Ocideck/lib/models/deck.dart
Brenno de Winter 227abf351e Add annotation layer (laser, pen, highlighter) over slides
Draw on slides while presenting, kept as a layer fully separate from the
Marp content so the deck stays pure, portable Marp.

Presenter tools (D pen, T highlighter, E eraser, X laser, C clear, Esc
puts the tool away) with a small floating colour/tool bar shown only when
a tool is active. Strokes use coordinates normalized to the 16:9 slide so
they render identically on the laptop and the beamer; in dual-screen mode
the ink and the laser are mirrored live to the audience window.

Persistence (decoupled from the .md):
- In memory the layer is keyed by Slide.id (stable within a session).
- On disk it lives in a sidecar <name>.ink.json next to the deck and as a
  separate entry inside the .ocideck package; the markdown is untouched.
- Because slide ids are regenerated on load, the sidecar anchors strokes
  by order + a content fingerprint, re-attaching them after reordering and
  dropping them when a slide's content changed.
- Deck.annotations carries the layer in memory but is never serialized to
  markdown; deckProvider.setAnnotations keeps it out of undo/redo.

flutter analyze is clean, all tests pass (incl. new stroke/codec tests),
and the macOS debug build compiles. Drawing and live beamer sync still
need verification on real hardware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:14:51 +02:00

168 lines
4.7 KiB
Dart

import 'annotation.dart';
import 'slide.dart';
import 'settings.dart';
/// Traffic Light Protocol-classificatie (FIRST TLP 2.0) van een presentatie.
///
/// De volgorde loopt van minst naar meest beperkend; [TlpLevel.index] is dus
/// bruikbaar om niveaus te vergelijken.
enum TlpLevel { none, clear, green, amber, amberStrict, red }
/// Of [slide] getoond mag worden wanneer de presentatie op [presentationTlp]
/// wordt gedeeld. Een slide wordt achtergehouden zodra zijn eigen TLP-niveau
/// strenger (hoger) is dan het voor de presentatie gekozen niveau.
bool slideVisibleAtTlp(Slide slide, TlpLevel presentationTlp) =>
slide.tlp.index <= presentationTlp.index;
extension TlpLevelX on TlpLevel {
/// De officiële markering die op de slides verschijnt ('' bij [none]).
String get label {
switch (this) {
case TlpLevel.none:
return '';
case TlpLevel.clear:
return 'TLP:CLEAR';
case TlpLevel.green:
return 'TLP:GREEN';
case TlpLevel.amber:
return 'TLP:AMBER';
case TlpLevel.amberStrict:
return 'TLP:AMBER+STRICT';
case TlpLevel.red:
return 'TLP:RED';
}
}
/// Tekst voor de keuzelijst.
String get menuLabel => this == TlpLevel.none ? 'Geen' : label;
/// Stabiele sleutel voor opslag in de front matter.
String get key {
switch (this) {
case TlpLevel.none:
return 'none';
case TlpLevel.clear:
return 'clear';
case TlpLevel.green:
return 'green';
case TlpLevel.amber:
return 'amber';
case TlpLevel.amberStrict:
return 'amber+strict';
case TlpLevel.red:
return 'red';
}
}
/// Officiële TLP 2.0-voorgrondkleur (ARGB). Achtergrond is altijd zwart.
int get foreground {
switch (this) {
case TlpLevel.none:
return 0x00000000;
case TlpLevel.clear:
return 0xFFFFFFFF;
case TlpLevel.green:
return 0xFF33FF00;
case TlpLevel.amber:
case TlpLevel.amberStrict:
return 0xFFFFC000;
case TlpLevel.red:
return 0xFFFF2B2B;
}
}
static TlpLevel fromKey(String raw) {
switch (raw.trim().toLowerCase()) {
case 'clear':
return TlpLevel.clear;
case 'green':
return TlpLevel.green;
case 'amber':
return TlpLevel.amber;
case 'amber+strict':
case 'amberstrict':
return TlpLevel.amberStrict;
case 'red':
return TlpLevel.red;
default:
return TlpLevel.none;
}
}
}
class Deck {
final String title;
final String theme;
final bool paginate;
final List<Slide> slides;
final String? projectPath;
final ThemeProfile themeProfile;
// ── General presentation metadata (stored in the markdown front matter) ──
final String author;
final String organization;
final String version;
final String date;
final String description;
final String keywords;
/// Traffic Light Protocol-classificatie van deze presentatie.
final TlpLevel tlp;
/// Annotatielaag: vrije-hand-tekeningen per slide, gekeyd op [Slide.id].
/// Bewust géén onderdeel van de Marp-markdown — dit wordt los bewaard in een
/// sidecar zodat het deck pure, uitwisselbare Marp blijft.
final Map<String, List<InkStroke>> annotations;
const Deck({
required this.title,
this.theme = 'ocideck',
this.paginate = true,
this.slides = const [],
this.projectPath,
this.themeProfile = const ThemeProfile(),
this.author = '',
this.organization = '',
this.version = '',
this.date = '',
this.description = '',
this.keywords = '',
this.tlp = TlpLevel.none,
this.annotations = const {},
});
Deck copyWith({
String? title,
String? theme,
bool? paginate,
List<Slide>? slides,
String? projectPath,
ThemeProfile? themeProfile,
bool clearProjectPath = false,
String? author,
String? organization,
String? version,
String? date,
String? description,
String? keywords,
TlpLevel? tlp,
Map<String, List<InkStroke>>? annotations,
}) {
return Deck(
title: title ?? this.title,
theme: theme ?? this.theme,
paginate: paginate ?? this.paginate,
slides: slides ?? this.slides,
projectPath: clearProjectPath ? null : (projectPath ?? this.projectPath),
themeProfile: themeProfile ?? this.themeProfile,
author: author ?? this.author,
organization: organization ?? this.organization,
version: version ?? this.version,
date: date ?? this.date,
description: description ?? this.description,
keywords: keywords ?? this.keywords,
tlp: tlp ?? this.tlp,
annotations: annotations ?? this.annotations,
);
}
}