diff --git a/lib/constants.dart b/lib/constants.dart index b650717a4..a47f46bd5 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -91,6 +91,8 @@ enum AppEnvironment { const String kSharedPrefs = 'shared_prefs'; const String kSharedPrefUrl = 'url'; const String kSharedPrefToken = 'checksum'; +const String kSharedPrefWidth = 'width'; +const String kSharedPrefHeight = 'height'; const String kProductProPlanMonth = 'pro_plan'; const String kProductEnterprisePlanMonth_2 = 'enterprise_plan'; diff --git a/lib/main.dart b/lib/main.dart index 95f730acc..2b73025fb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -58,9 +58,23 @@ import 'package:invoiceninja_flutter/utils/web_stub.dart' // STARTER: import - do not remove comment import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_middleware.dart'; +import 'package:window_manager/window_manager.dart'; void main({bool isTesting = false}) async { WidgetsFlutterBinding.ensureInitialized(); + await windowManager.ensureInitialized(); + + final prefs = await SharedPreferences.getInstance(); + windowManager.waitUntilReadyToShow( + WindowOptions( + size: Size( + prefs.getDouble(kSharedPrefWidth) ?? 800, + prefs.getDouble(kSharedPrefHeight) ?? 600, + ), + ), () async { + await windowManager.show(); + await windowManager.focus(); + }); final store = Store(appReducer, initialState: await _initialState(isTesting), diff --git a/lib/main_app.dart b/lib/main_app.dart index cade64be1..c039644cb 100644 --- a/lib/main_app.dart +++ b/lib/main_app.dart @@ -147,7 +147,9 @@ class InvoiceNinjaAppState extends State { void initState() { super.initState(); - WebUtils.warnChanges(widget.store); + if (kIsWeb) { + WebUtils.warnChanges(widget.store); + } Timer.periodic(Duration(milliseconds: kMillisecondsToTimerRefreshData), (_) { diff --git a/lib/ui/app/main_screen.dart b/lib/ui/app/main_screen.dart index 96ec66b4b..55fd219e8 100644 --- a/lib/ui/app/main_screen.dart +++ b/lib/ui/app/main_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/ui/app/app_title_bar.dart'; +import 'package:invoiceninja_flutter/ui/app/window_manager.dart'; import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit_vm.dart'; import 'package:invoiceninja_flutter/ui/purchase_order/purchase_order_email_vm.dart'; import 'package:invoiceninja_flutter/ui/purchase_order/purchase_order_pdf_vm.dart'; @@ -302,29 +303,31 @@ class MainScreen extends StatelessWidget { return false; }, - child: DesktopSessionTimeout( - child: SafeArea( - child: FocusTraversalGroup( - policy: ReadingOrderTraversalPolicy(), - child: Column( - children: [ - if (isWindows()) AppTitleBar(), - Expanded( - child: ChangeLayoutBanner( - appLayout: prefState.appLayout, - suggestedLayout: AppLayout.desktop, - child: Row(children: [ - if (prefState.showMenu) MenuDrawerBuilder(), - Expanded( - child: AppBorder( - child: screen, - isLeft: prefState.showMenu && - (!state.isFullScreen || showFilterSidebar), - )), - ]), + child: WindowManager( + child: DesktopSessionTimeout( + child: SafeArea( + child: FocusTraversalGroup( + policy: ReadingOrderTraversalPolicy(), + child: Column( + children: [ + if (isWindows()) AppTitleBar(), + Expanded( + child: ChangeLayoutBanner( + appLayout: prefState.appLayout, + suggestedLayout: AppLayout.desktop, + child: Row(children: [ + if (prefState.showMenu) MenuDrawerBuilder(), + Expanded( + child: AppBorder( + child: screen, + isLeft: prefState.showMenu && + (!state.isFullScreen || showFilterSidebar), + )), + ]), + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/ui/app/window_manager.dart b/lib/ui/app/window_manager.dart new file mode 100644 index 000000000..bba2c94ae --- /dev/null +++ b/lib/ui/app/window_manager.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/ui/app/dialogs/alert_dialog.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:window_manager/window_manager.dart'; + +class WindowManager extends StatefulWidget { + const WindowManager({Key key, this.child}) : super(key: key); + + final Widget child; + + @override + State createState() => _WindowManagerState(); +} + +class _WindowManagerState extends State with WindowListener { + @override + void initState() { + windowManager.addListener(this); + _init(); + + super.initState(); + } + + void _init() async { + await windowManager.setPreventClose(true); + setState(() {}); + } + + @override + void onWindowResize() async { + final size = await windowManager.getSize(); + final prefs = await SharedPreferences.getInstance(); + prefs.setDouble(kSharedPrefWidth, size.width); + prefs.setDouble(kSharedPrefHeight, size.height); + } + + @override + void onWindowClose() async { + final localization = AppLocalization.of(context); + final store = StoreProvider.of(navigatorKey.currentContext); + final state = store.state; + + if (await windowManager.isPreventClose()) { + if (state.hasChanges()) { + showDialog( + context: context, + builder: (context) => MessageDialog( + localization.errorUnsavedChanges, + dismissLabel: localization.continueEditing, + onDiscard: () async { + await windowManager.destroy(); + }, + )); + } else { + await windowManager.destroy(); + } + } + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 71d3db665..4f4474e54 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,17 +7,25 @@ #include "generated_plugin_registrant.h" #include +#include #include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) printing_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin"); printing_plugin_register_with_registrar(printing_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); + screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); g_autoptr(FlPluginRegistrar) sentry_flutter_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin"); sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index bfbaf253b..bcb65d5b2 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,8 +4,10 @@ list(APPEND FLUTTER_PLUGIN_LIST printing + screen_retriever sentry_flutter url_launcher_linux + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 123aa6c66..09ab373a4 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,11 +10,13 @@ import package_info import package_info_plus_macos import path_provider_macos import printing +import screen_retriever import sentry_flutter import shared_preferences_macos import sign_in_with_apple import sqflite import url_launcher_macos +import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) @@ -22,9 +24,11 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 669875972..695f3ccd4 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -74,6 +74,7 @@ dependencies: image_cropper: ^2.0.2 msal_js: ^2.14.0 sign_in_with_apple: ^4.0.0 + window_manager: ^0.2.5 # bitsdojo_window: ^0.1.2 # quick_actions: ^0.2.1 # idb_shim: ^1.11.1+1 diff --git a/pubspec.lock b/pubspec.lock index fada2e39f..b7b2aaf18 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1015,6 +1015,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.27.3" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" sentry: dependency: transitive description: @@ -1463,6 +1470,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.5.1" + window_manager: + dependency: "direct main" + description: + name: window_manager + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.5" xdg_directories: dependency: transitive description: diff --git a/pubspec.next.yaml b/pubspec.next.yaml index f524ca74f..6bf8edc26 100644 --- a/pubspec.next.yaml +++ b/pubspec.next.yaml @@ -74,6 +74,7 @@ dependencies: image_cropper: ^2.0.2 msal_js: ^2.14.0 sign_in_with_apple: ^4.0.0 + window_manager: ^0.2.5 # bitsdojo_window: ^0.1.2 # quick_actions: ^0.2.1 # idb_shim: ^1.11.1+1 diff --git a/pubspec.yaml b/pubspec.yaml index 6bdf6b344..f5af148c6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,6 +74,7 @@ dependencies: image_cropper: ^2.0.2 msal_js: ^2.14.0 sign_in_with_apple: ^4.0.0 + window_manager: ^0.2.5 # bitsdojo_window: ^0.1.2 # quick_actions: ^0.2.1 # idb_shim: ^1.11.1+1 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d6eac6a2d..5a5588b1a 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,14 +7,20 @@ #include "generated_plugin_registrant.h" #include +#include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { PrintingPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PrintingPlugin")); + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); SentryFlutterPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SentryFlutterPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 53fcac6db..bd0b41963 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,8 +4,10 @@ list(APPEND FLUTTER_PLUGIN_LIST printing + screen_retriever sentry_flutter url_launcher_windows + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST