From d1862935aba8b058dce33bccb28fbd4528c10192 Mon Sep 17 00:00:00 2001 From: Brenno de Winter Date: Sat, 6 Jun 2026 22:34:42 +0200 Subject: [PATCH] Add per-slide TLP classification with sharing-level filtering Each slide can now carry its own Traffic Light Protocol level. When the presentation is shared at a given TLP level, slides classified stricter than that level are withheld, so the same deck can be shown safely to audiences with different clearances. - Slide.tlp field with markdown round-trip via a marker (also on code slides). - Editor: a per-slide "TLP van deze slide" dropdown. - Central rule slideVisibleAtTlp() compares levels on the TLP severity order (none < CLEAR < GREEN < AMBER < AMBER+STRICT < RED). - Filtering lives in _slidesForPresentationOrExport, the single source of slides for presenting (single-window and dual-screen) and for every export (PDF, PPTX, HTML), so all paths honour it. - Translations for the new strings in all supported languages, plus tests for the round-trip and the visibility rule. flutter analyze is clean and all tests pass. Co-Authored-By: Claude Opus 4.8 --- lib/l10n/app_localizations.dart | 7 ++++ lib/models/deck.dart | 9 +++++ lib/models/slide.dart | 9 +++++ lib/services/markdown_service.dart | 14 +++++++ lib/widgets/app_shell.dart | 10 ++++- lib/widgets/panels/editor_panel.dart | 55 ++++++++++++++++++++++++++++ test/markdown_round_trip_test.dart | 26 +++++++++++++ test/tlp_test.dart | 36 ++++++++++++++++++ 8 files changed, 164 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 5a4d81a..28d246a 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1174,6 +1174,7 @@ const _dutchSourceStrings = { 'Vrije Markdown': 'Markdown libero', 'Broncode': 'Codice sorgente', 'Programmeertaal': 'Linguaggio di programmazione', + 'TLP van deze slide': 'TLP di questa slide', 'Plak of typ hier je broncode...': 'Incolla o digita qui il tuo codice sorgente...', 'Overgeslagen': 'Saltata', @@ -1345,6 +1346,7 @@ const _dutchSourceStrings = { 'Vrije Markdown': 'Freies Markdown', 'Broncode': 'Quellcode', 'Programmeertaal': 'Programmiersprache', + 'TLP van deze slide': 'TLP dieser Folie', 'Plak of typ hier je broncode...': 'Quellcode hier einfügen oder eingeben...', 'Overgeslagen': 'Übersprungen', @@ -1517,6 +1519,7 @@ const _dutchSourceStrings = { 'Vrije Markdown': 'Markdown libre', 'Broncode': 'Code source', 'Programmeertaal': 'Langage de programmation', + 'TLP van deze slide': 'TLP de cette diapositive', 'Plak of typ hier je broncode...': 'Collez ou tapez votre code source ici...', 'Overgeslagen': 'Ignorée', @@ -1688,6 +1691,7 @@ const _dutchSourceStrings = { 'Vrije Markdown': 'Markdown libre', 'Broncode': 'Código fuente', 'Programmeertaal': 'Lenguaje de programación', + 'TLP van deze slide': 'TLP de esta diapositiva', 'Plak of typ hier je broncode...': 'Pega o escribe aquí tu código fuente...', 'Overgeslagen': 'Omitida', @@ -1860,6 +1864,7 @@ const _dutchSourceStrings = { 'Vrije Markdown': 'Frije Markdown', 'Broncode': 'Boarnekoade', 'Programmeertaal': 'Programmeartaal', + 'TLP van deze slide': 'TLP fan dizze slide', 'Plak of typ hier je broncode...': 'Plak of typ hjir dyn boarnekoade...', 'Overgeslagen': 'Oerslein', 'Kopiëren': 'Kopiearje', @@ -2032,6 +2037,7 @@ const _dutchSourceStrings = { 'Vrije Markdown': 'Markdown liber', 'Broncode': 'Código fuente', 'Programmeertaal': 'Lenguahe di programashon', + 'TLP van deze slide': 'TLP di e slide aki', 'Plak of typ hier je broncode...': 'Pega òf tek bo código fuente akinan...', 'Overgeslagen': 'Saltá', 'Kopiëren': 'Kopia', @@ -2193,6 +2199,7 @@ const _dutchSourceStringAdditions = { 'Bullet': 'Bullet', 'Plak of typ hier je broncode...': 'Paste or type your source code here...', 'Programmeertaal': 'Programming language', + 'TLP van deze slide': 'TLP of this slide', 'Platte tekst': 'Plain text', 'Titel (optioneel)': 'Title (optional)', 'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.': diff --git a/lib/models/deck.dart b/lib/models/deck.dart index e5e44ce..b61675d 100644 --- a/lib/models/deck.dart +++ b/lib/models/deck.dart @@ -2,8 +2,17 @@ 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 { diff --git a/lib/models/slide.dart b/lib/models/slide.dart index 909848a..dfd2b53 100644 --- a/lib/models/slide.dart +++ b/lib/models/slide.dart @@ -1,4 +1,5 @@ import 'package:uuid/uuid.dart'; +import 'deck.dart'; const _uuid = Uuid(); @@ -104,6 +105,10 @@ class Slide { final bool showLogo; // show the profile logo on this slide (default true) final bool showFooter; // show the profile footer on this slide (default true) final bool skipped; // skip this slide when presenting and exporting + /// Per-slide Traffic Light Protocol classification. The slide is withheld + /// when the presentation is shared at a lower (less restrictive) level than + /// this. [TlpLevel.none] = no per-slide restriction (always shown). + final TlpLevel tlp; final List> tableRows; // first row is the header const Slide({ @@ -132,6 +137,7 @@ class Slide { this.showLogo = true, this.showFooter = true, this.skipped = false, + this.tlp = TlpLevel.none, this.tableRows = const [], }); @@ -184,6 +190,7 @@ class Slide { showLogo: src.showLogo, showFooter: src.showFooter, skipped: src.skipped, + tlp: src.tlp, tableRows: src.tableRows.map((r) => List.from(r)).toList(), ); } @@ -213,6 +220,7 @@ class Slide { bool? showLogo, bool? showFooter, bool? skipped, + TlpLevel? tlp, List>? tableRows, }) { return Slide( @@ -241,6 +249,7 @@ class Slide { showLogo: showLogo ?? this.showLogo, showFooter: showFooter ?? this.showFooter, skipped: skipped ?? this.skipped, + tlp: tlp ?? this.tlp, tableRows: tableRows ?? this.tableRows, ); } diff --git a/lib/services/markdown_service.dart b/lib/services/markdown_service.dart index 4b91eba..81a5d16 100644 --- a/lib/services/markdown_service.dart +++ b/lib/services/markdown_service.dart @@ -354,6 +354,13 @@ class MarkdownService { buf.writeln(''); } + // Per-slide TLP classification (used to withhold the slide when sharing at + // a lower level). Persisted so it survives save/load round-trips. + if (slide.tlp != TlpLevel.none) { + buf.writeln(); + buf.writeln(''); + } + if (slide.notes.isNotEmpty) { buf.writeln(); buf.writeln('