Refine code slides: title outside the panel, fit-to-space, font choice
- The slide title now renders above the code panel (styled like other slide types) instead of inside the dark code window — it is the slide's title. - Code is sized to fill the panel: scaled up to use spare space (capped) and down so long fragments still fit, instead of a small block in a big box. - Add a per-profile monospace font for code slides (e.g. Courier), applied in the preview and the HTML export. - Settings: a banner on the Colours and Logo tabs makes clear they edit the loaded style profile, and colour pickers now accept a custom hex value. - Update docs and translations for the new strings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
dd54d36a60
commit
e0379ade59
12 changed files with 312 additions and 59 deletions
|
|
@ -9,9 +9,11 @@ and the project aims to follow [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Source-code slides** — a "code sheet" with per-language syntax highlighting,
|
- **Source-code slides** — a "code sheet" with per-language syntax highlighting,
|
||||||
stored as a fenced code block. Background and text colours are part of the style
|
stored as a fenced code block. Background, text colour and monospace font are
|
||||||
profile, with a syntax-colouring toggle; turning it off renders the block in a
|
part of the style profile, with a syntax-colouring toggle; turning it off renders
|
||||||
single colour (e.g. green on black for a CRT-terminal look).
|
the block in a single colour (e.g. green on black for a CRT-terminal look). The
|
||||||
|
code is sized to fill the panel — larger when there's room, smaller for long
|
||||||
|
fragments.
|
||||||
- **Charts** — bar, line, pie, and **spider/radar** chart slides. Data is entered
|
- **Charts** — bar, line, pie, and **spider/radar** chart slides. Data is entered
|
||||||
in an in-app grid or imported from CSV; the spec is stored as JSON in a ```chart
|
in an in-app grid or imported from CSV; the spec is stored as JSON in a ```chart
|
||||||
block. Data can stay inline or be linked to a CSV in a separate `data/`
|
block. Data can stay inline or be linked to a CSV in a separate `data/`
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ Built with Flutter for macOS, Windows, and Linux.
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Structured slide editors** — dedicated editors per slide type: title, bullets, two-column bullets, bullets + image, single/two images, quote, table, section divider, image-only, video, audio, source code, charts, and free-form Markdown.
|
- **Structured slide editors** — dedicated editors per slide type: title, bullets, two-column bullets, bullets + image, single/two images, quote, table, section divider, image-only, video, audio, source code, charts, and free-form Markdown.
|
||||||
- **Source-code slides** — a "code sheet" with per-language syntax highlighting, stored as a fenced code block. Background and text colours come from the style profile, with an optional syntax-colouring toggle (turn it off for a single-colour, CRT-terminal look).
|
- **Source-code slides** — a "code sheet" with per-language syntax highlighting, stored as a fenced code block. Background, text colour and monospace font come from the style profile, with an optional syntax-colouring toggle (turn it off for a single-colour, CRT-terminal look). The code auto-sizes to fill the panel.
|
||||||
- **Charts** — bar, line, pie, and spider/radar charts rendered natively (preview, presenter, PDF, PPTX) and as self-contained SVG in the HTML export. Data is entered in an in-app grid or imported from CSV; the spec is stored as JSON in the Markdown, with optional linking to a CSV kept in a tidy `data/` directory. Optional min/max draw reference lines (bar/line) or fix the scale (radar); legend hover highlights a series, and line tooltips pick the dot under the cursor.
|
- **Charts** — bar, line, pie, and spider/radar charts rendered natively (preview, presenter, PDF, PPTX) and as self-contained SVG in the HTML export. Data is entered in an in-app grid or imported from CSV; the spec is stored as JSON in the Markdown, with optional linking to a CSV kept in a tidy `data/` directory. Optional min/max draw reference lines (bar/line) or fix the scale (radar); legend hover highlights a series, and line tooltips pick the dot under the cursor.
|
||||||
- **Live preview** — see each slide rendered as you edit, with inline Markdown, footers, and TLP (Traffic Light Protocol) marking. Free-Markdown slides render fenced code with syntax highlighting and `$…$` / `$$…$$` LaTeX math.
|
- **Live preview** — see each slide rendered as you edit, with inline Markdown, footers, and TLP (Traffic Light Protocol) marking. Free-Markdown slides render fenced code with syntax highlighting and `$…$` / `$$…$$` LaTeX math.
|
||||||
- **Traffic Light Protocol** — a deck-wide classification plus an optional **per-slide TLP level**; slides classified stricter than the level the deck is shown at are automatically withheld, both when presenting and exporting.
|
- **Traffic Light Protocol** — a deck-wide classification plus an optional **per-slide TLP level**; slides classified stricter than the level the deck is shown at are automatically withheld, both when presenting and exporting.
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ JSON heeft deze velden (met standaardwaarden):
|
||||||
| `codeBackgroundColor` | `#282C34` | Achtergrond van broncode-slides. |
|
| `codeBackgroundColor` | `#282C34` | Achtergrond van broncode-slides. |
|
||||||
| `codeTextColor` | `#ABB2BF` | Tekstkleur van broncode-slides. |
|
| `codeTextColor` | `#ABB2BF` | Tekstkleur van broncode-slides. |
|
||||||
| `codeHighlightSyntax` | `true` | Syntaxkleuring aan/uit. Uit = alles in één kleur (bijv. groen op zwart voor een CRT-look). |
|
| `codeHighlightSyntax` | `true` | Syntaxkleuring aan/uit. Uit = alles in één kleur (bijv. groen op zwart voor een CRT-look). |
|
||||||
|
| `codeFontFamily` | `monospace` | Lettertype van broncode-slides (bijv. `Courier New`). |
|
||||||
| `logoPath` | `null` | Pad naar logo (relatief in `logos/`). |
|
| `logoPath` | `null` | Pad naar logo (relatief in `logos/`). |
|
||||||
| `logoPosition` | `bottom-right` | `top-left`/`top-right`/`bottom-left`/`bottom-right`. |
|
| `logoPosition` | `bottom-right` | `top-left`/`top-right`/`bottom-left`/`bottom-right`. |
|
||||||
| `logoSize` | `96` | Logogrootte in px. |
|
| `logoSize` | `96` | Logogrootte in px. |
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,12 @@ highlighting and `$…$` / `$$…$$` LaTeX math.
|
||||||
### Source-code slides
|
### Source-code slides
|
||||||
|
|
||||||
Choose a programming language for syntax highlighting (or "plain text") and paste
|
Choose a programming language for syntax highlighting (or "plain text") and paste
|
||||||
your code. It renders as a "code sheet" whose background and text colour come from
|
your code. It renders as a "code sheet" whose background, text colour and
|
||||||
the active **style profile**. Turn **syntax colouring** off to show the whole block
|
**monospace font** come from the active **style profile** (e.g. Courier). Turn
|
||||||
in a single colour — e.g. bright green on black for a classic CRT-terminal look.
|
**syntax colouring** off to show the whole block in a single colour — e.g. bright
|
||||||
Stored as a fenced code block in the Markdown.
|
green on black for a classic CRT-terminal look. The text is sized to fill the
|
||||||
|
panel — larger when there's room, smaller for long fragments. Stored as a fenced
|
||||||
|
code block in the Markdown.
|
||||||
|
|
||||||
### Charts
|
### Charts
|
||||||
|
|
||||||
|
|
@ -112,10 +114,11 @@ Export to:
|
||||||
|
|
||||||
## Theming and language
|
## Theming and language
|
||||||
|
|
||||||
- **Style profiles** control deck colours (including the source-code background and
|
- **Style profiles** control deck colours (including the source-code background,
|
||||||
text, with an optional syntax-colouring toggle), fonts, logo, and footer. Every
|
text, font and an optional syntax-colouring toggle), fonts, logo, and footer.
|
||||||
colour can be picked from the presets or entered as a custom hex value. The
|
Every colour can be picked from the presets or entered as a custom hex value. The
|
||||||
bundled Marp theme is `assets/themes/ocideck.css`.
|
Colours and Logo tabs show which profile you're editing. The bundled Marp theme
|
||||||
|
is `assets/themes/ocideck.css`.
|
||||||
- **App appearance** (including a dark interface) is configurable in settings.
|
- **App appearance** (including a dark interface) is configurable in settings.
|
||||||
- The interface is available in Dutch, English, Italian, German, French, Spanish,
|
- The interface is available in Dutch, English, Italian, German, French, Spanish,
|
||||||
Frisian, and Papiamento.
|
Frisian, and Papiamento.
|
||||||
|
|
|
||||||
|
|
@ -2382,6 +2382,9 @@ const _dutchSourceStringAdditions = {
|
||||||
'Eigen kleur (hex)': 'Custom colour (hex)',
|
'Eigen kleur (hex)': 'Custom colour (hex)',
|
||||||
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
||||||
'For example #33FF33 for a CRT-green screen.',
|
'For example #33FF33 for a CRT-green screen.',
|
||||||
|
'Onderdeel van stijlprofiel ': 'Part of style profile ',
|
||||||
|
'Broncode lettertype': 'Code font',
|
||||||
|
'Systeem (monospace)': 'System (monospace)',
|
||||||
'Platte tekst': 'Plain text',
|
'Platte tekst': 'Plain text',
|
||||||
'Titel (optioneel)': 'Title (optional)',
|
'Titel (optioneel)': 'Title (optional)',
|
||||||
'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.':
|
'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.':
|
||||||
|
|
@ -2418,6 +2421,9 @@ const _dutchSourceStringAdditions = {
|
||||||
'Eigen kleur (hex)': 'Colore personalizzato (hex)',
|
'Eigen kleur (hex)': 'Colore personalizzato (hex)',
|
||||||
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
||||||
'Ad esempio #33FF33 per uno schermo verde CRT.',
|
'Ad esempio #33FF33 per uno schermo verde CRT.',
|
||||||
|
'Onderdeel van stijlprofiel ': 'Parte del profilo di stile ',
|
||||||
|
'Broncode lettertype': 'Font del codice',
|
||||||
|
'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',
|
||||||
'Hexkleur': 'Colore esadecimale',
|
'Hexkleur': 'Colore esadecimale',
|
||||||
|
|
@ -2644,6 +2650,9 @@ const _dutchSourceStringAdditions = {
|
||||||
'Eigen kleur (hex)': 'Eigene Farbe (Hex)',
|
'Eigen kleur (hex)': 'Eigene Farbe (Hex)',
|
||||||
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
||||||
'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 ',
|
||||||
|
'Broncode lettertype': 'Code-Schriftart',
|
||||||
|
'Systeem (monospace)': 'System (monospace)',
|
||||||
'Kleur van reeks': 'Reihenfarbe',
|
'Kleur van reeks': 'Reihenfarbe',
|
||||||
'Kleur van rij': 'Zeilenfarbe',
|
'Kleur van rij': 'Zeilenfarbe',
|
||||||
'Hexkleur': 'Hex-Farbe',
|
'Hexkleur': 'Hex-Farbe',
|
||||||
|
|
@ -2871,6 +2880,9 @@ const _dutchSourceStringAdditions = {
|
||||||
'Eigen kleur (hex)': 'Couleur personnalisée (hex)',
|
'Eigen kleur (hex)': 'Couleur personnalisée (hex)',
|
||||||
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
||||||
'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 ',
|
||||||
|
'Broncode lettertype': 'Police du code',
|
||||||
|
'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',
|
||||||
'Hexkleur': 'Couleur hexadécimale',
|
'Hexkleur': 'Couleur hexadécimale',
|
||||||
|
|
@ -3098,6 +3110,9 @@ const _dutchSourceStringAdditions = {
|
||||||
'Eigen kleur (hex)': 'Color personalizado (hex)',
|
'Eigen kleur (hex)': 'Color personalizado (hex)',
|
||||||
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
||||||
'Por ejemplo #33FF33 para una pantalla verde CRT.',
|
'Por ejemplo #33FF33 para una pantalla verde CRT.',
|
||||||
|
'Onderdeel van stijlprofiel ': 'Parte del perfil de estilo ',
|
||||||
|
'Broncode lettertype': 'Fuente del código',
|
||||||
|
'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',
|
||||||
'Hexkleur': 'Color hexadecimal',
|
'Hexkleur': 'Color hexadecimal',
|
||||||
|
|
@ -3325,6 +3340,9 @@ const _dutchSourceStringAdditions = {
|
||||||
'Eigen kleur (hex)': 'Eigen kleur (hex)',
|
'Eigen kleur (hex)': 'Eigen kleur (hex)',
|
||||||
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
||||||
'Bygelyks #33FF33 foar in CRT-grien skerm.',
|
'Bygelyks #33FF33 foar in CRT-grien skerm.',
|
||||||
|
'Onderdeel van stijlprofiel ': 'Underdiel fan stylprofyl ',
|
||||||
|
'Broncode lettertype': 'Boarnekoade lettertype',
|
||||||
|
'Systeem (monospace)': 'Systeem (monospace)',
|
||||||
'Kleur van reeks': 'Rigekleur',
|
'Kleur van reeks': 'Rigekleur',
|
||||||
'Kleur van rij': 'Rijekleur',
|
'Kleur van rij': 'Rijekleur',
|
||||||
'Hexkleur': 'Hekskleur',
|
'Hexkleur': 'Hekskleur',
|
||||||
|
|
@ -3549,6 +3567,9 @@ const _dutchSourceStringAdditions = {
|
||||||
'Eigen kleur (hex)': 'Koló propio (hex)',
|
'Eigen kleur (hex)': 'Koló propio (hex)',
|
||||||
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
'Bijvoorbeeld #33FF33 voor een CRT-groen scherm.':
|
||||||
'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 ',
|
||||||
|
'Broncode lettertype': 'Tipo di lèter di kódigo',
|
||||||
|
'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',
|
||||||
'Hexkleur': 'Koló hexadecimal',
|
'Hexkleur': 'Koló hexadecimal',
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ class ThemeProfile {
|
||||||
/// syntax colours) — required for a believable single-colour CRT screen.
|
/// syntax colours) — required for a believable single-colour CRT screen.
|
||||||
final bool codeHighlightSyntax;
|
final bool codeHighlightSyntax;
|
||||||
|
|
||||||
|
/// Monospace font family for code slides. `monospace` uses the system default;
|
||||||
|
/// e.g. `Courier New` for a typewriter look.
|
||||||
|
final String codeFontFamily;
|
||||||
|
|
||||||
final String? logoPath;
|
final String? logoPath;
|
||||||
final String logoPosition;
|
final String logoPosition;
|
||||||
final int logoSize;
|
final int logoSize;
|
||||||
|
|
@ -54,6 +58,7 @@ class ThemeProfile {
|
||||||
this.codeBackgroundColor = '#282C34',
|
this.codeBackgroundColor = '#282C34',
|
||||||
this.codeTextColor = '#ABB2BF',
|
this.codeTextColor = '#ABB2BF',
|
||||||
this.codeHighlightSyntax = true,
|
this.codeHighlightSyntax = true,
|
||||||
|
this.codeFontFamily = 'monospace',
|
||||||
this.logoPath,
|
this.logoPath,
|
||||||
this.logoPosition = 'bottom-right',
|
this.logoPosition = 'bottom-right',
|
||||||
this.logoSize = 96,
|
this.logoSize = 96,
|
||||||
|
|
@ -87,6 +92,7 @@ class ThemeProfile {
|
||||||
String? codeBackgroundColor,
|
String? codeBackgroundColor,
|
||||||
String? codeTextColor,
|
String? codeTextColor,
|
||||||
bool? codeHighlightSyntax,
|
bool? codeHighlightSyntax,
|
||||||
|
String? codeFontFamily,
|
||||||
String? logoPath,
|
String? logoPath,
|
||||||
String? logoPosition,
|
String? logoPosition,
|
||||||
int? logoSize,
|
int? logoSize,
|
||||||
|
|
@ -112,6 +118,7 @@ class ThemeProfile {
|
||||||
codeBackgroundColor: codeBackgroundColor ?? this.codeBackgroundColor,
|
codeBackgroundColor: codeBackgroundColor ?? this.codeBackgroundColor,
|
||||||
codeTextColor: codeTextColor ?? this.codeTextColor,
|
codeTextColor: codeTextColor ?? this.codeTextColor,
|
||||||
codeHighlightSyntax: codeHighlightSyntax ?? this.codeHighlightSyntax,
|
codeHighlightSyntax: codeHighlightSyntax ?? this.codeHighlightSyntax,
|
||||||
|
codeFontFamily: codeFontFamily ?? this.codeFontFamily,
|
||||||
logoPath: clearLogo ? null : (logoPath ?? this.logoPath),
|
logoPath: clearLogo ? null : (logoPath ?? this.logoPath),
|
||||||
logoPosition: logoPosition ?? this.logoPosition,
|
logoPosition: logoPosition ?? this.logoPosition,
|
||||||
logoSize: logoSize ?? this.logoSize,
|
logoSize: logoSize ?? this.logoSize,
|
||||||
|
|
@ -139,6 +146,7 @@ class ThemeProfile {
|
||||||
'codeBackgroundColor': codeBackgroundColor,
|
'codeBackgroundColor': codeBackgroundColor,
|
||||||
'codeTextColor': codeTextColor,
|
'codeTextColor': codeTextColor,
|
||||||
'codeHighlightSyntax': codeHighlightSyntax,
|
'codeHighlightSyntax': codeHighlightSyntax,
|
||||||
|
'codeFontFamily': codeFontFamily,
|
||||||
'logoPath': logoPath,
|
'logoPath': logoPath,
|
||||||
'logoPosition': logoPosition,
|
'logoPosition': logoPosition,
|
||||||
'logoSize': logoSize,
|
'logoSize': logoSize,
|
||||||
|
|
@ -173,6 +181,7 @@ class ThemeProfile {
|
||||||
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',
|
||||||
logoPath: json['logoPath'] as String?,
|
logoPath: json['logoPath'] as String?,
|
||||||
logoPosition: json['logoPosition'] as String? ?? 'bottom-right',
|
logoPosition: json['logoPosition'] as String? ?? 'bottom-right',
|
||||||
logoSize: (json['logoSize'] as num?)?.round() ?? 96,
|
logoSize: (json['logoSize'] as num?)?.round() ?? 96,
|
||||||
|
|
@ -370,6 +379,17 @@ class AppSettings {
|
||||||
'Courier New',
|
'Courier New',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Monospace families offered for code slides. `monospace` is the system
|
||||||
|
/// default; the rest are common typewriter/coding faces.
|
||||||
|
static const codeFonts = [
|
||||||
|
'monospace',
|
||||||
|
'Courier New',
|
||||||
|
'Menlo',
|
||||||
|
'Consolas',
|
||||||
|
'Roboto Mono',
|
||||||
|
'Cascadia Code',
|
||||||
|
];
|
||||||
|
|
||||||
AppSettings copyWith({
|
AppSettings copyWith({
|
||||||
String? languageCode,
|
String? languageCode,
|
||||||
String? homeDirectory,
|
String? homeDirectory,
|
||||||
|
|
|
||||||
|
|
@ -589,6 +589,11 @@ class MarpHtmlService {
|
||||||
Future<String> _themedCss(ThemeProfile t) async {
|
Future<String> _themedCss(ThemeProfile t) async {
|
||||||
final fontFace = await _ebGaramondFontFace(t.fontFamily);
|
final fontFace = await _ebGaramondFontFace(t.fontFamily);
|
||||||
final family = _cssFontStack(t.fontFamily);
|
final family = _cssFontStack(t.fontFamily);
|
||||||
|
final codePrefix = t.codeFontFamily == 'monospace'
|
||||||
|
? ''
|
||||||
|
: "'${t.codeFontFamily}',";
|
||||||
|
final codeFamily =
|
||||||
|
'${codePrefix}SFMono-Regular,Consolas,"Liberation Mono",monospace';
|
||||||
return '$fontFace\n'
|
return '$fontFace\n'
|
||||||
'*{box-sizing:border-box}'
|
'*{box-sizing:border-box}'
|
||||||
'html,body{margin:0;padding:0}'
|
'html,body{margin:0;padding:0}'
|
||||||
|
|
@ -603,9 +608,9 @@ class MarpHtmlService {
|
||||||
'.slide p,.slide li{font-size:24px;line-height:1.45}'
|
'.slide p,.slide li{font-size:24px;line-height:1.45}'
|
||||||
'.slide pre{background:${t.codeBackgroundColor};color:${t.codeTextColor};'
|
'.slide pre{background:${t.codeBackgroundColor};color:${t.codeTextColor};'
|
||||||
'border:1px solid ${t.codeTextColor}38;border-radius:6px;'
|
'border:1px solid ${t.codeTextColor}38;border-radius:6px;'
|
||||||
'padding:16px;overflow:auto;font-size:18px}'
|
'padding:16px;overflow:auto;font-size:18px;font-family:$codeFamily}'
|
||||||
'.slide pre code{color:${t.codeTextColor};background:transparent}'
|
'.slide pre code{color:${t.codeTextColor};background:transparent}'
|
||||||
'.slide code{font-family:SFMono-Regular,Consolas,"Liberation Mono",monospace}'
|
'.slide code{font-family:$codeFamily}'
|
||||||
'.slide pre.mermaid{background:transparent;border:0;text-align:center}'
|
'.slide pre.mermaid{background:transparent;border:0;text-align:center}'
|
||||||
'.slide img{max-width:100%}'
|
'.slide img{max-width:100%}'
|
||||||
'.slide blockquote{border-left:4px solid ${t.accentColor};margin:.5em 0;'
|
'.slide blockquote{border-left:4px solid ${t.accentColor};margin:.5em 0;'
|
||||||
|
|
|
||||||
|
|
@ -882,11 +882,47 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A banner shown on tabs that edit the active style profile, so it is clear
|
||||||
|
/// these settings belong to the loaded profile (and which one).
|
||||||
|
Widget _profileScopeBanner() {
|
||||||
|
final name = _themeProfile.name;
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.accent.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border(left: BorderSide(color: AppTheme.accent, width: 3)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.style_outlined, size: 16, color: AppTheme.accent),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(text: context.l10n.d('Onderdeel van stijlprofiel ')),
|
||||||
|
TextSpan(
|
||||||
|
text: '“$name”',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 12, color: Color(0xFF334155)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _colorsTab() {
|
Widget _colorsTab() {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
_profileScopeBanner(),
|
||||||
_sectionTitle(l10n.d('Kleuren')),
|
_sectionTitle(l10n.d('Kleuren')),
|
||||||
_colorSetting(
|
_colorSetting(
|
||||||
l10n.d('Achtergrond slides'),
|
l10n.d('Achtergrond slides'),
|
||||||
|
|
@ -974,6 +1010,33 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
dense: true,
|
dense: true,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
initialValue: AppSettings.codeFonts.contains(_themeProfile.codeFontFamily)
|
||||||
|
? _themeProfile.codeFontFamily
|
||||||
|
: 'monospace',
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: l10n.d('Broncode lettertype'),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
for (final f in AppSettings.codeFonts)
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: f,
|
||||||
|
child: Text(
|
||||||
|
f == 'monospace' ? l10n.d('Systeem (monospace)') : f,
|
||||||
|
style: TextStyle(fontFamily: f),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v == null) return;
|
||||||
|
setState(() {
|
||||||
|
_themeProfile = _themeProfile.copyWith(codeFontFamily: v);
|
||||||
|
_profileTouched = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
_stylePreview(),
|
_stylePreview(),
|
||||||
],
|
],
|
||||||
|
|
@ -985,6 +1048,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
_profileScopeBanner(),
|
||||||
_sectionTitle(l10n.d('Logo')),
|
_sectionTitle(l10n.d('Logo')),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
|
|
@ -2086,6 +2086,16 @@ class _CodePreview extends StatelessWidget {
|
||||||
required this.profile,
|
required this.profile,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Natural (unwrapped) size of [text] in [style]: width is the longest line,
|
||||||
|
/// height the full block. Used to scale code to the available space.
|
||||||
|
static Size _measureMono(String text, TextStyle style) {
|
||||||
|
final painter = TextPainter(
|
||||||
|
text: TextSpan(text: text.isEmpty ? ' ' : text, style: style),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
return painter.size;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_ensureHighlightLanguages();
|
_ensureHighlightLanguages();
|
||||||
|
|
@ -2098,10 +2108,20 @@ class _CodePreview extends StatelessWidget {
|
||||||
final codeBg = _hexColor(profile.codeBackgroundColor);
|
final codeBg = _hexColor(profile.codeBackgroundColor);
|
||||||
final codeFg = _hexColor(profile.codeTextColor);
|
final codeFg = _hexColor(profile.codeTextColor);
|
||||||
|
|
||||||
final mono = TextStyle(
|
// The chosen monospace family, always backed by a generic monospace fallback
|
||||||
fontFamily: 'monospace',
|
// so an uninstalled face still renders fixed-width.
|
||||||
fontFamilyFallback: const ['Menlo', 'Consolas', 'Courier New'],
|
final fallback = <String>[
|
||||||
fontSize: w * 0.024,
|
'Menlo',
|
||||||
|
'Consolas',
|
||||||
|
'Courier New',
|
||||||
|
'monospace',
|
||||||
|
]..removeWhere((f) => f == profile.codeFontFamily);
|
||||||
|
final baseFont = w * 0.024;
|
||||||
|
final maxFont = w * 0.040; // grow to fill, but never huge
|
||||||
|
TextStyle monoAt(double size) => TextStyle(
|
||||||
|
fontFamily: profile.codeFontFamily,
|
||||||
|
fontFamilyFallback: fallback,
|
||||||
|
fontSize: size,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
color: codeFg,
|
color: codeFg,
|
||||||
);
|
);
|
||||||
|
|
@ -2109,23 +2129,25 @@ class _CodePreview extends StatelessWidget {
|
||||||
// HighlightView throws on an unknown language, so fall back to plain (but
|
// HighlightView throws on an unknown language, so fall back to plain (but
|
||||||
// monospace) text. When syntax highlighting is off we always render plain
|
// monospace) text. When syntax highlighting is off we always render plain
|
||||||
// text so the whole block is one colour — needed for a CRT-green screen.
|
// text so the whole block is one colour — needed for a CRT-green screen.
|
||||||
final Widget codeContent = (known && profile.codeHighlightSyntax)
|
final useHighlight = known && profile.codeHighlightSyntax;
|
||||||
|
final highlightTheme = {
|
||||||
|
...atomOneDarkTheme,
|
||||||
|
// Keep atom-one-dark's per-token colours but drop its own background so
|
||||||
|
// our themed [codeBg] shows through unchanged.
|
||||||
|
'root': (atomOneDarkTheme['root'] ?? const TextStyle()).copyWith(
|
||||||
|
backgroundColor: codeBg,
|
||||||
|
color: codeFg,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
Widget buildCode(TextStyle style) => useHighlight
|
||||||
? HighlightView(
|
? HighlightView(
|
||||||
code,
|
code,
|
||||||
language: lang,
|
language: lang,
|
||||||
// Keep atom-one-dark's per-token colours but drop its own
|
theme: highlightTheme,
|
||||||
// background so our themed [codeBg] shows through unchanged.
|
|
||||||
theme: {
|
|
||||||
...atomOneDarkTheme,
|
|
||||||
'root': (atomOneDarkTheme['root'] ?? const TextStyle()).copyWith(
|
|
||||||
backgroundColor: codeBg,
|
|
||||||
color: codeFg,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
textStyle: mono,
|
textStyle: style,
|
||||||
)
|
)
|
||||||
: Text(code, style: mono);
|
: Text(code, style: style);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
color: _hexColor(profile.slideBackgroundColor),
|
color: _hexColor(profile.slideBackgroundColor),
|
||||||
|
|
@ -2136,45 +2158,80 @@ class _CodePreview extends StatelessWidget {
|
||||||
pad,
|
pad,
|
||||||
pad + safe.bottom,
|
pad + safe.bottom,
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Column(
|
||||||
width: double.infinity,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
decoration: BoxDecoration(
|
children: [
|
||||||
color: codeBg,
|
// The slide title belongs to the slide, not inside the code window,
|
||||||
borderRadius: BorderRadius.circular(w * 0.012),
|
// so it sits above the panel like other slide types.
|
||||||
border: Border.all(color: codeFg.withValues(alpha: 0.22)),
|
if (slide.title.isNotEmpty) ...[
|
||||||
),
|
Container(
|
||||||
padding: EdgeInsets.all(w * 0.03),
|
width: double.infinity,
|
||||||
child: Column(
|
padding: EdgeInsets.symmetric(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
horizontal: w * 0.025,
|
||||||
children: [
|
vertical: w * 0.01,
|
||||||
if (slide.title.isNotEmpty) ...[
|
),
|
||||||
_md(
|
decoration: BoxDecoration(
|
||||||
|
color: _hexColor(profile.titleBackgroundColor),
|
||||||
|
borderRadius: BorderRadius.circular(w * 0.012),
|
||||||
|
border: Border(
|
||||||
|
left: BorderSide(
|
||||||
|
color: _hexColor(profile.accentColor),
|
||||||
|
width: w * 0.006,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _md(
|
||||||
context,
|
context,
|
||||||
slide.title,
|
slide.title,
|
||||||
_applyFont(
|
_applyFont(
|
||||||
font,
|
font,
|
||||||
TextStyle(
|
TextStyle(
|
||||||
fontSize: w * 0.03,
|
fontSize: w * 0.032,
|
||||||
|
height: 1.1,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: codeFg,
|
color: _hexColor(profile.titleTextColor),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
linkColor: _hexColor(profile.accentColor),
|
linkColor: _hexColor(profile.accentColor),
|
||||||
),
|
),
|
||||||
SizedBox(height: w * 0.02),
|
),
|
||||||
],
|
SizedBox(height: w * 0.018),
|
||||||
Expanded(
|
],
|
||||||
child: FittedBox(
|
Expanded(
|
||||||
fit: BoxFit.scaleDown,
|
child: Container(
|
||||||
alignment: Alignment.topLeft,
|
width: double.infinity,
|
||||||
// Een onbegrensde breedte laat code-regels op hun natuurlijke
|
decoration: BoxDecoration(
|
||||||
// lengte staan (geen woordafbreking), waarna de FittedBox het
|
color: codeBg,
|
||||||
// geheel verkleint tot het past.
|
borderRadius: BorderRadius.circular(w * 0.012),
|
||||||
child: codeContent,
|
border: Border.all(color: codeFg.withValues(alpha: 0.22)),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(w * 0.03),
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// Size the code to fill the panel: scale up to use spare
|
||||||
|
// space (capped at [maxFont]) and down so long fragments
|
||||||
|
// still fit, rather than leaving a small block in a big box.
|
||||||
|
final measured = useHighlight
|
||||||
|
? code.replaceAll('\t', ' ')
|
||||||
|
: code;
|
||||||
|
final natural = _measureMono(measured, monoAt(baseFont));
|
||||||
|
final availW = math.max(1.0, constraints.maxWidth - 1);
|
||||||
|
final availH = math.max(1.0, constraints.maxHeight - 1);
|
||||||
|
var scale = math.min(
|
||||||
|
availW / natural.width,
|
||||||
|
availH / natural.height,
|
||||||
|
);
|
||||||
|
if (!scale.isFinite || scale <= 0) scale = 1;
|
||||||
|
final size = math.min(baseFont * scale, maxFont);
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: buildCode(monoAt(size)),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -80,4 +80,78 @@ void main() {
|
||||||
expect(codeText.style?.color, _hex('#33FF33'));
|
expect(codeText.style?.color, _hex('#33FF33'));
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('short code is enlarged to use the space; long code shrinks', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
const profile = ThemeProfile(codeHighlightSyntax: false);
|
||||||
|
|
||||||
|
const short = 'x';
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_host(
|
||||||
|
Slide.create(SlideType.code).copyWith(customMarkdown: short),
|
||||||
|
profile,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
final shortSize = tester.widget<Text>(find.text(short)).style!.fontSize!;
|
||||||
|
|
||||||
|
final long = List.generate(
|
||||||
|
40,
|
||||||
|
(i) => 'final someRatherLongVariableName$i = compute($i);',
|
||||||
|
).join('\n');
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_host(
|
||||||
|
Slide.create(SlideType.code).copyWith(customMarkdown: long),
|
||||||
|
profile,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
final longSize = tester.widget<Text>(find.text(long)).style!.fontSize!;
|
||||||
|
|
||||||
|
// A tiny snippet is scaled up to fill; a big one is scaled down to fit.
|
||||||
|
expect(longSize, lessThan(shortSize));
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('the slide title sits above the code panel, not inside it', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final slide = Slide.create(
|
||||||
|
SlideType.code,
|
||||||
|
).copyWith(title: 'Voorbeeld', customMarkdown: 'print("hi")');
|
||||||
|
const profile = ThemeProfile(
|
||||||
|
titleTextColor: '#FFFFFF',
|
||||||
|
titleBackgroundColor: '#1C2B47',
|
||||||
|
codeBackgroundColor: '#000000',
|
||||||
|
codeTextColor: '#33FF33',
|
||||||
|
codeHighlightSyntax: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(_host(slide, profile));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// The title is rendered above the code panel rather than inside it.
|
||||||
|
final titleBottom = tester.getBottomLeft(find.text('Voorbeeld')).dy;
|
||||||
|
final codeTop = tester.getTopLeft(find.text('print("hi")')).dy;
|
||||||
|
expect(titleBottom, lessThanOrEqualTo(codeTop));
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('code uses the chosen monospace font family', (tester) async {
|
||||||
|
final slide = Slide.create(
|
||||||
|
SlideType.code,
|
||||||
|
).copyWith(customMarkdown: 'void main() {}');
|
||||||
|
const profile = ThemeProfile(
|
||||||
|
codeFontFamily: 'Courier New',
|
||||||
|
codeHighlightSyntax: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(_host(slide, profile));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final codeText = tester.widget<Text>(find.text('void main() {}'));
|
||||||
|
expect(codeText.style?.fontFamily, 'Courier New');
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,11 +106,14 @@ void main() {}
|
||||||
const theme = ThemeProfile(
|
const theme = ThemeProfile(
|
||||||
codeBackgroundColor: '#000000',
|
codeBackgroundColor: '#000000',
|
||||||
codeTextColor: '#33FF33',
|
codeTextColor: '#33FF33',
|
||||||
|
codeFontFamily: 'Courier New',
|
||||||
);
|
);
|
||||||
final html = await service.build('```dart\nvoid main() {}\n```', theme: theme);
|
final html = await service.build('```dart\nvoid main() {}\n```', theme: theme);
|
||||||
|
|
||||||
expect(html, contains('.slide pre{background:#000000;color:#33FF33'));
|
expect(html, contains('.slide pre{background:#000000;color:#33FF33'));
|
||||||
expect(html, contains('.slide pre code{color:#33FF33'));
|
expect(html, contains('.slide pre code{color:#33FF33'));
|
||||||
|
// The chosen code font is used (with a monospace fallback chain).
|
||||||
|
expect(html, contains("font-family:'Courier New',"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('EB Garamond theme embeds the font for offline rendering', () async {
|
test('EB Garamond theme embeds the font for offline rendering', () async {
|
||||||
|
|
|
||||||
|
|
@ -15,24 +15,27 @@ Future<SettingsNotifier> _loadedNotifier() async {
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
test('ThemeProfile round-trips the code colours through JSON', () {
|
test('ThemeProfile round-trips the code styling through JSON', () {
|
||||||
const profile = ThemeProfile(
|
const profile = ThemeProfile(
|
||||||
codeBackgroundColor: '#000000',
|
codeBackgroundColor: '#000000',
|
||||||
codeTextColor: '#33FF33',
|
codeTextColor: '#33FF33',
|
||||||
codeHighlightSyntax: false,
|
codeHighlightSyntax: false,
|
||||||
|
codeFontFamily: 'Courier New',
|
||||||
);
|
);
|
||||||
final back = ThemeProfile.fromJson(profile.toJson());
|
final back = ThemeProfile.fromJson(profile.toJson());
|
||||||
expect(back.codeBackgroundColor, '#000000');
|
expect(back.codeBackgroundColor, '#000000');
|
||||||
expect(back.codeTextColor, '#33FF33');
|
expect(back.codeTextColor, '#33FF33');
|
||||||
expect(back.codeHighlightSyntax, isFalse);
|
expect(back.codeHighlightSyntax, isFalse);
|
||||||
|
expect(back.codeFontFamily, 'Courier New');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ThemeProfile code colours default 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'});
|
||||||
expect(back.codeBackgroundColor, '#282C34');
|
expect(back.codeBackgroundColor, '#282C34');
|
||||||
expect(back.codeTextColor, '#ABB2BF');
|
expect(back.codeTextColor, '#ABB2BF');
|
||||||
expect(back.codeHighlightSyntax, isTrue);
|
expect(back.codeHighlightSyntax, isTrue);
|
||||||
|
expect(back.codeFontFamily, 'monospace');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('starts with a single default profile', () async {
|
test('starts with a single default profile', () async {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue