App-thema’s, meerschermen, annotaties en grafiekslides #1
8 changed files with 164 additions and 2 deletions
|
|
@ -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.':
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<List<String>> 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<String>.from(r)).toList(),
|
||||
);
|
||||
}
|
||||
|
|
@ -213,6 +220,7 @@ class Slide {
|
|||
bool? showLogo,
|
||||
bool? showFooter,
|
||||
bool? skipped,
|
||||
TlpLevel? tlp,
|
||||
List<List<String>>? 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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -354,6 +354,13 @@ class MarkdownService {
|
|||
buf.writeln('<!-- skip -->');
|
||||
}
|
||||
|
||||
// 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('<!-- tlp: ${slide.tlp.key} -->');
|
||||
}
|
||||
|
||||
if (slide.notes.isNotEmpty) {
|
||||
buf.writeln();
|
||||
buf.writeln('<!--');
|
||||
|
|
@ -597,6 +604,7 @@ class MarkdownService {
|
|||
final notesBuffer = StringBuffer();
|
||||
double advanceDuration = 0;
|
||||
bool skipped = false;
|
||||
TlpLevel slideTlp = TlpLevel.none;
|
||||
final bullets = <String>[];
|
||||
var bullets2 = <String>[];
|
||||
// bulletsImage slides store their panel width in `<!-- _style:
|
||||
|
|
@ -610,6 +618,8 @@ class MarkdownService {
|
|||
advanceDuration = double.tryParse(content.substring(8).trim()) ?? 0;
|
||||
} else if (content == 'skip') {
|
||||
skipped = true;
|
||||
} else if (content.startsWith('tlp:')) {
|
||||
slideTlp = TlpLevelX.fromKey(content.substring(4));
|
||||
} else if (content.startsWith('_style:')) {
|
||||
final w = RegExp(r'--image-width:\s*(\d+)%').firstMatch(content);
|
||||
if (w != null) styleImageWidth = int.tryParse(w.group(1)!) ?? 0;
|
||||
|
|
@ -636,6 +646,7 @@ class MarkdownService {
|
|||
notes: notes,
|
||||
advanceDuration: advanceDuration,
|
||||
skipped: skipped,
|
||||
tlp: slideTlp,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -820,6 +831,7 @@ class MarkdownService {
|
|||
showLogo: showLogo,
|
||||
showFooter: showFooter,
|
||||
skipped: skipped,
|
||||
tlp: slideTlp,
|
||||
tableRows: type == SlideType.table ? tableRows : const [],
|
||||
);
|
||||
}
|
||||
|
|
@ -832,6 +844,7 @@ class MarkdownService {
|
|||
required String notes,
|
||||
required double advanceDuration,
|
||||
required bool skipped,
|
||||
TlpLevel tlp = TlpLevel.none,
|
||||
}) {
|
||||
final lines = remaining.split('\n');
|
||||
String title = '';
|
||||
|
|
@ -892,6 +905,7 @@ class MarkdownService {
|
|||
showLogo: !classTokens.contains('no-logo'),
|
||||
showFooter: !classTokens.contains('no-footer'),
|
||||
skipped: skipped,
|
||||
tlp: tlp,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,11 @@ List<String> _imageUsages(WidgetRef ref, String absolutePath) {
|
|||
}
|
||||
|
||||
List<Slide> _slidesForPresentationOrExport(Deck deck) {
|
||||
final slides = deck.slides.where((s) => !s.skipped).toList();
|
||||
// Drop skipped slides and slides whose TLP classification is stricter than
|
||||
// the level chosen for this presentation/export.
|
||||
final slides = deck.slides
|
||||
.where((s) => !s.skipped && slideVisibleAtTlp(s, deck.tlp))
|
||||
.toList();
|
||||
final closingMarkdown = deck.themeProfile.closingSlideMarkdown.trim();
|
||||
if (deck.themeProfile.closingSlideEnabled && closingMarkdown.isNotEmpty) {
|
||||
slides.add(
|
||||
|
|
@ -947,7 +951,9 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
// zichtbare slide vertalen.
|
||||
final visible = <int>[
|
||||
for (var i = 0; i < deck.slides.length; i++)
|
||||
if (!deck.slides[i].skipped) i,
|
||||
if (!deck.slides[i].skipped &&
|
||||
slideVisibleAtTlp(deck.slides[i], deck.tlp))
|
||||
i,
|
||||
];
|
||||
final slides = _slidesForPresentationOrExport(deck);
|
||||
if (slides.isEmpty) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../models/deck.dart';
|
||||
import '../../models/settings.dart';
|
||||
import '../../models/slide.dart';
|
||||
import '../../services/image_service.dart';
|
||||
|
|
@ -126,6 +127,8 @@ class EditorPanel extends ConsumerWidget {
|
|||
const Divider(height: 1),
|
||||
_SlideTimingControl(slide: slide, onUpdate: update),
|
||||
const Divider(height: 1),
|
||||
_SlideTlpControl(slide: slide, onUpdate: update),
|
||||
const Divider(height: 1),
|
||||
_NotesField(slide: slide, onUpdate: update),
|
||||
],
|
||||
),
|
||||
|
|
@ -174,6 +177,7 @@ class EditorPanel extends ConsumerWidget {
|
|||
imageSize: slide.imageSize,
|
||||
showLogo: slide.showLogo,
|
||||
showFooter: slide.showFooter,
|
||||
tlp: slide.tlp,
|
||||
tableRows: newType == SlideType.table
|
||||
? (slide.tableRows.isNotEmpty
|
||||
? slide.tableRows
|
||||
|
|
@ -660,6 +664,57 @@ class _SlideFooterControl extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Per-slide TLP-classificatie ───────────────────────────────────────────────
|
||||
|
||||
class _SlideTlpControl extends StatelessWidget {
|
||||
final Slide slide;
|
||||
final ValueChanged<Slide> onUpdate;
|
||||
const _SlideTlpControl({required this.slide, required this.onUpdate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.shield_outlined, size: 14, color: Color(0xFF64748B)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.d('TLP van deze slide'),
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF475569)),
|
||||
),
|
||||
),
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton<TlpLevel>(
|
||||
value: slide.tlp,
|
||||
isDense: true,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF0F172A)),
|
||||
items: [
|
||||
for (final level in TlpLevel.values)
|
||||
DropdownMenuItem(
|
||||
value: level,
|
||||
child: Text(
|
||||
level == TlpLevel.none
|
||||
? l10n.d('Geen')
|
||||
: level.menuLabel,
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v != null) onUpdate(slide.copyWith(tlp: v));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Speakernotes veld ─────────────────────────────────────────────────────────
|
||||
|
||||
class _NotesField extends StatefulWidget {
|
||||
|
|
|
|||
|
|
@ -307,6 +307,32 @@ void main() {
|
|||
expect(normal.skipped, isFalse);
|
||||
});
|
||||
|
||||
test('keeps the per-slide TLP classification', () {
|
||||
final out = _roundTrip(
|
||||
Slide.create(
|
||||
SlideType.bullets,
|
||||
).copyWith(title: 'Gevoelig', bullets: ['Geheim'], tlp: TlpLevel.amber),
|
||||
);
|
||||
expect(out.tlp, TlpLevel.amber);
|
||||
|
||||
final none = _roundTrip(
|
||||
Slide.create(SlideType.bullets).copyWith(bullets: ['Open']),
|
||||
);
|
||||
expect(none.tlp, TlpLevel.none);
|
||||
});
|
||||
|
||||
test('keeps the per-slide TLP on a code slide', () {
|
||||
final out = _roundTrip(
|
||||
Slide.create(SlideType.code).copyWith(
|
||||
customMarkdown: 'secret_key = 42',
|
||||
codeLanguage: 'python',
|
||||
tlp: TlpLevel.red,
|
||||
),
|
||||
);
|
||||
expect(out.type, SlideType.code);
|
||||
expect(out.tlp, TlpLevel.red);
|
||||
});
|
||||
|
||||
test('keeps general presentation metadata in the front matter', () {
|
||||
final service = MarkdownService();
|
||||
final markdown = service.generateDeck(
|
||||
|
|
|
|||
|
|
@ -34,6 +34,42 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group('slideVisibleAtTlp', () {
|
||||
Slide slideAt(TlpLevel level) =>
|
||||
Slide.create(SlideType.bullets).copyWith(tlp: level);
|
||||
|
||||
test('an unclassified slide is always visible', () {
|
||||
for (final level in TlpLevel.values) {
|
||||
expect(slideVisibleAtTlp(slideAt(TlpLevel.none), level), isTrue);
|
||||
}
|
||||
});
|
||||
|
||||
test('a slide stricter than the presentation is withheld', () {
|
||||
// Presentation at GREEN: CLEAR/GREEN shown, AMBER/RED withheld.
|
||||
expect(slideVisibleAtTlp(slideAt(TlpLevel.clear), TlpLevel.green), isTrue);
|
||||
expect(slideVisibleAtTlp(slideAt(TlpLevel.green), TlpLevel.green), isTrue);
|
||||
expect(
|
||||
slideVisibleAtTlp(slideAt(TlpLevel.amber), TlpLevel.green),
|
||||
isFalse,
|
||||
);
|
||||
expect(slideVisibleAtTlp(slideAt(TlpLevel.red), TlpLevel.green), isFalse);
|
||||
});
|
||||
|
||||
test('a RED presentation shows every slide', () {
|
||||
for (final level in TlpLevel.values) {
|
||||
expect(slideVisibleAtTlp(slideAt(level), TlpLevel.red), isTrue);
|
||||
}
|
||||
});
|
||||
|
||||
test('an unset presentation only shows unclassified slides', () {
|
||||
expect(slideVisibleAtTlp(slideAt(TlpLevel.none), TlpLevel.none), isTrue);
|
||||
expect(
|
||||
slideVisibleAtTlp(slideAt(TlpLevel.clear), TlpLevel.none),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('TLP marking on slides', () {
|
||||
Widget host(TlpLevel tlp) => MaterialApp(
|
||||
home: Scaffold(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue