2026-06-02 23:28:39 +02:00
|
|
|
import 'package:uuid/uuid.dart';
|
2026-06-06 22:34:42 +02:00
|
|
|
import 'deck.dart';
|
2026-06-02 23:28:39 +02:00
|
|
|
|
|
|
|
|
const _uuid = Uuid();
|
|
|
|
|
|
|
|
|
|
enum SlideType {
|
|
|
|
|
title,
|
|
|
|
|
section,
|
|
|
|
|
bullets,
|
|
|
|
|
twoBullets,
|
|
|
|
|
bulletsImage,
|
|
|
|
|
twoImages,
|
|
|
|
|
image,
|
|
|
|
|
video,
|
|
|
|
|
quote,
|
|
|
|
|
table,
|
|
|
|
|
freeMarkdown,
|
2026-06-06 20:41:24 +02:00
|
|
|
code,
|
2026-06-07 11:42:44 +02:00
|
|
|
chart,
|
2026-06-02 23:28:39 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-09 13:28:23 +02:00
|
|
|
enum ListStyle { bullets, numbered, checklist }
|
|
|
|
|
|
|
|
|
|
int bulletLevel(String value) {
|
|
|
|
|
var level = 0;
|
|
|
|
|
while (level < value.length && value[level] == '\t') {
|
|
|
|
|
level++;
|
|
|
|
|
}
|
|
|
|
|
return level;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String bulletText(String value) => value.substring(bulletLevel(value));
|
|
|
|
|
|
|
|
|
|
bool checklistItemChecked(String value) =>
|
|
|
|
|
RegExp(r'^\[[xX]\]\s*').hasMatch(bulletText(value));
|
|
|
|
|
|
|
|
|
|
String checklistItemText(String value) =>
|
|
|
|
|
bulletText(value).replaceFirst(RegExp(r'^\[[ xX]\]\s*'), '');
|
|
|
|
|
|
|
|
|
|
String checklistBullet({
|
|
|
|
|
required int level,
|
|
|
|
|
required String text,
|
|
|
|
|
required bool checked,
|
|
|
|
|
}) => '${'\t' * level}[${checked ? 'x' : ' '}] $text';
|
|
|
|
|
|
2026-06-02 23:28:39 +02:00
|
|
|
extension SlideTypeExtension on SlideType {
|
|
|
|
|
String get label {
|
|
|
|
|
switch (this) {
|
|
|
|
|
case SlideType.title:
|
|
|
|
|
return 'Titelpagina';
|
|
|
|
|
case SlideType.section:
|
|
|
|
|
return 'Tussentitel';
|
|
|
|
|
case SlideType.bullets:
|
|
|
|
|
return 'Alleen Bullets';
|
|
|
|
|
case SlideType.twoBullets:
|
|
|
|
|
return 'Twee Bulletkolommen';
|
|
|
|
|
case SlideType.bulletsImage:
|
|
|
|
|
return 'Bullets + Afbeelding';
|
|
|
|
|
case SlideType.twoImages:
|
|
|
|
|
return 'Twee Afbeeldingen';
|
|
|
|
|
case SlideType.image:
|
|
|
|
|
return 'Grote Afbeelding';
|
|
|
|
|
case SlideType.video:
|
|
|
|
|
return 'Video';
|
|
|
|
|
case SlideType.quote:
|
|
|
|
|
return 'Quote';
|
|
|
|
|
case SlideType.table:
|
|
|
|
|
return 'Tabel';
|
|
|
|
|
case SlideType.freeMarkdown:
|
|
|
|
|
return 'Vrije Markdown';
|
2026-06-06 20:41:24 +02:00
|
|
|
case SlideType.code:
|
|
|
|
|
return 'Broncode';
|
2026-06-07 11:42:44 +02:00
|
|
|
case SlideType.chart:
|
|
|
|
|
return 'Grafiek';
|
2026-06-02 23:28:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String get marpClass {
|
|
|
|
|
switch (this) {
|
|
|
|
|
case SlideType.title:
|
|
|
|
|
return 'title';
|
|
|
|
|
case SlideType.section:
|
|
|
|
|
return 'section';
|
|
|
|
|
case SlideType.bullets:
|
|
|
|
|
return '';
|
|
|
|
|
case SlideType.twoBullets:
|
|
|
|
|
return 'two-bullets';
|
|
|
|
|
case SlideType.bulletsImage:
|
|
|
|
|
return 'split';
|
|
|
|
|
case SlideType.twoImages:
|
|
|
|
|
return '';
|
|
|
|
|
case SlideType.image:
|
|
|
|
|
return '';
|
|
|
|
|
case SlideType.video:
|
|
|
|
|
return 'video';
|
|
|
|
|
case SlideType.quote:
|
|
|
|
|
return 'quote';
|
|
|
|
|
case SlideType.table:
|
|
|
|
|
return 'table';
|
|
|
|
|
case SlideType.freeMarkdown:
|
|
|
|
|
return '';
|
2026-06-06 20:41:24 +02:00
|
|
|
case SlideType.code:
|
|
|
|
|
return 'code';
|
2026-06-07 11:42:44 +02:00
|
|
|
case SlideType.chart:
|
|
|
|
|
return 'chart';
|
2026-06-02 23:28:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Slide {
|
|
|
|
|
final String id;
|
|
|
|
|
final SlideType type;
|
|
|
|
|
final String title;
|
|
|
|
|
final String subtitle;
|
|
|
|
|
final List<String> bullets;
|
|
|
|
|
final List<String> bullets2;
|
2026-06-09 13:28:23 +02:00
|
|
|
final ListStyle listStyle;
|
|
|
|
|
final bool showChecklistProgress;
|
2026-06-08 21:48:06 +02:00
|
|
|
|
|
|
|
|
/// Optional headings above the two bullet columns (twoBullets only). Empty =
|
|
|
|
|
/// no heading for that column.
|
|
|
|
|
final String columnTitle1;
|
|
|
|
|
final String columnTitle2;
|
2026-06-02 23:28:39 +02:00
|
|
|
final String imagePath;
|
|
|
|
|
final String imagePath2;
|
|
|
|
|
final String imageCaption;
|
|
|
|
|
final String imageCaption2;
|
|
|
|
|
final String videoPath;
|
|
|
|
|
final bool videoAutoplay;
|
|
|
|
|
final String audioPath;
|
|
|
|
|
final bool audioAutoplay;
|
|
|
|
|
final String quote;
|
|
|
|
|
final String quoteAuthor;
|
|
|
|
|
final String customMarkdown;
|
Add dual-screen presenter mode (slide on beamer, notes on laptop)
When a second display is connected (macOS), presenting now opens a
borderless audience window on the beamer showing the slide, while the
main window shows the presenter view (current/next slide, speaker notes,
clock, controls) on the laptop. The two windows stay in sync over method
channels: navigation, blank screen, audio-complete and beamer clicks are
forwarded between them, and media plays only on the beamer to avoid
double audio. Falls back to the existing single-window presenter when
there is one display or the second window can't be created.
- Vendors a fork of desktop_multi_window in third_party/ that re-adds the
native macOS window geometry/fullscreen calls (coverScreen, setFrame,
close) the published 0.3.0 dropped; wired via a path dependency.
- Registers the app's plugins for sub-windows in MainFlutterWindow so
video/image rendering works on the beamer.
- Routes the multi_window dart entrypoint to a minimal AudienceWindowApp.
Compiles (flutter analyze + macOS debug build) and all tests pass;
runtime two-screen behaviour still needs verification on real hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:25:34 +02:00
|
|
|
final String
|
|
|
|
|
codeLanguage; // highlight.js language id for code slides ('' = plain)
|
2026-06-02 23:28:39 +02:00
|
|
|
final String cssClass;
|
|
|
|
|
final String notes;
|
|
|
|
|
final double advanceDuration; // 0 = no auto-advance
|
|
|
|
|
final int imageSize; // 0 = auto; image: bg %, bulletsImage: right panel %
|
|
|
|
|
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
|
2026-06-06 22:34:42 +02:00
|
|
|
/// 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;
|
2026-06-02 23:28:39 +02:00
|
|
|
final List<List<String>> tableRows; // first row is the header
|
|
|
|
|
|
|
|
|
|
const Slide({
|
|
|
|
|
required this.id,
|
|
|
|
|
required this.type,
|
|
|
|
|
this.title = '',
|
|
|
|
|
this.subtitle = '',
|
|
|
|
|
this.bullets = const [],
|
|
|
|
|
this.bullets2 = const [],
|
2026-06-09 13:28:23 +02:00
|
|
|
this.listStyle = ListStyle.bullets,
|
|
|
|
|
this.showChecklistProgress = false,
|
2026-06-08 21:48:06 +02:00
|
|
|
this.columnTitle1 = '',
|
|
|
|
|
this.columnTitle2 = '',
|
2026-06-02 23:28:39 +02:00
|
|
|
this.imagePath = '',
|
|
|
|
|
this.imagePath2 = '',
|
|
|
|
|
this.imageCaption = '',
|
|
|
|
|
this.imageCaption2 = '',
|
|
|
|
|
this.videoPath = '',
|
|
|
|
|
this.videoAutoplay = false,
|
|
|
|
|
this.audioPath = '',
|
|
|
|
|
this.audioAutoplay = false,
|
|
|
|
|
this.quote = '',
|
|
|
|
|
this.quoteAuthor = '',
|
|
|
|
|
this.customMarkdown = '',
|
2026-06-06 20:41:24 +02:00
|
|
|
this.codeLanguage = '',
|
2026-06-02 23:28:39 +02:00
|
|
|
this.cssClass = '',
|
|
|
|
|
this.notes = '',
|
|
|
|
|
this.advanceDuration = 0,
|
|
|
|
|
this.imageSize = 0,
|
|
|
|
|
this.showLogo = true,
|
|
|
|
|
this.showFooter = true,
|
|
|
|
|
this.skipped = false,
|
2026-06-06 22:34:42 +02:00
|
|
|
this.tlp = TlpLevel.none,
|
2026-06-02 23:28:39 +02:00
|
|
|
this.tableRows = const [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
factory Slide.create(SlideType type) {
|
|
|
|
|
return Slide(
|
|
|
|
|
id: _uuid.v4(),
|
|
|
|
|
type: type,
|
|
|
|
|
bullets:
|
|
|
|
|
(type == SlideType.bullets ||
|
|
|
|
|
type == SlideType.twoBullets ||
|
|
|
|
|
type == SlideType.bulletsImage)
|
|
|
|
|
? const ['']
|
|
|
|
|
: const [],
|
|
|
|
|
bullets2: type == SlideType.twoBullets ? const [''] : const [],
|
|
|
|
|
tableRows: type == SlideType.table
|
|
|
|
|
? const [
|
|
|
|
|
// Lege koppen: de editor toont 'Kolom 1' etc. als hint, zodat de
|
|
|
|
|
// gebruiker niets hoeft te verwijderen voordat hij begint.
|
|
|
|
|
['', ''],
|
|
|
|
|
['', ''],
|
|
|
|
|
]
|
|
|
|
|
: const [],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
factory Slide.duplicate(Slide src) {
|
|
|
|
|
return Slide(
|
|
|
|
|
id: _uuid.v4(),
|
|
|
|
|
type: src.type,
|
|
|
|
|
title: src.title,
|
|
|
|
|
subtitle: src.subtitle,
|
|
|
|
|
bullets: List<String>.from(src.bullets),
|
|
|
|
|
bullets2: List<String>.from(src.bullets2),
|
2026-06-09 13:28:23 +02:00
|
|
|
listStyle: src.listStyle,
|
|
|
|
|
showChecklistProgress: src.showChecklistProgress,
|
2026-06-08 21:48:06 +02:00
|
|
|
columnTitle1: src.columnTitle1,
|
|
|
|
|
columnTitle2: src.columnTitle2,
|
2026-06-02 23:28:39 +02:00
|
|
|
imagePath: src.imagePath,
|
|
|
|
|
imagePath2: src.imagePath2,
|
|
|
|
|
imageCaption: src.imageCaption,
|
|
|
|
|
imageCaption2: src.imageCaption2,
|
|
|
|
|
videoPath: src.videoPath,
|
|
|
|
|
videoAutoplay: src.videoAutoplay,
|
|
|
|
|
audioPath: src.audioPath,
|
|
|
|
|
audioAutoplay: src.audioAutoplay,
|
|
|
|
|
quote: src.quote,
|
|
|
|
|
quoteAuthor: src.quoteAuthor,
|
|
|
|
|
customMarkdown: src.customMarkdown,
|
2026-06-06 20:41:24 +02:00
|
|
|
codeLanguage: src.codeLanguage,
|
2026-06-02 23:28:39 +02:00
|
|
|
cssClass: src.cssClass,
|
|
|
|
|
notes: src.notes,
|
|
|
|
|
advanceDuration: src.advanceDuration,
|
|
|
|
|
imageSize: src.imageSize,
|
|
|
|
|
showLogo: src.showLogo,
|
|
|
|
|
showFooter: src.showFooter,
|
|
|
|
|
skipped: src.skipped,
|
2026-06-06 22:34:42 +02:00
|
|
|
tlp: src.tlp,
|
2026-06-02 23:28:39 +02:00
|
|
|
tableRows: src.tableRows.map((r) => List<String>.from(r)).toList(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Slide copyWith({
|
|
|
|
|
SlideType? type,
|
|
|
|
|
String? title,
|
|
|
|
|
String? subtitle,
|
|
|
|
|
List<String>? bullets,
|
|
|
|
|
List<String>? bullets2,
|
2026-06-09 13:28:23 +02:00
|
|
|
ListStyle? listStyle,
|
|
|
|
|
bool? showChecklistProgress,
|
2026-06-08 21:48:06 +02:00
|
|
|
String? columnTitle1,
|
|
|
|
|
String? columnTitle2,
|
2026-06-02 23:28:39 +02:00
|
|
|
String? imagePath,
|
|
|
|
|
String? imagePath2,
|
|
|
|
|
String? imageCaption,
|
|
|
|
|
String? imageCaption2,
|
|
|
|
|
String? videoPath,
|
|
|
|
|
bool? videoAutoplay,
|
|
|
|
|
String? audioPath,
|
|
|
|
|
bool? audioAutoplay,
|
|
|
|
|
String? quote,
|
|
|
|
|
String? quoteAuthor,
|
|
|
|
|
String? customMarkdown,
|
2026-06-06 20:41:24 +02:00
|
|
|
String? codeLanguage,
|
2026-06-02 23:28:39 +02:00
|
|
|
String? cssClass,
|
|
|
|
|
String? notes,
|
|
|
|
|
double? advanceDuration,
|
|
|
|
|
int? imageSize,
|
|
|
|
|
bool? showLogo,
|
|
|
|
|
bool? showFooter,
|
|
|
|
|
bool? skipped,
|
2026-06-06 22:34:42 +02:00
|
|
|
TlpLevel? tlp,
|
2026-06-02 23:28:39 +02:00
|
|
|
List<List<String>>? tableRows,
|
|
|
|
|
}) {
|
|
|
|
|
return Slide(
|
|
|
|
|
id: id,
|
|
|
|
|
type: type ?? this.type,
|
|
|
|
|
title: title ?? this.title,
|
|
|
|
|
subtitle: subtitle ?? this.subtitle,
|
|
|
|
|
bullets: bullets ?? this.bullets,
|
|
|
|
|
bullets2: bullets2 ?? this.bullets2,
|
2026-06-09 13:28:23 +02:00
|
|
|
listStyle: listStyle ?? this.listStyle,
|
|
|
|
|
showChecklistProgress:
|
|
|
|
|
showChecklistProgress ?? this.showChecklistProgress,
|
2026-06-08 21:48:06 +02:00
|
|
|
columnTitle1: columnTitle1 ?? this.columnTitle1,
|
|
|
|
|
columnTitle2: columnTitle2 ?? this.columnTitle2,
|
2026-06-02 23:28:39 +02:00
|
|
|
imagePath: imagePath ?? this.imagePath,
|
|
|
|
|
imagePath2: imagePath2 ?? this.imagePath2,
|
|
|
|
|
imageCaption: imageCaption ?? this.imageCaption,
|
|
|
|
|
imageCaption2: imageCaption2 ?? this.imageCaption2,
|
|
|
|
|
videoPath: videoPath ?? this.videoPath,
|
|
|
|
|
videoAutoplay: videoAutoplay ?? this.videoAutoplay,
|
|
|
|
|
audioPath: audioPath ?? this.audioPath,
|
|
|
|
|
audioAutoplay: audioAutoplay ?? this.audioAutoplay,
|
|
|
|
|
quote: quote ?? this.quote,
|
|
|
|
|
quoteAuthor: quoteAuthor ?? this.quoteAuthor,
|
|
|
|
|
customMarkdown: customMarkdown ?? this.customMarkdown,
|
2026-06-06 20:41:24 +02:00
|
|
|
codeLanguage: codeLanguage ?? this.codeLanguage,
|
2026-06-02 23:28:39 +02:00
|
|
|
cssClass: cssClass ?? this.cssClass,
|
|
|
|
|
notes: notes ?? this.notes,
|
|
|
|
|
advanceDuration: advanceDuration ?? this.advanceDuration,
|
|
|
|
|
imageSize: imageSize ?? this.imageSize,
|
|
|
|
|
showLogo: showLogo ?? this.showLogo,
|
|
|
|
|
showFooter: showFooter ?? this.showFooter,
|
|
|
|
|
skipped: skipped ?? this.skipped,
|
2026-06-06 22:34:42 +02:00
|
|
|
tlp: tlp ?? this.tlp,
|
2026-06-02 23:28:39 +02:00
|
|
|
tableRows: tableRows ?? this.tableRows,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|