From d59c6ee761f41e3b86371575f707d725cfa9ff12 Mon Sep 17 00:00:00 2001 From: Brenno de Winter Date: Thu, 4 Jun 2026 08:17:12 +0200 Subject: [PATCH] feat: refine presenter and language options --- lib/app.dart | 3 +- lib/l10n/app_localizations.dart | 605 +++++++++++++++++- lib/widgets/app_shell.dart | 7 - .../presentation/fullscreen_presenter.dart | 262 +++----- pubspec.lock | 2 +- pubspec.yaml | 1 + test/app_localizations_test.dart | 35 + test/free_markdown_preview_test.dart | 12 +- test/fullscreen_presenter_test.dart | 47 +- 9 files changed, 752 insertions(+), 222 deletions(-) create mode 100644 test/app_localizations_test.dart diff --git a/lib/app.dart b/lib/app.dart index 7235d5c..4283111 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -14,11 +14,12 @@ class OciDeckApp extends ConsumerWidget { final languageCode = ref.watch( settingsProvider.select((s) => s.languageCode), ); + AppLocalizations.setActiveLanguageCode(languageCode); return MaterialApp( title: 'OciDeck', theme: AppTheme.light, debugShowCheckedModeBanner: false, - locale: Locale(languageCode), + locale: AppLocalizations.materialLocaleFor(languageCode), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: const [ AppLocalizations.delegate, diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 0bd70bc..147dc8c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -13,6 +13,8 @@ class AppLocalizations { Locale('de'), Locale('fr'), Locale('es'), + Locale('fy'), + Locale('pap'), ]; static const languageNames = { @@ -22,8 +24,31 @@ class AppLocalizations { 'de': 'Deutsch', 'fr': 'Français', 'es': 'Español', + 'fy': 'Frysk', + 'pap': 'Papiamento', }; + static const _materialLocaleFallbacks = { + 'nl': Locale('nl'), + 'en': Locale('en'), + 'it': Locale('it'), + 'de': Locale('de'), + 'fr': Locale('fr'), + 'es': Locale('es'), + 'fy': Locale('en'), + 'pap': Locale('en'), + }; + + static String _activeLanguageCode = 'nl'; + + static void setActiveLanguageCode(String code) { + _activeLanguageCode = languageNames.containsKey(code) ? code : 'nl'; + } + + static Locale materialLocaleFor(String code) { + return _materialLocaleFallbacks[code] ?? const Locale('nl'); + } + static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); @@ -32,13 +57,19 @@ class AppLocalizations { const AppLocalizations(Locale('nl')); } + String get languageCode => _activeLanguageCode; + String t(String key) { - return _strings[locale.languageCode]?[key] ?? _strings['nl']![key] ?? key; + if (languageCode == 'nl') return _strings['nl']![key] ?? key; + return _strings[languageCode]?[key] ?? + _strings['en']?[key] ?? + _strings['nl']![key] ?? + key; } String d(String dutchText) { - if (locale.languageCode == 'nl') return dutchText; - return _dutchSourceStrings[locale.languageCode]?[dutchText] ?? + if (languageCode == 'nl') return dutchText; + return _dutchSourceStrings[languageCode]?[dutchText] ?? _dutchSourceStrings['en']?[dutchText] ?? dutchText; } @@ -567,6 +598,168 @@ const _strings = { 'of': 'de', 'exportedTo': 'Exportado a:', }, + 'fy': { + 'newPresentation': 'Nije presintaasje', + 'open': 'Iepenje...', + 'openEllipsis': 'Iepenje…', + 'recentPresentations': 'Resinte presintaasjes', + 'newTab': 'Nij ljepblêd', + 'undo': 'Ungedien meitsje (Ctrl/Cmd+Z)', + 'redo': 'Opnij útfiere (Ctrl/Cmd+Shift+Z)', + 'imageLibrary': 'Ofbyldingsbibleteek', + 'presentFullscreen': 'Presintearje folslein skerm · P foar presenter view', + 'visualMode': 'Fisuele modus', + 'markdownMode': 'Markdown-modus', + 'save': 'Bewarje', + 'saveShortcut': 'Bewarje (Ctrl/Cmd+S)', + 'more': 'Mear', + 'export': 'Eksportearje', + 'exportReady': 'Eksportearje (PDF/PPTX/HTML)', + 'exportNeedsSave': 'Bewarje de presintaasje earst om te eksportearjen', + 'exportNeedsClean': 'Bewarje dyn wizigingen earst om te eksportearjen', + 'saved': 'Bewarre', + 'unsaved': 'Net bewarre', + 'unsavedChanges': 'Wizigingen bewarje (Ctrl/Cmd+S)', + 'noUnsavedChanges': 'Gjin net-bewarre wizigingen', + 'notSavedYet': 'Noch net bewarre', + 'noFileYet': 'Dizze presintaasje hat noch gjin bestân', + 'slides': 'slides', + 'skipped': 'oerslein', + 'allSlidesIncluded': 'Alle slides wurde presintearre en eksportearre', + 'skippedSlidesExcluded': 'slide(s) wurde net presintearre of eksportearre', + 'styleProfile': 'Stylprofyl', + 'classification': 'Klassifikaasje', + 'exportNextToDeck': 'Eksport neist deck', + 'exportsNextToDeck': 'Eksporten wurde neist it deck bewarre', + 'exportFolder': 'Eksport', + 'newPresentationTab': 'Nije presintaasje (ljepblêd)', + 'exportPackage': 'Pakket eksportearje…', + 'importPackage': 'Pakket ymportearje…', + 'importUrl': 'Ymportearje fan URL…', + 'findReplace': 'Sykje en ferfange', + 'fullDeckPreview': 'Hiel deck besjen', + 'presentationProperties': 'Presintaasje-eigenskippen', + 'settings': 'Ynstellingen', + 'settingsGeneral': 'Algemien', + 'settingsColors': 'Kleuren', + 'settingsLogo': 'Logo', + 'language': 'Taal', + 'applicationLanguage': 'Applikaasjetaal', + 'languageHelp': + 'De interface wikselet daliks fan taal. Presintaasje-ynhâld bliuwt itselde.', + 'presentationFolder': 'Presintaasjemap', + 'exportFolderSetting': 'Eksportmap', + 'notSet': 'Net ynsteld', + 'nextToPresentationFile': 'Neist it presintaasjebestân', + 'choose': 'Kieze', + 'removeDefaultFolder': 'Standertmap fuortsmite', + 'removeExportFolder': 'Eksportmap fuortsmite', + 'exportFolderHelp': + 'Alle eksporten (PDF/PPTX) wurde hjir bewarre. Net ynsteld? Dan komme se neist it presintaasjebestân.', + 'cancel': 'Annulearje', + 'close': 'Slute', + 'saveSettings': 'Bewarje', + 'exportDialogTitle': 'Eksportearje', + 'exportAgain': 'Nochris eksportearje', + 'exportIntro': + 'De eksport brûkt krekt de werjefte út de editor, ynklusyf dyn stylprofyl.', + 'imageQualityPdf': 'Ofbyldingskwaliteit (PDF)', + 'normal': 'Normaal', + 'compressed': 'Komprimearre', + 'compressedHelp': + 'JPEG op legere resolúsje, bedoeld as handout, mei in folle lytser bestân (apart bewarre as “-compact”).', + 'losslessHelp': 'Ferliesfrije ôfbyldings op folsleine resolúsje.', + 'exportAsPdf': 'Eksportearje as PDF', + 'exportAsPptx': 'Eksportearje as PPTX', + 'exportAsHtml': 'Eksportearje as HTML (Marp, offline)', + 'renderingSlides': 'Slides renderje…', + 'buildingHtml': 'HTML bouwe…', + 'buildingExport': 'bouwe…', + 'slideOf': 'Slide', + 'of': 'fan', + 'exportedTo': 'Eksportearre nei:', + }, + 'pap': { + 'newPresentation': 'Presentashon nobo', + 'open': 'Habri...', + 'openEllipsis': 'Habri…', + 'recentPresentations': 'Presentashonnan resien', + 'newTab': 'Tab nobo', + 'undo': 'Deshasí (Ctrl/Cmd+Z)', + 'redo': 'Hasi atrobe (Ctrl/Cmd+Shift+Z)', + 'imageLibrary': 'Biblioteka di imágen', + 'presentFullscreen': 'Presentá na pantalla kompletu · P pa presenter view', + 'visualMode': 'Modo visual', + 'markdownMode': 'Modo Markdown', + 'save': 'Warda', + 'saveShortcut': 'Warda (Ctrl/Cmd+S)', + 'more': 'Mas', + 'export': 'Eksportá', + 'exportReady': 'Eksportá (PDF/PPTX/HTML)', + 'exportNeedsSave': 'Warda e presentashon promé ku eksportá', + 'exportNeedsClean': 'Warda bo kambionan promé ku eksportá', + 'saved': 'Wardá', + 'unsaved': 'No wardá', + 'unsavedChanges': 'Warda kambionan (Ctrl/Cmd+S)', + 'noUnsavedChanges': 'No tin kambionan sin warda', + 'notSavedYet': 'Ainda no wardá', + 'noFileYet': 'E presentashon aki ainda no tin un file', + 'slides': 'slides', + 'skipped': 'saltá', + 'allSlidesIncluded': 'Tur slides lo wordu presentá i eksportá', + 'skippedSlidesExcluded': 'slide(s) no lo wordu presentá òf eksportá', + 'styleProfile': 'Perfil di estilo', + 'classification': 'Klasifikashon', + 'exportNextToDeck': 'Eksportá banda di deck', + 'exportsNextToDeck': 'Eksportnan ta wordu wardá banda di e deck', + 'exportFolder': 'Eksport', + 'newPresentationTab': 'Presentashon nobo (tab)', + 'exportPackage': 'Eksportá pakete…', + 'importPackage': 'Importá pakete…', + 'importUrl': 'Importá for di URL…', + 'findReplace': 'Busca i reemplasá', + 'fullDeckPreview': 'Mira henter e deck', + 'presentationProperties': 'Propiedatnan di presentashon', + 'settings': 'Preferensianan', + 'settingsGeneral': 'General', + 'settingsColors': 'Kolónan', + 'settingsLogo': 'Logo', + 'language': 'Idioma', + 'applicationLanguage': 'Idioma di aplikashon', + 'languageHelp': + 'E interface ta cambia idioma mesora. Kontenido di presentashon ta keda igual.', + 'presentationFolder': 'Folder di presentashon', + 'exportFolderSetting': 'Folder di eksport', + 'notSet': 'No konfigurá', + 'nextToPresentationFile': 'Banda di e file di presentashon', + 'choose': 'Skohe', + 'removeDefaultFolder': 'Kita folder standard', + 'removeExportFolder': 'Kita folder di eksport', + 'exportFolderHelp': + 'Tur eksportnan (PDF/PPTX) ta wordu wardá aki. Si no konfigurá, nan ta wordu wardá banda di e file di presentashon.', + 'cancel': 'Kanselá', + 'close': 'Sera', + 'saveSettings': 'Warda', + 'exportDialogTitle': 'Eksportá', + 'exportAgain': 'Eksportá atrobe', + 'exportIntro': + 'E eksport ta usa mesun bista ku e editor, inkluyendo bo perfil di estilo.', + 'imageQualityPdf': 'Kalidat di imágen (PDF)', + 'normal': 'Normal', + 'compressed': 'Komprimí', + 'compressedHelp': + 'JPEG ku resolushon mas abou, pa handout, ku un file hopi mas chikí (wardá apart komo “-compact”).', + 'losslessHelp': 'Imágennan sin pèrdida na resolushon kompletu.', + 'exportAsPdf': 'Eksportá komo PDF', + 'exportAsPptx': 'Eksportá komo PPTX', + 'exportAsHtml': 'Eksportá komo HTML (Marp, offline)', + 'renderingSlides': 'Render slides…', + 'buildingHtml': 'Trahando HTML…', + 'buildingExport': 'trahando…', + 'slideOf': 'Slide', + 'of': 'di', + 'exportedTo': 'Eksportá na:', + }, }; const _dutchSourceStrings = { @@ -852,21 +1045,26 @@ const _dutchSourceStrings = { '↑↓←→ navigeren · Enter kiezen · Dubbelklik selecteert': '↑↓←→ navigate · Enter chooses · Double-click selects', 'Sneltoetsen': 'Keyboard shortcuts', + 'Toetsenlegenda': 'Key legend', 'spatie': 'space', 'klik': 'click', 'cijfers': 'numbers', 'Klik of druk op ? / H / Esc om te sluiten': 'Click or press ? / H / Esc to close', + 'Klik of druk op H / Esc om te sluiten': 'Click or press H / Esc to close', 'Naar slidenummer': 'Go to slide number', 'Eerste · laatste slide': 'First · last slide', 'Slide-overzicht': 'Slide overview', + 'Slide-overzicht (pijltjes + Enter)': 'Slide overview (arrows + Enter)', 'Presenter view (notities, klok)': 'Presenter view (notes, clock)', + 'Scherm wisselen (meerdere schermen)': 'Switch screen (multiple displays)', 'Zwart · wit scherm': 'Black · white screen', 'Verstreken tijd resetten': 'Reset elapsed time', 'Automatische modus aan/uit': 'Automatic mode on/off', 'Herhalen (loop) aan/uit': 'Repeat (loop) on/off', 'Na audio automatisch doorgaan': 'Advance automatically after audio', 'Dit overzicht': 'This overview', + 'Deze legenda': 'This legend', 'Terug / afsluiten': 'Back / exit', 'Auto (A)': 'Auto (A)', 'Handmatig (A)': 'Manual (A)', @@ -883,8 +1081,16 @@ const _dutchSourceStrings = { 'Verstreken': 'Elapsed', 'Klok': 'Clock', 'Geen notities voor deze slide.': 'No notes for this slide.', + 'Wissel scherm (S)': 'Switch screen (S)', + 'Kon niet van scherm wisselen.': 'Could not switch screens.', 'P publiek · G overzicht · B/W zwart/wit · R tijd · Esc stop': 'P audience · G overview · B/W black/white · R time · Esc stop', + 'P publiek · S scherm · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P audience · S screen · G overview · B/W black/white · R time · Esc stop', + 'P publiek · H legenda · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P audience · H legend · G overview · B/W black/white · R time · Esc stop', + 'P publiek · H legenda · S scherm · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P audience · H legend · S screen · G overview · B/W black/white · R time · Esc stop', 'pijltjes + Enter of klik om te springen': 'arrows + Enter or click to jump', 'Afsluiten (Escape)': 'Exit (Escape)', @@ -1033,12 +1239,35 @@ const _dutchSourceStrings = { 'Gekopieerd': 'Copiato', 'Afbeelding verwijderen?': 'Eliminare immagine?', 'Sneltoetsen': 'Scorciatoie da tastiera', + 'Toetsenlegenda': 'Legenda tasti', + 'spatie': 'spazio', + 'klik': 'clic', + 'cijfers': 'numeri', + 'Klik of druk op H / Esc om te sluiten': + 'Fai clic o premi H / Esc per chiudere', + 'Naar slidenummer': 'Vai al numero slide', + 'Eerste · laatste slide': 'Prima · ultima slide', 'Slide-overzicht': 'Panoramica slide', + 'Slide-overzicht (pijltjes + Enter)': 'Panoramica slide (frecce + Enter)', + 'Presenter view (notities, klok)': 'Vista relatore (note, orologio)', + 'Scherm wisselen (meerdere schermen)': 'Cambia schermo (piu schermi)', + 'Zwart · wit scherm': 'Schermo nero · bianco', + 'Verstreken tijd resetten': 'Reimposta tempo trascorso', + 'Automatische modus aan/uit': 'Modalita automatica on/off', + 'Herhalen (loop) aan/uit': 'Ripetizione (loop) on/off', + 'Na audio automatisch doorgaan': 'Avanza automaticamente dopo audio', + 'Deze legenda': 'Questa legenda', + 'Terug / afsluiten': 'Indietro / esci', 'HUIDIGE SLIDE': 'SLIDE ATTUALE', 'VOLGENDE': 'PROSSIMA', 'NOTITIES': 'NOTE', 'Verstreken': 'Trascorso', 'Klok': 'Orologio', + 'Wissel scherm (S)': 'Cambia schermo (S)', + 'P publiek · H legenda · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P pubblico · H legenda · G panoramica · B/W nero/bianco · R tempo · Esc stop', + 'P publiek · H legenda · S scherm · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P pubblico · H legenda · S schermo · G panoramica · B/W nero/bianco · R tempo · Esc stop', }, 'de': { 'Geen': 'Keine', @@ -1158,12 +1387,36 @@ const _dutchSourceStrings = { 'Gekopieerd': 'Kopiert', 'Afbeelding verwijderen?': 'Bild löschen?', 'Sneltoetsen': 'Tastenkürzel', + 'Toetsenlegenda': 'Tastenlegende', + 'spatie': 'Leertaste', + 'klik': 'Klick', + 'cijfers': 'Zahlen', + 'Klik of druk op H / Esc om te sluiten': + 'Klicken oder H / Esc drücken zum Schließen', + 'Naar slidenummer': 'Zu Foliennummer', + 'Eerste · laatste slide': 'Erste · letzte Folie', 'Slide-overzicht': 'Folienübersicht', + 'Slide-overzicht (pijltjes + Enter)': 'Folienübersicht (Pfeile + Enter)', + 'Presenter view (notities, klok)': 'Presenter-Ansicht (Notizen, Uhr)', + 'Scherm wisselen (meerdere schermen)': + 'Bildschirm wechseln (mehrere Bildschirme)', + 'Zwart · wit scherm': 'Schwarzer · weißer Bildschirm', + 'Verstreken tijd resetten': 'Vergangene Zeit zurücksetzen', + 'Automatische modus aan/uit': 'Automatikmodus ein/aus', + 'Herhalen (loop) aan/uit': 'Wiederholen (Loop) ein/aus', + 'Na audio automatisch doorgaan': 'Nach Audio automatisch weiter', + 'Deze legenda': 'Diese Legende', + 'Terug / afsluiten': 'Zurück / beenden', 'HUIDIGE SLIDE': 'AKTUELLE FOLIE', 'VOLGENDE': 'NÄCHSTE', 'NOTITIES': 'NOTIZEN', 'Verstreken': 'Vergangen', 'Klok': 'Uhr', + 'Wissel scherm (S)': 'Bildschirm wechseln (S)', + 'P publiek · H legenda · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P Publikum · H Legende · G Übersicht · B/W schwarz/weiß · R Zeit · Esc Stopp', + 'P publiek · H legenda · S scherm · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P Publikum · H Legende · S Bildschirm · G Übersicht · B/W schwarz/weiß · R Zeit · Esc Stopp', }, 'fr': { 'Geen': 'Aucun', @@ -1283,12 +1536,35 @@ const _dutchSourceStrings = { 'Gekopieerd': 'Copié', 'Afbeelding verwijderen?': 'Supprimer l’image ?', 'Sneltoetsen': 'Raccourcis clavier', + 'Toetsenlegenda': 'Legende des touches', + 'spatie': 'espace', + 'klik': 'clic', + 'cijfers': 'chiffres', + 'Klik of druk op H / Esc om te sluiten': + 'Cliquez ou appuyez sur H / Esc pour fermer', + 'Naar slidenummer': 'Aller au numero de diapositive', + 'Eerste · laatste slide': 'Premiere · derniere diapositive', 'Slide-overzicht': 'Vue d’ensemble', + 'Slide-overzicht (pijltjes + Enter)': 'Vue d’ensemble (fleches + Entree)', + 'Presenter view (notities, klok)': 'Vue presentateur (notes, horloge)', + 'Scherm wisselen (meerdere schermen)': 'Changer d’ecran (plusieurs ecrans)', + 'Zwart · wit scherm': 'Ecran noir · blanc', + 'Verstreken tijd resetten': 'Reinitialiser le temps ecoule', + 'Automatische modus aan/uit': 'Mode automatique active/desactive', + 'Herhalen (loop) aan/uit': 'Boucle activee/desactivee', + 'Na audio automatisch doorgaan': 'Avancer automatiquement apres l’audio', + 'Deze legenda': 'Cette legende', + 'Terug / afsluiten': 'Retour / quitter', 'HUIDIGE SLIDE': 'DIAPOSITIVE ACTUELLE', 'VOLGENDE': 'SUIVANTE', 'NOTITIES': 'NOTES', 'Verstreken': 'Écoulé', 'Klok': 'Horloge', + 'Wissel scherm (S)': 'Changer d’ecran (S)', + 'P publiek · H legenda · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P public · H legende · G vue d’ensemble · B/W noir/blanc · R temps · Esc arret', + 'P publiek · H legenda · S scherm · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P public · H legende · S ecran · G vue d’ensemble · B/W noir/blanc · R temps · Esc arret', }, 'es': { 'Geen': 'Ninguno', @@ -1408,11 +1684,334 @@ const _dutchSourceStrings = { 'Gekopieerd': 'Copiado', 'Afbeelding verwijderen?': '¿Eliminar imagen?', 'Sneltoetsen': 'Atajos de teclado', + 'Toetsenlegenda': 'Leyenda de teclas', + 'spatie': 'espacio', + 'klik': 'clic', + 'cijfers': 'números', + 'Klik of druk op H / Esc om te sluiten': + 'Haz clic o pulsa H / Esc para cerrar', + 'Naar slidenummer': 'Ir al número de diapositiva', + 'Eerste · laatste slide': 'Primera · última diapositiva', 'Slide-overzicht': 'Vista general', + 'Slide-overzicht (pijltjes + Enter)': 'Vista general (flechas + Enter)', + 'Presenter view (notities, klok)': 'Vista de presentador (notas, reloj)', + 'Scherm wisselen (meerdere schermen)': + 'Cambiar pantalla (varias pantallas)', + 'Zwart · wit scherm': 'Pantalla negra · blanca', + 'Verstreken tijd resetten': 'Restablecer tiempo transcurrido', + 'Automatische modus aan/uit': 'Modo automático activar/desactivar', + 'Herhalen (loop) aan/uit': 'Repetir (bucle) activar/desactivar', + 'Na audio automatisch doorgaan': 'Avanzar automáticamente tras el audio', + 'Deze legenda': 'Esta leyenda', + 'Terug / afsluiten': 'Volver / salir', 'HUIDIGE SLIDE': 'DIAPOSITIVA ACTUAL', 'VOLGENDE': 'SIGUIENTE', 'NOTITIES': 'NOTAS', 'Verstreken': 'Transcurrido', 'Klok': 'Reloj', + 'Wissel scherm (S)': 'Cambiar pantalla (S)', + 'P publiek · H legenda · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P público · H leyenda · G vista general · B/W negro/blanco · R tiempo · Esc detener', + 'P publiek · H legenda · S scherm · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P público · H leyenda · S pantalla · G vista general · B/W negro/blanco · R tiempo · Esc detener', + }, + 'fy': { + 'Geen': 'Gjin', + 'Nieuw': 'Nij', + 'Verwijderen': 'Fuortsmite', + 'Herstellen': 'Weromsette', + 'Opslaan en sluiten': 'Bewarje en slute', + 'Importeren via URL': 'Ymportearje fan URL', + 'Ophalen': 'Ophelje', + 'Titelpagina': 'Titelslide', + 'Tussentitel': 'Tuskenkop', + 'Alleen Bullets': 'Allinnich bullets', + 'Twee Bulletkolommen': 'Twa bulletkolommen', + 'Bullets + Afbeelding': 'Bullets + Ofbylding', + 'Twee Afbeeldingen': 'Twa ôfbyldings', + 'Grote Afbeelding': 'Grutte ôfbylding', + 'Tabel': 'Tabel', + 'Vrije Markdown': 'Frije Markdown', + 'Overgeslagen': 'Oerslein', + 'Kopiëren': 'Kopiearje', + 'Kopieer als afbeelding': 'Kopiearje as ôfbylding', + 'Dupliceren': 'Duplisearje', + 'Niet meer overslaan': 'Net mear oerslaan', + 'Overslaan': 'Oerslaan', + 'Titel': 'Titel', + 'Titel (optioneel)': 'Titel (opsjoneel)', + 'Slide titel': 'Slidetitel', + 'Ondertitel': 'Undertitel', + 'Optionele subtitel': 'Opsjonele subtitel', + 'Bullets': 'Bullets', + 'Bullet toevoegen': 'Bullet tafoegje', + 'Verwijder': 'Fuortsmite', + 'Citaat': 'Sitaat', + 'Auteur': 'Auteur', + 'Achtergrondafbeelding': 'Eftergrûnôfbylding', + 'Achtergrondafbeelding (optioneel)': 'Eftergrûnôfbylding (opsjoneel)', + 'Zoom achtergrond': 'Eftergrûn zoom', + 'Zoom afbeelding': 'Ofbylding zoom', + 'Afbeelding (rechts)': 'Ofbylding (rjochts)', + 'Bullets (links)': 'Bullets (lofts)', + 'Linker afbeelding': 'Linker ôfbylding', + 'Rechter afbeelding': 'Rjochter ôfbylding', + 'Audio bij deze slide': 'Audio by dizze slide', + 'Audio automatisch afspelen': 'Audio automatysk ôfspylje', + 'Video automatisch afspelen': 'Fideo automatysk ôfspylje', + 'Geen audiobestand gekozen': 'Gjin audiobestân keazen', + 'Geen video gekozen': 'Gjin fideo keazen', + 'Kiezen': 'Kieze', + 'Uit bibliotheek…': 'Ut bibleteek…', + 'Van computer…': 'Fan kompjûter…', + 'Geen afbeelding gekozen': 'Gjin ôfbylding keazen', + 'Caption / bronvermelding': 'Byskrift / boarne', + 'Beschrijving (doorzoekbaar)': 'Beskriuwing (trochsykber)', + 'Markdown inhoud': 'Markdown-ynhâld', + 'Rij toevoegen': 'Rige tafoegje', + 'Kolom toevoegen': 'Kolom tafoegje', + 'Kolom': 'Kolom', + 'Presentatie openen': 'Presintaasje iepenje', + 'Opslaan als': 'Bewarje as', + 'Pakket importeren': 'Pakket ymportearje', + 'Pakket exporteren': 'Pakket eksportearje', + 'Bladeren…': 'Blêdzje…', + 'Geen map gekozen': 'Gjin map keazen', + 'Map kiezen': 'Map kieze', + 'Slide zoeken': 'Slide sykje', + 'Slides importeren': 'Slides ymportearje', + 'Importeren': 'Ymportearje', + 'Klaar': 'Klear', + 'Toevoegen': 'Tafoegje', + 'Toegevoegd': 'Tafoege', + 'Selecteer alles': 'Alles selektearje', + 'Deselecteer alles': 'Alles deselektearje', + 'Zoeken en vervangen': 'Sykje en ferfange', + 'Zoeken naar': 'Sykje nei', + 'Vervangen door': 'Ferfange troch', + 'Hoofdlettergevoelig': 'Haadlettergefoelich', + 'Vervang alles': 'Alles ferfange', + 'Nieuwe presentatie': 'Nije presintaasje', + 'Aanmaken': 'Oanmeitsje', + 'Slide type kiezen': 'Slidetype kieze', + 'Presentatie-eigenschappen': 'Presintaasje-eigenskippen', + 'Versie': 'Ferzje', + 'Organisatie': 'Organisaasje', + 'Datum': 'Datum', + 'Beschrijving': 'Beskriuwing', + 'Trefwoorden': 'Trefwurden', + 'Profielnaam': 'Profylnamme', + 'Stijlprofiel': 'Stylprofyl', + 'Lettertype': 'Lettertype', + 'Kleuren': 'Kleuren', + 'Tekst': 'Tekst', + 'Logo': 'Logo', + 'Geen logo ingesteld': 'Gjin logo ynsteld', + 'Logo positie': 'Logoposysje', + 'Linksboven': 'Loftsboppe', + 'Rechtsboven': 'Rjochtsboppe', + 'Linksonder': 'Loftsûnder', + 'Rechtsonder': 'Rjochtsûnder', + 'Footertekst': 'Footer-tekst', + 'Footerpositie': 'Footer-posysje', + 'Links': 'Lofts', + 'Midden': 'Midden', + 'Rechts': 'Rjochts', + 'Voorvertoning': 'Foarbyld', + 'Preview': 'Foarbyld', + 'Uitzoomen': 'Utzoome', + 'Inzoomen': 'Ynzoome', + 'Zoom resetten': 'Zoom weromsette', + 'Vorige slide': 'Foarige slide', + 'Volgende slide': 'Folgjende slide', + 'Thema': 'Tema', + 'Afbeelding kiezen': 'Ofbylding kieze', + 'Afbeeldingen laden…': 'Ofbyldings lade…', + 'Sluiten (Esc)': 'Slute (Esc)', + 'Raster': 'Raster', + 'Geen afbeeldingen gevonden': 'Gjin ôfbyldings fûn', + 'Gekopieerd': 'Kopiearre', + 'Afbeelding verwijderen?': 'Ofbylding fuortsmite?', + 'Sneltoetsen': 'Fluchtoetsen', + 'Toetsenlegenda': 'Toetsleginda', + 'spatie': 'spaasje', + 'klik': 'klik', + 'cijfers': 'sifers', + 'Klik of druk op H / Esc om te sluiten': + 'Klik of druk op H / Esc om te sluten', + 'Naar slidenummer': 'Nei slidenûmer', + 'Eerste · laatste slide': 'Earste · lêste slide', + 'Slide-overzicht': 'Slide-oersjoch', + 'Slide-overzicht (pijltjes + Enter)': 'Slide-oersjoch (pylken + Enter)', + 'Presenter view (notities, klok)': 'Presenter view (notysjes, klok)', + 'Scherm wisselen (meerdere schermen)': 'Skerm wikselje (mear skermen)', + 'Zwart · wit scherm': 'Swart · wyt skerm', + 'Verstreken tijd resetten': 'Ferrûne tiid weromsette', + 'Automatische modus aan/uit': 'Automatyske modus oan/út', + 'Herhalen (loop) aan/uit': 'Werhelje (loop) oan/út', + 'Na audio automatisch doorgaan': 'Nei audio automatysk trochgean', + 'Deze legenda': 'Dizze leginda', + 'Terug / afsluiten': 'Werom / ôfslute', + 'HUIDIGE SLIDE': 'AKTUELE SLIDE', + 'VOLGENDE': 'FOLGJENDE', + 'NOTITIES': 'NOTYSJES', + 'Verstreken': 'Ferrûn', + 'Klok': 'Klok', + 'Geen notities voor deze slide.': 'Gjin notysjes foar dizze slide.', + 'Wissel scherm (S)': 'Skerm wikselje (S)', + 'Kon niet van scherm wisselen.': 'Koe net fan skerm wikselje.', + 'P publiek · H legenda · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P publyk · H leginda · G oersjoch · B/W swart/wyt · R tiid · Esc stop', + 'P publiek · H legenda · S scherm · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P publyk · H leginda · S skerm · G oersjoch · B/W swart/wyt · R tiid · Esc stop', + }, + 'pap': { + 'Geen': 'Ningun', + 'Nieuw': 'Nobo', + 'Verwijderen': 'Kita', + 'Herstellen': 'Restorá', + 'Opslaan en sluiten': 'Warda i sera', + 'Importeren via URL': 'Importá for di URL', + 'Ophalen': 'Tuma', + 'Titelpagina': 'Slide di título', + 'Tussentitel': 'Título di sekshon', + 'Alleen Bullets': 'Solamente bullets', + 'Twee Bulletkolommen': 'Dos kolòm di bullets', + 'Bullets + Afbeelding': 'Bullets + Imágen', + 'Twee Afbeeldingen': 'Dos imágen', + 'Grote Afbeelding': 'Imágen grandi', + 'Tabel': 'Tabel', + 'Vrije Markdown': 'Markdown liber', + 'Overgeslagen': 'Saltá', + 'Kopiëren': 'Kopia', + 'Kopieer als afbeelding': 'Kopia komo imágen', + 'Dupliceren': 'Dupliká', + 'Niet meer overslaan': 'No salta mas', + 'Overslaan': 'Salta', + 'Titel': 'Título', + 'Titel (optioneel)': 'Título (opshonal)', + 'Slide titel': 'Título di slide', + 'Ondertitel': 'Subtítulo', + 'Optionele subtitel': 'Subtítulo opshonal', + 'Bullets': 'Bullets', + 'Bullet toevoegen': 'Añadí bullet', + 'Verwijder': 'Kita', + 'Citaat': 'Sitá', + 'Auteur': 'Outor', + 'Achtergrondafbeelding': 'Imágen di fondo', + 'Achtergrondafbeelding (optioneel)': 'Imágen di fondo (opshonal)', + 'Zoom achtergrond': 'Zoom di fondo', + 'Zoom afbeelding': 'Zoom di imágen', + 'Afbeelding (rechts)': 'Imágen (banda drechi)', + 'Bullets (links)': 'Bullets (banda robes)', + 'Linker afbeelding': 'Imágen robes', + 'Rechter afbeelding': 'Imágen drechi', + 'Audio bij deze slide': 'Audio pa e slide aki', + 'Audio automatisch afspelen': 'Toka audio outomátiko', + 'Video automatisch afspelen': 'Toka video outomátiko', + 'Geen audiobestand gekozen': 'Ningun file di audio skohe', + 'Geen video gekozen': 'Ningun video skohe', + 'Kiezen': 'Skohe', + 'Uit bibliotheek…': 'For di biblioteka…', + 'Van computer…': 'For di kòmpiuter…', + 'Geen afbeelding gekozen': 'Ningun imágen skohe', + 'Caption / bronvermelding': 'Caption / fuente', + 'Beschrijving (doorzoekbaar)': 'Deskripshon (buskabel)', + 'Markdown inhoud': 'Kontenido Markdown', + 'Rij toevoegen': 'Añadí rei', + 'Kolom toevoegen': 'Añadí kolòm', + 'Kolom': 'Kolòm', + 'Presentatie openen': 'Habri presentashon', + 'Opslaan als': 'Warda komo', + 'Pakket importeren': 'Importá pakete', + 'Pakket exporteren': 'Eksportá pakete', + 'Bladeren…': 'Navegá…', + 'Geen map gekozen': 'Ningun folder skohe', + 'Map kiezen': 'Skohe folder', + 'Slide zoeken': 'Busca slide', + 'Slides importeren': 'Importá slides', + 'Importeren': 'Importá', + 'Klaar': 'Kla', + 'Toevoegen': 'Añadí', + 'Toegevoegd': 'Añadí', + 'Selecteer alles': 'Selektá tur', + 'Deselecteer alles': 'Deselektá tur', + 'Zoeken en vervangen': 'Busca i reemplasá', + 'Zoeken naar': 'Busca', + 'Vervangen door': 'Reemplasá ku', + 'Hoofdlettergevoelig': 'Sensitivo pa mayúskula', + 'Vervang alles': 'Reemplasá tur', + 'Nieuwe presentatie': 'Presentashon nobo', + 'Aanmaken': 'Krea', + 'Slide type kiezen': 'Skohe tipo di slide', + 'Presentatie-eigenschappen': 'Propiedatnan di presentashon', + 'Versie': 'Vershon', + 'Organisatie': 'Organisashon', + 'Datum': 'Fecha', + 'Beschrijving': 'Deskripshon', + 'Trefwoorden': 'Palabranan klave', + 'Profielnaam': 'Nòmber di perfil', + 'Stijlprofiel': 'Perfil di estilo', + 'Lettertype': 'Font', + 'Kleuren': 'Kolónan', + 'Tekst': 'Teksto', + 'Logo': 'Logo', + 'Geen logo ingesteld': 'Ningun logo konfigurá', + 'Logo positie': 'Posishon di logo', + 'Linksboven': 'Ariba robes', + 'Rechtsboven': 'Ariba drechi', + 'Linksonder': 'Abou robes', + 'Rechtsonder': 'Abou drechi', + 'Footertekst': 'Teksto di footer', + 'Footerpositie': 'Posishon di footer', + 'Links': 'Robes', + 'Midden': 'Meimei', + 'Rechts': 'Drechi', + 'Voorvertoning': 'Preview', + 'Preview': 'Preview', + 'Uitzoomen': 'Zoom out', + 'Inzoomen': 'Zoom in', + 'Zoom resetten': 'Reset zoom', + 'Vorige slide': 'Slide anterior', + 'Volgende slide': 'Siguiente slide', + 'Thema': 'Tema', + 'Afbeelding kiezen': 'Skohe imágen', + 'Afbeeldingen laden…': 'Kargando imágennan…', + 'Sluiten (Esc)': 'Sera (Esc)', + 'Raster': 'Grid', + 'Geen afbeeldingen gevonden': 'No a haña imágen', + 'Gekopieerd': 'Kopiá', + 'Afbeelding verwijderen?': 'Kita imágen?', + 'Sneltoetsen': 'Atahonan di tekla', + 'Toetsenlegenda': 'Legenda di tekla', + 'spatie': 'spasio', + 'klik': 'klik', + 'cijfers': 'numbernan', + 'Klik of druk op H / Esc om te sluiten': 'Klik òf primi H / Esc pa sera', + 'Naar slidenummer': 'Bai na number di slide', + 'Eerste · laatste slide': 'Promé · último slide', + 'Slide-overzicht': 'Resumen di slides', + 'Slide-overzicht (pijltjes + Enter)': 'Resumen di slides (flecha + Enter)', + 'Presenter view (notities, klok)': 'Presenter view (notanan, oloshi)', + 'Scherm wisselen (meerdere schermen)': 'Kambia pantalla (mas pantalla)', + 'Zwart · wit scherm': 'Pantalla pretu · blanku', + 'Verstreken tijd resetten': 'Reset tempu transkurí', + 'Automatische modus aan/uit': 'Modo outomátiko on/off', + 'Herhalen (loop) aan/uit': 'Ripití (loop) on/off', + 'Na audio automatisch doorgaan': 'Sigui outomátiko despues di audio', + 'Deze legenda': 'E legenda aki', + 'Terug / afsluiten': 'Bèk / sali', + 'HUIDIGE SLIDE': 'SLIDE AKTUAL', + 'VOLGENDE': 'SIGUIENTE', + 'NOTITIES': 'NOTANAN', + 'Verstreken': 'Transkurí', + 'Klok': 'Oloshi', + 'Geen notities voor deze slide.': 'No tin nota pa e slide aki.', + 'Wissel scherm (S)': 'Kambia pantalla (S)', + 'Kon niet van scherm wisselen.': 'No por a kambia pantalla.', + 'P publiek · H legenda · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P públiko · H legenda · G resumen · B/W pretu/blanku · R tempu · Esc stop', + 'P publiek · H legenda · S scherm · G overzicht · B/W zwart/wit · R tijd · Esc stop': + 'P públiko · H legenda · S pantalla · G resumen · B/W pretu/blanku · R tempu · Esc stop', }, }; diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart index 82cc75c..2fb047a 100644 --- a/lib/widgets/app_shell.dart +++ b/lib/widgets/app_shell.dart @@ -1184,13 +1184,6 @@ class _MainLayoutState extends ConsumerState<_MainLayout> { onPressed: saveDeck, ), ), - Tooltip( - message: exportTooltip, - child: IconButton( - icon: const Icon(Icons.upload_file_outlined, size: 18), - onPressed: canExport ? exportDeck : null, - ), - ), const _ActionsDivider(), // ── Overig (minder vaak gebruikt) ─────────────────────────── PopupMenuButton( diff --git a/lib/widgets/presentation/fullscreen_presenter.dart b/lib/widgets/presentation/fullscreen_presenter.dart index 15ebf6b..149d334 100644 --- a/lib/widgets/presentation/fullscreen_presenter.dart +++ b/lib/widgets/presentation/fullscreen_presenter.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:screen_retriever/screen_retriever.dart'; import 'package:window_manager/window_manager.dart'; import '../../models/deck.dart'; import '../../models/settings.dart'; @@ -107,6 +108,12 @@ class _FullscreenPresenterState extends State { /// Met M te wisselen. bool _advanceOnAudioEnd = true; + /// Known displays for moving the fullscreen presentation window. This is not + /// a second presenter window; it keeps the current output movable between + /// screens with S or the presenter-view button. + List _displays = const []; + int _displayIndex = 0; + @override void initState() { super.initState(); @@ -119,6 +126,7 @@ class _FullscreenPresenterState extends State { }); WidgetsBinding.instance.addPostFrameCallback((_) { _focusNode.requestFocus(); + _loadDisplays(); _scheduleAdvance(); }); } @@ -209,6 +217,55 @@ class _FullscreenPresenterState extends State { _scheduleAdvance(); } + Future _loadDisplays() async { + try { + final displays = await screenRetriever.getAllDisplays(); + if (!mounted || displays.isEmpty) return; + final bounds = await windowManager.getBounds(); + final center = bounds.center; + final current = displays.indexWhere((d) { + final p = d.visiblePosition ?? Offset.zero; + final s = d.visibleSize ?? d.size; + return Rect.fromLTWH(p.dx, p.dy, s.width, s.height).contains(center); + }); + setState(() { + _displays = displays; + _displayIndex = current < 0 ? 0 : current; + }); + } catch (_) { + // Screen detection is best-effort; presenting should still work. + } + } + + Future _moveToDisplay(int index) async { + if (_displays.length < 2) return; + final display = _displays[index.clamp(0, _displays.length - 1)]; + final position = display.visiblePosition ?? Offset.zero; + final size = display.visibleSize ?? display.size; + try { + await windowManager.setFullScreen(false); + await windowManager.setBounds( + Rect.fromLTWH(position.dx, position.dy, size.width, size.height), + ); + await windowManager.setFullScreen(true); + if (mounted) setState(() => _displayIndex = index); + } catch (_) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.d('Kon niet van scherm wisselen.')), + ), + ); + } + } + } + + Future _cycleDisplay() async { + if (_displays.isEmpty) await _loadDisplays(); + if (_displays.length < 2) return; + await _moveToDisplay((_displayIndex + 1) % _displays.length); + } + Future _exit() async { _advanceTimer?.cancel(); await windowManager.setFullScreen(false); @@ -452,6 +509,9 @@ class _FullscreenPresenterState extends State { case LogicalKeyboardKey.keyM: _toggleAudioAdvance(); return KeyEventResult.handled; + case LogicalKeyboardKey.keyS: + _cycleDisplay(); + return KeyEventResult.handled; case LogicalKeyboardKey.escape: // Gelaagd: getypt nummer wissen, dan blanco scherm, dan pas afsluiten. if (_typed.isNotEmpty) { @@ -587,12 +647,13 @@ class _FullscreenPresenterState extends State { ('Home · End', l10n.d('Eerste · laatste slide')), ('G', l10n.d('Slide-overzicht (pijltjes + Enter)')), ('P', l10n.d('Presenter view (notities, klok)')), + ('S', l10n.d('Scherm wisselen (meerdere schermen)')), ('B · W', l10n.d('Zwart · wit scherm')), ('R', l10n.d('Verstreken tijd resetten')), ('A', l10n.d('Automatische modus aan/uit')), ('L', l10n.d('Herhalen (loop) aan/uit')), ('M', l10n.d('Na audio automatisch doorgaan')), - ('? · H', l10n.d('Dit overzicht')), + ('H', l10n.d('Deze legenda')), ('Esc', l10n.d('Terug / afsluiten')), ]; return GestureDetector( @@ -627,7 +688,7 @@ class _FullscreenPresenterState extends State { ), const SizedBox(width: 10), Text( - l10n.d('Sneltoetsen'), + l10n.d('Toetsenlegenda'), style: const TextStyle( color: Colors.white, fontSize: 18, @@ -668,7 +729,7 @@ class _FullscreenPresenterState extends State { const SizedBox(height: 16), Center( child: Text( - l10n.d('Klik of druk op ? / H / Esc om te sluiten'), + l10n.d('Klik of druk op H / Esc om te sluiten'), style: const TextStyle( color: Colors.white30, fontSize: 12, @@ -684,44 +745,6 @@ class _FullscreenPresenterState extends State { ); } - /// Subtiele statusindicator (linksonder) voor de automatische modus. Toont - /// of auto-play, herhalen en 'na audio doorgaan' actief zijn. - Widget _autoPlayStatus() { - final l10n = context.l10n; - final items = <(IconData, String, bool)>[ - ( - _autoPlay ? Icons.play_circle_outline : Icons.pause_circle_outline, - _autoPlay ? l10n.d('Auto (A)') : l10n.d('Handmatig (A)'), - _autoPlay, - ), - (Icons.repeat, l10n.d('Herhalen (L)'), _loop), - (Icons.graphic_eq, l10n.d('Na audio (M)'), _advanceOnAudioEnd), - ]; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - for (final (icon, label, active) in items) - Padding( - padding: const EdgeInsets.only(right: 12), - child: Opacity( - opacity: active ? 0.7 : 0.28, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 14, color: Colors.white), - const SizedBox(width: 4), - Text( - label, - style: const TextStyle(color: Colors.white, fontSize: 11), - ), - ], - ), - ), - ), - ], - ); - } - /// Vol-vlak zwart/wit scherm dat met een klik weer verdwijnt. Widget _blankFill() { return GestureDetector( @@ -783,148 +806,7 @@ class _FullscreenPresenterState extends State { return GestureDetector( onTap: _next, onSecondaryTap: _prev, - child: Stack( - children: [ - // ── Slide canvas ───────────────────────────────────────────────── - Positioned.fill(child: _slideCanvas(slide)), - - // ── Voortgangsbalk (auto-advance) ──────────────────────────────── - if (_progress > 0) - Positioned( - bottom: 0, - left: 0, - right: 0, - child: LinearProgressIndicator( - value: _progress, - backgroundColor: Colors.white12, - color: Colors.white54, - minHeight: 3, - ), - ), - - // ── Slide counter ──────────────────────────────────────────────── - Positioned( - right: 24, - bottom: 10, - child: Text( - '${_index + 1} / $total', - style: const TextStyle( - color: Colors.white38, - fontSize: 13, - fontWeight: FontWeight.w500, - ), - ), - ), - - // ── Auto-play status (linksonder) ──────────────────────────────── - Positioned(left: 24, bottom: 10, child: _autoPlayStatus()), - - // ── Navigation arrows ──────────────────────────────────────────── - Positioned( - left: 0, - top: 0, - bottom: 0, - child: MouseRegion( - cursor: _index > 0 ? SystemMouseCursors.click : MouseCursor.defer, - child: GestureDetector( - onTap: _prev, - child: Container( - width: 60, - color: Colors.transparent, - child: _index > 0 - ? const Icon( - Icons.chevron_left, - color: Colors.white24, - size: 40, - ) - : null, - ), - ), - ), - ), - Positioned( - right: 0, - top: 0, - bottom: 0, - child: MouseRegion( - cursor: _index < total - 1 - ? SystemMouseCursors.click - : MouseCursor.defer, - child: GestureDetector( - onTap: _next, - child: Container( - width: 60, - color: Colors.transparent, - child: _index < total - 1 - ? const Icon( - Icons.chevron_right, - color: Colors.white24, - size: 40, - ) - : null, - ), - ), - ), - ), - - // ── Top-right controls (presenter view + afsluiten) ────────────── - Positioned( - top: 16, - right: 16, - child: Row( - children: [ - Tooltip( - message: context.l10n.d('Sneltoetsen (?)'), - child: IconButton( - onPressed: _toggleHelp, - icon: const Icon(Icons.help_outline), - color: Colors.white, - style: IconButton.styleFrom( - backgroundColor: Colors.black45, - ), - ), - ), - const SizedBox(width: 8), - Tooltip( - message: context.l10n.d('Slide-overzicht (G)'), - child: IconButton( - onPressed: _toggleGrid, - icon: const Icon(Icons.grid_view_rounded), - color: Colors.white, - style: IconButton.styleFrom( - backgroundColor: Colors.black45, - ), - ), - ), - const SizedBox(width: 8), - Tooltip( - message: context.l10n.d('Presenter view (P)'), - child: IconButton( - onPressed: _togglePresenterView, - icon: const Icon(Icons.co_present_outlined), - color: Colors.white, - style: IconButton.styleFrom( - backgroundColor: Colors.black45, - ), - ), - ), - const SizedBox(width: 8), - Tooltip( - message: context.l10n.d('Afsluiten (Escape)'), - child: IconButton( - onPressed: _exit, - icon: const Icon(Icons.close), - color: Colors.white, - style: IconButton.styleFrom( - backgroundColor: Colors.black45, - ), - ), - ), - ], - ), - ), - ], - ), + child: SizedBox.expand(child: _slideCanvas(slide)), ); } @@ -1151,6 +1033,16 @@ class _FullscreenPresenterState extends State { icon: Icons.chevron_right, onTap: _index < total - 1 ? _next : null, ), + if (_displays.length > 1) ...[ + const SizedBox(width: 8), + Tooltip( + message: l10n.d('Wissel scherm (S)'), + child: _NavButton( + icon: Icons.screen_share_outlined, + onTap: _cycleDisplay, + ), + ), + ], const SizedBox(width: 16), Text( '${l10n.d('Slide')} ${_index + 1} / $total', @@ -1164,7 +1056,9 @@ class _FullscreenPresenterState extends State { Expanded( child: Text( l10n.d( - 'P publiek · G overzicht · B/W zwart/wit · R tijd · Esc stop', + _displays.length > 1 + ? 'P publiek · H legenda · S scherm · G overzicht · B/W zwart/wit · R tijd · Esc stop' + : 'P publiek · H legenda · G overzicht · B/W zwart/wit · R tijd · Esc stop', ), textAlign: TextAlign.right, maxLines: 1, diff --git a/pubspec.lock b/pubspec.lock index 64213f4..2718a22 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -654,7 +654,7 @@ packages: source: hosted version: "3.2.1" screen_retriever: - dependency: transitive + dependency: "direct main" description: name: screen_retriever sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" diff --git a/pubspec.yaml b/pubspec.yaml index dbc5540..2c8671d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: path_provider: ^2.1.5 path: ^1.9.1 uuid: ^4.5.1 + screen_retriever: ^0.2.0 window_manager: ^0.5.1 shared_preferences: ^2.3.3 pasteboard: ^0.5.0 diff --git a/test/app_localizations_test.dart b/test/app_localizations_test.dart new file mode 100644 index 0000000..c6de210 --- /dev/null +++ b/test/app_localizations_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:ocideck/l10n/app_localizations.dart'; + +void main() { + tearDown(() => AppLocalizations.setActiveLanguageCode('nl')); + + test('supports Frisian and Papiamento language choices', () { + expect(AppLocalizations.languageNames['fy'], 'Frysk'); + expect(AppLocalizations.languageNames['pap'], 'Papiamento'); + expect(AppLocalizations.supportedLocales, contains(const Locale('fy'))); + expect(AppLocalizations.supportedLocales, contains(const Locale('pap'))); + }); + + test('uses app translations while Material falls back safely', () { + AppLocalizations.setActiveLanguageCode('fy'); + expect(const AppLocalizations(Locale('en')).t('settings'), 'Ynstellingen'); + expect( + const AppLocalizations(Locale('en')).d('Toetsenlegenda'), + 'Toetsleginda', + ); + expect(AppLocalizations.materialLocaleFor('fy'), const Locale('en')); + + AppLocalizations.setActiveLanguageCode('pap'); + expect( + const AppLocalizations(Locale('en')).t('settings'), + 'Preferensianan', + ); + expect( + const AppLocalizations(Locale('en')).d('Toetsenlegenda'), + 'Legenda di tekla', + ); + expect(AppLocalizations.materialLocaleFor('pap'), const Locale('en')); + }); +} diff --git a/test/free_markdown_preview_test.dart b/test/free_markdown_preview_test.dart index dbe4044..f8230ea 100644 --- a/test/free_markdown_preview_test.dart +++ b/test/free_markdown_preview_test.dart @@ -29,9 +29,9 @@ void main() { }); testWidgets('free Markdown renders display math', (tester) async { - final slide = Slide.create(SlideType.freeMarkdown).copyWith( - customMarkdown: 'Stelling:\n\n\$\$E = mc^2\$\$\n', - ); + final slide = Slide.create( + SlideType.freeMarkdown, + ).copyWith(customMarkdown: 'Stelling:\n\n\$\$E = mc^2\$\$\n'); await tester.pumpWidget(_host(slide)); expect(find.byType(Math), findsOneWidget); @@ -40,9 +40,9 @@ void main() { testWidgets('an unknown code language falls back without throwing', ( tester, ) async { - final slide = Slide.create(SlideType.freeMarkdown).copyWith( - customMarkdown: '```nonexistentlang\nsome code\n```\n', - ); + final slide = Slide.create( + SlideType.freeMarkdown, + ).copyWith(customMarkdown: '```nonexistentlang\nsome code\n```\n'); await tester.pumpWidget(_host(slide)); // No HighlightView (unknown language) and no exception during build. diff --git a/test/fullscreen_presenter_test.dart b/test/fullscreen_presenter_test.dart index f2a45eb..f164670 100644 --- a/test/fullscreen_presenter_test.dart +++ b/test/fullscreen_presenter_test.dart @@ -30,7 +30,12 @@ void main() { await tester.pumpWidget(_host(slides)); await tester.pump(); - expect(find.text('1 / 2'), findsOneWidget); // slide counter + expect(find.text('Eerste'), findsOneWidget); + expect(find.text('1 / 2'), findsNothing); // no audience chrome + expect(find.byIcon(Icons.help_outline), findsNothing); + expect(find.byIcon(Icons.grid_view_rounded), findsNothing); + expect(find.byIcon(Icons.co_present_outlined), findsNothing); + expect(find.byIcon(Icons.close), findsNothing); expect(find.text('NOTITIES'), findsNothing); // presenter-only expect(find.text('VOLGENDE'), findsNothing); @@ -88,17 +93,17 @@ void main() { testWidgets('B blanks the audience screen and toggles back', (tester) async { await tester.pumpWidget(_host(slides)); await tester.pump(); - expect(find.text('1 / 2'), findsOneWidget); + expect(find.text('Eerste'), findsOneWidget); - // Blank to black: the slide (and its counter) disappears. + // Blank to black: the slide disappears. await tester.sendKeyEvent(LogicalKeyboardKey.keyB); await tester.pump(); - expect(find.text('1 / 2'), findsNothing); + expect(find.text('Eerste'), findsNothing); // Same key restores the slide. await tester.sendKeyEvent(LogicalKeyboardKey.keyB); await tester.pump(); - expect(find.text('1 / 2'), findsOneWidget); + expect(find.text('Eerste'), findsOneWidget); await tester.pumpWidget(const SizedBox()); }); @@ -111,12 +116,12 @@ void main() { await tester.sendKeyEvent(LogicalKeyboardKey.keyB); await tester.pump(); - expect(find.text('1 / 2'), findsNothing); + expect(find.text('Eerste'), findsNothing); // First arrow un-blanks without advancing (still on slide 1). await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pump(); - expect(find.text('1 / 2'), findsOneWidget); + expect(find.text('Eerste'), findsOneWidget); await tester.pumpWidget(const SizedBox()); }); @@ -142,7 +147,7 @@ void main() { // Grid closed and we jumped to slide 2. expect(find.text('Slide-overzicht'), findsNothing); - expect(find.text('2 / 2'), findsOneWidget); + expect(find.text('Tweede'), findsOneWidget); await tester.pumpWidget(const SizedBox()); }); @@ -152,15 +157,15 @@ void main() { ) async { await tester.pumpWidget(_host(slides)); await tester.pump(); - expect(find.text('1 / 2'), findsOneWidget); + expect(find.text('Eerste'), findsOneWidget); await tester.sendKeyEvent(LogicalKeyboardKey.end); await tester.pump(); - expect(find.text('2 / 2'), findsOneWidget); + expect(find.text('Tweede'), findsOneWidget); await tester.sendKeyEvent(LogicalKeyboardKey.home); await tester.pump(); - expect(find.text('1 / 2'), findsOneWidget); + expect(find.text('Eerste'), findsOneWidget); await tester.pumpWidget(const SizedBox()); }); @@ -187,7 +192,7 @@ void main() { await tester.pump(); expect(find.text('Slide-overzicht'), findsNothing); - expect(find.text('2 / 2'), findsOneWidget); + expect(find.text('Tweede'), findsOneWidget); await tester.pumpWidget(const SizedBox()); }); @@ -199,7 +204,7 @@ void main() { ]; await tester.pumpWidget(_host(three)); await tester.pump(); - expect(find.text('1 / 3'), findsOneWidget); + expect(find.text('Eerste'), findsOneWidget); // Type "3" → a badge appears, Enter jumps to slide 3. await tester.sendKeyEvent(LogicalKeyboardKey.digit3); @@ -208,8 +213,9 @@ void main() { await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pump(); - // Badge gone, now actually on slide 3 (counter shows it). - expect(find.text('3 / 3'), findsOneWidget); // slide counter now + // Badge gone, now actually on slide 3. + expect(find.text('Derde'), findsOneWidget); + expect(find.text('3 / 3'), findsNothing); expect(find.byIcon(Icons.south_east), findsNothing); // badge icon gone await tester.pumpWidget(const SizedBox()); @@ -218,17 +224,18 @@ void main() { testWidgets('? toggles the shortcut cheatsheet', (tester) async { await tester.pumpWidget(_host(slides)); await tester.pump(); - expect(find.text('Sneltoetsen'), findsNothing); + expect(find.text('Toetsenlegenda'), findsNothing); await tester.sendKeyEvent(LogicalKeyboardKey.keyH); await tester.pump(); - expect(find.text('Sneltoetsen'), findsOneWidget); + expect(find.text('Toetsenlegenda'), findsOneWidget); + expect(find.text('Scherm wisselen (meerdere schermen)'), findsOneWidget); await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); - expect(find.text('Sneltoetsen'), findsNothing); + expect(find.text('Toetsenlegenda'), findsNothing); // Esc closed the help, not the presentation. - expect(find.text('1 / 2'), findsOneWidget); + expect(find.text('Eerste'), findsOneWidget); await tester.pumpWidget(const SizedBox()); }); @@ -250,7 +257,7 @@ void main() { await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pump(); expect(find.text('Slide-overzicht'), findsNothing); - expect(find.text('1 / 2'), findsOneWidget); + expect(find.text('Eerste'), findsOneWidget); await tester.pumpWidget(const SizedBox()); });