From b7db54e03302a5924c185225ffce08d742255a49 Mon Sep 17 00:00:00 2001 From: Brenno de Winter Date: Sat, 6 Jun 2026 20:41:24 +0200 Subject: [PATCH] Add app theming, code slides, and flicker-free transitions Bundles several in-progress changes from the working tree: - App appearance / look-and-feel: customizable app theme profiles (colors, dark interface) with a settings UI and persistence. - New "Broncode" (source code) slide type: dark code sheet with syntax highlighting, a dedicated editor with a language picker, and Marp markdown round-trip via a fenced code block. - Presenter: eliminate the brief black frame between slides by precaching neighbouring slide images and enabling gaplessPlayback, so recordings stay clean. Adds round-trip tests for the code slide and translations for the new strings across all supported languages. Co-Authored-By: Claude Opus 4.8 --- lib/app.dart | 5 +- lib/l10n/app_localizations.dart | 162 ++++++++ lib/main.dart | 3 +- lib/models/settings.dart | 149 +++++++ lib/models/slide.dart | 10 + lib/services/markdown_service.dart | 96 +++++ lib/state/settings_provider.dart | 128 +++++++ lib/theme/app_theme.dart | 117 +++++- lib/widgets/app_shell.dart | 94 +++-- lib/widgets/dialogs/add_slide_dialog.dart | 1 + lib/widgets/dialogs/settings_dialog.dart | 362 +++++++++++++++++- lib/widgets/editors/code_editor.dart | 145 +++++++ lib/widgets/panels/editor_panel.dart | 10 + lib/widgets/panels/slide_list_panel.dart | 6 +- .../presentation/fullscreen_presenter.dart | 31 ++ lib/widgets/slides/slide_preview.dart | 122 +++++- macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- macos/Podfile.lock | 2 +- pubspec.lock | 6 +- pubspec.yaml | 3 + test/markdown_round_trip_test.dart | 25 ++ test/settings_provider_test.dart | 35 ++ test/widget_test.dart | 6 + 23 files changed, 1461 insertions(+), 59 deletions(-) create mode 100644 lib/widgets/editors/code_editor.dart diff --git a/lib/app.dart b/lib/app.dart index 4283111..33c3564 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -14,10 +14,13 @@ class OciDeckApp extends ConsumerWidget { final languageCode = ref.watch( settingsProvider.select((s) => s.languageCode), ); + final appearance = ref.watch( + settingsProvider.select((s) => s.appAppearanceProfile), + ); AppLocalizations.setActiveLanguageCode(languageCode); return MaterialApp( title: 'OciDeck', - theme: AppTheme.light, + theme: AppTheme.fromProfile(appearance), debugShowCheckedModeBanner: false, locale: AppLocalizations.materialLocaleFor(languageCode), supportedLocales: AppLocalizations.supportedLocales, diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a1f5a2d..0f25d6e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -964,6 +964,25 @@ const _dutchSourceStrings = { 'Comma-separated, e.g. quarterly, numbers, 2026', 'Deze gegevens worden in de markdown opgeslagen en zijn doorzoekbaar bij het openen.': 'These details are stored in the Markdown and searchable when opening.', + 'App-thema': 'App theme', + 'Look-and-feel': 'Look and feel', + 'Kopie maken en aanpassen': 'Create and customize a copy', + 'Thema verwijderen': 'Delete theme', + 'Themanaam': 'Theme name', + 'Dit is een ingebouwd thema. Maak een kopie om kleuren aan te passen.': + 'This is a built-in theme. Create a copy to customize its colors.', + 'Donkere interface': 'Dark interface', + 'Past contrast, invoervelden en systeemcomponenten aan.': + 'Adjusts contrast, input fields, and system components.', + 'Hoofdkleur en bovenbalk': 'Primary color and top bar', + 'Knoppen en accenten': 'Buttons and accents', + 'Schermachtergrond': 'Screen background', + 'Kaarten en dialogen': 'Cards and dialogs', + 'Gedempte tekst': 'Muted text', + 'Zijpanelen': 'Side panels', + 'Tekst op zijpanelen': 'Text on side panels', + 'Voorbeeldtekst': 'Sample text', + 'Knop': 'Button', 'Profielnaam': 'Profile name', 'Naam van het stijlprofiel': 'Name of the style profile', 'Stijlprofiel': 'Style profile', @@ -1153,6 +1172,10 @@ const _dutchSourceStrings = { 'Grote Afbeelding': 'Immagine grande', 'Tabel': 'Tabella', 'Vrije Markdown': 'Markdown libero', + 'Broncode': 'Codice sorgente', + 'Programmeertaal': 'Linguaggio di programmazione', + 'Plak of typ hier je broncode...': + 'Incolla o digita qui il tuo codice sorgente...', 'Overgeslagen': 'Saltata', 'Kopiëren': 'Copia', 'Kopieer als afbeelding': 'Copia come immagine', @@ -1221,6 +1244,25 @@ const _dutchSourceStrings = { 'Datum': 'Data', 'Beschrijving': 'Descrizione', 'Trefwoorden': 'Parole chiave', + 'App-thema': 'Tema dell’app', + 'Look-and-feel': 'Aspetto', + 'Kopie maken en aanpassen': 'Crea e personalizza una copia', + 'Thema verwijderen': 'Elimina tema', + 'Themanaam': 'Nome del tema', + 'Dit is een ingebouwd thema. Maak een kopie om kleuren aan te passen.': + 'Questo è un tema integrato. Crea una copia per personalizzare i colori.', + 'Donkere interface': 'Interfaccia scura', + 'Past contrast, invoervelden en systeemcomponenten aan.': + 'Adatta contrasto, campi di input e componenti di sistema.', + 'Hoofdkleur en bovenbalk': 'Colore principale e barra superiore', + 'Knoppen en accenten': 'Pulsanti e accenti', + 'Schermachtergrond': 'Sfondo dello schermo', + 'Kaarten en dialogen': 'Schede e finestre di dialogo', + 'Gedempte tekst': 'Testo attenuato', + 'Zijpanelen': 'Pannelli laterali', + 'Tekst op zijpanelen': 'Testo sui pannelli laterali', + 'Voorbeeldtekst': 'Testo di esempio', + 'Knop': 'Pulsante', 'Profielnaam': 'Nome profilo', 'Stijlprofiel': 'Profilo stile', 'Lettertype': 'Font', @@ -1301,6 +1343,10 @@ const _dutchSourceStrings = { 'Grote Afbeelding': 'Großes Bild', 'Tabel': 'Tabelle', 'Vrije Markdown': 'Freies Markdown', + 'Broncode': 'Quellcode', + 'Programmeertaal': 'Programmiersprache', + 'Plak of typ hier je broncode...': + 'Quellcode hier einfügen oder eingeben...', 'Overgeslagen': 'Übersprungen', 'Kopiëren': 'Kopieren', 'Kopieer als afbeelding': 'Als Bild kopieren', @@ -1369,6 +1415,25 @@ const _dutchSourceStrings = { 'Datum': 'Datum', 'Beschrijving': 'Beschreibung', 'Trefwoorden': 'Schlüsselwörter', + 'App-thema': 'App-Design', + 'Look-and-feel': 'Erscheinungsbild', + 'Kopie maken en aanpassen': 'Kopie erstellen und anpassen', + 'Thema verwijderen': 'Design löschen', + 'Themanaam': 'Designname', + 'Dit is een ingebouwd thema. Maak een kopie om kleuren aan te passen.': + 'Dies ist ein integriertes Design. Erstellen Sie eine Kopie, um die Farben anzupassen.', + 'Donkere interface': 'Dunkle Oberfläche', + 'Past contrast, invoervelden en systeemcomponenten aan.': + 'Passt Kontrast, Eingabefelder und Systemkomponenten an.', + 'Hoofdkleur en bovenbalk': 'Hauptfarbe und obere Leiste', + 'Knoppen en accenten': 'Schaltflächen und Akzente', + 'Schermachtergrond': 'Bildschirmhintergrund', + 'Kaarten en dialogen': 'Karten und Dialoge', + 'Gedempte tekst': 'Gedämpfter Text', + 'Zijpanelen': 'Seitenleisten', + 'Tekst op zijpanelen': 'Text auf Seitenleisten', + 'Voorbeeldtekst': 'Beispieltext', + 'Knop': 'Schaltfläche', 'Profielnaam': 'Profilname', 'Stijlprofiel': 'Stilprofil', 'Lettertype': 'Schriftart', @@ -1450,6 +1515,10 @@ const _dutchSourceStrings = { 'Grote Afbeelding': 'Grande image', 'Tabel': 'Tableau', 'Vrije Markdown': 'Markdown libre', + 'Broncode': 'Code source', + 'Programmeertaal': 'Langage de programmation', + 'Plak of typ hier je broncode...': + 'Collez ou tapez votre code source ici...', 'Overgeslagen': 'Ignorée', 'Kopiëren': 'Copier', 'Kopieer als afbeelding': 'Copier comme image', @@ -1518,6 +1587,25 @@ const _dutchSourceStrings = { 'Datum': 'Date', 'Beschrijving': 'Description', 'Trefwoorden': 'Mots-clés', + 'App-thema': 'Thème de l’application', + 'Look-and-feel': 'Apparence', + 'Kopie maken en aanpassen': 'Créer et personnaliser une copie', + 'Thema verwijderen': 'Supprimer le thème', + 'Themanaam': 'Nom du thème', + 'Dit is een ingebouwd thema. Maak een kopie om kleuren aan te passen.': + 'Ce thème est intégré. Créez une copie pour personnaliser ses couleurs.', + 'Donkere interface': 'Interface sombre', + 'Past contrast, invoervelden en systeemcomponenten aan.': + 'Adapte le contraste, les champs de saisie et les composants système.', + 'Hoofdkleur en bovenbalk': 'Couleur principale et barre supérieure', + 'Knoppen en accenten': 'Boutons et accents', + 'Schermachtergrond': 'Arrière-plan de l’écran', + 'Kaarten en dialogen': 'Cartes et boîtes de dialogue', + 'Gedempte tekst': 'Texte atténué', + 'Zijpanelen': 'Panneaux latéraux', + 'Tekst op zijpanelen': 'Texte des panneaux latéraux', + 'Voorbeeldtekst': 'Exemple de texte', + 'Knop': 'Bouton', 'Profielnaam': 'Nom du profil', 'Stijlprofiel': 'Profil de style', 'Lettertype': 'Police', @@ -1598,6 +1686,10 @@ const _dutchSourceStrings = { 'Grote Afbeelding': 'Imagen grande', 'Tabel': 'Tabla', 'Vrije Markdown': 'Markdown libre', + 'Broncode': 'Código fuente', + 'Programmeertaal': 'Lenguaje de programación', + 'Plak of typ hier je broncode...': + 'Pega o escribe aquí tu código fuente...', 'Overgeslagen': 'Omitida', 'Kopiëren': 'Copiar', 'Kopieer als afbeelding': 'Copiar como imagen', @@ -1666,6 +1758,25 @@ const _dutchSourceStrings = { 'Datum': 'Fecha', 'Beschrijving': 'Descripción', 'Trefwoorden': 'Palabras clave', + 'App-thema': 'Tema de la aplicación', + 'Look-and-feel': 'Apariencia', + 'Kopie maken en aanpassen': 'Crear y personalizar una copia', + 'Thema verwijderen': 'Eliminar tema', + 'Themanaam': 'Nombre del tema', + 'Dit is een ingebouwd thema. Maak een kopie om kleuren aan te passen.': + 'Este es un tema integrado. Crea una copia para personalizar los colores.', + 'Donkere interface': 'Interfaz oscura', + 'Past contrast, invoervelden en systeemcomponenten aan.': + 'Ajusta el contraste, los campos de entrada y los componentes del sistema.', + 'Hoofdkleur en bovenbalk': 'Color principal y barra superior', + 'Knoppen en accenten': 'Botones y acentos', + 'Schermachtergrond': 'Fondo de pantalla', + 'Kaarten en dialogen': 'Tarjetas y diálogos', + 'Gedempte tekst': 'Texto atenuado', + 'Zijpanelen': 'Paneles laterales', + 'Tekst op zijpanelen': 'Texto de los paneles laterales', + 'Voorbeeldtekst': 'Texto de ejemplo', + 'Knop': 'Botón', 'Profielnaam': 'Nombre del perfil', 'Stijlprofiel': 'Perfil de estilo', 'Lettertype': 'Fuente', @@ -1747,6 +1858,10 @@ const _dutchSourceStrings = { 'Grote Afbeelding': 'Grutte ôfbylding', 'Tabel': 'Tabel', 'Vrije Markdown': 'Frije Markdown', + 'Broncode': 'Boarnekoade', + 'Programmeertaal': 'Programmeartaal', + 'Plak of typ hier je broncode...': + 'Plak of typ hjir dyn boarnekoade...', 'Overgeslagen': 'Oerslein', 'Kopiëren': 'Kopiearje', 'Kopieer als afbeelding': 'Kopiearje as ôfbylding', @@ -1815,6 +1930,25 @@ const _dutchSourceStrings = { 'Datum': 'Datum', 'Beschrijving': 'Beskriuwing', 'Trefwoorden': 'Trefwurden', + 'App-thema': 'App-tema', + 'Look-and-feel': 'Uterlik', + 'Kopie maken en aanpassen': 'Kopy meitsje en oanpasse', + 'Thema verwijderen': 'Tema wiskje', + 'Themanaam': 'Temanamme', + 'Dit is een ingebouwd thema. Maak een kopie om kleuren aan te passen.': + 'Dit is in ynboud tema. Meitsje in kopy om de kleuren oan te passen.', + 'Donkere interface': 'Donkere ynterface', + 'Past contrast, invoervelden en systeemcomponenten aan.': + 'Past kontrast, ynfierfjilden en systeemkomponinten oan.', + 'Hoofdkleur en bovenbalk': 'Haadkleur en boppebalke', + 'Knoppen en accenten': 'Knoppen en aksinten', + 'Schermachtergrond': 'Skermeftergrûn', + 'Kaarten en dialogen': 'Kaarten en dialoochfinsters', + 'Gedempte tekst': 'Dimde tekst', + 'Zijpanelen': 'Sydpanielen', + 'Tekst op zijpanelen': 'Tekst op sydpanielen', + 'Voorbeeldtekst': 'Foarbyldtekst', + 'Knop': 'Knop', 'Profielnaam': 'Profylnamme', 'Stijlprofiel': 'Stylprofyl', 'Lettertype': 'Lettertype', @@ -1897,6 +2031,10 @@ const _dutchSourceStrings = { 'Grote Afbeelding': 'Imágen grandi', 'Tabel': 'Tabel', 'Vrije Markdown': 'Markdown liber', + 'Broncode': 'Código fuente', + 'Programmeertaal': 'Lenguahe di programashon', + 'Plak of typ hier je broncode...': + 'Pega òf tek bo código fuente akinan...', 'Overgeslagen': 'Saltá', 'Kopiëren': 'Kopia', 'Kopieer als afbeelding': 'Kopia komo imágen', @@ -1965,6 +2103,25 @@ const _dutchSourceStrings = { 'Datum': 'Fecha', 'Beschrijving': 'Deskripshon', 'Trefwoorden': 'Palabranan klave', + 'App-thema': 'Tema di app', + 'Look-and-feel': 'Aparensia', + 'Kopie maken en aanpassen': 'Krea i personalisá un kopia', + 'Thema verwijderen': 'Kita tema', + 'Themanaam': 'Nòmber di tema', + 'Dit is een ingebouwd thema. Maak een kopie om kleuren aan te passen.': + 'Esaki ta un tema integrá. Krea un kopia pa personalisá e kolónan.', + 'Donkere interface': 'Interfas skur', + 'Past contrast, invoervelden en systeemcomponenten aan.': + 'Ta adaptá kontraste, kamponan di entrada i komponentenan di sistema.', + 'Hoofdkleur en bovenbalk': 'Koló prinsipal i bara ariba', + 'Knoppen en accenten': 'Botonnan i aksèntnan', + 'Schermachtergrond': 'Fondo di pantaya', + 'Kaarten en dialogen': 'Karchinan i diálogonan', + 'Gedempte tekst': 'Teksto suavisa', + 'Zijpanelen': 'Panelnan lateral', + 'Tekst op zijpanelen': 'Teksto riba panelnan lateral', + 'Voorbeeldtekst': 'Teksto di ehèmpel', + 'Knop': 'Boton', 'Profielnaam': 'Nòmber di perfil', 'Stijlprofiel': 'Perfil di estilo', 'Lettertype': 'Font', @@ -2034,7 +2191,12 @@ const _dutchSourceStrings = { const _dutchSourceStringAdditions = { 'en': { 'Afbeelding': 'Image', + 'Broncode': 'Source code', 'Bullet': 'Bullet', + 'Plak of typ hier je broncode...': 'Paste or type your source code here...', + 'Programmeertaal': 'Programming language', + 'Platte tekst': 'Plain text', + 'Titel (optioneel)': 'Title (optional)', 'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.': 'HTML opens in any browser without internet and renders code blocks, math and Mermaid diagrams.', 'Laatste slide': 'Final slide', diff --git a/lib/main.dart b/lib/main.dart index 970944d..3fc5877 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:window_manager/window_manager.dart'; @@ -7,7 +8,7 @@ import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { + if (!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux)) { await windowManager.ensureInitialized(); const options = WindowOptions( minimumSize: Size(1000, 650), diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 12e8d2d..5257d79 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -160,6 +160,137 @@ class ThemeProfile { } } +class AppAppearanceProfile { + final String name; + final bool isBuiltIn; + final bool isDark; + final String primaryColor; + final String accentColor; + final String backgroundColor; + final String surfaceColor; + final String textColor; + final String mutedTextColor; + final String panelColor; + final String panelTextColor; + + const AppAppearanceProfile({ + required this.name, + this.isBuiltIn = false, + this.isDark = false, + required this.primaryColor, + required this.accentColor, + required this.backgroundColor, + required this.surfaceColor, + required this.textColor, + required this.mutedTextColor, + required this.panelColor, + required this.panelTextColor, + }); + + static const basic = AppAppearanceProfile( + name: 'Basic', + isBuiltIn: true, + primaryColor: '#1C2B47', + accentColor: '#2563EB', + backgroundColor: '#F8F9FA', + surfaceColor: '#FFFFFF', + textColor: '#1E293B', + mutedTextColor: '#64748B', + panelColor: '#1E2028', + panelTextColor: '#E2E8F0', + ); + + static const europa = AppAppearanceProfile( + name: 'Europa', + isBuiltIn: true, + primaryColor: '#003399', + accentColor: '#FFCC00', + backgroundColor: '#F4F7FC', + surfaceColor: '#FFFFFF', + textColor: '#17233D', + mutedTextColor: '#5D6B85', + panelColor: '#00266F', + panelTextColor: '#FFFFFF', + ); + + static const dark = AppAppearanceProfile( + name: 'Donker', + isBuiltIn: true, + isDark: true, + primaryColor: '#111827', + accentColor: '#60A5FA', + backgroundColor: '#0F172A', + surfaceColor: '#1E293B', + textColor: '#F1F5F9', + mutedTextColor: '#94A3B8', + panelColor: '#090E1A', + panelTextColor: '#E2E8F0', + ); + + static const builtIns = [basic, europa, dark]; + + AppAppearanceProfile copyWith({ + String? name, + bool? isBuiltIn, + bool? isDark, + String? primaryColor, + String? accentColor, + String? backgroundColor, + String? surfaceColor, + String? textColor, + String? mutedTextColor, + String? panelColor, + String? panelTextColor, + }) { + return AppAppearanceProfile( + name: name ?? this.name, + isBuiltIn: isBuiltIn ?? this.isBuiltIn, + isDark: isDark ?? this.isDark, + primaryColor: primaryColor ?? this.primaryColor, + accentColor: accentColor ?? this.accentColor, + backgroundColor: backgroundColor ?? this.backgroundColor, + surfaceColor: surfaceColor ?? this.surfaceColor, + textColor: textColor ?? this.textColor, + mutedTextColor: mutedTextColor ?? this.mutedTextColor, + panelColor: panelColor ?? this.panelColor, + panelTextColor: panelTextColor ?? this.panelTextColor, + ); + } + + Map toJson() { + return { + 'name': name, + 'isBuiltIn': isBuiltIn, + 'isDark': isDark, + 'primaryColor': primaryColor, + 'accentColor': accentColor, + 'backgroundColor': backgroundColor, + 'surfaceColor': surfaceColor, + 'textColor': textColor, + 'mutedTextColor': mutedTextColor, + 'panelColor': panelColor, + 'panelTextColor': panelTextColor, + }; + } + + factory AppAppearanceProfile.fromJson(Map json) { + return AppAppearanceProfile( + name: json['name'] as String? ?? 'Eigen thema', + isBuiltIn: json['isBuiltIn'] as bool? ?? false, + isDark: json['isDark'] as bool? ?? false, + primaryColor: json['primaryColor'] as String? ?? basic.primaryColor, + accentColor: json['accentColor'] as String? ?? basic.accentColor, + backgroundColor: + json['backgroundColor'] as String? ?? basic.backgroundColor, + surfaceColor: json['surfaceColor'] as String? ?? basic.surfaceColor, + textColor: json['textColor'] as String? ?? basic.textColor, + mutedTextColor: json['mutedTextColor'] as String? ?? basic.mutedTextColor, + panelColor: json['panelColor'] as String? ?? basic.panelColor, + panelTextColor: json['panelTextColor'] as String? ?? basic.panelTextColor, + ); + } +} + class AppSettings { final String languageCode; final String? homeDirectory; @@ -169,6 +300,8 @@ class AppSettings { final String? exportDirectory; final List themeProfiles; final String selectedThemeProfileName; + final List appAppearanceProfiles; + final String selectedAppAppearanceProfileName; final List recentFiles; const AppSettings({ @@ -177,6 +310,8 @@ class AppSettings { this.exportDirectory, this.themeProfiles = const [ThemeProfile()], this.selectedThemeProfileName = 'Standaard', + this.appAppearanceProfiles = AppAppearanceProfile.builtIns, + this.selectedAppAppearanceProfileName = 'Basic', this.recentFiles = const [], }); @@ -187,6 +322,13 @@ class AppSettings { ); } + AppAppearanceProfile get appAppearanceProfile { + return appAppearanceProfiles.firstWhere( + (p) => p.name == selectedAppAppearanceProfileName, + orElse: () => appAppearanceProfiles.first, + ); + } + static const availableFonts = [ 'Arial', 'EB Garamond', @@ -208,6 +350,8 @@ class AppSettings { ThemeProfile? themeProfile, List? themeProfiles, String? selectedThemeProfileName, + List? appAppearanceProfiles, + String? selectedAppAppearanceProfileName, List? recentFiles, bool clearHomeDirectory = false, bool clearExportDirectory = false, @@ -236,6 +380,11 @@ class AppSettings { selectedThemeProfileName ?? themeProfile?.name ?? this.selectedThemeProfileName, + appAppearanceProfiles: + appAppearanceProfiles ?? this.appAppearanceProfiles, + selectedAppAppearanceProfileName: + selectedAppAppearanceProfileName ?? + this.selectedAppAppearanceProfileName, recentFiles: recentFiles ?? this.recentFiles, ); } diff --git a/lib/models/slide.dart b/lib/models/slide.dart index 2782747..7d3fb9e 100644 --- a/lib/models/slide.dart +++ b/lib/models/slide.dart @@ -14,6 +14,7 @@ enum SlideType { quote, table, freeMarkdown, + code, } extension SlideTypeExtension on SlideType { @@ -41,6 +42,8 @@ extension SlideTypeExtension on SlideType { return 'Tabel'; case SlideType.freeMarkdown: return 'Vrije Markdown'; + case SlideType.code: + return 'Broncode'; } } @@ -68,6 +71,8 @@ extension SlideTypeExtension on SlideType { return 'table'; case SlideType.freeMarkdown: return ''; + case SlideType.code: + return 'code'; } } } @@ -90,6 +95,7 @@ class Slide { final String quote; final String quoteAuthor; final String customMarkdown; + final String codeLanguage; // highlight.js language id for code slides ('' = plain) final String cssClass; final String notes; final double advanceDuration; // 0 = no auto-advance @@ -117,6 +123,7 @@ class Slide { this.quote = '', this.quoteAuthor = '', this.customMarkdown = '', + this.codeLanguage = '', this.cssClass = '', this.notes = '', this.advanceDuration = 0, @@ -168,6 +175,7 @@ class Slide { quote: src.quote, quoteAuthor: src.quoteAuthor, customMarkdown: src.customMarkdown, + codeLanguage: src.codeLanguage, cssClass: src.cssClass, notes: src.notes, advanceDuration: src.advanceDuration, @@ -196,6 +204,7 @@ class Slide { String? quote, String? quoteAuthor, String? customMarkdown, + String? codeLanguage, String? cssClass, String? notes, double? advanceDuration, @@ -223,6 +232,7 @@ class Slide { quote: quote ?? this.quote, quoteAuthor: quoteAuthor ?? this.quoteAuthor, customMarkdown: customMarkdown ?? this.customMarkdown, + codeLanguage: codeLanguage ?? this.codeLanguage, cssClass: cssClass ?? this.cssClass, notes: notes ?? this.notes, advanceDuration: advanceDuration ?? this.advanceDuration, diff --git a/lib/services/markdown_service.dart b/lib/services/markdown_service.dart index 027212c..4b91eba 100644 --- a/lib/services/markdown_service.dart +++ b/lib/services/markdown_service.dart @@ -317,6 +317,19 @@ class MarkdownService { !slide.customMarkdown.endsWith('\n')) { buf.writeln(); } + + case SlideType.code: + if (slide.title.isNotEmpty) { + buf.writeln('# ${slide.title}'); + buf.writeln(); + } + buf.writeln('```${slide.codeLanguage.trim()}'); + buf.write(slide.customMarkdown); + if (slide.customMarkdown.isNotEmpty && + !slide.customMarkdown.endsWith('\n')) { + buf.writeln(); + } + buf.writeln('```'); } if (slide.audioPath.isNotEmpty) { @@ -614,6 +627,18 @@ class MarkdownService { ).trim(); final notes = notesBuffer.toString().trim(); + // Code slides carry a fenced block that the generic line parser below would + // mangle (the body lines aren't markdown). Handle them up front. + if (cssClass.split(RegExp(r'\s+')).contains('code')) { + return _parseCodeBlock( + remaining: remaining, + cssClass: cssClass, + notes: notes, + advanceDuration: advanceDuration, + skipped: skipped, + ); + } + final lines = remaining.split('\n'); String h1 = ''; String h2 = ''; @@ -798,4 +823,75 @@ class MarkdownService { tableRows: type == SlideType.table ? tableRows : const [], ); } + + /// Parse a `` slide: an optional `# title`, the fenced + /// code block (its info string is the language) and an optional `