feat: polish app icons and presentation exports

This commit is contained in:
Brenno de Winter 2026-06-05 00:02:51 +02:00
parent d59c6ee761
commit 20906ddb65
57 changed files with 756 additions and 10 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

After

Width:  |  Height:  |  Size: 518 KiB

View file

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"

View file

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"

43
ios/Podfile Normal file
View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -25,6 +25,11 @@ class ThemeProfile {
/// Horizontale positie van de footer: left, center of right. /// Horizontale positie van de footer: left, center of right.
final String footerPosition; 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({ const ThemeProfile({
this.name = 'Standaard', this.name = 'Standaard',
this.slideBackgroundColor = '#FFFFFF', this.slideBackgroundColor = '#FFFFFF',
@ -42,6 +47,8 @@ class ThemeProfile {
this.footerText = '', this.footerText = '',
this.footerShowPageNumbers = false, this.footerShowPageNumbers = false,
this.footerPosition = 'right', this.footerPosition = 'right',
this.closingSlideEnabled = false,
this.closingSlideMarkdown = '# Bedankt\n\nVragen?',
}) : tableTextColor = tableTextColor ?? textColor; }) : tableTextColor = tableTextColor ?? textColor;
static const logoPositions = [ static const logoPositions = [
@ -70,6 +77,8 @@ class ThemeProfile {
String? footerText, String? footerText,
bool? footerShowPageNumbers, bool? footerShowPageNumbers,
String? footerPosition, String? footerPosition,
bool? closingSlideEnabled,
String? closingSlideMarkdown,
bool clearLogo = false, bool clearLogo = false,
}) { }) {
return ThemeProfile( return ThemeProfile(
@ -91,6 +100,8 @@ class ThemeProfile {
footerShowPageNumbers: footerShowPageNumbers:
footerShowPageNumbers ?? this.footerShowPageNumbers, footerShowPageNumbers ?? this.footerShowPageNumbers,
footerPosition: footerPosition ?? this.footerPosition, footerPosition: footerPosition ?? this.footerPosition,
closingSlideEnabled: closingSlideEnabled ?? this.closingSlideEnabled,
closingSlideMarkdown: closingSlideMarkdown ?? this.closingSlideMarkdown,
); );
} }
@ -112,6 +123,8 @@ class ThemeProfile {
'footerText': footerText, 'footerText': footerText,
'footerShowPageNumbers': footerShowPageNumbers, 'footerShowPageNumbers': footerShowPageNumbers,
'footerPosition': footerPosition, 'footerPosition': footerPosition,
'closingSlideEnabled': closingSlideEnabled,
'closingSlideMarkdown': closingSlideMarkdown,
}; };
} }
@ -140,6 +153,9 @@ class ThemeProfile {
footerText: json['footerText'] as String? ?? '', footerText: json['footerText'] as String? ?? '',
footerShowPageNumbers: json['footerShowPageNumbers'] as bool? ?? false, footerShowPageNumbers: json['footerShowPageNumbers'] as bool? ?? false,
footerPosition: json['footerPosition'] as String? ?? 'right', footerPosition: json['footerPosition'] as String? ?? 'right',
closingSlideEnabled: json['closingSlideEnabled'] as bool? ?? false,
closingSlideMarkdown:
json['closingSlideMarkdown'] as String? ?? '# Bedankt\n\nVragen?',
); );
} }
} }

View file

@ -34,6 +34,7 @@ class SlideRasterizer {
TlpLevel tlp = TlpLevel.none, TlpLevel tlp = TlpLevel.none,
int targetWidth = 1920, int targetWidth = 1920,
void Function(int done, int total)? onProgress, void Function(int done, int total)? onProgress,
void Function(String phase, int done, int total)? onStage,
}) async { }) async {
final overlay = Overlay.of(context, rootOverlay: true); final overlay = Overlay.of(context, rootOverlay: true);
final pixelRatio = targetWidth / logicalSize.width; final pixelRatio = targetWidth / logicalSize.width;
@ -54,6 +55,7 @@ class SlideRasterizer {
final results = <Uint8List>[]; final results = <Uint8List>[];
try { try {
for (var i = 0; i < slides.length; i++) { 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 // Warm this slide's images immediately before capturing it. Doing it
// per slide (instead of once up front) guarantees the bitmap is decoded // 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 // and resident in the cache at capture time, no matter how many images
@ -66,6 +68,7 @@ class SlideRasterizer {
]); ]);
if (!context.mounted) break; if (!context.mounted) break;
onStage?.call('render', i, slides.length);
final key = GlobalKey(); final key = GlobalKey();
final entry = OverlayEntry( final entry = OverlayEntry(
builder: (_) => Positioned( builder: (_) => Positioned(
@ -96,6 +99,7 @@ class SlideRasterizer {
} finally { } finally {
entry.remove(); entry.remove();
} }
onStage?.call('done', i + 1, slides.length);
onProgress?.call(i + 1, slides.length); onProgress?.call(i + 1, slides.length);
} }
} finally { } finally {

View file

@ -138,6 +138,19 @@ List<String> _imageUsages(WidgetRef ref, String absolutePath) {
return usages; return usages;
} }
List<Slide> _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 // App shell
class AppShell extends ConsumerStatefulWidget { class AppShell extends ConsumerStatefulWidget {
@ -904,7 +917,8 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
for (var i = 0; i < deck.slides.length; i++) for (var i = 0; i < deck.slides.length; i++)
if (!deck.slides[i].skipped) i, if (!deck.slides[i].skipped) i,
]; ];
if (visible.isEmpty) { final slides = _slidesForPresentationOrExport(deck);
if (slides.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
@ -916,9 +930,10 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
} }
var initial = visible.indexWhere((i) => i >= editor.selectedIndex); var initial = visible.indexWhere((i) => i >= editor.selectedIndex);
if (initial < 0) initial = visible.length - 1; if (initial < 0) initial = visible.length - 1;
if (initial < 0) initial = 0;
FullscreenPresenter.show( FullscreenPresenter.show(
context, context,
slides: [for (final i in visible) deck.slides[i]], slides: slides,
projectPath: deck.projectPath, projectPath: deck.projectPath,
themeProfile: deck.themeProfile, themeProfile: deck.themeProfile,
initialIndex: initial, initialIndex: initial,
@ -927,7 +942,7 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
} }
void exportDeck() { void exportDeck() {
final slides = deck.slides.where((s) => !s.skipped).toList(); final slides = _slidesForPresentationOrExport(deck);
if (slides.isEmpty) { if (slides.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@ -947,7 +962,9 @@ class _MainLayoutState extends ConsumerState<_MainLayout> {
exportService: widget.exportService, exportService: widget.exportService,
tlp: deck.tlp, tlp: deck.tlp,
exportDirectory: ref.read(settingsProvider).exportDirectory, exportDirectory: ref.read(settingsProvider).exportDirectory,
markdown: deckNotifier.generateMarkdown(), markdown: ref
.read(markdownServiceProvider)
.generateDeck(deck.copyWith(slides: slides)),
); );
} }

View file

@ -91,6 +91,12 @@ class _ExportDialogState extends State<ExportDialog> {
_total = needsRaster ? widget.slides.length : 0; _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<void>.delayed(Duration.zero);
if (!mounted) return;
final images = needsRaster final images = needsRaster
? await SlideRasterizer.rasterize( ? await SlideRasterizer.rasterize(
context: context, context: context,
@ -101,6 +107,14 @@ class _ExportDialogState extends State<ExportDialog> {
onProgress: (done, total) { onProgress: (done, total) {
if (mounted) setState(() => _done = done); if (mounted) setState(() => _done = done);
}, },
onStage: (phase, done, total) {
if (!mounted) return;
setState(() {
_phase = _stageText(phase, done, total);
_done = done;
_total = total;
});
},
) )
: const <Uint8List>[]; : const <Uint8List>[];
@ -129,6 +143,23 @@ class _ExportDialogState extends State<ExportDialog> {
}); });
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;

View file

@ -35,6 +35,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
late TextEditingController _profileName; late TextEditingController _profileName;
late TextEditingController _logoSize; late TextEditingController _logoSize;
late TextEditingController _footerText; late TextEditingController _footerText;
late TextEditingController _closingSlideMarkdown;
/// Whether the user changed the active profile in this session. Used to /// Whether the user changed the active profile in this session. Used to
/// decide whether to apply the profile to the currently open presentation. /// decide whether to apply the profile to the currently open presentation.
@ -74,6 +75,9 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
_profileName = TextEditingController(text: _themeProfile.name); _profileName = TextEditingController(text: _themeProfile.name);
_logoSize = TextEditingController(text: _themeProfile.logoSize.toString()); _logoSize = TextEditingController(text: _themeProfile.logoSize.toString());
_footerText = TextEditingController(text: _themeProfile.footerText); _footerText = TextEditingController(text: _themeProfile.footerText);
_closingSlideMarkdown = TextEditingController(
text: _themeProfile.closingSlideMarkdown,
);
} }
@override @override
@ -81,6 +85,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
_profileName.dispose(); _profileName.dispose();
_logoSize.dispose(); _logoSize.dispose();
_footerText.dispose(); _footerText.dispose();
_closingSlideMarkdown.dispose();
super.dispose(); super.dispose();
} }
@ -130,6 +135,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
_profileName.text = profile.name; _profileName.text = profile.name;
_logoSize.text = profile.logoSize.toString(); _logoSize.text = profile.logoSize.toString();
_footerText.text = profile.footerText; _footerText.text = profile.footerText;
_closingSlideMarkdown.text = profile.closingSlideMarkdown;
_profileTouched = true; _profileTouched = true;
}); });
} }
@ -142,6 +148,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
name: name.isEmpty ? 'Stijlprofiel' : name, name: name.isEmpty ? 'Stijlprofiel' : name,
logoSize: size, logoSize: size,
footerText: _footerText.text, footerText: _footerText.text,
closingSlideMarkdown: _closingSlideMarkdown.text,
); );
notifier.setHomeDirectory(_homeDirectory); notifier.setHomeDirectory(_homeDirectory);
notifier.setExportDirectory(_exportDirectory); notifier.setExportDirectory(_exportDirectory);
@ -295,6 +302,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
_profileName.text = profile.name; _profileName.text = profile.name;
_logoSize.text = profile.logoSize.toString(); _logoSize.text = profile.logoSize.toString();
_footerText.text = profile.footerText; _footerText.text = profile.footerText;
_closingSlideMarkdown.text = profile.closingSlideMarkdown;
_profileTouched = true; _profileTouched = true;
}); });
}, },
@ -312,6 +320,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
_profileName.text = profile.name; _profileName.text = profile.name;
_logoSize.text = profile.logoSize.toString(); _logoSize.text = profile.logoSize.toString();
_footerText.text = profile.footerText; _footerText.text = profile.footerText;
_closingSlideMarkdown.text = profile.closingSlideMarkdown;
_profileTouched = true; _profileTouched = true;
}); });
} }
@ -327,6 +336,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
_profileName.text = created.name; _profileName.text = created.name;
_logoSize.text = created.logoSize.toString(); _logoSize.text = created.logoSize.toString();
_footerText.text = created.footerText; _footerText.text = created.footerText;
_closingSlideMarkdown.text = created.closingSlideMarkdown;
_profileTouched = true; _profileTouched = true;
}); });
} }
@ -690,6 +700,47 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
dense: true, 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;
},
),
], ],
); );
} }

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:screen_retriever/screen_retriever.dart'; import 'package:screen_retriever/screen_retriever.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import '../../models/deck.dart'; import '../../models/deck.dart';
import '../../models/settings.dart'; import '../../models/settings.dart';
@ -124,6 +125,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
_clockTimer = Timer.periodic(const Duration(seconds: 1), (_) { _clockTimer = Timer.periodic(const Duration(seconds: 1), (_) {
if (mounted && _presenterView) setState(() {}); if (mounted && _presenterView) setState(() {});
}); });
_enableWakeLock();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus(); _focusNode.requestFocus();
_loadDisplays(); _loadDisplays();
@ -136,11 +138,28 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
_advanceTimer?.cancel(); _advanceTimer?.cancel();
_clockTimer?.cancel(); _clockTimer?.cancel();
_typedTimer?.cancel(); _typedTimer?.cancel();
_disableWakeLock();
_gridScroll.dispose(); _gridScroll.dispose();
_focusNode.dispose(); _focusNode.dispose();
super.dispose(); super.dispose();
} }
Future<void> _enableWakeLock() async {
try {
await WakelockPlus.enable();
} catch (_) {
// Best-effort: unsupported platforms should not interrupt presenting.
}
}
Future<void> _disableWakeLock() async {
try {
await WakelockPlus.disable();
} catch (_) {
// Best-effort cleanup.
}
}
void _scheduleAdvance() { void _scheduleAdvance() {
_advanceTimer?.cancel(); _advanceTimer?.cancel();
_advanceTimer = null; _advanceTimer = null;
@ -268,6 +287,7 @@ class _FullscreenPresenterState extends State<FullscreenPresenter> {
Future<void> _exit() async { Future<void> _exit() async {
_advanceTimer?.cancel(); _advanceTimer?.cancel();
await _disableWakeLock();
await windowManager.setFullScreen(false); await windowManager.setFullScreen(false);
if (mounted) Navigator.pop(context); if (mounted) Navigator.pop(context);
} }

View file

@ -97,6 +97,10 @@ install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime) 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}" install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime) COMPONENT Runtime)

View file

@ -14,6 +14,20 @@ struct _MyApplication {
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 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. // Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) { static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(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); MyApplication* self = MY_APPLICATION(application);
GtkWindow* window = GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 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 // 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 // by applications and is the setup most users will be using (e.g. Ubuntu

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View file

@ -7,20 +7,24 @@ import Foundation
import desktop_drop import desktop_drop
import file_picker import file_picker
import package_info_plus
import pasteboard import pasteboard
import screen_retriever_macos import screen_retriever_macos
import shared_preferences_foundation import shared_preferences_foundation
import url_launcher_macos import url_launcher_macos
import video_player_avfoundation import video_player_avfoundation
import wakelock_plus
import window_manager import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
} }

View file

@ -38,5 +38,16 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(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
end end

View file

@ -1,22 +1,78 @@
PODS: PODS:
- desktop_drop (0.0.1):
- FlutterMacOS
- file_picker (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- package_info_plus (0.0.1):
- FlutterMacOS
- pasteboard (0.0.1):
- FlutterMacOS
- screen_retriever_macos (0.0.1): - screen_retriever_macos (0.0.1):
- FlutterMacOS - 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: 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`) - 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`) - 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: EXTERNAL SOURCES:
desktop_drop:
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
file_picker:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :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: screen_retriever_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/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: SPEC CHECKSUMS:
desktop_drop: 10a3e6a7fa9dbe350541f2574092fecfa345a07b
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
pasteboard: b594eaf838d930b276d7a35a44a32b4f489170cb
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f 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 COCOAPODS: 1.16.2

View file

@ -375,6 +375,7 @@
}; };
33CC111E2044C6BF0003C045 /* ShellScript */ = { 33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
@ -589,6 +590,10 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
OTHER_CFLAGS = (
"$(inherited)",
"-Wno-deprecated-declarations",
);
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };
@ -721,6 +726,10 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
OTHER_CFLAGS = (
"$(inherited)",
"-Wno-deprecated-declarations",
);
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -741,6 +750,10 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
OTHER_CFLAGS = (
"$(inherited)",
"-Wno-deprecated-declarations",
);
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };

View file

@ -3,6 +3,14 @@ import FlutterMacOS
@main @main
class AppDelegate: FlutterAppDelegate { 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 { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true return true
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -493,6 +493,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" 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: pasteboard:
dependency: "direct main" dependency: "direct main"
description: description:
@ -670,12 +686,11 @@ packages:
source: hosted source: hosted
version: "0.2.0" version: "0.2.0"
screen_retriever_macos: screen_retriever_macos:
dependency: transitive dependency: "direct overridden"
description: description:
name: screen_retriever_macos path: "third_party/screen_retriever_macos"
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" relative: true
url: "https://pub.dev" source: path
source: hosted
version: "0.2.0" version: "0.2.0"
screen_retriever_platform_interface: screen_retriever_platform_interface:
dependency: transitive dependency: transitive
@ -1050,6 +1065,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.2.0" 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: watcher:
dependency: transitive dependency: transitive
description: description:

View file

@ -31,6 +31,7 @@ dependencies:
flutter_highlight: ^0.7.0 flutter_highlight: ^0.7.0
flutter_math_fork: ^0.7.4 flutter_math_fork: ^0.7.4
highlight: ^0.7.0 highlight: ^0.7.0
wakelock_plus: ^1.5.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -38,7 +39,13 @@ dev_dependencies:
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
xml: ^6.6.1 xml: ^6.6.1
dependency_overrides:
screen_retriever_macos:
path: third_party/screen_retriever_macos
flutter: flutter:
config:
enable-swift-package-manager: false
uses-material-design: true uses-material-design: true
assets: assets:
- assets/images/de-winter-wittegeheel.png - assets/images/de-winter-wittegeheel.png

View file

@ -83,6 +83,8 @@ void main() {
footerText: 'Vertrouwelijk · {page}/{total}', footerText: 'Vertrouwelijk · {page}/{total}',
footerShowPageNumbers: true, footerShowPageNumbers: true,
footerPosition: 'center', footerPosition: 'center',
closingSlideEnabled: true,
closingSlideMarkdown: '# Einde\n\nDank voor jullie aandacht.',
); );
final markdown = service.generateDeck( final markdown = service.generateDeck(
@ -106,6 +108,11 @@ void main() {
expect(deck.themeProfile.footerText, 'Vertrouwelijk · {page}/{total}'); expect(deck.themeProfile.footerText, 'Vertrouwelijk · {page}/{total}');
expect(deck.themeProfile.footerShowPageNumbers, isTrue); expect(deck.themeProfile.footerShowPageNumbers, isTrue);
expect(deck.themeProfile.footerPosition, 'center'); 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', () { test('adds logo-safe class when deck profile has logo', () {

View file

@ -0,0 +1,3 @@
## 0.2.0
* First release.

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-2024 LiJianying <lijy91@foxmail.com>
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.

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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

View file

@ -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: []
)
]
)

View file

@ -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)
}
}

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 363 KiB