Compare commits
No commits in common. "839474fa19ec43ae1a3e1b06b3c5112964f1f3f5" and "bd6db5cbd7ef24fdccf397b94830725bef618f98" have entirely different histories.
839474fa19
...
bd6db5cbd7
14 changed files with 46 additions and 539 deletions
|
|
@ -42,11 +42,6 @@ and the project aims to follow [Semantic Versioning](https://semver.org/).
|
||||||
the EUPL-1.2 licence text.
|
the EUPL-1.2 licence text.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **Bullet slides** can now carry an optional **subheading** under the title; the
|
|
||||||
**two bullet columns** type can have an optional **heading above each column**,
|
|
||||||
separate from the slide title.
|
|
||||||
- Slide text auto-sizing now measures with the deck's own font, so text grows to
|
|
||||||
use the available space more accurately instead of staying smaller than needed.
|
|
||||||
- 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.
|
||||||
|
|
|
||||||
|
|
@ -216,11 +216,10 @@ afbeelding geschreven.
|
||||||
Optionele toelichtende paragraaf
|
Optionele toelichtende paragraaf
|
||||||
```
|
```
|
||||||
|
|
||||||
**Bullets** (geen class) — optioneel een **subkop** (`## …`, komt in `subtitle`),
|
**Bullets** (geen class) — inspringen met tabs in het model → 2 spaties per
|
||||||
en inspringen met tabs in het model → 2 spaties per niveau in Markdown:
|
niveau in Markdown:
|
||||||
```markdown
|
```markdown
|
||||||
# Kop
|
# Kop
|
||||||
## Subkop (optioneel)
|
|
||||||
|
|
||||||
- Eerste punt
|
- Eerste punt
|
||||||
- Subpunt
|
- Subpunt
|
||||||
|
|
@ -228,17 +227,13 @@ en inspringen met tabs in het model → 2 spaties per niveau in Markdown:
|
||||||
|
|
||||||
**Twee bulletkolommen** (`two-bullets`) — naast de zichtbare HTML-grid worden de
|
**Twee bulletkolommen** (`two-bullets`) — naast de zichtbare HTML-grid worden de
|
||||||
twee kolommen ook **canoniek opgeslagen** in commentaar (base64url van een JSON-
|
twee kolommen ook **canoniek opgeslagen** in commentaar (base64url van een JSON-
|
||||||
array), zodat ze verliesvrij teruggelezen worden. Elke kolom kan optioneel een
|
array), zodat ze verliesvrij teruggelezen worden:
|
||||||
**kop** krijgen (`*_title`, base64url van platte tekst); die wordt alleen
|
|
||||||
geschreven als hij gevuld is en verschijnt als `<h3>` boven de kolom:
|
|
||||||
```markdown
|
```markdown
|
||||||
<!-- ocideck_two_bullets_left: <base64url(JSON[])> -->
|
<!-- ocideck_two_bullets_left: <base64url(JSON[])> -->
|
||||||
<!-- ocideck_two_bullets_right: <base64url(JSON[])> -->
|
<!-- ocideck_two_bullets_right: <base64url(JSON[])> -->
|
||||||
<!-- ocideck_two_bullets_left_title: <base64url(tekst)> --> (optioneel)
|
|
||||||
<!-- ocideck_two_bullets_right_title: <base64url(tekst)> --> (optioneel)
|
|
||||||
<div class="ocideck-two-bullets" style="…">
|
<div class="ocideck-two-bullets" style="…">
|
||||||
<div><h3>…</h3><ul>…</ul></div>
|
<ul>…</ul>
|
||||||
<div><h3>…</h3><ul>…</ul></div>
|
<ul>…</ul>
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2384,9 +2384,6 @@ const _dutchSourceStringAdditions = {
|
||||||
'For example #33FF33 for a CRT-green screen.',
|
'For example #33FF33 for a CRT-green screen.',
|
||||||
'Onderdeel van stijlprofiel ': 'Part of style profile ',
|
'Onderdeel van stijlprofiel ': 'Part of style profile ',
|
||||||
'Broncode lettertype': 'Code font',
|
'Broncode lettertype': 'Code font',
|
||||||
'Kop (optioneel)': 'Heading (optional)',
|
|
||||||
'Subkop (optioneel)': 'Subheading (optional)',
|
|
||||||
'Subkop': 'Subheading',
|
|
||||||
'Systeem (monospace)': 'System (monospace)',
|
'Systeem (monospace)': 'System (monospace)',
|
||||||
'Platte tekst': 'Plain text',
|
'Platte tekst': 'Plain text',
|
||||||
'Titel (optioneel)': 'Title (optional)',
|
'Titel (optioneel)': 'Title (optional)',
|
||||||
|
|
@ -2426,9 +2423,6 @@ const _dutchSourceStringAdditions = {
|
||||||
'Ad esempio #33FF33 per uno schermo verde CRT.',
|
'Ad esempio #33FF33 per uno schermo verde CRT.',
|
||||||
'Onderdeel van stijlprofiel ': 'Parte del profilo di stile ',
|
'Onderdeel van stijlprofiel ': 'Parte del profilo di stile ',
|
||||||
'Broncode lettertype': 'Font del codice',
|
'Broncode lettertype': 'Font del codice',
|
||||||
'Kop (optioneel)': 'Intestazione (facoltativa)',
|
|
||||||
'Subkop (optioneel)': 'Sottotitolo (facoltativo)',
|
|
||||||
'Subkop': 'Sottotitolo',
|
|
||||||
'Systeem (monospace)': 'Sistema (monospace)',
|
'Systeem (monospace)': 'Sistema (monospace)',
|
||||||
'Kleur van reeks': 'Colore della serie',
|
'Kleur van reeks': 'Colore della serie',
|
||||||
'Kleur van rij': 'Colore della riga',
|
'Kleur van rij': 'Colore della riga',
|
||||||
|
|
@ -2658,9 +2652,6 @@ const _dutchSourceStringAdditions = {
|
||||||
'Zum Beispiel #33FF33 für einen CRT-grünen Bildschirm.',
|
'Zum Beispiel #33FF33 für einen CRT-grünen Bildschirm.',
|
||||||
'Onderdeel van stijlprofiel ': 'Teil des Stilprofils ',
|
'Onderdeel van stijlprofiel ': 'Teil des Stilprofils ',
|
||||||
'Broncode lettertype': 'Code-Schriftart',
|
'Broncode lettertype': 'Code-Schriftart',
|
||||||
'Kop (optioneel)': 'Überschrift (optional)',
|
|
||||||
'Subkop (optioneel)': 'Unterüberschrift (optional)',
|
|
||||||
'Subkop': 'Unterüberschrift',
|
|
||||||
'Systeem (monospace)': 'System (monospace)',
|
'Systeem (monospace)': 'System (monospace)',
|
||||||
'Kleur van reeks': 'Reihenfarbe',
|
'Kleur van reeks': 'Reihenfarbe',
|
||||||
'Kleur van rij': 'Zeilenfarbe',
|
'Kleur van rij': 'Zeilenfarbe',
|
||||||
|
|
@ -2891,9 +2882,6 @@ const _dutchSourceStringAdditions = {
|
||||||
'Par exemple #33FF33 pour un écran vert CRT.',
|
'Par exemple #33FF33 pour un écran vert CRT.',
|
||||||
'Onderdeel van stijlprofiel ': 'Fait partie du profil de style ',
|
'Onderdeel van stijlprofiel ': 'Fait partie du profil de style ',
|
||||||
'Broncode lettertype': 'Police du code',
|
'Broncode lettertype': 'Police du code',
|
||||||
'Kop (optioneel)': 'En-tête (facultatif)',
|
|
||||||
'Subkop (optioneel)': 'Sous-titre (facultatif)',
|
|
||||||
'Subkop': 'Sous-titre',
|
|
||||||
'Systeem (monospace)': 'Système (monospace)',
|
'Systeem (monospace)': 'Système (monospace)',
|
||||||
'Kleur van reeks': 'Couleur de la série',
|
'Kleur van reeks': 'Couleur de la série',
|
||||||
'Kleur van rij': 'Couleur de la ligne',
|
'Kleur van rij': 'Couleur de la ligne',
|
||||||
|
|
@ -3124,9 +3112,6 @@ const _dutchSourceStringAdditions = {
|
||||||
'Por ejemplo #33FF33 para una pantalla verde CRT.',
|
'Por ejemplo #33FF33 para una pantalla verde CRT.',
|
||||||
'Onderdeel van stijlprofiel ': 'Parte del perfil de estilo ',
|
'Onderdeel van stijlprofiel ': 'Parte del perfil de estilo ',
|
||||||
'Broncode lettertype': 'Fuente del código',
|
'Broncode lettertype': 'Fuente del código',
|
||||||
'Kop (optioneel)': 'Encabezado (opcional)',
|
|
||||||
'Subkop (optioneel)': 'Subtítulo (opcional)',
|
|
||||||
'Subkop': 'Subtítulo',
|
|
||||||
'Systeem (monospace)': 'Sistema (monospace)',
|
'Systeem (monospace)': 'Sistema (monospace)',
|
||||||
'Kleur van reeks': 'Color de la serie',
|
'Kleur van reeks': 'Color de la serie',
|
||||||
'Kleur van rij': 'Color de la fila',
|
'Kleur van rij': 'Color de la fila',
|
||||||
|
|
@ -3357,9 +3342,6 @@ const _dutchSourceStringAdditions = {
|
||||||
'Bygelyks #33FF33 foar in CRT-grien skerm.',
|
'Bygelyks #33FF33 foar in CRT-grien skerm.',
|
||||||
'Onderdeel van stijlprofiel ': 'Underdiel fan stylprofyl ',
|
'Onderdeel van stijlprofiel ': 'Underdiel fan stylprofyl ',
|
||||||
'Broncode lettertype': 'Boarnekoade lettertype',
|
'Broncode lettertype': 'Boarnekoade lettertype',
|
||||||
'Kop (optioneel)': 'Kop (opsjoneel)',
|
|
||||||
'Subkop (optioneel)': 'Subkop (opsjoneel)',
|
|
||||||
'Subkop': 'Subkop',
|
|
||||||
'Systeem (monospace)': 'Systeem (monospace)',
|
'Systeem (monospace)': 'Systeem (monospace)',
|
||||||
'Kleur van reeks': 'Rigekleur',
|
'Kleur van reeks': 'Rigekleur',
|
||||||
'Kleur van rij': 'Rijekleur',
|
'Kleur van rij': 'Rijekleur',
|
||||||
|
|
@ -3587,9 +3569,6 @@ const _dutchSourceStringAdditions = {
|
||||||
'Por ehèmpel #33FF33 pa un pantaya berde CRT.',
|
'Por ehèmpel #33FF33 pa un pantaya berde CRT.',
|
||||||
'Onderdeel van stijlprofiel ': 'Parti di e perfil di estilo ',
|
'Onderdeel van stijlprofiel ': 'Parti di e perfil di estilo ',
|
||||||
'Broncode lettertype': 'Tipo di lèter di kódigo',
|
'Broncode lettertype': 'Tipo di lèter di kódigo',
|
||||||
'Kop (optioneel)': 'Enkabesado (opshonal)',
|
|
||||||
'Subkop (optioneel)': 'Subtítulo (opshonal)',
|
|
||||||
'Subkop': 'Subtítulo',
|
|
||||||
'Systeem (monospace)': 'Sistema (monospace)',
|
'Systeem (monospace)': 'Sistema (monospace)',
|
||||||
'Kleur van reeks': 'Koló di serie',
|
'Kleur van reeks': 'Koló di serie',
|
||||||
'Kleur van rij': 'Koló di liña',
|
'Kleur van rij': 'Koló di liña',
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,6 @@ class Slide {
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
final List<String> bullets;
|
final List<String> bullets;
|
||||||
final List<String> bullets2;
|
final List<String> bullets2;
|
||||||
|
|
||||||
/// Optional headings above the two bullet columns (twoBullets only). Empty =
|
|
||||||
/// no heading for that column.
|
|
||||||
final String columnTitle1;
|
|
||||||
final String columnTitle2;
|
|
||||||
final String imagePath;
|
final String imagePath;
|
||||||
final String imagePath2;
|
final String imagePath2;
|
||||||
final String imageCaption;
|
final String imageCaption;
|
||||||
|
|
@ -128,8 +123,6 @@ class Slide {
|
||||||
this.subtitle = '',
|
this.subtitle = '',
|
||||||
this.bullets = const [],
|
this.bullets = const [],
|
||||||
this.bullets2 = const [],
|
this.bullets2 = const [],
|
||||||
this.columnTitle1 = '',
|
|
||||||
this.columnTitle2 = '',
|
|
||||||
this.imagePath = '',
|
this.imagePath = '',
|
||||||
this.imagePath2 = '',
|
this.imagePath2 = '',
|
||||||
this.imageCaption = '',
|
this.imageCaption = '',
|
||||||
|
|
@ -183,8 +176,6 @@ class Slide {
|
||||||
subtitle: src.subtitle,
|
subtitle: src.subtitle,
|
||||||
bullets: List<String>.from(src.bullets),
|
bullets: List<String>.from(src.bullets),
|
||||||
bullets2: List<String>.from(src.bullets2),
|
bullets2: List<String>.from(src.bullets2),
|
||||||
columnTitle1: src.columnTitle1,
|
|
||||||
columnTitle2: src.columnTitle2,
|
|
||||||
imagePath: src.imagePath,
|
imagePath: src.imagePath,
|
||||||
imagePath2: src.imagePath2,
|
imagePath2: src.imagePath2,
|
||||||
imageCaption: src.imageCaption,
|
imageCaption: src.imageCaption,
|
||||||
|
|
@ -215,8 +206,6 @@ class Slide {
|
||||||
String? subtitle,
|
String? subtitle,
|
||||||
List<String>? bullets,
|
List<String>? bullets,
|
||||||
List<String>? bullets2,
|
List<String>? bullets2,
|
||||||
String? columnTitle1,
|
|
||||||
String? columnTitle2,
|
|
||||||
String? imagePath,
|
String? imagePath,
|
||||||
String? imagePath2,
|
String? imagePath2,
|
||||||
String? imageCaption,
|
String? imageCaption,
|
||||||
|
|
@ -246,8 +235,6 @@ class Slide {
|
||||||
subtitle: subtitle ?? this.subtitle,
|
subtitle: subtitle ?? this.subtitle,
|
||||||
bullets: bullets ?? this.bullets,
|
bullets: bullets ?? this.bullets,
|
||||||
bullets2: bullets2 ?? this.bullets2,
|
bullets2: bullets2 ?? this.bullets2,
|
||||||
columnTitle1: columnTitle1 ?? this.columnTitle1,
|
|
||||||
columnTitle2: columnTitle2 ?? this.columnTitle2,
|
|
||||||
imagePath: imagePath ?? this.imagePath,
|
imagePath: imagePath ?? this.imagePath,
|
||||||
imagePath2: imagePath2 ?? this.imagePath2,
|
imagePath2: imagePath2 ?? this.imagePath2,
|
||||||
imageCaption: imageCaption ?? this.imageCaption,
|
imageCaption: imageCaption ?? this.imageCaption,
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,6 @@ class MarkdownService {
|
||||||
buf.writeln('theme: ${deck.theme}');
|
buf.writeln('theme: ${deck.theme}');
|
||||||
if (deck.paginate) buf.writeln('paginate: true');
|
if (deck.paginate) buf.writeln('paginate: true');
|
||||||
// General presentation metadata (also picked up by Marp where applicable).
|
// General presentation metadata (also picked up by Marp where applicable).
|
||||||
if (deck.title.isNotEmpty) {
|
|
||||||
buf.writeln('title: ${_yamlScalar(deck.title)}');
|
|
||||||
}
|
|
||||||
if (deck.author.isNotEmpty) {
|
if (deck.author.isNotEmpty) {
|
||||||
buf.writeln('author: ${_yamlScalar(deck.author)}');
|
buf.writeln('author: ${_yamlScalar(deck.author)}');
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +212,6 @@ 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}');
|
|
||||||
buf.writeln();
|
buf.writeln();
|
||||||
for (final b in slide.bullets) {
|
for (final b in slide.bullets) {
|
||||||
_writeBullet(buf, b);
|
_writeBullet(buf, b);
|
||||||
|
|
@ -224,13 +220,7 @@ class MarkdownService {
|
||||||
case SlideType.twoBullets:
|
case SlideType.twoBullets:
|
||||||
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
|
if (slide.title.isNotEmpty) buf.writeln('# ${slide.title}');
|
||||||
buf.writeln();
|
buf.writeln();
|
||||||
_writeTwoBulletColumns(
|
_writeTwoBulletColumns(buf, slide.bullets, slide.bullets2);
|
||||||
buf,
|
|
||||||
slide.bullets,
|
|
||||||
slide.bullets2,
|
|
||||||
slide.columnTitle1,
|
|
||||||
slide.columnTitle2,
|
|
||||||
);
|
|
||||||
|
|
||||||
case SlideType.bulletsImage:
|
case SlideType.bulletsImage:
|
||||||
if (slide.imagePath.isNotEmpty) {
|
if (slide.imagePath.isNotEmpty) {
|
||||||
|
|
@ -416,42 +406,17 @@ class MarkdownService {
|
||||||
StringBuffer buf,
|
StringBuffer buf,
|
||||||
List<String> left,
|
List<String> left,
|
||||||
List<String> right,
|
List<String> right,
|
||||||
String leftTitle,
|
|
||||||
String rightTitle,
|
|
||||||
) {
|
) {
|
||||||
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 (leftTitle.isNotEmpty) {
|
|
||||||
buf.writeln(
|
|
||||||
'<!-- ocideck_two_bullets_left_title: ${_encodeText(leftTitle)} -->',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (rightTitle.isNotEmpty) {
|
|
||||||
buf.writeln(
|
|
||||||
'<!-- ocideck_two_bullets_right_title: ${_encodeText(rightTitle)} -->',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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, right, rightTitle);
|
|
||||||
buf.writeln('</div>');
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _writeBulletColumn(
|
|
||||||
StringBuffer buf,
|
|
||||||
List<String> bullets,
|
|
||||||
String columnTitle,
|
|
||||||
) {
|
|
||||||
buf.writeln('<div>');
|
|
||||||
if (columnTitle.isNotEmpty) {
|
|
||||||
buf.writeln(
|
|
||||||
'<h3 style="margin:0 0 .5rem;">${_escapeHtml(columnTitle)}</h3>',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
buf.writeln('<ul style="margin:0; padding-left:1.3em;">');
|
buf.writeln('<ul style="margin:0; padding-left:1.3em;">');
|
||||||
_writeHtmlBulletItems(buf, bullets);
|
_writeHtmlBulletItems(buf, left);
|
||||||
|
buf.writeln('</ul>');
|
||||||
|
buf.writeln('<ul style="margin:0; padding-left:1.3em;">');
|
||||||
|
_writeHtmlBulletItems(buf, right);
|
||||||
buf.writeln('</ul>');
|
buf.writeln('</ul>');
|
||||||
buf.writeln('</div>');
|
buf.writeln('</div>');
|
||||||
}
|
}
|
||||||
|
|
@ -460,17 +425,6 @@ class MarkdownService {
|
||||||
return base64Url.encode(utf8.encode(jsonEncode(bullets)));
|
return base64Url.encode(utf8.encode(jsonEncode(bullets)));
|
||||||
}
|
}
|
||||||
|
|
||||||
static String _encodeText(String value) =>
|
|
||||||
base64Url.encode(utf8.encode(value));
|
|
||||||
|
|
||||||
static String _decodeText(String encoded) {
|
|
||||||
try {
|
|
||||||
return utf8.decode(base64Url.decode(encoded.trim()));
|
|
||||||
} catch (_) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<String> _decodeBullets(String encoded) {
|
static List<String> _decodeBullets(String encoded) {
|
||||||
try {
|
try {
|
||||||
final decoded = utf8.decode(base64Url.decode(encoded.trim()));
|
final decoded = utf8.decode(base64Url.decode(encoded.trim()));
|
||||||
|
|
@ -569,7 +523,6 @@ class MarkdownService {
|
||||||
String theme = 'ocideck';
|
String theme = 'ocideck';
|
||||||
bool paginate = true;
|
bool paginate = true;
|
||||||
ThemeProfile themeProfile = const ThemeProfile();
|
ThemeProfile themeProfile = const ThemeProfile();
|
||||||
String? presentationTitle;
|
|
||||||
String author = '';
|
String author = '';
|
||||||
String organization = '';
|
String organization = '';
|
||||||
String version = '';
|
String version = '';
|
||||||
|
|
@ -588,8 +541,6 @@ class MarkdownService {
|
||||||
theme = line.substring(6).trim();
|
theme = line.substring(6).trim();
|
||||||
} else if (line.startsWith('paginate:')) {
|
} else if (line.startsWith('paginate:')) {
|
||||||
paginate = line.substring(9).trim() == 'true';
|
paginate = line.substring(9).trim() == 'true';
|
||||||
} else if (line.startsWith('title:')) {
|
|
||||||
presentationTitle = _parseScalar(line.substring(6));
|
|
||||||
} else if (line.startsWith('author:')) {
|
} else if (line.startsWith('author:')) {
|
||||||
author = _parseScalar(line.substring(7));
|
author = _parseScalar(line.substring(7));
|
||||||
} else if (line.startsWith('organization:')) {
|
} else if (line.startsWith('organization:')) {
|
||||||
|
|
@ -623,11 +574,10 @@ class MarkdownService {
|
||||||
if (slide != null) slides.add(slide);
|
if (slide != null) slides.add(slide);
|
||||||
}
|
}
|
||||||
|
|
||||||
final title =
|
String title = 'Presentatie';
|
||||||
presentationTitle ??
|
if (slides.isNotEmpty && slides.first.title.isNotEmpty) {
|
||||||
(slides.isNotEmpty && slides.first.title.isNotEmpty
|
title = slides.first.title;
|
||||||
? slides.first.title
|
}
|
||||||
: 'Presentatie');
|
|
||||||
|
|
||||||
String? projectPath;
|
String? projectPath;
|
||||||
if (filePath != null) {
|
if (filePath != null) {
|
||||||
|
|
@ -676,8 +626,6 @@ class MarkdownService {
|
||||||
TlpLevel slideTlp = TlpLevel.none;
|
TlpLevel slideTlp = TlpLevel.none;
|
||||||
final bullets = <String>[];
|
final bullets = <String>[];
|
||||||
var bullets2 = <String>[];
|
var bullets2 = <String>[];
|
||||||
var columnTitle1 = '';
|
|
||||||
var columnTitle2 = '';
|
|
||||||
// bulletsImage slides store their panel width in `<!-- _style:
|
// bulletsImage slides store their panel width in `<!-- _style:
|
||||||
// --image-width: N%; -->`; capture it before the comment is stripped.
|
// --image-width: N%; -->`; capture it before the comment is stripped.
|
||||||
int styleImageWidth = 0;
|
int styleImageWidth = 0;
|
||||||
|
|
@ -698,10 +646,6 @@ class MarkdownService {
|
||||||
bullets
|
bullets
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(_decodeBullets(content.substring(25)));
|
..addAll(_decodeBullets(content.substring(25)));
|
||||||
} else if (content.startsWith('ocideck_two_bullets_left_title:')) {
|
|
||||||
columnTitle1 = _decodeText(content.substring(31));
|
|
||||||
} else if (content.startsWith('ocideck_two_bullets_right_title:')) {
|
|
||||||
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('_')) {
|
} else if (!content.startsWith('_')) {
|
||||||
|
|
@ -900,8 +844,6 @@ class MarkdownService {
|
||||||
subtitle: type == SlideType.section ? paragraph : h2,
|
subtitle: type == SlideType.section ? paragraph : h2,
|
||||||
bullets: bullets,
|
bullets: bullets,
|
||||||
bullets2: bullets2,
|
bullets2: bullets2,
|
||||||
columnTitle1: columnTitle1,
|
|
||||||
columnTitle2: columnTitle2,
|
|
||||||
imagePath: imagePath,
|
imagePath: imagePath,
|
||||||
imagePath2: imagePath2,
|
imagePath2: imagePath2,
|
||||||
imageCaption: imageCaption,
|
imageCaption: imageCaption,
|
||||||
|
|
|
||||||
|
|
@ -550,8 +550,7 @@ class _AppTabBar extends StatelessWidget {
|
||||||
_TabChip(
|
_TabChip(
|
||||||
tab: tabsState.tabs[i],
|
tab: tabsState.tabs[i],
|
||||||
isActive: i == tabsState.clampedIndex,
|
isActive: i == tabsState.clampedIndex,
|
||||||
showClose:
|
showClose: tabsState.tabs.length > 1,
|
||||||
tabsState.tabs.length > 1 || tabsState.tabs[i].isOpen,
|
|
||||||
panelText: palette.panelText,
|
panelText: palette.panelText,
|
||||||
accent: Theme.of(context).colorScheme.secondary,
|
accent: Theme.of(context).colorScheme.secondary,
|
||||||
onTap: () => onSelect(i),
|
onTap: () => onSelect(i),
|
||||||
|
|
|
||||||
|
|
@ -91,13 +91,6 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setCurrentDate() {
|
|
||||||
final now = DateTime.now();
|
|
||||||
String twoDigits(int value) => value.toString().padLeft(2, '0');
|
|
||||||
_date.text = '${now.year}-${twoDigits(now.month)}-${twoDigits(now.day)}';
|
|
||||||
_date.selection = TextSelection.collapsed(offset: _date.text.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
|
|
@ -150,12 +143,7 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 120,
|
width: 120,
|
||||||
child: _field(
|
child: _field(_date, 'Datum', 'Bijv. 2026-05-30'),
|
||||||
_date,
|
|
||||||
'Datum',
|
|
||||||
'Bijv. 2026-05-30',
|
|
||||||
onDoubleTap: _setCurrentDate,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -202,10 +190,9 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
||||||
String label,
|
String label,
|
||||||
String hint, {
|
String hint, {
|
||||||
int maxLines = 1,
|
int maxLines = 1,
|
||||||
VoidCallback? onDoubleTap,
|
|
||||||
}) {
|
}) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
final field = TextField(
|
return TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
|
@ -215,11 +202,5 @@ class _PresentationInfoDialogState extends State<PresentationInfoDialog> {
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (onDoubleTap == null) return field;
|
|
||||||
return GestureDetector(
|
|
||||||
behavior: HitTestBehavior.translucent,
|
|
||||||
onDoubleTap: onDoubleTap,
|
|
||||||
child: field,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ class BulletsEditor extends StatefulWidget {
|
||||||
|
|
||||||
class _BulletsEditorState extends State<BulletsEditor> {
|
class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
late final TextEditingController _title;
|
late final TextEditingController _title;
|
||||||
late final TextEditingController _subtitle;
|
|
||||||
late List<TextEditingController> _bullets;
|
late List<TextEditingController> _bullets;
|
||||||
late List<int> _levels;
|
late List<int> _levels;
|
||||||
late List<FocusNode> _focusNodes;
|
late List<FocusNode> _focusNodes;
|
||||||
|
|
@ -28,8 +27,6 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_title = TextEditingController(text: widget.slide.title);
|
_title = TextEditingController(text: widget.slide.title);
|
||||||
_title.addListener(_emit);
|
_title.addListener(_emit);
|
||||||
_subtitle = TextEditingController(text: widget.slide.subtitle);
|
|
||||||
_subtitle.addListener(_emit);
|
|
||||||
_initBullets(widget.slide.bullets);
|
_initBullets(widget.slide.bullets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +55,6 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
widget.onUpdate(
|
widget.onUpdate(
|
||||||
widget.slide.copyWith(
|
widget.slide.copyWith(
|
||||||
title: _title.text,
|
title: _title.text,
|
||||||
subtitle: _subtitle.text,
|
|
||||||
bullets: List.generate(
|
bullets: List.generate(
|
||||||
_bullets.length,
|
_bullets.length,
|
||||||
(i) => '\t' * _levels[i] + _bullets[i].text,
|
(i) => '\t' * _levels[i] + _bullets[i].text,
|
||||||
|
|
@ -155,7 +151,6 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_title.dispose();
|
_title.dispose();
|
||||||
_subtitle.dispose();
|
|
||||||
for (final c in _bullets) {
|
for (final c in _bullets) {
|
||||||
c.dispose();
|
c.dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -172,12 +167,6 @@ class _BulletsEditorState extends State<BulletsEditor> {
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'),
|
EditorField(label: 'Titel', controller: _title, hint: 'Slide titel'),
|
||||||
const SizedBox(height: 12),
|
|
||||||
EditorField(
|
|
||||||
label: l10n.d('Subkop (optioneel)'),
|
|
||||||
controller: _subtitle,
|
|
||||||
hint: l10n.d('Subkop'),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const SectionLabel('Bullets'),
|
const SectionLabel('Bullets'),
|
||||||
ReorderableListView(
|
ReorderableListView(
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,6 @@ class TwoBulletsEditor extends StatefulWidget {
|
||||||
|
|
||||||
class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
||||||
late final TextEditingController _title;
|
late final TextEditingController _title;
|
||||||
late final TextEditingController _heading1;
|
|
||||||
late final TextEditingController _heading2;
|
|
||||||
late _BulletSet _left;
|
late _BulletSet _left;
|
||||||
late _BulletSet _right;
|
late _BulletSet _right;
|
||||||
|
|
||||||
|
|
@ -32,10 +30,6 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_title = TextEditingController(text: widget.slide.title);
|
_title = TextEditingController(text: widget.slide.title);
|
||||||
_title.addListener(_emit);
|
_title.addListener(_emit);
|
||||||
_heading1 = TextEditingController(text: widget.slide.columnTitle1);
|
|
||||||
_heading2 = TextEditingController(text: widget.slide.columnTitle2);
|
|
||||||
_heading1.addListener(_emit);
|
|
||||||
_heading2.addListener(_emit);
|
|
||||||
_left = _BulletSet(widget.slide.bullets, _emit);
|
_left = _BulletSet(widget.slide.bullets, _emit);
|
||||||
_right = _BulletSet(widget.slide.bullets2, _emit);
|
_right = _BulletSet(widget.slide.bullets2, _emit);
|
||||||
}
|
}
|
||||||
|
|
@ -44,8 +38,6 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
||||||
widget.onUpdate(
|
widget.onUpdate(
|
||||||
widget.slide.copyWith(
|
widget.slide.copyWith(
|
||||||
title: _title.text,
|
title: _title.text,
|
||||||
columnTitle1: _heading1.text,
|
|
||||||
columnTitle2: _heading2.text,
|
|
||||||
bullets: _left.values,
|
bullets: _left.values,
|
||||||
bullets2: _right.values,
|
bullets2: _right.values,
|
||||||
),
|
),
|
||||||
|
|
@ -55,8 +47,6 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_title.dispose();
|
_title.dispose();
|
||||||
_heading1.dispose();
|
|
||||||
_heading2.dispose();
|
|
||||||
_left.dispose();
|
_left.dispose();
|
||||||
_right.dispose();
|
_right.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
@ -73,18 +63,8 @@ class _TwoBulletsEditorState extends State<TwoBulletsEditor> {
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final narrow = constraints.maxWidth < 560;
|
final narrow = constraints.maxWidth < 560;
|
||||||
final columns = [
|
final columns = [
|
||||||
_BulletColumn(
|
_BulletColumn(label: 'Bullets links', set: _left, emit: _emit),
|
||||||
label: 'Bullets links',
|
_BulletColumn(label: 'Bullets rechts', set: _right, emit: _emit),
|
||||||
set: _left,
|
|
||||||
emit: _emit,
|
|
||||||
headingController: _heading1,
|
|
||||||
),
|
|
||||||
_BulletColumn(
|
|
||||||
label: 'Bullets rechts',
|
|
||||||
set: _right,
|
|
||||||
emit: _emit,
|
|
||||||
headingController: _heading2,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
if (narrow) {
|
if (narrow) {
|
||||||
return Column(
|
return Column(
|
||||||
|
|
@ -222,13 +202,11 @@ class _BulletColumn extends StatefulWidget {
|
||||||
final String label;
|
final String label;
|
||||||
final _BulletSet set;
|
final _BulletSet set;
|
||||||
final VoidCallback emit;
|
final VoidCallback emit;
|
||||||
final TextEditingController headingController;
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -244,14 +222,6 @@ class _BulletColumnState extends State<_BulletColumn> {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
|
||||||
controller: widget.headingController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: l10n.d('Kop (optioneel)'),
|
|
||||||
isDense: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
SectionLabel(widget.label),
|
SectionLabel(widget.label),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
for (int i = 0; i < set.controllers.length; i++) _buildRow(i),
|
for (int i = 0; i < set.controllers.length; i++) _buildRow(i),
|
||||||
|
|
|
||||||
|
|
@ -646,7 +646,6 @@ class _BulletsPreview extends StatelessWidget {
|
||||||
final pad = w * 0.07;
|
final pad = w * 0.07;
|
||||||
final safe = slide.showLogo ? _logoSafeInsets(w, profile) : EdgeInsets.zero;
|
final safe = slide.showLogo ? _logoSafeInsets(w, profile) : EdgeInsets.zero;
|
||||||
final titleSize = w * 0.042;
|
final titleSize = w * 0.042;
|
||||||
final subtitleSize = w * 0.030;
|
|
||||||
final bulletSize = w * 0.026;
|
final bulletSize = w * 0.026;
|
||||||
final spacing = pad * 0.5;
|
final spacing = pad * 0.5;
|
||||||
final bulletGap = w * 0.006;
|
final bulletGap = w * 0.006;
|
||||||
|
|
@ -654,8 +653,6 @@ class _BulletsPreview extends StatelessWidget {
|
||||||
.where((b) => b.trimLeft().isNotEmpty)
|
.where((b) => b.trimLeft().isNotEmpty)
|
||||||
.toList();
|
.toList();
|
||||||
final hasTitle = slide.title.isNotEmpty;
|
final hasTitle = slide.title.isNotEmpty;
|
||||||
final subtitle = slide.subtitle;
|
|
||||||
final hasSubtitle = subtitle.isNotEmpty;
|
|
||||||
|
|
||||||
final slideHeight = w * 9 / 16;
|
final slideHeight = w * 9 / 16;
|
||||||
final availW = (w - pad * 2).clamp(w * 0.12, w);
|
final availW = (w - pad * 2).clamp(w * 0.12, w);
|
||||||
|
|
@ -672,9 +669,6 @@ class _BulletsPreview extends StatelessWidget {
|
||||||
bulletSize: bulletSize,
|
bulletSize: bulletSize,
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
bulletGap: bulletGap,
|
bulletGap: bulletGap,
|
||||||
font: font,
|
|
||||||
subtitle: subtitle,
|
|
||||||
subtitleSize: subtitleSize,
|
|
||||||
maxScale: _kSplitBulletsMaxScale,
|
maxScale: _kSplitBulletsMaxScale,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -711,23 +705,7 @@ class _BulletsPreview extends StatelessWidget {
|
||||||
),
|
),
|
||||||
linkColor: _hexColor(profile.accentColor),
|
linkColor: _hexColor(profile.accentColor),
|
||||||
),
|
),
|
||||||
if (hasSubtitle) ...[
|
if (hasTitle && bullets.isNotEmpty)
|
||||||
SizedBox(height: spacing * scale * 0.4),
|
|
||||||
_md(
|
|
||||||
context,
|
|
||||||
subtitle,
|
|
||||||
_applyFont(
|
|
||||||
font,
|
|
||||||
TextStyle(
|
|
||||||
fontSize: subtitleSize * scale,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: _hexColor(profile.accentColor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
linkColor: _hexColor(profile.accentColor),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if ((hasTitle || hasSubtitle) && bullets.isNotEmpty)
|
|
||||||
SizedBox(height: spacing * scale),
|
SizedBox(height: spacing * scale),
|
||||||
...bullets.map((b) {
|
...bullets.map((b) {
|
||||||
int level = 0;
|
int level = 0;
|
||||||
|
|
@ -922,62 +900,6 @@ class _TwoBulletsPreview extends StatelessWidget {
|
||||||
required this.profile,
|
required this.profile,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// One bullet column with an optional heading above it. When any column has a
|
|
||||||
/// heading, an equal-height slot is reserved in both so the bullet lists line
|
|
||||||
/// up.
|
|
||||||
Widget _bulletColumn(
|
|
||||||
BuildContext context, {
|
|
||||||
required String title,
|
|
||||||
required List<String> bullets,
|
|
||||||
required double columnW,
|
|
||||||
required double headingSize,
|
|
||||||
required double headingSlotH,
|
|
||||||
required double headingGap,
|
|
||||||
required double bulletSize,
|
|
||||||
required double bulletGap,
|
|
||||||
required double scale,
|
|
||||||
}) {
|
|
||||||
return SizedBox(
|
|
||||||
width: columnW,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (headingSlotH > 0) ...[
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: headingSlotH,
|
|
||||||
child: title.isEmpty
|
|
||||||
? null
|
|
||||||
: _md(
|
|
||||||
context,
|
|
||||||
title,
|
|
||||||
_applyFont(
|
|
||||||
font,
|
|
||||||
TextStyle(
|
|
||||||
fontSize: headingSize,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: _hexColor(profile.accentColor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
linkColor: _hexColor(profile.accentColor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: headingGap),
|
|
||||||
],
|
|
||||||
_BulletListColumn(
|
|
||||||
bullets: bullets,
|
|
||||||
font: font,
|
|
||||||
profile: profile,
|
|
||||||
bulletSize: bulletSize,
|
|
||||||
bulletGap: bulletGap,
|
|
||||||
scale: scale,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final pad = w * 0.065;
|
final pad = w * 0.065;
|
||||||
|
|
@ -995,12 +917,6 @@ class _TwoBulletsPreview extends StatelessWidget {
|
||||||
.toList();
|
.toList();
|
||||||
final hasTitle = slide.title.isNotEmpty;
|
final hasTitle = slide.title.isNotEmpty;
|
||||||
|
|
||||||
final col1Title = slide.columnTitle1.trim();
|
|
||||||
final col2Title = slide.columnTitle2.trim();
|
|
||||||
final hasColumnTitles = col1Title.isNotEmpty || col2Title.isNotEmpty;
|
|
||||||
final headingSize = w * 0.03;
|
|
||||||
final headingGap = w * 0.012;
|
|
||||||
|
|
||||||
final slideHeight = w * 9 / 16;
|
final slideHeight = w * 9 / 16;
|
||||||
final contentW = (w - pad * 2).clamp(w * 0.12, w);
|
final contentW = (w - pad * 2).clamp(w * 0.12, w);
|
||||||
final columnW = ((contentW - columnGap) / 2).clamp(w * 0.12, w);
|
final columnW = ((contentW - columnGap) / 2).clamp(w * 0.12, w);
|
||||||
|
|
@ -1011,19 +927,9 @@ class _TwoBulletsPreview extends StatelessWidget {
|
||||||
titleSize,
|
titleSize,
|
||||||
contentW,
|
contentW,
|
||||||
bold: true,
|
bold: true,
|
||||||
fontFamily: font,
|
|
||||||
);
|
);
|
||||||
availH -= spacing;
|
availH -= spacing;
|
||||||
}
|
}
|
||||||
// Reserve room for the (optional) column headings so the bullets still fit.
|
|
||||||
double headingHeight(String t) => t.isEmpty
|
|
||||||
? 0
|
|
||||||
: _measureTextHeight(t, headingSize, columnW, bold: true, fontFamily: font);
|
|
||||||
final maxHeadingH = math.max(
|
|
||||||
headingHeight(col1Title),
|
|
||||||
headingHeight(col2Title),
|
|
||||||
);
|
|
||||||
if (hasColumnTitles) availH -= maxHeadingH + headingGap;
|
|
||||||
final leftScale = _bulletsFitScale(
|
final leftScale = _bulletsFitScale(
|
||||||
availW: columnW,
|
availW: columnW,
|
||||||
availH: availH,
|
availH: availH,
|
||||||
|
|
@ -1034,7 +940,6 @@ class _TwoBulletsPreview extends StatelessWidget {
|
||||||
bulletSize: bulletSize,
|
bulletSize: bulletSize,
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
bulletGap: bulletGap,
|
bulletGap: bulletGap,
|
||||||
font: font,
|
|
||||||
maxScale: _kBulletsMaxScale,
|
maxScale: _kBulletsMaxScale,
|
||||||
);
|
);
|
||||||
final rightScale = _bulletsFitScale(
|
final rightScale = _bulletsFitScale(
|
||||||
|
|
@ -1047,7 +952,6 @@ class _TwoBulletsPreview extends StatelessWidget {
|
||||||
bulletSize: bulletSize,
|
bulletSize: bulletSize,
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
bulletGap: bulletGap,
|
bulletGap: bulletGap,
|
||||||
font: font,
|
|
||||||
maxScale: _kBulletsMaxScale,
|
maxScale: _kBulletsMaxScale,
|
||||||
);
|
);
|
||||||
final scale = leftScale < rightScale ? leftScale : rightScale;
|
final scale = leftScale < rightScale ? leftScale : rightScale;
|
||||||
|
|
@ -1089,30 +993,28 @@ class _TwoBulletsPreview extends StatelessWidget {
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_bulletColumn(
|
SizedBox(
|
||||||
context,
|
width: columnW,
|
||||||
title: col1Title,
|
child: _BulletListColumn(
|
||||||
bullets: leftBullets,
|
bullets: leftBullets,
|
||||||
columnW: columnW,
|
font: font,
|
||||||
headingSize: headingSize,
|
profile: profile,
|
||||||
headingSlotH: hasColumnTitles ? maxHeadingH : 0,
|
bulletSize: bulletSize,
|
||||||
headingGap: headingGap,
|
bulletGap: bulletGap,
|
||||||
bulletSize: bulletSize,
|
scale: scale,
|
||||||
bulletGap: bulletGap,
|
),
|
||||||
scale: scale,
|
|
||||||
),
|
),
|
||||||
SizedBox(width: columnGap),
|
SizedBox(width: columnGap),
|
||||||
_bulletColumn(
|
SizedBox(
|
||||||
context,
|
width: columnW,
|
||||||
title: col2Title,
|
child: _BulletListColumn(
|
||||||
bullets: rightBullets,
|
bullets: rightBullets,
|
||||||
columnW: columnW,
|
font: font,
|
||||||
headingSize: headingSize,
|
profile: profile,
|
||||||
headingSlotH: hasColumnTitles ? maxHeadingH : 0,
|
bulletSize: bulletSize,
|
||||||
headingGap: headingGap,
|
bulletGap: bulletGap,
|
||||||
bulletSize: bulletSize,
|
scale: scale,
|
||||||
bulletGap: bulletGap,
|
),
|
||||||
scale: scale,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -1185,7 +1087,6 @@ class _BulletsImagePreview extends StatelessWidget {
|
||||||
bulletSize: bulletSize,
|
bulletSize: bulletSize,
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
bulletGap: bulletGap,
|
bulletGap: bulletGap,
|
||||||
font: font,
|
|
||||||
maxScale: _kBulletsMaxScale,
|
maxScale: _kBulletsMaxScale,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1428,9 +1329,6 @@ double _bulletsFitScale({
|
||||||
required double bulletSize,
|
required double bulletSize,
|
||||||
required double spacing,
|
required double spacing,
|
||||||
required double bulletGap,
|
required double bulletGap,
|
||||||
required String font,
|
|
||||||
String subtitle = '',
|
|
||||||
double subtitleSize = 0,
|
|
||||||
double minScale = 0.2,
|
double minScale = 0.2,
|
||||||
double maxScale = 1.0,
|
double maxScale = 1.0,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -1447,9 +1345,6 @@ double _bulletsFitScale({
|
||||||
bulletSize: bulletSize,
|
bulletSize: bulletSize,
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
bulletGap: bulletGap,
|
bulletGap: bulletGap,
|
||||||
font: font,
|
|
||||||
subtitle: subtitle,
|
|
||||||
subtitleSize: subtitleSize,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Everything already fits at the largest allowed size → use it.
|
// Everything already fits at the largest allowed size → use it.
|
||||||
|
|
@ -1486,33 +1381,12 @@ double _bulletsBlockHeight({
|
||||||
required double bulletSize,
|
required double bulletSize,
|
||||||
required double spacing,
|
required double spacing,
|
||||||
required double bulletGap,
|
required double bulletGap,
|
||||||
required String font,
|
|
||||||
String subtitle = '',
|
|
||||||
double subtitleSize = 0,
|
|
||||||
}) {
|
}) {
|
||||||
var height = 0.0;
|
var height = 0.0;
|
||||||
if (hasTitle) {
|
if (hasTitle) {
|
||||||
height += _measureTextHeight(
|
height += _measureTextHeight(title, titleSize * scale, availW, bold: true);
|
||||||
title,
|
|
||||||
titleSize * scale,
|
|
||||||
availW,
|
|
||||||
bold: true,
|
|
||||||
fontFamily: font,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (subtitle.isNotEmpty) {
|
|
||||||
height += spacing * scale * 0.4;
|
|
||||||
height += _measureTextHeight(
|
|
||||||
subtitle,
|
|
||||||
subtitleSize * scale,
|
|
||||||
availW,
|
|
||||||
bold: true,
|
|
||||||
fontFamily: font,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ((hasTitle || subtitle.isNotEmpty) && bullets.isNotEmpty) {
|
|
||||||
height += spacing * scale;
|
|
||||||
}
|
}
|
||||||
|
if (hasTitle && bullets.isNotEmpty) height += spacing * scale;
|
||||||
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') {
|
||||||
|
|
@ -1522,21 +1396,15 @@ double _bulletsBlockHeight({
|
||||||
final fontSize = bulletSize * _bulletLevelScale(level) * scale;
|
final fontSize = bulletSize * _bulletLevelScale(level) * scale;
|
||||||
final indent = level * bulletSize * 1.05 * scale;
|
final indent = level * bulletSize * 1.05 * scale;
|
||||||
final marker = '${_bulletMarkerForLevel(level)} ';
|
final marker = '${_bulletMarkerForLevel(level)} ';
|
||||||
final markerW = _measureTextWidth(marker, fontSize, bold: true, fontFamily: font);
|
final markerW = _measureTextWidth(marker, fontSize, bold: true);
|
||||||
final wrapW = (availW - indent - markerW).clamp(1.0, availW);
|
final wrapW = (availW - indent - markerW).clamp(1.0, availW);
|
||||||
final textH = _measureTextHeight(
|
final textH = _measureTextHeight(
|
||||||
text,
|
text,
|
||||||
fontSize,
|
fontSize,
|
||||||
wrapW,
|
wrapW,
|
||||||
lineHeight: _kBulletLineHeight,
|
lineHeight: _kBulletLineHeight,
|
||||||
fontFamily: font,
|
|
||||||
);
|
|
||||||
final markerH = _measureTextHeight(
|
|
||||||
marker,
|
|
||||||
fontSize,
|
|
||||||
double.infinity,
|
|
||||||
fontFamily: font,
|
|
||||||
);
|
);
|
||||||
|
final markerH = _measureTextHeight(marker, fontSize, double.infinity);
|
||||||
height += bulletGap * scale * 2 + (textH > markerH ? textH : markerH);
|
height += bulletGap * scale * 2 + (textH > markerH ? textH : markerH);
|
||||||
}
|
}
|
||||||
return height;
|
return height;
|
||||||
|
|
@ -1548,13 +1416,11 @@ double _measureTextHeight(
|
||||||
double maxWidth, {
|
double maxWidth, {
|
||||||
double? lineHeight,
|
double? lineHeight,
|
||||||
bool bold = false,
|
bool bold = false,
|
||||||
String? fontFamily,
|
|
||||||
}) {
|
}) {
|
||||||
final painter = TextPainter(
|
final painter = TextPainter(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: stripInlineMarkdown(text),
|
text: stripInlineMarkdown(text),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: fontFamily,
|
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
height: lineHeight,
|
height: lineHeight,
|
||||||
fontWeight: bold ? FontWeight.bold : null,
|
fontWeight: bold ? FontWeight.bold : null,
|
||||||
|
|
@ -1565,17 +1431,11 @@ double _measureTextHeight(
|
||||||
return painter.height;
|
return painter.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
double _measureTextWidth(
|
double _measureTextWidth(String text, double fontSize, {bool bold = false}) {
|
||||||
String text,
|
|
||||||
double fontSize, {
|
|
||||||
bool bold = false,
|
|
||||||
String? fontFamily,
|
|
||||||
}) {
|
|
||||||
final painter = TextPainter(
|
final painter = TextPainter(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: stripInlineMarkdown(text),
|
text: stripInlineMarkdown(text),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: fontFamily,
|
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
fontWeight: bold ? FontWeight.bold : null,
|
fontWeight: bold ? FontWeight.bold : null,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -76,20 +76,6 @@ void main() {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('bullets slide keeps an optional subheading', () {
|
|
||||||
final out = _roundTrip(
|
|
||||||
Slide.create(SlideType.bullets).copyWith(
|
|
||||||
title: 'Agenda',
|
|
||||||
subtitle: 'Vandaag',
|
|
||||||
bullets: ['Punt een', 'Punt twee'],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(out.type, SlideType.bullets);
|
|
||||||
expect(out.title, 'Agenda');
|
|
||||||
expect(out.subtitle, 'Vandaag');
|
|
||||||
expect(out.bullets, ['Punt een', 'Punt twee']);
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
||||||
|
|
@ -104,39 +90,6 @@ void main() {
|
||||||
expect(out.bullets2, ['Rechts punt', '\t\tRechts diep']);
|
expect(out.bullets2, ['Rechts punt', '\t\tRechts diep']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('twoBullets slide keeps optional column headings', () {
|
|
||||||
final out = _roundTrip(
|
|
||||||
Slide.create(SlideType.twoBullets).copyWith(
|
|
||||||
title: 'Vergelijking',
|
|
||||||
columnTitle1: 'Voordelen',
|
|
||||||
columnTitle2: 'Nadelen',
|
|
||||||
bullets: ['Snel'],
|
|
||||||
bullets2: ['Duur'],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(out.type, SlideType.twoBullets);
|
|
||||||
expect(out.columnTitle1, 'Voordelen');
|
|
||||||
expect(out.columnTitle2, 'Nadelen');
|
|
||||||
expect(out.bullets, ['Snel']);
|
|
||||||
expect(out.bullets2, ['Duur']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('twoBullets without headings stays empty (no spurious comments)', () {
|
|
||||||
final service = MarkdownService();
|
|
||||||
final md = service.generateDeck(
|
|
||||||
Deck(
|
|
||||||
title: 'Demo',
|
|
||||||
slides: [
|
|
||||||
Slide.create(SlideType.twoBullets).copyWith(bullets: ['A'], bullets2: ['B']),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(md, isNot(contains('ocideck_two_bullets_left_title')));
|
|
||||||
final out = service.parseDeck(md)!.slides.single;
|
|
||||||
expect(out.columnTitle1, '');
|
|
||||||
expect(out.columnTitle2, '');
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
||||||
|
|
@ -433,8 +386,7 @@ void main() {
|
||||||
);
|
);
|
||||||
final deck = service.parseDeck(markdown);
|
final deck = service.parseDeck(markdown);
|
||||||
expect(deck, isNotNull);
|
expect(deck, isNotNull);
|
||||||
expect(deck!.title, 'Demo');
|
expect(deck!.author, 'Jan Jansen');
|
||||||
expect(deck.author, 'Jan Jansen');
|
|
||||||
expect(deck.organization, 'Vigilis');
|
expect(deck.organization, 'Vigilis');
|
||||||
expect(deck.version, '1.2');
|
expect(deck.version, '1.2');
|
||||||
expect(deck.date, '2026-05-30');
|
expect(deck.date, '2026-05-30');
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:ocideck/models/deck.dart';
|
|
||||||
import 'package:ocideck/widgets/dialogs/presentation_info_dialog.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('double-clicking date fills in the current date', (tester) async {
|
|
||||||
await tester.pumpWidget(
|
|
||||||
const MaterialApp(
|
|
||||||
home: Scaffold(
|
|
||||||
body: PresentationInfoDialog(deck: Deck(title: 'Test')),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final dateField = find.byWidgetPredicate(
|
|
||||||
(widget) =>
|
|
||||||
widget is TextField && widget.decoration?.labelText == 'Datum',
|
|
||||||
);
|
|
||||||
final now = DateTime.now();
|
|
||||||
String twoDigits(int value) => value.toString().padLeft(2, '0');
|
|
||||||
final expected =
|
|
||||||
'${now.year}-${twoDigits(now.month)}-${twoDigits(now.day)}';
|
|
||||||
|
|
||||||
await tester.tap(dateField);
|
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
|
||||||
await tester.tap(dateField);
|
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
|
||||||
|
|
||||||
final field = tester.widget<TextField>(dateField);
|
|
||||||
expect(field.controller!.text, expected);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:ocideck/models/slide.dart';
|
|
||||||
import 'package:ocideck/widgets/slides/slide_preview.dart';
|
|
||||||
|
|
||||||
Widget _host(Slide slide) {
|
|
||||||
return MaterialApp(
|
|
||||||
home: Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: SizedBox(
|
|
||||||
width: 800,
|
|
||||||
height: 450,
|
|
||||||
child: SlidePreviewWidget(slide: slide),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('two-bullet columns render their optional headings', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
final slide = Slide.create(SlideType.twoBullets).copyWith(
|
|
||||||
title: 'Vergelijking',
|
|
||||||
columnTitle1: 'Voordelen',
|
|
||||||
columnTitle2: 'Nadelen',
|
|
||||||
bullets: const ['Snel', 'Goedkoop'],
|
|
||||||
bullets2: const ['Complex'],
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpWidget(_host(slide));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
expect(find.text('Voordelen'), findsOneWidget);
|
|
||||||
expect(find.text('Nadelen'), findsOneWidget);
|
|
||||||
// Heading sits above the bullets of its column.
|
|
||||||
final headingTop = tester.getTopLeft(find.text('Voordelen')).dy;
|
|
||||||
final bulletTop = tester.getTopLeft(find.text('Snel')).dy;
|
|
||||||
expect(headingTop, lessThan(bulletTop));
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('two-bullet columns without headings render no heading text', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
final slide = Slide.create(SlideType.twoBullets).copyWith(
|
|
||||||
title: 'Geen koppen',
|
|
||||||
bullets: const ['Links'],
|
|
||||||
bullets2: const ['Rechts'],
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpWidget(_host(slide));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
expect(find.text('Links'), findsOneWidget);
|
|
||||||
expect(find.text('Rechts'), findsOneWidget);
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('bullets slide renders an optional subheading below the title', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
final slide = Slide.create(SlideType.bullets).copyWith(
|
|
||||||
title: 'Agenda',
|
|
||||||
subtitle: 'Vandaag',
|
|
||||||
bullets: const ['Punt een', 'Punt twee'],
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpWidget(_host(slide));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
expect(find.text('Agenda'), findsOneWidget);
|
|
||||||
expect(find.text('Vandaag'), findsOneWidget);
|
|
||||||
final titleTop = tester.getTopLeft(find.text('Agenda')).dy;
|
|
||||||
final subTop = tester.getTopLeft(find.text('Vandaag')).dy;
|
|
||||||
final bulletTop = tester.getTopLeft(find.text('Punt een')).dy;
|
|
||||||
expect(titleTop, lessThan(subTop));
|
|
||||||
expect(subTop, lessThan(bulletTop));
|
|
||||||
expect(tester.takeException(), isNull);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -2,10 +2,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:ocideck/app.dart';
|
import 'package:ocideck/app.dart';
|
||||||
import 'package:ocideck/models/deck.dart';
|
|
||||||
import 'package:ocideck/models/slide.dart';
|
|
||||||
import 'package:ocideck/state/tabs_provider.dart';
|
|
||||||
import 'package:ocideck/widgets/app_shell.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Welcome screen shows startup logo', (WidgetTester tester) async {
|
testWidgets('Welcome screen shows startup logo', (WidgetTester tester) async {
|
||||||
|
|
@ -20,27 +16,4 @@ void main() {
|
||||||
await tester.pumpWidget(const ProviderScope(child: OciDeckApp()));
|
await tester.pumpWidget(const ProviderScope(child: OciDeckApp()));
|
||||||
expect(find.byIcon(Icons.settings_outlined), findsOneWidget);
|
expect(find.byIcon(Icons.settings_outlined), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('the only open presentation can be closed', (tester) async {
|
|
||||||
await tester.binding.setSurfaceSize(const Size(1600, 1000));
|
|
||||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
||||||
await tester.pumpWidget(const ProviderScope(child: OciDeckApp()));
|
|
||||||
final container = ProviderScope.containerOf(
|
|
||||||
tester.element(find.byType(AppShell)),
|
|
||||||
);
|
|
||||||
final tab = container.read(tabsProvider).current!;
|
|
||||||
tab.deckNotifier.loadDeck(
|
|
||||||
Deck(
|
|
||||||
title: 'Test',
|
|
||||||
slides: [Slide.create(SlideType.title).copyWith(title: 'Test')],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
expect(find.byIcon(Icons.close), findsOneWidget);
|
|
||||||
await tester.tap(find.byIcon(Icons.close));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
expect(container.read(tabsProvider).current!.isOpen, isFalse);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue