From ee66721de65022bdada99fe2ca1c4afc445bc4cc Mon Sep 17 00:00:00 2001 From: Brenno de Winter Date: Fri, 5 Jun 2026 19:14:54 +0200 Subject: [PATCH 1/9] Improve presentation settings and localization --- lib/l10n/app_localizations.dart | 1232 ++++++++++++++++- lib/state/deck_provider.dart | 2 + lib/widgets/app_shell.dart | 35 +- lib/widgets/dialogs/export_dialog.dart | 11 +- .../dialogs/presentation_info_dialog.dart | 15 +- lib/widgets/dialogs/settings_dialog.dart | 139 +- lib/widgets/panels/preview_panel.dart | 2 +- .../presentation/fullscreen_presenter.dart | 87 +- lib/widgets/slides/slide_preview.dart | 80 +- test/app_localizations_test.dart | 94 ++ test/deck_provider_test.dart | 7 + test/tlp_test.dart | 34 + 12 files changed, 1612 insertions(+), 126 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 147dc8c..a1f5a2d 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -69,17 +69,31 @@ class AppLocalizations { String d(String dutchText) { if (languageCode == 'nl') return dutchText; - return _dutchSourceStrings[languageCode]?[dutchText] ?? + return _dutchSourceStringAdditions[languageCode]?[dutchText] ?? + _dutchSourceStrings[languageCode]?[dutchText] ?? + _dutchSourceStringAdditions['en']?[dutchText] ?? _dutchSourceStrings['en']?[dutchText] ?? dutchText; } static String sourceFor(String languageCode, String dutchText) { if (languageCode == 'nl') return dutchText; - return _dutchSourceStrings[languageCode]?[dutchText] ?? + return _dutchSourceStringAdditions[languageCode]?[dutchText] ?? + _dutchSourceStrings[languageCode]?[dutchText] ?? + _dutchSourceStringAdditions['en']?[dutchText] ?? _dutchSourceStrings['en']?[dutchText] ?? dutchText; } + + static bool hasDirectDutchSourceTranslation( + String languageCode, + String dutchText, + ) { + if (languageCode == 'nl') return true; + return _dutchSourceStringAdditions[languageCode]?.containsKey(dutchText) == + true || + _dutchSourceStrings[languageCode]?.containsKey(dutchText) == true; + } } extension AppLocalizationsX on BuildContext { @@ -966,6 +980,7 @@ const _dutchSourceStrings = { 'Titelachtergrond': 'Title background', 'Titeltekst': 'Title text', 'Sectieachtergrond': 'Section background', + 'Geselecteerd': 'Selected', 'Logo': 'Logo', 'Geen logo ingesteld': 'No logo set', 'Verwijder logo': 'Remove logo', @@ -2015,3 +2030,1216 @@ const _dutchSourceStrings = { 'P públiko · H legenda · S pantalla · G resumen · B/W pretu/blanku · R tempu · Esc stop', }, }; + +const _dutchSourceStringAdditions = { + 'en': { + 'Afbeelding': 'Image', + 'Bullet': 'Bullet', + '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', + 'Logo px': 'Logo px', + 'Markdown voor laatste slide': 'Markdown for final slide', + 'PREVIEW': 'PREVIEW', + 'Slides gerenderd.': 'Slides rendered.', + 'Standaard laatste slide gebruiken': 'Use default final slide', + 'Wordt automatisch toegevoegd bij presenteren en exporteren.': + 'Automatically added when presenting and exporting.', + 'gerenderd.': 'rendered.', + 'renderen…': 'rendering…', + 'voorbereiden…': 'preparing…', + }, + 'it': { + '# Slide\n\nInhoud hier...': '# Slide\n\nContent here...', + '1 slide geïmporteerd.': '1 slide imported.', + '1 slide kopiëren naar…': 'Copy 1 slide to…', + '1 slide overgeslagen': '1 slide skipped', + 'Accent / bullets': 'Accent / bullets', + 'Achtergrond slides': 'Slide background', + 'Afbeelding': 'Immagine', + 'Afbeelding gekopieerd naar klembord.': 'Image copied to clipboard.', + 'Afbeelding plakken': 'Paste image', + 'Afbeelding plakken uit klembord': 'Paste image from clipboard', + 'Afbeeldingen → nieuwe slides · .md / .ocideck → openen': + 'Images → new slides · .md / .ocideck → open', + 'Afsluiten (Escape)': 'Exit (Escape)', + 'Alle slides zijn overgeslagen — niets om te exporteren.': + 'All slides are skipped, so there is nothing to export.', + 'Alle slides zijn overgeslagen — niets om te tonen.': + 'All slides are skipped, so there is nothing to show.', + 'Alles tonen': 'Show all', + 'Audio verwijderen': 'Remove audio', + 'Automatisch doorgaan na': 'Advance automatically after', + 'Bijv. Kwartaalupdate Q4': 'E.g. Q4 update', + 'Bullet': 'Punto elenco', + 'Caption / bronvermelding (bijv. © Naam Fotograaf)': + 'Caption / credit (e.g. © Photographer Name)', + 'Coverflow': 'Coverflow', + 'De afbeelding wordt schermvullend als achtergrond getoond met verminderde opaciteit zodat de tekst leesbaar blijft.': + 'The image is shown fullscreen as a background with reduced opacity so the text remains readable.', + 'De snelle bruine vos springt over de luie hond.': + 'The quick brown fox jumps over the lazy dog.', + 'Deze gegevens worden in de markdown opgeslagen en zijn doorzoekbaar bij het openen.': + 'These details are stored in the Markdown and searchable when opening.', + 'Deze presentatie heeft niet-opgeslagen wijzigingen. Sla de presentatie op voordat het tabblad sluit.': + 'This presentation has unsaved changes. Save it before closing the tab.', + 'Deze slide kan geen afbeelding ontvangen. Kies eerst een afbeeldingsslide.': + 'This slide cannot receive an image. Choose an image slide first.', + 'Eerste': 'First', + 'Einde van de presentatie': 'End of presentation', + 'Er is een presentatie met niet-opgeslagen wijzigingen gevonden van een vorige sessie:': + 'A presentation with unsaved changes was found from a previous session:', + 'Er zijn': 'There are', + 'Er zijn presentaties met niet-opgeslagen wijzigingen. Sla ze op voordat de app sluit.': + 'There are presentations with unsaved changes. Save them before closing the app.', + 'Export mislukt:': 'Export failed:', + 'Footer tonen op deze slide': 'Show footer on this slide', + 'Gebruik "Bladeren" om afbeeldingen van elke locatie te kiezen.': + 'Use “Browse” to choose images from any location.', + 'Geen afbeelding op het klembord gevonden.': + 'No image found on the clipboard.', + 'Geen ander deck open. Open eerst een ander tabblad.': + 'No other deck is open. Open another tab first.', + 'Geen andere presentaties (.md) in deze map gevonden.': + 'No other presentations (.md) found in this folder.', + 'Geen notities voor deze slide.': 'No notes for this slide.', + 'Geen presentaties (.md) in deze map gevonden.': + 'No presentations (.md) found in this folder.', + 'Geen presentaties gevonden voor': 'No presentations found for', + 'Geen resultaten': 'No results', + 'Geen resultaten voor': 'No results for', + 'Geen slides gevonden voor': 'No slides found for', + 'Geen slides met': 'No slides with', + 'Geselecteerd': 'Selected', + 'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.': + 'L’HTML si apre in qualsiasi browser senza internet e renderizza blocchi di codice, matematica e diagrammi Mermaid.', + 'Het bestand wordt permanent van schijf verwijderd. Deze actie kan niet ongedaan worden gemaakt.': + 'The file will be permanently deleted from disk. This action cannot be undone.', + 'Ingezoomd': 'Zoomed in', + 'Inzoomen (minder van de foto zichtbaar)': + 'Zoom in (less of the photo visible)', + 'Kies een afbeelding': 'Choose an image', + 'Kies een map met presentaties om te beginnen.': + 'Choose a folder with presentations to begin.', + 'Kon dit pakket niet importeren.': 'Could not import this package.', + 'Kon niet van scherm wisselen.': 'Could not switch screens.', + 'Kon van deze URL geen presentatie ophalen.': + 'Could not fetch a presentation from this URL.', + 'Kopieer afbeelding naar klembord': 'Copy image to clipboard', + 'Kopiëren mislukt.': 'Copy failed.', + 'Kopiëren naar ander deck': 'Copy to another deck', + 'Kopiëren naar klembord mislukt.': 'Copying to clipboard failed.', + 'Koprij verwijderen': 'Remove header row', + 'Laat los om toe te voegen': 'Release to add', + 'Laatste slide': 'Slide finale', + 'Let op: deze afbeelding wordt nog gebruikt in': + 'Warning: this image is still used in', + 'Logo kiezen': 'Choose logo', + 'Logo px': 'Logo px', + 'Logo tonen op deze slide': 'Show logo on this slide', + 'Map met presentaties kiezen': 'Choose presentation folder', + 'Map voor exports': 'Export folder', + 'Markdown kon niet worden verwerkt. Controleer de syntax.': + 'Markdown could not be processed. Check the syntax.', + 'Markdown modus — bewerk de volledige presentatie als Marp Markdown': + 'Markdown mode — edit the full presentation as Marp Markdown', + 'Markdown voor laatste slide': 'Markdown per la slide finale', + 'Naam van het stijlprofiel': 'Name of the style profile', + 'Niet-opgeslagen werk herstellen?': 'Restore unsaved work?', + 'Niet-opgeslagen wijzigingen': 'Unsaved changes', + 'Niets vervangen': 'Nothing replaced', + 'Nieuw profiel': 'New profile', + 'Open eerst een presentatie om afbeeldingen toe te voegen.': + 'Open a presentation before adding images.', + 'Overslaan bij presenteren/exporteren': 'Skip when presenting/exporting', + 'PREVIEW': 'ANTEPRIMA', + 'Paginanummers tonen (rechtsonder)': 'Show page numbers (bottom right)', + 'Pakket geëxporteerd naar:': 'Package exported to:', + 'Pas je zoekterm aan of voeg een beschrijving toe.': + 'Adjust your search term or add a description.', + 'Plak de link naar een .ocideck-pakket of een Marp-markdownbestand.': + 'Paste the link to an .ocideck package or a Marp Markdown file.', + 'Preview inklappen': 'Collapse preview', + 'Preview uitklappen': 'Expand preview', + 'Profiel verwijderen': 'Delete profile', + 'Rij verwijderen': 'Remove row', + 'SLIDES': 'SLIDES', + 'Sectieachtergrond': 'Section background', + 'Selecteer een\nafbeelding': 'Select an\nimage', + 'Selectie opheffen': 'Clear selection', + 'Sleep om de slide-preview breder of smaller te maken': + 'Drag to make the slide preview wider or narrower', + 'Slide': 'Slide', + 'Slide gekopieerd naar klembord.': 'Slide copied to clipboard.', + 'Slide plakken': 'Paste slide', + 'Slide renderen…': 'Rendering slide…', + 'Slide toevoegen': 'Add slide', + 'Slides gerenderd.': 'Slide renderizzate.', + 'Sluiten (G of Esc)': 'Close (G or Esc)', + 'Sprekersnotities...': 'Speaker notes...', + 'Standaard laatste slide gebruiken': 'Usa slide finale predefinita', + 'Standaard map voor presentaties': 'Default presentation folder', + 'Standaardprofiel laden': 'Load default profile', + 'TLP-classificatie (Traffic Light Protocol)': + 'TLP classification (Traffic Light Protocol)', + 'Tabel koptekst': 'Table header text', + 'Tabeltekst': 'Table text', + 'Terug naar standaardstijl': 'Back to default style', + 'Terugzetten (volledige afbeelding zichtbaar)': + 'Reset (full image visible)', + 'Tijd resetten (R)': 'Reset timer (R)', + 'Tip: druk op Enter binnen een cel voor een nieuwe regel.': + 'Tip: press Enter inside a cell for a new line.', + 'Titelachtergrond': 'Title background', + 'Titeltekst': 'Title text', + 'Toepassen': 'Apply', + 'Tokens: {page}, {total}, {date}, {title}. Footer verschijnt op alle slides behalve titel- en sectieslides, tenzij je hem per slide uitzet.': + 'Tokens: {page}, {total}, {date}, {title}. The footer appears on all slides except title and section slides, unless you disable it per slide.', + 'Typ zoektermen om slides uit al je presentaties te vinden.': + 'Type search terms to find slides across your presentations.', + 'Uitgezoomd': 'Zoomed out', + 'Uitzoomen (meer van de foto zichtbaar)': + 'Zoom out (more of the photo visible)', + 'Verwijder afbeelding': 'Remove image', + 'Verwijder logo': 'Remove logo', + 'Verwijderen maakt die slides leeg. Dit kan niet ongedaan worden gemaakt.': + 'Deleting will clear those slides. This cannot be undone.', + 'Volledig zichtbaar (100%)': 'Fully visible (100%)', + 'Vul een titel in': 'Enter a title', + 'Weer tonen': 'Show again', + 'Weer tonen bij presenteren/exporteren': + 'Show again when presenting/exporting', + 'Wordt automatisch toegevoegd bij presenteren en exporteren.': + 'Aggiunta automaticamente durante la presentazione e l’esportazione.', + 'Zoek in slides…': 'Search in slides…', + 'Zoek op bestandsnaam, titel of tekst in de slides…': + 'Search by file name, title or slide text…', + 'Zoek op naam of beschrijving…': 'Search by name or description…', + 'Zoek op presentatie, titel of tekst…': + 'Search by presentation, title or text…', + 'Zoek slides op tekst, titel, onderschrift, pad…': + 'Search slides by text, title, caption, path…', + 'bijv. Vertrouwelijk · {title} · {date}': + 'e.g. Confidential · {title} · {date}', + 'gerenderd.': 'renderizzata.', + 'geselecteerd': 'selected', + 'meer treffer(s)': 'more match(es)', + 'paginering aan': 'pagination on', + 'pijltjes + Enter of klik om te springen': + 'arrows + Enter or click to jump', + 'presentaties met niet-opgeslagen wijzigingen gevonden van een vorige sessie:': + 'presentations with unsaved changes from a previous session:', + 'renderen…': 'renderizzazione…', + 'resultaat': 'result', + 'resultaten': 'results', + 'slide': 'slide', + 'slide(s) gekopieerd naar': 'slide(s) copied to', + 'slides geïmporteerd.': 'slides imported.', + 'slides kopiëren naar…': 'slides to copy to…', + 'slides overgeslagen': 'slides skipped', + 'toegevoegd': 'added', + 'treffer(s)': 'match(es)', + 'treffers — verfijn je zoekopdracht': 'matches, refine your search', + 'van de foto zichtbaar': 'of the photo visible', + 'vervangen': 'replaced', + 'verwijderen': 'remove', + 'volledig deck': 'full deck', + 'voorbereiden…': 'preparazione…', + '↑↓←→ navigeren · Enter kiezen · Dubbelklik selecteert': + '↑↓←→ navigate · Enter chooses · Double-click selects', + }, + 'de': { + '# Slide\n\nInhoud hier...': '# Slide\n\nContent here...', + '1 slide geïmporteerd.': '1 slide imported.', + '1 slide kopiëren naar…': 'Copy 1 slide to…', + '1 slide overgeslagen': '1 slide skipped', + 'Accent / bullets': 'Accent / bullets', + 'Achtergrond slides': 'Slide background', + 'Afbeelding': 'Bild', + 'Afbeelding gekopieerd naar klembord.': 'Image copied to clipboard.', + 'Afbeelding plakken': 'Paste image', + 'Afbeelding plakken uit klembord': 'Paste image from clipboard', + 'Afbeeldingen → nieuwe slides · .md / .ocideck → openen': + 'Images → new slides · .md / .ocideck → open', + 'Afsluiten (Escape)': 'Exit (Escape)', + 'Alle slides zijn overgeslagen — niets om te exporteren.': + 'All slides are skipped, so there is nothing to export.', + 'Alle slides zijn overgeslagen — niets om te tonen.': + 'All slides are skipped, so there is nothing to show.', + 'Alles tonen': 'Show all', + 'Audio verwijderen': 'Remove audio', + 'Automatisch doorgaan na': 'Advance automatically after', + 'Bijv. Kwartaalupdate Q4': 'E.g. Q4 update', + 'Bullet': 'Stichpunkt', + 'Caption / bronvermelding (bijv. © Naam Fotograaf)': + 'Caption / credit (e.g. © Photographer Name)', + 'Coverflow': 'Coverflow', + 'De afbeelding wordt schermvullend als achtergrond getoond met verminderde opaciteit zodat de tekst leesbaar blijft.': + 'The image is shown fullscreen as a background with reduced opacity so the text remains readable.', + 'De snelle bruine vos springt over de luie hond.': + 'The quick brown fox jumps over the lazy dog.', + 'Deze gegevens worden in de markdown opgeslagen en zijn doorzoekbaar bij het openen.': + 'These details are stored in the Markdown and searchable when opening.', + 'Deze presentatie heeft niet-opgeslagen wijzigingen. Sla de presentatie op voordat het tabblad sluit.': + 'This presentation has unsaved changes. Save it before closing the tab.', + 'Deze slide kan geen afbeelding ontvangen. Kies eerst een afbeeldingsslide.': + 'This slide cannot receive an image. Choose an image slide first.', + 'Eerste': 'First', + 'Einde van de presentatie': 'End of presentation', + 'Er is een presentatie met niet-opgeslagen wijzigingen gevonden van een vorige sessie:': + 'A presentation with unsaved changes was found from a previous session:', + 'Er zijn': 'There are', + 'Er zijn presentaties met niet-opgeslagen wijzigingen. Sla ze op voordat de app sluit.': + 'There are presentations with unsaved changes. Save them before closing the app.', + 'Export mislukt:': 'Export failed:', + 'Footer tonen op deze slide': 'Show footer on this slide', + 'Gebruik "Bladeren" om afbeeldingen van elke locatie te kiezen.': + 'Use “Browse” to choose images from any location.', + 'Geen afbeelding op het klembord gevonden.': + 'No image found on the clipboard.', + 'Geen ander deck open. Open eerst een ander tabblad.': + 'No other deck is open. Open another tab first.', + 'Geen andere presentaties (.md) in deze map gevonden.': + 'No other presentations (.md) found in this folder.', + 'Geen notities voor deze slide.': 'No notes for this slide.', + 'Geen presentaties (.md) in deze map gevonden.': + 'No presentations (.md) found in this folder.', + 'Geen presentaties gevonden voor': 'No presentations found for', + 'Geen resultaten': 'No results', + 'Geen resultaten voor': 'No results for', + 'Geen slides gevonden voor': 'No slides found for', + 'Geen slides met': 'No slides with', + 'Geselecteerd': 'Selected', + 'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.': + 'HTML öffnet sich in jedem Browser ohne Internet und rendert Codeblöcke, Mathematik und Mermaid-Diagramme.', + 'Het bestand wordt permanent van schijf verwijderd. Deze actie kan niet ongedaan worden gemaakt.': + 'The file will be permanently deleted from disk. This action cannot be undone.', + 'Ingezoomd': 'Zoomed in', + 'Inzoomen (minder van de foto zichtbaar)': + 'Zoom in (less of the photo visible)', + 'Kies een afbeelding': 'Choose an image', + 'Kies een map met presentaties om te beginnen.': + 'Choose a folder with presentations to begin.', + 'Kon dit pakket niet importeren.': 'Could not import this package.', + 'Kon niet van scherm wisselen.': 'Could not switch screens.', + 'Kon van deze URL geen presentatie ophalen.': + 'Could not fetch a presentation from this URL.', + 'Kopieer afbeelding naar klembord': 'Copy image to clipboard', + 'Kopiëren mislukt.': 'Copy failed.', + 'Kopiëren naar ander deck': 'Copy to another deck', + 'Kopiëren naar klembord mislukt.': 'Copying to clipboard failed.', + 'Koprij verwijderen': 'Remove header row', + 'Laat los om toe te voegen': 'Release to add', + 'Laatste slide': 'Letzte Folie', + 'Let op: deze afbeelding wordt nog gebruikt in': + 'Warning: this image is still used in', + 'Logo kiezen': 'Choose logo', + 'Logo px': 'Logo px', + 'Logo tonen op deze slide': 'Show logo on this slide', + 'Map met presentaties kiezen': 'Choose presentation folder', + 'Map voor exports': 'Export folder', + 'Markdown kon niet worden verwerkt. Controleer de syntax.': + 'Markdown could not be processed. Check the syntax.', + 'Markdown modus — bewerk de volledige presentatie als Marp Markdown': + 'Markdown mode — edit the full presentation as Marp Markdown', + 'Markdown voor laatste slide': 'Markdown für die letzte Folie', + 'Naam van het stijlprofiel': 'Name of the style profile', + 'Niet-opgeslagen werk herstellen?': 'Restore unsaved work?', + 'Niet-opgeslagen wijzigingen': 'Unsaved changes', + 'Niets vervangen': 'Nothing replaced', + 'Nieuw profiel': 'New profile', + 'Open eerst een presentatie om afbeeldingen toe te voegen.': + 'Open a presentation before adding images.', + 'Overslaan bij presenteren/exporteren': 'Skip when presenting/exporting', + 'PREVIEW': 'VORSCHAU', + 'Paginanummers tonen (rechtsonder)': 'Show page numbers (bottom right)', + 'Pakket geëxporteerd naar:': 'Package exported to:', + 'Pas je zoekterm aan of voeg een beschrijving toe.': + 'Adjust your search term or add a description.', + 'Plak de link naar een .ocideck-pakket of een Marp-markdownbestand.': + 'Paste the link to an .ocideck package or a Marp Markdown file.', + 'Preview inklappen': 'Collapse preview', + 'Preview uitklappen': 'Expand preview', + 'Profiel verwijderen': 'Delete profile', + 'Rij verwijderen': 'Remove row', + 'SLIDES': 'SLIDES', + 'Sectieachtergrond': 'Section background', + 'Selecteer een\nafbeelding': 'Select an\nimage', + 'Selectie opheffen': 'Clear selection', + 'Sleep om de slide-preview breder of smaller te maken': + 'Drag to make the slide preview wider or narrower', + 'Slide': 'Slide', + 'Slide gekopieerd naar klembord.': 'Slide copied to clipboard.', + 'Slide plakken': 'Paste slide', + 'Slide renderen…': 'Rendering slide…', + 'Slide toevoegen': 'Add slide', + 'Slides gerenderd.': 'Folien gerendert.', + 'Sluiten (G of Esc)': 'Close (G or Esc)', + 'Sprekersnotities...': 'Speaker notes...', + 'Standaard laatste slide gebruiken': + 'Standardmäßige letzte Folie verwenden', + 'Standaard map voor presentaties': 'Default presentation folder', + 'Standaardprofiel laden': 'Load default profile', + 'TLP-classificatie (Traffic Light Protocol)': + 'TLP classification (Traffic Light Protocol)', + 'Tabel koptekst': 'Table header text', + 'Tabeltekst': 'Table text', + 'Terug naar standaardstijl': 'Back to default style', + 'Terugzetten (volledige afbeelding zichtbaar)': + 'Reset (full image visible)', + 'Tijd resetten (R)': 'Reset timer (R)', + 'Tip: druk op Enter binnen een cel voor een nieuwe regel.': + 'Tip: press Enter inside a cell for a new line.', + 'Titelachtergrond': 'Title background', + 'Titeltekst': 'Title text', + 'Toepassen': 'Apply', + 'Tokens: {page}, {total}, {date}, {title}. Footer verschijnt op alle slides behalve titel- en sectieslides, tenzij je hem per slide uitzet.': + 'Tokens: {page}, {total}, {date}, {title}. The footer appears on all slides except title and section slides, unless you disable it per slide.', + 'Typ zoektermen om slides uit al je presentaties te vinden.': + 'Type search terms to find slides across your presentations.', + 'Uitgezoomd': 'Zoomed out', + 'Uitzoomen (meer van de foto zichtbaar)': + 'Zoom out (more of the photo visible)', + 'Verwijder afbeelding': 'Remove image', + 'Verwijder logo': 'Remove logo', + 'Verwijderen maakt die slides leeg. Dit kan niet ongedaan worden gemaakt.': + 'Deleting will clear those slides. This cannot be undone.', + 'Volledig zichtbaar (100%)': 'Fully visible (100%)', + 'Vul een titel in': 'Enter a title', + 'Weer tonen': 'Show again', + 'Weer tonen bij presenteren/exporteren': + 'Show again when presenting/exporting', + 'Wordt automatisch toegevoegd bij presenteren en exporteren.': + 'Wird beim Präsentieren und Exportieren automatisch hinzugefügt.', + 'Zoek in slides…': 'Search in slides…', + 'Zoek op bestandsnaam, titel of tekst in de slides…': + 'Search by file name, title or slide text…', + 'Zoek op naam of beschrijving…': 'Search by name or description…', + 'Zoek op presentatie, titel of tekst…': + 'Search by presentation, title or text…', + 'Zoek slides op tekst, titel, onderschrift, pad…': + 'Search slides by text, title, caption, path…', + 'bijv. Vertrouwelijk · {title} · {date}': + 'e.g. Confidential · {title} · {date}', + 'gerenderd.': 'gerendert.', + 'geselecteerd': 'selected', + 'meer treffer(s)': 'more match(es)', + 'paginering aan': 'pagination on', + 'pijltjes + Enter of klik om te springen': + 'arrows + Enter or click to jump', + 'presentaties met niet-opgeslagen wijzigingen gevonden van een vorige sessie:': + 'presentations with unsaved changes from a previous session:', + 'renderen…': 'rendern…', + 'resultaat': 'result', + 'resultaten': 'results', + 'slide': 'slide', + 'slide(s) gekopieerd naar': 'slide(s) copied to', + 'slides geïmporteerd.': 'slides imported.', + 'slides kopiëren naar…': 'slides to copy to…', + 'slides overgeslagen': 'slides skipped', + 'toegevoegd': 'added', + 'treffer(s)': 'match(es)', + 'treffers — verfijn je zoekopdracht': 'matches, refine your search', + 'van de foto zichtbaar': 'of the photo visible', + 'vervangen': 'replaced', + 'verwijderen': 'remove', + 'volledig deck': 'full deck', + 'voorbereiden…': 'vorbereiten…', + '↑↓←→ navigeren · Enter kiezen · Dubbelklik selecteert': + '↑↓←→ navigate · Enter chooses · Double-click selects', + }, + 'fr': { + '# Slide\n\nInhoud hier...': '# Slide\n\nContent here...', + '1 slide geïmporteerd.': '1 slide imported.', + '1 slide kopiëren naar…': 'Copy 1 slide to…', + '1 slide overgeslagen': '1 slide skipped', + 'Accent / bullets': 'Accent / bullets', + 'Achtergrond slides': 'Slide background', + 'Afbeelding': 'Image', + 'Afbeelding gekopieerd naar klembord.': 'Image copied to clipboard.', + 'Afbeelding plakken': 'Paste image', + 'Afbeelding plakken uit klembord': 'Paste image from clipboard', + 'Afbeeldingen → nieuwe slides · .md / .ocideck → openen': + 'Images → new slides · .md / .ocideck → open', + 'Afsluiten (Escape)': 'Exit (Escape)', + 'Alle slides zijn overgeslagen — niets om te exporteren.': + 'All slides are skipped, so there is nothing to export.', + 'Alle slides zijn overgeslagen — niets om te tonen.': + 'All slides are skipped, so there is nothing to show.', + 'Alles tonen': 'Show all', + 'Audio verwijderen': 'Remove audio', + 'Automatisch doorgaan na': 'Advance automatically after', + 'Bijv. Kwartaalupdate Q4': 'E.g. Q4 update', + 'Bullet': 'Puce', + 'Caption / bronvermelding (bijv. © Naam Fotograaf)': + 'Caption / credit (e.g. © Photographer Name)', + 'Coverflow': 'Coverflow', + 'De afbeelding wordt schermvullend als achtergrond getoond met verminderde opaciteit zodat de tekst leesbaar blijft.': + 'The image is shown fullscreen as a background with reduced opacity so the text remains readable.', + 'De snelle bruine vos springt over de luie hond.': + 'The quick brown fox jumps over the lazy dog.', + 'Deze gegevens worden in de markdown opgeslagen en zijn doorzoekbaar bij het openen.': + 'These details are stored in the Markdown and searchable when opening.', + 'Deze presentatie heeft niet-opgeslagen wijzigingen. Sla de presentatie op voordat het tabblad sluit.': + 'This presentation has unsaved changes. Save it before closing the tab.', + 'Deze slide kan geen afbeelding ontvangen. Kies eerst een afbeeldingsslide.': + 'This slide cannot receive an image. Choose an image slide first.', + 'Eerste': 'First', + 'Einde van de presentatie': 'End of presentation', + 'Er is een presentatie met niet-opgeslagen wijzigingen gevonden van een vorige sessie:': + 'A presentation with unsaved changes was found from a previous session:', + 'Er zijn': 'There are', + 'Er zijn presentaties met niet-opgeslagen wijzigingen. Sla ze op voordat de app sluit.': + 'There are presentations with unsaved changes. Save them before closing the app.', + 'Export mislukt:': 'Export failed:', + 'Footer tonen op deze slide': 'Show footer on this slide', + 'Gebruik "Bladeren" om afbeeldingen van elke locatie te kiezen.': + 'Use “Browse” to choose images from any location.', + 'Geen afbeelding op het klembord gevonden.': + 'No image found on the clipboard.', + 'Geen ander deck open. Open eerst een ander tabblad.': + 'No other deck is open. Open another tab first.', + 'Geen andere presentaties (.md) in deze map gevonden.': + 'No other presentations (.md) found in this folder.', + 'Geen notities voor deze slide.': 'No notes for this slide.', + 'Geen presentaties (.md) in deze map gevonden.': + 'No presentations (.md) found in this folder.', + 'Geen presentaties gevonden voor': 'No presentations found for', + 'Geen resultaten': 'No results', + 'Geen resultaten voor': 'No results for', + 'Geen slides gevonden voor': 'No slides found for', + 'Geen slides met': 'No slides with', + 'Geselecteerd': 'Selected', + 'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.': + 'Le HTML s’ouvre dans n’importe quel navigateur sans internet et rend les blocs de code, les mathématiques et les diagrammes Mermaid.', + 'Het bestand wordt permanent van schijf verwijderd. Deze actie kan niet ongedaan worden gemaakt.': + 'The file will be permanently deleted from disk. This action cannot be undone.', + 'Ingezoomd': 'Zoomed in', + 'Inzoomen (minder van de foto zichtbaar)': + 'Zoom in (less of the photo visible)', + 'Kies een afbeelding': 'Choose an image', + 'Kies een map met presentaties om te beginnen.': + 'Choose a folder with presentations to begin.', + 'Kon dit pakket niet importeren.': 'Could not import this package.', + 'Kon niet van scherm wisselen.': 'Could not switch screens.', + 'Kon van deze URL geen presentatie ophalen.': + 'Could not fetch a presentation from this URL.', + 'Kopieer afbeelding naar klembord': 'Copy image to clipboard', + 'Kopiëren mislukt.': 'Copy failed.', + 'Kopiëren naar ander deck': 'Copy to another deck', + 'Kopiëren naar klembord mislukt.': 'Copying to clipboard failed.', + 'Koprij verwijderen': 'Remove header row', + 'Laat los om toe te voegen': 'Release to add', + 'Laatste slide': 'Diapositive finale', + 'Let op: deze afbeelding wordt nog gebruikt in': + 'Warning: this image is still used in', + 'Logo kiezen': 'Choose logo', + 'Logo px': 'Logo px', + 'Logo tonen op deze slide': 'Show logo on this slide', + 'Map met presentaties kiezen': 'Choose presentation folder', + 'Map voor exports': 'Export folder', + 'Markdown kon niet worden verwerkt. Controleer de syntax.': + 'Markdown could not be processed. Check the syntax.', + 'Markdown modus — bewerk de volledige presentatie als Marp Markdown': + 'Markdown mode — edit the full presentation as Marp Markdown', + 'Markdown voor laatste slide': 'Markdown pour la diapositive finale', + 'Naam van het stijlprofiel': 'Name of the style profile', + 'Niet-opgeslagen werk herstellen?': 'Restore unsaved work?', + 'Niet-opgeslagen wijzigingen': 'Unsaved changes', + 'Niets vervangen': 'Nothing replaced', + 'Nieuw profiel': 'New profile', + 'Open eerst een presentatie om afbeeldingen toe te voegen.': + 'Open a presentation before adding images.', + 'Overslaan bij presenteren/exporteren': 'Skip when presenting/exporting', + 'PREVIEW': 'APERÇU', + 'Paginanummers tonen (rechtsonder)': 'Show page numbers (bottom right)', + 'Pakket geëxporteerd naar:': 'Package exported to:', + 'Pas je zoekterm aan of voeg een beschrijving toe.': + 'Adjust your search term or add a description.', + 'Plak de link naar een .ocideck-pakket of een Marp-markdownbestand.': + 'Paste the link to an .ocideck package or a Marp Markdown file.', + 'Preview inklappen': 'Collapse preview', + 'Preview uitklappen': 'Expand preview', + 'Profiel verwijderen': 'Delete profile', + 'Rij verwijderen': 'Remove row', + 'SLIDES': 'SLIDES', + 'Sectieachtergrond': 'Section background', + 'Selecteer een\nafbeelding': 'Select an\nimage', + 'Selectie opheffen': 'Clear selection', + 'Sleep om de slide-preview breder of smaller te maken': + 'Drag to make the slide preview wider or narrower', + 'Slide': 'Slide', + 'Slide gekopieerd naar klembord.': 'Slide copied to clipboard.', + 'Slide plakken': 'Paste slide', + 'Slide renderen…': 'Rendering slide…', + 'Slide toevoegen': 'Add slide', + 'Slides gerenderd.': 'Diapositives rendues.', + 'Sluiten (G of Esc)': 'Close (G or Esc)', + 'Sprekersnotities...': 'Speaker notes...', + 'Standaard laatste slide gebruiken': + 'Utiliser la diapositive finale par défaut', + 'Standaard map voor presentaties': 'Default presentation folder', + 'Standaardprofiel laden': 'Load default profile', + 'TLP-classificatie (Traffic Light Protocol)': + 'TLP classification (Traffic Light Protocol)', + 'Tabel koptekst': 'Table header text', + 'Tabeltekst': 'Table text', + 'Terug naar standaardstijl': 'Back to default style', + 'Terugzetten (volledige afbeelding zichtbaar)': + 'Reset (full image visible)', + 'Tijd resetten (R)': 'Reset timer (R)', + 'Tip: druk op Enter binnen een cel voor een nieuwe regel.': + 'Tip: press Enter inside a cell for a new line.', + 'Titelachtergrond': 'Title background', + 'Titeltekst': 'Title text', + 'Toepassen': 'Apply', + 'Tokens: {page}, {total}, {date}, {title}. Footer verschijnt op alle slides behalve titel- en sectieslides, tenzij je hem per slide uitzet.': + 'Tokens: {page}, {total}, {date}, {title}. The footer appears on all slides except title and section slides, unless you disable it per slide.', + 'Typ zoektermen om slides uit al je presentaties te vinden.': + 'Type search terms to find slides across your presentations.', + 'Uitgezoomd': 'Zoomed out', + 'Uitzoomen (meer van de foto zichtbaar)': + 'Zoom out (more of the photo visible)', + 'Verwijder afbeelding': 'Remove image', + 'Verwijder logo': 'Remove logo', + 'Verwijderen maakt die slides leeg. Dit kan niet ongedaan worden gemaakt.': + 'Deleting will clear those slides. This cannot be undone.', + 'Volledig zichtbaar (100%)': 'Fully visible (100%)', + 'Vul een titel in': 'Enter a title', + 'Weer tonen': 'Show again', + 'Weer tonen bij presenteren/exporteren': + 'Show again when presenting/exporting', + 'Wordt automatisch toegevoegd bij presenteren en exporteren.': + 'Ajoutée automatiquement lors de la présentation et de l’exportation.', + 'Zoek in slides…': 'Search in slides…', + 'Zoek op bestandsnaam, titel of tekst in de slides…': + 'Search by file name, title or slide text…', + 'Zoek op naam of beschrijving…': 'Search by name or description…', + 'Zoek op presentatie, titel of tekst…': + 'Search by presentation, title or text…', + 'Zoek slides op tekst, titel, onderschrift, pad…': + 'Search slides by text, title, caption, path…', + 'bijv. Vertrouwelijk · {title} · {date}': + 'e.g. Confidential · {title} · {date}', + 'gerenderd.': 'rendue.', + 'geselecteerd': 'selected', + 'meer treffer(s)': 'more match(es)', + 'paginering aan': 'pagination on', + 'pijltjes + Enter of klik om te springen': + 'arrows + Enter or click to jump', + 'presentaties met niet-opgeslagen wijzigingen gevonden van een vorige sessie:': + 'presentations with unsaved changes from a previous session:', + 'renderen…': 'rendu…', + 'resultaat': 'result', + 'resultaten': 'results', + 'slide': 'slide', + 'slide(s) gekopieerd naar': 'slide(s) copied to', + 'slides geïmporteerd.': 'slides imported.', + 'slides kopiëren naar…': 'slides to copy to…', + 'slides overgeslagen': 'slides skipped', + 'toegevoegd': 'added', + 'treffer(s)': 'match(es)', + 'treffers — verfijn je zoekopdracht': 'matches, refine your search', + 'van de foto zichtbaar': 'of the photo visible', + 'vervangen': 'replaced', + 'verwijderen': 'remove', + 'volledig deck': 'full deck', + 'voorbereiden…': 'préparation…', + '↑↓←→ navigeren · Enter kiezen · Dubbelklik selecteert': + '↑↓←→ navigate · Enter chooses · Double-click selects', + }, + 'es': { + '# Slide\n\nInhoud hier...': '# Slide\n\nContent here...', + '1 slide geïmporteerd.': '1 slide imported.', + '1 slide kopiëren naar…': 'Copy 1 slide to…', + '1 slide overgeslagen': '1 slide skipped', + 'Accent / bullets': 'Accent / bullets', + 'Achtergrond slides': 'Slide background', + 'Afbeelding': 'Imagen', + 'Afbeelding gekopieerd naar klembord.': 'Image copied to clipboard.', + 'Afbeelding plakken': 'Paste image', + 'Afbeelding plakken uit klembord': 'Paste image from clipboard', + 'Afbeeldingen → nieuwe slides · .md / .ocideck → openen': + 'Images → new slides · .md / .ocideck → open', + 'Afsluiten (Escape)': 'Exit (Escape)', + 'Alle slides zijn overgeslagen — niets om te exporteren.': + 'All slides are skipped, so there is nothing to export.', + 'Alle slides zijn overgeslagen — niets om te tonen.': + 'All slides are skipped, so there is nothing to show.', + 'Alles tonen': 'Show all', + 'Audio verwijderen': 'Remove audio', + 'Automatisch doorgaan na': 'Advance automatically after', + 'Bijv. Kwartaalupdate Q4': 'E.g. Q4 update', + 'Bullet': 'Viñeta', + 'Caption / bronvermelding (bijv. © Naam Fotograaf)': + 'Caption / credit (e.g. © Photographer Name)', + 'Coverflow': 'Coverflow', + 'De afbeelding wordt schermvullend als achtergrond getoond met verminderde opaciteit zodat de tekst leesbaar blijft.': + 'The image is shown fullscreen as a background with reduced opacity so the text remains readable.', + 'De snelle bruine vos springt over de luie hond.': + 'The quick brown fox jumps over the lazy dog.', + 'Deze gegevens worden in de markdown opgeslagen en zijn doorzoekbaar bij het openen.': + 'These details are stored in the Markdown and searchable when opening.', + 'Deze presentatie heeft niet-opgeslagen wijzigingen. Sla de presentatie op voordat het tabblad sluit.': + 'This presentation has unsaved changes. Save it before closing the tab.', + 'Deze slide kan geen afbeelding ontvangen. Kies eerst een afbeeldingsslide.': + 'This slide cannot receive an image. Choose an image slide first.', + 'Eerste': 'First', + 'Einde van de presentatie': 'End of presentation', + 'Er is een presentatie met niet-opgeslagen wijzigingen gevonden van een vorige sessie:': + 'A presentation with unsaved changes was found from a previous session:', + 'Er zijn': 'There are', + 'Er zijn presentaties met niet-opgeslagen wijzigingen. Sla ze op voordat de app sluit.': + 'There are presentations with unsaved changes. Save them before closing the app.', + 'Export mislukt:': 'Export failed:', + 'Footer tonen op deze slide': 'Show footer on this slide', + 'Gebruik "Bladeren" om afbeeldingen van elke locatie te kiezen.': + 'Use “Browse” to choose images from any location.', + 'Geen afbeelding op het klembord gevonden.': + 'No image found on the clipboard.', + 'Geen ander deck open. Open eerst een ander tabblad.': + 'No other deck is open. Open another tab first.', + 'Geen andere presentaties (.md) in deze map gevonden.': + 'No other presentations (.md) found in this folder.', + 'Geen notities voor deze slide.': 'No notes for this slide.', + 'Geen presentaties (.md) in deze map gevonden.': + 'No presentations (.md) found in this folder.', + 'Geen presentaties gevonden voor': 'No presentations found for', + 'Geen resultaten': 'No results', + 'Geen resultaten voor': 'No results for', + 'Geen slides gevonden voor': 'No slides found for', + 'Geen slides met': 'No slides with', + 'Geselecteerd': 'Selected', + 'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.': + 'El HTML se abre en cualquier navegador sin internet y renderiza bloques de código, matemáticas y diagramas Mermaid.', + 'Het bestand wordt permanent van schijf verwijderd. Deze actie kan niet ongedaan worden gemaakt.': + 'The file will be permanently deleted from disk. This action cannot be undone.', + 'Ingezoomd': 'Zoomed in', + 'Inzoomen (minder van de foto zichtbaar)': + 'Zoom in (less of the photo visible)', + 'Kies een afbeelding': 'Choose an image', + 'Kies een map met presentaties om te beginnen.': + 'Choose a folder with presentations to begin.', + 'Kon dit pakket niet importeren.': 'Could not import this package.', + 'Kon niet van scherm wisselen.': 'Could not switch screens.', + 'Kon van deze URL geen presentatie ophalen.': + 'Could not fetch a presentation from this URL.', + 'Kopieer afbeelding naar klembord': 'Copy image to clipboard', + 'Kopiëren mislukt.': 'Copy failed.', + 'Kopiëren naar ander deck': 'Copy to another deck', + 'Kopiëren naar klembord mislukt.': 'Copying to clipboard failed.', + 'Koprij verwijderen': 'Remove header row', + 'Laat los om toe te voegen': 'Release to add', + 'Laatste slide': 'Diapositiva final', + 'Let op: deze afbeelding wordt nog gebruikt in': + 'Warning: this image is still used in', + 'Logo kiezen': 'Choose logo', + 'Logo px': 'Logo px', + 'Logo tonen op deze slide': 'Show logo on this slide', + 'Map met presentaties kiezen': 'Choose presentation folder', + 'Map voor exports': 'Export folder', + 'Markdown kon niet worden verwerkt. Controleer de syntax.': + 'Markdown could not be processed. Check the syntax.', + 'Markdown modus — bewerk de volledige presentatie als Marp Markdown': + 'Markdown mode — edit the full presentation as Marp Markdown', + 'Markdown voor laatste slide': 'Markdown para la diapositiva final', + 'Naam van het stijlprofiel': 'Name of the style profile', + 'Niet-opgeslagen werk herstellen?': 'Restore unsaved work?', + 'Niet-opgeslagen wijzigingen': 'Unsaved changes', + 'Niets vervangen': 'Nothing replaced', + 'Nieuw profiel': 'New profile', + 'Open eerst een presentatie om afbeeldingen toe te voegen.': + 'Open a presentation before adding images.', + 'Overslaan bij presenteren/exporteren': 'Skip when presenting/exporting', + 'PREVIEW': 'VISTA PREVIA', + 'Paginanummers tonen (rechtsonder)': 'Show page numbers (bottom right)', + 'Pakket geëxporteerd naar:': 'Package exported to:', + 'Pas je zoekterm aan of voeg een beschrijving toe.': + 'Adjust your search term or add a description.', + 'Plak de link naar een .ocideck-pakket of een Marp-markdownbestand.': + 'Paste the link to an .ocideck package or a Marp Markdown file.', + 'Preview inklappen': 'Collapse preview', + 'Preview uitklappen': 'Expand preview', + 'Profiel verwijderen': 'Delete profile', + 'Rij verwijderen': 'Remove row', + 'SLIDES': 'SLIDES', + 'Sectieachtergrond': 'Section background', + 'Selecteer een\nafbeelding': 'Select an\nimage', + 'Selectie opheffen': 'Clear selection', + 'Sleep om de slide-preview breder of smaller te maken': + 'Drag to make the slide preview wider or narrower', + 'Slide': 'Slide', + 'Slide gekopieerd naar klembord.': 'Slide copied to clipboard.', + 'Slide plakken': 'Paste slide', + 'Slide renderen…': 'Rendering slide…', + 'Slide toevoegen': 'Add slide', + 'Slides gerenderd.': 'Diapositivas renderizadas.', + 'Sluiten (G of Esc)': 'Close (G or Esc)', + 'Sprekersnotities...': 'Speaker notes...', + 'Standaard laatste slide gebruiken': + 'Usar diapositiva final predeterminada', + 'Standaard map voor presentaties': 'Default presentation folder', + 'Standaardprofiel laden': 'Load default profile', + 'TLP-classificatie (Traffic Light Protocol)': + 'TLP classification (Traffic Light Protocol)', + 'Tabel koptekst': 'Table header text', + 'Tabeltekst': 'Table text', + 'Terug naar standaardstijl': 'Back to default style', + 'Terugzetten (volledige afbeelding zichtbaar)': + 'Reset (full image visible)', + 'Tijd resetten (R)': 'Reset timer (R)', + 'Tip: druk op Enter binnen een cel voor een nieuwe regel.': + 'Tip: press Enter inside a cell for a new line.', + 'Titelachtergrond': 'Title background', + 'Titeltekst': 'Title text', + 'Toepassen': 'Apply', + 'Tokens: {page}, {total}, {date}, {title}. Footer verschijnt op alle slides behalve titel- en sectieslides, tenzij je hem per slide uitzet.': + 'Tokens: {page}, {total}, {date}, {title}. The footer appears on all slides except title and section slides, unless you disable it per slide.', + 'Typ zoektermen om slides uit al je presentaties te vinden.': + 'Type search terms to find slides across your presentations.', + 'Uitgezoomd': 'Zoomed out', + 'Uitzoomen (meer van de foto zichtbaar)': + 'Zoom out (more of the photo visible)', + 'Verwijder afbeelding': 'Remove image', + 'Verwijder logo': 'Remove logo', + 'Verwijderen maakt die slides leeg. Dit kan niet ongedaan worden gemaakt.': + 'Deleting will clear those slides. This cannot be undone.', + 'Volledig zichtbaar (100%)': 'Fully visible (100%)', + 'Vul een titel in': 'Enter a title', + 'Weer tonen': 'Show again', + 'Weer tonen bij presenteren/exporteren': + 'Show again when presenting/exporting', + 'Wordt automatisch toegevoegd bij presenteren en exporteren.': + 'Se añade automáticamente al presentar y exportar.', + 'Zoek in slides…': 'Search in slides…', + 'Zoek op bestandsnaam, titel of tekst in de slides…': + 'Search by file name, title or slide text…', + 'Zoek op naam of beschrijving…': 'Search by name or description…', + 'Zoek op presentatie, titel of tekst…': + 'Search by presentation, title or text…', + 'Zoek slides op tekst, titel, onderschrift, pad…': + 'Search slides by text, title, caption, path…', + 'bijv. Vertrouwelijk · {title} · {date}': + 'e.g. Confidential · {title} · {date}', + 'gerenderd.': 'renderizada.', + 'geselecteerd': 'selected', + 'meer treffer(s)': 'more match(es)', + 'paginering aan': 'pagination on', + 'pijltjes + Enter of klik om te springen': + 'arrows + Enter or click to jump', + 'presentaties met niet-opgeslagen wijzigingen gevonden van een vorige sessie:': + 'presentations with unsaved changes from a previous session:', + 'renderen…': 'renderizando…', + 'resultaat': 'result', + 'resultaten': 'results', + 'slide': 'slide', + 'slide(s) gekopieerd naar': 'slide(s) copied to', + 'slides geïmporteerd.': 'slides imported.', + 'slides kopiëren naar…': 'slides to copy to…', + 'slides overgeslagen': 'slides skipped', + 'toegevoegd': 'added', + 'treffer(s)': 'match(es)', + 'treffers — verfijn je zoekopdracht': 'matches, refine your search', + 'van de foto zichtbaar': 'of the photo visible', + 'vervangen': 'replaced', + 'verwijderen': 'remove', + 'volledig deck': 'full deck', + 'voorbereiden…': 'preparando…', + '↑↓←→ navigeren · Enter kiezen · Dubbelklik selecteert': + '↑↓←→ navigate · Enter chooses · Double-click selects', + }, + 'fy': { + '# Slide\n\nInhoud hier...': '# Slide\n\nContent here...', + '1 slide geïmporteerd.': '1 slide imported.', + '1 slide kopiëren naar…': 'Copy 1 slide to…', + '1 slide overgeslagen': '1 slide skipped', + 'Accent / bullets': 'Accent / bullets', + 'Achtergrond slides': 'Slide background', + 'Afbeelding': 'Ofbylding', + 'Afbeelding gekopieerd naar klembord.': 'Image copied to clipboard.', + 'Afbeelding plakken': 'Paste image', + 'Afbeelding plakken uit klembord': 'Paste image from clipboard', + 'Afbeeldingen → nieuwe slides · .md / .ocideck → openen': + 'Images → new slides · .md / .ocideck → open', + 'Afsluiten (Escape)': 'Exit (Escape)', + 'Alle slides zijn overgeslagen — niets om te exporteren.': + 'All slides are skipped, so there is nothing to export.', + 'Alle slides zijn overgeslagen — niets om te tonen.': + 'All slides are skipped, so there is nothing to show.', + 'Alles tonen': 'Show all', + 'Audio verwijderen': 'Remove audio', + 'Automatisch doorgaan na': 'Advance automatically after', + 'Bijv. Kwartaalupdate Q4': 'E.g. Q4 update', + 'Bullet': 'Puntsje', + 'Caption / bronvermelding (bijv. © Naam Fotograaf)': + 'Caption / credit (e.g. © Photographer Name)', + 'Coverflow': 'Coverflow', + 'De afbeelding wordt schermvullend als achtergrond getoond met verminderde opaciteit zodat de tekst leesbaar blijft.': + 'The image is shown fullscreen as a background with reduced opacity so the text remains readable.', + 'De snelle bruine vos springt over de luie hond.': + 'The quick brown fox jumps over the lazy dog.', + 'Deze gegevens worden in de markdown opgeslagen en zijn doorzoekbaar bij het openen.': + 'These details are stored in the Markdown and searchable when opening.', + 'Deze presentatie heeft niet-opgeslagen wijzigingen. Sla de presentatie op voordat het tabblad sluit.': + 'This presentation has unsaved changes. Save it before closing the tab.', + 'Deze slide kan geen afbeelding ontvangen. Kies eerst een afbeeldingsslide.': + 'This slide cannot receive an image. Choose an image slide first.', + 'Eerste': 'First', + 'Einde van de presentatie': 'End of presentation', + 'Er is een presentatie met niet-opgeslagen wijzigingen gevonden van een vorige sessie:': + 'A presentation with unsaved changes was found from a previous session:', + 'Er zijn': 'There are', + 'Er zijn presentaties met niet-opgeslagen wijzigingen. Sla ze op voordat de app sluit.': + 'There are presentations with unsaved changes. Save them before closing the app.', + 'Export mislukt:': 'Export failed:', + 'Footer tonen op deze slide': 'Show footer on this slide', + 'Gebruik "Bladeren" om afbeeldingen van elke locatie te kiezen.': + 'Use “Browse” to choose images from any location.', + 'Geen afbeelding op het klembord gevonden.': + 'No image found on the clipboard.', + 'Geen ander deck open. Open eerst een ander tabblad.': + 'No other deck is open. Open another tab first.', + 'Geen andere presentaties (.md) in deze map gevonden.': + 'No other presentations (.md) found in this folder.', + 'Geen presentaties (.md) in deze map gevonden.': + 'No presentations (.md) found in this folder.', + 'Geen presentaties gevonden voor': 'No presentations found for', + 'Geen resultaten': 'No results', + 'Geen resultaten voor': 'No results for', + 'Geen slides gevonden voor': 'No slides found for', + 'Geen slides met': 'No slides with', + 'Geselecteerd': 'Selected', + 'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.': + 'HTML iepenet yn elke browser sûnder ynternet en rendert koadeblokken, wiskunde en Mermaid-diagrammen.', + 'Het bestand wordt permanent van schijf verwijderd. Deze actie kan niet ongedaan worden gemaakt.': + 'The file will be permanently deleted from disk. This action cannot be undone.', + 'Ingezoomd': 'Zoomed in', + 'Inzoomen (minder van de foto zichtbaar)': + 'Zoom in (less of the photo visible)', + 'Kies een afbeelding': 'Choose an image', + 'Kies een map met presentaties om te beginnen.': + 'Choose a folder with presentations to begin.', + 'Kon dit pakket niet importeren.': 'Could not import this package.', + 'Kon van deze URL geen presentatie ophalen.': + 'Could not fetch a presentation from this URL.', + 'Kopieer afbeelding naar klembord': 'Copy image to clipboard', + 'Kopiëren mislukt.': 'Copy failed.', + 'Kopiëren naar ander deck': 'Copy to another deck', + 'Kopiëren naar klembord mislukt.': 'Copying to clipboard failed.', + 'Koprij verwijderen': 'Remove header row', + 'Laat los om toe te voegen': 'Release to add', + 'Laatste slide': 'Lêste slide', + 'Let op: deze afbeelding wordt nog gebruikt in': + 'Warning: this image is still used in', + 'Logo kiezen': 'Choose logo', + 'Logo px': 'Logo px', + 'Logo tonen op deze slide': 'Show logo on this slide', + 'Map met presentaties kiezen': 'Choose presentation folder', + 'Map voor exports': 'Export folder', + 'Markdown kon niet worden verwerkt. Controleer de syntax.': + 'Markdown could not be processed. Check the syntax.', + 'Markdown modus — bewerk de volledige presentatie als Marp Markdown': + 'Markdown mode — edit the full presentation as Marp Markdown', + 'Markdown voor laatste slide': 'Markdown foar de lêste slide', + 'Naam van het stijlprofiel': 'Name of the style profile', + 'Niet-opgeslagen werk herstellen?': 'Restore unsaved work?', + 'Niet-opgeslagen wijzigingen': 'Unsaved changes', + 'Niets vervangen': 'Nothing replaced', + 'Nieuw profiel': 'New profile', + 'Open eerst een presentatie om afbeeldingen toe te voegen.': + 'Open a presentation before adding images.', + 'Overslaan bij presenteren/exporteren': 'Skip when presenting/exporting', + 'PREVIEW': 'FOARBYLD', + 'Paginanummers tonen (rechtsonder)': 'Show page numbers (bottom right)', + 'Pakket geëxporteerd naar:': 'Package exported to:', + 'Pas je zoekterm aan of voeg een beschrijving toe.': + 'Adjust your search term or add a description.', + 'Plak de link naar een .ocideck-pakket of een Marp-markdownbestand.': + 'Paste the link to an .ocideck package or a Marp Markdown file.', + 'Preview inklappen': 'Collapse preview', + 'Preview uitklappen': 'Expand preview', + 'Profiel verwijderen': 'Delete profile', + 'Rij verwijderen': 'Remove row', + 'SLIDES': 'SLIDES', + 'Sectieachtergrond': 'Section background', + 'Selecteer een\nafbeelding': 'Select an\nimage', + 'Selectie opheffen': 'Clear selection', + 'Sleep om de slide-preview breder of smaller te maken': + 'Drag to make the slide preview wider or narrower', + 'Slide': 'Slide', + 'Slide gekopieerd naar klembord.': 'Slide copied to clipboard.', + 'Slide plakken': 'Paste slide', + 'Slide renderen…': 'Rendering slide…', + 'Slide toevoegen': 'Add slide', + 'Slides gerenderd.': 'Slides rendere.', + 'Sluiten (G of Esc)': 'Close (G or Esc)', + 'Sprekersnotities...': 'Speaker notes...', + 'Standaard laatste slide gebruiken': 'Standert lêste slide brûke', + 'Standaard map voor presentaties': 'Default presentation folder', + 'Standaardprofiel laden': 'Load default profile', + 'TLP-classificatie (Traffic Light Protocol)': + 'TLP classification (Traffic Light Protocol)', + 'Tabel koptekst': 'Table header text', + 'Tabeltekst': 'Table text', + 'Terug naar standaardstijl': 'Back to default style', + 'Terugzetten (volledige afbeelding zichtbaar)': + 'Reset (full image visible)', + 'Tijd resetten (R)': 'Reset timer (R)', + 'Tip: druk op Enter binnen een cel voor een nieuwe regel.': + 'Tip: press Enter inside a cell for a new line.', + 'Titelachtergrond': 'Title background', + 'Titeltekst': 'Title text', + 'Toepassen': 'Apply', + 'Tokens: {page}, {total}, {date}, {title}. Footer verschijnt op alle slides behalve titel- en sectieslides, tenzij je hem per slide uitzet.': + 'Tokens: {page}, {total}, {date}, {title}. The footer appears on all slides except title and section slides, unless you disable it per slide.', + 'Typ zoektermen om slides uit al je presentaties te vinden.': + 'Type search terms to find slides across your presentations.', + 'Uitgezoomd': 'Zoomed out', + 'Uitzoomen (meer van de foto zichtbaar)': + 'Zoom out (more of the photo visible)', + 'Verwijder afbeelding': 'Remove image', + 'Verwijder logo': 'Remove logo', + 'Verwijderen maakt die slides leeg. Dit kan niet ongedaan worden gemaakt.': + 'Deleting will clear those slides. This cannot be undone.', + 'Volledig zichtbaar (100%)': 'Fully visible (100%)', + 'Vul een titel in': 'Enter a title', + 'Weer tonen': 'Show again', + 'Weer tonen bij presenteren/exporteren': + 'Show again when presenting/exporting', + 'Wordt automatisch toegevoegd bij presenteren en exporteren.': + 'Wurdt automatysk tafoege by presintearjen en eksportearjen.', + 'Zoek in slides…': 'Search in slides…', + 'Zoek op bestandsnaam, titel of tekst in de slides…': + 'Search by file name, title or slide text…', + 'Zoek op naam of beschrijving…': 'Search by name or description…', + 'Zoek op presentatie, titel of tekst…': + 'Search by presentation, title or text…', + 'Zoek slides op tekst, titel, onderschrift, pad…': + 'Search slides by text, title, caption, path…', + 'bijv. Vertrouwelijk · {title} · {date}': + 'e.g. Confidential · {title} · {date}', + 'gerenderd.': 'rendere.', + 'geselecteerd': 'selected', + 'meer treffer(s)': 'more match(es)', + 'paginering aan': 'pagination on', + 'pijltjes + Enter of klik om te springen': + 'arrows + Enter or click to jump', + 'presentaties met niet-opgeslagen wijzigingen gevonden van een vorige sessie:': + 'presentations with unsaved changes from a previous session:', + 'renderen…': 'rendere…', + 'resultaat': 'result', + 'resultaten': 'results', + 'slide': 'slide', + 'slide(s) gekopieerd naar': 'slide(s) copied to', + 'slides geïmporteerd.': 'slides imported.', + 'slides kopiëren naar…': 'slides to copy to…', + 'slides overgeslagen': 'slides skipped', + 'toegevoegd': 'added', + 'treffer(s)': 'match(es)', + 'treffers — verfijn je zoekopdracht': 'matches, refine your search', + 'van de foto zichtbaar': 'of the photo visible', + 'vervangen': 'replaced', + 'verwijderen': 'remove', + 'volledig deck': 'full deck', + 'voorbereiden…': 'tariede…', + '↑↓←→ navigeren · Enter kiezen · Dubbelklik selecteert': + '↑↓←→ navigate · Enter chooses · Double-click selects', + }, + 'pap': { + '# Slide\n\nInhoud hier...': '# Slide\n\nContent here...', + '1 slide geïmporteerd.': '1 slide imported.', + '1 slide kopiëren naar…': 'Copy 1 slide to…', + '1 slide overgeslagen': '1 slide skipped', + 'Accent / bullets': 'Accent / bullets', + 'Achtergrond slides': 'Slide background', + 'Afbeelding': 'Imágen', + 'Afbeelding gekopieerd naar klembord.': 'Image copied to clipboard.', + 'Afbeelding plakken': 'Paste image', + 'Afbeelding plakken uit klembord': 'Paste image from clipboard', + 'Afbeeldingen → nieuwe slides · .md / .ocideck → openen': + 'Images → new slides · .md / .ocideck → open', + 'Afsluiten (Escape)': 'Exit (Escape)', + 'Alle slides zijn overgeslagen — niets om te exporteren.': + 'All slides are skipped, so there is nothing to export.', + 'Alle slides zijn overgeslagen — niets om te tonen.': + 'All slides are skipped, so there is nothing to show.', + 'Alles tonen': 'Show all', + 'Audio verwijderen': 'Remove audio', + 'Automatisch doorgaan na': 'Advance automatically after', + 'Bijv. Kwartaalupdate Q4': 'E.g. Q4 update', + 'Bullet': 'Punto', + 'Caption / bronvermelding (bijv. © Naam Fotograaf)': + 'Caption / credit (e.g. © Photographer Name)', + 'Coverflow': 'Coverflow', + 'De afbeelding wordt schermvullend als achtergrond getoond met verminderde opaciteit zodat de tekst leesbaar blijft.': + 'The image is shown fullscreen as a background with reduced opacity so the text remains readable.', + 'De snelle bruine vos springt over de luie hond.': + 'The quick brown fox jumps over the lazy dog.', + 'Deze gegevens worden in de markdown opgeslagen en zijn doorzoekbaar bij het openen.': + 'These details are stored in the Markdown and searchable when opening.', + 'Deze presentatie heeft niet-opgeslagen wijzigingen. Sla de presentatie op voordat het tabblad sluit.': + 'This presentation has unsaved changes. Save it before closing the tab.', + 'Deze slide kan geen afbeelding ontvangen. Kies eerst een afbeeldingsslide.': + 'This slide cannot receive an image. Choose an image slide first.', + 'Eerste': 'First', + 'Einde van de presentatie': 'End of presentation', + 'Er is een presentatie met niet-opgeslagen wijzigingen gevonden van een vorige sessie:': + 'A presentation with unsaved changes was found from a previous session:', + 'Er zijn': 'There are', + 'Er zijn presentaties met niet-opgeslagen wijzigingen. Sla ze op voordat de app sluit.': + 'There are presentations with unsaved changes. Save them before closing the app.', + 'Export mislukt:': 'Export failed:', + 'Footer tonen op deze slide': 'Show footer on this slide', + 'Gebruik "Bladeren" om afbeeldingen van elke locatie te kiezen.': + 'Use “Browse” to choose images from any location.', + 'Geen afbeelding op het klembord gevonden.': + 'No image found on the clipboard.', + 'Geen ander deck open. Open eerst een ander tabblad.': + 'No other deck is open. Open another tab first.', + 'Geen andere presentaties (.md) in deze map gevonden.': + 'No other presentations (.md) found in this folder.', + 'Geen presentaties (.md) in deze map gevonden.': + 'No presentations (.md) found in this folder.', + 'Geen presentaties gevonden voor': 'No presentations found for', + 'Geen resultaten': 'No results', + 'Geen resultaten voor': 'No results for', + 'Geen slides gevonden voor': 'No slides found for', + 'Geen slides met': 'No slides with', + 'Geselecteerd': 'Selected', + 'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.': + 'HTML ta habri den tur browser sin internet i ta render bloknan di kódigo, matemátika i diagramnan Mermaid.', + 'Het bestand wordt permanent van schijf verwijderd. Deze actie kan niet ongedaan worden gemaakt.': + 'The file will be permanently deleted from disk. This action cannot be undone.', + 'Ingezoomd': 'Zoomed in', + 'Inzoomen (minder van de foto zichtbaar)': + 'Zoom in (less of the photo visible)', + 'Kies een afbeelding': 'Choose an image', + 'Kies een map met presentaties om te beginnen.': + 'Choose a folder with presentations to begin.', + 'Kon dit pakket niet importeren.': 'Could not import this package.', + 'Kon van deze URL geen presentatie ophalen.': + 'Could not fetch a presentation from this URL.', + 'Kopieer afbeelding naar klembord': 'Copy image to clipboard', + 'Kopiëren mislukt.': 'Copy failed.', + 'Kopiëren naar ander deck': 'Copy to another deck', + 'Kopiëren naar klembord mislukt.': 'Copying to clipboard failed.', + 'Koprij verwijderen': 'Remove header row', + 'Laat los om toe te voegen': 'Release to add', + 'Laatste slide': 'Último slide', + 'Let op: deze afbeelding wordt nog gebruikt in': + 'Warning: this image is still used in', + 'Logo kiezen': 'Choose logo', + 'Logo px': 'Logo px', + 'Logo tonen op deze slide': 'Show logo on this slide', + 'Map met presentaties kiezen': 'Choose presentation folder', + 'Map voor exports': 'Export folder', + 'Markdown kon niet worden verwerkt. Controleer de syntax.': + 'Markdown could not be processed. Check the syntax.', + 'Markdown modus — bewerk de volledige presentatie als Marp Markdown': + 'Markdown mode — edit the full presentation as Marp Markdown', + 'Markdown voor laatste slide': 'Markdown pa e último slide', + 'Naam van het stijlprofiel': 'Name of the style profile', + 'Niet-opgeslagen werk herstellen?': 'Restore unsaved work?', + 'Niet-opgeslagen wijzigingen': 'Unsaved changes', + 'Niets vervangen': 'Nothing replaced', + 'Nieuw profiel': 'New profile', + 'Open eerst een presentatie om afbeeldingen toe te voegen.': + 'Open a presentation before adding images.', + 'Overslaan bij presenteren/exporteren': 'Skip when presenting/exporting', + 'PREVIEW': 'PREVIEW', + 'Paginanummers tonen (rechtsonder)': 'Show page numbers (bottom right)', + 'Pakket geëxporteerd naar:': 'Package exported to:', + 'Pas je zoekterm aan of voeg een beschrijving toe.': + 'Adjust your search term or add a description.', + 'Plak de link naar een .ocideck-pakket of een Marp-markdownbestand.': + 'Paste the link to an .ocideck package or a Marp Markdown file.', + 'Preview inklappen': 'Collapse preview', + 'Preview uitklappen': 'Expand preview', + 'Profiel verwijderen': 'Delete profile', + 'Rij verwijderen': 'Remove row', + 'SLIDES': 'SLIDES', + 'Sectieachtergrond': 'Section background', + 'Selecteer een\nafbeelding': 'Select an\nimage', + 'Selectie opheffen': 'Clear selection', + 'Sleep om de slide-preview breder of smaller te maken': + 'Drag to make the slide preview wider or narrower', + 'Slide': 'Slide', + 'Slide gekopieerd naar klembord.': 'Slide copied to clipboard.', + 'Slide plakken': 'Paste slide', + 'Slide renderen…': 'Rendering slide…', + 'Slide toevoegen': 'Add slide', + 'Slides gerenderd.': 'Slides a wordu render.', + 'Sluiten (G of Esc)': 'Close (G or Esc)', + 'Sprekersnotities...': 'Speaker notes...', + 'Standaard laatste slide gebruiken': 'Usa e último slide standard', + 'Standaard map voor presentaties': 'Default presentation folder', + 'Standaardprofiel laden': 'Load default profile', + 'TLP-classificatie (Traffic Light Protocol)': + 'TLP classification (Traffic Light Protocol)', + 'Tabel koptekst': 'Table header text', + 'Tabeltekst': 'Table text', + 'Terug naar standaardstijl': 'Back to default style', + 'Terugzetten (volledige afbeelding zichtbaar)': + 'Reset (full image visible)', + 'Tijd resetten (R)': 'Reset timer (R)', + 'Tip: druk op Enter binnen een cel voor een nieuwe regel.': + 'Tip: press Enter inside a cell for a new line.', + 'Titelachtergrond': 'Title background', + 'Titeltekst': 'Title text', + 'Toepassen': 'Apply', + 'Tokens: {page}, {total}, {date}, {title}. Footer verschijnt op alle slides behalve titel- en sectieslides, tenzij je hem per slide uitzet.': + 'Tokens: {page}, {total}, {date}, {title}. The footer appears on all slides except title and section slides, unless you disable it per slide.', + 'Typ zoektermen om slides uit al je presentaties te vinden.': + 'Type search terms to find slides across your presentations.', + 'Uitgezoomd': 'Zoomed out', + 'Uitzoomen (meer van de foto zichtbaar)': + 'Zoom out (more of the photo visible)', + 'Verwijder afbeelding': 'Remove image', + 'Verwijder logo': 'Remove logo', + 'Verwijderen maakt die slides leeg. Dit kan niet ongedaan worden gemaakt.': + 'Deleting will clear those slides. This cannot be undone.', + 'Volledig zichtbaar (100%)': 'Fully visible (100%)', + 'Vul een titel in': 'Enter a title', + 'Weer tonen': 'Show again', + 'Weer tonen bij presenteren/exporteren': + 'Show again when presenting/exporting', + 'Wordt automatisch toegevoegd bij presenteren en exporteren.': + 'Ta wordu agregá automáticamente ora di presentá i eksportá.', + 'Zoek in slides…': 'Search in slides…', + 'Zoek op bestandsnaam, titel of tekst in de slides…': + 'Search by file name, title or slide text…', + 'Zoek op naam of beschrijving…': 'Search by name or description…', + 'Zoek op presentatie, titel of tekst…': + 'Search by presentation, title or text…', + 'Zoek slides op tekst, titel, onderschrift, pad…': + 'Search slides by text, title, caption, path…', + 'bijv. Vertrouwelijk · {title} · {date}': + 'e.g. Confidential · {title} · {date}', + 'gerenderd.': 'render.', + 'geselecteerd': 'selected', + 'meer treffer(s)': 'more match(es)', + 'paginering aan': 'pagination on', + 'pijltjes + Enter of klik om te springen': + 'arrows + Enter or click to jump', + 'presentaties met niet-opgeslagen wijzigingen gevonden van een vorige sessie:': + 'presentations with unsaved changes from a previous session:', + 'renderen…': 'render…', + 'resultaat': 'result', + 'resultaten': 'results', + 'slide': 'slide', + 'slide(s) gekopieerd naar': 'slide(s) copied to', + 'slides geïmporteerd.': 'slides imported.', + 'slides kopiëren naar…': 'slides to copy to…', + 'slides overgeslagen': 'slides skipped', + 'toegevoegd': 'added', + 'treffer(s)': 'match(es)', + 'treffers — verfijn je zoekopdracht': 'matches, refine your search', + 'van de foto zichtbaar': 'of the photo visible', + 'vervangen': 'replaced', + 'verwijderen': 'remove', + 'volledig deck': 'full deck', + 'voorbereiden…': 'preparando…', + '↑↓←→ navigeren · Enter kiezen · Dubbelklik selecteert': + '↑↓←→ navigate · Enter chooses · Double-click selects', + }, +}; diff --git a/lib/state/deck_provider.dart b/lib/state/deck_provider.dart index 38a2228..d056a4c 100644 --- a/lib/state/deck_provider.dart +++ b/lib/state/deck_provider.dart @@ -384,6 +384,7 @@ class DeckNotifier extends StateNotifier { } void updateInfo({ + String? title, String? author, String? organization, String? version, @@ -396,6 +397,7 @@ class DeckNotifier extends StateNotifier { if (deck == null) return; _mutate( deck.copyWith( + title: title, author: author, organization: organization, version: version, diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart index 0fd1e6f..0e92909 100644 --- a/lib/widgets/app_shell.dart +++ b/lib/widgets/app_shell.dart @@ -477,27 +477,32 @@ class _DropOverlay extends StatelessWidget { borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFF60A5FA), width: 2), ), - child: const Column( + child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon( + const Icon( Icons.file_download_outlined, size: 40, color: Color(0xFF2563EB), ), - SizedBox(height: 10), + const SizedBox(height: 10), Text( - 'Laat los om toe te voegen', - style: TextStyle( + context.l10n.d('Laat los om toe te voegen'), + style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1E293B), ), ), - SizedBox(height: 4), + const SizedBox(height: 4), Text( - 'Afbeeldingen → nieuwe slides · .md / .ocideck → openen', - style: TextStyle(fontSize: 12, color: Color(0xFF64748B)), + context.l10n.d( + 'Afbeeldingen → nieuwe slides · .md / .ocideck → openen', + ), + style: const TextStyle( + fontSize: 12, + color: Color(0xFF64748B), + ), ), ], ), @@ -1007,6 +1012,7 @@ class _MainLayoutState extends ConsumerState<_MainLayout> { final info = await PresentationInfoDialog.show(context, deck); if (info == null) return; deckNotifier.updateInfo( + title: info.title, author: info.author, organization: info.organization, version: info.version, @@ -1146,6 +1152,14 @@ class _MainLayoutState extends ConsumerState<_MainLayout> { tlp: deck.tlp, onSelected: (level) => deckNotifier.updateInfo(tlp: level), ), + const SizedBox(width: 6), + Tooltip( + message: l10n.t('presentationProperties'), + child: IconButton( + icon: const Icon(Icons.info_outline, size: 18), + onPressed: openProperties, + ), + ), ], ), actions: [ @@ -1292,11 +1306,6 @@ class _MainLayoutState extends ConsumerState<_MainLayout> { ), ), const PopupMenuDivider(), - menuItem( - 'properties', - Icons.info_outline, - l10n.t('presentationProperties'), - ), menuItem( 'settings', Icons.settings_outlined, diff --git a/lib/widgets/dialogs/export_dialog.dart b/lib/widgets/dialogs/export_dialog.dart index 26acf00..a9db743 100644 --- a/lib/widgets/dialogs/export_dialog.dart +++ b/lib/widgets/dialogs/export_dialog.dart @@ -293,12 +293,13 @@ class _ExportDialogState extends State { label: l10n.t('exportAsHtml'), onPressed: () => _export(ExportFormat.html), ), - const Padding( - padding: EdgeInsets.only(top: 4), + Padding( + padding: const EdgeInsets.only(top: 4), child: Text( - 'HTML opent in elke browser zonder internet en rendert codeblokken, ' - 'wiskunde en mermaid-diagrammen.', - style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8)), + l10n.d( + 'HTML opent in elke browser zonder internet en rendert codeblokken, wiskunde en mermaid-diagrammen.', + ), + style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)), ), ), ], diff --git a/lib/widgets/dialogs/presentation_info_dialog.dart b/lib/widgets/dialogs/presentation_info_dialog.dart index 83f31dd..2535395 100644 --- a/lib/widgets/dialogs/presentation_info_dialog.dart +++ b/lib/widgets/dialogs/presentation_info_dialog.dart @@ -5,6 +5,7 @@ import '../../l10n/app_localizations.dart'; /// The editable general metadata of a presentation. class PresentationInfo { + final String title; final String author; final String organization; final String version; @@ -13,6 +14,7 @@ class PresentationInfo { final String keywords; const PresentationInfo({ + required this.title, required this.author, required this.organization, required this.version, @@ -42,6 +44,7 @@ class PresentationInfoDialog extends StatefulWidget { } class _PresentationInfoDialogState extends State { + late final TextEditingController _title; late final TextEditingController _author; late final TextEditingController _organization; late final TextEditingController _version; @@ -52,6 +55,7 @@ class _PresentationInfoDialogState extends State { @override void initState() { super.initState(); + _title = TextEditingController(text: widget.deck.title); _author = TextEditingController(text: widget.deck.author); _organization = TextEditingController(text: widget.deck.organization); _version = TextEditingController(text: widget.deck.version); @@ -62,6 +66,7 @@ class _PresentationInfoDialogState extends State { @override void dispose() { + _title.dispose(); _author.dispose(); _organization.dispose(); _version.dispose(); @@ -75,6 +80,7 @@ class _PresentationInfoDialogState extends State { Navigator.pop( context, PresentationInfo( + title: _title.text.trim(), author: _author.text.trim(), organization: _organization.text.trim(), version: _version.text.trim(), @@ -108,14 +114,7 @@ class _PresentationInfoDialogState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.deck.title, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF64748B), - ), - ), + _field(_title, 'Titel', 'Titel van de presentatie'), const SizedBox(height: 12), Row( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/widgets/dialogs/settings_dialog.dart b/lib/widgets/dialogs/settings_dialog.dart index f4cd958..6833573 100644 --- a/lib/widgets/dialogs/settings_dialog.dart +++ b/lib/widgets/dialogs/settings_dialog.dart @@ -173,25 +173,26 @@ class _SettingsDialogState extends ConsumerState { : profiles.first.name; return DefaultTabController( - length: 3, + length: 4, child: AlertDialog( title: Text(l10n.t('settings')), content: SizedBox( width: 520, - height: 560, + height: 600, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _profileSelector(profiles, dropdownValue), - const SizedBox(height: 12), - _profileNameField(), - const SizedBox(height: 12), TabBar( + isScrollable: true, tabs: [ Tab( icon: const Icon(Icons.tune), text: l10n.t('settingsGeneral'), ), + Tab( + icon: const Icon(Icons.style_outlined), + text: l10n.t('styleProfile'), + ), Tab( icon: const Icon(Icons.palette_outlined), text: l10n.t('settingsColors'), @@ -207,6 +208,7 @@ class _SettingsDialogState extends ConsumerState { child: TabBarView( children: [ _tabBody(_generalTab()), + _tabBody(_styleTab(profiles, dropdownValue)), _tabBody(_colorsTab()), _tabBody(_logoTab()), ], @@ -350,6 +352,24 @@ class _SettingsDialogState extends ConsumerState { ); } + Widget _styleTab(List profiles, String dropdownValue) { + final l10n = context.l10n; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionTitle(l10n.t('styleProfile')), + _profileSelector(profiles, dropdownValue), + const SizedBox(height: 12), + _profileNameField(), + const SizedBox(height: 20), + _sectionTitle(l10n.d('Lettertype')), + _fontSection(), + const SizedBox(height: 18), + _stylePreview(), + ], + ); + } + Widget _generalTab() { final l10n = context.l10n; final languageCode = ref.watch( @@ -507,9 +527,6 @@ class _SettingsDialogState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _sectionTitle(l10n.d('Lettertype')), - _fontSection(), - const SizedBox(height: 20), _sectionTitle(l10n.d('Kleuren')), _colorSetting( l10n.d('Achtergrond slides'), @@ -638,7 +655,10 @@ class _SettingsDialogState extends ConsumerState { width: 160, child: TextField( controller: _logoSize, - decoration: InputDecoration(labelText: 'Logo px', isDense: true), + decoration: InputDecoration( + labelText: context.l10n.d('Logo px'), + isDense: true, + ), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (_) => _profileTouched = true, @@ -754,7 +774,7 @@ class _SettingsDialogState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - label, + '$label $value', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, @@ -767,29 +787,13 @@ class _SettingsDialogState extends ConsumerState { runSpacing: 6, children: [ for (final color in _colorPresets) - Tooltip( - message: color, - child: InkWell( - onTap: () => setState(() { - onChanged(color); - _profileTouched = true; - }), - borderRadius: BorderRadius.circular(12), - child: Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: _parseColor(color), - shape: BoxShape.circle, - border: Border.all( - color: value == color - ? AppTheme.accent - : const Color(0xFFCBD5E1), - width: value == color ? 2 : 1, - ), - ), - ), - ), + _colorSwatch( + color, + selected: value == color, + onTap: () => setState(() { + onChanged(color); + _profileTouched = true; + }), ), ], ), @@ -797,6 +801,73 @@ class _SettingsDialogState extends ConsumerState { ); } + Widget _colorSwatch( + String color, { + required bool selected, + required VoidCallback onTap, + }) { + final parsed = _parseColor(color); + final checkColor = parsed.computeLuminance() > 0.55 + ? const Color(0xFF0F172A) + : Colors.white; + return Tooltip( + message: selected ? '${context.l10n.d('Geselecteerd')}: $color' : color, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: AnimatedContainer( + duration: const Duration(milliseconds: 120), + width: 34, + height: 34, + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: selected + ? AppTheme.accent.withValues(alpha: 0.12) + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: selected ? AppTheme.accent : const Color(0xFFCBD5E1), + width: selected ? 2 : 1, + ), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: parsed, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: const [ + BoxShadow( + color: Color(0x330F172A), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + ), + if (selected) + Icon( + Icons.check, + size: 16, + color: checkColor, + shadows: [ + Shadow( + color: checkColor == Colors.white + ? Colors.black54 + : Colors.white70, + blurRadius: 2, + ), + ], + ), + ], + ), + ), + ), + ); + } + Widget _stylePreview() { final l10n = context.l10n; return Container( diff --git a/lib/widgets/panels/preview_panel.dart b/lib/widgets/panels/preview_panel.dart index 445277d..5daa044 100644 --- a/lib/widgets/panels/preview_panel.dart +++ b/lib/widgets/panels/preview_panel.dart @@ -442,7 +442,7 @@ class CollapsedPreviewBar extends ConsumerWidget { RotatedBox( quarterTurns: 1, child: Text( - 'PREVIEW', + context.l10n.d('PREVIEW'), style: TextStyle( fontSize: 10, letterSpacing: 1.5, diff --git a/lib/widgets/presentation/fullscreen_presenter.dart b/lib/widgets/presentation/fullscreen_presenter.dart index 0a92b14..e397dc6 100644 --- a/lib/widgets/presentation/fullscreen_presenter.dart +++ b/lib/widgets/presentation/fullscreen_presenter.dart @@ -38,24 +38,30 @@ class FullscreenPresenter extends StatefulWidget { required int initialIndex, TlpLevel tlp = TlpLevel.none, }) async { - await windowManager.setFullScreen(true); - if (context.mounted) { - await Navigator.push( - context, - PageRouteBuilder( - opaque: true, - pageBuilder: (context, anim, anim2) => FullscreenPresenter( - slides: slides, - projectPath: projectPath, - themeProfile: themeProfile, - initialIndex: initialIndex, - tlp: tlp, + final hadWakeLock = await _wakeLockEnabled(); + await _enableWakeLock(); + try { + await windowManager.setFullScreen(true); + if (context.mounted) { + await Navigator.push( + context, + PageRouteBuilder( + opaque: true, + pageBuilder: (context, anim, anim2) => FullscreenPresenter( + slides: slides, + projectPath: projectPath, + themeProfile: themeProfile, + initialIndex: initialIndex, + tlp: tlp, + ), + transitionsBuilder: (context, animation, secondary, child) => + FadeTransition(opacity: animation, child: child), + transitionDuration: const Duration(milliseconds: 200), ), - transitionsBuilder: (context, animation, secondary, child) => - FadeTransition(opacity: animation, child: child), - transitionDuration: const Duration(milliseconds: 200), - ), - ); + ); + } + } finally { + await _restoreWakeLock(hadWakeLock); } } @@ -63,6 +69,34 @@ class FullscreenPresenter extends StatefulWidget { State createState() => _FullscreenPresenterState(); } +Future _wakeLockEnabled() async { + try { + return await WakelockPlus.enabled; + } catch (_) { + return false; + } +} + +Future _enableWakeLock() async { + try { + await WakelockPlus.enable(); + } catch (_) { + // Best-effort: unsupported platforms should not interrupt presenting. + } +} + +Future _restoreWakeLock(bool enabledBeforePresentation) async { + try { + if (enabledBeforePresentation) { + await WakelockPlus.enable(); + } else { + await WakelockPlus.disable(); + } + } catch (_) { + // Best-effort cleanup. + } +} + class _FullscreenPresenterState extends State { late int _index; late FocusNode _focusNode; @@ -125,7 +159,6 @@ class _FullscreenPresenterState extends State { _clockTimer = Timer.periodic(const Duration(seconds: 1), (_) { if (mounted && _presenterView) setState(() {}); }); - _enableWakeLock(); WidgetsBinding.instance.addPostFrameCallback((_) { _focusNode.requestFocus(); _loadDisplays(); @@ -138,28 +171,11 @@ class _FullscreenPresenterState extends State { _advanceTimer?.cancel(); _clockTimer?.cancel(); _typedTimer?.cancel(); - _disableWakeLock(); _gridScroll.dispose(); _focusNode.dispose(); super.dispose(); } - Future _enableWakeLock() async { - try { - await WakelockPlus.enable(); - } catch (_) { - // Best-effort: unsupported platforms should not interrupt presenting. - } - } - - Future _disableWakeLock() async { - try { - await WakelockPlus.disable(); - } catch (_) { - // Best-effort cleanup. - } - } - void _scheduleAdvance() { _advanceTimer?.cancel(); _advanceTimer = null; @@ -287,7 +303,6 @@ class _FullscreenPresenterState extends State { Future _exit() async { _advanceTimer?.cancel(); - await _disableWakeLock(); await windowManager.setFullScreen(false); if (mounted) Navigator.pop(context); } diff --git a/lib/widgets/slides/slide_preview.dart b/lib/widgets/slides/slide_preview.dart index 37f3030..a37eb5b 100644 --- a/lib/widgets/slides/slide_preview.dart +++ b/lib/widgets/slides/slide_preview.dart @@ -6,6 +6,7 @@ import 'package:flutter_math_fork/flutter_math.dart'; import 'package:highlight/highlight.dart' show highlight; import 'package:highlight/languages/all.dart' show allLanguages; import 'package:video_player/video_player.dart'; +import '../../l10n/app_localizations.dart'; import '../../models/deck.dart'; import '../../models/settings.dart'; import '../../models/slide.dart'; @@ -154,6 +155,10 @@ class SlidePreviewWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final hasBottomRightTlp = + tlp != TlpLevel.none && + !((themeProfile.logoPath?.isNotEmpty == true && slide.showLogo) && + themeProfile.logoPosition == 'bottom-right'); // Make the widget self-sufficient for text rendering. On screen it sits // inside a Material (which supplies a clean DefaultTextStyle), but the // export rasterizer mounts it in a bare Overlay subtree. Without an @@ -172,7 +177,7 @@ class SlidePreviewWidget extends StatelessWidget { ), child: _SlideLinkScope( onTapLink: onLinkTap, - hasBottomTlp: tlp != TlpLevel.none, + hasBottomTlp: hasBottomRightTlp, child: _buildSlide(), ), ), @@ -199,7 +204,14 @@ class SlidePreviewWidget extends StatelessWidget { tlp: tlp, ), if (tlp != TlpLevel.none) - _TlpOverlay(tlp: tlp, w: w, profile: themeProfile), + _TlpOverlay( + tlp: tlp, + w: w, + profile: themeProfile, + hasLogo: + themeProfile.logoPath?.isNotEmpty == true && + slide.showLogo, + ), if (themeProfile.logoPath?.isNotEmpty == true && slide.showLogo) _LogoOverlay( logoPath: themeProfile.logoPath!, @@ -502,6 +514,7 @@ class _TitlePreview extends StatelessWidget { fit: StackFit.expand, children: [ _zoomedImage( + context, slide.imagePath, projectPath, slide.imageSize, @@ -1065,13 +1078,8 @@ class _BulletsImagePreview extends StatelessWidget { child: Stack( fit: StackFit.expand, children: [ - _resolvedImage(slide.imagePath, projectPath), - _captionOverlay( - context, - slide.imageCaption, - w, - right: w * 0.018, - ), + _resolvedImage(context, slide.imagePath, projectPath), + _captionOverlay(context, slide.imageCaption, w), ], ), ), @@ -1449,7 +1457,7 @@ class _TwoImagesPreview extends StatelessWidget { child: Stack( fit: StackFit.expand, children: [ - _resolvedImage(slide.imagePath, projectPath), + _resolvedImage(context, slide.imagePath, projectPath), _captionOverlay(context, slide.imageCaption, w), ], ), @@ -1459,7 +1467,7 @@ class _TwoImagesPreview extends StatelessWidget { child: Stack( fit: StackFit.expand, children: [ - _resolvedImage(slide.imagePath2, projectPath), + _resolvedImage(context, slide.imagePath2, projectPath), _captionOverlay(context, slide.imageCaption2, w), ], ), @@ -1524,6 +1532,7 @@ class _ImagePreview extends StatelessWidget { fit: StackFit.expand, children: [ _zoomedImage( + context, slide.imagePath, projectPath, slide.imageSize, @@ -1792,6 +1801,7 @@ class _QuotePreview extends StatelessWidget { fit: StackFit.expand, children: [ _zoomedImage( + context, slide.imagePath, projectPath, slide.imageSize, @@ -1831,7 +1841,12 @@ class _LogoOverlay extends StatelessWidget { child: SizedBox( width: size, height: size, - child: _resolvedImage(logoPath, projectPath, fit: BoxFit.contain), + child: _resolvedImage( + context, + logoPath, + projectPath, + fit: BoxFit.contain, + ), ), ); } @@ -2047,6 +2062,7 @@ void _ensureHighlightLanguages() { /// imageSize > 100 → inzoomen: groter dan contain, bijgesneden door ClipRect /// imageSize < 100 → nog meer uitzoomen: afbeelding kleiner dan contain Widget _zoomedImage( + BuildContext context, String imagePath, String? projectPath, int imageSize, { @@ -2054,7 +2070,11 @@ Widget _zoomedImage( Alignment alignment = Alignment.center, }) { if (imageSize == 0) { - return _resolvedImage(imagePath, projectPath); // BoxFit.cover standaard + return _resolvedImage( + context, + imagePath, + projectPath, + ); // BoxFit.cover standaard } final scale = imageSize / 100.0; // Size the image box to `scale` × the available area and let BoxFit.contain @@ -2076,6 +2096,7 @@ Widget _zoomedImage( height: boxH, // BoxFit.contain: toont de volledige afbeelding zonder bijsnijden child: _resolvedImage( + context, imagePath, projectPath, fit: BoxFit.contain, @@ -2089,11 +2110,12 @@ Widget _zoomedImage( } Widget _resolvedImage( + BuildContext context, String imagePath, String? projectPath, { BoxFit fit = BoxFit.cover, }) { - if (imagePath.isEmpty) return _imagePlaceholder(); + if (imagePath.isEmpty) return _imagePlaceholder(context); final String resolved; if (imagePath.startsWith('/') || imagePath.contains(':\\')) { @@ -2109,7 +2131,7 @@ Widget _resolvedImage( fit: fit, width: double.infinity, height: double.infinity, - errorBuilder: (context, error, stackTrace) => _imagePlaceholder(), + errorBuilder: (context, error, stackTrace) => _imagePlaceholder(context), ); } @@ -2128,8 +2150,8 @@ Widget _captionOverlay( ? _tlpVerticalReserve(w) : 0.0; return Positioned( - right: right ?? w * 0.018, - bottom: (bottom ?? w * 0.014) + lift, + right: right ?? w * _kTlpEdge, + bottom: (bottom ?? _tlpBottomInset(w)) + lift, child: Container( constraints: BoxConstraints(maxWidth: w * 0.5), padding: EdgeInsets.symmetric(horizontal: w * 0.008, vertical: w * 0.005), @@ -2165,13 +2187,15 @@ const double _kTlpEdge = 0.025; // afstand tot de slidehoek (× breedte) const double _kTlpHPad = 0.011; const double _kTlpVPad = 0.005; +double _tlpBottomInset(double w) => w * 0.022; + /// Geschatte breedte van de TLP-badge, zodat de footer ervoor kan uitwijken. double _tlpBadgeWidth(double w, TlpLevel tlp) => tlp.label.length * w * _kTlpFont * 0.62 + 2 * (w * _kTlpHPad); /// Verticale ruimte die een TLP-badge rechtsonder inneemt (voor bijschriften). double _tlpVerticalReserve(double w) => - w * _kTlpFont + 2 * (w * _kTlpVPad) + w * 0.014; + w * _kTlpFont + 2 * (w * _kTlpVPad) + _tlpBottomInset(w); /// Officiële TLP 2.0-markering (FIRST): de gekleurde label op een zwart vlak, /// rechtsonder. Wijkt uit naar linksonder als het logo rechtsonder staat. @@ -2179,18 +2203,20 @@ class _TlpOverlay extends StatelessWidget { final TlpLevel tlp; final double w; final ThemeProfile profile; + final bool hasLogo; const _TlpOverlay({ required this.tlp, required this.w, required this.profile, + required this.hasLogo, }); @override Widget build(BuildContext context) { - final toLeft = profile.logoPosition == 'bottom-right'; + final toLeft = hasLogo && profile.logoPosition == 'bottom-right'; return Positioned( - bottom: w * 0.022, + bottom: _tlpBottomInset(w), left: toLeft ? w * _kTlpEdge : null, right: toLeft ? null : w * _kTlpEdge, child: Container( @@ -2306,7 +2332,7 @@ class _FooterOverlay extends StatelessWidget { final logoOnLeft = profile.logoPosition.endsWith('left'); final logoSpan = w * (profile.logoSize / 1280) * 1.28 + w * 0.012; final logoLeftEdge = w * (profile.logoSize / 1280) * 0.28; - final tlpOnRight = profile.logoPosition != 'bottom-right'; + final tlpOnRight = !(hasLogo && profile.logoPosition == 'bottom-right'); final tlpSpan = tlp == TlpLevel.none ? 0.0 : w * _kTlpEdge + _tlpBadgeWidth(w, tlp) + w * 0.012; @@ -2403,18 +2429,18 @@ Widget _mediaPlaceholder(IconData icon, String label) { ); } -Widget _imagePlaceholder() { +Widget _imagePlaceholder(BuildContext context) { return Container( color: const Color(0xFFE2E8F0), - child: const Center( + child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.image_outlined, color: Color(0xFF94A3B8), size: 24), - SizedBox(height: 4), + const Icon(Icons.image_outlined, color: Color(0xFF94A3B8), size: 24), + const SizedBox(height: 4), Text( - 'Afbeelding', - style: TextStyle(color: Color(0xFF94A3B8), fontSize: 10), + context.l10n.d('Afbeelding'), + style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 10), ), ], ), diff --git a/test/app_localizations_test.dart b/test/app_localizations_test.dart index c6de210..8fd4607 100644 --- a/test/app_localizations_test.dart +++ b/test/app_localizations_test.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ocideck/l10n/app_localizations.dart'; @@ -32,4 +34,96 @@ void main() { ); expect(AppLocalizations.materialLocaleFor('pap'), const Locale('en')); }); + + test('all literal Dutch source strings have an English fallback', () { + AppLocalizations.setActiveLanguageCode('en'); + + const unchangedInEnglish = { + 'Accent / bullets', + 'Bullet', + 'Coverflow', + 'Logo', + 'Logo px', + 'PREVIEW', + 'Preview', + 'SLIDES', + 'Slide', + 'slide', + }; + final expression = RegExp(r'''\.d\(\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*")'''); + final files = Directory('lib') + .listSync(recursive: true) + .whereType() + .where((file) => file.path.endsWith('.dart')); + final sources = {}; + + for (final file in files) { + final content = file.readAsStringSync(); + for (final match in expression.allMatches(content)) { + sources.add(_unquoteDartString(match.group(1)!)); + } + } + + final english = const AppLocalizations(Locale('en')); + final missing = sources.where((source) { + final translated = english.d(source); + return translated == source && !unchangedInEnglish.contains(source); + }).toList()..sort(); + + expect(missing, isEmpty); + }); + + test('all literal Dutch source strings are translated in every language', () { + const unchangedInAllLanguages = { + 'Accent / bullets', + 'Bullet', + 'Coverflow', + 'Logo', + 'Logo px', + 'PREVIEW', + 'Preview', + 'SLIDES', + 'Slide', + 'slide', + }; + final expression = RegExp(r'''\.d\(\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*")'''); + final files = Directory('lib') + .listSync(recursive: true) + .whereType() + .where((file) => file.path.endsWith('.dart')); + final sources = {}; + + for (final file in files) { + final content = file.readAsStringSync(); + for (final match in expression.allMatches(content)) { + sources.add(_unquoteDartString(match.group(1)!)); + } + } + + final missingByLanguage = >{}; + for (final languageCode in AppLocalizations.languageNames.keys) { + if (languageCode == 'nl') continue; + final missing = sources.where((source) { + if (unchangedInAllLanguages.contains(source)) return false; + return !AppLocalizations.hasDirectDutchSourceTranslation( + languageCode, + source, + ); + }).toList()..sort(); + if (missing.isNotEmpty) missingByLanguage[languageCode] = missing; + } + + expect(missingByLanguage, isEmpty); + }); +} + +String _unquoteDartString(String value) { + final quote = value[0]; + final body = value.substring(1, value.length - 1); + return body + .replaceAll(r'\\', r'\') + .replaceAll('\\$quote', quote) + .replaceAll(r'\n', '\n') + .replaceAll(r'\r', '\r') + .replaceAll(r'\t', '\t'); } diff --git a/test/deck_provider_test.dart b/test/deck_provider_test.dart index 917b270..6c540ce 100644 --- a/test/deck_provider_test.dart +++ b/test/deck_provider_test.dart @@ -112,6 +112,13 @@ void main() { expect(n.state.deck!.paginate, isFalse); }); + test('updateInfo can update the presentation title', () { + final n = _notifier()..newDeck('D'); + n.updateInfo(title: 'Nieuwe presentatietitel', author: 'Auteur'); + expect(n.state.deck!.title, 'Nieuwe presentatietitel'); + expect(n.state.deck!.author, 'Auteur'); + }); + test('generateMarkdown and applyMarkdown round-trip the deck', () { final n = _notifier()..newDeck('D'); n.addSlide(SlideType.bulletsImage, afterIndex: 0); diff --git a/test/tlp_test.dart b/test/tlp_test.dart index bb4998d..ba029cd 100644 --- a/test/tlp_test.dart +++ b/test/tlp_test.dart @@ -63,5 +63,39 @@ void main() { await tester.pump(); expect(find.textContaining('TLP:'), findsNothing); }); + + testWidgets('right-side image caption aligns with the TLP badge', ( + tester, + ) async { + const caption = 'Foto: iemand'; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 800, + height: 450, + child: SlidePreviewWidget( + slide: Slide.create( + SlideType.bulletsImage, + ).copyWith(title: 'T', bullets: ['a'], imageCaption: caption), + tlp: TlpLevel.red, + ), + ), + ), + ), + ), + ); + await tester.pump(); + + final captionRight = tester.getTopRight(find.text(caption)).dx; + final tlpRight = tester.getTopRight(find.text('TLP:RED')).dx; + + expect( + (captionRight - tlpRight).abs(), + lessThan(4), + reason: 'Caption and TLP badge should share the same right edge.', + ); + }); }); } -- 2.45.3 From b7db54e03302a5924c185225ffce08d742255a49 Mon Sep 17 00:00:00 2001 From: Brenno de Winter Date: Sat, 6 Jun 2026 20:41:24 +0200 Subject: [PATCH 2/9] 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 `