Compare commits
No commits in common. "815f5f2ceeacc9a7415a266fb687ffcf19f35527" and "839474fa19ec43ae1a3e1b06b3c5112964f1f3f5" have entirely different histories.
815f5f2cee
...
839474fa19
27 changed files with 363 additions and 2682 deletions
|
|
@ -47,8 +47,6 @@ and the project aims to follow [Semantic Versioning](https://semver.org/).
|
|||
separate from the slide title.
|
||||
- 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.
|
||||
- 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
|
||||
images are precached and `gaplessPlayback` is enabled) — important for
|
||||
recording.
|
||||
|
|
|
|||
|
|
@ -2327,35 +2327,6 @@ const _dutchSourceStrings = {
|
|||
const _dutchSourceStringAdditions = {
|
||||
'en': {
|
||||
'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',
|
||||
'Broncode': 'Source code',
|
||||
'Bullet': 'Bullet',
|
||||
|
|
@ -2435,36 +2406,6 @@ const _dutchSourceStringAdditions = {
|
|||
},
|
||||
'it': {
|
||||
'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 l’ordine dopo la diapositiva corrente.',
|
||||
'Spider': 'Radar',
|
||||
'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.',
|
||||
|
|
@ -2697,35 +2638,6 @@ const _dutchSourceStringAdditions = {
|
|||
},
|
||||
'de': {
|
||||
'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',
|
||||
'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.',
|
||||
|
|
@ -2959,35 +2871,6 @@ const _dutchSourceStringAdditions = {
|
|||
},
|
||||
'fr': {
|
||||
'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',
|
||||
'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.',
|
||||
|
|
@ -3221,36 +3104,6 @@ const _dutchSourceStringAdditions = {
|
|||
},
|
||||
'es': {
|
||||
'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',
|
||||
'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.',
|
||||
|
|
@ -3484,35 +3337,6 @@ const _dutchSourceStringAdditions = {
|
|||
},
|
||||
'fy': {
|
||||
'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': 'Dia’s 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 dia’s brûke deselde gegevens, kleuren en titel. Kies mei de pylken de folchoarder nei de aktive dia.',
|
||||
'Spider': 'Spider',
|
||||
'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.',
|
||||
|
|
@ -3743,35 +3567,6 @@ const _dutchSourceStringAdditions = {
|
|||
},
|
||||
'pap': {
|
||||
'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',
|
||||
'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.',
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ class ThemeProfile {
|
|||
final String slideBackgroundColor;
|
||||
final String textColor;
|
||||
final String accentColor;
|
||||
final String checklistCheckedColor;
|
||||
final String checklistUncheckedColor;
|
||||
final bool checklistStrikeThrough;
|
||||
final String tableTextColor;
|
||||
final String tableHeaderTextColor;
|
||||
final String titleBackgroundColor;
|
||||
|
|
@ -53,9 +50,6 @@ class ThemeProfile {
|
|||
this.slideBackgroundColor = '#FFFFFF',
|
||||
this.textColor = '#222222',
|
||||
this.accentColor = '#2E7D64',
|
||||
this.checklistCheckedColor = '#2E7D64',
|
||||
this.checklistUncheckedColor = '#CBD5E1',
|
||||
this.checklistStrikeThrough = true,
|
||||
String? tableTextColor,
|
||||
this.tableHeaderTextColor = '#FFFFFF',
|
||||
this.titleBackgroundColor = '#1C2B47',
|
||||
|
|
@ -90,9 +84,6 @@ class ThemeProfile {
|
|||
String? slideBackgroundColor,
|
||||
String? textColor,
|
||||
String? accentColor,
|
||||
String? checklistCheckedColor,
|
||||
String? checklistUncheckedColor,
|
||||
bool? checklistStrikeThrough,
|
||||
String? tableTextColor,
|
||||
String? tableHeaderTextColor,
|
||||
String? titleBackgroundColor,
|
||||
|
|
@ -118,12 +109,6 @@ class ThemeProfile {
|
|||
slideBackgroundColor: slideBackgroundColor ?? this.slideBackgroundColor,
|
||||
textColor: textColor ?? this.textColor,
|
||||
accentColor: accentColor ?? this.accentColor,
|
||||
checklistCheckedColor:
|
||||
checklistCheckedColor ?? this.checklistCheckedColor,
|
||||
checklistUncheckedColor:
|
||||
checklistUncheckedColor ?? this.checklistUncheckedColor,
|
||||
checklistStrikeThrough:
|
||||
checklistStrikeThrough ?? this.checklistStrikeThrough,
|
||||
tableTextColor: tableTextColor ?? this.tableTextColor,
|
||||
tableHeaderTextColor: tableHeaderTextColor ?? this.tableHeaderTextColor,
|
||||
titleBackgroundColor: titleBackgroundColor ?? this.titleBackgroundColor,
|
||||
|
|
@ -153,9 +138,6 @@ class ThemeProfile {
|
|||
'name': name,
|
||||
'textColor': textColor,
|
||||
'accentColor': accentColor,
|
||||
'checklistCheckedColor': checklistCheckedColor,
|
||||
'checklistUncheckedColor': checklistUncheckedColor,
|
||||
'checklistStrikeThrough': checklistStrikeThrough,
|
||||
'tableTextColor': tableTextColor,
|
||||
'tableHeaderTextColor': tableHeaderTextColor,
|
||||
'titleBackgroundColor': titleBackgroundColor,
|
||||
|
|
@ -184,13 +166,6 @@ class ThemeProfile {
|
|||
name: json['name'] as String? ?? 'Standaard',
|
||||
textColor: json['textColor'] as String? ?? '#222222',
|
||||
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:
|
||||
json['tableTextColor'] as String? ??
|
||||
json['textColor'] as String? ??
|
||||
|
|
@ -202,7 +177,8 @@ class ThemeProfile {
|
|||
titleTextColor: json['titleTextColor'] as String? ?? '#FFFFFF',
|
||||
sectionBackgroundColor:
|
||||
json['sectionBackgroundColor'] as String? ?? '#2E7D64',
|
||||
codeBackgroundColor: json['codeBackgroundColor'] as String? ?? '#282C34',
|
||||
codeBackgroundColor:
|
||||
json['codeBackgroundColor'] as String? ?? '#282C34',
|
||||
codeTextColor: json['codeTextColor'] as String? ?? '#ABB2BF',
|
||||
codeHighlightSyntax: json['codeHighlightSyntax'] as bool? ?? true,
|
||||
codeFontFamily: json['codeFontFamily'] as String? ?? 'monospace',
|
||||
|
|
|
|||
|
|
@ -19,30 +19,6 @@ enum SlideType {
|
|||
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 {
|
||||
String get label {
|
||||
switch (this) {
|
||||
|
|
@ -114,8 +90,6 @@ class Slide {
|
|||
final String subtitle;
|
||||
final List<String> bullets;
|
||||
final List<String> bullets2;
|
||||
final ListStyle listStyle;
|
||||
final bool showChecklistProgress;
|
||||
|
||||
/// Optional headings above the two bullet columns (twoBullets only). Empty =
|
||||
/// no heading for that column.
|
||||
|
|
@ -154,8 +128,6 @@ class Slide {
|
|||
this.subtitle = '',
|
||||
this.bullets = const [],
|
||||
this.bullets2 = const [],
|
||||
this.listStyle = ListStyle.bullets,
|
||||
this.showChecklistProgress = false,
|
||||
this.columnTitle1 = '',
|
||||
this.columnTitle2 = '',
|
||||
this.imagePath = '',
|
||||
|
|
@ -211,8 +183,6 @@ class Slide {
|
|||
subtitle: src.subtitle,
|
||||
bullets: List<String>.from(src.bullets),
|
||||
bullets2: List<String>.from(src.bullets2),
|
||||
listStyle: src.listStyle,
|
||||
showChecklistProgress: src.showChecklistProgress,
|
||||
columnTitle1: src.columnTitle1,
|
||||
columnTitle2: src.columnTitle2,
|
||||
imagePath: src.imagePath,
|
||||
|
|
@ -245,8 +215,6 @@ class Slide {
|
|||
String? subtitle,
|
||||
List<String>? bullets,
|
||||
List<String>? bullets2,
|
||||
ListStyle? listStyle,
|
||||
bool? showChecklistProgress,
|
||||
String? columnTitle1,
|
||||
String? columnTitle2,
|
||||
String? imagePath,
|
||||
|
|
@ -278,9 +246,6 @@ class Slide {
|
|||
subtitle: subtitle ?? this.subtitle,
|
||||
bullets: bullets ?? this.bullets,
|
||||
bullets2: bullets2 ?? this.bullets2,
|
||||
listStyle: listStyle ?? this.listStyle,
|
||||
showChecklistProgress:
|
||||
showChecklistProgress ?? this.showChecklistProgress,
|
||||
columnTitle1: columnTitle1 ?? this.columnTitle1,
|
||||
columnTitle2: columnTitle2 ?? this.columnTitle2,
|
||||
imagePath: imagePath ?? this.imagePath,
|
||||
|
|
|
|||
|
|
@ -216,12 +216,10 @@ class MarkdownService {
|
|||
case SlideType.bullets:
|
||||
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
|
||||
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();
|
||||
_writeList(buf, slide.bullets, slide.listStyle);
|
||||
for (final b in slide.bullets) {
|
||||
_writeBullet(buf, b);
|
||||
}
|
||||
|
||||
case SlideType.twoBullets:
|
||||
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
|
||||
|
|
@ -232,9 +230,6 @@ class MarkdownService {
|
|||
slide.bullets2,
|
||||
slide.columnTitle1,
|
||||
slide.columnTitle2,
|
||||
slide.listStyle,
|
||||
slide.showChecklistProgress,
|
||||
themeProfile ?? const ThemeProfile(),
|
||||
);
|
||||
|
||||
case SlideType.bulletsImage:
|
||||
|
|
@ -253,12 +248,10 @@ class MarkdownService {
|
|||
);
|
||||
buf.writeln();
|
||||
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();
|
||||
_writeList(buf, slide.bullets, slide.listStyle);
|
||||
for (final b in slide.bullets) {
|
||||
_writeBullet(buf, b);
|
||||
}
|
||||
buf.writeln();
|
||||
buf.writeln('</div>');
|
||||
buf.writeln();
|
||||
|
|
@ -270,12 +263,10 @@ class MarkdownService {
|
|||
buf.writeln('</div>');
|
||||
} else {
|
||||
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();
|
||||
_writeList(buf, slide.bullets, slide.listStyle);
|
||||
for (final b in slide.bullets) {
|
||||
_writeBullet(buf, b);
|
||||
}
|
||||
}
|
||||
|
||||
case SlideType.twoImages:
|
||||
|
|
@ -410,35 +401,14 @@ class MarkdownService {
|
|||
return buf.toString();
|
||||
}
|
||||
|
||||
static void _writeList(
|
||||
StringBuffer buf,
|
||||
List<String> items,
|
||||
ListStyle style,
|
||||
) {
|
||||
final counters = <int>[];
|
||||
for (final item in items) {
|
||||
static void _writeBullet(StringBuffer buf, String bullet) {
|
||||
int level = 0;
|
||||
while (level < item.length && item[level] == '\t') {
|
||||
while (level < bullet.length && bullet[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');
|
||||
final text = bullet.substring(level);
|
||||
if (text.isNotEmpty) {
|
||||
buf.writeln('${' ' * level}- $text');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -448,18 +418,9 @@ class MarkdownService {
|
|||
List<String> right,
|
||||
String leftTitle,
|
||||
String rightTitle,
|
||||
ListStyle listStyle,
|
||||
bool showChecklistProgress,
|
||||
ThemeProfile themeProfile,
|
||||
) {
|
||||
buf.writeln('<!-- ocideck_two_bullets_left: ${_encodeBullets(left)} -->');
|
||||
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) {
|
||||
buf.writeln(
|
||||
'<!-- ocideck_two_bullets_left_title: ${_encodeText(leftTitle)} -->',
|
||||
|
|
@ -473,23 +434,15 @@ class MarkdownService {
|
|||
buf.writeln(
|
||||
'<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, right, rightTitle, listStyle, themeProfile);
|
||||
_writeBulletColumn(buf, left, leftTitle);
|
||||
_writeBulletColumn(buf, right, rightTitle);
|
||||
buf.writeln('</div>');
|
||||
}
|
||||
|
||||
static void _writeChecklistProgress(StringBuffer buf, Slide slide) {
|
||||
if (slide.showChecklistProgress) {
|
||||
buf.writeln('<!-- ocideck_checklist_progress: true -->');
|
||||
}
|
||||
}
|
||||
|
||||
static void _writeBulletColumn(
|
||||
StringBuffer buf,
|
||||
List<String> bullets,
|
||||
String columnTitle,
|
||||
ListStyle listStyle,
|
||||
ThemeProfile themeProfile,
|
||||
) {
|
||||
buf.writeln('<div>');
|
||||
if (columnTitle.isNotEmpty) {
|
||||
|
|
@ -497,10 +450,9 @@ class MarkdownService {
|
|||
'<h3 style="margin:0 0 .5rem;">${_escapeHtml(columnTitle)}</h3>',
|
||||
);
|
||||
}
|
||||
final tag = listStyle == ListStyle.numbered ? 'ol' : 'ul';
|
||||
buf.writeln('<$tag style="margin:0; padding-left:1.3em;">');
|
||||
_writeHtmlBulletItems(buf, bullets, listStyle, themeProfile);
|
||||
buf.writeln('</$tag>');
|
||||
buf.writeln('<ul style="margin:0; padding-left:1.3em;">');
|
||||
_writeHtmlBulletItems(buf, bullets);
|
||||
buf.writeln('</ul>');
|
||||
buf.writeln('</div>');
|
||||
}
|
||||
|
||||
|
|
@ -528,41 +480,16 @@ class MarkdownService {
|
|||
return const [];
|
||||
}
|
||||
|
||||
static void _writeHtmlBulletItems(
|
||||
StringBuffer buf,
|
||||
List<String> bullets,
|
||||
ListStyle listStyle,
|
||||
ThemeProfile themeProfile,
|
||||
) {
|
||||
final counters = <int>[];
|
||||
static void _writeHtmlBulletItems(StringBuffer buf, List<String> bullets) {
|
||||
for (final b in bullets) {
|
||||
int level = 0;
|
||||
while (level < b.length && b[level] == '\t') {
|
||||
level++;
|
||||
}
|
||||
final text = listStyle == ListStyle.checklist
|
||||
? checklistItemText(b).trim()
|
||||
: b.substring(level).trim();
|
||||
final text = b.substring(level).trim();
|
||||
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 value = listStyle == ListStyle.numbered
|
||||
? ' 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>');
|
||||
buf.writeln('<li$style>${_escapeHtml(text)}</li>');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -749,8 +676,6 @@ class MarkdownService {
|
|||
TlpLevel slideTlp = TlpLevel.none;
|
||||
final bullets = <String>[];
|
||||
var bullets2 = <String>[];
|
||||
var listStyle = ListStyle.bullets;
|
||||
var showChecklistProgress = false;
|
||||
var columnTitle1 = '';
|
||||
var columnTitle2 = '';
|
||||
// bulletsImage slides store their panel width in `<!-- _style:
|
||||
|
|
@ -779,16 +704,6 @@ class MarkdownService {
|
|||
columnTitle2 = _decodeText(content.substring(32));
|
||||
} else if (content.startsWith('ocideck_two_bullets_right:')) {
|
||||
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('_')) {
|
||||
notesBuffer.write(notesBuffer.isEmpty ? content : '\n$content');
|
||||
}
|
||||
|
|
@ -858,23 +773,7 @@ class MarkdownService {
|
|||
}
|
||||
}
|
||||
final level = spaces ~/ 2;
|
||||
final body = 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;
|
||||
bullets.add('\t' * level + t.substring(2));
|
||||
} else if (t.startsWith('> ')) {
|
||||
quote = t.substring(2);
|
||||
} else if (t.startsWith('— ')) {
|
||||
|
|
@ -1001,8 +900,6 @@ class MarkdownService {
|
|||
subtitle: type == SlideType.section ? paragraph : h2,
|
||||
bullets: bullets,
|
||||
bullets2: bullets2,
|
||||
listStyle: listStyle,
|
||||
showChecklistProgress: showChecklistProgress,
|
||||
columnTitle1: columnTitle1,
|
||||
columnTitle2: columnTitle2,
|
||||
imagePath: imagePath,
|
||||
|
|
|
|||
|
|
@ -305,55 +305,6 @@ class DeckNotifier extends StateNotifier<DeckState> {
|
|||
_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 ─────────────────────────────────────────────────────
|
||||
|
||||
/// 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
|
||||
/// (zodat typen niet per teken een aparte stap oplevert). Een [coalesceKey]
|
||||
/// van null markeert een losse, discrete stap.
|
||||
///
|
||||
/// 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}) {
|
||||
void _mutate(Deck deck, {String? coalesceKey}) {
|
||||
final previous = state.deck;
|
||||
if (previous != null) {
|
||||
final now = DateTime.now();
|
||||
|
|
@ -588,7 +532,6 @@ class DeckNotifier extends StateNotifier<DeckState> {
|
|||
isDirty: true,
|
||||
canUndo: _undoStack.isNotEmpty,
|
||||
canRedo: false,
|
||||
revision: bumpRevision ? state.revision + 1 : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
final idx = editor.selectedIndex.clamp(0, deck.slides.length - 1);
|
||||
final slide = deck.slides[idx];
|
||||
|
|
@ -1026,14 +979,6 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
tlp: deck.tlp,
|
||||
annotations: deck.annotations,
|
||||
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();
|
||||
case 'find':
|
||||
openFindReplace();
|
||||
case 'clear_checklists':
|
||||
clearAllChecklists();
|
||||
case 'full_preview':
|
||||
openFullDeckPreview();
|
||||
case 'properties':
|
||||
|
|
@ -1372,11 +1315,6 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
|||
menuItem('import_url', Icons.link, l10n.t('importUrl')),
|
||||
const PopupMenuDivider(),
|
||||
menuItem('find', Icons.find_replace, l10n.t('findReplace')),
|
||||
menuItem(
|
||||
'clear_checklists',
|
||||
Icons.check_box_outline_blank,
|
||||
l10n.d('Alle checkboxen legen'),
|
||||
),
|
||||
menuItem(
|
||||
'full_preview',
|
||||
Icons.preview_outlined,
|
||||
|
|
|
|||
|
|
@ -942,43 +942,6 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
_themeProfile.accentColor,
|
||||
(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),
|
||||
_colorSetting(
|
||||
l10n.d('Tabeltekst'),
|
||||
|
|
@ -1017,7 +980,8 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
_colorSetting(
|
||||
l10n.d('Broncode achtergrond'),
|
||||
_themeProfile.codeBackgroundColor,
|
||||
(v) => _themeProfile = _themeProfile.copyWith(codeBackgroundColor: v),
|
||||
(v) =>
|
||||
_themeProfile = _themeProfile.copyWith(codeBackgroundColor: v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_colorSetting(
|
||||
|
|
@ -1048,8 +1012,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
),
|
||||
const SizedBox(height: 10),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue:
|
||||
AppSettings.codeFonts.contains(_themeProfile.codeFontFamily)
|
||||
initialValue: AppSettings.codeFonts.contains(_themeProfile.codeFontFamily)
|
||||
? _themeProfile.codeFontFamily
|
||||
: 'monospace',
|
||||
decoration: InputDecoration(
|
||||
|
|
@ -1319,7 +1282,11 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
|||
borderRadius: BorderRadius.circular(8),
|
||||
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),
|
||||
Text(
|
||||
l10n.d('Bijvoorbeeld #33FF33 voor een CRT-groen scherm.'),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
|
|||
import '../../models/slide.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '_editor_field.dart';
|
||||
import 'list_style_selector.dart';
|
||||
|
||||
class BulletsEditor extends StatefulWidget {
|
||||
final Slide slide;
|
||||
|
|
@ -20,10 +19,7 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
late final TextEditingController _subtitle;
|
||||
late List<TextEditingController> _bullets;
|
||||
late List<int> _levels;
|
||||
late List<bool> _checked;
|
||||
late List<FocusNode> _focusNodes;
|
||||
late ListStyle _listStyle;
|
||||
late bool _showChecklistProgress;
|
||||
|
||||
static const _maxLevel = 4;
|
||||
|
||||
|
|
@ -34,16 +30,13 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
_title.addListener(_emit);
|
||||
_subtitle = TextEditingController(text: widget.slide.subtitle);
|
||||
_subtitle.addListener(_emit);
|
||||
_listStyle = widget.slide.listStyle;
|
||||
_showChecklistProgress = widget.slide.showChecklistProgress;
|
||||
_initBullets(widget.slide.bullets);
|
||||
}
|
||||
|
||||
void _initBullets(List<String> raw) {
|
||||
final list = raw.isEmpty ? [''] : raw;
|
||||
_levels = list.map(_levelOf).toList();
|
||||
_checked = list.map(checklistItemChecked).toList();
|
||||
_bullets = list.map((b) => _makeCtrl(checklistItemText(b))).toList();
|
||||
_bullets = list.map((b) => _makeCtrl(b.trimLeft())).toList();
|
||||
_focusNodes = List.generate(_bullets.length, (_) => FocusNode());
|
||||
}
|
||||
|
||||
|
|
@ -66,17 +59,9 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
widget.slide.copyWith(
|
||||
title: _title.text,
|
||||
subtitle: _subtitle.text,
|
||||
listStyle: _listStyle,
|
||||
showChecklistProgress: _showChecklistProgress,
|
||||
bullets: List.generate(
|
||||
_bullets.length,
|
||||
(i) => _listStyle == ListStyle.checklist
|
||||
? checklistBullet(
|
||||
level: _levels[i],
|
||||
text: _bullets[i].text,
|
||||
checked: _checked[i],
|
||||
)
|
||||
: '\t' * _levels[i] + _bullets[i].text,
|
||||
(i) => '\t' * _levels[i] + _bullets[i].text,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -90,11 +75,9 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
setState(() {
|
||||
final ctrl = _bullets.removeAt(oldIndex);
|
||||
final level = _levels.removeAt(oldIndex);
|
||||
final checked = _checked.removeAt(oldIndex);
|
||||
final focus = _focusNodes.removeAt(oldIndex);
|
||||
_bullets.insert(newIndex, ctrl);
|
||||
_levels.insert(newIndex, level);
|
||||
_checked.insert(newIndex, checked);
|
||||
_focusNodes.insert(newIndex, focus);
|
||||
});
|
||||
_emit();
|
||||
|
|
@ -105,7 +88,6 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
setState(() {
|
||||
_bullets.insert(i + 1, _makeCtrl(''));
|
||||
_levels.insert(i + 1, newLevel);
|
||||
_checked.insert(i + 1, false);
|
||||
_focusNodes.insert(i + 1, FocusNode());
|
||||
});
|
||||
_emit();
|
||||
|
|
@ -115,25 +97,13 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
}
|
||||
|
||||
void _removeBulletAndFocus(int i) {
|
||||
if (_bullets.length == 1) {
|
||||
setState(() {
|
||||
_bullets[i].removeListener(_emit);
|
||||
_bullets[i].clear();
|
||||
_bullets[i].addListener(_emit);
|
||||
_levels[i] = 0;
|
||||
_checked[i] = false;
|
||||
});
|
||||
_emit();
|
||||
_focusNodes[i].requestFocus();
|
||||
return;
|
||||
}
|
||||
if (_bullets.length <= 1) return;
|
||||
final target = (i - 1).clamp(0, _bullets.length - 2);
|
||||
setState(() {
|
||||
_bullets[i].removeListener(_emit);
|
||||
_bullets[i].dispose();
|
||||
_bullets.removeAt(i);
|
||||
_levels.removeAt(i);
|
||||
_checked.removeAt(i);
|
||||
_focusNodes[i].dispose();
|
||||
_focusNodes.removeAt(i);
|
||||
});
|
||||
|
|
@ -172,7 +142,6 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
for (int j = 1; j < lines.length; j++) {
|
||||
_bullets.insert(i + j, _makeCtrl(lines[j]));
|
||||
_levels.insert(i + j, _levels[i]);
|
||||
_checked.insert(i + j, false);
|
||||
_focusNodes.insert(i + j, FocusNode());
|
||||
}
|
||||
});
|
||||
|
|
@ -210,27 +179,6 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
hint: l10n.d('Subkop'),
|
||||
),
|
||||
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'),
|
||||
ReorderableListView(
|
||||
shrinkWrap: true,
|
||||
|
|
@ -272,23 +220,8 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
if (_listStyle == ListStyle.checklist)
|
||||
SizedBox(
|
||||
width: 24,
|
||||
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),
|
||||
_markerForLevel(level),
|
||||
style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
|
@ -338,13 +271,14 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
),
|
||||
),
|
||||
IconButton(
|
||||
key: ValueKey('remove-bullet-$i'),
|
||||
icon: const Icon(
|
||||
Icons.remove_circle_outline,
|
||||
size: 18,
|
||||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
onPressed: () => _removeBulletAndFocus(i),
|
||||
onPressed: _bullets.length > 1
|
||||
? () => _removeBulletAndFocus(i)
|
||||
: null,
|
||||
tooltip: l10n.d('Verwijder'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
constraints: const BoxConstraints(minWidth: 28),
|
||||
|
|
@ -358,18 +292,4 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
|||
const markers = ['•', '◦', '▪', '▫', '–'];
|
||||
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.';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import '../../models/slide.dart';
|
|||
import '../../services/image_service.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '_editor_field.dart';
|
||||
import 'list_style_selector.dart';
|
||||
|
||||
class BulletsImageEditor extends StatefulWidget {
|
||||
final Slide slide;
|
||||
|
|
@ -30,10 +29,7 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
late final TextEditingController _title;
|
||||
late List<TextEditingController> _bullets;
|
||||
late List<int> _levels;
|
||||
late List<bool> _checked;
|
||||
late List<FocusNode> _focusNodes;
|
||||
late ListStyle _listStyle;
|
||||
late bool _showChecklistProgress;
|
||||
|
||||
static const _maxLevel = 4;
|
||||
|
||||
|
|
@ -42,12 +38,9 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
super.initState();
|
||||
_title = TextEditingController(text: widget.slide.title);
|
||||
_title.addListener(_emit);
|
||||
_listStyle = widget.slide.listStyle;
|
||||
_showChecklistProgress = widget.slide.showChecklistProgress;
|
||||
final list = widget.slide.bullets.isEmpty ? [''] : widget.slide.bullets;
|
||||
_levels = list.map(_levelOf).toList();
|
||||
_checked = list.map(checklistItemChecked).toList();
|
||||
_bullets = list.map((b) => _makeCtrl(checklistItemText(b))).toList();
|
||||
_bullets = list.map((b) => _makeCtrl(b.trimLeft())).toList();
|
||||
_focusNodes = List.generate(_bullets.length, (_) => FocusNode());
|
||||
}
|
||||
|
||||
|
|
@ -69,17 +62,9 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
widget.onUpdate(
|
||||
widget.slide.copyWith(
|
||||
title: _title.text,
|
||||
listStyle: _listStyle,
|
||||
showChecklistProgress: _showChecklistProgress,
|
||||
bullets: List.generate(
|
||||
_bullets.length,
|
||||
(i) => _listStyle == ListStyle.checklist
|
||||
? checklistBullet(
|
||||
level: _levels[i],
|
||||
text: _bullets[i].text,
|
||||
checked: _checked[i],
|
||||
)
|
||||
: '\t' * _levels[i] + _bullets[i].text,
|
||||
(i) => '\t' * _levels[i] + _bullets[i].text,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -89,11 +74,9 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
setState(() {
|
||||
final ctrl = _bullets.removeAt(oldIndex);
|
||||
final level = _levels.removeAt(oldIndex);
|
||||
final checked = _checked.removeAt(oldIndex);
|
||||
final focus = _focusNodes.removeAt(oldIndex);
|
||||
_bullets.insert(newIndex, ctrl);
|
||||
_levels.insert(newIndex, level);
|
||||
_checked.insert(newIndex, checked);
|
||||
_focusNodes.insert(newIndex, focus);
|
||||
});
|
||||
_emit();
|
||||
|
|
@ -103,7 +86,6 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
setState(() {
|
||||
_bullets.insert(i + 1, _makeCtrl(''));
|
||||
_levels.insert(i + 1, _levels[i]);
|
||||
_checked.insert(i + 1, false);
|
||||
_focusNodes.insert(i + 1, FocusNode());
|
||||
});
|
||||
_emit();
|
||||
|
|
@ -113,25 +95,13 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
}
|
||||
|
||||
void _removeBulletAndFocus(int i) {
|
||||
if (_bullets.length == 1) {
|
||||
setState(() {
|
||||
_bullets[i].removeListener(_emit);
|
||||
_bullets[i].clear();
|
||||
_bullets[i].addListener(_emit);
|
||||
_levels[i] = 0;
|
||||
_checked[i] = false;
|
||||
});
|
||||
_emit();
|
||||
_focusNodes[i].requestFocus();
|
||||
return;
|
||||
}
|
||||
if (_bullets.length <= 1) return;
|
||||
final target = (i - 1).clamp(0, _bullets.length - 2);
|
||||
setState(() {
|
||||
_bullets[i].removeListener(_emit);
|
||||
_bullets[i].dispose();
|
||||
_bullets.removeAt(i);
|
||||
_levels.removeAt(i);
|
||||
_checked.removeAt(i);
|
||||
_focusNodes[i].dispose();
|
||||
_focusNodes.removeAt(i);
|
||||
});
|
||||
|
|
@ -168,7 +138,6 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
for (int j = 1; j < lines.length; j++) {
|
||||
_bullets.insert(i + j, _makeCtrl(lines[j]));
|
||||
_levels.insert(i + j, _levels[i]);
|
||||
_checked.insert(i + j, false);
|
||||
_focusNodes.insert(i + j, FocusNode());
|
||||
}
|
||||
});
|
||||
|
|
@ -215,27 +184,6 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
children: [
|
||||
EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'),
|
||||
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)'),
|
||||
ReorderableListView(
|
||||
shrinkWrap: true,
|
||||
|
|
@ -306,23 +254,8 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
if (_listStyle == ListStyle.checklist)
|
||||
SizedBox(
|
||||
width: 24,
|
||||
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),
|
||||
_markerForLevel(level),
|
||||
style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
|
@ -368,13 +301,14 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
),
|
||||
),
|
||||
IconButton(
|
||||
key: ValueKey('remove-bullet-$i'),
|
||||
icon: const Icon(
|
||||
Icons.remove_circle_outline,
|
||||
size: 18,
|
||||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
onPressed: () => _removeBulletAndFocus(i),
|
||||
onPressed: _bullets.length > 1
|
||||
? () => _removeBulletAndFocus(i)
|
||||
: null,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
constraints: const BoxConstraints(minWidth: 28),
|
||||
),
|
||||
|
|
@ -387,18 +321,4 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
|||
const markers = ['•', '◦', '▪', '▫', '–'];
|
||||
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.';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,12 @@ import '_editor_field.dart';
|
|||
class ChartEditor extends StatefulWidget {
|
||||
final Slide slide;
|
||||
final ValueChanged<Slide> onUpdate;
|
||||
final ValueChanged<List<Slide>>? onAddVariants;
|
||||
final String? projectPath;
|
||||
|
||||
const ChartEditor({
|
||||
super.key,
|
||||
required this.slide,
|
||||
required this.onUpdate,
|
||||
this.onAddVariants,
|
||||
this.projectPath,
|
||||
});
|
||||
|
||||
|
|
@ -32,114 +30,6 @@ class ChartEditor extends StatefulWidget {
|
|||
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> {
|
||||
late final TextEditingController _title;
|
||||
late final TextEditingController _minBound;
|
||||
|
|
@ -221,7 +111,7 @@ class _ChartEditorState extends State<ChartEditor> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
ChartSpec _currentSpec() {
|
||||
void _emit() {
|
||||
final series = <ChartSeries>[
|
||||
for (var c = 0; c < _seriesNames.length; c++)
|
||||
ChartSeries(
|
||||
|
|
@ -238,7 +128,7 @@ class _ChartEditorState extends State<ChartEditor> {
|
|||
],
|
||||
),
|
||||
];
|
||||
return ChartSpec(
|
||||
final spec = ChartSpec(
|
||||
type: _type,
|
||||
title: _title.text,
|
||||
source: _source,
|
||||
|
|
@ -248,33 +138,7 @@ class _ChartEditorState extends State<ChartEditor> {
|
|||
minBound: _supportsBounds ? _parseBound(_minBound.text) : null,
|
||||
maxBound: _supportsBounds ? _parseBound(_maxBound.text) : null,
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
),
|
||||
]);
|
||||
widget.onUpdate(widget.slide.copyWith(customMarkdown: spec.toBlock()));
|
||||
}
|
||||
|
||||
void _bump() => setState(() => _rev++);
|
||||
|
|
@ -617,15 +481,6 @@ class _ChartEditorState extends State<ChartEditor> {
|
|||
},
|
||||
),
|
||||
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(
|
||||
onPressed: _importCsv,
|
||||
icon: const Icon(Icons.upload_file, size: 16),
|
||||
|
|
@ -932,7 +787,9 @@ class _ChartEditorState extends State<ChartEditor> {
|
|||
decimal: true,
|
||||
signed: true,
|
||||
),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.,\-]'))],
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[0-9.,\-]')),
|
||||
],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
|
|
|
|||
|
|
@ -35,28 +35,17 @@ class _ImageSlideEditorState extends State<ImageSlideEditor> {
|
|||
|
||||
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 {
|
||||
final path = await widget.imageService.pasteImage();
|
||||
if (path != null) {
|
||||
_setImage(path);
|
||||
widget.onUpdate(widget.slide.copyWith(imagePath: path, imageCaption: ''));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
final path = await widget.imageService.pickImage();
|
||||
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,
|
||||
searchPaths: widget.searchPaths,
|
||||
captionBasePath: widget.captionBasePath,
|
||||
onPicked: (path, caption) => _setImage(path, caption: caption),
|
||||
onPicked: (path, caption) => widget.onUpdate(
|
||||
widget.slide.copyWith(imagePath: path, imageCaption: caption),
|
||||
),
|
||||
onBrowse: _pickImage,
|
||||
onPaste: _pasteImage,
|
||||
onClear: widget.slide.imagePath.isNotEmpty
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
|
|||
import '../../models/slide.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '_editor_field.dart';
|
||||
import 'list_style_selector.dart';
|
||||
|
||||
typedef _Mutate = void Function(VoidCallback fn);
|
||||
|
||||
|
|
@ -27,8 +26,6 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
|||
late final TextEditingController _heading2;
|
||||
late _BulletSet _left;
|
||||
late _BulletSet _right;
|
||||
late ListStyle _listStyle;
|
||||
late bool _showChecklistProgress;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -39,8 +36,6 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
|||
_heading2 = TextEditingController(text: widget.slide.columnTitle2);
|
||||
_heading1.addListener(_emit);
|
||||
_heading2.addListener(_emit);
|
||||
_listStyle = widget.slide.listStyle;
|
||||
_showChecklistProgress = widget.slide.showChecklistProgress;
|
||||
_left = _BulletSet(widget.slide.bullets, _emit);
|
||||
_right = _BulletSet(widget.slide.bullets2, _emit);
|
||||
}
|
||||
|
|
@ -51,10 +46,8 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
|||
title: _title.text,
|
||||
columnTitle1: _heading1.text,
|
||||
columnTitle2: _heading2.text,
|
||||
listStyle: _listStyle,
|
||||
showChecklistProgress: _showChecklistProgress,
|
||||
bullets: _left.values(_listStyle),
|
||||
bullets2: _right.values(_listStyle),
|
||||
bullets: _left.values,
|
||||
bullets2: _right.values,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -76,29 +69,6 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
|||
children: [
|
||||
EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'),
|
||||
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(
|
||||
builder: (context, constraints) {
|
||||
final narrow = constraints.maxWidth < 560;
|
||||
|
|
@ -108,14 +78,12 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
|||
set: _left,
|
||||
emit: _emit,
|
||||
headingController: _heading1,
|
||||
listStyle: _listStyle,
|
||||
),
|
||||
_BulletColumn(
|
||||
label: 'Bullets rechts',
|
||||
set: _right,
|
||||
emit: _emit,
|
||||
headingController: _heading2,
|
||||
listStyle: _listStyle,
|
||||
),
|
||||
];
|
||||
if (narrow) {
|
||||
|
|
@ -144,26 +112,18 @@ class _BulletSet {
|
|||
final VoidCallback emit;
|
||||
late List<TextEditingController> controllers;
|
||||
late List<int> levels;
|
||||
late List<bool> checked;
|
||||
late List<FocusNode> focusNodes;
|
||||
|
||||
_BulletSet(List<String> raw, this.emit) {
|
||||
final list = raw.isEmpty ? [''] : raw;
|
||||
levels = list.map(_levelOf).toList();
|
||||
checked = list.map(checklistItemChecked).toList();
|
||||
controllers = list.map((b) => _makeCtrl(checklistItemText(b))).toList();
|
||||
controllers = list.map((b) => _makeCtrl(b.trimLeft())).toList();
|
||||
focusNodes = List.generate(controllers.length, (_) => FocusNode());
|
||||
}
|
||||
|
||||
List<String> values(ListStyle listStyle) => List.generate(
|
||||
List<String> get values => List.generate(
|
||||
controllers.length,
|
||||
(i) => listStyle == ListStyle.checklist
|
||||
? checklistBullet(
|
||||
level: levels[i],
|
||||
text: controllers[i].text,
|
||||
checked: checked[i],
|
||||
)
|
||||
: '\t' * levels[i] + controllers[i].text,
|
||||
(i) => '\t' * levels[i] + controllers[i].text,
|
||||
);
|
||||
|
||||
static int _levelOf(String b) {
|
||||
|
|
@ -184,7 +144,6 @@ class _BulletSet {
|
|||
mutate(() {
|
||||
controllers.insert(i + 1, _makeCtrl(''));
|
||||
levels.insert(i + 1, levels[i]);
|
||||
checked.insert(i + 1, false);
|
||||
focusNodes.insert(i + 1, FocusNode());
|
||||
});
|
||||
emit();
|
||||
|
|
@ -194,25 +153,13 @@ class _BulletSet {
|
|||
}
|
||||
|
||||
void removeAndFocus(_Mutate mutate, int i) {
|
||||
if (controllers.length == 1) {
|
||||
mutate(() {
|
||||
controllers[i].removeListener(emit);
|
||||
controllers[i].clear();
|
||||
controllers[i].addListener(emit);
|
||||
levels[i] = 0;
|
||||
checked[i] = false;
|
||||
});
|
||||
emit();
|
||||
focusNodes[i].requestFocus();
|
||||
return;
|
||||
}
|
||||
if (controllers.length <= 1) return;
|
||||
final target = (i - 1).clamp(0, controllers.length - 2);
|
||||
mutate(() {
|
||||
controllers[i].removeListener(emit);
|
||||
controllers[i].dispose();
|
||||
controllers.removeAt(i);
|
||||
levels.removeAt(i);
|
||||
checked.removeAt(i);
|
||||
focusNodes[i].dispose();
|
||||
focusNodes.removeAt(i);
|
||||
});
|
||||
|
|
@ -251,7 +198,6 @@ class _BulletSet {
|
|||
for (int j = 1; j < lines.length; j++) {
|
||||
controllers.insert(i + j, _makeCtrl(lines[j]));
|
||||
levels.insert(i + j, levels[i]);
|
||||
checked.insert(i + j, false);
|
||||
focusNodes.insert(i + j, FocusNode());
|
||||
}
|
||||
});
|
||||
|
|
@ -277,14 +223,12 @@ class _BulletColumn extends StatefulWidget {
|
|||
final _BulletSet set;
|
||||
final VoidCallback emit;
|
||||
final TextEditingController headingController;
|
||||
final ListStyle listStyle;
|
||||
|
||||
const _BulletColumn({
|
||||
required this.label,
|
||||
required this.set,
|
||||
required this.emit,
|
||||
required this.headingController,
|
||||
required this.listStyle,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -331,23 +275,8 @@ class _BulletColumnState extends State<_BulletColumn> {
|
|||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (widget.listStyle == ListStyle.checklist)
|
||||
SizedBox(
|
||||
width: 24,
|
||||
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),
|
||||
_markerForLevel(level),
|
||||
style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
|
@ -395,13 +324,14 @@ class _BulletColumnState extends State<_BulletColumn> {
|
|||
),
|
||||
),
|
||||
IconButton(
|
||||
key: ValueKey('remove-bullet-${widget.label}-$i'),
|
||||
icon: const Icon(
|
||||
Icons.remove_circle_outline,
|
||||
size: 18,
|
||||
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'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
constraints: const BoxConstraints(minWidth: 28),
|
||||
|
|
@ -415,18 +345,4 @@ class _BulletColumnState extends State<_BulletColumn> {
|
|||
const markers = ['•', '◦', '▪', '▫', '–'];
|
||||
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.';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,13 +102,6 @@ class EditorPanel extends ConsumerWidget {
|
|||
imgService,
|
||||
searchPaths,
|
||||
deck.projectPath,
|
||||
(variants) {
|
||||
final first = deckNotifier.insertSlides(
|
||||
variants,
|
||||
afterIndex: idx,
|
||||
);
|
||||
if (first >= 0) editorNotifier.select(first);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (slide.type != SlideType.video) ...[
|
||||
|
|
@ -167,8 +160,6 @@ class EditorPanel extends ConsumerWidget {
|
|||
bullets2: newType == SlideType.twoBullets
|
||||
? (slide.bullets2.isNotEmpty ? slide.bullets2 : [''])
|
||||
: const [],
|
||||
listStyle: slide.listStyle,
|
||||
showChecklistProgress: slide.showChecklistProgress,
|
||||
imagePath: keepsImage ? slide.imagePath : '',
|
||||
imagePath2: newType == SlideType.twoImages ? slide.imagePath2 : '',
|
||||
imageCaption: keepsImage ? slide.imageCaption : '',
|
||||
|
|
@ -206,7 +197,6 @@ class EditorPanel extends ConsumerWidget {
|
|||
ImageService imgService,
|
||||
List<String> searchPaths,
|
||||
String? captionBasePath,
|
||||
ValueChanged<List<Slide>> onAddChartVariants,
|
||||
) {
|
||||
switch (slide.type) {
|
||||
case SlideType.title:
|
||||
|
|
@ -299,7 +289,6 @@ class EditorPanel extends ConsumerWidget {
|
|||
key: ValueKey(slide.id),
|
||||
slide: slide,
|
||||
onUpdate: onUpdate,
|
||||
onAddVariants: onAddChartVariants,
|
||||
projectPath: captionBasePath,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,16 +103,6 @@ class _AudienceWindowAppState extends State<AudienceWindowApp> {
|
|||
? null
|
||||
: 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':
|
||||
try {
|
||||
final self = await WindowController.fromCurrentEngine();
|
||||
|
|
@ -122,9 +112,9 @@ class _AudienceWindowAppState extends State<AudienceWindowApp> {
|
|||
return null;
|
||||
}
|
||||
|
||||
void _send(String method, [Object? arguments]) {
|
||||
void _send(String method) {
|
||||
// Best-effort: the presenter may already be gone.
|
||||
presenterChannel.invokeMethod(method, arguments).catchError((_) => null);
|
||||
presenterChannel.invokeMethod(method).catchError((_) => null);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -179,23 +169,11 @@ class _AudienceWindowAppState extends State<AudienceWindowApp> {
|
|||
slideCount: _slides.length,
|
||||
tlp: _tlp,
|
||||
presentationMode: true,
|
||||
onChecklistItemToggle: (column, itemIndex) =>
|
||||
_send('checklistToggle', {
|
||||
'slideIndex': _index,
|
||||
'column': column,
|
||||
'itemIndex': itemIndex,
|
||||
}),
|
||||
enableMedia: true,
|
||||
autoplayMedia: true,
|
||||
// Media finishing on the beamer drives auto-advance.
|
||||
onAudioComplete: () => _send('mediaComplete', {
|
||||
'index': _index,
|
||||
'kind': 'audio',
|
||||
}),
|
||||
onVideoComplete: () => _send('mediaComplete', {
|
||||
'index': _index,
|
||||
'kind': 'video',
|
||||
}),
|
||||
// Audio finishing on the beamer drives the presenter's
|
||||
// auto-advance.
|
||||
onAudioComplete: () => _send('audioComplete'),
|
||||
),
|
||||
AnnotationLayer(
|
||||
strokes: _ink[_index] ?? const [],
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ class FullscreenPresenter extends StatefulWidget {
|
|||
/// made while presenting back to the deck.
|
||||
final Map<String, List<InkStroke>> initialAnnotations;
|
||||
final void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged;
|
||||
final ValueChanged<Slide>? onSlideChanged;
|
||||
|
||||
const FullscreenPresenter({
|
||||
super.key,
|
||||
|
|
@ -49,7 +48,6 @@ class FullscreenPresenter extends StatefulWidget {
|
|||
this.audienceWindow,
|
||||
this.initialAnnotations = const {},
|
||||
this.onAnnotationsChanged,
|
||||
this.onSlideChanged,
|
||||
});
|
||||
|
||||
/// 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,
|
||||
Map<String, List<InkStroke>> annotations = const {},
|
||||
void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged,
|
||||
ValueChanged<Slide>? onSlideChanged,
|
||||
}) async {
|
||||
var displayCount = 0;
|
||||
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
||||
|
|
@ -92,7 +89,6 @@ class FullscreenPresenter extends StatefulWidget {
|
|||
tlp: tlp,
|
||||
annotations: annotations,
|
||||
onAnnotationsChanged: onAnnotationsChanged,
|
||||
onSlideChanged: onSlideChanged,
|
||||
);
|
||||
} else {
|
||||
await show(
|
||||
|
|
@ -104,7 +100,6 @@ class FullscreenPresenter extends StatefulWidget {
|
|||
tlp: tlp,
|
||||
annotations: annotations,
|
||||
onAnnotationsChanged: onAnnotationsChanged,
|
||||
onSlideChanged: onSlideChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -118,7 +113,6 @@ class FullscreenPresenter extends StatefulWidget {
|
|||
TlpLevel tlp = TlpLevel.none,
|
||||
Map<String, List<InkStroke>> annotations = const {},
|
||||
void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged,
|
||||
ValueChanged<Slide>? onSlideChanged,
|
||||
}) async {
|
||||
final hadWakeLock = await _wakeLockEnabled();
|
||||
await _enableWakeLock();
|
||||
|
|
@ -137,7 +131,6 @@ class FullscreenPresenter extends StatefulWidget {
|
|||
tlp: tlp,
|
||||
initialAnnotations: annotations,
|
||||
onAnnotationsChanged: onAnnotationsChanged,
|
||||
onSlideChanged: onSlideChanged,
|
||||
),
|
||||
transitionsBuilder: (context, animation, secondary, child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
|
|
@ -163,7 +156,6 @@ class FullscreenPresenter extends StatefulWidget {
|
|||
TlpLevel tlp = TlpLevel.none,
|
||||
Map<String, List<InkStroke>> annotations = const {},
|
||||
void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged,
|
||||
ValueChanged<Slide>? onSlideChanged,
|
||||
}) async {
|
||||
// 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.
|
||||
|
|
@ -213,7 +205,6 @@ class FullscreenPresenter extends StatefulWidget {
|
|||
tlp: tlp,
|
||||
annotations: annotations,
|
||||
onAnnotationsChanged: onAnnotationsChanged,
|
||||
onSlideChanged: onSlideChanged,
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
|
@ -236,7 +227,6 @@ class FullscreenPresenter extends StatefulWidget {
|
|||
audienceWindow: audience,
|
||||
initialAnnotations: annotations,
|
||||
onAnnotationsChanged: onAnnotationsChanged,
|
||||
onSlideChanged: onSlideChanged,
|
||||
),
|
||||
transitionsBuilder: (context, animation, secondary, child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
|
|
@ -265,16 +255,6 @@ bool shouldUseDualScreen({
|
|||
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 {
|
||||
try {
|
||||
return await WakelockPlus.enabled;
|
||||
|
|
@ -345,9 +325,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
/// laatste slide staan). Met L te wisselen.
|
||||
bool _loop = false;
|
||||
|
||||
/// Wissel ná het afspelen van autoplay-media i.p.v. op de tijdwissel.
|
||||
/// Wissel ná het afspelen van de audio op de slide i.p.v. op de tijdwissel.
|
||||
/// Met M te wisselen.
|
||||
bool _advanceOnMediaEnd = true;
|
||||
bool _advanceOnAudioEnd = true;
|
||||
|
||||
/// Known displays for moving the fullscreen presentation window. This is not
|
||||
/// a second presenter window; it keeps the current output movable between
|
||||
|
|
@ -405,20 +385,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
case 'exit':
|
||||
_exit();
|
||||
case 'audioComplete':
|
||||
_onMediaCompleted(kind: 'audio');
|
||||
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,
|
||||
);
|
||||
_onAudioCompleted();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
|
@ -464,38 +431,6 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
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 ─────────────────────────────────────────────────────────
|
||||
|
||||
/// 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)];
|
||||
|
||||
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;
|
||||
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
|
||||
/// blijft de laatste slide gewoon staan.
|
||||
void _autoAdvance() {
|
||||
|
|
@ -633,20 +573,10 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
}
|
||||
}
|
||||
|
||||
void _onMediaCompleted({int? index, String? kind}) {
|
||||
if (index != null && index != _index) return;
|
||||
final slide = widget.slides[_index.clamp(0, widget.slides.length - 1)];
|
||||
// A video is primary on a video slide. Ignore an attached audio track that
|
||||
// happens to finish earlier.
|
||||
if (kind == 'audio' &&
|
||||
slide.type == SlideType.video &&
|
||||
slide.videoPath.isNotEmpty &&
|
||||
slide.videoAutoplay) {
|
||||
return;
|
||||
}
|
||||
if (_autoPlay && _advanceOnMediaEnd && autoAdvanceWaitsForMedia(slide)) {
|
||||
_autoAdvance();
|
||||
}
|
||||
/// Aangeroepen door de audiospeler zodra de audio op de huidige slide klaar
|
||||
/// is. In automatische modus met 'na audio doorgaan' schakelen we dan door.
|
||||
void _onAudioCompleted() {
|
||||
if (_autoPlay && _advanceOnAudioEnd) _autoAdvance();
|
||||
}
|
||||
|
||||
void _toggleAutoPlay() {
|
||||
|
|
@ -659,8 +589,8 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
_scheduleAdvance();
|
||||
}
|
||||
|
||||
void _toggleMediaAdvance() {
|
||||
setState(() => _advanceOnMediaEnd = !_advanceOnMediaEnd);
|
||||
void _toggleAudioAdvance() {
|
||||
setState(() => _advanceOnAudioEnd = !_advanceOnAudioEnd);
|
||||
_scheduleAdvance();
|
||||
}
|
||||
|
||||
|
|
@ -962,7 +892,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
_toggleLoop();
|
||||
return KeyEventResult.handled;
|
||||
case LogicalKeyboardKey.keyM:
|
||||
_toggleMediaAdvance();
|
||||
_toggleAudioAdvance();
|
||||
return KeyEventResult.handled;
|
||||
case LogicalKeyboardKey.keyS:
|
||||
_cycleDisplay();
|
||||
|
|
@ -1225,7 +1155,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
('R', l10n.d('Verstreken tijd resetten')),
|
||||
('A', l10n.d('Automatische modus 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')),
|
||||
('Esc', l10n.d('Terug / afsluiten')),
|
||||
];
|
||||
|
|
@ -1359,20 +1289,13 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
|||
slideCount: widget.slides.length,
|
||||
tlp: widget.tlp,
|
||||
presentationMode: true,
|
||||
onChecklistItemToggle: (column, itemIndex) =>
|
||||
_toggleChecklistItem(
|
||||
slideIndex: _index,
|
||||
column: column,
|
||||
itemIndex: itemIndex,
|
||||
),
|
||||
// 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,
|
||||
// anders zou het geluid dubbel klinken.
|
||||
enableMedia: !_dual,
|
||||
autoplayMedia: !_dual,
|
||||
onAudioComplete: () => _onMediaCompleted(kind: 'audio'),
|
||||
onVideoComplete: () => _onMediaCompleted(kind: 'video'),
|
||||
onAudioComplete: _onAudioCompleted,
|
||||
),
|
||||
// Annotatielaag bovenop de dia. Laat klikken door wanneer er
|
||||
// geen gereedschap actief is (zodat tikken blijft doorbladeren).
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -4,21 +4,13 @@ import 'package:ocideck/models/chart.dart';
|
|||
import 'package:ocideck/models/slide.dart';
|
||||
import 'package:ocideck/widgets/editors/chart_editor.dart';
|
||||
|
||||
Widget _host(
|
||||
Slide slide,
|
||||
ValueChanged<Slide> onUpdate, {
|
||||
ValueChanged<List<Slide>>? onAddVariants,
|
||||
}) {
|
||||
Widget _host(Slide slide, ValueChanged<Slide> onUpdate) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 900,
|
||||
height: 650,
|
||||
child: ChartEditor(
|
||||
slide: slide,
|
||||
onUpdate: onUpdate,
|
||||
onAddVariants: onAddVariants,
|
||||
),
|
||||
child: ChartEditor(slide: slide, onUpdate: onUpdate),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -144,7 +136,10 @@ void main() {
|
|||
expect(find.byKey(const ValueKey('chart-min-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();
|
||||
|
||||
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-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');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -291,11 +291,7 @@ void main() {
|
|||
final radar = tester.widget<RadarChart>(find.byType(RadarChart));
|
||||
// Two visible series plus one invisible scale anchor.
|
||||
expect(radar.data.dataSets.length, 3);
|
||||
expect(radar.data.dataSets.first.dataEntries.map((e) => e.value), [
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
]);
|
||||
expect(radar.data.dataSets.first.dataEntries.map((e) => e.value), [3, 4, 5]);
|
||||
expect(radar.data.dataSets.last.fillColor, Colors.transparent);
|
||||
// The spoke labels are supplied through getTitle (canvas-painted).
|
||||
expect(radar.data.getTitle!(0, 0).text, 'Snelheid');
|
||||
|
|
@ -306,43 +302,6 @@ void main() {
|
|||
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', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
|
|||
|
|
@ -137,74 +137,6 @@ void main() {
|
|||
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', () {
|
||||
final n = _notifier()..newDeck('D');
|
||||
n.updateMeta(title: 'Andere titel', theme: 'custom', paginate: false);
|
||||
|
|
|
|||
|
|
@ -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', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -90,51 +90,6 @@ void main() {
|
|||
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', () {
|
||||
final out = _roundTrip(
|
||||
Slide.create(SlideType.twoBullets).copyWith(
|
||||
|
|
@ -172,9 +127,7 @@ void main() {
|
|||
Deck(
|
||||
title: 'Demo',
|
||||
slides: [
|
||||
Slide.create(
|
||||
SlideType.twoBullets,
|
||||
).copyWith(bullets: ['A'], bullets2: ['B']),
|
||||
Slide.create(SlideType.twoBullets).copyWith(bullets: ['A'], bullets2: ['B']),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -184,24 +137,6 @@ void main() {
|
|||
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', () {
|
||||
final out = _roundTrip(
|
||||
Slide.create(SlideType.bulletsImage).copyWith(
|
||||
|
|
|
|||
|
|
@ -29,18 +29,6 @@ void main() {
|
|||
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', () {
|
||||
// Older decks without the fields fall back to the dark editor defaults.
|
||||
final back = ThemeProfile.fromJson(const {'name': 'Legacy'});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ocideck/models/slide.dart';
|
||||
import 'package:ocideck/widgets/slides/inline_markdown.dart';
|
||||
import 'package:ocideck/widgets/slides/slide_preview.dart';
|
||||
|
||||
Widget _host(Slide slide) {
|
||||
|
|
@ -59,28 +58,6 @@ void main() {
|
|||
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', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue