Compare commits

..

No commits in common. "815f5f2ceeacc9a7415a266fb687ffcf19f35527" and "839474fa19ec43ae1a3e1b06b3c5112964f1f3f5" have entirely different histories.

27 changed files with 363 additions and 2682 deletions

View file

@ -47,8 +47,6 @@ and the project aims to follow [Semantic Versioning](https://semver.org/).
separate from the slide title. separate from the slide title.
- Slide text auto-sizing now measures with the deck's own font, so text grows to - Slide text auto-sizing now measures with the deck's own font, so text grows to
use the available space more accurately instead of staying smaller than needed. use the available space more accurately instead of staying smaller than needed.
- The two bullet columns now scale **independently**, so a column with few items
is no longer shrunk down to the size of a crowded one beside it.
- Slide transitions in the presenter no longer flash a black frame (neighbour - Slide transitions in the presenter no longer flash a black frame (neighbour
images are precached and `gaplessPlayback` is enabled) — important for images are precached and `gaplessPlayback` is enabled) — important for
recording. recording.

View file

@ -2327,35 +2327,6 @@ const _dutchSourceStrings = {
const _dutchSourceStringAdditions = { const _dutchSourceStringAdditions = {
'en': { 'en': {
'Annuleren': 'Cancel', 'Annuleren': 'Cancel',
'Checklist': 'Task checklist',
'Voortgangsgrafiek tonen': 'Show progress chart',
'Toont afgevinkt en niet afgevinkt als percentages.':
'Shows checked and unchecked items as percentages.',
'Afgevinkt': 'Checked',
'Niet afgevinkt': 'Unchecked',
'Er zijn geen aangevinkte checklist-items om te legen.':
'There are no checked checklist items to clear.',
'Alle checkboxen legen?': 'Clear all checkboxes?',
'Hiermee worden alle': 'This will uncheck all',
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
'checked checklist items in the entire presentation. You can undo this with Ctrl/Cmd+Z.',
'Alles legen': 'Clear all',
'checklist-items uitgevinkt.': 'checklist items unchecked.',
'Alle checkboxen legen': 'Clear all checkboxes',
'Afgevinkte tekst doorhalen': 'Strike through checked text',
'Toont een streep door voltooide checklistitems.':
'Shows completed checklist items with a strike-through.',
'Na media automatisch doorgaan': 'Advance automatically after media',
'Opsomming': 'Bullets',
'Nummering': 'Numbering',
'Varianten': 'Variants',
'Grafiekvarianten maken': 'Create chart variants',
'Slides toevoegen': 'Add slides',
'Omhoog': 'Move up',
'Omlaag': 'Move down',
'Niet toevoegen': 'Do not add',
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
'These slides use the same data, colors, and title. Use the arrows to choose their order after the current slide.',
'Afbeelding': 'Image', 'Afbeelding': 'Image',
'Broncode': 'Source code', 'Broncode': 'Source code',
'Bullet': 'Bullet', 'Bullet': 'Bullet',
@ -2435,36 +2406,6 @@ const _dutchSourceStringAdditions = {
}, },
'it': { 'it': {
'Annuleren': 'Annulla', 'Annuleren': 'Annulla',
'Checklist': 'Lista di controllo',
'Voortgangsgrafiek tonen': 'Mostra grafico di avanzamento',
'Toont afgevinkt en niet afgevinkt als percentages.':
'Mostra gli elementi selezionati e non selezionati in percentuale.',
'Afgevinkt': 'Selezionati',
'Niet afgevinkt': 'Non selezionati',
'Er zijn geen aangevinkte checklist-items om te legen.':
'Non ci sono elementi della lista selezionati da azzerare.',
'Alle checkboxen legen?': 'Azzerare tutte le caselle?',
'Hiermee worden alle': 'Verranno deselezionati tutti i',
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
'elementi selezionati della lista in tutta la presentazione. Puoi annullare con Ctrl/Cmd+Z.',
'Alles legen': 'Azzera tutto',
'checklist-items uitgevinkt.': 'elementi della lista deselezionati.',
'Alle checkboxen legen': 'Azzera tutte le caselle',
'Afgevinkte tekst doorhalen': 'Barra il testo selezionato',
'Toont een streep door voltooide checklistitems.':
'Mostra gli elementi completati con il testo barrato.',
'Na media automatisch doorgaan':
'Avanza automaticamente dopo i contenuti multimediali',
'Opsomming': 'Elenco puntato',
'Nummering': 'Numerazione',
'Varianten': 'Varianti',
'Grafiekvarianten maken': 'Crea varianti del grafico',
'Slides toevoegen': 'Aggiungi diapositive',
'Omhoog': 'Sposta su',
'Omlaag': 'Sposta giù',
'Niet toevoegen': 'Non aggiungere',
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
'Queste diapositive usano gli stessi dati, colori e titolo. Usa le frecce per scegliere lordine dopo la diapositiva corrente.',
'Spider': 'Radar', 'Spider': 'Radar',
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.': 'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
'Un grafico radar richiede almeno tre etichette (assi); ogni serie forma una superficie.', 'Un grafico radar richiede almeno tre etichette (assi); ogni serie forma una superficie.',
@ -2697,35 +2638,6 @@ const _dutchSourceStringAdditions = {
}, },
'de': { 'de': {
'Annuleren': 'Abbrechen', 'Annuleren': 'Abbrechen',
'Checklist': 'Checkliste',
'Voortgangsgrafiek tonen': 'Fortschrittsdiagramm anzeigen',
'Toont afgevinkt en niet afgevinkt als percentages.':
'Zeigt erledigte und offene Einträge als Prozentwerte.',
'Afgevinkt': 'Erledigt',
'Niet afgevinkt': 'Offen',
'Er zijn geen aangevinkte checklist-items om te legen.':
'Es gibt keine angehakten Checklisten-Einträge zum Zurücksetzen.',
'Alle checkboxen legen?': 'Alle Kontrollkästchen leeren?',
'Hiermee worden alle': 'Damit werden die Häkchen von allen',
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
'angehakten Checklisten-Einträgen in der gesamten Präsentation entfernt. Du kannst dies mit Strg/Cmd+Z rückgängig machen.',
'Alles legen': 'Alle leeren',
'checklist-items uitgevinkt.': 'Checklisten-Einträge zurückgesetzt.',
'Alle checkboxen legen': 'Alle Kontrollkästchen leeren',
'Afgevinkte tekst doorhalen': 'Erledigten Text durchstreichen',
'Toont een streep door voltooide checklistitems.':
'Zeigt erledigte Checklistenpunkte durchgestrichen an.',
'Na media automatisch doorgaan': 'Nach Medienwiedergabe automatisch weiter',
'Opsomming': 'Aufzählung',
'Nummering': 'Nummerierung',
'Varianten': 'Varianten',
'Grafiekvarianten maken': 'Diagrammvarianten erstellen',
'Slides toevoegen': 'Folien hinzufügen',
'Omhoog': 'Nach oben',
'Omlaag': 'Nach unten',
'Niet toevoegen': 'Nicht hinzufügen',
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
'Diese Folien verwenden dieselben Daten, Farben und denselben Titel. Lege mit den Pfeilen die Reihenfolge nach der aktuellen Folie fest.',
'Spider': 'Netz', 'Spider': 'Netz',
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.': 'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
'Ein Netzdiagramm braucht mindestens drei Beschriftungen (Achsen); jede Reihe bildet eine Fläche.', 'Ein Netzdiagramm braucht mindestens drei Beschriftungen (Achsen); jede Reihe bildet eine Fläche.',
@ -2959,35 +2871,6 @@ const _dutchSourceStringAdditions = {
}, },
'fr': { 'fr': {
'Annuleren': 'Annuler', 'Annuleren': 'Annuler',
'Checklist': 'Liste de contrôle',
'Voortgangsgrafiek tonen': 'Afficher le graphique de progression',
'Toont afgevinkt en niet afgevinkt als percentages.':
'Affiche les éléments cochés et non cochés en pourcentage.',
'Afgevinkt': 'Coché',
'Niet afgevinkt': 'Non coché',
'Er zijn geen aangevinkte checklist-items om te legen.':
'Aucun élément de liste coché à effacer.',
'Alle checkboxen legen?': 'Effacer toutes les cases à cocher ?',
'Hiermee worden alle': 'Cela décochera tous les',
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
'éléments de liste cochés dans toute la présentation. Vous pouvez annuler avec Ctrl/Cmd+Z.',
'Alles legen': 'Tout effacer',
'checklist-items uitgevinkt.': 'éléments de liste décochés.',
'Alle checkboxen legen': 'Effacer toutes les cases à cocher',
'Afgevinkte tekst doorhalen': 'Barrer le texte coché',
'Toont een streep door voltooide checklistitems.':
'Affiche les éléments terminés avec un texte barré.',
'Na media automatisch doorgaan': 'Avancer automatiquement après le média',
'Opsomming': 'Liste à puces',
'Nummering': 'Numérotation',
'Varianten': 'Variantes',
'Grafiekvarianten maken': 'Créer des variantes du graphique',
'Slides toevoegen': 'Ajouter les diapositives',
'Omhoog': 'Monter',
'Omlaag': 'Descendre',
'Niet toevoegen': 'Ne pas ajouter',
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
'Ces diapositives utilisent les mêmes données, couleurs et titre. Utilisez les flèches pour choisir leur ordre après la diapositive actuelle.',
'Spider': 'Radar', 'Spider': 'Radar',
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.': 'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
'Un graphique radar nécessite au moins trois étiquettes (axes); chaque série forme une surface.', 'Un graphique radar nécessite au moins trois étiquettes (axes); chaque série forme une surface.',
@ -3221,36 +3104,6 @@ const _dutchSourceStringAdditions = {
}, },
'es': { 'es': {
'Annuleren': 'Cancelar', 'Annuleren': 'Cancelar',
'Checklist': 'Lista de verificación',
'Voortgangsgrafiek tonen': 'Mostrar gráfico de progreso',
'Toont afgevinkt en niet afgevinkt als percentages.':
'Muestra los elementos marcados y sin marcar como porcentajes.',
'Afgevinkt': 'Marcado',
'Niet afgevinkt': 'Sin marcar',
'Er zijn geen aangevinkte checklist-items om te legen.':
'No hay elementos de lista marcados que vaciar.',
'Alle checkboxen legen?': '¿Vaciar todas las casillas?',
'Hiermee worden alle': 'Se desmarcarán todos los',
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
'elementos de lista marcados de toda la presentación. Puedes deshacerlo con Ctrl/Cmd+Z.',
'Alles legen': 'Vaciar todo',
'checklist-items uitgevinkt.': 'elementos de lista desmarcados.',
'Alle checkboxen legen': 'Vaciar todas las casillas',
'Afgevinkte tekst doorhalen': 'Tachar el texto marcado',
'Toont een streep door voltooide checklistitems.':
'Muestra tachados los elementos completados.',
'Na media automatisch doorgaan':
'Avanzar automáticamente tras el contenido multimedia',
'Opsomming': 'Viñetas',
'Nummering': 'Numeración',
'Varianten': 'Variantes',
'Grafiekvarianten maken': 'Crear variantes del gráfico',
'Slides toevoegen': 'Añadir diapositivas',
'Omhoog': 'Subir',
'Omlaag': 'Bajar',
'Niet toevoegen': 'No añadir',
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
'Estas diapositivas usan los mismos datos, colores y título. Usa las flechas para elegir su orden después de la diapositiva actual.',
'Spider': 'Radar', 'Spider': 'Radar',
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.': 'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
'Un gráfico radar necesita al menos tres etiquetas (ejes); cada serie forma una superficie.', 'Un gráfico radar necesita al menos tres etiquetas (ejes); cada serie forma una superficie.',
@ -3484,35 +3337,6 @@ const _dutchSourceStringAdditions = {
}, },
'fy': { 'fy': {
'Annuleren': 'Annulearje', 'Annuleren': 'Annulearje',
'Checklist': 'Kontrôlelist',
'Voortgangsgrafiek tonen': 'Fuortgongsgrafyk toane',
'Toont afgevinkt en niet afgevinkt als percentages.':
'Toant ôffinkte en net ôffinkte items as persintaazjes.',
'Afgevinkt': 'Ôffinkt',
'Niet afgevinkt': 'Net ôffinkt',
'Er zijn geen aangevinkte checklist-items om te legen.':
'Der binne gjin oanfinkte checklist-items om leech te meitsjen.',
'Alle checkboxen legen?': 'Alle oanfinkfakjes leechje?',
'Hiermee worden alle': 'Hjirmei wurde alle',
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
'oanfinkte checklist-items yn de hiele presintaasje útfinkt. Dit kinst weromdraaie mei Ctrl/Cmd+Z.',
'Alles legen': 'Alles leechje',
'checklist-items uitgevinkt.': 'checklist-items útfinke.',
'Alle checkboxen legen': 'Alle oanfinkfakjes leechje',
'Afgevinkte tekst doorhalen': 'Ôffinkte tekst trochstreekje',
'Toont een streep door voltooide checklistitems.':
'Toant foltôge kontrôlelistitems mei in streek der troch.',
'Na media automatisch doorgaan': 'Nei media automatysk trochgean',
'Opsomming': 'Puntelist',
'Nummering': 'Nûmering',
'Varianten': 'Farianten',
'Grafiekvarianten maken': 'Grafykfarianten meitsje',
'Slides toevoegen': 'Dias tafoegje',
'Omhoog': 'Omheech',
'Omlaag': 'Omleech',
'Niet toevoegen': 'Net tafoegje',
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
'Dizze dias brûke deselde gegevens, kleuren en titel. Kies mei de pylken de folchoarder nei de aktive dia.',
'Spider': 'Spider', 'Spider': 'Spider',
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.': 'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
'In spiderdiagram hat op syn minst trije labels (assen) nedich; eltse rige foarmet in flak.', 'In spiderdiagram hat op syn minst trije labels (assen) nedich; eltse rige foarmet in flak.',
@ -3743,35 +3567,6 @@ const _dutchSourceStringAdditions = {
}, },
'pap': { 'pap': {
'Annuleren': 'Kanselá', 'Annuleren': 'Kanselá',
'Checklist': 'Lista di kontrol',
'Voortgangsgrafiek tonen': 'Mustra gráfiko di progreso',
'Toont afgevinkt en niet afgevinkt als percentages.':
'Ta mustra elementonan marká i no marká komo porsentahe.',
'Afgevinkt': 'Marká',
'Niet afgevinkt': 'No marká',
'Er zijn geen aangevinkte checklist-items om te legen.':
'No tin elemento di lista marká pa kita.',
'Alle checkboxen legen?': 'Kita tur kasita di chèk?',
'Hiermee worden alle': 'Esaki ta desmarká tur',
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
'elemento di lista marká den henter e presentashon. Bo por deshasé esaki ku Ctrl/Cmd+Z.',
'Alles legen': 'Kita tur',
'checklist-items uitgevinkt.': 'elemento di lista desmarká.',
'Alle checkboxen legen': 'Kita tur kasita di chèk',
'Afgevinkte tekst doorhalen': 'Raya teksto marká',
'Toont een streep door voltooide checklistitems.':
'Ta mustra elementonan kompletá ku un raya den e teksto.',
'Na media automatisch doorgaan': 'Sigui outomátiko despues di multimedia',
'Opsomming': 'Lista ku punto',
'Nummering': 'Numerashon',
'Varianten': 'Variantenan',
'Grafiekvarianten maken': 'Krea variantenan di gráfiko',
'Slides toevoegen': 'Agregá diapositivanan',
'Omhoog': 'Move ariba',
'Omlaag': 'Move abou',
'Niet toevoegen': 'No agregá',
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
'E diapositivanan aki ta usa e mesun datonan, kolónan i título. Usa e flechanan pa skohe nan órden despues di e diapositiva aktual.',
'Spider': 'Radar', 'Spider': 'Radar',
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.': 'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
'Un grafiko radar mester di por lo ménos tres etiketa (ehe); kada serie ta forma un superfisie.', 'Un grafiko radar mester di por lo ménos tres etiketa (ehe); kada serie ta forma un superfisie.',

View file

@ -3,9 +3,6 @@ class ThemeProfile {
final String slideBackgroundColor; final String slideBackgroundColor;
final String textColor; final String textColor;
final String accentColor; final String accentColor;
final String checklistCheckedColor;
final String checklistUncheckedColor;
final bool checklistStrikeThrough;
final String tableTextColor; final String tableTextColor;
final String tableHeaderTextColor; final String tableHeaderTextColor;
final String titleBackgroundColor; final String titleBackgroundColor;
@ -53,9 +50,6 @@ class ThemeProfile {
this.slideBackgroundColor = '#FFFFFF', this.slideBackgroundColor = '#FFFFFF',
this.textColor = '#222222', this.textColor = '#222222',
this.accentColor = '#2E7D64', this.accentColor = '#2E7D64',
this.checklistCheckedColor = '#2E7D64',
this.checklistUncheckedColor = '#CBD5E1',
this.checklistStrikeThrough = true,
String? tableTextColor, String? tableTextColor,
this.tableHeaderTextColor = '#FFFFFF', this.tableHeaderTextColor = '#FFFFFF',
this.titleBackgroundColor = '#1C2B47', this.titleBackgroundColor = '#1C2B47',
@ -90,9 +84,6 @@ class ThemeProfile {
String? slideBackgroundColor, String? slideBackgroundColor,
String? textColor, String? textColor,
String? accentColor, String? accentColor,
String? checklistCheckedColor,
String? checklistUncheckedColor,
bool? checklistStrikeThrough,
String? tableTextColor, String? tableTextColor,
String? tableHeaderTextColor, String? tableHeaderTextColor,
String? titleBackgroundColor, String? titleBackgroundColor,
@ -118,12 +109,6 @@ class ThemeProfile {
slideBackgroundColor: slideBackgroundColor ?? this.slideBackgroundColor, slideBackgroundColor: slideBackgroundColor ?? this.slideBackgroundColor,
textColor: textColor ?? this.textColor, textColor: textColor ?? this.textColor,
accentColor: accentColor ?? this.accentColor, accentColor: accentColor ?? this.accentColor,
checklistCheckedColor:
checklistCheckedColor ?? this.checklistCheckedColor,
checklistUncheckedColor:
checklistUncheckedColor ?? this.checklistUncheckedColor,
checklistStrikeThrough:
checklistStrikeThrough ?? this.checklistStrikeThrough,
tableTextColor: tableTextColor ?? this.tableTextColor, tableTextColor: tableTextColor ?? this.tableTextColor,
tableHeaderTextColor: tableHeaderTextColor ?? this.tableHeaderTextColor, tableHeaderTextColor: tableHeaderTextColor ?? this.tableHeaderTextColor,
titleBackgroundColor: titleBackgroundColor ?? this.titleBackgroundColor, titleBackgroundColor: titleBackgroundColor ?? this.titleBackgroundColor,
@ -153,9 +138,6 @@ class ThemeProfile {
'name': name, 'name': name,
'textColor': textColor, 'textColor': textColor,
'accentColor': accentColor, 'accentColor': accentColor,
'checklistCheckedColor': checklistCheckedColor,
'checklistUncheckedColor': checklistUncheckedColor,
'checklistStrikeThrough': checklistStrikeThrough,
'tableTextColor': tableTextColor, 'tableTextColor': tableTextColor,
'tableHeaderTextColor': tableHeaderTextColor, 'tableHeaderTextColor': tableHeaderTextColor,
'titleBackgroundColor': titleBackgroundColor, 'titleBackgroundColor': titleBackgroundColor,
@ -184,13 +166,6 @@ class ThemeProfile {
name: json['name'] as String? ?? 'Standaard', name: json['name'] as String? ?? 'Standaard',
textColor: json['textColor'] as String? ?? '#222222', textColor: json['textColor'] as String? ?? '#222222',
accentColor: json['accentColor'] as String? ?? '#2E7D64', accentColor: json['accentColor'] as String? ?? '#2E7D64',
checklistCheckedColor:
json['checklistCheckedColor'] as String? ??
json['accentColor'] as String? ??
'#2E7D64',
checklistUncheckedColor:
json['checklistUncheckedColor'] as String? ?? '#CBD5E1',
checklistStrikeThrough: json['checklistStrikeThrough'] as bool? ?? true,
tableTextColor: tableTextColor:
json['tableTextColor'] as String? ?? json['tableTextColor'] as String? ??
json['textColor'] as String? ?? json['textColor'] as String? ??
@ -202,7 +177,8 @@ class ThemeProfile {
titleTextColor: json['titleTextColor'] as String? ?? '#FFFFFF', titleTextColor: json['titleTextColor'] as String? ?? '#FFFFFF',
sectionBackgroundColor: sectionBackgroundColor:
json['sectionBackgroundColor'] as String? ?? '#2E7D64', json['sectionBackgroundColor'] as String? ?? '#2E7D64',
codeBackgroundColor: json['codeBackgroundColor'] as String? ?? '#282C34', codeBackgroundColor:
json['codeBackgroundColor'] as String? ?? '#282C34',
codeTextColor: json['codeTextColor'] as String? ?? '#ABB2BF', codeTextColor: json['codeTextColor'] as String? ?? '#ABB2BF',
codeHighlightSyntax: json['codeHighlightSyntax'] as bool? ?? true, codeHighlightSyntax: json['codeHighlightSyntax'] as bool? ?? true,
codeFontFamily: json['codeFontFamily'] as String? ?? 'monospace', codeFontFamily: json['codeFontFamily'] as String? ?? 'monospace',

View file

@ -19,30 +19,6 @@ enum SlideType {
chart, chart,
} }
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';
extension SlideTypeExtension on SlideType { extension SlideTypeExtension on SlideType {
String get label { String get label {
switch (this) { switch (this) {
@ -114,8 +90,6 @@ class Slide {
final String subtitle; final String subtitle;
final List<String> bullets; final List<String> bullets;
final List<String> bullets2; final List<String> bullets2;
final ListStyle listStyle;
final bool showChecklistProgress;
/// Optional headings above the two bullet columns (twoBullets only). Empty = /// Optional headings above the two bullet columns (twoBullets only). Empty =
/// no heading for that column. /// no heading for that column.
@ -154,8 +128,6 @@ class Slide {
this.subtitle = '', this.subtitle = '',
this.bullets = const [], this.bullets = const [],
this.bullets2 = const [], this.bullets2 = const [],
this.listStyle = ListStyle.bullets,
this.showChecklistProgress = false,
this.columnTitle1 = '', this.columnTitle1 = '',
this.columnTitle2 = '', this.columnTitle2 = '',
this.imagePath = '', this.imagePath = '',
@ -211,8 +183,6 @@ class Slide {
subtitle: src.subtitle, subtitle: src.subtitle,
bullets: List<String>.from(src.bullets), bullets: List<String>.from(src.bullets),
bullets2: List<String>.from(src.bullets2), bullets2: List<String>.from(src.bullets2),
listStyle: src.listStyle,
showChecklistProgress: src.showChecklistProgress,
columnTitle1: src.columnTitle1, columnTitle1: src.columnTitle1,
columnTitle2: src.columnTitle2, columnTitle2: src.columnTitle2,
imagePath: src.imagePath, imagePath: src.imagePath,
@ -245,8 +215,6 @@ class Slide {
String? subtitle, String? subtitle,
List<String>? bullets, List<String>? bullets,
List<String>? bullets2, List<String>? bullets2,
ListStyle? listStyle,
bool? showChecklistProgress,
String? columnTitle1, String? columnTitle1,
String? columnTitle2, String? columnTitle2,
String? imagePath, String? imagePath,
@ -278,9 +246,6 @@ class Slide {
subtitle: subtitle ?? this.subtitle, subtitle: subtitle ?? this.subtitle,
bullets: bullets ?? this.bullets, bullets: bullets ?? this.bullets,
bullets2: bullets2 ?? this.bullets2, bullets2: bullets2 ?? this.bullets2,
listStyle: listStyle ?? this.listStyle,
showChecklistProgress:
showChecklistProgress ?? this.showChecklistProgress,
columnTitle1: columnTitle1 ?? this.columnTitle1, columnTitle1: columnTitle1 ?? this.columnTitle1,
columnTitle2: columnTitle2 ?? this.columnTitle2, columnTitle2: columnTitle2 ?? this.columnTitle2,
imagePath: imagePath ?? this.imagePath, imagePath: imagePath ?? this.imagePath,

View file

@ -216,12 +216,10 @@ class MarkdownService {
case SlideType.bullets: case SlideType.bullets:
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}'); if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
if (slide.subtitle.isNotEmpty) buf.writeln('## ${slide.subtitle}'); if (slide.subtitle.isNotEmpty) buf.writeln('## ${slide.subtitle}');
if (slide.listStyle != ListStyle.bullets) {
buf.writeln('<!-- ocideck_list_style: ${slide.listStyle.name} -->');
}
_writeChecklistProgress(buf, slide);
buf.writeln(); buf.writeln();
_writeList(buf, slide.bullets, slide.listStyle); for (final b in slide.bullets) {
_writeBullet(buf, b);
}
case SlideType.twoBullets: case SlideType.twoBullets:
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}'); if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
@ -232,9 +230,6 @@ class MarkdownService {
slide.bullets2, slide.bullets2,
slide.columnTitle1, slide.columnTitle1,
slide.columnTitle2, slide.columnTitle2,
slide.listStyle,
slide.showChecklistProgress,
themeProfile ?? const ThemeProfile(),
); );
case SlideType.bulletsImage: case SlideType.bulletsImage:
@ -253,12 +248,10 @@ class MarkdownService {
); );
buf.writeln(); buf.writeln();
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}'); if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
if (slide.listStyle != ListStyle.bullets) {
buf.writeln('<!-- ocideck_list_style: ${slide.listStyle.name} -->');
}
_writeChecklistProgress(buf, slide);
buf.writeln(); buf.writeln();
_writeList(buf, slide.bullets, slide.listStyle); for (final b in slide.bullets) {
_writeBullet(buf, b);
}
buf.writeln(); buf.writeln();
buf.writeln('</div>'); buf.writeln('</div>');
buf.writeln(); buf.writeln();
@ -270,12 +263,10 @@ class MarkdownService {
buf.writeln('</div>'); buf.writeln('</div>');
} else { } else {
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}'); if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
if (slide.listStyle != ListStyle.bullets) {
buf.writeln('<!-- ocideck_list_style: ${slide.listStyle.name} -->');
}
_writeChecklistProgress(buf, slide);
buf.writeln(); buf.writeln();
_writeList(buf, slide.bullets, slide.listStyle); for (final b in slide.bullets) {
_writeBullet(buf, b);
}
} }
case SlideType.twoImages: case SlideType.twoImages:
@ -410,35 +401,14 @@ class MarkdownService {
return buf.toString(); return buf.toString();
} }
static void _writeList( static void _writeBullet(StringBuffer buf, String bullet) {
StringBuffer buf, int level = 0;
List<String> items, while (level < bullet.length && bullet[level] == '\t') {
ListStyle style, level++;
) { }
final counters = <int>[]; final text = bullet.substring(level);
for (final item in items) { if (text.isNotEmpty) {
int level = 0; buf.writeln('${' ' * level}- $text');
while (level < item.length && item[level] == '\t') {
level++;
}
final text = item.substring(level);
if (text.isEmpty) continue;
while (counters.length <= level) {
counters.add(0);
}
counters[level]++;
if (counters.length > level + 1) {
counters.removeRange(level + 1, counters.length);
}
final marker = switch (style) {
ListStyle.numbered => '${counters[level]}.',
ListStyle.bullets || ListStyle.checklist => '-',
};
final body = style == ListStyle.checklist
? '[${checklistItemChecked(item) ? 'x' : ' '}] '
'${checklistItemText(item)}'
: text;
buf.writeln('${' ' * level}$marker $body');
} }
} }
@ -448,18 +418,9 @@ class MarkdownService {
List<String> right, List<String> right,
String leftTitle, String leftTitle,
String rightTitle, String rightTitle,
ListStyle listStyle,
bool showChecklistProgress,
ThemeProfile themeProfile,
) { ) {
buf.writeln('<!-- ocideck_two_bullets_left: ${_encodeBullets(left)} -->'); buf.writeln('<!-- ocideck_two_bullets_left: ${_encodeBullets(left)} -->');
buf.writeln('<!-- ocideck_two_bullets_right: ${_encodeBullets(right)} -->'); buf.writeln('<!-- ocideck_two_bullets_right: ${_encodeBullets(right)} -->');
if (listStyle != ListStyle.bullets) {
buf.writeln('<!-- ocideck_list_style: ${listStyle.name} -->');
}
if (showChecklistProgress) {
buf.writeln('<!-- ocideck_checklist_progress: true -->');
}
if (leftTitle.isNotEmpty) { if (leftTitle.isNotEmpty) {
buf.writeln( buf.writeln(
'<!-- ocideck_two_bullets_left_title: ${_encodeText(leftTitle)} -->', '<!-- ocideck_two_bullets_left_title: ${_encodeText(leftTitle)} -->',
@ -473,23 +434,15 @@ class MarkdownService {
buf.writeln( buf.writeln(
'<div class="ocideck-two-bullets" style="display:grid; grid-template-columns:1fr 1fr; gap:3rem; align-items:start;">', '<div class="ocideck-two-bullets" style="display:grid; grid-template-columns:1fr 1fr; gap:3rem; align-items:start;">',
); );
_writeBulletColumn(buf, left, leftTitle, listStyle, themeProfile); _writeBulletColumn(buf, left, leftTitle);
_writeBulletColumn(buf, right, rightTitle, listStyle, themeProfile); _writeBulletColumn(buf, right, rightTitle);
buf.writeln('</div>'); buf.writeln('</div>');
} }
static void _writeChecklistProgress(StringBuffer buf, Slide slide) {
if (slide.showChecklistProgress) {
buf.writeln('<!-- ocideck_checklist_progress: true -->');
}
}
static void _writeBulletColumn( static void _writeBulletColumn(
StringBuffer buf, StringBuffer buf,
List<String> bullets, List<String> bullets,
String columnTitle, String columnTitle,
ListStyle listStyle,
ThemeProfile themeProfile,
) { ) {
buf.writeln('<div>'); buf.writeln('<div>');
if (columnTitle.isNotEmpty) { if (columnTitle.isNotEmpty) {
@ -497,10 +450,9 @@ class MarkdownService {
'<h3 style="margin:0 0 .5rem;">${_escapeHtml(columnTitle)}</h3>', '<h3 style="margin:0 0 .5rem;">${_escapeHtml(columnTitle)}</h3>',
); );
} }
final tag = listStyle == ListStyle.numbered ? 'ol' : 'ul'; buf.writeln('<ul style="margin:0; padding-left:1.3em;">');
buf.writeln('<$tag style="margin:0; padding-left:1.3em;">'); _writeHtmlBulletItems(buf, bullets);
_writeHtmlBulletItems(buf, bullets, listStyle, themeProfile); buf.writeln('</ul>');
buf.writeln('</$tag>');
buf.writeln('</div>'); buf.writeln('</div>');
} }
@ -528,41 +480,16 @@ class MarkdownService {
return const []; return const [];
} }
static void _writeHtmlBulletItems( static void _writeHtmlBulletItems(StringBuffer buf, List<String> bullets) {
StringBuffer buf,
List<String> bullets,
ListStyle listStyle,
ThemeProfile themeProfile,
) {
final counters = <int>[];
for (final b in bullets) { for (final b in bullets) {
int level = 0; int level = 0;
while (level < b.length && b[level] == '\t') { while (level < b.length && b[level] == '\t') {
level++; level++;
} }
final text = listStyle == ListStyle.checklist final text = b.substring(level).trim();
? checklistItemText(b).trim()
: b.substring(level).trim();
if (text.isEmpty) continue; if (text.isEmpty) continue;
while (counters.length <= level) {
counters.add(0);
}
counters[level]++;
if (counters.length > level + 1) {
counters.removeRange(level + 1, counters.length);
}
final style = level == 0 ? '' : ' style="margin-left:${level * 1.4}em;"'; final style = level == 0 ? '' : ' style="margin-left:${level * 1.4}em;"';
final value = listStyle == ListStyle.numbered buf.writeln('<li$style>${_escapeHtml(text)}</li>');
? ' value="${counters[level]}"'
: '';
final checkbox = listStyle == ListStyle.checklist
? '${checklistItemChecked(b) ? '' : ''} '
: '';
final decoration = listStyle == ListStyle.checklist
? ' style="${level == 0 ? '' : 'margin-left:${level * 1.4}em;'}'
'${checklistItemChecked(b) && themeProfile.checklistStrikeThrough ? 'text-decoration:line-through;opacity:.7;' : ''}"'
: style;
buf.writeln('<li$value$decoration>${_escapeHtml(checkbox + text)}</li>');
} }
} }
@ -749,8 +676,6 @@ class MarkdownService {
TlpLevel slideTlp = TlpLevel.none; TlpLevel slideTlp = TlpLevel.none;
final bullets = <String>[]; final bullets = <String>[];
var bullets2 = <String>[]; var bullets2 = <String>[];
var listStyle = ListStyle.bullets;
var showChecklistProgress = false;
var columnTitle1 = ''; var columnTitle1 = '';
var columnTitle2 = ''; var columnTitle2 = '';
// bulletsImage slides store their panel width in `<!-- _style: // bulletsImage slides store their panel width in `<!-- _style:
@ -779,16 +704,6 @@ class MarkdownService {
columnTitle2 = _decodeText(content.substring(32)); columnTitle2 = _decodeText(content.substring(32));
} else if (content.startsWith('ocideck_two_bullets_right:')) { } else if (content.startsWith('ocideck_two_bullets_right:')) {
bullets2 = _decodeBullets(content.substring(26)); bullets2 = _decodeBullets(content.substring(26));
} else if (content.startsWith('ocideck_list_style:')) {
final name = content.substring(19).trim();
listStyle = ListStyle.values.firstWhere(
(style) => style.name == name,
orElse: () => ListStyle.bullets,
);
} else if (content.startsWith('ocideck_checklist_progress:')) {
showChecklistProgress =
content.substring('ocideck_checklist_progress:'.length).trim() ==
'true';
} else if (!content.startsWith('_')) { } else if (!content.startsWith('_')) {
notesBuffer.write(notesBuffer.isEmpty ? content : '\n$content'); notesBuffer.write(notesBuffer.isEmpty ? content : '\n$content');
} }
@ -858,23 +773,7 @@ class MarkdownService {
} }
} }
final level = spaces ~/ 2; final level = spaces ~/ 2;
final body = t.substring(2); bullets.add('\t' * level + t.substring(2));
bullets.add('\t' * level + body);
if (RegExp(r'^\[[ xX]\]\s*').hasMatch(body)) {
listStyle = ListStyle.checklist;
}
} else if (RegExp(r'^\d+\.\s+').hasMatch(t)) {
int spaces = 0;
for (final ch in line.characters) {
if (ch == ' ') {
spaces++;
} else {
break;
}
}
final level = spaces ~/ 2;
bullets.add('\t' * level + t.replaceFirst(RegExp(r'^\d+\.\s+'), ''));
listStyle = ListStyle.numbered;
} else if (t.startsWith('> ')) { } else if (t.startsWith('> ')) {
quote = t.substring(2); quote = t.substring(2);
} else if (t.startsWith('')) { } else if (t.startsWith('')) {
@ -1001,8 +900,6 @@ class MarkdownService {
subtitle: type == SlideType.section ? paragraph : h2, subtitle: type == SlideType.section ? paragraph : h2,
bullets: bullets, bullets: bullets,
bullets2: bullets2, bullets2: bullets2,
listStyle: listStyle,
showChecklistProgress: showChecklistProgress,
columnTitle1: columnTitle1, columnTitle1: columnTitle1,
columnTitle2: columnTitle2, columnTitle2: columnTitle2,
imagePath: imagePath, imagePath: imagePath,

View file

@ -305,55 +305,6 @@ class DeckNotifier extends StateNotifier<DeckState> {
_mutate(deck.copyWith(slides: slides)); _mutate(deck.copyWith(slides: slides));
} }
/// Hoeveel checklist-items in de hele presentatie momenteel afgevinkt zijn.
int get checkedChecklistCount {
final deck = state.deck;
if (deck == null) return 0;
var total = 0;
for (final s in deck.slides) {
total += s.bullets.where(checklistItemChecked).length;
total += s.bullets2.where(checklistItemChecked).length;
}
return total;
}
/// Vink in één keer alle checklist-items in de hele presentatie uit (bijv.
/// om een ingevulde checklist opnieuw te kunnen aflopen). Eén
/// ongedaan-maken-stap. No-op wanneer er niets is aangevinkt.
void clearAllChecklists() {
final deck = state.deck;
if (deck == null) return;
String uncheck(String bullet) => checklistItemChecked(bullet)
? checklistBullet(
level: bulletLevel(bullet),
text: checklistItemText(bullet),
checked: false,
)
: bullet;
var changed = false;
final slides = <Slide>[];
for (final s in deck.slides) {
if (s.bullets.any(checklistItemChecked) ||
s.bullets2.any(checklistItemChecked)) {
changed = true;
slides.add(
s.copyWith(
bullets: [for (final b in s.bullets) uncheck(b)],
bullets2: [for (final b in s.bullets2) uncheck(b)],
),
);
} else {
slides.add(s);
}
}
// Bump de revisie zodat de editor van de geselecteerde slide remount en de
// uitgevinkte checkboxen ook in het invoerpaneel toont (niet alleen in de
// slidepreview).
if (changed) {
_mutate(deck.copyWith(slides: slides), bumpRevision: true);
}
}
// Zoeken & vervangen // Zoeken & vervangen
/// Tel hoe vaak [query] in alle tekstvelden van de presentatie voorkomt. /// Tel hoe vaak [query] in alle tekstvelden van de presentatie voorkomt.
@ -558,14 +509,7 @@ class DeckNotifier extends StateNotifier<DeckState> {
/// binnen [_coalesceWindow] valt, wordt geen nieuwe ongedaan-stap aangemaakt /// binnen [_coalesceWindow] valt, wordt geen nieuwe ongedaan-stap aangemaakt
/// (zodat typen niet per teken een aparte stap oplevert). Een [coalesceKey] /// (zodat typen niet per teken een aparte stap oplevert). Een [coalesceKey]
/// van null markeert een losse, discrete stap. /// van null markeert een losse, discrete stap.
/// void _mutate(Deck deck, {String? coalesceKey}) {
/// Wanneer [bumpRevision] waar is, wordt de inhouds-revisie opgehoogd. Dat
/// dwingt de editor-subtree (die op `revision` is gesleuteld) om te remounten
/// en zijn velden opnieuw uit de slide te laden. Nodig bij deck-brede
/// bewerkingen die de huidige slide aanpassen zonder dat de editor zelf de
/// bron van de wijziging was (anders blijft de editor de oude, gecachte
/// waarden tonen).
void _mutate(Deck deck, {String? coalesceKey, bool bumpRevision = false}) {
final previous = state.deck; final previous = state.deck;
if (previous != null) { if (previous != null) {
final now = DateTime.now(); final now = DateTime.now();
@ -588,7 +532,6 @@ class DeckNotifier extends StateNotifier<DeckState> {
isDirty: true, isDirty: true,
canUndo: _undoStack.isNotEmpty, canUndo: _undoStack.isNotEmpty,
canRedo: false, canRedo: false,
revision: bumpRevision ? state.revision + 1 : null,
); );
} }
} }

View file

@ -886,53 +886,6 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
); );
} }
Future<void> clearAllChecklists() async {
final count = deckNotifier.checkedChecklistCount;
if (count == 0) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
l10n.d('Er zijn geen aangevinkte checklist-items om te legen.'),
),
),
);
return;
}
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) {
final l10n = ctx.l10n;
return AlertDialog(
title: Text(l10n.d('Alle checkboxen legen?')),
content: Text(
'${l10n.d('Hiermee worden alle')} $count '
'${l10n.d('aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.')}',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text(l10n.t('cancel')),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text(l10n.d('Alles legen')),
),
],
);
},
);
if (confirmed != true) return;
deckNotifier.clearAllChecklists();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'$count ${l10n.d('checklist-items uitgevinkt.')}',
),
),
);
}
Future<void> openImageCarousel() async { Future<void> openImageCarousel() async {
final idx = editor.selectedIndex.clamp(0, deck.slides.length - 1); final idx = editor.selectedIndex.clamp(0, deck.slides.length - 1);
final slide = deck.slides[idx]; final slide = deck.slides[idx];
@ -1026,14 +979,6 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
tlp: deck.tlp, tlp: deck.tlp,
annotations: deck.annotations, annotations: deck.annotations,
onAnnotationsChanged: deckNotifier.setAnnotations, onAnnotationsChanged: deckNotifier.setAnnotations,
onSlideChanged: (updated) {
final index = deckNotifier.currentState.deck?.slides.indexWhere(
(slide) => slide.id == updated.id,
);
if (index != null && index >= 0) {
deckNotifier.updateSlide(index, updated);
}
},
); );
} }
@ -1328,8 +1273,6 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
importUrl(); importUrl();
case 'find': case 'find':
openFindReplace(); openFindReplace();
case 'clear_checklists':
clearAllChecklists();
case 'full_preview': case 'full_preview':
openFullDeckPreview(); openFullDeckPreview();
case 'properties': case 'properties':
@ -1372,11 +1315,6 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
menuItem('import_url', Icons.link, l10n.t('importUrl')), menuItem('import_url', Icons.link, l10n.t('importUrl')),
const PopupMenuDivider(), const PopupMenuDivider(),
menuItem('find', Icons.find_replace, l10n.t('findReplace')), menuItem('find', Icons.find_replace, l10n.t('findReplace')),
menuItem(
'clear_checklists',
Icons.check_box_outline_blank,
l10n.d('Alle checkboxen legen'),
),
menuItem( menuItem(
'full_preview', 'full_preview',
Icons.preview_outlined, Icons.preview_outlined,

View file

@ -942,43 +942,6 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
_themeProfile.accentColor, _themeProfile.accentColor,
(v) => _themeProfile = _themeProfile.copyWith(accentColor: v), (v) => _themeProfile = _themeProfile.copyWith(accentColor: v),
), ),
const SizedBox(height: 24),
_sectionTitle(l10n.d('Checklist')),
_colorSetting(
l10n.d('Afgevinkt'),
_themeProfile.checklistCheckedColor,
(v) =>
_themeProfile = _themeProfile.copyWith(checklistCheckedColor: v),
),
const SizedBox(height: 12),
_colorSetting(
l10n.d('Niet afgevinkt'),
_themeProfile.checklistUncheckedColor,
(v) => _themeProfile = _themeProfile.copyWith(
checklistUncheckedColor: v,
),
),
const SizedBox(height: 6),
SwitchListTile(
value: _themeProfile.checklistStrikeThrough,
onChanged: (value) => setState(() {
_themeProfile = _themeProfile.copyWith(
checklistStrikeThrough: value,
);
_profileTouched = true;
}),
title: Text(
l10n.d('Afgevinkte tekst doorhalen'),
style: const TextStyle(fontSize: 13),
),
subtitle: Text(
l10n.d('Toont een streep door voltooide checklistitems.'),
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
dense: true,
),
const SizedBox(height: 12), const SizedBox(height: 12),
_colorSetting( _colorSetting(
l10n.d('Tabeltekst'), l10n.d('Tabeltekst'),
@ -1017,7 +980,8 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
_colorSetting( _colorSetting(
l10n.d('Broncode achtergrond'), l10n.d('Broncode achtergrond'),
_themeProfile.codeBackgroundColor, _themeProfile.codeBackgroundColor,
(v) => _themeProfile = _themeProfile.copyWith(codeBackgroundColor: v), (v) =>
_themeProfile = _themeProfile.copyWith(codeBackgroundColor: v),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_colorSetting( _colorSetting(
@ -1048,8 +1012,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
initialValue: initialValue: AppSettings.codeFonts.contains(_themeProfile.codeFontFamily)
AppSettings.codeFonts.contains(_themeProfile.codeFontFamily)
? _themeProfile.codeFontFamily ? _themeProfile.codeFontFamily
: 'monospace', : 'monospace',
decoration: InputDecoration( decoration: InputDecoration(
@ -1319,7 +1282,11 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFCBD5E1)), border: Border.all(color: const Color(0xFFCBD5E1)),
), ),
child: const Icon(Icons.tune, size: 18, color: Color(0xFF64748B)), child: const Icon(
Icons.tune,
size: 18,
color: Color(0xFF64748B),
),
), ),
), ),
); );
@ -1396,10 +1363,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
l10n.d('Bijvoorbeeld #33FF33 voor een CRT-groen scherm.'), l10n.d('Bijvoorbeeld #33FF33 voor een CRT-groen scherm.'),
style: const TextStyle( style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
fontSize: 11,
color: Color(0xFF94A3B8),
),
), ),
], ],
), ),

View file

@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
import '../../models/slide.dart'; import '../../models/slide.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
import '_editor_field.dart'; import '_editor_field.dart';
import 'list_style_selector.dart';
class BulletsEditor extends StatefulWidget { class BulletsEditor extends StatefulWidget {
final Slide slide; final Slide slide;
@ -20,10 +19,7 @@ class _BulletsEditorState extends State<BulletsEditor> {
late final TextEditingController _subtitle; late final TextEditingController _subtitle;
late List<TextEditingController> _bullets; late List<TextEditingController> _bullets;
late List<int> _levels; late List<int> _levels;
late List<bool> _checked;
late List<FocusNode> _focusNodes; late List<FocusNode> _focusNodes;
late ListStyle _listStyle;
late bool _showChecklistProgress;
static const _maxLevel = 4; static const _maxLevel = 4;
@ -34,16 +30,13 @@ class _BulletsEditorState extends State<BulletsEditor> {
_title.addListener(_emit); _title.addListener(_emit);
_subtitle = TextEditingController(text: widget.slide.subtitle); _subtitle = TextEditingController(text: widget.slide.subtitle);
_subtitle.addListener(_emit); _subtitle.addListener(_emit);
_listStyle = widget.slide.listStyle;
_showChecklistProgress = widget.slide.showChecklistProgress;
_initBullets(widget.slide.bullets); _initBullets(widget.slide.bullets);
} }
void _initBullets(List<String> raw) { void _initBullets(List<String> raw) {
final list = raw.isEmpty ? [''] : raw; final list = raw.isEmpty ? [''] : raw;
_levels = list.map(_levelOf).toList(); _levels = list.map(_levelOf).toList();
_checked = list.map(checklistItemChecked).toList(); _bullets = list.map((b) => _makeCtrl(b.trimLeft())).toList();
_bullets = list.map((b) => _makeCtrl(checklistItemText(b))).toList();
_focusNodes = List.generate(_bullets.length, (_) => FocusNode()); _focusNodes = List.generate(_bullets.length, (_) => FocusNode());
} }
@ -66,17 +59,9 @@ class _BulletsEditorState extends State<BulletsEditor> {
widget.slide.copyWith( widget.slide.copyWith(
title: _title.text, title: _title.text,
subtitle: _subtitle.text, subtitle: _subtitle.text,
listStyle: _listStyle,
showChecklistProgress: _showChecklistProgress,
bullets: List.generate( bullets: List.generate(
_bullets.length, _bullets.length,
(i) => _listStyle == ListStyle.checklist (i) => '\t' * _levels[i] + _bullets[i].text,
? checklistBullet(
level: _levels[i],
text: _bullets[i].text,
checked: _checked[i],
)
: '\t' * _levels[i] + _bullets[i].text,
), ),
), ),
); );
@ -90,11 +75,9 @@ class _BulletsEditorState extends State<BulletsEditor> {
setState(() { setState(() {
final ctrl = _bullets.removeAt(oldIndex); final ctrl = _bullets.removeAt(oldIndex);
final level = _levels.removeAt(oldIndex); final level = _levels.removeAt(oldIndex);
final checked = _checked.removeAt(oldIndex);
final focus = _focusNodes.removeAt(oldIndex); final focus = _focusNodes.removeAt(oldIndex);
_bullets.insert(newIndex, ctrl); _bullets.insert(newIndex, ctrl);
_levels.insert(newIndex, level); _levels.insert(newIndex, level);
_checked.insert(newIndex, checked);
_focusNodes.insert(newIndex, focus); _focusNodes.insert(newIndex, focus);
}); });
_emit(); _emit();
@ -105,7 +88,6 @@ class _BulletsEditorState extends State<BulletsEditor> {
setState(() { setState(() {
_bullets.insert(i + 1, _makeCtrl('')); _bullets.insert(i + 1, _makeCtrl(''));
_levels.insert(i + 1, newLevel); _levels.insert(i + 1, newLevel);
_checked.insert(i + 1, false);
_focusNodes.insert(i + 1, FocusNode()); _focusNodes.insert(i + 1, FocusNode());
}); });
_emit(); _emit();
@ -115,25 +97,13 @@ class _BulletsEditorState extends State<BulletsEditor> {
} }
void _removeBulletAndFocus(int i) { void _removeBulletAndFocus(int i) {
if (_bullets.length == 1) { if (_bullets.length <= 1) return;
setState(() {
_bullets[i].removeListener(_emit);
_bullets[i].clear();
_bullets[i].addListener(_emit);
_levels[i] = 0;
_checked[i] = false;
});
_emit();
_focusNodes[i].requestFocus();
return;
}
final target = (i - 1).clamp(0, _bullets.length - 2); final target = (i - 1).clamp(0, _bullets.length - 2);
setState(() { setState(() {
_bullets[i].removeListener(_emit); _bullets[i].removeListener(_emit);
_bullets[i].dispose(); _bullets[i].dispose();
_bullets.removeAt(i); _bullets.removeAt(i);
_levels.removeAt(i); _levels.removeAt(i);
_checked.removeAt(i);
_focusNodes[i].dispose(); _focusNodes[i].dispose();
_focusNodes.removeAt(i); _focusNodes.removeAt(i);
}); });
@ -172,7 +142,6 @@ class _BulletsEditorState extends State<BulletsEditor> {
for (int j = 1; j < lines.length; j++) { for (int j = 1; j < lines.length; j++) {
_bullets.insert(i + j, _makeCtrl(lines[j])); _bullets.insert(i + j, _makeCtrl(lines[j]));
_levels.insert(i + j, _levels[i]); _levels.insert(i + j, _levels[i]);
_checked.insert(i + j, false);
_focusNodes.insert(i + j, FocusNode()); _focusNodes.insert(i + j, FocusNode());
} }
}); });
@ -210,27 +179,6 @@ class _BulletsEditorState extends State<BulletsEditor> {
hint: l10n.d('Subkop'), hint: l10n.d('Subkop'),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ListStyleSelector(
value: _listStyle,
onChanged: (value) {
setState(() => _listStyle = value);
_emit();
},
),
if (_listStyle == ListStyle.checklist)
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(l10n.d('Voortgangsgrafiek tonen')),
subtitle: Text(
l10n.d('Toont afgevinkt en niet afgevinkt als percentages.'),
),
value: _showChecklistProgress,
onChanged: (value) {
setState(() => _showChecklistProgress = value);
_emit();
},
),
const SizedBox(height: 16),
const SectionLabel('Bullets'), const SectionLabel('Bullets'),
ReorderableListView( ReorderableListView(
shrinkWrap: true, shrinkWrap: true,
@ -272,25 +220,10 @@ class _BulletsEditorState extends State<BulletsEditor> {
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
if (_listStyle == ListStyle.checklist) Text(
SizedBox( _markerForLevel(level),
width: 24, style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
height: 24, ),
child: Checkbox(
key: ValueKey('checklist-item-$i'),
value: _checked[i],
onChanged: (value) {
setState(() => _checked[i] = value ?? false);
_emit();
},
visualDensity: VisualDensity.compact,
),
)
else
Text(
_markerForItem(i),
style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Focus( child: Focus(
@ -338,13 +271,14 @@ class _BulletsEditorState extends State<BulletsEditor> {
), ),
), ),
IconButton( IconButton(
key: ValueKey('remove-bullet-$i'),
icon: const Icon( icon: const Icon(
Icons.remove_circle_outline, Icons.remove_circle_outline,
size: 18, size: 18,
color: Color(0xFF94A3B8), color: Color(0xFF94A3B8),
), ),
onPressed: () => _removeBulletAndFocus(i), onPressed: _bullets.length > 1
? () => _removeBulletAndFocus(i)
: null,
tooltip: l10n.d('Verwijder'), tooltip: l10n.d('Verwijder'),
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
constraints: const BoxConstraints(minWidth: 28), constraints: const BoxConstraints(minWidth: 28),
@ -358,18 +292,4 @@ class _BulletsEditorState extends State<BulletsEditor> {
const markers = ['', '', '', '', '']; const markers = ['', '', '', '', ''];
return markers[level.clamp(0, markers.length - 1)]; return markers[level.clamp(0, markers.length - 1)];
} }
String _markerForItem(int index) {
if (_listStyle == ListStyle.bullets) {
return _markerForLevel(_levels[index]);
}
if (_listStyle == ListStyle.checklist) return '';
final level = _levels[index];
var number = 0;
for (var i = 0; i <= index; i++) {
if (_levels[i] == level) number++;
if (_levels[i] < level) number = 0;
}
return '$number.';
}
} }

View file

@ -4,7 +4,6 @@ import '../../models/slide.dart';
import '../../services/image_service.dart'; import '../../services/image_service.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
import '_editor_field.dart'; import '_editor_field.dart';
import 'list_style_selector.dart';
class BulletsImageEditor extends StatefulWidget { class BulletsImageEditor extends StatefulWidget {
final Slide slide; final Slide slide;
@ -30,10 +29,7 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
late final TextEditingController _title; late final TextEditingController _title;
late List<TextEditingController> _bullets; late List<TextEditingController> _bullets;
late List<int> _levels; late List<int> _levels;
late List<bool> _checked;
late List<FocusNode> _focusNodes; late List<FocusNode> _focusNodes;
late ListStyle _listStyle;
late bool _showChecklistProgress;
static const _maxLevel = 4; static const _maxLevel = 4;
@ -42,12 +38,9 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
super.initState(); super.initState();
_title = TextEditingController(text: widget.slide.title); _title = TextEditingController(text: widget.slide.title);
_title.addListener(_emit); _title.addListener(_emit);
_listStyle = widget.slide.listStyle;
_showChecklistProgress = widget.slide.showChecklistProgress;
final list = widget.slide.bullets.isEmpty ? [''] : widget.slide.bullets; final list = widget.slide.bullets.isEmpty ? [''] : widget.slide.bullets;
_levels = list.map(_levelOf).toList(); _levels = list.map(_levelOf).toList();
_checked = list.map(checklistItemChecked).toList(); _bullets = list.map((b) => _makeCtrl(b.trimLeft())).toList();
_bullets = list.map((b) => _makeCtrl(checklistItemText(b))).toList();
_focusNodes = List.generate(_bullets.length, (_) => FocusNode()); _focusNodes = List.generate(_bullets.length, (_) => FocusNode());
} }
@ -69,17 +62,9 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
widget.onUpdate( widget.onUpdate(
widget.slide.copyWith( widget.slide.copyWith(
title: _title.text, title: _title.text,
listStyle: _listStyle,
showChecklistProgress: _showChecklistProgress,
bullets: List.generate( bullets: List.generate(
_bullets.length, _bullets.length,
(i) => _listStyle == ListStyle.checklist (i) => '\t' * _levels[i] + _bullets[i].text,
? checklistBullet(
level: _levels[i],
text: _bullets[i].text,
checked: _checked[i],
)
: '\t' * _levels[i] + _bullets[i].text,
), ),
), ),
); );
@ -89,11 +74,9 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
setState(() { setState(() {
final ctrl = _bullets.removeAt(oldIndex); final ctrl = _bullets.removeAt(oldIndex);
final level = _levels.removeAt(oldIndex); final level = _levels.removeAt(oldIndex);
final checked = _checked.removeAt(oldIndex);
final focus = _focusNodes.removeAt(oldIndex); final focus = _focusNodes.removeAt(oldIndex);
_bullets.insert(newIndex, ctrl); _bullets.insert(newIndex, ctrl);
_levels.insert(newIndex, level); _levels.insert(newIndex, level);
_checked.insert(newIndex, checked);
_focusNodes.insert(newIndex, focus); _focusNodes.insert(newIndex, focus);
}); });
_emit(); _emit();
@ -103,7 +86,6 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
setState(() { setState(() {
_bullets.insert(i + 1, _makeCtrl('')); _bullets.insert(i + 1, _makeCtrl(''));
_levels.insert(i + 1, _levels[i]); _levels.insert(i + 1, _levels[i]);
_checked.insert(i + 1, false);
_focusNodes.insert(i + 1, FocusNode()); _focusNodes.insert(i + 1, FocusNode());
}); });
_emit(); _emit();
@ -113,25 +95,13 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
} }
void _removeBulletAndFocus(int i) { void _removeBulletAndFocus(int i) {
if (_bullets.length == 1) { if (_bullets.length <= 1) return;
setState(() {
_bullets[i].removeListener(_emit);
_bullets[i].clear();
_bullets[i].addListener(_emit);
_levels[i] = 0;
_checked[i] = false;
});
_emit();
_focusNodes[i].requestFocus();
return;
}
final target = (i - 1).clamp(0, _bullets.length - 2); final target = (i - 1).clamp(0, _bullets.length - 2);
setState(() { setState(() {
_bullets[i].removeListener(_emit); _bullets[i].removeListener(_emit);
_bullets[i].dispose(); _bullets[i].dispose();
_bullets.removeAt(i); _bullets.removeAt(i);
_levels.removeAt(i); _levels.removeAt(i);
_checked.removeAt(i);
_focusNodes[i].dispose(); _focusNodes[i].dispose();
_focusNodes.removeAt(i); _focusNodes.removeAt(i);
}); });
@ -168,7 +138,6 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
for (int j = 1; j < lines.length; j++) { for (int j = 1; j < lines.length; j++) {
_bullets.insert(i + j, _makeCtrl(lines[j])); _bullets.insert(i + j, _makeCtrl(lines[j]));
_levels.insert(i + j, _levels[i]); _levels.insert(i + j, _levels[i]);
_checked.insert(i + j, false);
_focusNodes.insert(i + j, FocusNode()); _focusNodes.insert(i + j, FocusNode());
} }
}); });
@ -215,27 +184,6 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
children: [ children: [
EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'), EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'),
const SizedBox(height: 16), const SizedBox(height: 16),
ListStyleSelector(
value: _listStyle,
onChanged: (value) {
setState(() => _listStyle = value);
_emit();
},
),
if (_listStyle == ListStyle.checklist)
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(l10n.d('Voortgangsgrafiek tonen')),
subtitle: Text(
l10n.d('Toont afgevinkt en niet afgevinkt als percentages.'),
),
value: _showChecklistProgress,
onChanged: (value) {
setState(() => _showChecklistProgress = value);
_emit();
},
),
const SizedBox(height: 16),
const SectionLabel('Bullets (links)'), const SectionLabel('Bullets (links)'),
ReorderableListView( ReorderableListView(
shrinkWrap: true, shrinkWrap: true,
@ -306,25 +254,10 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
if (_listStyle == ListStyle.checklist) Text(
SizedBox( _markerForLevel(level),
width: 24, style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
height: 24, ),
child: Checkbox(
key: ValueKey('checklist-item-$i'),
value: _checked[i],
onChanged: (value) {
setState(() => _checked[i] = value ?? false);
_emit();
},
visualDensity: VisualDensity.compact,
),
)
else
Text(
_markerForItem(i),
style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Focus( child: Focus(
@ -368,13 +301,14 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
), ),
), ),
IconButton( IconButton(
key: ValueKey('remove-bullet-$i'),
icon: const Icon( icon: const Icon(
Icons.remove_circle_outline, Icons.remove_circle_outline,
size: 18, size: 18,
color: Color(0xFF94A3B8), color: Color(0xFF94A3B8),
), ),
onPressed: () => _removeBulletAndFocus(i), onPressed: _bullets.length > 1
? () => _removeBulletAndFocus(i)
: null,
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
constraints: const BoxConstraints(minWidth: 28), constraints: const BoxConstraints(minWidth: 28),
), ),
@ -387,18 +321,4 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
const markers = ['', '', '', '', '']; const markers = ['', '', '', '', ''];
return markers[level.clamp(0, markers.length - 1)]; return markers[level.clamp(0, markers.length - 1)];
} }
String _markerForItem(int index) {
if (_listStyle == ListStyle.bullets) {
return _markerForLevel(_levels[index]);
}
if (_listStyle == ListStyle.checklist) return '';
final level = _levels[index];
var number = 0;
for (var i = 0; i <= index; i++) {
if (_levels[i] == level) number++;
if (_levels[i] < level) number = 0;
}
return '$number.';
}
} }

View file

@ -17,14 +17,12 @@ import '_editor_field.dart';
class ChartEditor extends StatefulWidget { class ChartEditor extends StatefulWidget {
final Slide slide; final Slide slide;
final ValueChanged<Slide> onUpdate; final ValueChanged<Slide> onUpdate;
final ValueChanged<List<Slide>>? onAddVariants;
final String? projectPath; final String? projectPath;
const ChartEditor({ const ChartEditor({
super.key, super.key,
required this.slide, required this.slide,
required this.onUpdate, required this.onUpdate,
this.onAddVariants,
this.projectPath, this.projectPath,
}); });
@ -32,114 +30,6 @@ class ChartEditor extends StatefulWidget {
State<ChartEditor> createState() => _ChartEditorState(); State<ChartEditor> createState() => _ChartEditorState();
} }
class _ChartVariantsDialog extends StatefulWidget {
final ChartType currentType;
const _ChartVariantsDialog({required this.currentType});
@override
State<_ChartVariantsDialog> createState() => _ChartVariantsDialogState();
}
class _ChartVariantsDialogState extends State<_ChartVariantsDialog> {
late final List<ChartType> _types = [
for (final type in ChartType.values)
if (type != widget.currentType) type,
];
String _label(BuildContext context, ChartType type) {
final l10n = context.l10n;
return switch (type) {
ChartType.bar => l10n.d('Staaf'),
ChartType.line => l10n.d('Lijn'),
ChartType.pie => l10n.d('Cirkel'),
ChartType.radar => l10n.d('Spider'),
};
}
void _move(int index, int delta) {
final target = index + delta;
if (target < 0 || target >= _types.length) return;
setState(() {
final type = _types.removeAt(index);
_types.insert(target, type);
});
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return AlertDialog(
title: Text(l10n.d('Grafiekvarianten maken')),
content: SizedBox(
width: 420,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.d(
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.',
),
style: const TextStyle(fontSize: 12, color: Color(0xFF64748B)),
),
const SizedBox(height: 12),
for (var i = 0; i < _types.length; i++)
ListTile(
key: ValueKey('chart-variant-${_types[i].name}'),
dense: true,
contentPadding: EdgeInsets.zero,
leading: Icon(switch (_types[i]) {
ChartType.bar => Icons.bar_chart,
ChartType.line => Icons.show_chart,
ChartType.pie => Icons.pie_chart_outline,
ChartType.radar => Icons.radar,
}),
title: Text(_label(context, _types[i])),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
key: ValueKey('chart-variant-up-$i'),
onPressed: i == 0 ? null : () => _move(i, -1),
icon: const Icon(Icons.arrow_upward, size: 18),
tooltip: l10n.d('Omhoog'),
),
IconButton(
key: ValueKey('chart-variant-down-$i'),
onPressed: i == _types.length - 1
? null
: () => _move(i, 1),
icon: const Icon(Icons.arrow_downward, size: 18),
tooltip: l10n.d('Omlaag'),
),
IconButton(
onPressed: () => setState(() => _types.removeAt(i)),
icon: const Icon(Icons.close, size: 18),
tooltip: l10n.d('Niet toevoegen'),
),
],
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.d('Annuleren')),
),
FilledButton(
onPressed: _types.isEmpty
? null
: () => Navigator.pop(context, List<ChartType>.from(_types)),
child: Text(l10n.d('Slides toevoegen')),
),
],
);
}
}
class _ChartEditorState extends State<ChartEditor> { class _ChartEditorState extends State<ChartEditor> {
late final TextEditingController _title; late final TextEditingController _title;
late final TextEditingController _minBound; late final TextEditingController _minBound;
@ -221,7 +111,7 @@ class _ChartEditorState extends State<ChartEditor> {
super.dispose(); super.dispose();
} }
ChartSpec _currentSpec() { void _emit() {
final series = <ChartSeries>[ final series = <ChartSeries>[
for (var c = 0; c < _seriesNames.length; c++) for (var c = 0; c < _seriesNames.length; c++)
ChartSeries( ChartSeries(
@ -238,7 +128,7 @@ class _ChartEditorState extends State<ChartEditor> {
], ],
), ),
]; ];
return ChartSpec( final spec = ChartSpec(
type: _type, type: _type,
title: _title.text, title: _title.text,
source: _source, source: _source,
@ -248,33 +138,7 @@ class _ChartEditorState extends State<ChartEditor> {
minBound: _supportsBounds ? _parseBound(_minBound.text) : null, minBound: _supportsBounds ? _parseBound(_minBound.text) : null,
maxBound: _supportsBounds ? _parseBound(_maxBound.text) : null, maxBound: _supportsBounds ? _parseBound(_maxBound.text) : null,
); );
} widget.onUpdate(widget.slide.copyWith(customMarkdown: spec.toBlock()));
void _emit() {
widget.onUpdate(
widget.slide.copyWith(customMarkdown: _currentSpec().toBlock()),
);
}
Future<void> _createVariants() async {
final selected = await showDialog<List<ChartType>>(
context: context,
builder: (context) => _ChartVariantsDialog(currentType: _type),
);
if (selected == null || selected.isEmpty) return;
final base = _currentSpec();
widget.onAddVariants?.call([
for (final type in selected)
widget.slide.copyWith(
customMarkdown: base
.copyWith(
type: type,
clearMinBound: type == ChartType.pie,
clearMaxBound: type == ChartType.pie,
)
.toBlock(),
),
]);
} }
void _bump() => setState(() => _rev++); void _bump() => setState(() => _rev++);
@ -617,15 +481,6 @@ class _ChartEditorState extends State<ChartEditor> {
}, },
), ),
const Spacer(), const Spacer(),
if (widget.onAddVariants != null) ...[
TextButton.icon(
key: const ValueKey('chart-create-variants'),
onPressed: _createVariants,
icon: const Icon(Icons.auto_awesome_motion, size: 16),
label: Text(l10n.d('Varianten')),
),
const SizedBox(width: 4),
],
TextButton.icon( TextButton.icon(
onPressed: _importCsv, onPressed: _importCsv,
icon: const Icon(Icons.upload_file, size: 16), icon: const Icon(Icons.upload_file, size: 16),
@ -932,7 +787,9 @@ class _ChartEditorState extends State<ChartEditor> {
decimal: true, decimal: true,
signed: true, signed: true,
), ),
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.,\-]'))], inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9.,\-]')),
],
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
decoration: InputDecoration( decoration: InputDecoration(
labelText: label, labelText: label,

View file

@ -35,28 +35,17 @@ class _ImageSlideEditorState extends State<ImageSlideEditor> {
void _emit() => widget.onUpdate(widget.slide.copyWith(title: _title.text)); void _emit() => widget.onUpdate(widget.slide.copyWith(title: _title.text));
void _setImage(String path, {String caption = ''}) {
widget.onUpdate(
widget.slide.copyWith(
imagePath: path,
imageCaption: caption,
// A full-slide image should start at the largest uncropped size.
imageSize: 100,
),
);
}
Future<void> _pasteImage() async { Future<void> _pasteImage() async {
final path = await widget.imageService.pasteImage(); final path = await widget.imageService.pasteImage();
if (path != null) { if (path != null) {
_setImage(path); widget.onUpdate(widget.slide.copyWith(imagePath: path, imageCaption: ''));
} }
} }
Future<void> _pickImage() async { Future<void> _pickImage() async {
final path = await widget.imageService.pickImage(); final path = await widget.imageService.pickImage();
if (path != null) { if (path != null) {
_setImage(path); widget.onUpdate(widget.slide.copyWith(imagePath: path, imageCaption: ''));
} }
} }
@ -77,7 +66,9 @@ class _ImageSlideEditorState extends State<ImageSlideEditor> {
imageCaption: widget.slide.imageCaption, imageCaption: widget.slide.imageCaption,
searchPaths: widget.searchPaths, searchPaths: widget.searchPaths,
captionBasePath: widget.captionBasePath, captionBasePath: widget.captionBasePath,
onPicked: (path, caption) => _setImage(path, caption: caption), onPicked: (path, caption) => widget.onUpdate(
widget.slide.copyWith(imagePath: path, imageCaption: caption),
),
onBrowse: _pickImage, onBrowse: _pickImage,
onPaste: _pasteImage, onPaste: _pasteImage,
onClear: widget.slide.imagePath.isNotEmpty onClear: widget.slide.imagePath.isNotEmpty

View file

@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
import '../../l10n/app_localizations.dart';
import '../../models/slide.dart';
class ListStyleSelector extends StatelessWidget {
final ListStyle value;
final ValueChanged<ListStyle> onChanged;
const ListStyleSelector({
super.key,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return SegmentedButton<ListStyle>(
segments: [
ButtonSegment(
value: ListStyle.bullets,
icon: const Icon(Icons.format_list_bulleted, size: 18),
label: Text(l10n.d('Opsomming')),
),
ButtonSegment(
value: ListStyle.numbered,
icon: const Icon(Icons.format_list_numbered, size: 18),
label: Text(l10n.d('Nummering')),
),
ButtonSegment(
value: ListStyle.checklist,
icon: const Icon(Icons.checklist, size: 18),
label: Text(l10n.d('Checklist')),
),
],
selected: {value},
showSelectedIcon: false,
onSelectionChanged: (selection) => onChanged(selection.first),
);
}
}

View file

@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
import '../../models/slide.dart'; import '../../models/slide.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
import '_editor_field.dart'; import '_editor_field.dart';
import 'list_style_selector.dart';
typedef _Mutate = void Function(VoidCallback fn); typedef _Mutate = void Function(VoidCallback fn);
@ -27,8 +26,6 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
late final TextEditingController _heading2; late final TextEditingController _heading2;
late _BulletSet _left; late _BulletSet _left;
late _BulletSet _right; late _BulletSet _right;
late ListStyle _listStyle;
late bool _showChecklistProgress;
@override @override
void initState() { void initState() {
@ -39,8 +36,6 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
_heading2 = TextEditingController(text: widget.slide.columnTitle2); _heading2 = TextEditingController(text: widget.slide.columnTitle2);
_heading1.addListener(_emit); _heading1.addListener(_emit);
_heading2.addListener(_emit); _heading2.addListener(_emit);
_listStyle = widget.slide.listStyle;
_showChecklistProgress = widget.slide.showChecklistProgress;
_left = _BulletSet(widget.slide.bullets, _emit); _left = _BulletSet(widget.slide.bullets, _emit);
_right = _BulletSet(widget.slide.bullets2, _emit); _right = _BulletSet(widget.slide.bullets2, _emit);
} }
@ -51,10 +46,8 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
title: _title.text, title: _title.text,
columnTitle1: _heading1.text, columnTitle1: _heading1.text,
columnTitle2: _heading2.text, columnTitle2: _heading2.text,
listStyle: _listStyle, bullets: _left.values,
showChecklistProgress: _showChecklistProgress, bullets2: _right.values,
bullets: _left.values(_listStyle),
bullets2: _right.values(_listStyle),
), ),
); );
} }
@ -76,29 +69,6 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
children: [ children: [
EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'), EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'),
const SizedBox(height: 16), const SizedBox(height: 16),
ListStyleSelector(
value: _listStyle,
onChanged: (value) {
setState(() => _listStyle = value);
_emit();
},
),
if (_listStyle == ListStyle.checklist)
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.d('Voortgangsgrafiek tonen')),
subtitle: Text(
context.l10n.d(
'Toont afgevinkt en niet afgevinkt als percentages.',
),
),
value: _showChecklistProgress,
onChanged: (value) {
setState(() => _showChecklistProgress = value);
_emit();
},
),
const SizedBox(height: 16),
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final narrow = constraints.maxWidth < 560; final narrow = constraints.maxWidth < 560;
@ -108,14 +78,12 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
set: _left, set: _left,
emit: _emit, emit: _emit,
headingController: _heading1, headingController: _heading1,
listStyle: _listStyle,
), ),
_BulletColumn( _BulletColumn(
label: 'Bullets rechts', label: 'Bullets rechts',
set: _right, set: _right,
emit: _emit, emit: _emit,
headingController: _heading2, headingController: _heading2,
listStyle: _listStyle,
), ),
]; ];
if (narrow) { if (narrow) {
@ -144,26 +112,18 @@ class _BulletSet {
final VoidCallback emit; final VoidCallback emit;
late List<TextEditingController> controllers; late List<TextEditingController> controllers;
late List<int> levels; late List<int> levels;
late List<bool> checked;
late List<FocusNode> focusNodes; late List<FocusNode> focusNodes;
_BulletSet(List<String> raw, this.emit) { _BulletSet(List<String> raw, this.emit) {
final list = raw.isEmpty ? [''] : raw; final list = raw.isEmpty ? [''] : raw;
levels = list.map(_levelOf).toList(); levels = list.map(_levelOf).toList();
checked = list.map(checklistItemChecked).toList(); controllers = list.map((b) => _makeCtrl(b.trimLeft())).toList();
controllers = list.map((b) => _makeCtrl(checklistItemText(b))).toList();
focusNodes = List.generate(controllers.length, (_) => FocusNode()); focusNodes = List.generate(controllers.length, (_) => FocusNode());
} }
List<String> values(ListStyle listStyle) => List.generate( List<String> get values => List.generate(
controllers.length, controllers.length,
(i) => listStyle == ListStyle.checklist (i) => '\t' * levels[i] + controllers[i].text,
? checklistBullet(
level: levels[i],
text: controllers[i].text,
checked: checked[i],
)
: '\t' * levels[i] + controllers[i].text,
); );
static int _levelOf(String b) { static int _levelOf(String b) {
@ -184,7 +144,6 @@ class _BulletSet {
mutate(() { mutate(() {
controllers.insert(i + 1, _makeCtrl('')); controllers.insert(i + 1, _makeCtrl(''));
levels.insert(i + 1, levels[i]); levels.insert(i + 1, levels[i]);
checked.insert(i + 1, false);
focusNodes.insert(i + 1, FocusNode()); focusNodes.insert(i + 1, FocusNode());
}); });
emit(); emit();
@ -194,25 +153,13 @@ class _BulletSet {
} }
void removeAndFocus(_Mutate mutate, int i) { void removeAndFocus(_Mutate mutate, int i) {
if (controllers.length == 1) { if (controllers.length <= 1) return;
mutate(() {
controllers[i].removeListener(emit);
controllers[i].clear();
controllers[i].addListener(emit);
levels[i] = 0;
checked[i] = false;
});
emit();
focusNodes[i].requestFocus();
return;
}
final target = (i - 1).clamp(0, controllers.length - 2); final target = (i - 1).clamp(0, controllers.length - 2);
mutate(() { mutate(() {
controllers[i].removeListener(emit); controllers[i].removeListener(emit);
controllers[i].dispose(); controllers[i].dispose();
controllers.removeAt(i); controllers.removeAt(i);
levels.removeAt(i); levels.removeAt(i);
checked.removeAt(i);
focusNodes[i].dispose(); focusNodes[i].dispose();
focusNodes.removeAt(i); focusNodes.removeAt(i);
}); });
@ -251,7 +198,6 @@ class _BulletSet {
for (int j = 1; j < lines.length; j++) { for (int j = 1; j < lines.length; j++) {
controllers.insert(i + j, _makeCtrl(lines[j])); controllers.insert(i + j, _makeCtrl(lines[j]));
levels.insert(i + j, levels[i]); levels.insert(i + j, levels[i]);
checked.insert(i + j, false);
focusNodes.insert(i + j, FocusNode()); focusNodes.insert(i + j, FocusNode());
} }
}); });
@ -277,14 +223,12 @@ class _BulletColumn extends StatefulWidget {
final _BulletSet set; final _BulletSet set;
final VoidCallback emit; final VoidCallback emit;
final TextEditingController headingController; final TextEditingController headingController;
final ListStyle listStyle;
const _BulletColumn({ const _BulletColumn({
required this.label, required this.label,
required this.set, required this.set,
required this.emit, required this.emit,
required this.headingController, required this.headingController,
required this.listStyle,
}); });
@override @override
@ -331,25 +275,10 @@ class _BulletColumnState extends State<_BulletColumn> {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if (widget.listStyle == ListStyle.checklist) Text(
SizedBox( _markerForLevel(level),
width: 24, style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
height: 24, ),
child: Checkbox(
key: ValueKey('checklist-item-${widget.label}-$i'),
value: set.checked[i],
onChanged: (value) {
setState(() => set.checked[i] = value ?? false);
widget.emit();
},
visualDensity: VisualDensity.compact,
),
)
else
Text(
_markerForItem(i),
style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Focus( child: Focus(
@ -395,13 +324,14 @@ class _BulletColumnState extends State<_BulletColumn> {
), ),
), ),
IconButton( IconButton(
key: ValueKey('remove-bullet-${widget.label}-$i'),
icon: const Icon( icon: const Icon(
Icons.remove_circle_outline, Icons.remove_circle_outline,
size: 18, size: 18,
color: Color(0xFF94A3B8), color: Color(0xFF94A3B8),
), ),
onPressed: () => set.removeAndFocus((fn) => setState(fn), i), onPressed: set.controllers.length > 1
? () => set.removeAndFocus((fn) => setState(fn), i)
: null,
tooltip: l10n.d('Verwijder'), tooltip: l10n.d('Verwijder'),
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
constraints: const BoxConstraints(minWidth: 28), constraints: const BoxConstraints(minWidth: 28),
@ -415,18 +345,4 @@ class _BulletColumnState extends State<_BulletColumn> {
const markers = ['', '', '', '', '']; const markers = ['', '', '', '', ''];
return markers[level.clamp(0, markers.length - 1)]; return markers[level.clamp(0, markers.length - 1)];
} }
String _markerForItem(int index) {
if (widget.listStyle == ListStyle.bullets) {
return _markerForLevel(set.levels[index]);
}
if (widget.listStyle == ListStyle.checklist) return '';
final level = set.levels[index];
var number = 0;
for (var i = 0; i <= index; i++) {
if (set.levels[i] == level) number++;
if (set.levels[i] < level) number = 0;
}
return '$number.';
}
} }

View file

@ -102,13 +102,6 @@ class EditorPanel extends ConsumerWidget {
imgService, imgService,
searchPaths, searchPaths,
deck.projectPath, deck.projectPath,
(variants) {
final first = deckNotifier.insertSlides(
variants,
afterIndex: idx,
);
if (first >= 0) editorNotifier.select(first);
},
), ),
), ),
if (slide.type != SlideType.video) ...[ if (slide.type != SlideType.video) ...[
@ -167,8 +160,6 @@ class EditorPanel extends ConsumerWidget {
bullets2: newType == SlideType.twoBullets bullets2: newType == SlideType.twoBullets
? (slide.bullets2.isNotEmpty ? slide.bullets2 : ['']) ? (slide.bullets2.isNotEmpty ? slide.bullets2 : [''])
: const [], : const [],
listStyle: slide.listStyle,
showChecklistProgress: slide.showChecklistProgress,
imagePath: keepsImage ? slide.imagePath : '', imagePath: keepsImage ? slide.imagePath : '',
imagePath2: newType == SlideType.twoImages ? slide.imagePath2 : '', imagePath2: newType == SlideType.twoImages ? slide.imagePath2 : '',
imageCaption: keepsImage ? slide.imageCaption : '', imageCaption: keepsImage ? slide.imageCaption : '',
@ -206,7 +197,6 @@ class EditorPanel extends ConsumerWidget {
ImageService imgService, ImageService imgService,
List<String> searchPaths, List<String> searchPaths,
String? captionBasePath, String? captionBasePath,
ValueChanged<List<Slide>> onAddChartVariants,
) { ) {
switch (slide.type) { switch (slide.type) {
case SlideType.title: case SlideType.title:
@ -299,7 +289,6 @@ class EditorPanel extends ConsumerWidget {
key: ValueKey(slide.id), key: ValueKey(slide.id),
slide: slide, slide: slide,
onUpdate: onUpdate, onUpdate: onUpdate,
onAddVariants: onAddChartVariants,
projectPath: captionBasePath, projectPath: captionBasePath,
); );
} }

View file

@ -103,16 +103,6 @@ class _AudienceWindowAppState extends State<AudienceWindowApp> {
? null ? null
: Offset((pt[0] as num).toDouble(), (pt[1] as num).toDouble()); : Offset((pt[0] as num).toDouble(), (pt[1] as num).toDouble());
}); });
case 'checklistUpdate':
final m = Map<String, dynamic>.from(call.arguments as Map);
final i = (m['slideIndex'] as num?)?.toInt();
if (i == null || i < 0 || i >= _slides.length || !mounted) return null;
setState(() {
_slides[i] = _slides[i].copyWith(
bullets: List<String>.from(m['bullets'] as List? ?? const []),
bullets2: List<String>.from(m['bullets2'] as List? ?? const []),
);
});
case 'close': case 'close':
try { try {
final self = await WindowController.fromCurrentEngine(); final self = await WindowController.fromCurrentEngine();
@ -122,9 +112,9 @@ class _AudienceWindowAppState extends State<AudienceWindowApp> {
return null; return null;
} }
void _send(String method, [Object? arguments]) { void _send(String method) {
// Best-effort: the presenter may already be gone. // Best-effort: the presenter may already be gone.
presenterChannel.invokeMethod(method, arguments).catchError((_) => null); presenterChannel.invokeMethod(method).catchError((_) => null);
} }
@override @override
@ -179,23 +169,11 @@ class _AudienceWindowAppState extends State<AudienceWindowApp> {
slideCount: _slides.length, slideCount: _slides.length,
tlp: _tlp, tlp: _tlp,
presentationMode: true, presentationMode: true,
onChecklistItemToggle: (column, itemIndex) =>
_send('checklistToggle', {
'slideIndex': _index,
'column': column,
'itemIndex': itemIndex,
}),
enableMedia: true, enableMedia: true,
autoplayMedia: true, autoplayMedia: true,
// Media finishing on the beamer drives auto-advance. // Audio finishing on the beamer drives the presenter's
onAudioComplete: () => _send('mediaComplete', { // auto-advance.
'index': _index, onAudioComplete: () => _send('audioComplete'),
'kind': 'audio',
}),
onVideoComplete: () => _send('mediaComplete', {
'index': _index,
'kind': 'video',
}),
), ),
AnnotationLayer( AnnotationLayer(
strokes: _ink[_index] ?? const [], strokes: _ink[_index] ?? const [],

View file

@ -37,7 +37,6 @@ class FullscreenPresenter extends StatefulWidget {
/// made while presenting back to the deck. /// made while presenting back to the deck.
final Map<String, List<InkStroke>> initialAnnotations; final Map<String, List<InkStroke>> initialAnnotations;
final void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged; final void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged;
final ValueChanged<Slide>? onSlideChanged;
const FullscreenPresenter({ const FullscreenPresenter({
super.key, super.key,
@ -49,7 +48,6 @@ class FullscreenPresenter extends StatefulWidget {
this.audienceWindow, this.audienceWindow,
this.initialAnnotations = const {}, this.initialAnnotations = const {},
this.onAnnotationsChanged, this.onAnnotationsChanged,
this.onSlideChanged,
}); });
/// Entry point used by the app: pick dual-screen mode when a second display is /// Entry point used by the app: pick dual-screen mode when a second display is
@ -64,7 +62,6 @@ class FullscreenPresenter extends StatefulWidget {
TlpLevel tlp = TlpLevel.none, TlpLevel tlp = TlpLevel.none,
Map<String, List<InkStroke>> annotations = const {}, Map<String, List<InkStroke>> annotations = const {},
void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged, void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged,
ValueChanged<Slide>? onSlideChanged,
}) async { }) async {
var displayCount = 0; var displayCount = 0;
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
@ -92,7 +89,6 @@ class FullscreenPresenter extends StatefulWidget {
tlp: tlp, tlp: tlp,
annotations: annotations, annotations: annotations,
onAnnotationsChanged: onAnnotationsChanged, onAnnotationsChanged: onAnnotationsChanged,
onSlideChanged: onSlideChanged,
); );
} else { } else {
await show( await show(
@ -104,7 +100,6 @@ class FullscreenPresenter extends StatefulWidget {
tlp: tlp, tlp: tlp,
annotations: annotations, annotations: annotations,
onAnnotationsChanged: onAnnotationsChanged, onAnnotationsChanged: onAnnotationsChanged,
onSlideChanged: onSlideChanged,
); );
} }
} }
@ -118,7 +113,6 @@ class FullscreenPresenter extends StatefulWidget {
TlpLevel tlp = TlpLevel.none, TlpLevel tlp = TlpLevel.none,
Map<String, List<InkStroke>> annotations = const {}, Map<String, List<InkStroke>> annotations = const {},
void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged, void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged,
ValueChanged<Slide>? onSlideChanged,
}) async { }) async {
final hadWakeLock = await _wakeLockEnabled(); final hadWakeLock = await _wakeLockEnabled();
await _enableWakeLock(); await _enableWakeLock();
@ -137,7 +131,6 @@ class FullscreenPresenter extends StatefulWidget {
tlp: tlp, tlp: tlp,
initialAnnotations: annotations, initialAnnotations: annotations,
onAnnotationsChanged: onAnnotationsChanged, onAnnotationsChanged: onAnnotationsChanged,
onSlideChanged: onSlideChanged,
), ),
transitionsBuilder: (context, animation, secondary, child) => transitionsBuilder: (context, animation, secondary, child) =>
FadeTransition(opacity: animation, child: child), FadeTransition(opacity: animation, child: child),
@ -163,7 +156,6 @@ class FullscreenPresenter extends StatefulWidget {
TlpLevel tlp = TlpLevel.none, TlpLevel tlp = TlpLevel.none,
Map<String, List<InkStroke>> annotations = const {}, Map<String, List<InkStroke>> annotations = const {},
void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged, void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged,
ValueChanged<Slide>? onSlideChanged,
}) async { }) async {
// A self-contained markdown deck is the payload for the audience window; it // A self-contained markdown deck is the payload for the audience window; it
// carries the slides, the style profile and the TLP level in one string. // carries the slides, the style profile and the TLP level in one string.
@ -213,7 +205,6 @@ class FullscreenPresenter extends StatefulWidget {
tlp: tlp, tlp: tlp,
annotations: annotations, annotations: annotations,
onAnnotationsChanged: onAnnotationsChanged, onAnnotationsChanged: onAnnotationsChanged,
onSlideChanged: onSlideChanged,
); );
} }
return; return;
@ -236,7 +227,6 @@ class FullscreenPresenter extends StatefulWidget {
audienceWindow: audience, audienceWindow: audience,
initialAnnotations: annotations, initialAnnotations: annotations,
onAnnotationsChanged: onAnnotationsChanged, onAnnotationsChanged: onAnnotationsChanged,
onSlideChanged: onSlideChanged,
), ),
transitionsBuilder: (context, animation, secondary, child) => transitionsBuilder: (context, animation, secondary, child) =>
FadeTransition(opacity: animation, child: child), FadeTransition(opacity: animation, child: child),
@ -265,16 +255,6 @@ bool shouldUseDualScreen({
return (isMacOS || isWindows || isLinux) && displayCount >= 2; return (isMacOS || isWindows || isLinux) && displayCount >= 2;
} }
@visibleForTesting
bool autoAdvanceWaitsForMedia(Slide slide) {
final autoplayVideo =
slide.type == SlideType.video &&
slide.videoPath.isNotEmpty &&
slide.videoAutoplay;
final autoplayAudio = slide.audioPath.isNotEmpty && slide.audioAutoplay;
return autoplayVideo || autoplayAudio;
}
Future<bool> _wakeLockEnabled() async { Future<bool> _wakeLockEnabled() async {
try { try {
return await WakelockPlus.enabled; return await WakelockPlus.enabled;
@ -345,9 +325,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
/// laatste slide staan). Met L te wisselen. /// laatste slide staan). Met L te wisselen.
bool _loop = false; bool _loop = false;
/// Wissel het afspelen van autoplay-media i.p.v. op de tijdwissel. /// Wissel het afspelen van de audio op de slide i.p.v. op de tijdwissel.
/// Met M te wisselen. /// Met M te wisselen.
bool _advanceOnMediaEnd = true; bool _advanceOnAudioEnd = true;
/// Known displays for moving the fullscreen presentation window. This is not /// Known displays for moving the fullscreen presentation window. This is not
/// a second presenter window; it keeps the current output movable between /// a second presenter window; it keeps the current output movable between
@ -405,20 +385,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
case 'exit': case 'exit':
_exit(); _exit();
case 'audioComplete': case 'audioComplete':
_onMediaCompleted(kind: 'audio'); _onAudioCompleted();
case 'mediaComplete':
final args = Map<String, dynamic>.from(call.arguments as Map);
_onMediaCompleted(
index: (args['index'] as num?)?.toInt(),
kind: args['kind']?.toString(),
);
case 'checklistToggle':
final args = Map<String, dynamic>.from(call.arguments as Map);
_toggleChecklistItem(
slideIndex: (args['slideIndex'] as num?)?.toInt() ?? _index,
column: (args['column'] as num?)?.toInt() ?? 0,
itemIndex: (args['itemIndex'] as num?)?.toInt() ?? 0,
);
} }
return null; return null;
}); });
@ -464,38 +431,6 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
if (indexChanged) _pushInk(); if (indexChanged) _pushInk();
} }
void _toggleChecklistItem({
required int slideIndex,
required int column,
required int itemIndex,
}) {
if (slideIndex < 0 || slideIndex >= widget.slides.length) return;
final slide = widget.slides[slideIndex];
final source = column == 1 ? slide.bullets2 : slide.bullets;
if (itemIndex < 0 || itemIndex >= source.length) return;
final updatedItems = List<String>.from(source);
final item = updatedItems[itemIndex];
updatedItems[itemIndex] = checklistBullet(
level: bulletLevel(item),
text: checklistItemText(item),
checked: !checklistItemChecked(item),
);
final updated = column == 1
? slide.copyWith(bullets2: updatedItems)
: slide.copyWith(bullets: updatedItems);
setState(() => widget.slides[slideIndex] = updated);
widget.onSlideChanged?.call(updated);
if (_dual) {
audienceChannel
.invokeMethod('checklistUpdate', {
'slideIndex': slideIndex,
'bullets': updated.bullets,
'bullets2': updated.bullets2,
})
.catchError((_) => null);
}
}
// Annotatielaag // Annotatielaag
/// Send the current slide's strokes to the beamer (keyed by index there). /// Send the current slide's strokes to the beamer (keyed by index there).
@ -593,7 +528,12 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
final slide = widget.slides[_index.clamp(0, widget.slides.length - 1)]; final slide = widget.slides[_index.clamp(0, widget.slides.length - 1)];
if (_advanceOnMediaEnd && autoAdvanceWaitsForMedia(slide)) return; // Audio-gestuurd: heeft deze slide audio die vanzelf speelt én is de keuze
// 'na audio doorgaan' actief? Dan wachten we op het audio-einde (de
// _AudioPlayback meldt zich via onAudioComplete) en zetten we geen timer.
final audioDriven =
_advanceOnAudioEnd && slide.audioPath.isNotEmpty && slide.audioAutoplay;
if (audioDriven) return;
final dur = slide.advanceDuration; final dur = slide.advanceDuration;
if (dur <= 0) return; if (dur <= 0) return;
@ -619,7 +559,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
}); });
} }
/// Automatisch doorschakelen (tijd of media-einde): naar de volgende slide, /// Automatisch doorschakelen (tijd of audio-einde): naar de volgende slide,
/// of bij herhaling vanaf de laatste terug naar de eerste. Zonder herhaling /// of bij herhaling vanaf de laatste terug naar de eerste. Zonder herhaling
/// blijft de laatste slide gewoon staan. /// blijft de laatste slide gewoon staan.
void _autoAdvance() { void _autoAdvance() {
@ -633,20 +573,10 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
} }
} }
void _onMediaCompleted({int? index, String? kind}) { /// Aangeroepen door de audiospeler zodra de audio op de huidige slide klaar
if (index != null && index != _index) return; /// is. In automatische modus met 'na audio doorgaan' schakelen we dan door.
final slide = widget.slides[_index.clamp(0, widget.slides.length - 1)]; void _onAudioCompleted() {
// A video is primary on a video slide. Ignore an attached audio track that if (_autoPlay && _advanceOnAudioEnd) _autoAdvance();
// happens to finish earlier.
if (kind == 'audio' &&
slide.type == SlideType.video &&
slide.videoPath.isNotEmpty &&
slide.videoAutoplay) {
return;
}
if (_autoPlay && _advanceOnMediaEnd && autoAdvanceWaitsForMedia(slide)) {
_autoAdvance();
}
} }
void _toggleAutoPlay() { void _toggleAutoPlay() {
@ -659,8 +589,8 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
_scheduleAdvance(); _scheduleAdvance();
} }
void _toggleMediaAdvance() { void _toggleAudioAdvance() {
setState(() => _advanceOnMediaEnd = !_advanceOnMediaEnd); setState(() => _advanceOnAudioEnd = !_advanceOnAudioEnd);
_scheduleAdvance(); _scheduleAdvance();
} }
@ -962,7 +892,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
_toggleLoop(); _toggleLoop();
return KeyEventResult.handled; return KeyEventResult.handled;
case LogicalKeyboardKey.keyM: case LogicalKeyboardKey.keyM:
_toggleMediaAdvance(); _toggleAudioAdvance();
return KeyEventResult.handled; return KeyEventResult.handled;
case LogicalKeyboardKey.keyS: case LogicalKeyboardKey.keyS:
_cycleDisplay(); _cycleDisplay();
@ -1225,7 +1155,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
('R', l10n.d('Verstreken tijd resetten')), ('R', l10n.d('Verstreken tijd resetten')),
('A', l10n.d('Automatische modus aan/uit')), ('A', l10n.d('Automatische modus aan/uit')),
('L', l10n.d('Herhalen (loop) aan/uit')), ('L', l10n.d('Herhalen (loop) aan/uit')),
('M', l10n.d('Na media automatisch doorgaan')), ('M', l10n.d('Na audio automatisch doorgaan')),
('H', l10n.d('Deze legenda')), ('H', l10n.d('Deze legenda')),
('Esc', l10n.d('Terug / afsluiten')), ('Esc', l10n.d('Terug / afsluiten')),
]; ];
@ -1359,20 +1289,13 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
slideCount: widget.slides.length, slideCount: widget.slides.length,
tlp: widget.tlp, tlp: widget.tlp,
presentationMode: true, presentationMode: true,
onChecklistItemToggle: (column, itemIndex) =>
_toggleChecklistItem(
slideIndex: _index,
column: column,
itemIndex: itemIndex,
),
// Tijdens het presenteren speelt media en starten audio/video // Tijdens het presenteren speelt media en starten audio/video
// vanzelf; het media-einde stuurt auto-advance aan. In dual- // vanzelf; het audio-einde stuurt de auto-advance aan. In dual-
// schermmodus speelt de media op het beamervenster, niet hier, // schermmodus speelt de media op het beamervenster, niet hier,
// anders zou het geluid dubbel klinken. // anders zou het geluid dubbel klinken.
enableMedia: !_dual, enableMedia: !_dual,
autoplayMedia: !_dual, autoplayMedia: !_dual,
onAudioComplete: () => _onMediaCompleted(kind: 'audio'), onAudioComplete: _onAudioCompleted,
onVideoComplete: () => _onMediaCompleted(kind: 'video'),
), ),
// Annotatielaag bovenop de dia. Laat klikken door wanneer er // Annotatielaag bovenop de dia. Laat klikken door wanneer er
// geen gereedschap actief is (zodat tikken blijft doorbladeren). // geen gereedschap actief is (zodat tikken blijft doorbladeren).

