Checklist clear-all actie, grotere voortgangsgrafiek en leesbaardere bullet auto-fit #4
27 changed files with 2684 additions and 365 deletions
|
|
@ -47,6 +47,8 @@ and the project aims to follow [Semantic Versioning](https://semver.org/).
|
||||||
separate from the slide title.
|
separate from the slide title.
|
||||||
- Slide text auto-sizing now measures with the deck's own font, so text grows to
|
- Slide text auto-sizing now measures with the deck's own font, so text grows to
|
||||||
use the available space more accurately instead of staying smaller than needed.
|
use the available space more accurately instead of staying smaller than needed.
|
||||||
|
- The two bullet columns now scale **independently**, so a column with few items
|
||||||
|
is no longer shrunk down to the size of a crowded one beside it.
|
||||||
- Slide transitions in the presenter no longer flash a black frame (neighbour
|
- Slide transitions in the presenter no longer flash a black frame (neighbour
|
||||||
images are precached and `gaplessPlayback` is enabled) — important for
|
images are precached and `gaplessPlayback` is enabled) — important for
|
||||||
recording.
|
recording.
|
||||||
|
|
|
||||||
|
|
@ -2327,6 +2327,35 @@ const _dutchSourceStrings = {
|
||||||
const _dutchSourceStringAdditions = {
|
const _dutchSourceStringAdditions = {
|
||||||
'en': {
|
'en': {
|
||||||
'Annuleren': 'Cancel',
|
'Annuleren': 'Cancel',
|
||||||
|
'Checklist': 'Task checklist',
|
||||||
|
'Voortgangsgrafiek tonen': 'Show progress chart',
|
||||||
|
'Toont afgevinkt en niet afgevinkt als percentages.':
|
||||||
|
'Shows checked and unchecked items as percentages.',
|
||||||
|
'Afgevinkt': 'Checked',
|
||||||
|
'Niet afgevinkt': 'Unchecked',
|
||||||
|
'Er zijn geen aangevinkte checklist-items om te legen.':
|
||||||
|
'There are no checked checklist items to clear.',
|
||||||
|
'Alle checkboxen legen?': 'Clear all checkboxes?',
|
||||||
|
'Hiermee worden alle': 'This will uncheck all',
|
||||||
|
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
|
||||||
|
'checked checklist items in the entire presentation. You can undo this with Ctrl/Cmd+Z.',
|
||||||
|
'Alles legen': 'Clear all',
|
||||||
|
'checklist-items uitgevinkt.': 'checklist items unchecked.',
|
||||||
|
'Alle checkboxen legen': 'Clear all checkboxes',
|
||||||
|
'Afgevinkte tekst doorhalen': 'Strike through checked text',
|
||||||
|
'Toont een streep door voltooide checklistitems.':
|
||||||
|
'Shows completed checklist items with a strike-through.',
|
||||||
|
'Na media automatisch doorgaan': 'Advance automatically after media',
|
||||||
|
'Opsomming': 'Bullets',
|
||||||
|
'Nummering': 'Numbering',
|
||||||
|
'Varianten': 'Variants',
|
||||||
|
'Grafiekvarianten maken': 'Create chart variants',
|
||||||
|
'Slides toevoegen': 'Add slides',
|
||||||
|
'Omhoog': 'Move up',
|
||||||
|
'Omlaag': 'Move down',
|
||||||
|
'Niet toevoegen': 'Do not add',
|
||||||
|
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
|
||||||
|
'These slides use the same data, colors, and title. Use the arrows to choose their order after the current slide.',
|
||||||
'Afbeelding': 'Image',
|
'Afbeelding': 'Image',
|
||||||
'Broncode': 'Source code',
|
'Broncode': 'Source code',
|
||||||
'Bullet': 'Bullet',
|
'Bullet': 'Bullet',
|
||||||
|
|
@ -2406,6 +2435,36 @@ const _dutchSourceStringAdditions = {
|
||||||
},
|
},
|
||||||
'it': {
|
'it': {
|
||||||
'Annuleren': 'Annulla',
|
'Annuleren': 'Annulla',
|
||||||
|
'Checklist': 'Lista di controllo',
|
||||||
|
'Voortgangsgrafiek tonen': 'Mostra grafico di avanzamento',
|
||||||
|
'Toont afgevinkt en niet afgevinkt als percentages.':
|
||||||
|
'Mostra gli elementi selezionati e non selezionati in percentuale.',
|
||||||
|
'Afgevinkt': 'Selezionati',
|
||||||
|
'Niet afgevinkt': 'Non selezionati',
|
||||||
|
'Er zijn geen aangevinkte checklist-items om te legen.':
|
||||||
|
'Non ci sono elementi della lista selezionati da azzerare.',
|
||||||
|
'Alle checkboxen legen?': 'Azzerare tutte le caselle?',
|
||||||
|
'Hiermee worden alle': 'Verranno deselezionati tutti i',
|
||||||
|
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
|
||||||
|
'elementi selezionati della lista in tutta la presentazione. Puoi annullare con Ctrl/Cmd+Z.',
|
||||||
|
'Alles legen': 'Azzera tutto',
|
||||||
|
'checklist-items uitgevinkt.': 'elementi della lista deselezionati.',
|
||||||
|
'Alle checkboxen legen': 'Azzera tutte le caselle',
|
||||||
|
'Afgevinkte tekst doorhalen': 'Barra il testo selezionato',
|
||||||
|
'Toont een streep door voltooide checklistitems.':
|
||||||
|
'Mostra gli elementi completati con il testo barrato.',
|
||||||
|
'Na media automatisch doorgaan':
|
||||||
|
'Avanza automaticamente dopo i contenuti multimediali',
|
||||||
|
'Opsomming': 'Elenco puntato',
|
||||||
|
'Nummering': 'Numerazione',
|
||||||
|
'Varianten': 'Varianti',
|
||||||
|
'Grafiekvarianten maken': 'Crea varianti del grafico',
|
||||||
|
'Slides toevoegen': 'Aggiungi diapositive',
|
||||||
|
'Omhoog': 'Sposta su',
|
||||||
|
'Omlaag': 'Sposta giù',
|
||||||
|
'Niet toevoegen': 'Non aggiungere',
|
||||||
|
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
|
||||||
|
'Queste diapositive usano gli stessi dati, colori e titolo. Usa le frecce per scegliere l’ordine dopo la diapositiva corrente.',
|
||||||
'Spider': 'Radar',
|
'Spider': 'Radar',
|
||||||
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
|
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
|
||||||
'Un grafico radar richiede almeno tre etichette (assi); ogni serie forma una superficie.',
|
'Un grafico radar richiede almeno tre etichette (assi); ogni serie forma una superficie.',
|
||||||
|
|
@ -2638,6 +2697,35 @@ const _dutchSourceStringAdditions = {
|
||||||
},
|
},
|
||||||
'de': {
|
'de': {
|
||||||
'Annuleren': 'Abbrechen',
|
'Annuleren': 'Abbrechen',
|
||||||
|
'Checklist': 'Checkliste',
|
||||||
|
'Voortgangsgrafiek tonen': 'Fortschrittsdiagramm anzeigen',
|
||||||
|
'Toont afgevinkt en niet afgevinkt als percentages.':
|
||||||
|
'Zeigt erledigte und offene Einträge als Prozentwerte.',
|
||||||
|
'Afgevinkt': 'Erledigt',
|
||||||
|
'Niet afgevinkt': 'Offen',
|
||||||
|
'Er zijn geen aangevinkte checklist-items om te legen.':
|
||||||
|
'Es gibt keine angehakten Checklisten-Einträge zum Zurücksetzen.',
|
||||||
|
'Alle checkboxen legen?': 'Alle Kontrollkästchen leeren?',
|
||||||
|
'Hiermee worden alle': 'Damit werden die Häkchen von allen',
|
||||||
|
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
|
||||||
|
'angehakten Checklisten-Einträgen in der gesamten Präsentation entfernt. Du kannst dies mit Strg/Cmd+Z rückgängig machen.',
|
||||||
|
'Alles legen': 'Alle leeren',
|
||||||
|
'checklist-items uitgevinkt.': 'Checklisten-Einträge zurückgesetzt.',
|
||||||
|
'Alle checkboxen legen': 'Alle Kontrollkästchen leeren',
|
||||||
|
'Afgevinkte tekst doorhalen': 'Erledigten Text durchstreichen',
|
||||||
|
'Toont een streep door voltooide checklistitems.':
|
||||||
|
'Zeigt erledigte Checklistenpunkte durchgestrichen an.',
|
||||||
|
'Na media automatisch doorgaan': 'Nach Medienwiedergabe automatisch weiter',
|
||||||
|
'Opsomming': 'Aufzählung',
|
||||||
|
'Nummering': 'Nummerierung',
|
||||||
|
'Varianten': 'Varianten',
|
||||||
|
'Grafiekvarianten maken': 'Diagrammvarianten erstellen',
|
||||||
|
'Slides toevoegen': 'Folien hinzufügen',
|
||||||
|
'Omhoog': 'Nach oben',
|
||||||
|
'Omlaag': 'Nach unten',
|
||||||
|
'Niet toevoegen': 'Nicht hinzufügen',
|
||||||
|
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
|
||||||
|
'Diese Folien verwenden dieselben Daten, Farben und denselben Titel. Lege mit den Pfeilen die Reihenfolge nach der aktuellen Folie fest.',
|
||||||
'Spider': 'Netz',
|
'Spider': 'Netz',
|
||||||
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
|
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
|
||||||
'Ein Netzdiagramm braucht mindestens drei Beschriftungen (Achsen); jede Reihe bildet eine Fläche.',
|
'Ein Netzdiagramm braucht mindestens drei Beschriftungen (Achsen); jede Reihe bildet eine Fläche.',
|
||||||
|
|
@ -2871,6 +2959,35 @@ const _dutchSourceStringAdditions = {
|
||||||
},
|
},
|
||||||
'fr': {
|
'fr': {
|
||||||
'Annuleren': 'Annuler',
|
'Annuleren': 'Annuler',
|
||||||
|
'Checklist': 'Liste de contrôle',
|
||||||
|
'Voortgangsgrafiek tonen': 'Afficher le graphique de progression',
|
||||||
|
'Toont afgevinkt en niet afgevinkt als percentages.':
|
||||||
|
'Affiche les éléments cochés et non cochés en pourcentage.',
|
||||||
|
'Afgevinkt': 'Coché',
|
||||||
|
'Niet afgevinkt': 'Non coché',
|
||||||
|
'Er zijn geen aangevinkte checklist-items om te legen.':
|
||||||
|
'Aucun élément de liste coché à effacer.',
|
||||||
|
'Alle checkboxen legen?': 'Effacer toutes les cases à cocher ?',
|
||||||
|
'Hiermee worden alle': 'Cela décochera tous les',
|
||||||
|
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
|
||||||
|
'éléments de liste cochés dans toute la présentation. Vous pouvez annuler avec Ctrl/Cmd+Z.',
|
||||||
|
'Alles legen': 'Tout effacer',
|
||||||
|
'checklist-items uitgevinkt.': 'éléments de liste décochés.',
|
||||||
|
'Alle checkboxen legen': 'Effacer toutes les cases à cocher',
|
||||||
|
'Afgevinkte tekst doorhalen': 'Barrer le texte coché',
|
||||||
|
'Toont een streep door voltooide checklistitems.':
|
||||||
|
'Affiche les éléments terminés avec un texte barré.',
|
||||||
|
'Na media automatisch doorgaan': 'Avancer automatiquement après le média',
|
||||||
|
'Opsomming': 'Liste à puces',
|
||||||
|
'Nummering': 'Numérotation',
|
||||||
|
'Varianten': 'Variantes',
|
||||||
|
'Grafiekvarianten maken': 'Créer des variantes du graphique',
|
||||||
|
'Slides toevoegen': 'Ajouter les diapositives',
|
||||||
|
'Omhoog': 'Monter',
|
||||||
|
'Omlaag': 'Descendre',
|
||||||
|
'Niet toevoegen': 'Ne pas ajouter',
|
||||||
|
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
|
||||||
|
'Ces diapositives utilisent les mêmes données, couleurs et titre. Utilisez les flèches pour choisir leur ordre après la diapositive actuelle.',
|
||||||
'Spider': 'Radar',
|
'Spider': 'Radar',
|
||||||
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
|
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
|
||||||
'Un graphique radar nécessite au moins trois étiquettes (axes); chaque série forme une surface.',
|
'Un graphique radar nécessite au moins trois étiquettes (axes); chaque série forme une surface.',
|
||||||
|
|
@ -3104,6 +3221,36 @@ const _dutchSourceStringAdditions = {
|
||||||
},
|
},
|
||||||
'es': {
|
'es': {
|
||||||
'Annuleren': 'Cancelar',
|
'Annuleren': 'Cancelar',
|
||||||
|
'Checklist': 'Lista de verificación',
|
||||||
|
'Voortgangsgrafiek tonen': 'Mostrar gráfico de progreso',
|
||||||
|
'Toont afgevinkt en niet afgevinkt als percentages.':
|
||||||
|
'Muestra los elementos marcados y sin marcar como porcentajes.',
|
||||||
|
'Afgevinkt': 'Marcado',
|
||||||
|
'Niet afgevinkt': 'Sin marcar',
|
||||||
|
'Er zijn geen aangevinkte checklist-items om te legen.':
|
||||||
|
'No hay elementos de lista marcados que vaciar.',
|
||||||
|
'Alle checkboxen legen?': '¿Vaciar todas las casillas?',
|
||||||
|
'Hiermee worden alle': 'Se desmarcarán todos los',
|
||||||
|
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
|
||||||
|
'elementos de lista marcados de toda la presentación. Puedes deshacerlo con Ctrl/Cmd+Z.',
|
||||||
|
'Alles legen': 'Vaciar todo',
|
||||||
|
'checklist-items uitgevinkt.': 'elementos de lista desmarcados.',
|
||||||
|
'Alle checkboxen legen': 'Vaciar todas las casillas',
|
||||||
|
'Afgevinkte tekst doorhalen': 'Tachar el texto marcado',
|
||||||
|
'Toont een streep door voltooide checklistitems.':
|
||||||
|
'Muestra tachados los elementos completados.',
|
||||||
|
'Na media automatisch doorgaan':
|
||||||
|
'Avanzar automáticamente tras el contenido multimedia',
|
||||||
|
'Opsomming': 'Viñetas',
|
||||||
|
'Nummering': 'Numeración',
|
||||||
|
'Varianten': 'Variantes',
|
||||||
|
'Grafiekvarianten maken': 'Crear variantes del gráfico',
|
||||||
|
'Slides toevoegen': 'Añadir diapositivas',
|
||||||
|
'Omhoog': 'Subir',
|
||||||
|
'Omlaag': 'Bajar',
|
||||||
|
'Niet toevoegen': 'No añadir',
|
||||||
|
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
|
||||||
|
'Estas diapositivas usan los mismos datos, colores y título. Usa las flechas para elegir su orden después de la diapositiva actual.',
|
||||||
'Spider': 'Radar',
|
'Spider': 'Radar',
|
||||||
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
|
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
|
||||||
'Un gráfico radar necesita al menos tres etiquetas (ejes); cada serie forma una superficie.',
|
'Un gráfico radar necesita al menos tres etiquetas (ejes); cada serie forma una superficie.',
|
||||||
|
|
@ -3337,6 +3484,35 @@ const _dutchSourceStringAdditions = {
|
||||||
},
|
},
|
||||||
'fy': {
|
'fy': {
|
||||||
'Annuleren': 'Annulearje',
|
'Annuleren': 'Annulearje',
|
||||||
|
'Checklist': 'Kontrôlelist',
|
||||||
|
'Voortgangsgrafiek tonen': 'Fuortgongsgrafyk toane',
|
||||||
|
'Toont afgevinkt en niet afgevinkt als percentages.':
|
||||||
|
'Toant ôffinkte en net ôffinkte items as persintaazjes.',
|
||||||
|
'Afgevinkt': 'Ôffinkt',
|
||||||
|
'Niet afgevinkt': 'Net ôffinkt',
|
||||||
|
'Er zijn geen aangevinkte checklist-items om te legen.':
|
||||||
|
'Der binne gjin oanfinkte checklist-items om leech te meitsjen.',
|
||||||
|
'Alle checkboxen legen?': 'Alle oanfinkfakjes leechje?',
|
||||||
|
'Hiermee worden alle': 'Hjirmei wurde alle',
|
||||||
|
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
|
||||||
|
'oanfinkte checklist-items yn de hiele presintaasje útfinkt. Dit kinst weromdraaie mei Ctrl/Cmd+Z.',
|
||||||
|
'Alles legen': 'Alles leechje',
|
||||||
|
'checklist-items uitgevinkt.': 'checklist-items útfinke.',
|
||||||
|
'Alle checkboxen legen': 'Alle oanfinkfakjes leechje',
|
||||||
|
'Afgevinkte tekst doorhalen': 'Ôffinkte tekst trochstreekje',
|
||||||
|
'Toont een streep door voltooide checklistitems.':
|
||||||
|
'Toant foltôge kontrôlelistitems mei in streek der troch.',
|
||||||
|
'Na media automatisch doorgaan': 'Nei media automatysk trochgean',
|
||||||
|
'Opsomming': 'Puntelist',
|
||||||
|
'Nummering': 'Nûmering',
|
||||||
|
'Varianten': 'Farianten',
|
||||||
|
'Grafiekvarianten maken': 'Grafykfarianten meitsje',
|
||||||
|
'Slides toevoegen': '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',
|
'Spider': 'Spider',
|
||||||
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
|
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
|
||||||
'In spiderdiagram hat op syn minst trije labels (assen) nedich; eltse rige foarmet in flak.',
|
'In spiderdiagram hat op syn minst trije labels (assen) nedich; eltse rige foarmet in flak.',
|
||||||
|
|
@ -3567,6 +3743,35 @@ const _dutchSourceStringAdditions = {
|
||||||
},
|
},
|
||||||
'pap': {
|
'pap': {
|
||||||
'Annuleren': 'Kanselá',
|
'Annuleren': 'Kanselá',
|
||||||
|
'Checklist': 'Lista di kontrol',
|
||||||
|
'Voortgangsgrafiek tonen': 'Mustra gráfiko di progreso',
|
||||||
|
'Toont afgevinkt en niet afgevinkt als percentages.':
|
||||||
|
'Ta mustra elementonan marká i no marká komo porsentahe.',
|
||||||
|
'Afgevinkt': 'Marká',
|
||||||
|
'Niet afgevinkt': 'No marká',
|
||||||
|
'Er zijn geen aangevinkte checklist-items om te legen.':
|
||||||
|
'No tin elemento di lista marká pa kita.',
|
||||||
|
'Alle checkboxen legen?': 'Kita tur kasita di chèk?',
|
||||||
|
'Hiermee worden alle': 'Esaki ta desmarká tur',
|
||||||
|
'aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.':
|
||||||
|
'elemento di lista marká den henter e presentashon. Bo por deshasé esaki ku Ctrl/Cmd+Z.',
|
||||||
|
'Alles legen': 'Kita tur',
|
||||||
|
'checklist-items uitgevinkt.': 'elemento di lista desmarká.',
|
||||||
|
'Alle checkboxen legen': 'Kita tur kasita di chèk',
|
||||||
|
'Afgevinkte tekst doorhalen': 'Raya teksto marká',
|
||||||
|
'Toont een streep door voltooide checklistitems.':
|
||||||
|
'Ta mustra elementonan kompletá ku un raya den e teksto.',
|
||||||
|
'Na media automatisch doorgaan': 'Sigui outomátiko despues di multimedia',
|
||||||
|
'Opsomming': 'Lista ku punto',
|
||||||
|
'Nummering': 'Numerashon',
|
||||||
|
'Varianten': 'Variantenan',
|
||||||
|
'Grafiekvarianten maken': 'Krea variantenan di gráfiko',
|
||||||
|
'Slides toevoegen': 'Agregá diapositivanan',
|
||||||
|
'Omhoog': 'Move ariba',
|
||||||
|
'Omlaag': 'Move abou',
|
||||||
|
'Niet toevoegen': 'No agregá',
|
||||||
|
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.':
|
||||||
|
'E diapositivanan aki ta usa e mesun datonan, kolónan i título. Usa e flechanan pa skohe nan órden despues di e diapositiva aktual.',
|
||||||
'Spider': 'Radar',
|
'Spider': 'Radar',
|
||||||
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
|
'Een spider-diagram heeft minstens drie labels (assen) nodig; elke reeks vormt een vlak.':
|
||||||
'Un grafiko radar mester di por lo ménos tres etiketa (ehe); kada serie ta forma un superfisie.',
|
'Un grafiko radar mester di por lo ménos tres etiketa (ehe); kada serie ta forma un superfisie.',
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ class ThemeProfile {
|
||||||
final String slideBackgroundColor;
|
final String slideBackgroundColor;
|
||||||
final String textColor;
|
final String textColor;
|
||||||
final String accentColor;
|
final String accentColor;
|
||||||
|
final String checklistCheckedColor;
|
||||||
|
final String checklistUncheckedColor;
|
||||||
|
final bool checklistStrikeThrough;
|
||||||
final String tableTextColor;
|
final String tableTextColor;
|
||||||
final String tableHeaderTextColor;
|
final String tableHeaderTextColor;
|
||||||
final String titleBackgroundColor;
|
final String titleBackgroundColor;
|
||||||
|
|
@ -50,6 +53,9 @@ class ThemeProfile {
|
||||||
this.slideBackgroundColor = '#FFFFFF',
|
this.slideBackgroundColor = '#FFFFFF',
|
||||||
this.textColor = '#222222',
|
this.textColor = '#222222',
|
||||||
this.accentColor = '#2E7D64',
|
this.accentColor = '#2E7D64',
|
||||||
|
this.checklistCheckedColor = '#2E7D64',
|
||||||
|
this.checklistUncheckedColor = '#CBD5E1',
|
||||||
|
this.checklistStrikeThrough = true,
|
||||||
String? tableTextColor,
|
String? tableTextColor,
|
||||||
this.tableHeaderTextColor = '#FFFFFF',
|
this.tableHeaderTextColor = '#FFFFFF',
|
||||||
this.titleBackgroundColor = '#1C2B47',
|
this.titleBackgroundColor = '#1C2B47',
|
||||||
|
|
@ -84,6 +90,9 @@ class ThemeProfile {
|
||||||
String? slideBackgroundColor,
|
String? slideBackgroundColor,
|
||||||
String? textColor,
|
String? textColor,
|
||||||
String? accentColor,
|
String? accentColor,
|
||||||
|
String? checklistCheckedColor,
|
||||||
|
String? checklistUncheckedColor,
|
||||||
|
bool? checklistStrikeThrough,
|
||||||
String? tableTextColor,
|
String? tableTextColor,
|
||||||
String? tableHeaderTextColor,
|
String? tableHeaderTextColor,
|
||||||
String? titleBackgroundColor,
|
String? titleBackgroundColor,
|
||||||
|
|
@ -109,6 +118,12 @@ class ThemeProfile {
|
||||||
slideBackgroundColor: slideBackgroundColor ?? this.slideBackgroundColor,
|
slideBackgroundColor: slideBackgroundColor ?? this.slideBackgroundColor,
|
||||||
textColor: textColor ?? this.textColor,
|
textColor: textColor ?? this.textColor,
|
||||||
accentColor: accentColor ?? this.accentColor,
|
accentColor: accentColor ?? this.accentColor,
|
||||||
|
checklistCheckedColor:
|
||||||
|
checklistCheckedColor ?? this.checklistCheckedColor,
|
||||||
|
checklistUncheckedColor:
|
||||||
|
checklistUncheckedColor ?? this.checklistUncheckedColor,
|
||||||
|
checklistStrikeThrough:
|
||||||
|
checklistStrikeThrough ?? this.checklistStrikeThrough,
|
||||||
tableTextColor: tableTextColor ?? this.tableTextColor,
|
tableTextColor: tableTextColor ?? this.tableTextColor,
|
||||||
tableHeaderTextColor: tableHeaderTextColor ?? this.tableHeaderTextColor,
|
tableHeaderTextColor: tableHeaderTextColor ?? this.tableHeaderTextColor,
|
||||||
titleBackgroundColor: titleBackgroundColor ?? this.titleBackgroundColor,
|
titleBackgroundColor: titleBackgroundColor ?? this.titleBackgroundColor,
|
||||||
|
|
@ -138,6 +153,9 @@ class ThemeProfile {
|
||||||
'name': name,
|
'name': name,
|
||||||
'textColor': textColor,
|
'textColor': textColor,
|
||||||
'accentColor': accentColor,
|
'accentColor': accentColor,
|
||||||
|
'checklistCheckedColor': checklistCheckedColor,
|
||||||
|
'checklistUncheckedColor': checklistUncheckedColor,
|
||||||
|
'checklistStrikeThrough': checklistStrikeThrough,
|
||||||
'tableTextColor': tableTextColor,
|
'tableTextColor': tableTextColor,
|
||||||
'tableHeaderTextColor': tableHeaderTextColor,
|
'tableHeaderTextColor': tableHeaderTextColor,
|
||||||
'titleBackgroundColor': titleBackgroundColor,
|
'titleBackgroundColor': titleBackgroundColor,
|
||||||
|
|
@ -166,6 +184,13 @@ class ThemeProfile {
|
||||||
name: json['name'] as String? ?? 'Standaard',
|
name: json['name'] as String? ?? 'Standaard',
|
||||||
textColor: json['textColor'] as String? ?? '#222222',
|
textColor: json['textColor'] as String? ?? '#222222',
|
||||||
accentColor: json['accentColor'] as String? ?? '#2E7D64',
|
accentColor: json['accentColor'] as String? ?? '#2E7D64',
|
||||||
|
checklistCheckedColor:
|
||||||
|
json['checklistCheckedColor'] as String? ??
|
||||||
|
json['accentColor'] as String? ??
|
||||||
|
'#2E7D64',
|
||||||
|
checklistUncheckedColor:
|
||||||
|
json['checklistUncheckedColor'] as String? ?? '#CBD5E1',
|
||||||
|
checklistStrikeThrough: json['checklistStrikeThrough'] as bool? ?? true,
|
||||||
tableTextColor:
|
tableTextColor:
|
||||||
json['tableTextColor'] as String? ??
|
json['tableTextColor'] as String? ??
|
||||||
json['textColor'] as String? ??
|
json['textColor'] as String? ??
|
||||||
|
|
@ -177,8 +202,7 @@ class ThemeProfile {
|
||||||
titleTextColor: json['titleTextColor'] as String? ?? '#FFFFFF',
|
titleTextColor: json['titleTextColor'] as String? ?? '#FFFFFF',
|
||||||
sectionBackgroundColor:
|
sectionBackgroundColor:
|
||||||
json['sectionBackgroundColor'] as String? ?? '#2E7D64',
|
json['sectionBackgroundColor'] as String? ?? '#2E7D64',
|
||||||
codeBackgroundColor:
|
codeBackgroundColor: json['codeBackgroundColor'] as String? ?? '#282C34',
|
||||||
json['codeBackgroundColor'] as String? ?? '#282C34',
|
|
||||||
codeTextColor: json['codeTextColor'] as String? ?? '#ABB2BF',
|
codeTextColor: json['codeTextColor'] as String? ?? '#ABB2BF',
|
||||||
codeHighlightSyntax: json['codeHighlightSyntax'] as bool? ?? true,
|
codeHighlightSyntax: json['codeHighlightSyntax'] as bool? ?? true,
|
||||||
codeFontFamily: json['codeFontFamily'] as String? ?? 'monospace',
|
codeFontFamily: json['codeFontFamily'] as String? ?? 'monospace',
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,30 @@ enum SlideType {
|
||||||
chart,
|
chart,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ListStyle { bullets, numbered, checklist }
|
||||||
|
|
||||||
|
int bulletLevel(String value) {
|
||||||
|
var level = 0;
|
||||||
|
while (level < value.length && value[level] == '\t') {
|
||||||
|
level++;
|
||||||
|
}
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
String bulletText(String value) => value.substring(bulletLevel(value));
|
||||||
|
|
||||||
|
bool checklistItemChecked(String value) =>
|
||||||
|
RegExp(r'^\[[xX]\]\s*').hasMatch(bulletText(value));
|
||||||
|
|
||||||
|
String checklistItemText(String value) =>
|
||||||
|
bulletText(value).replaceFirst(RegExp(r'^\[[ xX]\]\s*'), '');
|
||||||
|
|
||||||
|
String checklistBullet({
|
||||||
|
required int level,
|
||||||
|
required String text,
|
||||||
|
required bool checked,
|
||||||
|
}) => '${'\t' * level}[${checked ? 'x' : ' '}] $text';
|
||||||
|
|
||||||
extension SlideTypeExtension on SlideType {
|
extension SlideTypeExtension on SlideType {
|
||||||
String get label {
|
String get label {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
|
|
@ -90,6 +114,8 @@ class Slide {
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
final List<String> bullets;
|
final List<String> bullets;
|
||||||
final List<String> bullets2;
|
final List<String> bullets2;
|
||||||
|
final ListStyle listStyle;
|
||||||
|
final bool showChecklistProgress;
|
||||||
|
|
||||||
/// Optional headings above the two bullet columns (twoBullets only). Empty =
|
/// Optional headings above the two bullet columns (twoBullets only). Empty =
|
||||||
/// no heading for that column.
|
/// no heading for that column.
|
||||||
|
|
@ -128,6 +154,8 @@ class Slide {
|
||||||
this.subtitle = '',
|
this.subtitle = '',
|
||||||
this.bullets = const [],
|
this.bullets = const [],
|
||||||
this.bullets2 = const [],
|
this.bullets2 = const [],
|
||||||
|
this.listStyle = ListStyle.bullets,
|
||||||
|
this.showChecklistProgress = false,
|
||||||
this.columnTitle1 = '',
|
this.columnTitle1 = '',
|
||||||
this.columnTitle2 = '',
|
this.columnTitle2 = '',
|
||||||
this.imagePath = '',
|
this.imagePath = '',
|
||||||
|
|
@ -183,6 +211,8 @@ class Slide {
|
||||||
subtitle: src.subtitle,
|
subtitle: src.subtitle,
|
||||||
bullets: List<String>.from(src.bullets),
|
bullets: List<String>.from(src.bullets),
|
||||||
bullets2: List<String>.from(src.bullets2),
|
bullets2: List<String>.from(src.bullets2),
|
||||||
|
listStyle: src.listStyle,
|
||||||
|
showChecklistProgress: src.showChecklistProgress,
|
||||||
columnTitle1: src.columnTitle1,
|
columnTitle1: src.columnTitle1,
|
||||||
columnTitle2: src.columnTitle2,
|
columnTitle2: src.columnTitle2,
|
||||||
imagePath: src.imagePath,
|
imagePath: src.imagePath,
|
||||||
|
|
@ -215,6 +245,8 @@ class Slide {
|
||||||
String? subtitle,
|
String? subtitle,
|
||||||
List<String>? bullets,
|
List<String>? bullets,
|
||||||
List<String>? bullets2,
|
List<String>? bullets2,
|
||||||
|
ListStyle? listStyle,
|
||||||
|
bool? showChecklistProgress,
|
||||||
String? columnTitle1,
|
String? columnTitle1,
|
||||||
String? columnTitle2,
|
String? columnTitle2,
|
||||||
String? imagePath,
|
String? imagePath,
|
||||||
|
|
@ -246,6 +278,9 @@ class Slide {
|
||||||
subtitle: subtitle ?? this.subtitle,
|
subtitle: subtitle ?? this.subtitle,
|
||||||
bullets: bullets ?? this.bullets,
|
bullets: bullets ?? this.bullets,
|
||||||
bullets2: bullets2 ?? this.bullets2,
|
bullets2: bullets2 ?? this.bullets2,
|
||||||
|
listStyle: listStyle ?? this.listStyle,
|
||||||
|
showChecklistProgress:
|
||||||
|
showChecklistProgress ?? this.showChecklistProgress,
|
||||||
columnTitle1: columnTitle1 ?? this.columnTitle1,
|
columnTitle1: columnTitle1 ?? this.columnTitle1,
|
||||||
columnTitle2: columnTitle2 ?? this.columnTitle2,
|
columnTitle2: columnTitle2 ?? this.columnTitle2,
|
||||||
imagePath: imagePath ?? this.imagePath,
|
imagePath: imagePath ?? this.imagePath,
|
||||||
|
|
|
||||||
|
|
@ -216,10 +216,12 @@ class MarkdownService {
|
||||||
case SlideType.bullets:
|
case SlideType.bullets:
|
||||||
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
|
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
|
||||||
if (slide.subtitle.isNotEmpty) buf.writeln('## ${slide.subtitle}');
|
if (slide.subtitle.isNotEmpty) buf.writeln('## ${slide.subtitle}');
|
||||||
buf.writeln();
|
if (slide.listStyle != ListStyle.bullets) {
|
||||||
for (final b in slide.bullets) {
|
buf.writeln('<!-- ocideck_list_style: ${slide.listStyle.name} -->');
|
||||||
_writeBullet(buf, b);
|
|
||||||
}
|
}
|
||||||
|
_writeChecklistProgress(buf, slide);
|
||||||
|
buf.writeln();
|
||||||
|
_writeList(buf, slide.bullets, slide.listStyle);
|
||||||
|
|
||||||
case SlideType.twoBullets:
|
case SlideType.twoBullets:
|
||||||
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
|
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
|
||||||
|
|
@ -230,6 +232,9 @@ class MarkdownService {
|
||||||
slide.bullets2,
|
slide.bullets2,
|
||||||
slide.columnTitle1,
|
slide.columnTitle1,
|
||||||
slide.columnTitle2,
|
slide.columnTitle2,
|
||||||
|
slide.listStyle,
|
||||||
|
slide.showChecklistProgress,
|
||||||
|
themeProfile ?? const ThemeProfile(),
|
||||||
);
|
);
|
||||||
|
|
||||||
case SlideType.bulletsImage:
|
case SlideType.bulletsImage:
|
||||||
|
|
@ -248,10 +253,12 @@ class MarkdownService {
|
||||||
);
|
);
|
||||||
buf.writeln();
|
buf.writeln();
|
||||||
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
|
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
|
||||||
buf.writeln();
|
if (slide.listStyle != ListStyle.bullets) {
|
||||||
for (final b in slide.bullets) {
|
buf.writeln('<!-- ocideck_list_style: ${slide.listStyle.name} -->');
|
||||||
_writeBullet(buf, b);
|
|
||||||
}
|
}
|
||||||
|
_writeChecklistProgress(buf, slide);
|
||||||
|
buf.writeln();
|
||||||
|
_writeList(buf, slide.bullets, slide.listStyle);
|
||||||
buf.writeln();
|
buf.writeln();
|
||||||
buf.writeln('</div>');
|
buf.writeln('</div>');
|
||||||
buf.writeln();
|
buf.writeln();
|
||||||
|
|
@ -263,10 +270,12 @@ class MarkdownService {
|
||||||
buf.writeln('</div>');
|
buf.writeln('</div>');
|
||||||
} else {
|
} else {
|
||||||
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
|
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
|
||||||
buf.writeln();
|
if (slide.listStyle != ListStyle.bullets) {
|
||||||
for (final b in slide.bullets) {
|
buf.writeln('<!-- ocideck_list_style: ${slide.listStyle.name} -->');
|
||||||
_writeBullet(buf, b);
|
|
||||||
}
|
}
|
||||||
|
_writeChecklistProgress(buf, slide);
|
||||||
|
buf.writeln();
|
||||||
|
_writeList(buf, slide.bullets, slide.listStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
case SlideType.twoImages:
|
case SlideType.twoImages:
|
||||||
|
|
@ -401,14 +410,35 @@ class MarkdownService {
|
||||||
return buf.toString();
|
return buf.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _writeBullet(StringBuffer buf, String bullet) {
|
static void _writeList(
|
||||||
|
StringBuffer buf,
|
||||||
|
List<String> items,
|
||||||
|
ListStyle style,
|
||||||
|
) {
|
||||||
|
final counters = <int>[];
|
||||||
|
for (final item in items) {
|
||||||
int level = 0;
|
int level = 0;
|
||||||
while (level < bullet.length && bullet[level] == '\t') {
|
while (level < item.length && item[level] == '\t') {
|
||||||
level++;
|
level++;
|
||||||
}
|
}
|
||||||
final text = bullet.substring(level);
|
final text = item.substring(level);
|
||||||
if (text.isNotEmpty) {
|
if (text.isEmpty) continue;
|
||||||
buf.writeln('${' ' * level}- $text');
|
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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -418,9 +448,18 @@ class MarkdownService {
|
||||||
List<String> right,
|
List<String> right,
|
||||||
String leftTitle,
|
String leftTitle,
|
||||||
String rightTitle,
|
String rightTitle,
|
||||||
|
ListStyle listStyle,
|
||||||
|
bool showChecklistProgress,
|
||||||
|
ThemeProfile themeProfile,
|
||||||
) {
|
) {
|
||||||
buf.writeln('<!-- ocideck_two_bullets_left: ${_encodeBullets(left)} -->');
|
buf.writeln('<!-- ocideck_two_bullets_left: ${_encodeBullets(left)} -->');
|
||||||
buf.writeln('<!-- ocideck_two_bullets_right: ${_encodeBullets(right)} -->');
|
buf.writeln('<!-- ocideck_two_bullets_right: ${_encodeBullets(right)} -->');
|
||||||
|
if (listStyle != ListStyle.bullets) {
|
||||||
|
buf.writeln('<!-- ocideck_list_style: ${listStyle.name} -->');
|
||||||
|
}
|
||||||
|
if (showChecklistProgress) {
|
||||||
|
buf.writeln('<!-- ocideck_checklist_progress: true -->');
|
||||||
|
}
|
||||||
if (leftTitle.isNotEmpty) {
|
if (leftTitle.isNotEmpty) {
|
||||||
buf.writeln(
|
buf.writeln(
|
||||||
'<!-- ocideck_two_bullets_left_title: ${_encodeText(leftTitle)} -->',
|
'<!-- ocideck_two_bullets_left_title: ${_encodeText(leftTitle)} -->',
|
||||||
|
|
@ -434,15 +473,23 @@ class MarkdownService {
|
||||||
buf.writeln(
|
buf.writeln(
|
||||||
'<div class="ocideck-two-bullets" style="display:grid; grid-template-columns:1fr 1fr; gap:3rem; align-items:start;">',
|
'<div class="ocideck-two-bullets" style="display:grid; grid-template-columns:1fr 1fr; gap:3rem; align-items:start;">',
|
||||||
);
|
);
|
||||||
_writeBulletColumn(buf, left, leftTitle);
|
_writeBulletColumn(buf, left, leftTitle, listStyle, themeProfile);
|
||||||
_writeBulletColumn(buf, right, rightTitle);
|
_writeBulletColumn(buf, right, rightTitle, listStyle, themeProfile);
|
||||||
buf.writeln('</div>');
|
buf.writeln('</div>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void _writeChecklistProgress(StringBuffer buf, Slide slide) {
|
||||||
|
if (slide.showChecklistProgress) {
|
||||||
|
buf.writeln('<!-- ocideck_checklist_progress: true -->');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void _writeBulletColumn(
|
static void _writeBulletColumn(
|
||||||
StringBuffer buf,
|
StringBuffer buf,
|
||||||
List<String> bullets,
|
List<String> bullets,
|
||||||
String columnTitle,
|
String columnTitle,
|
||||||
|
ListStyle listStyle,
|
||||||
|
ThemeProfile themeProfile,
|
||||||
) {
|
) {
|
||||||
buf.writeln('<div>');
|
buf.writeln('<div>');
|
||||||
if (columnTitle.isNotEmpty) {
|
if (columnTitle.isNotEmpty) {
|
||||||
|
|
@ -450,9 +497,10 @@ class MarkdownService {
|
||||||
'<h3 style="margin:0 0 .5rem;">${_escapeHtml(columnTitle)}</h3>',
|
'<h3 style="margin:0 0 .5rem;">${_escapeHtml(columnTitle)}</h3>',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
buf.writeln('<ul style="margin:0; padding-left:1.3em;">');
|
final tag = listStyle == ListStyle.numbered ? 'ol' : 'ul';
|
||||||
_writeHtmlBulletItems(buf, bullets);
|
buf.writeln('<$tag style="margin:0; padding-left:1.3em;">');
|
||||||
buf.writeln('</ul>');
|
_writeHtmlBulletItems(buf, bullets, listStyle, themeProfile);
|
||||||
|
buf.writeln('</$tag>');
|
||||||
buf.writeln('</div>');
|
buf.writeln('</div>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -480,16 +528,41 @@ class MarkdownService {
|
||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _writeHtmlBulletItems(StringBuffer buf, List<String> bullets) {
|
static void _writeHtmlBulletItems(
|
||||||
|
StringBuffer buf,
|
||||||
|
List<String> bullets,
|
||||||
|
ListStyle listStyle,
|
||||||
|
ThemeProfile themeProfile,
|
||||||
|
) {
|
||||||
|
final counters = <int>[];
|
||||||
for (final b in bullets) {
|
for (final b in bullets) {
|
||||||
int level = 0;
|
int level = 0;
|
||||||
while (level < b.length && b[level] == '\t') {
|
while (level < b.length && b[level] == '\t') {
|
||||||
level++;
|
level++;
|
||||||
}
|
}
|
||||||
final text = b.substring(level).trim();
|
final text = listStyle == ListStyle.checklist
|
||||||
|
? checklistItemText(b).trim()
|
||||||
|
: b.substring(level).trim();
|
||||||
if (text.isEmpty) continue;
|
if (text.isEmpty) continue;
|
||||||
|
while (counters.length <= level) {
|
||||||
|
counters.add(0);
|
||||||
|
}
|
||||||
|
counters[level]++;
|
||||||
|
if (counters.length > level + 1) {
|
||||||
|
counters.removeRange(level + 1, counters.length);
|
||||||
|
}
|
||||||
final style = level == 0 ? '' : ' style="margin-left:${level * 1.4}em;"';
|
final style = level == 0 ? '' : ' style="margin-left:${level * 1.4}em;"';
|
||||||
buf.writeln('<li$style>${_escapeHtml(text)}</li>');
|
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>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -676,6 +749,8 @@ class MarkdownService {
|
||||||
TlpLevel slideTlp = TlpLevel.none;
|
TlpLevel slideTlp = TlpLevel.none;
|
||||||
final bullets = <String>[];
|
final bullets = <String>[];
|
||||||
var bullets2 = <String>[];
|
var bullets2 = <String>[];
|
||||||
|
var listStyle = ListStyle.bullets;
|
||||||
|
var showChecklistProgress = false;
|
||||||
var columnTitle1 = '';
|
var columnTitle1 = '';
|
||||||
var columnTitle2 = '';
|
var columnTitle2 = '';
|
||||||
// bulletsImage slides store their panel width in `<!-- _style:
|
// bulletsImage slides store their panel width in `<!-- _style:
|
||||||
|
|
@ -704,6 +779,16 @@ class MarkdownService {
|
||||||
columnTitle2 = _decodeText(content.substring(32));
|
columnTitle2 = _decodeText(content.substring(32));
|
||||||
} else if (content.startsWith('ocideck_two_bullets_right:')) {
|
} else if (content.startsWith('ocideck_two_bullets_right:')) {
|
||||||
bullets2 = _decodeBullets(content.substring(26));
|
bullets2 = _decodeBullets(content.substring(26));
|
||||||
|
} else if (content.startsWith('ocideck_list_style:')) {
|
||||||
|
final name = content.substring(19).trim();
|
||||||
|
listStyle = ListStyle.values.firstWhere(
|
||||||
|
(style) => style.name == name,
|
||||||
|
orElse: () => ListStyle.bullets,
|
||||||
|
);
|
||||||
|
} else if (content.startsWith('ocideck_checklist_progress:')) {
|
||||||
|
showChecklistProgress =
|
||||||
|
content.substring('ocideck_checklist_progress:'.length).trim() ==
|
||||||
|
'true';
|
||||||
} else if (!content.startsWith('_')) {
|
} else if (!content.startsWith('_')) {
|
||||||
notesBuffer.write(notesBuffer.isEmpty ? content : '\n$content');
|
notesBuffer.write(notesBuffer.isEmpty ? content : '\n$content');
|
||||||
}
|
}
|
||||||
|
|
@ -773,7 +858,23 @@ class MarkdownService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final level = spaces ~/ 2;
|
final level = spaces ~/ 2;
|
||||||
bullets.add('\t' * level + t.substring(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;
|
||||||
} else if (t.startsWith('> ')) {
|
} else if (t.startsWith('> ')) {
|
||||||
quote = t.substring(2);
|
quote = t.substring(2);
|
||||||
} else if (t.startsWith('— ')) {
|
} else if (t.startsWith('— ')) {
|
||||||
|
|
@ -900,6 +1001,8 @@ class MarkdownService {
|
||||||
subtitle: type == SlideType.section ? paragraph : h2,
|
subtitle: type == SlideType.section ? paragraph : h2,
|
||||||
bullets: bullets,
|
bullets: bullets,
|
||||||
bullets2: bullets2,
|
bullets2: bullets2,
|
||||||
|
listStyle: listStyle,
|
||||||
|
showChecklistProgress: showChecklistProgress,
|
||||||
columnTitle1: columnTitle1,
|
columnTitle1: columnTitle1,
|
||||||
columnTitle2: columnTitle2,
|
columnTitle2: columnTitle2,
|
||||||
imagePath: imagePath,
|
imagePath: imagePath,
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,55 @@ class DeckNotifier extends StateNotifier<DeckState> {
|
||||||
_mutate(deck.copyWith(slides: slides));
|
_mutate(deck.copyWith(slides: slides));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hoeveel checklist-items in de hele presentatie momenteel afgevinkt zijn.
|
||||||
|
int get checkedChecklistCount {
|
||||||
|
final deck = state.deck;
|
||||||
|
if (deck == null) return 0;
|
||||||
|
var total = 0;
|
||||||
|
for (final s in deck.slides) {
|
||||||
|
total += s.bullets.where(checklistItemChecked).length;
|
||||||
|
total += s.bullets2.where(checklistItemChecked).length;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vink in één keer alle checklist-items in de hele presentatie uit (bijv.
|
||||||
|
/// om een ingevulde checklist opnieuw te kunnen aflopen). Eén
|
||||||
|
/// ongedaan-maken-stap. No-op wanneer er niets is aangevinkt.
|
||||||
|
void clearAllChecklists() {
|
||||||
|
final deck = state.deck;
|
||||||
|
if (deck == null) return;
|
||||||
|
String uncheck(String bullet) => checklistItemChecked(bullet)
|
||||||
|
? checklistBullet(
|
||||||
|
level: bulletLevel(bullet),
|
||||||
|
text: checklistItemText(bullet),
|
||||||
|
checked: false,
|
||||||
|
)
|
||||||
|
: bullet;
|
||||||
|
var changed = false;
|
||||||
|
final slides = <Slide>[];
|
||||||
|
for (final s in deck.slides) {
|
||||||
|
if (s.bullets.any(checklistItemChecked) ||
|
||||||
|
s.bullets2.any(checklistItemChecked)) {
|
||||||
|
changed = true;
|
||||||
|
slides.add(
|
||||||
|
s.copyWith(
|
||||||
|
bullets: [for (final b in s.bullets) uncheck(b)],
|
||||||
|
bullets2: [for (final b in s.bullets2) uncheck(b)],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
slides.add(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Bump de revisie zodat de editor van de geselecteerde slide remount en de
|
||||||
|
// uitgevinkte checkboxen ook in het invoerpaneel toont (niet alleen in de
|
||||||
|
// slidepreview).
|
||||||
|
if (changed) {
|
||||||
|
_mutate(deck.copyWith(slides: slides), bumpRevision: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Zoeken & vervangen ─────────────────────────────────────────────────────
|
// ── Zoeken & vervangen ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Tel hoe vaak [query] in alle tekstvelden van de presentatie voorkomt.
|
/// Tel hoe vaak [query] in alle tekstvelden van de presentatie voorkomt.
|
||||||
|
|
@ -509,7 +558,14 @@ class DeckNotifier extends StateNotifier<DeckState> {
|
||||||
/// binnen [_coalesceWindow] valt, wordt geen nieuwe ongedaan-stap aangemaakt
|
/// binnen [_coalesceWindow] valt, wordt geen nieuwe ongedaan-stap aangemaakt
|
||||||
/// (zodat typen niet per teken een aparte stap oplevert). Een [coalesceKey]
|
/// (zodat typen niet per teken een aparte stap oplevert). Een [coalesceKey]
|
||||||
/// van null markeert een losse, discrete stap.
|
/// van null markeert een losse, discrete stap.
|
||||||
void _mutate(Deck deck, {String? coalesceKey}) {
|
///
|
||||||
|
/// Wanneer [bumpRevision] waar is, wordt de inhouds-revisie opgehoogd. Dat
|
||||||
|
/// dwingt de editor-subtree (die op `revision` is gesleuteld) om te remounten
|
||||||
|
/// en zijn velden opnieuw uit de slide te laden. Nodig bij deck-brede
|
||||||
|
/// bewerkingen die de huidige slide aanpassen zonder dat de editor zelf de
|
||||||
|
/// bron van de wijziging was (anders blijft de editor de oude, gecachte
|
||||||
|
/// waarden tonen).
|
||||||
|
void _mutate(Deck deck, {String? coalesceKey, bool bumpRevision = false}) {
|
||||||
final previous = state.deck;
|
final previous = state.deck;
|
||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
@ -532,6 +588,7 @@ class DeckNotifier extends StateNotifier<DeckState> {
|
||||||
isDirty: true,
|
isDirty: true,
|
||||||
canUndo: _undoStack.isNotEmpty,
|
canUndo: _undoStack.isNotEmpty,
|
||||||
canRedo: false,
|
canRedo: false,
|
||||||
|
revision: bumpRevision ? state.revision + 1 : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -886,6 +886,53 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> clearAllChecklists() async {
|
||||||
|
final count = deckNotifier.checkedChecklistCount;
|
||||||
|
if (count == 0) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
l10n.d('Er zijn geen aangevinkte checklist-items om te legen.'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) {
|
||||||
|
final l10n = ctx.l10n;
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(l10n.d('Alle checkboxen legen?')),
|
||||||
|
content: Text(
|
||||||
|
'${l10n.d('Hiermee worden alle')} $count '
|
||||||
|
'${l10n.d('aangevinkte checklist-items in de hele presentatie uitgevinkt. Dit kun je ongedaan maken met Ctrl/Cmd+Z.')}',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: Text(l10n.t('cancel')),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: Text(l10n.d('Alles legen')),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (confirmed != true) return;
|
||||||
|
deckNotifier.clearAllChecklists();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'$count ${l10n.d('checklist-items uitgevinkt.')}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> openImageCarousel() async {
|
Future<void> openImageCarousel() async {
|
||||||
final idx = editor.selectedIndex.clamp(0, deck.slides.length - 1);
|
final idx = editor.selectedIndex.clamp(0, deck.slides.length - 1);
|
||||||
final slide = deck.slides[idx];
|
final slide = deck.slides[idx];
|
||||||
|
|
@ -979,6 +1026,14 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
tlp: deck.tlp,
|
tlp: deck.tlp,
|
||||||
annotations: deck.annotations,
|
annotations: deck.annotations,
|
||||||
onAnnotationsChanged: deckNotifier.setAnnotations,
|
onAnnotationsChanged: deckNotifier.setAnnotations,
|
||||||
|
onSlideChanged: (updated) {
|
||||||
|
final index = deckNotifier.currentState.deck?.slides.indexWhere(
|
||||||
|
(slide) => slide.id == updated.id,
|
||||||
|
);
|
||||||
|
if (index != null && index >= 0) {
|
||||||
|
deckNotifier.updateSlide(index, updated);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1273,6 +1328,8 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
importUrl();
|
importUrl();
|
||||||
case 'find':
|
case 'find':
|
||||||
openFindReplace();
|
openFindReplace();
|
||||||
|
case 'clear_checklists':
|
||||||
|
clearAllChecklists();
|
||||||
case 'full_preview':
|
case 'full_preview':
|
||||||
openFullDeckPreview();
|
openFullDeckPreview();
|
||||||
case 'properties':
|
case 'properties':
|
||||||
|
|
@ -1315,6 +1372,11 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
|
||||||
menuItem('import_url', Icons.link, l10n.t('importUrl')),
|
menuItem('import_url', Icons.link, l10n.t('importUrl')),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
menuItem('find', Icons.find_replace, l10n.t('findReplace')),
|
menuItem('find', Icons.find_replace, l10n.t('findReplace')),
|
||||||
|
menuItem(
|
||||||
|
'clear_checklists',
|
||||||
|
Icons.check_box_outline_blank,
|
||||||
|
l10n.d('Alle checkboxen legen'),
|
||||||
|
),
|
||||||
menuItem(
|
menuItem(
|
||||||
'full_preview',
|
'full_preview',
|
||||||
Icons.preview_outlined,
|
Icons.preview_outlined,
|
||||||
|
|
|
||||||
|
|
@ -942,6 +942,43 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
_themeProfile.accentColor,
|
_themeProfile.accentColor,
|
||||||
(v) => _themeProfile = _themeProfile.copyWith(accentColor: v),
|
(v) => _themeProfile = _themeProfile.copyWith(accentColor: v),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_sectionTitle(l10n.d('Checklist')),
|
||||||
|
_colorSetting(
|
||||||
|
l10n.d('Afgevinkt'),
|
||||||
|
_themeProfile.checklistCheckedColor,
|
||||||
|
(v) =>
|
||||||
|
_themeProfile = _themeProfile.copyWith(checklistCheckedColor: v),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_colorSetting(
|
||||||
|
l10n.d('Niet afgevinkt'),
|
||||||
|
_themeProfile.checklistUncheckedColor,
|
||||||
|
(v) => _themeProfile = _themeProfile.copyWith(
|
||||||
|
checklistUncheckedColor: v,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
SwitchListTile(
|
||||||
|
value: _themeProfile.checklistStrikeThrough,
|
||||||
|
onChanged: (value) => setState(() {
|
||||||
|
_themeProfile = _themeProfile.copyWith(
|
||||||
|
checklistStrikeThrough: value,
|
||||||
|
);
|
||||||
|
_profileTouched = true;
|
||||||
|
}),
|
||||||
|
title: Text(
|
||||||
|
l10n.d('Afgevinkte tekst doorhalen'),
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
l10n.d('Toont een streep door voltooide checklistitems.'),
|
||||||
|
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||||
|
),
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
dense: true,
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_colorSetting(
|
_colorSetting(
|
||||||
l10n.d('Tabeltekst'),
|
l10n.d('Tabeltekst'),
|
||||||
|
|
@ -980,8 +1017,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
_colorSetting(
|
_colorSetting(
|
||||||
l10n.d('Broncode achtergrond'),
|
l10n.d('Broncode achtergrond'),
|
||||||
_themeProfile.codeBackgroundColor,
|
_themeProfile.codeBackgroundColor,
|
||||||
(v) =>
|
(v) => _themeProfile = _themeProfile.copyWith(codeBackgroundColor: v),
|
||||||
_themeProfile = _themeProfile.copyWith(codeBackgroundColor: v),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_colorSetting(
|
_colorSetting(
|
||||||
|
|
@ -1012,7 +1048,8 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
initialValue: AppSettings.codeFonts.contains(_themeProfile.codeFontFamily)
|
initialValue:
|
||||||
|
AppSettings.codeFonts.contains(_themeProfile.codeFontFamily)
|
||||||
? _themeProfile.codeFontFamily
|
? _themeProfile.codeFontFamily
|
||||||
: 'monospace',
|
: 'monospace',
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
|
@ -1282,11 +1319,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: const Color(0xFFCBD5E1)),
|
border: Border.all(color: const Color(0xFFCBD5E1)),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(Icons.tune, size: 18, color: Color(0xFF64748B)),
|
||||||
Icons.tune,
|
|
||||||
size: 18,
|
|
||||||
color: Color(0xFF64748B),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -1363,7 +1396,10 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.d('Bijvoorbeeld #33FF33 voor een CRT-groen scherm.'),
|
l10n.d('Bijvoorbeeld #33FF33 voor een CRT-groen scherm.'),
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Color(0xFF94A3B8),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||||
import '../../models/slide.dart';
|
import '../../models/slide.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '_editor_field.dart';
|
import '_editor_field.dart';
|
||||||
|
import 'list_style_selector.dart';
|
||||||
|
|
||||||
class BulletsEditor extends StatefulWidget {
|
class BulletsEditor extends StatefulWidget {
|
||||||
final Slide slide;
|
final Slide slide;
|
||||||
|
|
@ -19,7 +20,10 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
late final TextEditingController _subtitle;
|
late final TextEditingController _subtitle;
|
||||||
late List<TextEditingController> _bullets;
|
late List<TextEditingController> _bullets;
|
||||||
late List<int> _levels;
|
late List<int> _levels;
|
||||||
|
late List<bool> _checked;
|
||||||
late List<FocusNode> _focusNodes;
|
late List<FocusNode> _focusNodes;
|
||||||
|
late ListStyle _listStyle;
|
||||||
|
late bool _showChecklistProgress;
|
||||||
|
|
||||||
static const _maxLevel = 4;
|
static const _maxLevel = 4;
|
||||||
|
|
||||||
|
|
@ -30,13 +34,16 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
_title.addListener(_emit);
|
_title.addListener(_emit);
|
||||||
_subtitle = TextEditingController(text: widget.slide.subtitle);
|
_subtitle = TextEditingController(text: widget.slide.subtitle);
|
||||||
_subtitle.addListener(_emit);
|
_subtitle.addListener(_emit);
|
||||||
|
_listStyle = widget.slide.listStyle;
|
||||||
|
_showChecklistProgress = widget.slide.showChecklistProgress;
|
||||||
_initBullets(widget.slide.bullets);
|
_initBullets(widget.slide.bullets);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initBullets(List<String> raw) {
|
void _initBullets(List<String> raw) {
|
||||||
final list = raw.isEmpty ? [''] : raw;
|
final list = raw.isEmpty ? [''] : raw;
|
||||||
_levels = list.map(_levelOf).toList();
|
_levels = list.map(_levelOf).toList();
|
||||||
_bullets = list.map((b) => _makeCtrl(b.trimLeft())).toList();
|
_checked = list.map(checklistItemChecked).toList();
|
||||||
|
_bullets = list.map((b) => _makeCtrl(checklistItemText(b))).toList();
|
||||||
_focusNodes = List.generate(_bullets.length, (_) => FocusNode());
|
_focusNodes = List.generate(_bullets.length, (_) => FocusNode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,9 +66,17 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
widget.slide.copyWith(
|
widget.slide.copyWith(
|
||||||
title: _title.text,
|
title: _title.text,
|
||||||
subtitle: _subtitle.text,
|
subtitle: _subtitle.text,
|
||||||
|
listStyle: _listStyle,
|
||||||
|
showChecklistProgress: _showChecklistProgress,
|
||||||
bullets: List.generate(
|
bullets: List.generate(
|
||||||
_bullets.length,
|
_bullets.length,
|
||||||
(i) => '\t' * _levels[i] + _bullets[i].text,
|
(i) => _listStyle == ListStyle.checklist
|
||||||
|
? checklistBullet(
|
||||||
|
level: _levels[i],
|
||||||
|
text: _bullets[i].text,
|
||||||
|
checked: _checked[i],
|
||||||
|
)
|
||||||
|
: '\t' * _levels[i] + _bullets[i].text,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -75,9 +90,11 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
setState(() {
|
setState(() {
|
||||||
final ctrl = _bullets.removeAt(oldIndex);
|
final ctrl = _bullets.removeAt(oldIndex);
|
||||||
final level = _levels.removeAt(oldIndex);
|
final level = _levels.removeAt(oldIndex);
|
||||||
|
final checked = _checked.removeAt(oldIndex);
|
||||||
final focus = _focusNodes.removeAt(oldIndex);
|
final focus = _focusNodes.removeAt(oldIndex);
|
||||||
_bullets.insert(newIndex, ctrl);
|
_bullets.insert(newIndex, ctrl);
|
||||||
_levels.insert(newIndex, level);
|
_levels.insert(newIndex, level);
|
||||||
|
_checked.insert(newIndex, checked);
|
||||||
_focusNodes.insert(newIndex, focus);
|
_focusNodes.insert(newIndex, focus);
|
||||||
});
|
});
|
||||||
_emit();
|
_emit();
|
||||||
|
|
@ -88,6 +105,7 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_bullets.insert(i + 1, _makeCtrl(''));
|
_bullets.insert(i + 1, _makeCtrl(''));
|
||||||
_levels.insert(i + 1, newLevel);
|
_levels.insert(i + 1, newLevel);
|
||||||
|
_checked.insert(i + 1, false);
|
||||||
_focusNodes.insert(i + 1, FocusNode());
|
_focusNodes.insert(i + 1, FocusNode());
|
||||||
});
|
});
|
||||||
_emit();
|
_emit();
|
||||||
|
|
@ -97,13 +115,25 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeBulletAndFocus(int i) {
|
void _removeBulletAndFocus(int i) {
|
||||||
if (_bullets.length <= 1) return;
|
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;
|
||||||
|
}
|
||||||
final target = (i - 1).clamp(0, _bullets.length - 2);
|
final target = (i - 1).clamp(0, _bullets.length - 2);
|
||||||
setState(() {
|
setState(() {
|
||||||
_bullets[i].removeListener(_emit);
|
_bullets[i].removeListener(_emit);
|
||||||
_bullets[i].dispose();
|
_bullets[i].dispose();
|
||||||
_bullets.removeAt(i);
|
_bullets.removeAt(i);
|
||||||
_levels.removeAt(i);
|
_levels.removeAt(i);
|
||||||
|
_checked.removeAt(i);
|
||||||
_focusNodes[i].dispose();
|
_focusNodes[i].dispose();
|
||||||
_focusNodes.removeAt(i);
|
_focusNodes.removeAt(i);
|
||||||
});
|
});
|
||||||
|
|
@ -142,6 +172,7 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
for (int j = 1; j < lines.length; j++) {
|
for (int j = 1; j < lines.length; j++) {
|
||||||
_bullets.insert(i + j, _makeCtrl(lines[j]));
|
_bullets.insert(i + j, _makeCtrl(lines[j]));
|
||||||
_levels.insert(i + j, _levels[i]);
|
_levels.insert(i + j, _levels[i]);
|
||||||
|
_checked.insert(i + j, false);
|
||||||
_focusNodes.insert(i + j, FocusNode());
|
_focusNodes.insert(i + j, FocusNode());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -179,6 +210,27 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
hint: l10n.d('Subkop'),
|
hint: l10n.d('Subkop'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
ListStyleSelector(
|
||||||
|
value: _listStyle,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _listStyle = value);
|
||||||
|
_emit();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (_listStyle == ListStyle.checklist)
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(l10n.d('Voortgangsgrafiek tonen')),
|
||||||
|
subtitle: Text(
|
||||||
|
l10n.d('Toont afgevinkt en niet afgevinkt als percentages.'),
|
||||||
|
),
|
||||||
|
value: _showChecklistProgress,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _showChecklistProgress = value);
|
||||||
|
_emit();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
const SectionLabel('Bullets'),
|
const SectionLabel('Bullets'),
|
||||||
ReorderableListView(
|
ReorderableListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
|
@ -220,8 +272,23 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
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(
|
Text(
|
||||||
_markerForLevel(level),
|
_markerForItem(i),
|
||||||
style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|
@ -271,14 +338,13 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
key: ValueKey('remove-bullet-$i'),
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.remove_circle_outline,
|
Icons.remove_circle_outline,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: Color(0xFF94A3B8),
|
color: Color(0xFF94A3B8),
|
||||||
),
|
),
|
||||||
onPressed: _bullets.length > 1
|
onPressed: () => _removeBulletAndFocus(i),
|
||||||
? () => _removeBulletAndFocus(i)
|
|
||||||
: null,
|
|
||||||
tooltip: l10n.d('Verwijder'),
|
tooltip: l10n.d('Verwijder'),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
constraints: const BoxConstraints(minWidth: 28),
|
constraints: const BoxConstraints(minWidth: 28),
|
||||||
|
|
@ -292,4 +358,18 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
const markers = ['•', '◦', '▪', '▫', '–'];
|
const markers = ['•', '◦', '▪', '▫', '–'];
|
||||||
return markers[level.clamp(0, markers.length - 1)];
|
return markers[level.clamp(0, markers.length - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _markerForItem(int index) {
|
||||||
|
if (_listStyle == ListStyle.bullets) {
|
||||||
|
return _markerForLevel(_levels[index]);
|
||||||
|
}
|
||||||
|
if (_listStyle == ListStyle.checklist) return '';
|
||||||
|
final level = _levels[index];
|
||||||
|
var number = 0;
|
||||||
|
for (var i = 0; i <= index; i++) {
|
||||||
|
if (_levels[i] == level) number++;
|
||||||
|
if (_levels[i] < level) number = 0;
|
||||||
|
}
|
||||||
|
return '$number.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import '../../models/slide.dart';
|
||||||
import '../../services/image_service.dart';
|
import '../../services/image_service.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '_editor_field.dart';
|
import '_editor_field.dart';
|
||||||
|
import 'list_style_selector.dart';
|
||||||
|
|
||||||
class BulletsImageEditor extends StatefulWidget {
|
class BulletsImageEditor extends StatefulWidget {
|
||||||
final Slide slide;
|
final Slide slide;
|
||||||
|
|
@ -29,7 +30,10 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
||||||
late final TextEditingController _title;
|
late final TextEditingController _title;
|
||||||
late List<TextEditingController> _bullets;
|
late List<TextEditingController> _bullets;
|
||||||
late List<int> _levels;
|
late List<int> _levels;
|
||||||
|
late List<bool> _checked;
|
||||||
late List<FocusNode> _focusNodes;
|
late List<FocusNode> _focusNodes;
|
||||||
|
late ListStyle _listStyle;
|
||||||
|
late bool _showChecklistProgress;
|
||||||
|
|
||||||
static const _maxLevel = 4;
|
static const _maxLevel = 4;
|
||||||
|
|
||||||
|
|
@ -38,9 +42,12 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_title = TextEditingController(text: widget.slide.title);
|
_title = TextEditingController(text: widget.slide.title);
|
||||||
_title.addListener(_emit);
|
_title.addListener(_emit);
|
||||||
|
_listStyle = widget.slide.listStyle;
|
||||||
|
_showChecklistProgress = widget.slide.showChecklistProgress;
|
||||||
final list = widget.slide.bullets.isEmpty ? [''] : widget.slide.bullets;
|
final list = widget.slide.bullets.isEmpty ? [''] : widget.slide.bullets;
|
||||||
_levels = list.map(_levelOf).toList();
|
_levels = list.map(_levelOf).toList();
|
||||||
_bullets = list.map((b) => _makeCtrl(b.trimLeft())).toList();
|
_checked = list.map(checklistItemChecked).toList();
|
||||||
|
_bullets = list.map((b) => _makeCtrl(checklistItemText(b))).toList();
|
||||||
_focusNodes = List.generate(_bullets.length, (_) => FocusNode());
|
_focusNodes = List.generate(_bullets.length, (_) => FocusNode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,9 +69,17 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
||||||
widget.onUpdate(
|
widget.onUpdate(
|
||||||
widget.slide.copyWith(
|
widget.slide.copyWith(
|
||||||
title: _title.text,
|
title: _title.text,
|
||||||
|
listStyle: _listStyle,
|
||||||
|
showChecklistProgress: _showChecklistProgress,
|
||||||
bullets: List.generate(
|
bullets: List.generate(
|
||||||
_bullets.length,
|
_bullets.length,
|
||||||
(i) => '\t' * _levels[i] + _bullets[i].text,
|
(i) => _listStyle == ListStyle.checklist
|
||||||
|
? checklistBullet(
|
||||||
|
level: _levels[i],
|
||||||
|
text: _bullets[i].text,
|
||||||
|
checked: _checked[i],
|
||||||
|
)
|
||||||
|
: '\t' * _levels[i] + _bullets[i].text,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -74,9 +89,11 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
||||||
setState(() {
|
setState(() {
|
||||||
final ctrl = _bullets.removeAt(oldIndex);
|
final ctrl = _bullets.removeAt(oldIndex);
|
||||||
final level = _levels.removeAt(oldIndex);
|
final level = _levels.removeAt(oldIndex);
|
||||||
|
final checked = _checked.removeAt(oldIndex);
|
||||||
final focus = _focusNodes.removeAt(oldIndex);
|
final focus = _focusNodes.removeAt(oldIndex);
|
||||||
_bullets.insert(newIndex, ctrl);
|
_bullets.insert(newIndex, ctrl);
|
||||||
_levels.insert(newIndex, level);
|
_levels.insert(newIndex, level);
|
||||||
|
_checked.insert(newIndex, checked);
|
||||||
_focusNodes.insert(newIndex, focus);
|
_focusNodes.insert(newIndex, focus);
|
||||||
});
|
});
|
||||||
_emit();
|
_emit();
|
||||||
|
|
@ -86,6 +103,7 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_bullets.insert(i + 1, _makeCtrl(''));
|
_bullets.insert(i + 1, _makeCtrl(''));
|
||||||
_levels.insert(i + 1, _levels[i]);
|
_levels.insert(i + 1, _levels[i]);
|
||||||
|
_checked.insert(i + 1, false);
|
||||||
_focusNodes.insert(i + 1, FocusNode());
|
_focusNodes.insert(i + 1, FocusNode());
|
||||||
});
|
});
|
||||||
_emit();
|
_emit();
|
||||||
|
|
@ -95,13 +113,25 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeBulletAndFocus(int i) {
|
void _removeBulletAndFocus(int i) {
|
||||||
if (_bullets.length <= 1) return;
|
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;
|
||||||
|
}
|
||||||
final target = (i - 1).clamp(0, _bullets.length - 2);
|
final target = (i - 1).clamp(0, _bullets.length - 2);
|
||||||
setState(() {
|
setState(() {
|
||||||
_bullets[i].removeListener(_emit);
|
_bullets[i].removeListener(_emit);
|
||||||
_bullets[i].dispose();
|
_bullets[i].dispose();
|
||||||
_bullets.removeAt(i);
|
_bullets.removeAt(i);
|
||||||
_levels.removeAt(i);
|
_levels.removeAt(i);
|
||||||
|
_checked.removeAt(i);
|
||||||
_focusNodes[i].dispose();
|
_focusNodes[i].dispose();
|
||||||
_focusNodes.removeAt(i);
|
_focusNodes.removeAt(i);
|
||||||
});
|
});
|
||||||
|
|
@ -138,6 +168,7 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
||||||
for (int j = 1; j < lines.length; j++) {
|
for (int j = 1; j < lines.length; j++) {
|
||||||
_bullets.insert(i + j, _makeCtrl(lines[j]));
|
_bullets.insert(i + j, _makeCtrl(lines[j]));
|
||||||
_levels.insert(i + j, _levels[i]);
|
_levels.insert(i + j, _levels[i]);
|
||||||
|
_checked.insert(i + j, false);
|
||||||
_focusNodes.insert(i + j, FocusNode());
|
_focusNodes.insert(i + j, FocusNode());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -184,6 +215,27 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
||||||
children: [
|
children: [
|
||||||
EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'),
|
EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
ListStyleSelector(
|
||||||
|
value: _listStyle,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _listStyle = value);
|
||||||
|
_emit();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (_listStyle == ListStyle.checklist)
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(l10n.d('Voortgangsgrafiek tonen')),
|
||||||
|
subtitle: Text(
|
||||||
|
l10n.d('Toont afgevinkt en niet afgevinkt als percentages.'),
|
||||||
|
),
|
||||||
|
value: _showChecklistProgress,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _showChecklistProgress = value);
|
||||||
|
_emit();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
const SectionLabel('Bullets (links)'),
|
const SectionLabel('Bullets (links)'),
|
||||||
ReorderableListView(
|
ReorderableListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
|
@ -254,8 +306,23 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
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(
|
Text(
|
||||||
_markerForLevel(level),
|
_markerForItem(i),
|
||||||
style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|
@ -301,14 +368,13 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
key: ValueKey('remove-bullet-$i'),
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.remove_circle_outline,
|
Icons.remove_circle_outline,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: Color(0xFF94A3B8),
|
color: Color(0xFF94A3B8),
|
||||||
),
|
),
|
||||||
onPressed: _bullets.length > 1
|
onPressed: () => _removeBulletAndFocus(i),
|
||||||
? () => _removeBulletAndFocus(i)
|
|
||||||
: null,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
constraints: const BoxConstraints(minWidth: 28),
|
constraints: const BoxConstraints(minWidth: 28),
|
||||||
),
|
),
|
||||||
|
|
@ -321,4 +387,18 @@ class _BulletsImageEditorState extends State<BulletsImageEditor> {
|
||||||
const markers = ['•', '◦', '▪', '▫', '–'];
|
const markers = ['•', '◦', '▪', '▫', '–'];
|
||||||
return markers[level.clamp(0, markers.length - 1)];
|
return markers[level.clamp(0, markers.length - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _markerForItem(int index) {
|
||||||
|
if (_listStyle == ListStyle.bullets) {
|
||||||
|
return _markerForLevel(_levels[index]);
|
||||||
|
}
|
||||||
|
if (_listStyle == ListStyle.checklist) return '';
|
||||||
|
final level = _levels[index];
|
||||||
|
var number = 0;
|
||||||
|
for (var i = 0; i <= index; i++) {
|
||||||
|
if (_levels[i] == level) number++;
|
||||||
|
if (_levels[i] < level) number = 0;
|
||||||
|
}
|
||||||
|
return '$number.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,14 @@ import '_editor_field.dart';
|
||||||
class ChartEditor extends StatefulWidget {
|
class ChartEditor extends StatefulWidget {
|
||||||
final Slide slide;
|
final Slide slide;
|
||||||
final ValueChanged<Slide> onUpdate;
|
final ValueChanged<Slide> onUpdate;
|
||||||
|
final ValueChanged<List<Slide>>? onAddVariants;
|
||||||
final String? projectPath;
|
final String? projectPath;
|
||||||
|
|
||||||
const ChartEditor({
|
const ChartEditor({
|
||||||
super.key,
|
super.key,
|
||||||
required this.slide,
|
required this.slide,
|
||||||
required this.onUpdate,
|
required this.onUpdate,
|
||||||
|
this.onAddVariants,
|
||||||
this.projectPath,
|
this.projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -30,6 +32,114 @@ class ChartEditor extends StatefulWidget {
|
||||||
State<ChartEditor> createState() => _ChartEditorState();
|
State<ChartEditor> createState() => _ChartEditorState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ChartVariantsDialog extends StatefulWidget {
|
||||||
|
final ChartType currentType;
|
||||||
|
|
||||||
|
const _ChartVariantsDialog({required this.currentType});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ChartVariantsDialog> createState() => _ChartVariantsDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChartVariantsDialogState extends State<_ChartVariantsDialog> {
|
||||||
|
late final List<ChartType> _types = [
|
||||||
|
for (final type in ChartType.values)
|
||||||
|
if (type != widget.currentType) type,
|
||||||
|
];
|
||||||
|
|
||||||
|
String _label(BuildContext context, ChartType type) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return switch (type) {
|
||||||
|
ChartType.bar => l10n.d('Staaf'),
|
||||||
|
ChartType.line => l10n.d('Lijn'),
|
||||||
|
ChartType.pie => l10n.d('Cirkel'),
|
||||||
|
ChartType.radar => l10n.d('Spider'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void _move(int index, int delta) {
|
||||||
|
final target = index + delta;
|
||||||
|
if (target < 0 || target >= _types.length) return;
|
||||||
|
setState(() {
|
||||||
|
final type = _types.removeAt(index);
|
||||||
|
_types.insert(target, type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(l10n.d('Grafiekvarianten maken')),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 420,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.d(
|
||||||
|
'Deze slides gebruiken dezelfde data, kleuren en titel. Kies met de pijlen de volgorde na de huidige slide.',
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 12, color: Color(0xFF64748B)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
for (var i = 0; i < _types.length; i++)
|
||||||
|
ListTile(
|
||||||
|
key: ValueKey('chart-variant-${_types[i].name}'),
|
||||||
|
dense: true,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: Icon(switch (_types[i]) {
|
||||||
|
ChartType.bar => Icons.bar_chart,
|
||||||
|
ChartType.line => Icons.show_chart,
|
||||||
|
ChartType.pie => Icons.pie_chart_outline,
|
||||||
|
ChartType.radar => Icons.radar,
|
||||||
|
}),
|
||||||
|
title: Text(_label(context, _types[i])),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
key: ValueKey('chart-variant-up-$i'),
|
||||||
|
onPressed: i == 0 ? null : () => _move(i, -1),
|
||||||
|
icon: const Icon(Icons.arrow_upward, size: 18),
|
||||||
|
tooltip: l10n.d('Omhoog'),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
key: ValueKey('chart-variant-down-$i'),
|
||||||
|
onPressed: i == _types.length - 1
|
||||||
|
? null
|
||||||
|
: () => _move(i, 1),
|
||||||
|
icon: const Icon(Icons.arrow_downward, size: 18),
|
||||||
|
tooltip: l10n.d('Omlaag'),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => setState(() => _types.removeAt(i)),
|
||||||
|
icon: const Icon(Icons.close, size: 18),
|
||||||
|
tooltip: l10n.d('Niet toevoegen'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(l10n.d('Annuleren')),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: _types.isEmpty
|
||||||
|
? null
|
||||||
|
: () => Navigator.pop(context, List<ChartType>.from(_types)),
|
||||||
|
child: Text(l10n.d('Slides toevoegen')),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ChartEditorState extends State<ChartEditor> {
|
class _ChartEditorState extends State<ChartEditor> {
|
||||||
late final TextEditingController _title;
|
late final TextEditingController _title;
|
||||||
late final TextEditingController _minBound;
|
late final TextEditingController _minBound;
|
||||||
|
|
@ -111,7 +221,7 @@ class _ChartEditorState extends State<ChartEditor> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _emit() {
|
ChartSpec _currentSpec() {
|
||||||
final series = <ChartSeries>[
|
final series = <ChartSeries>[
|
||||||
for (var c = 0; c < _seriesNames.length; c++)
|
for (var c = 0; c < _seriesNames.length; c++)
|
||||||
ChartSeries(
|
ChartSeries(
|
||||||
|
|
@ -128,7 +238,7 @@ class _ChartEditorState extends State<ChartEditor> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
final spec = ChartSpec(
|
return ChartSpec(
|
||||||
type: _type,
|
type: _type,
|
||||||
title: _title.text,
|
title: _title.text,
|
||||||
source: _source,
|
source: _source,
|
||||||
|
|
@ -138,7 +248,33 @@ class _ChartEditorState extends State<ChartEditor> {
|
||||||
minBound: _supportsBounds ? _parseBound(_minBound.text) : null,
|
minBound: _supportsBounds ? _parseBound(_minBound.text) : null,
|
||||||
maxBound: _supportsBounds ? _parseBound(_maxBound.text) : null,
|
maxBound: _supportsBounds ? _parseBound(_maxBound.text) : null,
|
||||||
);
|
);
|
||||||
widget.onUpdate(widget.slide.copyWith(customMarkdown: spec.toBlock()));
|
}
|
||||||
|
|
||||||
|
void _emit() {
|
||||||
|
widget.onUpdate(
|
||||||
|
widget.slide.copyWith(customMarkdown: _currentSpec().toBlock()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createVariants() async {
|
||||||
|
final selected = await showDialog<List<ChartType>>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _ChartVariantsDialog(currentType: _type),
|
||||||
|
);
|
||||||
|
if (selected == null || selected.isEmpty) return;
|
||||||
|
final base = _currentSpec();
|
||||||
|
widget.onAddVariants?.call([
|
||||||
|
for (final type in selected)
|
||||||
|
widget.slide.copyWith(
|
||||||
|
customMarkdown: base
|
||||||
|
.copyWith(
|
||||||
|
type: type,
|
||||||
|
clearMinBound: type == ChartType.pie,
|
||||||
|
clearMaxBound: type == ChartType.pie,
|
||||||
|
)
|
||||||
|
.toBlock(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _bump() => setState(() => _rev++);
|
void _bump() => setState(() => _rev++);
|
||||||
|
|
@ -481,6 +617,15 @@ class _ChartEditorState extends State<ChartEditor> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
if (widget.onAddVariants != null) ...[
|
||||||
|
TextButton.icon(
|
||||||
|
key: const ValueKey('chart-create-variants'),
|
||||||
|
onPressed: _createVariants,
|
||||||
|
icon: const Icon(Icons.auto_awesome_motion, size: 16),
|
||||||
|
label: Text(l10n.d('Varianten')),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: _importCsv,
|
onPressed: _importCsv,
|
||||||
icon: const Icon(Icons.upload_file, size: 16),
|
icon: const Icon(Icons.upload_file, size: 16),
|
||||||
|
|
@ -787,9 +932,7 @@ class _ChartEditorState extends State<ChartEditor> {
|
||||||
decimal: true,
|
decimal: true,
|
||||||
signed: true,
|
signed: true,
|
||||||
),
|
),
|
||||||
inputFormatters: [
|
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.,\-]'))],
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'[0-9.,\-]')),
|
|
||||||
],
|
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: label,
|
labelText: label,
|
||||||
|
|
|
||||||
|
|
@ -35,17 +35,28 @@ class _ImageSlideEditorState extends State<ImageSlideEditor> {
|
||||||
|
|
||||||
void _emit() => widget.onUpdate(widget.slide.copyWith(title: _title.text));
|
void _emit() => widget.onUpdate(widget.slide.copyWith(title: _title.text));
|
||||||
|
|
||||||
|
void _setImage(String path, {String caption = ''}) {
|
||||||
|
widget.onUpdate(
|
||||||
|
widget.slide.copyWith(
|
||||||
|
imagePath: path,
|
||||||
|
imageCaption: caption,
|
||||||
|
// A full-slide image should start at the largest uncropped size.
|
||||||
|
imageSize: 100,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _pasteImage() async {
|
Future<void> _pasteImage() async {
|
||||||
final path = await widget.imageService.pasteImage();
|
final path = await widget.imageService.pasteImage();
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
widget.onUpdate(widget.slide.copyWith(imagePath: path, imageCaption: ''));
|
_setImage(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickImage() async {
|
Future<void> _pickImage() async {
|
||||||
final path = await widget.imageService.pickImage();
|
final path = await widget.imageService.pickImage();
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
widget.onUpdate(widget.slide.copyWith(imagePath: path, imageCaption: ''));
|
_setImage(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,9 +77,7 @@ class _ImageSlideEditorState extends State<ImageSlideEditor> {
|
||||||
imageCaption: widget.slide.imageCaption,
|
imageCaption: widget.slide.imageCaption,
|
||||||
searchPaths: widget.searchPaths,
|
searchPaths: widget.searchPaths,
|
||||||
captionBasePath: widget.captionBasePath,
|
captionBasePath: widget.captionBasePath,
|
||||||
onPicked: (path, caption) => widget.onUpdate(
|
onPicked: (path, caption) => _setImage(path, caption: caption),
|
||||||
widget.slide.copyWith(imagePath: path, imageCaption: caption),
|
|
||||||
),
|
|
||||||
onBrowse: _pickImage,
|
onBrowse: _pickImage,
|
||||||
onPaste: _pasteImage,
|
onPaste: _pasteImage,
|
||||||
onClear: widget.slide.imagePath.isNotEmpty
|
onClear: widget.slide.imagePath.isNotEmpty
|
||||||
|
|
|
||||||
41
lib/widgets/editors/list_style_selector.dart
Normal file
41
lib/widgets/editors/list_style_selector.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
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,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||||
import '../../models/slide.dart';
|
import '../../models/slide.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '_editor_field.dart';
|
import '_editor_field.dart';
|
||||||
|
import 'list_style_selector.dart';
|
||||||
|
|
||||||
typedef _Mutate = void Function(VoidCallback fn);
|
typedef _Mutate = void Function(VoidCallback fn);
|
||||||
|
|
||||||
|
|
@ -26,6 +27,8 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
||||||
late final TextEditingController _heading2;
|
late final TextEditingController _heading2;
|
||||||
late _BulletSet _left;
|
late _BulletSet _left;
|
||||||
late _BulletSet _right;
|
late _BulletSet _right;
|
||||||
|
late ListStyle _listStyle;
|
||||||
|
late bool _showChecklistProgress;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -36,6 +39,8 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
||||||
_heading2 = TextEditingController(text: widget.slide.columnTitle2);
|
_heading2 = TextEditingController(text: widget.slide.columnTitle2);
|
||||||
_heading1.addListener(_emit);
|
_heading1.addListener(_emit);
|
||||||
_heading2.addListener(_emit);
|
_heading2.addListener(_emit);
|
||||||
|
_listStyle = widget.slide.listStyle;
|
||||||
|
_showChecklistProgress = widget.slide.showChecklistProgress;
|
||||||
_left = _BulletSet(widget.slide.bullets, _emit);
|
_left = _BulletSet(widget.slide.bullets, _emit);
|
||||||
_right = _BulletSet(widget.slide.bullets2, _emit);
|
_right = _BulletSet(widget.slide.bullets2, _emit);
|
||||||
}
|
}
|
||||||
|
|
@ -46,8 +51,10 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
||||||
title: _title.text,
|
title: _title.text,
|
||||||
columnTitle1: _heading1.text,
|
columnTitle1: _heading1.text,
|
||||||
columnTitle2: _heading2.text,
|
columnTitle2: _heading2.text,
|
||||||
bullets: _left.values,
|
listStyle: _listStyle,
|
||||||
bullets2: _right.values,
|
showChecklistProgress: _showChecklistProgress,
|
||||||
|
bullets: _left.values(_listStyle),
|
||||||
|
bullets2: _right.values(_listStyle),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +76,29 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
||||||
children: [
|
children: [
|
||||||
EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'),
|
EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
ListStyleSelector(
|
||||||
|
value: _listStyle,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _listStyle = value);
|
||||||
|
_emit();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (_listStyle == ListStyle.checklist)
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(context.l10n.d('Voortgangsgrafiek tonen')),
|
||||||
|
subtitle: Text(
|
||||||
|
context.l10n.d(
|
||||||
|
'Toont afgevinkt en niet afgevinkt als percentages.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
value: _showChecklistProgress,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _showChecklistProgress = value);
|
||||||
|
_emit();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final narrow = constraints.maxWidth < 560;
|
final narrow = constraints.maxWidth < 560;
|
||||||
|
|
@ -78,12 +108,14 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
||||||
set: _left,
|
set: _left,
|
||||||
emit: _emit,
|
emit: _emit,
|
||||||
headingController: _heading1,
|
headingController: _heading1,
|
||||||
|
listStyle: _listStyle,
|
||||||
),
|
),
|
||||||
_BulletColumn(
|
_BulletColumn(
|
||||||
label: 'Bullets rechts',
|
label: 'Bullets rechts',
|
||||||
set: _right,
|
set: _right,
|
||||||
emit: _emit,
|
emit: _emit,
|
||||||
headingController: _heading2,
|
headingController: _heading2,
|
||||||
|
listStyle: _listStyle,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (narrow) {
|
if (narrow) {
|
||||||
|
|
@ -112,18 +144,26 @@ class _BulletSet {
|
||||||
final VoidCallback emit;
|
final VoidCallback emit;
|
||||||
late List<TextEditingController> controllers;
|
late List<TextEditingController> controllers;
|
||||||
late List<int> levels;
|
late List<int> levels;
|
||||||
|
late List<bool> checked;
|
||||||
late List<FocusNode> focusNodes;
|
late List<FocusNode> focusNodes;
|
||||||
|
|
||||||
_BulletSet(List<String> raw, this.emit) {
|
_BulletSet(List<String> raw, this.emit) {
|
||||||
final list = raw.isEmpty ? [''] : raw;
|
final list = raw.isEmpty ? [''] : raw;
|
||||||
levels = list.map(_levelOf).toList();
|
levels = list.map(_levelOf).toList();
|
||||||
controllers = list.map((b) => _makeCtrl(b.trimLeft())).toList();
|
checked = list.map(checklistItemChecked).toList();
|
||||||
|
controllers = list.map((b) => _makeCtrl(checklistItemText(b))).toList();
|
||||||
focusNodes = List.generate(controllers.length, (_) => FocusNode());
|
focusNodes = List.generate(controllers.length, (_) => FocusNode());
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> get values => List.generate(
|
List<String> values(ListStyle listStyle) => List.generate(
|
||||||
controllers.length,
|
controllers.length,
|
||||||
(i) => '\t' * levels[i] + controllers[i].text,
|
(i) => listStyle == ListStyle.checklist
|
||||||
|
? checklistBullet(
|
||||||
|
level: levels[i],
|
||||||
|
text: controllers[i].text,
|
||||||
|
checked: checked[i],
|
||||||
|
)
|
||||||
|
: '\t' * levels[i] + controllers[i].text,
|
||||||
);
|
);
|
||||||
|
|
||||||
static int _levelOf(String b) {
|
static int _levelOf(String b) {
|
||||||
|
|
@ -144,6 +184,7 @@ class _BulletSet {
|
||||||
mutate(() {
|
mutate(() {
|
||||||
controllers.insert(i + 1, _makeCtrl(''));
|
controllers.insert(i + 1, _makeCtrl(''));
|
||||||
levels.insert(i + 1, levels[i]);
|
levels.insert(i + 1, levels[i]);
|
||||||
|
checked.insert(i + 1, false);
|
||||||
focusNodes.insert(i + 1, FocusNode());
|
focusNodes.insert(i + 1, FocusNode());
|
||||||
});
|
});
|
||||||
emit();
|
emit();
|
||||||
|
|
@ -153,13 +194,25 @@ class _BulletSet {
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeAndFocus(_Mutate mutate, int i) {
|
void removeAndFocus(_Mutate mutate, int i) {
|
||||||
if (controllers.length <= 1) return;
|
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;
|
||||||
|
}
|
||||||
final target = (i - 1).clamp(0, controllers.length - 2);
|
final target = (i - 1).clamp(0, controllers.length - 2);
|
||||||
mutate(() {
|
mutate(() {
|
||||||
controllers[i].removeListener(emit);
|
controllers[i].removeListener(emit);
|
||||||
controllers[i].dispose();
|
controllers[i].dispose();
|
||||||
controllers.removeAt(i);
|
controllers.removeAt(i);
|
||||||
levels.removeAt(i);
|
levels.removeAt(i);
|
||||||
|
checked.removeAt(i);
|
||||||
focusNodes[i].dispose();
|
focusNodes[i].dispose();
|
||||||
focusNodes.removeAt(i);
|
focusNodes.removeAt(i);
|
||||||
});
|
});
|
||||||
|
|
@ -198,6 +251,7 @@ class _BulletSet {
|
||||||
for (int j = 1; j < lines.length; j++) {
|
for (int j = 1; j < lines.length; j++) {
|
||||||
controllers.insert(i + j, _makeCtrl(lines[j]));
|
controllers.insert(i + j, _makeCtrl(lines[j]));
|
||||||
levels.insert(i + j, levels[i]);
|
levels.insert(i + j, levels[i]);
|
||||||
|
checked.insert(i + j, false);
|
||||||
focusNodes.insert(i + j, FocusNode());
|
focusNodes.insert(i + j, FocusNode());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -223,12 +277,14 @@ class _BulletColumn extends StatefulWidget {
|
||||||
final _BulletSet set;
|
final _BulletSet set;
|
||||||
final VoidCallback emit;
|
final VoidCallback emit;
|
||||||
final TextEditingController headingController;
|
final TextEditingController headingController;
|
||||||
|
final ListStyle listStyle;
|
||||||
|
|
||||||
const _BulletColumn({
|
const _BulletColumn({
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.set,
|
required this.set,
|
||||||
required this.emit,
|
required this.emit,
|
||||||
required this.headingController,
|
required this.headingController,
|
||||||
|
required this.listStyle,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -275,8 +331,23 @@ class _BulletColumnState extends State<_BulletColumn> {
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
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(
|
Text(
|
||||||
_markerForLevel(level),
|
_markerForItem(i),
|
||||||
style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
style: const TextStyle(fontSize: 16, color: Color(0xFF94A3B8)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|
@ -324,14 +395,13 @@ class _BulletColumnState extends State<_BulletColumn> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
key: ValueKey('remove-bullet-${widget.label}-$i'),
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.remove_circle_outline,
|
Icons.remove_circle_outline,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: Color(0xFF94A3B8),
|
color: Color(0xFF94A3B8),
|
||||||
),
|
),
|
||||||
onPressed: set.controllers.length > 1
|
onPressed: () => set.removeAndFocus((fn) => setState(fn), i),
|
||||||
? () => set.removeAndFocus((fn) => setState(fn), i)
|
|
||||||
: null,
|
|
||||||
tooltip: l10n.d('Verwijder'),
|
tooltip: l10n.d('Verwijder'),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
constraints: const BoxConstraints(minWidth: 28),
|
constraints: const BoxConstraints(minWidth: 28),
|
||||||
|
|
@ -345,4 +415,18 @@ class _BulletColumnState extends State<_BulletColumn> {
|
||||||
const markers = ['•', '◦', '▪', '▫', '–'];
|
const markers = ['•', '◦', '▪', '▫', '–'];
|
||||||
return markers[level.clamp(0, markers.length - 1)];
|
return markers[level.clamp(0, markers.length - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _markerForItem(int index) {
|
||||||
|
if (widget.listStyle == ListStyle.bullets) {
|
||||||
|
return _markerForLevel(set.levels[index]);
|
||||||
|
}
|
||||||
|
if (widget.listStyle == ListStyle.checklist) return '';
|
||||||
|
final level = set.levels[index];
|
||||||
|
var number = 0;
|
||||||
|
for (var i = 0; i <= index; i++) {
|
||||||
|
if (set.levels[i] == level) number++;
|
||||||
|
if (set.levels[i] < level) number = 0;
|
||||||
|
}
|
||||||
|
return '$number.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,13 @@ class EditorPanel extends ConsumerWidget {
|
||||||
imgService,
|
imgService,
|
||||||
searchPaths,
|
searchPaths,
|
||||||
deck.projectPath,
|
deck.projectPath,
|
||||||
|
(variants) {
|
||||||
|
final first = deckNotifier.insertSlides(
|
||||||
|
variants,
|
||||||
|
afterIndex: idx,
|
||||||
|
);
|
||||||
|
if (first >= 0) editorNotifier.select(first);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (slide.type != SlideType.video) ...[
|
if (slide.type != SlideType.video) ...[
|
||||||
|
|
@ -160,6 +167,8 @@ class EditorPanel extends ConsumerWidget {
|
||||||
bullets2: newType == SlideType.twoBullets
|
bullets2: newType == SlideType.twoBullets
|
||||||
? (slide.bullets2.isNotEmpty ? slide.bullets2 : [''])
|
? (slide.bullets2.isNotEmpty ? slide.bullets2 : [''])
|
||||||
: const [],
|
: const [],
|
||||||
|
listStyle: slide.listStyle,
|
||||||
|
showChecklistProgress: slide.showChecklistProgress,
|
||||||
imagePath: keepsImage ? slide.imagePath : '',
|
imagePath: keepsImage ? slide.imagePath : '',
|
||||||
imagePath2: newType == SlideType.twoImages ? slide.imagePath2 : '',
|
imagePath2: newType == SlideType.twoImages ? slide.imagePath2 : '',
|
||||||
imageCaption: keepsImage ? slide.imageCaption : '',
|
imageCaption: keepsImage ? slide.imageCaption : '',
|
||||||
|
|
@ -197,6 +206,7 @@ class EditorPanel extends ConsumerWidget {
|
||||||
ImageService imgService,
|
ImageService imgService,
|
||||||
List<String> searchPaths,
|
List<String> searchPaths,
|
||||||
String? captionBasePath,
|
String? captionBasePath,
|
||||||
|
ValueChanged<List<Slide>> onAddChartVariants,
|
||||||
) {
|
) {
|
||||||
switch (slide.type) {
|
switch (slide.type) {
|
||||||
case SlideType.title:
|
case SlideType.title:
|
||||||
|
|
@ -289,6 +299,7 @@ class EditorPanel extends ConsumerWidget {
|
||||||
key: ValueKey(slide.id),
|
key: ValueKey(slide.id),
|
||||||
slide: slide,
|
slide: slide,
|
||||||
onUpdate: onUpdate,
|
onUpdate: onUpdate,
|
||||||
|
onAddVariants: onAddChartVariants,
|
||||||
projectPath: captionBasePath,
|
projectPath: captionBasePath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,16 @@ class _AudienceWindowAppState extends State<AudienceWindowApp> {
|
||||||
? null
|
? null
|
||||||
: Offset((pt[0] as num).toDouble(), (pt[1] as num).toDouble());
|
: Offset((pt[0] as num).toDouble(), (pt[1] as num).toDouble());
|
||||||
});
|
});
|
||||||
|
case 'checklistUpdate':
|
||||||
|
final m = Map<String, dynamic>.from(call.arguments as Map);
|
||||||
|
final i = (m['slideIndex'] as num?)?.toInt();
|
||||||
|
if (i == null || i < 0 || i >= _slides.length || !mounted) return null;
|
||||||
|
setState(() {
|
||||||
|
_slides[i] = _slides[i].copyWith(
|
||||||
|
bullets: List<String>.from(m['bullets'] as List? ?? const []),
|
||||||
|
bullets2: List<String>.from(m['bullets2'] as List? ?? const []),
|
||||||
|
);
|
||||||
|
});
|
||||||
case 'close':
|
case 'close':
|
||||||
try {
|
try {
|
||||||
final self = await WindowController.fromCurrentEngine();
|
final self = await WindowController.fromCurrentEngine();
|
||||||
|
|
@ -112,9 +122,9 @@ class _AudienceWindowAppState extends State<AudienceWindowApp> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _send(String method) {
|
void _send(String method, [Object? arguments]) {
|
||||||
// Best-effort: the presenter may already be gone.
|
// Best-effort: the presenter may already be gone.
|
||||||
presenterChannel.invokeMethod(method).catchError((_) => null);
|
presenterChannel.invokeMethod(method, arguments).catchError((_) => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -169,11 +179,23 @@ class _AudienceWindowAppState extends State<AudienceWindowApp> {
|
||||||
slideCount: _slides.length,
|
slideCount: _slides.length,
|
||||||
tlp: _tlp,
|
tlp: _tlp,
|
||||||
presentationMode: true,
|
presentationMode: true,
|
||||||
|
onChecklistItemToggle: (column, itemIndex) =>
|
||||||
|
_send('checklistToggle', {
|
||||||
|
'slideIndex': _index,
|
||||||
|
'column': column,
|
||||||
|
'itemIndex': itemIndex,
|
||||||
|
}),
|
||||||
enableMedia: true,
|
enableMedia: true,
|
||||||
autoplayMedia: true,
|
autoplayMedia: true,
|
||||||
// Audio finishing on the beamer drives the presenter's
|
// Media finishing on the beamer drives auto-advance.
|
||||||
// auto-advance.
|
onAudioComplete: () => _send('mediaComplete', {
|
||||||
onAudioComplete: () => _send('audioComplete'),
|
'index': _index,
|
||||||
|
'kind': 'audio',
|
||||||
|
}),
|
||||||
|
onVideoComplete: () => _send('mediaComplete', {
|
||||||
|
'index': _index,
|
||||||
|
'kind': 'video',
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
AnnotationLayer(
|
AnnotationLayer(
|
||||||
strokes: _ink[_index] ?? const [],
|
strokes: _ink[_index] ?? const [],
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
/// made while presenting back to the deck.
|
/// made while presenting back to the deck.
|
||||||
final Map<String, List<InkStroke>> initialAnnotations;
|
final Map<String, List<InkStroke>> initialAnnotations;
|
||||||
final void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged;
|
final void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged;
|
||||||
|
final ValueChanged<Slide>? onSlideChanged;
|
||||||
|
|
||||||
const FullscreenPresenter({
|
const FullscreenPresenter({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -48,6 +49,7 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
this.audienceWindow,
|
this.audienceWindow,
|
||||||
this.initialAnnotations = const {},
|
this.initialAnnotations = const {},
|
||||||
this.onAnnotationsChanged,
|
this.onAnnotationsChanged,
|
||||||
|
this.onSlideChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Entry point used by the app: pick dual-screen mode when a second display is
|
/// Entry point used by the app: pick dual-screen mode when a second display is
|
||||||
|
|
@ -62,6 +64,7 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
TlpLevel tlp = TlpLevel.none,
|
TlpLevel tlp = TlpLevel.none,
|
||||||
Map<String, List<InkStroke>> annotations = const {},
|
Map<String, List<InkStroke>> annotations = const {},
|
||||||
void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged,
|
void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged,
|
||||||
|
ValueChanged<Slide>? onSlideChanged,
|
||||||
}) async {
|
}) async {
|
||||||
var displayCount = 0;
|
var displayCount = 0;
|
||||||
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
||||||
|
|
@ -89,6 +92,7 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
tlp: tlp,
|
tlp: tlp,
|
||||||
annotations: annotations,
|
annotations: annotations,
|
||||||
onAnnotationsChanged: onAnnotationsChanged,
|
onAnnotationsChanged: onAnnotationsChanged,
|
||||||
|
onSlideChanged: onSlideChanged,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await show(
|
await show(
|
||||||
|
|
@ -100,6 +104,7 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
tlp: tlp,
|
tlp: tlp,
|
||||||
annotations: annotations,
|
annotations: annotations,
|
||||||
onAnnotationsChanged: onAnnotationsChanged,
|
onAnnotationsChanged: onAnnotationsChanged,
|
||||||
|
onSlideChanged: onSlideChanged,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +118,7 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
TlpLevel tlp = TlpLevel.none,
|
TlpLevel tlp = TlpLevel.none,
|
||||||
Map<String, List<InkStroke>> annotations = const {},
|
Map<String, List<InkStroke>> annotations = const {},
|
||||||
void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged,
|
void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged,
|
||||||
|
ValueChanged<Slide>? onSlideChanged,
|
||||||
}) async {
|
}) async {
|
||||||
final hadWakeLock = await _wakeLockEnabled();
|
final hadWakeLock = await _wakeLockEnabled();
|
||||||
await _enableWakeLock();
|
await _enableWakeLock();
|
||||||
|
|
@ -131,6 +137,7 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
tlp: tlp,
|
tlp: tlp,
|
||||||
initialAnnotations: annotations,
|
initialAnnotations: annotations,
|
||||||
onAnnotationsChanged: onAnnotationsChanged,
|
onAnnotationsChanged: onAnnotationsChanged,
|
||||||
|
onSlideChanged: onSlideChanged,
|
||||||
),
|
),
|
||||||
transitionsBuilder: (context, animation, secondary, child) =>
|
transitionsBuilder: (context, animation, secondary, child) =>
|
||||||
FadeTransition(opacity: animation, child: child),
|
FadeTransition(opacity: animation, child: child),
|
||||||
|
|
@ -156,6 +163,7 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
TlpLevel tlp = TlpLevel.none,
|
TlpLevel tlp = TlpLevel.none,
|
||||||
Map<String, List<InkStroke>> annotations = const {},
|
Map<String, List<InkStroke>> annotations = const {},
|
||||||
void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged,
|
void Function(Map<String, List<InkStroke>>)? onAnnotationsChanged,
|
||||||
|
ValueChanged<Slide>? onSlideChanged,
|
||||||
}) async {
|
}) async {
|
||||||
// A self-contained markdown deck is the payload for the audience window; it
|
// A self-contained markdown deck is the payload for the audience window; it
|
||||||
// carries the slides, the style profile and the TLP level in one string.
|
// carries the slides, the style profile and the TLP level in one string.
|
||||||
|
|
@ -205,6 +213,7 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
tlp: tlp,
|
tlp: tlp,
|
||||||
annotations: annotations,
|
annotations: annotations,
|
||||||
onAnnotationsChanged: onAnnotationsChanged,
|
onAnnotationsChanged: onAnnotationsChanged,
|
||||||
|
onSlideChanged: onSlideChanged,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -227,6 +236,7 @@ class FullscreenPresenter extends StatefulWidget {
|
||||||
audienceWindow: audience,
|
audienceWindow: audience,
|
||||||
initialAnnotations: annotations,
|
initialAnnotations: annotations,
|
||||||
onAnnotationsChanged: onAnnotationsChanged,
|
onAnnotationsChanged: onAnnotationsChanged,
|
||||||
|
onSlideChanged: onSlideChanged,
|
||||||
),
|
),
|
||||||
transitionsBuilder: (context, animation, secondary, child) =>
|
transitionsBuilder: (context, animation, secondary, child) =>
|
||||||
FadeTransition(opacity: animation, child: child),
|
FadeTransition(opacity: animation, child: child),
|
||||||
|
|
@ -255,6 +265,16 @@ bool shouldUseDualScreen({
|
||||||
return (isMacOS || isWindows || isLinux) && displayCount >= 2;
|
return (isMacOS || isWindows || isLinux) && displayCount >= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool autoAdvanceWaitsForMedia(Slide slide) {
|
||||||
|
final autoplayVideo =
|
||||||
|
slide.type == SlideType.video &&
|
||||||
|
slide.videoPath.isNotEmpty &&
|
||||||
|
slide.videoAutoplay;
|
||||||
|
final autoplayAudio = slide.audioPath.isNotEmpty && slide.audioAutoplay;
|
||||||
|
return autoplayVideo || autoplayAudio;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> _wakeLockEnabled() async {
|
Future<bool> _wakeLockEnabled() async {
|
||||||
try {
|
try {
|
||||||
return await WakelockPlus.enabled;
|
return await WakelockPlus.enabled;
|
||||||
|
|
@ -325,9 +345,9 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
/// laatste slide staan). Met L te wisselen.
|
/// laatste slide staan). Met L te wisselen.
|
||||||
bool _loop = false;
|
bool _loop = false;
|
||||||
|
|
||||||
/// Wissel ná het afspelen van de audio op de slide i.p.v. op de tijdwissel.
|
/// Wissel ná het afspelen van autoplay-media i.p.v. op de tijdwissel.
|
||||||
/// Met M te wisselen.
|
/// Met M te wisselen.
|
||||||
bool _advanceOnAudioEnd = true;
|
bool _advanceOnMediaEnd = true;
|
||||||
|
|
||||||
/// Known displays for moving the fullscreen presentation window. This is not
|
/// Known displays for moving the fullscreen presentation window. This is not
|
||||||
/// a second presenter window; it keeps the current output movable between
|
/// a second presenter window; it keeps the current output movable between
|
||||||
|
|
@ -385,7 +405,20 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
case 'exit':
|
case 'exit':
|
||||||
_exit();
|
_exit();
|
||||||
case 'audioComplete':
|
case 'audioComplete':
|
||||||
_onAudioCompleted();
|
_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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
@ -431,6 +464,38 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
if (indexChanged) _pushInk();
|
if (indexChanged) _pushInk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _toggleChecklistItem({
|
||||||
|
required int slideIndex,
|
||||||
|
required int column,
|
||||||
|
required int itemIndex,
|
||||||
|
}) {
|
||||||
|
if (slideIndex < 0 || slideIndex >= widget.slides.length) return;
|
||||||
|
final slide = widget.slides[slideIndex];
|
||||||
|
final source = column == 1 ? slide.bullets2 : slide.bullets;
|
||||||
|
if (itemIndex < 0 || itemIndex >= source.length) return;
|
||||||
|
final updatedItems = List<String>.from(source);
|
||||||
|
final item = updatedItems[itemIndex];
|
||||||
|
updatedItems[itemIndex] = checklistBullet(
|
||||||
|
level: bulletLevel(item),
|
||||||
|
text: checklistItemText(item),
|
||||||
|
checked: !checklistItemChecked(item),
|
||||||
|
);
|
||||||
|
final updated = column == 1
|
||||||
|
? slide.copyWith(bullets2: updatedItems)
|
||||||
|
: slide.copyWith(bullets: updatedItems);
|
||||||
|
setState(() => widget.slides[slideIndex] = updated);
|
||||||
|
widget.onSlideChanged?.call(updated);
|
||||||
|
if (_dual) {
|
||||||
|
audienceChannel
|
||||||
|
.invokeMethod('checklistUpdate', {
|
||||||
|
'slideIndex': slideIndex,
|
||||||
|
'bullets': updated.bullets,
|
||||||
|
'bullets2': updated.bullets2,
|
||||||
|
})
|
||||||
|
.catchError((_) => null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Annotatielaag ─────────────────────────────────────────────────────────
|
// ── Annotatielaag ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Send the current slide's strokes to the beamer (keyed by index there).
|
/// Send the current slide's strokes to the beamer (keyed by index there).
|
||||||
|
|
@ -528,12 +593,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
|
|
||||||
final slide = widget.slides[_index.clamp(0, widget.slides.length - 1)];
|
final slide = widget.slides[_index.clamp(0, widget.slides.length - 1)];
|
||||||
|
|
||||||
// Audio-gestuurd: heeft deze slide audio die vanzelf speelt én is de keuze
|
if (_advanceOnMediaEnd && autoAdvanceWaitsForMedia(slide)) return;
|
||||||
// 'na audio doorgaan' actief? Dan wachten we op het audio-einde (de
|
|
||||||
// _AudioPlayback meldt zich via onAudioComplete) en zetten we geen timer.
|
|
||||||
final audioDriven =
|
|
||||||
_advanceOnAudioEnd && slide.audioPath.isNotEmpty && slide.audioAutoplay;
|
|
||||||
if (audioDriven) return;
|
|
||||||
|
|
||||||
final dur = slide.advanceDuration;
|
final dur = slide.advanceDuration;
|
||||||
if (dur <= 0) return;
|
if (dur <= 0) return;
|
||||||
|
|
@ -559,7 +619,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Automatisch doorschakelen (tijd of audio-einde): naar de volgende slide,
|
/// Automatisch doorschakelen (tijd of media-einde): naar de volgende slide,
|
||||||
/// of bij herhaling vanaf de laatste terug naar de eerste. Zonder herhaling
|
/// of bij herhaling vanaf de laatste terug naar de eerste. Zonder herhaling
|
||||||
/// blijft de laatste slide gewoon staan.
|
/// blijft de laatste slide gewoon staan.
|
||||||
void _autoAdvance() {
|
void _autoAdvance() {
|
||||||
|
|
@ -573,10 +633,20 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Aangeroepen door de audiospeler zodra de audio op de huidige slide klaar
|
void _onMediaCompleted({int? index, String? kind}) {
|
||||||
/// is. In automatische modus met 'na audio doorgaan' schakelen we dan door.
|
if (index != null && index != _index) return;
|
||||||
void _onAudioCompleted() {
|
final slide = widget.slides[_index.clamp(0, widget.slides.length - 1)];
|
||||||
if (_autoPlay && _advanceOnAudioEnd) _autoAdvance();
|
// 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleAutoPlay() {
|
void _toggleAutoPlay() {
|
||||||
|
|
@ -589,8 +659,8 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
_scheduleAdvance();
|
_scheduleAdvance();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleAudioAdvance() {
|
void _toggleMediaAdvance() {
|
||||||
setState(() => _advanceOnAudioEnd = !_advanceOnAudioEnd);
|
setState(() => _advanceOnMediaEnd = !_advanceOnMediaEnd);
|
||||||
_scheduleAdvance();
|
_scheduleAdvance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -892,7 +962,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
_toggleLoop();
|
_toggleLoop();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
case LogicalKeyboardKey.keyM:
|
case LogicalKeyboardKey.keyM:
|
||||||
_toggleAudioAdvance();
|
_toggleMediaAdvance();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
case LogicalKeyboardKey.keyS:
|
case LogicalKeyboardKey.keyS:
|
||||||
_cycleDisplay();
|
_cycleDisplay();
|
||||||
|
|
@ -1155,7 +1225,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
('R', l10n.d('Verstreken tijd resetten')),
|
('R', l10n.d('Verstreken tijd resetten')),
|
||||||
('A', l10n.d('Automatische modus aan/uit')),
|
('A', l10n.d('Automatische modus aan/uit')),
|
||||||
('L', l10n.d('Herhalen (loop) aan/uit')),
|
('L', l10n.d('Herhalen (loop) aan/uit')),
|
||||||
('M', l10n.d('Na audio automatisch doorgaan')),
|
('M', l10n.d('Na media automatisch doorgaan')),
|
||||||
('H', l10n.d('Deze legenda')),
|
('H', l10n.d('Deze legenda')),
|
||||||
('Esc', l10n.d('Terug / afsluiten')),
|
('Esc', l10n.d('Terug / afsluiten')),
|
||||||
];
|
];
|
||||||
|
|
@ -1289,13 +1359,20 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
|
||||||
slideCount: widget.slides.length,
|
slideCount: widget.slides.length,
|
||||||
tlp: widget.tlp,
|
tlp: widget.tlp,
|
||||||
presentationMode: true,
|
presentationMode: true,
|
||||||
|
onChecklistItemToggle: (column, itemIndex) =>
|
||||||
|
_toggleChecklistItem(
|
||||||
|
slideIndex: _index,
|
||||||
|
column: column,
|
||||||
|
itemIndex: itemIndex,
|
||||||
|
),
|
||||||
// Tijdens het presenteren speelt media en starten audio/video
|
// Tijdens het presenteren speelt media en starten audio/video
|
||||||
// vanzelf; het audio-einde stuurt de auto-advance aan. In dual-
|
// vanzelf; het media-einde stuurt auto-advance aan. In dual-
|
||||||
// schermmodus speelt de media op het beamervenster, niet hier,
|
// schermmodus speelt de media op het beamervenster, niet hier,
|
||||||
// anders zou het geluid dubbel klinken.
|
// anders zou het geluid dubbel klinken.
|
||||||
enableMedia: !_dual,
|
enableMedia: !_dual,
|
||||||
autoplayMedia: !_dual,
|
autoplayMedia: !_dual,
|
||||||
onAudioComplete: _onAudioCompleted,
|
onAudioComplete: () => _onMediaCompleted(kind: 'audio'),
|
||||||
|
onVideoComplete: () => _onMediaCompleted(kind: 'video'),
|
||||||
),
|
),
|
||||||
// Annotatielaag bovenop de dia. Laat klikken door wanneer er
|
// Annotatielaag bovenop de dia. Laat klikken door wanneer er
|
||||||
// geen gereedschap actief is (zodat tikken blijft doorbladeren).
|
// geen gereedschap actief is (zodat tikken blijft doorbladeren).
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
286
test/bullets_editor_test.dart
Normal file
286
test/bullets_editor_test.dart
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
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,13 +4,21 @@ import 'package:ocideck/models/chart.dart';
|
||||||
import 'package:ocideck/models/slide.dart';
|
import 'package:ocideck/models/slide.dart';
|
||||||
import 'package:ocideck/widgets/editors/chart_editor.dart';
|
import 'package:ocideck/widgets/editors/chart_editor.dart';
|
||||||
|
|
||||||
Widget _host(Slide slide, ValueChanged<Slide> onUpdate) {
|
Widget _host(
|
||||||
|
Slide slide,
|
||||||
|
ValueChanged<Slide> onUpdate, {
|
||||||
|
ValueChanged<List<Slide>>? onAddVariants,
|
||||||
|
}) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: SizedBox(
|
body: SizedBox(
|
||||||
width: 900,
|
width: 900,
|
||||||
height: 650,
|
height: 650,
|
||||||
child: ChartEditor(slide: slide, onUpdate: onUpdate),
|
child: ChartEditor(
|
||||||
|
slide: slide,
|
||||||
|
onUpdate: onUpdate,
|
||||||
|
onAddVariants: onAddVariants,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -136,10 +144,7 @@ void main() {
|
||||||
expect(find.byKey(const ValueKey('chart-min-bound')), findsOneWidget);
|
expect(find.byKey(const ValueKey('chart-min-bound')), findsOneWidget);
|
||||||
expect(find.byKey(const ValueKey('chart-max-bound')), findsOneWidget);
|
expect(find.byKey(const ValueKey('chart-max-bound')), findsOneWidget);
|
||||||
|
|
||||||
await tester.enterText(
|
await tester.enterText(find.byKey(const ValueKey('chart-max-bound')), '20');
|
||||||
find.byKey(const ValueKey('chart-max-bound')),
|
|
||||||
'20',
|
|
||||||
);
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(ChartSpec.parse(updated.customMarkdown).maxBound, 20);
|
expect(ChartSpec.parse(updated.customMarkdown).maxBound, 20);
|
||||||
|
|
@ -163,4 +168,39 @@ void main() {
|
||||||
expect(find.byKey(const ValueKey('chart-min-bound')), findsNothing);
|
expect(find.byKey(const ValueKey('chart-min-bound')), findsNothing);
|
||||||
expect(find.byKey(const ValueKey('chart-max-bound')), findsNothing);
|
expect(find.byKey(const ValueKey('chart-max-bound')), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('chart variants reuse data in the chosen order', (tester) async {
|
||||||
|
const spec = ChartSpec(
|
||||||
|
type: ChartType.bar,
|
||||||
|
title: 'Omzet',
|
||||||
|
x: ['A', 'B'],
|
||||||
|
series: [
|
||||||
|
ChartSeries(name: 'Waarde', data: [10, 20], color: '#003399'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
final slide = Slide.create(
|
||||||
|
SlideType.chart,
|
||||||
|
).copyWith(customMarkdown: spec.toBlock());
|
||||||
|
List<Slide>? variants;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_host(slide, (_) {}, onAddVariants: (value) => variants = value),
|
||||||
|
);
|
||||||
|
await tester.tap(find.byKey(const ValueKey('chart-create-variants')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const ValueKey('chart-variant-down-0')));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.text('Slides toevoegen'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final specs = variants!.map((s) => ChartSpec.parse(s.customMarkdown));
|
||||||
|
expect(specs.map((s) => s.type), [
|
||||||
|
ChartType.pie,
|
||||||
|
ChartType.line,
|
||||||
|
ChartType.radar,
|
||||||
|
]);
|
||||||
|
expect(specs.first.x, ['A', 'B']);
|
||||||
|
expect(specs.first.series.single.data, [10, 20]);
|
||||||
|
expect(specs.first.series.single.color, '#003399');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,11 @@ void main() {
|
||||||
final radar = tester.widget<RadarChart>(find.byType(RadarChart));
|
final radar = tester.widget<RadarChart>(find.byType(RadarChart));
|
||||||
// Two visible series plus one invisible scale anchor.
|
// Two visible series plus one invisible scale anchor.
|
||||||
expect(radar.data.dataSets.length, 3);
|
expect(radar.data.dataSets.length, 3);
|
||||||
expect(radar.data.dataSets.first.dataEntries.map((e) => e.value), [3, 4, 5]);
|
expect(radar.data.dataSets.first.dataEntries.map((e) => e.value), [
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
]);
|
||||||
expect(radar.data.dataSets.last.fillColor, Colors.transparent);
|
expect(radar.data.dataSets.last.fillColor, Colors.transparent);
|
||||||
// The spoke labels are supplied through getTitle (canvas-painted).
|
// The spoke labels are supplied through getTitle (canvas-painted).
|
||||||
expect(radar.data.getTitle!(0, 0).text, 'Snelheid');
|
expect(radar.data.getTitle!(0, 0).text, 'Snelheid');
|
||||||
|
|
@ -302,6 +306,43 @@ void main() {
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('long radar labels stay outside the diagram and each other', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
const spec = ChartSpec(
|
||||||
|
type: ChartType.radar,
|
||||||
|
x: [
|
||||||
|
'Strategische wendbaarheid',
|
||||||
|
'Operationele betrouwbaarheid',
|
||||||
|
'Klantgerichte innovatie',
|
||||||
|
'Duurzame inzetbaarheid',
|
||||||
|
'Digitale volwassenheid',
|
||||||
|
'Financiële weerbaarheid',
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
ChartSeries(name: 'Score', data: [3, 4, 5, 2, 4, 3]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(_host(spec, presentationMode: true));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final radarRect = tester.getRect(find.byType(RadarChart));
|
||||||
|
final labelRects = [
|
||||||
|
for (var i = 0; i < spec.x.length; i++)
|
||||||
|
tester.getRect(find.byKey(ValueKey('radar-axis-label-$i'))),
|
||||||
|
];
|
||||||
|
for (final rect in labelRects) {
|
||||||
|
expect(rect.overlaps(radarRect), isFalse);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < labelRects.length; i++) {
|
||||||
|
for (var j = i + 1; j < labelRects.length; j++) {
|
||||||
|
expect(labelRects[i].overlaps(labelRects[j]), isFalse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('radar honours an explicit min/max scale with even ticks', (
|
testWidgets('radar honours an explicit min/max scale with even ticks', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,74 @@ void main() {
|
||||||
expect(n.state.deck!.slides.first.title, 'Nieuw');
|
expect(n.state.deck!.slides.first.title, 'Nieuw');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('clearAllChecklists unchecks every checklist item across the deck', () {
|
||||||
|
final n = _notifier()..newDeck('D');
|
||||||
|
final s1 = Slide.create(SlideType.bullets).copyWith(
|
||||||
|
listStyle: ListStyle.checklist,
|
||||||
|
bullets: ['[x] Klaar', '\t[X] Subklaar', '[ ] Open'],
|
||||||
|
);
|
||||||
|
final s2 = Slide.create(SlideType.bullets).copyWith(
|
||||||
|
listStyle: ListStyle.checklist,
|
||||||
|
bullets: ['[x] Eerste kolom'],
|
||||||
|
bullets2: ['[x] Tweede kolom', '[ ] Nog open'],
|
||||||
|
);
|
||||||
|
n.loadDeck(n.state.deck!.copyWith(slides: [s1, s2]));
|
||||||
|
|
||||||
|
expect(n.checkedChecklistCount, 4);
|
||||||
|
final revisionBefore = n.state.revision;
|
||||||
|
|
||||||
|
n.clearAllChecklists();
|
||||||
|
|
||||||
|
expect(n.checkedChecklistCount, 0);
|
||||||
|
final out = n.state.deck!.slides;
|
||||||
|
expect(out[0].bullets, ['[ ] Klaar', '\t[ ] Subklaar', '[ ] Open']);
|
||||||
|
expect(out[1].bullets, ['[ ] Eerste kolom']);
|
||||||
|
expect(out[1].bullets2, ['[ ] Tweede kolom', '[ ] Nog open']);
|
||||||
|
// Revision bumps so the open slide editor remounts and reflects the change.
|
||||||
|
expect(n.state.revision, greaterThan(revisionBefore));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearAllChecklists is a single undoable step that restores the checks', () {
|
||||||
|
final n = _notifier()..newDeck('D');
|
||||||
|
final slide = Slide.create(SlideType.bullets).copyWith(
|
||||||
|
listStyle: ListStyle.checklist,
|
||||||
|
bullets: ['[x] Klaar', '[ ] Open'],
|
||||||
|
bullets2: ['[x] Tweede'],
|
||||||
|
);
|
||||||
|
n.loadDeck(n.state.deck!.copyWith(slides: [slide]));
|
||||||
|
expect(n.checkedChecklistCount, 2);
|
||||||
|
|
||||||
|
n.clearAllChecklists();
|
||||||
|
expect(n.checkedChecklistCount, 0);
|
||||||
|
expect(n.state.canUndo, isTrue);
|
||||||
|
final revisionAfterClear = n.state.revision;
|
||||||
|
|
||||||
|
n.undo();
|
||||||
|
|
||||||
|
// One undo restores every checked item in both columns...
|
||||||
|
expect(n.checkedChecklistCount, 2);
|
||||||
|
expect(n.state.deck!.slides.first.bullets, ['[x] Klaar', '[ ] Open']);
|
||||||
|
expect(n.state.deck!.slides.first.bullets2, ['[x] Tweede']);
|
||||||
|
// ...and bumps the revision again so the open editor reflects the restore.
|
||||||
|
expect(n.state.revision, greaterThan(revisionAfterClear));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearAllChecklists is a no-op when nothing is checked', () {
|
||||||
|
final n = _notifier()..newDeck('D');
|
||||||
|
final slide = Slide.create(SlideType.bullets).copyWith(
|
||||||
|
listStyle: ListStyle.checklist,
|
||||||
|
bullets: ['[ ] Open'],
|
||||||
|
);
|
||||||
|
n.loadDeck(n.state.deck!.copyWith(slides: [slide]));
|
||||||
|
expect(n.state.canUndo, isFalse);
|
||||||
|
|
||||||
|
n.clearAllChecklists();
|
||||||
|
|
||||||
|
// No checked items, so no history entry is recorded.
|
||||||
|
expect(n.state.canUndo, isFalse);
|
||||||
|
expect(n.checkedChecklistCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test('updateMeta changes deck title/theme/paginate', () {
|
test('updateMeta changes deck title/theme/paginate', () {
|
||||||
final n = _notifier()..newDeck('D');
|
final n = _notifier()..newDeck('D');
|
||||||
n.updateMeta(title: 'Andere titel', theme: 'custom', paginate: false);
|
n.updateMeta(title: 'Andere titel', theme: 'custom', paginate: false);
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,91 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('autoplay audio or video takes precedence over slide timing', () {
|
||||||
|
expect(
|
||||||
|
autoAdvanceWaitsForMedia(
|
||||||
|
Slide.create(SlideType.bullets).copyWith(
|
||||||
|
advanceDuration: 3,
|
||||||
|
audioPath: 'sound.mp3',
|
||||||
|
audioAutoplay: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
autoAdvanceWaitsForMedia(
|
||||||
|
Slide.create(SlideType.video).copyWith(
|
||||||
|
advanceDuration: 3,
|
||||||
|
videoPath: 'movie.mp4',
|
||||||
|
videoAutoplay: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
autoAdvanceWaitsForMedia(
|
||||||
|
Slide.create(SlideType.video).copyWith(
|
||||||
|
advanceDuration: 3,
|
||||||
|
videoPath: 'movie.mp4',
|
||||||
|
videoAutoplay: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('slide timer does not interrupt autoplay media', (tester) async {
|
||||||
|
final mediaSlides = [
|
||||||
|
Slide.create(SlideType.video).copyWith(
|
||||||
|
title: 'Video blijft staan',
|
||||||
|
videoPath: '/tmp/does-not-exist.mp4',
|
||||||
|
videoAutoplay: true,
|
||||||
|
advanceDuration: 3,
|
||||||
|
),
|
||||||
|
Slide.create(SlideType.bullets).copyWith(title: 'Na video'),
|
||||||
|
];
|
||||||
|
|
||||||
|
await tester.pumpWidget(_host(mediaSlides));
|
||||||
|
await tester.pump(const Duration(seconds: 4));
|
||||||
|
|
||||||
|
expect(find.text('Video blijft staan'), findsOneWidget);
|
||||||
|
expect(find.text('Na video'), findsNothing);
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('checklist changes during presenting are persisted', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
Slide? updated;
|
||||||
|
final checklistSlides = [
|
||||||
|
Slide.create(SlideType.bullets).copyWith(
|
||||||
|
title: 'Taken',
|
||||||
|
bullets: ['[ ] Live afvinken'],
|
||||||
|
listStyle: ListStyle.checklist,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: FullscreenPresenter(
|
||||||
|
slides: checklistSlides,
|
||||||
|
projectPath: null,
|
||||||
|
themeProfile: const ThemeProfile(),
|
||||||
|
initialIndex: 0,
|
||||||
|
onSlideChanged: (slide) => updated = slide,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.tap(
|
||||||
|
find.byKey(const ValueKey('checklist-preview-toggle-0-0')),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(updated?.bullets, ['[x] Live afvinken']);
|
||||||
|
expect(find.text('☑ '), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('starts in audience view without presenter chrome', (
|
testWidgets('starts in audience view without presenter chrome', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
||||||
71
test/image_slide_editor_test.dart
Normal file
71
test/image_slide_editor_test.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
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,6 +90,51 @@ void main() {
|
||||||
expect(out.bullets, ['Punt een', 'Punt twee']);
|
expect(out.bullets, ['Punt een', 'Punt twee']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('numbered list style round-trips', () {
|
||||||
|
final service = MarkdownService();
|
||||||
|
final markdown = service.generateDeck(
|
||||||
|
Deck(
|
||||||
|
title: 'Demo',
|
||||||
|
slides: [
|
||||||
|
Slide.create(SlideType.bullets).copyWith(
|
||||||
|
title: 'Stappen',
|
||||||
|
bullets: ['Eerst', '\tDetail', 'Daarna'],
|
||||||
|
listStyle: ListStyle.numbered,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final out = service.parseDeck(markdown)!.slides.single;
|
||||||
|
expect(out.listStyle, ListStyle.numbered);
|
||||||
|
expect(out.bullets, ['Eerst', '\tDetail', 'Daarna']);
|
||||||
|
expect(markdown, contains('1. Eerst'));
|
||||||
|
expect(markdown, contains('2. Daarna'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('checklist style and checked items round-trip', () {
|
||||||
|
final service = MarkdownService();
|
||||||
|
final markdown = service.generateDeck(
|
||||||
|
Deck(
|
||||||
|
title: 'Demo',
|
||||||
|
slides: [
|
||||||
|
Slide.create(SlideType.bullets).copyWith(
|
||||||
|
title: 'Taken',
|
||||||
|
bullets: ['[x] Gedaan', '[ ] Nog doen', '\t[x] Subtaak'],
|
||||||
|
listStyle: ListStyle.checklist,
|
||||||
|
showChecklistProgress: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final out = service.parseDeck(markdown)!.slides.single;
|
||||||
|
expect(out.listStyle, ListStyle.checklist);
|
||||||
|
expect(out.showChecklistProgress, isTrue);
|
||||||
|
expect(out.bullets, ['[x] Gedaan', '[ ] Nog doen', '\t[x] Subtaak']);
|
||||||
|
expect(markdown, contains('- [x] Gedaan'));
|
||||||
|
expect(markdown, contains('- [ ] Nog doen'));
|
||||||
|
expect(markdown, contains('ocideck_checklist_progress: true'));
|
||||||
|
});
|
||||||
|
|
||||||
test('twoBullets slide keeps both bullet columns', () {
|
test('twoBullets slide keeps both bullet columns', () {
|
||||||
final out = _roundTrip(
|
final out = _roundTrip(
|
||||||
Slide.create(SlideType.twoBullets).copyWith(
|
Slide.create(SlideType.twoBullets).copyWith(
|
||||||
|
|
@ -127,7 +172,9 @@ void main() {
|
||||||
Deck(
|
Deck(
|
||||||
title: 'Demo',
|
title: 'Demo',
|
||||||
slides: [
|
slides: [
|
||||||
Slide.create(SlideType.twoBullets).copyWith(bullets: ['A'], bullets2: ['B']),
|
Slide.create(
|
||||||
|
SlideType.twoBullets,
|
||||||
|
).copyWith(bullets: ['A'], bullets2: ['B']),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -137,6 +184,24 @@ void main() {
|
||||||
expect(out.columnTitle2, '');
|
expect(out.columnTitle2, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('two-column checklist export respects disabled strike-through', () {
|
||||||
|
final service = MarkdownService();
|
||||||
|
final markdown = service.generateDeck(
|
||||||
|
Deck(
|
||||||
|
title: 'Demo',
|
||||||
|
themeProfile: const ThemeProfile(checklistStrikeThrough: false),
|
||||||
|
slides: [
|
||||||
|
Slide.create(SlideType.twoBullets).copyWith(
|
||||||
|
bullets: ['[x] Klaar'],
|
||||||
|
bullets2: ['[ ] Open'],
|
||||||
|
listStyle: ListStyle.checklist,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(markdown, isNot(contains('text-decoration:line-through')));
|
||||||
|
});
|
||||||
|
|
||||||
test('bulletsImage slide keeps bullets, image, size and caption', () {
|
test('bulletsImage slide keeps bullets, image, size and caption', () {
|
||||||
final out = _roundTrip(
|
final out = _roundTrip(
|
||||||
Slide.create(SlideType.bulletsImage).copyWith(
|
Slide.create(SlideType.bulletsImage).copyWith(
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,18 @@ void main() {
|
||||||
expect(back.codeFontFamily, 'Courier New');
|
expect(back.codeFontFamily, 'Courier New');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ThemeProfile round-trips checklist styling through JSON', () {
|
||||||
|
const profile = ThemeProfile(
|
||||||
|
checklistCheckedColor: '#00AA00',
|
||||||
|
checklistUncheckedColor: '#CC0000',
|
||||||
|
checklistStrikeThrough: false,
|
||||||
|
);
|
||||||
|
final back = ThemeProfile.fromJson(profile.toJson());
|
||||||
|
expect(back.checklistCheckedColor, '#00AA00');
|
||||||
|
expect(back.checklistUncheckedColor, '#CC0000');
|
||||||
|
expect(back.checklistStrikeThrough, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
test('ThemeProfile code styling defaults to the atom-one-dark look', () {
|
test('ThemeProfile code styling defaults to the atom-one-dark look', () {
|
||||||
// Older decks without the fields fall back to the dark editor defaults.
|
// Older decks without the fields fall back to the dark editor defaults.
|
||||||
final back = ThemeProfile.fromJson(const {'name': 'Legacy'});
|
final back = ThemeProfile.fromJson(const {'name': 'Legacy'});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:ocideck/models/slide.dart';
|
import 'package:ocideck/models/slide.dart';
|
||||||
|
import 'package:ocideck/widgets/slides/inline_markdown.dart';
|
||||||
import 'package:ocideck/widgets/slides/slide_preview.dart';
|
import 'package:ocideck/widgets/slides/slide_preview.dart';
|
||||||
|
|
||||||
Widget _host(Slide slide) {
|
Widget _host(Slide slide) {
|
||||||
|
|
@ -58,6 +59,28 @@ void main() {
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('two-bullet columns use the same font size', (tester) async {
|
||||||
|
final slide = Slide.create(SlideType.twoBullets).copyWith(
|
||||||
|
bullets: const ['Solo'],
|
||||||
|
bullets2: List.generate(14, (i) => 'Item $i'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(_host(slide));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
double sizeOf(String text) => tester
|
||||||
|
.widget<InlineMarkdownText>(
|
||||||
|
find.byWidgetPredicate(
|
||||||
|
(x) => x is InlineMarkdownText && x.text == text,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.style
|
||||||
|
.fontSize!;
|
||||||
|
|
||||||
|
expect(sizeOf('Solo'), sizeOf('Item 0'));
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('bullets slide renders an optional subheading below the title', (
|
testWidgets('bullets slide renders an optional subheading below the title', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue