diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..68a0f1f 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..592ac38 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..5a6a211 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..d5a7efb 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..ce737bc 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/images/logo-icon.png b/assets/images/logo-icon.png index b18d5ff..8046ffb 100644 Binary files a/assets/images/logo-icon.png and b/assets/images/logo-icon.png differ diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index c5724ae..5b7590e 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index e76dec8..6b3020c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 372c779..b527c31 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 2c5970f..96f0acd 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 72a3329..7f8ed97 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index f3bbb48..57d6629 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 92495c5..fa5fbc7 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 372c779..ec2707d 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 84bad53..ebf31a0 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index b4dd7da..4973293 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index b4dd7da..4973293 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index e14592b..10e1ee6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index d9ee7b2..f1f5a2c 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index fd3fde5..b6d0d96 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index d1e3192..1983851 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 8c8599b..12e8d2d 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -25,6 +25,11 @@ class ThemeProfile { /// Horizontale positie van de footer: left, center of right. final String footerPosition; + /// Optional markdown slide that is appended when presenting/exporting with + /// this theme profile. It stays out of the editable deck slide list. + final bool closingSlideEnabled; + final String closingSlideMarkdown; + const ThemeProfile({ this.name = 'Standaard', this.slideBackgroundColor = '#FFFFFF', @@ -42,6 +47,8 @@ class ThemeProfile { this.footerText = '', this.footerShowPageNumbers = false, this.footerPosition = 'right', + this.closingSlideEnabled = false, + this.closingSlideMarkdown = '# Bedankt\n\nVragen?', }) : tableTextColor = tableTextColor ?? textColor; static const logoPositions = [ @@ -70,6 +77,8 @@ class ThemeProfile { String? footerText, bool? footerShowPageNumbers, String? footerPosition, + bool? closingSlideEnabled, + String? closingSlideMarkdown, bool clearLogo = false, }) { return ThemeProfile( @@ -91,6 +100,8 @@ class ThemeProfile { footerShowPageNumbers: footerShowPageNumbers ?? this.footerShowPageNumbers, footerPosition: footerPosition ?? this.footerPosition, + closingSlideEnabled: closingSlideEnabled ?? this.closingSlideEnabled, + closingSlideMarkdown: closingSlideMarkdown ?? this.closingSlideMarkdown, ); } @@ -112,6 +123,8 @@ class ThemeProfile { 'footerText': footerText, 'footerShowPageNumbers': footerShowPageNumbers, 'footerPosition': footerPosition, + 'closingSlideEnabled': closingSlideEnabled, + 'closingSlideMarkdown': closingSlideMarkdown, }; } @@ -140,6 +153,9 @@ class ThemeProfile { footerText: json['footerText'] as String? ?? '', footerShowPageNumbers: json['footerShowPageNumbers'] as bool? ?? false, footerPosition: json['footerPosition'] as String? ?? 'right', + closingSlideEnabled: json['closingSlideEnabled'] as bool? ?? false, + closingSlideMarkdown: + json['closingSlideMarkdown'] as String? ?? '# Bedankt\n\nVragen?', ); } } diff --git a/lib/services/slide_rasterizer.dart b/lib/services/slide_rasterizer.dart index 7a47a15..ca300d5 100644 --- a/lib/services/slide_rasterizer.dart +++ b/lib/services/slide_rasterizer.dart @@ -34,6 +34,7 @@ class SlideRasterizer { TlpLevel tlp = TlpLevel.none, int targetWidth = 1920, void Function(int done, int total)? onProgress, + void Function(String phase, int done, int total)? onStage, }) async { final overlay = Overlay.of(context, rootOverlay: true); final pixelRatio = targetWidth / logicalSize.width; @@ -54,6 +55,7 @@ class SlideRasterizer { final results = []; try { for (var i = 0; i < slides.length; i++) { + onStage?.call('prepare', i, slides.length); // Warm this slide's images immediately before capturing it. Doing it // per slide (instead of once up front) guarantees the bitmap is decoded // and resident in the cache at capture time, no matter how many images @@ -66,6 +68,7 @@ class SlideRasterizer { ]); if (!context.mounted) break; + onStage?.call('render', i, slides.length); final key = GlobalKey(); final entry = OverlayEntry( builder: (_) => Positioned( @@ -96,6 +99,7 @@ class SlideRasterizer { } finally { entry.remove(); } + onStage?.call('done', i + 1, slides.length); onProgress?.call(i + 1, slides.length); } } finally { diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart index 2fb047a..0fd1e6f 100644 --- a/lib/widgets/app_shell.dart +++ b/lib/widgets/app_shell.dart @@ -138,6 +138,19 @@ List _imageUsages(WidgetRef ref, String absolutePath) { return usages; } +List _slidesForPresentationOrExport(Deck deck) { + final slides = deck.slides.where((s) => !s.skipped).toList(); + final closingMarkdown = deck.themeProfile.closingSlideMarkdown.trim(); + if (deck.themeProfile.closingSlideEnabled && closingMarkdown.isNotEmpty) { + slides.add( + Slide.create( + SlideType.freeMarkdown, + ).copyWith(customMarkdown: closingMarkdown), + ); + } + return slides; +} + // ── App shell ───────────────────────────────────────────────────────────────── class AppShell extends ConsumerStatefulWidget { @@ -904,7 +917,8 @@ class _MainLayoutState extends ConsumerState<_MainLayout> { for (var i = 0; i < deck.slides.length; i++) if (!deck.slides[i].skipped) i, ]; - if (visible.isEmpty) { + final slides = _slidesForPresentationOrExport(deck); + if (slides.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( @@ -916,9 +930,10 @@ class _MainLayoutState extends ConsumerState<_MainLayout> { } var initial = visible.indexWhere((i) => i >= editor.selectedIndex); if (initial < 0) initial = visible.length - 1; + if (initial < 0) initial = 0; FullscreenPresenter.show( context, - slides: [for (final i in visible) deck.slides[i]], + slides: slides, projectPath: deck.projectPath, themeProfile: deck.themeProfile, initialIndex: initial, @@ -927,7 +942,7 @@ class _MainLayoutState extends ConsumerState<_MainLayout> { } void exportDeck() { - final slides = deck.slides.where((s) => !s.skipped).toList(); + final slides = _slidesForPresentationOrExport(deck); if (slides.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -947,7 +962,9 @@ class _MainLayoutState extends ConsumerState<_MainLayout> { exportService: widget.exportService, tlp: deck.tlp, exportDirectory: ref.read(settingsProvider).exportDirectory, - markdown: deckNotifier.generateMarkdown(), + markdown: ref + .read(markdownServiceProvider) + .generateDeck(deck.copyWith(slides: slides)), ); } diff --git a/lib/widgets/dialogs/export_dialog.dart b/lib/widgets/dialogs/export_dialog.dart index 7c42f41..26acf00 100644 --- a/lib/widgets/dialogs/export_dialog.dart +++ b/lib/widgets/dialogs/export_dialog.dart @@ -91,6 +91,12 @@ class _ExportDialogState extends State { _total = needsRaster ? widget.slides.length : 0; }); + // Give the dialog a frame to paint before the potentially expensive first + // image decode/raster pass starts. + await WidgetsBinding.instance.endOfFrame; + await Future.delayed(Duration.zero); + if (!mounted) return; + final images = needsRaster ? await SlideRasterizer.rasterize( context: context, @@ -101,6 +107,14 @@ class _ExportDialogState extends State { onProgress: (done, total) { if (mounted) setState(() => _done = done); }, + onStage: (phase, done, total) { + if (!mounted) return; + setState(() { + _phase = _stageText(phase, done, total); + _done = done; + _total = total; + }); + }, ) : const []; @@ -129,6 +143,23 @@ class _ExportDialogState extends State { }); } + String _stageText(String phase, int done, int total) { + final l10n = context.l10n; + final number = (done + 1).clamp(1, total); + switch (phase) { + case 'prepare': + return '${l10n.d('Slide')} $number ${l10n.d('voorbereiden…')}'; + case 'render': + return '${l10n.d('Slide')} $number ${l10n.d('renderen…')}'; + case 'done': + return done >= total + ? l10n.d('Slides gerenderd.') + : '${l10n.d('Slide')} $done ${l10n.d('gerenderd.')}'; + default: + return l10n.t('renderingSlides'); + } + } + @override Widget build(BuildContext context) { final l10n = context.l10n; diff --git a/lib/widgets/dialogs/settings_dialog.dart b/lib/widgets/dialogs/settings_dialog.dart index 12db091..f4cd958 100644 --- a/lib/widgets/dialogs/settings_dialog.dart +++ b/lib/widgets/dialogs/settings_dialog.dart @@ -35,6 +35,7 @@ class _SettingsDialogState extends ConsumerState { late TextEditingController _profileName; late TextEditingController _logoSize; late TextEditingController _footerText; + late TextEditingController _closingSlideMarkdown; /// Whether the user changed the active profile in this session. Used to /// decide whether to apply the profile to the currently open presentation. @@ -74,6 +75,9 @@ class _SettingsDialogState extends ConsumerState { _profileName = TextEditingController(text: _themeProfile.name); _logoSize = TextEditingController(text: _themeProfile.logoSize.toString()); _footerText = TextEditingController(text: _themeProfile.footerText); + _closingSlideMarkdown = TextEditingController( + text: _themeProfile.closingSlideMarkdown, + ); } @override @@ -81,6 +85,7 @@ class _SettingsDialogState extends ConsumerState { _profileName.dispose(); _logoSize.dispose(); _footerText.dispose(); + _closingSlideMarkdown.dispose(); super.dispose(); } @@ -130,6 +135,7 @@ class _SettingsDialogState extends ConsumerState { _profileName.text = profile.name; _logoSize.text = profile.logoSize.toString(); _footerText.text = profile.footerText; + _closingSlideMarkdown.text = profile.closingSlideMarkdown; _profileTouched = true; }); } @@ -142,6 +148,7 @@ class _SettingsDialogState extends ConsumerState { name: name.isEmpty ? 'Stijlprofiel' : name, logoSize: size, footerText: _footerText.text, + closingSlideMarkdown: _closingSlideMarkdown.text, ); notifier.setHomeDirectory(_homeDirectory); notifier.setExportDirectory(_exportDirectory); @@ -295,6 +302,7 @@ class _SettingsDialogState extends ConsumerState { _profileName.text = profile.name; _logoSize.text = profile.logoSize.toString(); _footerText.text = profile.footerText; + _closingSlideMarkdown.text = profile.closingSlideMarkdown; _profileTouched = true; }); }, @@ -312,6 +320,7 @@ class _SettingsDialogState extends ConsumerState { _profileName.text = profile.name; _logoSize.text = profile.logoSize.toString(); _footerText.text = profile.footerText; + _closingSlideMarkdown.text = profile.closingSlideMarkdown; _profileTouched = true; }); } @@ -327,6 +336,7 @@ class _SettingsDialogState extends ConsumerState { _profileName.text = created.name; _logoSize.text = created.logoSize.toString(); _footerText.text = created.footerText; + _closingSlideMarkdown.text = created.closingSlideMarkdown; _profileTouched = true; }); } @@ -690,6 +700,47 @@ class _SettingsDialogState extends ConsumerState { contentPadding: EdgeInsets.zero, dense: true, ), + const SizedBox(height: 24), + _sectionTitle(l10n.d('Laatste slide')), + SwitchListTile( + value: _themeProfile.closingSlideEnabled, + onChanged: (v) => setState(() { + _themeProfile = _themeProfile.copyWith( + closingSlideEnabled: v, + closingSlideMarkdown: _closingSlideMarkdown.text, + ); + _profileTouched = true; + }), + title: Text( + l10n.d('Standaard laatste slide gebruiken'), + style: const TextStyle(fontSize: 13), + ), + subtitle: Text( + l10n.d( + 'Wordt automatisch toegevoegd bij presenteren en exporteren.', + ), + style: const TextStyle(fontSize: 11, color: Color(0xFF64748B)), + ), + contentPadding: EdgeInsets.zero, + dense: true, + ), + const SizedBox(height: 8), + TextField( + controller: _closingSlideMarkdown, + enabled: _themeProfile.closingSlideEnabled, + minLines: 4, + maxLines: 8, + decoration: InputDecoration( + labelText: l10n.d('Markdown voor laatste slide'), + hintText: '# Bedankt\n\nVragen?', + alignLabelWithHint: true, + isDense: true, + ), + onChanged: (value) { + _themeProfile = _themeProfile.copyWith(closingSlideMarkdown: value); + _profileTouched = true; + }, + ), ], ); } diff --git a/lib/widgets/presentation/fullscreen_presenter.dart b/lib/widgets/presentation/fullscreen_presenter.dart index 149d334..0a92b14 100644 --- a/lib/widgets/presentation/fullscreen_presenter.dart +++ b/lib/widgets/presentation/fullscreen_presenter.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:screen_retriever/screen_retriever.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:window_manager/window_manager.dart'; import '../../models/deck.dart'; import '../../models/settings.dart'; @@ -124,6 +125,7 @@ class _FullscreenPresenterState extends State { _clockTimer = Timer.periodic(const Duration(seconds: 1), (_) { if (mounted && _presenterView) setState(() {}); }); + _enableWakeLock(); WidgetsBinding.instance.addPostFrameCallback((_) { _focusNode.requestFocus(); _loadDisplays(); @@ -136,11 +138,28 @@ 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; @@ -268,6 +287,7 @@ class _FullscreenPresenterState extends State { Future _exit() async { _advanceTimer?.cancel(); + await _disableWakeLock(); await windowManager.setFullScreen(false); if (mounted) Navigator.pop(context); } diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index c857ae5..535ebd5 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -97,6 +97,10 @@ install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/runner/resources/app_icon.png" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}/icons" + COMPONENT Runtime) + install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index 59800a0..5283728 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -14,6 +14,20 @@ struct _MyApplication { G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) +static void set_window_icon(GtkWindow* window) { + gtk_window_set_default_icon_name(APPLICATION_ID); + + g_autofree gchar* executable_path = g_file_read_link("/proc/self/exe", nullptr); + if (executable_path == nullptr) { + return; + } + + g_autofree gchar* executable_dir = g_path_get_dirname(executable_path); + g_autofree gchar* icon_path = + g_build_filename(executable_dir, "data", "icons", "app_icon.png", nullptr); + gtk_window_set_icon_from_file(window, icon_path, nullptr); +} + // Called when first Flutter frame received. static void first_frame_cb(MyApplication* self, FlView* view) { gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); @@ -24,6 +38,7 @@ static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + set_window_icon(window); // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu diff --git a/linux/runner/resources/app_icon.png b/linux/runner/resources/app_icon.png new file mode 100644 index 0000000..047bdb9 Binary files /dev/null and b/linux/runner/resources/app_icon.png differ diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 76d87d9..fc4c184 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,20 +7,24 @@ import Foundation import desktop_drop import file_picker +import package_info_plus import pasteboard import screen_retriever_macos import shared_preferences_foundation import url_launcher_macos import video_player_avfoundation +import wakelock_plus import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) + WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Podfile b/macos/Podfile index ff5ddb3..decd2fd 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -38,5 +38,16 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) + + next unless target.name == 'video_player_avfoundation' + + target.build_configurations.each do |config| + config.build_settings['GCC_WARN_INHIBIT_ALL_WARNINGS'] = 'YES' + config.build_settings['SWIFT_SUPPRESS_WARNINGS'] = 'YES' + config.build_settings['OTHER_CFLAGS'] = [ + '$(inherited)', + '-Wno-deprecated-declarations', + ] + end end end diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 972c8de..2d2725f 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,22 +1,78 @@ PODS: + - desktop_drop (0.0.1): + - FlutterMacOS + - file_picker (0.0.1): + - FlutterMacOS - FlutterMacOS (1.0.0) + - package_info_plus (0.0.1): + - FlutterMacOS + - pasteboard (0.0.1): + - FlutterMacOS - screen_retriever_macos (0.0.1): - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS + - wakelock_plus (0.0.1): + - FlutterMacOS + - window_manager (0.5.0): + - FlutterMacOS DEPENDENCIES: + - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) + - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) + - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) EXTERNAL SOURCES: + desktop_drop: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos + file_picker: + :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos FlutterMacOS: :path: Flutter/ephemeral + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + pasteboard: + :path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos screen_retriever_macos: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + video_player_avfoundation: + :path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin + wakelock_plus: + :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: + desktop_drop: 10a3e6a7fa9dbe350541f2574092fecfa345a07b + file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + pasteboard: b594eaf838d930b276d7a35a44a32b4f489170cb screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + video_player_avfoundation: 3453f792138786248960ca029747fcd9f318ef52 + wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b + window_manager: b729e31d38fb04905235df9ea896128991cad99e -PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 +PODFILE CHECKSUM: b3f873403194e4bfbc7fa8ecc38abe8f55968093 COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 25d35e0..22bd22f 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -375,6 +375,7 @@ }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -589,6 +590,10 @@ "$(inherited)", "@executable_path/../Frameworks", ); + OTHER_CFLAGS = ( + "$(inherited)", + "-Wno-deprecated-declarations", + ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -721,6 +726,10 @@ "$(inherited)", "@executable_path/../Frameworks", ); + OTHER_CFLAGS = ( + "$(inherited)", + "-Wno-deprecated-declarations", + ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -741,6 +750,10 @@ "$(inherited)", "@executable_path/../Frameworks", ); + OTHER_CFLAGS = ( + "$(inherited)", + "-Wno-deprecated-declarations", + ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index b3c1761..e963641 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -3,6 +3,14 @@ import FlutterMacOS @main class AppDelegate: FlutterAppDelegate { + override func applicationDidFinishLaunching(_ notification: Notification) { + if let iconPath = Bundle.main.path(forResource: "AppIcon", ofType: "icns") { + NSApp.applicationIconImage = NSImage(contentsOfFile: iconPath) + } + + super.applicationDidFinishLaunching(notification) + } + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 879f1f1..cf8063b 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index f259e62..f584a70 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 3e39eec..e094035 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index 38b1387..da93124 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 358c7a1..945f62e 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 60b205f..bc56adc 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index f459ae9..9335d2d 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/pubspec.lock b/pubspec.lock index 2718a22..62d036d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -493,6 +493,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" + url: "https://pub.dev" + source: hosted + version: "9.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" pasteboard: dependency: "direct main" description: @@ -670,12 +686,11 @@ packages: source: hosted version: "0.2.0" screen_retriever_macos: - dependency: transitive + dependency: "direct overridden" description: - name: screen_retriever_macos - sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" - url: "https://pub.dev" - source: hosted + path: "third_party/screen_retriever_macos" + relative: true + source: path version: "0.2.0" screen_retriever_platform_interface: dependency: transitive @@ -1050,6 +1065,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.2.0" + wakelock_plus: + dependency: "direct main" + description: + name: wakelock_plus + sha256: ddf3db70eaa10c37558ff817519b85d527dbd21034fd5d8e1c2e85f31588f1c1 + url: "https://pub.dev" + source: hosted + version: "1.5.2" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b + url: "https://pub.dev" + source: hosted + version: "1.5.1" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2c8671d..c4edd39 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: flutter_highlight: ^0.7.0 flutter_math_fork: ^0.7.4 highlight: ^0.7.0 + wakelock_plus: ^1.5.2 dev_dependencies: flutter_test: @@ -38,7 +39,13 @@ dev_dependencies: flutter_lints: ^6.0.0 xml: ^6.6.1 +dependency_overrides: + screen_retriever_macos: + path: third_party/screen_retriever_macos + flutter: + config: + enable-swift-package-manager: false uses-material-design: true assets: - assets/images/de-winter-wittegeheel.png diff --git a/test/markdown_service_test.dart b/test/markdown_service_test.dart index 40f3e9b..f415742 100644 --- a/test/markdown_service_test.dart +++ b/test/markdown_service_test.dart @@ -83,6 +83,8 @@ void main() { footerText: 'Vertrouwelijk · {page}/{total}', footerShowPageNumbers: true, footerPosition: 'center', + closingSlideEnabled: true, + closingSlideMarkdown: '# Einde\n\nDank voor jullie aandacht.', ); final markdown = service.generateDeck( @@ -106,6 +108,11 @@ void main() { expect(deck.themeProfile.footerText, 'Vertrouwelijk · {page}/{total}'); expect(deck.themeProfile.footerShowPageNumbers, isTrue); expect(deck.themeProfile.footerPosition, 'center'); + expect(deck.themeProfile.closingSlideEnabled, isTrue); + expect( + deck.themeProfile.closingSlideMarkdown, + '# Einde\n\nDank voor jullie aandacht.', + ); }); test('adds logo-safe class when deck profile has logo', () { diff --git a/third_party/screen_retriever_macos/CHANGELOG.md b/third_party/screen_retriever_macos/CHANGELOG.md new file mode 100644 index 0000000..2f145ce --- /dev/null +++ b/third_party/screen_retriever_macos/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.2.0 + +* First release. diff --git a/third_party/screen_retriever_macos/LICENSE b/third_party/screen_retriever_macos/LICENSE new file mode 100644 index 0000000..eea05ab --- /dev/null +++ b/third_party/screen_retriever_macos/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 LiJianying + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/third_party/screen_retriever_macos/README.md b/third_party/screen_retriever_macos/README.md new file mode 100644 index 0000000..743c992 --- /dev/null +++ b/third_party/screen_retriever_macos/README.md @@ -0,0 +1,12 @@ +# screen_retriever_macos + +[![pub version][pub-image]][pub-url] + +[pub-image]: https://img.shields.io/pub/v/screen_retriever_macos.svg +[pub-url]: https://pub.dev/packages/screen_retriever_macos + +The macos implementation of [screen_retriever](https://pub.dev/packages/screen_retriever). + +## License + +[MIT](./LICENSE) diff --git a/third_party/screen_retriever_macos/macos/Classes/ScreenRetrieverMacosPlugin.swift b/third_party/screen_retriever_macos/macos/Classes/ScreenRetrieverMacosPlugin.swift new file mode 100644 index 0000000..e0c801b --- /dev/null +++ b/third_party/screen_retriever_macos/macos/Classes/ScreenRetrieverMacosPlugin.swift @@ -0,0 +1,151 @@ +import Cocoa +import FlutterMacOS + +extension NSScreen { + var displayID: CGDirectDisplayID { + return deviceDescription[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID ?? 0 + } + + public func toDictionary() -> NSDictionary { + var name: String = ""; + if #available(macOS 10.15, *) { + name = self.localizedName + } + let size: NSDictionary = [ + "width": self.frame.width, + "height": self.frame.height, + ] + let visiblePosition: NSDictionary = [ + "dx": self.visibleFrame.topLeft.x, + "dy": self.visibleFrame.topLeft.y, + ] + let visibleSize: NSDictionary = [ + "width": self.visibleFrame.width, + "height": self.visibleFrame.height, + ] + let dict: NSDictionary = [ + "id": self.displayID.description, + "name": name, + "size": size, + "visiblePosition": visiblePosition, + "visibleSize": visibleSize, + ] + return dict; + } +} + +extension NSRect { + var topLeft: CGPoint { + set { + let screenFrameRect = NSScreen.screens[0].frame + origin.x = newValue.x + origin.y = screenFrameRect.height - newValue.y - size.height + } + get { + let screenFrameRect = NSScreen.screens[0].frame + return CGPoint(x: origin.x, y: screenFrameRect.height - origin.y - size.height) + } + } +} + +public class ScreenRetrieverMacosPlugin: NSObject, FlutterPlugin,FlutterStreamHandler { + private var _eventSink: FlutterEventSink? + + var externalDisplayCount:Int = 0 + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "dev.leanflutter.plugins/screen_retriever", binaryMessenger: registrar.messenger) + let instance = ScreenRetrieverMacosPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + let eventChannel = FlutterEventChannel(name: "dev.leanflutter.plugins/screen_retriever_event", binaryMessenger: registrar.messenger) + eventChannel.setStreamHandler(instance) + + instance.externalDisplayCount = NSScreen.screens.count + instance.setupNotificationCenter() + } + + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + self._eventSink = events + return nil; + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + self._eventSink = nil + return nil + } + + func setupNotificationCenter() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleDisplayConnection), + name: NSApplication.didChangeScreenParametersNotification, + object: nil) + } + + + @objc func handleDisplayConnection(notification: Notification) { + if externalDisplayCount < NSScreen.screens.count { + _emitEvent("display-added") + externalDisplayCount = NSScreen.screens.count + } else if externalDisplayCount > NSScreen.screens.count { + _emitEvent("display-removed") + externalDisplayCount = NSScreen.screens.count + } + } + + public func _emitEvent(_ eventName: String) { + guard let eventSink = self._eventSink else { + return + } + let event: NSDictionary = [ + "type": eventName, + ] + eventSink(event) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getCursorScreenPoint": + getCursorScreenPoint(call, result: result) + break + case "getPrimaryDisplay": + getPrimaryDisplay(call, result: result) + break + case "getAllDisplays": + getAllDisplays(call, result: result) + break + default: + result(FlutterMethodNotImplemented) + } + } + + public func getCursorScreenPoint(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let currentScreen = NSScreen.main! + let mouseLocation: NSPoint = NSEvent.mouseLocation; + + var visibleHeight = currentScreen.frame.maxY + for screen in NSScreen.screens { + if (visibleHeight > screen.frame.maxY) { + visibleHeight = screen.frame.maxY + } + } + let data: NSDictionary = [ + "dx": mouseLocation.x, + "dy": visibleHeight - mouseLocation.y, + ] + result(data) + } + + public func getPrimaryDisplay(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result(NSScreen.screens[0].toDictionary()) + } + + public func getAllDisplays(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let data: NSDictionary = [ + "displays": NSScreen.screens.map({ screen in + return screen.toDictionary() + }), + ] + result(data) + } +} diff --git a/third_party/screen_retriever_macos/macos/screen_retriever_macos.podspec b/third_party/screen_retriever_macos/macos/screen_retriever_macos.podspec new file mode 100644 index 0000000..443443d --- /dev/null +++ b/third_party/screen_retriever_macos/macos/screen_retriever_macos.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint screen_retriever_macos.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'screen_retriever_macos' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' +end diff --git a/third_party/screen_retriever_macos/macos/screen_retriever_macos/Package.swift b/third_party/screen_retriever_macos/macos/screen_retriever_macos/Package.swift new file mode 100644 index 0000000..3ac896c --- /dev/null +++ b/third_party/screen_retriever_macos/macos/screen_retriever_macos/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "screen_retriever_macos", + platforms: [ + .macOS("10.15") + ], + products: [ + .library(name: "screen-retriever-macos", targets: ["screen_retriever_macos"]) + ], + dependencies: [], + targets: [ + .target( + name: "screen_retriever_macos", + dependencies: [] + ) + ] +) diff --git a/third_party/screen_retriever_macos/macos/screen_retriever_macos/Sources/screen_retriever_macos/ScreenRetrieverMacosPlugin.swift b/third_party/screen_retriever_macos/macos/screen_retriever_macos/Sources/screen_retriever_macos/ScreenRetrieverMacosPlugin.swift new file mode 100644 index 0000000..e0c801b --- /dev/null +++ b/third_party/screen_retriever_macos/macos/screen_retriever_macos/Sources/screen_retriever_macos/ScreenRetrieverMacosPlugin.swift @@ -0,0 +1,151 @@ +import Cocoa +import FlutterMacOS + +extension NSScreen { + var displayID: CGDirectDisplayID { + return deviceDescription[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID ?? 0 + } + + public func toDictionary() -> NSDictionary { + var name: String = ""; + if #available(macOS 10.15, *) { + name = self.localizedName + } + let size: NSDictionary = [ + "width": self.frame.width, + "height": self.frame.height, + ] + let visiblePosition: NSDictionary = [ + "dx": self.visibleFrame.topLeft.x, + "dy": self.visibleFrame.topLeft.y, + ] + let visibleSize: NSDictionary = [ + "width": self.visibleFrame.width, + "height": self.visibleFrame.height, + ] + let dict: NSDictionary = [ + "id": self.displayID.description, + "name": name, + "size": size, + "visiblePosition": visiblePosition, + "visibleSize": visibleSize, + ] + return dict; + } +} + +extension NSRect { + var topLeft: CGPoint { + set { + let screenFrameRect = NSScreen.screens[0].frame + origin.x = newValue.x + origin.y = screenFrameRect.height - newValue.y - size.height + } + get { + let screenFrameRect = NSScreen.screens[0].frame + return CGPoint(x: origin.x, y: screenFrameRect.height - origin.y - size.height) + } + } +} + +public class ScreenRetrieverMacosPlugin: NSObject, FlutterPlugin,FlutterStreamHandler { + private var _eventSink: FlutterEventSink? + + var externalDisplayCount:Int = 0 + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "dev.leanflutter.plugins/screen_retriever", binaryMessenger: registrar.messenger) + let instance = ScreenRetrieverMacosPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + let eventChannel = FlutterEventChannel(name: "dev.leanflutter.plugins/screen_retriever_event", binaryMessenger: registrar.messenger) + eventChannel.setStreamHandler(instance) + + instance.externalDisplayCount = NSScreen.screens.count + instance.setupNotificationCenter() + } + + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + self._eventSink = events + return nil; + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + self._eventSink = nil + return nil + } + + func setupNotificationCenter() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleDisplayConnection), + name: NSApplication.didChangeScreenParametersNotification, + object: nil) + } + + + @objc func handleDisplayConnection(notification: Notification) { + if externalDisplayCount < NSScreen.screens.count { + _emitEvent("display-added") + externalDisplayCount = NSScreen.screens.count + } else if externalDisplayCount > NSScreen.screens.count { + _emitEvent("display-removed") + externalDisplayCount = NSScreen.screens.count + } + } + + public func _emitEvent(_ eventName: String) { + guard let eventSink = self._eventSink else { + return + } + let event: NSDictionary = [ + "type": eventName, + ] + eventSink(event) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getCursorScreenPoint": + getCursorScreenPoint(call, result: result) + break + case "getPrimaryDisplay": + getPrimaryDisplay(call, result: result) + break + case "getAllDisplays": + getAllDisplays(call, result: result) + break + default: + result(FlutterMethodNotImplemented) + } + } + + public func getCursorScreenPoint(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let currentScreen = NSScreen.main! + let mouseLocation: NSPoint = NSEvent.mouseLocation; + + var visibleHeight = currentScreen.frame.maxY + for screen in NSScreen.screens { + if (visibleHeight > screen.frame.maxY) { + visibleHeight = screen.frame.maxY + } + } + let data: NSDictionary = [ + "dx": mouseLocation.x, + "dy": visibleHeight - mouseLocation.y, + ] + result(data) + } + + public func getPrimaryDisplay(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result(NSScreen.screens[0].toDictionary()) + } + + public func getAllDisplays(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let data: NSDictionary = [ + "displays": NSScreen.screens.map({ screen in + return screen.toDictionary() + }), + ] + result(data) + } +} diff --git a/third_party/screen_retriever_macos/pubspec.yaml b/third_party/screen_retriever_macos/pubspec.yaml new file mode 100644 index 0000000..ad5eef3 --- /dev/null +++ b/third_party/screen_retriever_macos/pubspec.yaml @@ -0,0 +1,25 @@ +name: screen_retriever_macos +description: macOS implementation of the screen_retriever plugin. +version: 0.2.0 +repository: https://github.com/leanflutter/screen_retriever/tree/main/packages/screen_retriever_macos + +environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + screen_retriever_platform_interface: ^0.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mostly_reasonable_lints: ^0.1.2 + +flutter: + plugin: + implements: screen_retriever + platforms: + macos: + pluginClass: ScreenRetrieverMacosPlugin diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index c04e20c..bb4c137 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