File diff suppressed because it is too large Load diff

View file

@ -1,286 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:ocideck/models/settings.dart';
import 'package:ocideck/models/slide.dart';
import 'package:ocideck/widgets/editors/bullets_editor.dart';
import 'package:ocideck/widgets/slides/inline_markdown.dart';
import 'package:ocideck/widgets/slides/slide_preview.dart';
void main() {
testWidgets('checklist items can be marked as checked', (tester) async {
var updated = Slide.create(SlideType.bullets).copyWith(bullets: ['Taak']);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BulletsEditor(
slide: updated,
onUpdate: (slide) => updated = slide,
),
),
),
);
await tester.tap(find.text('Checklist'));
await tester.pump();
expect(updated.listStyle, ListStyle.checklist);
expect(updated.bullets, ['[ ] Taak']);
await tester.tap(find.byKey(const ValueKey('checklist-item-0')));
await tester.pump();
expect(updated.bullets, ['[x] Taak']);
});
testWidgets('removing the final bullet leaves an empty bullet', (
tester,
) async {
var updated = Slide.create(
SlideType.bullets,
).copyWith(bullets: ['Enige bullet']);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BulletsEditor(
slide: updated,
onUpdate: (slide) => updated = slide,
),
),
),
);
await tester.tap(find.byKey(const ValueKey('remove-bullet-0')));
await tester.pump();
expect(updated.bullets, ['']);
expect(find.byType(TextField), findsNWidgets(3));
});
testWidgets('removing the final checklist item also resets its state', (
tester,
) async {
var updated = Slide.create(
SlideType.bullets,
).copyWith(bullets: ['\t[x] Afgerond'], listStyle: ListStyle.checklist);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BulletsEditor(
slide: updated,
onUpdate: (slide) => updated = slide,
),
),
),
);
await tester.tap(find.byKey(const ValueKey('remove-bullet-0')));
await tester.pump();
expect(updated.bullets, ['[ ] ']);
expect(
tester
.widget<Checkbox>(find.byKey(const ValueKey('checklist-item-0')))
.value,
isFalse,
);
});
testWidgets('checked items render checked and struck through', (
tester,
) async {
final slide = Slide.create(SlideType.bullets).copyWith(
bullets: ['[x] Klaar', '[ ] Open'],
listStyle: ListStyle.checklist,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 800,
height: 450,
child: SlidePreviewWidget(slide: slide),
),
),
),
);
expect(find.text(''), findsOneWidget);
expect(find.text(''), findsOneWidget);
final checked = tester.widget<InlineMarkdownText>(
find.byWidgetPredicate(
(widget) => widget is InlineMarkdownText && widget.text == 'Klaar',
),
);
expect(checked.style.decoration, TextDecoration.lineThrough);
});
testWidgets('checked item strike-through follows the style profile', (
tester,
) async {
final slide = Slide.create(
SlideType.bullets,
).copyWith(bullets: ['[x] Klaar'], listStyle: ListStyle.checklist);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 800,
height: 450,
child: SlidePreviewWidget(
slide: slide,
themeProfile: const ThemeProfile(checklistStrikeThrough: false),
),
),
),
),
);
final checked = tester.widget<InlineMarkdownText>(
find.byWidgetPredicate(
(widget) => widget is InlineMarkdownText && widget.text == 'Klaar',
),
);
expect(checked.style.decoration, isNull);
});
testWidgets('checklist progress chart can be enabled', (tester) async {
var updated = Slide.create(SlideType.bullets).copyWith(
bullets: ['[x] Klaar', '[ ] Open'],
listStyle: ListStyle.checklist,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BulletsEditor(
slide: updated,
onUpdate: (slide) => updated = slide,
),
),
),
);
expect(find.text('Voortgangsgrafiek tonen'), findsOneWidget);
await tester.tap(find.byType(Switch));
await tester.pump();
expect(updated.showChecklistProgress, isTrue);
});
testWidgets('checklist progress chart shows checked percentages', (
tester,
) async {
final slide = Slide.create(SlideType.bullets).copyWith(
bullets: ['[x] Klaar', '[ ] Open'],
listStyle: ListStyle.checklist,
showChecklistProgress: true,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 800,
height: 450,
child: SlidePreviewWidget(
slide: slide,
themeProfile: const ThemeProfile(
checklistCheckedColor: '#00AA00',
checklistUncheckedColor: '#CC0000',
),
),
),
),
),
);
expect(find.text('Afgevinkt 50%'), findsOneWidget);
expect(find.text('Niet afgevinkt 50%'), findsOneWidget);
expect(
find.byKey(const ValueKey('checklist-progress-pie')),
findsOneWidget,
);
expect(
tester
.getSize(find.byKey(const ValueKey('checklist-progress-pie')))
.width,
greaterThan(200),
);
final pie = tester.widget<PieChart>(
find.byKey(const ValueKey('checklist-progress-pie')),
);
expect(pie.data.sections[0].color, const Color(0xFF00AA00));
expect(pie.data.sections[1].color, const Color(0xFFCC0000));
});
testWidgets('presented checklist items can be toggled', (tester) async {
Slide? updated;
final slide = Slide.create(
SlideType.bullets,
).copyWith(bullets: ['[ ] Open'], listStyle: ListStyle.checklist);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 800,
height: 450,
child: SlidePreviewWidget(
slide: slide,
presentationMode: true,
onChecklistItemToggle: (column, itemIndex) {
updated = slide.copyWith(bullets: ['[x] Open']);
},
),
),
),
),
);
await tester.tap(
find.byKey(const ValueKey('checklist-preview-toggle-0-0')),
);
expect(updated?.bullets, ['[x] Open']);
});
testWidgets('hovering progress highlights matching checklist items', (
tester,
) async {
final slide = Slide.create(SlideType.bullets).copyWith(
bullets: ['[x] Klaar', '[ ] Open'],
listStyle: ListStyle.checklist,
showChecklistProgress: true,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
width: 800,
height: 450,
child: SlidePreviewWidget(
slide: slide,
presentationMode: true,
onChecklistItemToggle: (_, _) {},
),
),
),
),
);
final checkedRow = find.byKey(const ValueKey('checklist-preview-item-0-0'));
var container = tester.widget<AnimatedContainer>(checkedRow);
expect((container.decoration as BoxDecoration).color, Colors.transparent);
final checkedSegment = tester.widget<MouseRegion>(
find.byKey(const ValueKey('checklist-progress-checked')),
);
checkedSegment.onEnter!(const PointerEnterEvent());
await tester.pumpAndSettle();
container = tester.widget<AnimatedContainer>(checkedRow);
expect(
(container.decoration as BoxDecoration).color,
isNot(Colors.transparent),
);
});
}

View file

@ -4,21 +4,13 @@ import 'package:ocideck/models/chart.dart';
import 'package:ocideck/models/slide.dart'; import 'package:ocideck/models/slide.dart';
import 'package:ocideck/widgets/editors/chart_editor.dart'; import 'package:ocideck/widgets/editors/chart_editor.dart';
Widget _host( Widget _host(Slide slide, ValueChanged<Slide> onUpdate) {
Slide slide,
ValueChanged<Slide> onUpdate, {
ValueChanged<List<Slide>>? onAddVariants,
}) {
return MaterialApp( return MaterialApp(
home: Scaffold( home: Scaffold(
body: SizedBox( body: SizedBox(
width: 900, width: 900,
height: 650, height: 650,
child: ChartEditor( child: ChartEditor(slide: slide, onUpdate: onUpdate),
slide: slide,
onUpdate: onUpdate,
onAddVariants: onAddVariants,
),
), ),
), ),
); );
@ -144,7 +136,10 @@ void main() {
expect(find.byKey(const ValueKey('chart-min-bound')), findsOneWidget); expect(find.byKey(const ValueKey('chart-min-bound')), findsOneWidget);
expect(find.byKey(const ValueKey('chart-max-bound')), findsOneWidget); expect(find.byKey(const ValueKey('chart-max-bound')), findsOneWidget);
await tester.enterText(find.byKey(const ValueKey('chart-max-bound')), '20'); await tester.enterText(
find.byKey(const ValueKey('chart-max-bound')),
'20',
);
await tester.pump(); await tester.pump();
expect(ChartSpec.parse(updated.customMarkdown).maxBound, 20); expect(ChartSpec.parse(updated.customMarkdown).maxBound, 20);
@ -168,39 +163,4 @@ void main() {
expect(find.byKey(const ValueKey('chart-min-bound')), findsNothing); expect(find.byKey(const ValueKey('chart-min-bound')), findsNothing);
expect(find.byKey(const ValueKey('chart-max-bound')), findsNothing); expect(find.byKey(const ValueKey('chart-max-bound')), findsNothing);
}); });
testWidgets('chart variants reuse data in the chosen order', (tester) async {
const spec = ChartSpec(
type: ChartType.bar,
title: 'Omzet',
x: ['A', 'B'],
series: [
ChartSeries(name: 'Waarde', data: [10, 20], color: '#003399'),
],
);
final slide = Slide.create(
SlideType.chart,
).copyWith(customMarkdown: spec.toBlock());
List<Slide>? variants;
await tester.pumpWidget(
_host(slide, (_) {}, onAddVariants: (value) => variants = value),
);
await tester.tap(find.byKey(const ValueKey('chart-create-variants')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey('chart-variant-down-0')));
await tester.pump();
await tester.tap(find.text('Slides toevoegen'));
await tester.pump();
final specs = variants!.map((s) => ChartSpec.parse(s.customMarkdown));
expect(specs.map((s) => s.type), [
ChartType.pie,
ChartType.line,
ChartType.radar,
]);
expect(specs.first.x, ['A', 'B']);
expect(specs.first.series.single.data, [10, 20]);
expect(specs.first.series.single.color, '#003399');
});
} }

View file

@ -291,11 +291,7 @@ void main() {
final radar = tester.widget<RadarChart>(find.byType(RadarChart)); final radar = tester.widget<RadarChart>(find.byType(RadarChart));
// Two visible series plus one invisible scale anchor. // Two visible series plus one invisible scale anchor.
expect(radar.data.dataSets.length, 3); expect(radar.data.dataSets.length, 3);
expect(radar.data.dataSets.first.dataEntries.map((e) => e.value), [ expect(radar.data.dataSets.first.dataEntries.map((e) => e.value), [3, 4, 5]);
3,
4,
5,
]);
expect(radar.data.dataSets.last.fillColor, Colors.transparent); expect(radar.data.dataSets.last.fillColor, Colors.transparent);
// The spoke labels are supplied through getTitle (canvas-painted). // The spoke labels are supplied through getTitle (canvas-painted).
expect(radar.data.getTitle!(0, 0).text, 'Snelheid'); expect(radar.data.getTitle!(0, 0).text, 'Snelheid');
@ -306,43 +302,6 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
testWidgets('long radar labels stay outside the diagram and each other', (
tester,
) async {
const spec = ChartSpec(
type: ChartType.radar,
x: [
'Strategische wendbaarheid',
'Operationele betrouwbaarheid',
'Klantgerichte innovatie',
'Duurzame inzetbaarheid',
'Digitale volwassenheid',
'Financiële weerbaarheid',
],
series: [
ChartSeries(name: 'Score', data: [3, 4, 5, 2, 4, 3]),
],
);
await tester.pumpWidget(_host(spec, presentationMode: true));
await tester.pump();
final radarRect = tester.getRect(find.byType(RadarChart));
final labelRects = [
for (var i = 0; i < spec.x.length; i++)
tester.getRect(find.byKey(ValueKey('radar-axis-label-$i'))),
];
for (final rect in labelRects) {
expect(rect.overlaps(radarRect), isFalse);
}
for (var i = 0; i < labelRects.length; i++) {
for (var j = i + 1; j < labelRects.length; j++) {
expect(labelRects[i].overlaps(labelRects[j]), isFalse);
}
}
expect(tester.takeException(), isNull);
});
testWidgets('radar honours an explicit min/max scale with even ticks', ( testWidgets('radar honours an explicit min/max scale with even ticks', (
tester, tester,
) async { ) async {

View file

@ -137,74 +137,6 @@ void main() {
expect(n.state.deck!.slides.first.title, 'Nieuw'); expect(n.state.deck!.slides.first.title, 'Nieuw');
}); });
test('clearAllChecklists unchecks every checklist item across the deck', () {
final n = _notifier()..newDeck('D');
final s1 = Slide.create(SlideType.bullets).copyWith(
listStyle: ListStyle.checklist,
bullets: ['[x] Klaar', '\t[X] Subklaar', '[ ] Open'],
);
final s2 = Slide.create(SlideType.bullets).copyWith(
listStyle: ListStyle.checklist,
bullets: ['[x] Eerste kolom'],
bullets2: ['[x] Tweede kolom', '[ ] Nog open'],
);
n.loadDeck(n.state.deck!.copyWith(slides: [s1, s2]));
expect(n.checkedChecklistCount, 4);
final revisionBefore = n.state.revision;
n.clearAllChecklists();
expect(n.checkedChecklistCount, 0);
final out = n.state.deck!.slides;
expect(out[0].bullets, ['[ ] Klaar', '\t[ ] Subklaar', '[ ] Open']);
expect(out[1].bullets, ['[ ] Eerste kolom']);
expect(out[1].bullets2, ['[ ] Tweede kolom', '[ ] Nog open']);
// Revision bumps so the open slide editor remounts and reflects the change.
expect(n.state.revision, greaterThan(revisionBefore));
});
test('clearAllChecklists is a single undoable step that restores the checks', () {
final n = _notifier()..newDeck('D');
final slide = Slide.create(SlideType.bullets).copyWith(
listStyle: ListStyle.checklist,
bullets: ['[x] Klaar', '[ ] Open'],
bullets2: ['[x] Tweede'],
);
n.loadDeck(n.state.deck!.copyWith(slides: [slide]));
expect(n.checkedChecklistCount, 2);
n.clearAllChecklists();
expect(n.checkedChecklistCount, 0);
expect(n.state.canUndo, isTrue);
final revisionAfterClear = n.state.revision;
n.undo();
// One undo restores every checked item in both columns...
expect(n.checkedChecklistCount, 2);
expect(n.state.deck!.slides.first.bullets, ['[x] Klaar', '[ ] Open']);
expect(n.state.deck!.slides.first.bullets2, ['[x] Tweede']);
// ...and bumps the revision again so the open editor reflects the restore.
expect(n.state.revision, greaterThan(revisionAfterClear));
});
test('clearAllChecklists is a no-op when nothing is checked', () {
final n = _notifier()..newDeck('D');
final slide = Slide.create(SlideType.bullets).copyWith(
listStyle: ListStyle.checklist,
bullets: ['[ ] Open'],
);
n.loadDeck(n.state.deck!.copyWith(slides: [slide]));
expect(n.state.canUndo, isFalse);
n.clearAllChecklists();
// No checked items, so no history entry is recorded.
expect(n.state.canUndo, isFalse);
expect(n.checkedChecklistCount, 0);
});
test('updateMeta changes deck title/theme/paginate', () { test('updateMeta changes deck title/theme/paginate', () {
final n = _notifier()..newDeck('D'); final n = _notifier()..newDeck('D');
n.updateMeta(title: 'Andere titel', theme: 'custom', paginate: false); n.updateMeta(title: 'Andere titel', theme: 'custom', paginate: false);

View file

@ -75,91 +75,6 @@ void main() {
); );
}); });
test('autoplay audio or video takes precedence over slide timing', () {
expect(
autoAdvanceWaitsForMedia(
Slide.create(SlideType.bullets).copyWith(
advanceDuration: 3,
audioPath: 'sound.mp3',
audioAutoplay: true,
),
),
isTrue,
);
expect(
autoAdvanceWaitsForMedia(
Slide.create(SlideType.video).copyWith(
advanceDuration: 3,
videoPath: 'movie.mp4',
videoAutoplay: true,
),
),
isTrue,
);
expect(
autoAdvanceWaitsForMedia(
Slide.create(SlideType.video).copyWith(
advanceDuration: 3,
videoPath: 'movie.mp4',
videoAutoplay: false,
),
),
isFalse,
);
});
testWidgets('slide timer does not interrupt autoplay media', (tester) async {
final mediaSlides = [
Slide.create(SlideType.video).copyWith(
title: 'Video blijft staan',
videoPath: '/tmp/does-not-exist.mp4',
videoAutoplay: true,
advanceDuration: 3,
),
Slide.create(SlideType.bullets).copyWith(title: 'Na video'),
];
await tester.pumpWidget(_host(mediaSlides));
await tester.pump(const Duration(seconds: 4));
expect(find.text('Video blijft staan'), findsOneWidget);
expect(find.text('Na video'), findsNothing);
await tester.pumpWidget(const SizedBox());
});
testWidgets('checklist changes during presenting are persisted', (
tester,
) async {
Slide? updated;
final checklistSlides = [
Slide.create(SlideType.bullets).copyWith(
title: 'Taken',
bullets: ['[ ] Live afvinken'],
listStyle: ListStyle.checklist,
),
];
await tester.pumpWidget(
MaterialApp(
home: FullscreenPresenter(
slides: checklistSlides,
projectPath: null,
themeProfile: const ThemeProfile(),
initialIndex: 0,
onSlideChanged: (slide) => updated = slide,
),
),
);
await tester.pump();
await tester.tap(
find.byKey(const ValueKey('checklist-preview-toggle-0-0')),
);
await tester.pump();
expect(updated?.bullets, ['[x] Live afvinken']);
expect(find.text(''), findsOneWidget);
});
testWidgets('starts in audience view without presenter chrome', ( testWidgets('starts in audience view without presenter chrome', (
tester, tester,
) async { ) async {

View file

@ -1,71 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ocideck/models/slide.dart';
import 'package:ocideck/services/image_service.dart';
import 'package:ocideck/widgets/editors/_editor_field.dart';
import 'package:ocideck/widgets/editors/image_slide_editor.dart';
class _FakeImageService extends ImageService {
final String? pastedPath;
_FakeImageService({this.pastedPath});
@override
Future<String?> pasteImage() async => pastedPath;
}
Widget _host(
Slide slide,
ImageService imageService,
ValueChanged<Slide> onUpdate,
) {
return ProviderScope(
child: MaterialApp(
home: Scaffold(
body: ImageSlideEditor(
slide: slide,
onUpdate: onUpdate,
imageService: imageService,
),
),
),
);
}
void main() {
testWidgets('pasted full-slide image starts fully visible', (tester) async {
var updated = Slide.create(SlideType.image);
await tester.pumpWidget(
_host(
updated,
_FakeImageService(pastedPath: '/tmp/pasted.png'),
(slide) => updated = slide,
),
);
await tester.tap(find.byIcon(Icons.content_paste));
await tester.pump();
expect(updated.imagePath, '/tmp/pasted.png');
expect(updated.imageSize, 100);
});
testWidgets('carousel selection resets an old crop to fully visible', (
tester,
) async {
var updated = Slide.create(
SlideType.image,
).copyWith(imagePath: 'old.png', imageSize: 160);
await tester.pumpWidget(
_host(updated, _FakeImageService(), (slide) => updated = slide),
);
final picker = tester.widget<ImagePickerBar>(find.byType(ImagePickerBar));
picker.onPicked('new.png', 'Bijschrift');
expect(updated.imagePath, 'new.png');
expect(updated.imageCaption, 'Bijschrift');
expect(updated.imageSize, 100);
});
}

View file

@ -90,51 +90,6 @@ void main() {
expect(out.bullets, ['Punt een', 'Punt twee']); expect(out.bullets, ['Punt een', 'Punt twee']);
}); });
test('numbered list style round-trips', () {
final service = MarkdownService();
final markdown = service.generateDeck(
Deck(
title: 'Demo',
slides: [
Slide.create(SlideType.bullets).copyWith(
title: 'Stappen',
bullets: ['Eerst', '\tDetail', 'Daarna'],
listStyle: ListStyle.numbered,
),
],
),
);
final out = service.parseDeck(markdown)!.slides.single;
expect(out.listStyle, ListStyle.numbered);
expect(out.bullets, ['Eerst', '\tDetail', 'Daarna']);
expect(markdown, contains('1. Eerst'));
expect(markdown, contains('2. Daarna'));
});
test('checklist style and checked items round-trip', () {
final service = MarkdownService();
final markdown = service.generateDeck(
Deck(
title: 'Demo',
slides: [
Slide.create(SlideType.bullets).copyWith(
title: 'Taken',
bullets: ['[x] Gedaan', '[ ] Nog doen', '\t[x] Subtaak'],
listStyle: ListStyle.checklist,
showChecklistProgress: true,
),
],
),
);
final out = service.parseDeck(markdown)!.slides.single;
expect(out.listStyle, ListStyle.checklist);
expect(out.showChecklistProgress, isTrue);
expect(out.bullets, ['[x] Gedaan', '[ ] Nog doen', '\t[x] Subtaak']);
expect(markdown, contains('- [x] Gedaan'));
expect(markdown, contains('- [ ] Nog doen'));
expect(markdown, contains('ocideck_checklist_progress: true'));
});
test('twoBullets slide keeps both bullet columns', () { test('twoBullets slide keeps both bullet columns', () {
final out = _roundTrip( final out = _roundTrip(
Slide.create(SlideType.twoBullets).copyWith( Slide.create(SlideType.twoBullets).copyWith(
@ -172,9 +127,7 @@ void main() {
Deck( Deck(
title: 'Demo', title: 'Demo',
slides: [ slides: [
Slide.create( Slide.create(SlideType.twoBullets).copyWith(bullets: ['A'], bullets2: ['B']),
SlideType.twoBullets,
).copyWith(bullets: ['A'], bullets2: ['B']),
], ],
), ),
); );
@ -184,24 +137,6 @@ void main() {
expect(out.columnTitle2, ''); expect(out.columnTitle2, '');
}); });
test('two-column checklist export respects disabled strike-through', () {
final service = MarkdownService();
final markdown = service.generateDeck(
Deck(
title: 'Demo',
themeProfile: const ThemeProfile(checklistStrikeThrough: false),
slides: [
Slide.create(SlideType.twoBullets).copyWith(
bullets: ['[x] Klaar'],
bullets2: ['[ ] Open'],
listStyle: ListStyle.checklist,
),
],
),
);
expect(markdown, isNot(contains('text-decoration:line-through')));
});
test('bulletsImage slide keeps bullets, image, size and caption', () { test('bulletsImage slide keeps bullets, image, size and caption', () {
final out = _roundTrip( final out = _roundTrip(
Slide.create(SlideType.bulletsImage).copyWith( Slide.create(SlideType.bulletsImage).copyWith(

View file

@ -29,18 +29,6 @@ void main() {
expect(back.codeFontFamily, 'Courier New'); expect(back.codeFontFamily, 'Courier New');
}); });
test('ThemeProfile round-trips checklist styling through JSON', () {
const profile = ThemeProfile(
checklistCheckedColor: '#00AA00',
checklistUncheckedColor: '#CC0000',
checklistStrikeThrough: false,
);
final back = ThemeProfile.fromJson(profile.toJson());
expect(back.checklistCheckedColor, '#00AA00');
expect(back.checklistUncheckedColor, '#CC0000');
expect(back.checklistStrikeThrough, isFalse);
});
test('ThemeProfile code styling defaults to the atom-one-dark look', () { test('ThemeProfile code styling defaults to the atom-one-dark look', () {
// Older decks without the fields fall back to the dark editor defaults. // Older decks without the fields fall back to the dark editor defaults.
final back = ThemeProfile.fromJson(const {'name': 'Legacy'}); final back = ThemeProfile.fromJson(const {'name': 'Legacy'});

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:ocideck/models/slide.dart'; import 'package:ocideck/models/slide.dart';
import 'package:ocideck/widgets/slides/inline_markdown.dart';
import 'package:ocideck/widgets/slides/slide_preview.dart'; import 'package:ocideck/widgets/slides/slide_preview.dart';
Widget _host(Slide slide) { Widget _host(Slide slide) {
@ -59,28 +58,6 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
testWidgets('two-bullet columns use the same font size', (tester) async {
final slide = Slide.create(SlideType.twoBullets).copyWith(
bullets: const ['Solo'],
bullets2: List.generate(14, (i) => 'Item $i'),
);
await tester.pumpWidget(_host(slide));
await tester.pump();
double sizeOf(String text) => tester
.widget<InlineMarkdownText>(
find.byWidgetPredicate(
(x) => x is InlineMarkdownText && x.text == text,
),
)
.style
.fontSize!;
expect(sizeOf('Solo'), sizeOf('Item 0'));
expect(tester.takeException(), isNull);
});
testWidgets('bullets slide renders an optional subheading below the title', ( testWidgets('bullets slide renders an optional subheading below the title', (
tester, tester,
) async { ) async {