From 66b8ce8fe8ce1237bb8eba3bd7d97584646825a5 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 24 Feb 2023 13:56:07 +0100 Subject: [PATCH 01/15] Add systray icons. --- resources/icons/com.yubico.yubioath-32x32.png | Bin 0 -> 979 bytes resources/icons/systray-template.eps | 122 ++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100755 resources/icons/com.yubico.yubioath-32x32.png create mode 100755 resources/icons/systray-template.eps diff --git a/resources/icons/com.yubico.yubioath-32x32.png b/resources/icons/com.yubico.yubioath-32x32.png new file mode 100755 index 0000000000000000000000000000000000000000..372b9718fb849a0da3a5b6ebb44fc5fd5aaafeb0 GIT binary patch literal 979 zcmV;^11$WBP)7l`evfwkGvCbYE=M9AeGRt-fvnTI{Tj=sirK@hC835ZVL(TK(`C)&jG8gdTl$?e`o!JwA3+Bw?0Z;Fab zf~3>&lnNY2H#L+X?4gjB-n_1$kZ3p>_hb$;e)^1)3iN*|1#&fsRwd)`_Qc6w%qT>C z>?x(O(R{Yv+&Y+2Br;Z7a10rkas2tICg-vdX$(<3pGl6N21(t+wKSnX(V_7J*OR7C(C&puE@wRIEZ5?gSr_c$J`ne=8d z~nlq3_8H07x<>CMHj64^NQg$R9dty3345qthBiF+_Xx zO-#h6-;XFVAC~P&sS&|*7(ZXp8D~5eh`3PM#UhiG0EGSB+g Date: Fri, 24 Feb 2023 13:59:09 +0100 Subject: [PATCH 02/15] Add new dependencies for systray. --- .github/workflows/linux.yml | 2 +- linux/flutter/generated_plugin_registrant.cc | 8 ++++ linux/flutter/generated_plugins.cmake | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 ++ macos/Podfile.lock | 12 ++++++ pubspec.lock | 40 +++++++++++++++++++ pubspec.yaml | 3 ++ .../flutter/generated_plugin_registrant.cc | 6 +++ windows/flutter/generated_plugins.cmake | 2 + 9 files changed, 78 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 50a80bce..fce292f3 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -18,7 +18,7 @@ jobs: - name: Install dependencies run: | apt-get update - apt-get install -qq software-properties-common + apt-get install -qq software-properties-common libnotify-dev libayatana-appindicator3-dev add-apt-repository -y ppa:git-core/ppa add-apt-repository -y ppa:deadsnakes/ppa apt-get install -qq git python$PYVER-dev python$PYVER-venv diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 7e11ab27..245f7282 100755 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,7 +7,9 @@ #include "generated_plugin_registrant.h" #include +#include #include +#include #include #include @@ -15,9 +17,15 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) desktop_drop_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); + g_autoptr(FlPluginRegistrar) local_notifier_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin"); + local_notifier_plugin_register_with_registrar(local_notifier_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) tray_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); + tray_manager_plugin_register_with_registrar(tray_manager_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); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 01f60315..72bfabc3 100755 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,7 +4,9 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_drop + local_notifier screen_retriever + tray_manager url_launcher_linux window_manager ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 48c04949..1d39e9d9 100755 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,15 +6,19 @@ import FlutterMacOS import Foundation import desktop_drop +import local_notifier import screen_retriever import shared_preferences_foundation +import tray_manager import url_launcher_macos import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) + LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 93c48787..0aaa6955 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,11 +2,15 @@ PODS: - desktop_drop (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) + - local_notifier (0.1.0): + - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - tray_manager (0.0.1): + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS - window_manager (0.2.0): @@ -15,8 +19,10 @@ PODS: DEPENDENCIES: - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos`) + - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) @@ -25,10 +31,14 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos FlutterMacOS: :path: Flutter/ephemeral + local_notifier: + :path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos + tray_manager: + :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos window_manager: @@ -37,8 +47,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca + tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 diff --git a/pubspec.lock b/pubspec.lock index 5aad0eb9..588c408a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -380,6 +380,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + local_notifier: + dependency: "direct main" + description: + name: local_notifier + sha256: cc855aa6362c8840e3d3b35b1c3b058a3a8becdb2b03d5a9aa3f3a1e861f0a03 + url: "https://pub.dev" + source: hosted + version: "0.1.5" logging: dependency: "direct main" description: @@ -404,6 +412,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + menu_base: + dependency: transitive + description: + name: menu_base + sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" + url: "https://pub.dev" + source: hosted + version: "0.1.1" meta: dependency: transitive description: @@ -603,6 +619,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + shortid: + dependency: transitive + description: + name: shortid + sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + url: "https://pub.dev" + source: hosted + version: "0.1.2" sky_engine: dependency: transitive description: flutter @@ -704,6 +728,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + sha256: b1975a05e0c6999e983cf9a58a6a098318c896040ccebac5398a3cc9e43b9c69 + url: "https://pub.dev" + source: hosted + version: "0.2.0" typed_data: dependency: transitive description: @@ -776,6 +808,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7c5b8e8c..63b8f14b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,8 @@ dependencies: path: android/flutter_plugins/qrscanner_zxing desktop_drop: ^0.4.0 url_launcher: ^6.1.7 + tray_manager: ^0.2.0 + local_notifier: ^0.1.5 dev_dependencies: integration_test: @@ -92,6 +94,7 @@ flutter: - assets/graphics/ - assets/licenses/ - assets/licenses/raw/ + - resources/icons/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d35fb0a0..83aed1fe 100755 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,15 +7,21 @@ #include "generated_plugin_registrant.h" #include +#include #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { DesktopDropPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopDropPlugin")); + LocalNotifierPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("LocalNotifierPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + TrayManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0d6d49e8..9a6162a7 100755 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,7 +4,9 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_drop + local_notifier screen_retriever + tray_manager url_launcher_windows window_manager ) From fc3238e829f13f8848c15cf43f62b9bd64db768c Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 24 Feb 2023 14:10:39 +0100 Subject: [PATCH 03/15] Allow hiding the window. --- lib/app/models.dart | 1 + lib/app/models.freezed.dart | 36 ++++++++++++++++++++++++++------- lib/desktop/init.dart | 35 +++++++++++++++++++++++--------- lib/desktop/state.dart | 35 +++++++++++++++++++++++--------- macos/Runner/AppDelegate.swift | 3 ++- windows/runner/win32_window.cpp | 4 +++- 6 files changed, 86 insertions(+), 28 deletions(-) diff --git a/lib/app/models.dart b/lib/app/models.dart index 1670b3ec..b3363d5c 100755 --- a/lib/app/models.dart +++ b/lib/app/models.dart @@ -134,5 +134,6 @@ class WindowState with _$WindowState { required bool focused, required bool visible, required bool active, + @Default(false) bool hidden, }) = _WindowState; } diff --git a/lib/app/models.freezed.dart b/lib/app/models.freezed.dart index c7e8d4b1..945299b5 100644 --- a/lib/app/models.freezed.dart +++ b/lib/app/models.freezed.dart @@ -799,6 +799,7 @@ mixin _$WindowState { bool get focused => throw _privateConstructorUsedError; bool get visible => throw _privateConstructorUsedError; bool get active => throw _privateConstructorUsedError; + bool get hidden => throw _privateConstructorUsedError; @JsonKey(ignore: true) $WindowStateCopyWith get copyWith => @@ -811,7 +812,7 @@ abstract class $WindowStateCopyWith<$Res> { WindowState value, $Res Function(WindowState) then) = _$WindowStateCopyWithImpl<$Res, WindowState>; @useResult - $Res call({bool focused, bool visible, bool active}); + $Res call({bool focused, bool visible, bool active, bool hidden}); } /// @nodoc @@ -830,6 +831,7 @@ class _$WindowStateCopyWithImpl<$Res, $Val extends WindowState> Object? focused = null, Object? visible = null, Object? active = null, + Object? hidden = null, }) { return _then(_value.copyWith( focused: null == focused @@ -844,6 +846,10 @@ class _$WindowStateCopyWithImpl<$Res, $Val extends WindowState> ? _value.active : active // ignore: cast_nullable_to_non_nullable as bool, + hidden: null == hidden + ? _value.hidden + : hidden // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -856,7 +862,7 @@ abstract class _$$_WindowStateCopyWith<$Res> __$$_WindowStateCopyWithImpl<$Res>; @override @useResult - $Res call({bool focused, bool visible, bool active}); + $Res call({bool focused, bool visible, bool active, bool hidden}); } /// @nodoc @@ -873,6 +879,7 @@ class __$$_WindowStateCopyWithImpl<$Res> Object? focused = null, Object? visible = null, Object? active = null, + Object? hidden = null, }) { return _then(_$_WindowState( focused: null == focused @@ -887,6 +894,10 @@ class __$$_WindowStateCopyWithImpl<$Res> ? _value.active : active // ignore: cast_nullable_to_non_nullable as bool, + hidden: null == hidden + ? _value.hidden + : hidden // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -895,7 +906,10 @@ class __$$_WindowStateCopyWithImpl<$Res> class _$_WindowState implements _WindowState { _$_WindowState( - {required this.focused, required this.visible, required this.active}); + {required this.focused, + required this.visible, + required this.active, + this.hidden = false}); @override final bool focused; @@ -903,10 +917,13 @@ class _$_WindowState implements _WindowState { final bool visible; @override final bool active; + @override + @JsonKey() + final bool hidden; @override String toString() { - return 'WindowState(focused: $focused, visible: $visible, active: $active)'; + return 'WindowState(focused: $focused, visible: $visible, active: $active, hidden: $hidden)'; } @override @@ -916,11 +933,13 @@ class _$_WindowState implements _WindowState { other is _$_WindowState && (identical(other.focused, focused) || other.focused == focused) && (identical(other.visible, visible) || other.visible == visible) && - (identical(other.active, active) || other.active == active)); + (identical(other.active, active) || other.active == active) && + (identical(other.hidden, hidden) || other.hidden == hidden)); } @override - int get hashCode => Object.hash(runtimeType, focused, visible, active); + int get hashCode => + Object.hash(runtimeType, focused, visible, active, hidden); @JsonKey(ignore: true) @override @@ -933,7 +952,8 @@ abstract class _WindowState implements WindowState { factory _WindowState( {required final bool focused, required final bool visible, - required final bool active}) = _$_WindowState; + required final bool active, + final bool hidden}) = _$_WindowState; @override bool get focused; @@ -942,6 +962,8 @@ abstract class _WindowState implements WindowState { @override bool get active; @override + bool get hidden; + @override @JsonKey(ignore: true) _$$_WindowStateCopyWith<_$_WindowState> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/desktop/init.dart b/lib/desktop/init.dart index dd3d0825..ad109a2c 100755 --- a/lib/desktop/init.dart +++ b/lib/desktop/init.dart @@ -52,9 +52,9 @@ final _log = Logger('desktop.init'); const String _keyWidth = 'DESKTOP_WINDOW_WIDTH'; const String _keyHeight = 'DESKTOP_WINDOW_HEIGHT'; -class _WindowResizeListener extends WindowListener { +class _WindowEventListener extends WindowListener { final SharedPreferences _prefs; - _WindowResizeListener(this._prefs); + _WindowEventListener(this._prefs); @override void onWindowResize() async { @@ -62,6 +62,13 @@ class _WindowResizeListener extends WindowListener { await _prefs.setDouble(_keyWidth, size.width); await _prefs.setDouble(_keyHeight, size.height); } + + @override + void onWindowClose() async { + if (Platform.isMacOS) { + await windowManager.destroy(); + } + } } Future initialize(List argv) async { @@ -69,14 +76,24 @@ Future initialize(List argv) async { await windowManager.ensureInitialized(); final prefs = await SharedPreferences.getInstance(); + final isHidden = prefs.getBool(windowHidden) ?? false; - unawaited(windowManager.waitUntilReadyToShow().then((_) async { - await windowManager.setMinimumSize(const Size(270, 0)); - final width = prefs.getDouble(_keyWidth) ?? 400; - final height = prefs.getDouble(_keyHeight) ?? 720; - await windowManager.setSize(Size(width, height)); - await windowManager.show(); - windowManager.addListener(_WindowResizeListener(prefs)); + unawaited(windowManager + .waitUntilReadyToShow(WindowOptions( + minimumSize: const Size(270, 0), + size: Size( + prefs.getDouble(_keyWidth) ?? 400, + prefs.getDouble(_keyHeight) ?? 720, + ), + skipTaskbar: isHidden, + )) + .then((_) async { + if (isHidden) { + await windowManager.setSkipTaskbar(true); + } else { + await windowManager.show(); + } + windowManager.addListener(_WindowEventListener(prefs)); })); // Either use the _HELPER_PATH environment variable, or look relative to executable. diff --git a/lib/desktop/state.dart b/lib/desktop/state.dart index c5cce453..1a7963e4 100755 --- a/lib/desktop/state.dart +++ b/lib/desktop/state.dart @@ -24,8 +24,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:yubico_authenticator/app/logging.dart'; +import '../app/logging.dart'; import '../app/models.dart'; import '../app/state.dart'; import '../core/state.dart'; @@ -58,20 +58,23 @@ class _RpcStateNotifier extends StateNotifier { } } -final _windowStateProvider = - StateNotifierProvider<_WindowStateNotifier, WindowState>( - (ref) => _WindowStateNotifier()); +final desktopWindowStateProvider = + StateNotifierProvider( + (ref) => DesktopWindowStateNotifier(ref.watch(prefProvider))); -final desktopWindowStateProvider = Provider( - (ref) => ref.watch(_windowStateProvider), -); +const String windowHidden = 'DESKTOP_WINDOW_HIDDEN'; -class _WindowStateNotifier extends StateNotifier +class DesktopWindowStateNotifier extends StateNotifier with WindowListener { + final SharedPreferences _prefs; Timer? _idleTimer; - _WindowStateNotifier() - : super(WindowState(focused: true, visible: true, active: true)) { + DesktopWindowStateNotifier(this._prefs) + : super(WindowState( + focused: true, + visible: true, + active: true, + hidden: _prefs.getBool(windowHidden) ?? false)) { _init(); } @@ -88,6 +91,17 @@ class _WindowStateNotifier extends StateNotifier } } + void setWindowHidden(bool hidden) async { + if (hidden) { + await windowManager.hide(); + } else { + await windowManager.show(); + } + await windowManager.setSkipTaskbar(hidden); + await _prefs.setBool(windowHidden, hidden); + state = state.copyWith(hidden: hidden); + } + @override void dispose() { windowManager.removeListener(this); @@ -101,6 +115,7 @@ class _WindowStateNotifier extends StateNotifier } @override + @protected void onWindowEvent(String eventName) { if (mounted) { switch (eventName) { diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index d53ef643..91cfe9a7 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -4,6 +4,7 @@ import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true + // Keep app running if window closes + return false } } diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index dab3e7ed..1249f952 100644 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -172,7 +172,9 @@ bool Win32Window::Create(const std::wstring& title, } bool Win32Window::Show() { - return ShowWindow(window_handle_, SW_SHOWNORMAL); + // We show the mindow manually + return true; + //return ShowWindow(window_handle_, SW_SHOWNORMAL); } // static From 1811344ed4010a2a1cef9ab60c845ffe6a9a109f Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 24 Feb 2023 14:17:14 +0100 Subject: [PATCH 04/15] Add notification support. --- lib/app/views/user_interaction.dart | 76 +++++++++++++++++++++++++++++ lib/desktop/init.dart | 6 +++ lib/desktop/oath/state.dart | 28 ++++++----- lib/l10n/app_en.arb | 2 + 4 files changed, 101 insertions(+), 11 deletions(-) diff --git a/lib/app/views/user_interaction.dart b/lib/app/views/user_interaction.dart index fbd1679a..888bcccf 100755 --- a/lib/app/views/user_interaction.dart +++ b/lib/app/views/user_interaction.dart @@ -17,6 +17,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:local_notifier/local_notifier.dart'; import '../message.dart'; @@ -130,6 +131,24 @@ UserInteractionController promptUserInteraction( required String description, Widget? icon, void Function()? onCancel, + bool headless = false, +}) { + if (headless) { + // No support for icon or onCancel. + return _notificationUserInteraction(context, + title: title, description: description); + } else { + return _dialogUserInteraction(context, + title: title, description: description, icon: icon, onCancel: onCancel); + } +} + +UserInteractionController _dialogUserInteraction( + BuildContext context, { + required String title, + required String description, + Widget? icon, + void Function()? onCancel, }) { var wasPopped = false; final controller = _UserInteractionController( @@ -164,3 +183,60 @@ UserInteractionController promptUserInteraction( return controller; } + +class _NotificationUserInteractionController extends UserInteractionController { + String title; + String description; + Widget? icon; + LocalNotification _notification; + _NotificationUserInteractionController({ + required this.title, + required this.description, + }) : _notification = LocalNotification( + title: title, + body: description, + )..show(); + + @override + void close() { + _notification.close(); + } + + Future _doUpdateNotification() async { + await _notification.close(); + await Future.delayed(const Duration(milliseconds: 200)); + _notification = LocalNotification(title: title, body: description); + await _notification.show(); + } + + @override + void updateContent({String? title, String? description, Widget? icon}) { + bool changed = false; + if (title != null) { + this.title = title; + changed = true; + } + if (description != null) { + this.description = description; + changed = true; + } + if (icon != null) { + this.icon = icon; + } + + if (changed) { + _doUpdateNotification(); + } + } +} + +UserInteractionController _notificationUserInteraction( + BuildContext context, { + required String title, + required String description, +}) { + return _NotificationUserInteractionController( + title: title, + description: description, + ); +} diff --git a/lib/desktop/init.dart b/lib/desktop/init.dart index ad109a2c..e6ba6555 100755 --- a/lib/desktop/init.dart +++ b/lib/desktop/init.dart @@ -23,6 +23,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:local_notifier/local_notifier.dart'; import 'package:logging/logging.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; @@ -123,6 +124,11 @@ Future initialize(List argv) async { final rpcFuture = _initHelper(exe!); _initLicenses(); + await localNotifier.setup( + appName: 'Yubico Authenticator', + shortcutPolicy: ShortcutPolicy.ignore, + ); + return ProviderScope( overrides: [ supportedAppsProvider.overrideWithValue([ diff --git a/lib/desktop/oath/state.dart b/lib/desktop/oath/state.dart index 9d662c1f..f0ac600c 100755 --- a/lib/desktop/oath/state.dart +++ b/lib/desktop/oath/state.dart @@ -20,6 +20,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:logging/logging.dart'; import '../../app/logging.dart'; @@ -192,9 +193,9 @@ class _DesktopOathStateNotifier extends OathStateNotifier { } final desktopOathCredentialListProvider = StateNotifierProvider.autoDispose - .family?, DevicePath>( + .family?, DevicePath>( (ref, devicePath) { - var notifier = _DesktopCredentialListNotifier( + var notifier = DesktopCredentialListNotifier( ref.watch(withContextProvider), ref.watch(_sessionProvider(devicePath)), ref.watch(oathStateProvider(devicePath) @@ -203,6 +204,7 @@ final desktopOathCredentialListProvider = StateNotifierProvider.autoDispose ref.listen(windowStateProvider, (_, windowState) { notifier._notifyWindowState(windowState); }, fireImmediately: true); + return notifier; }, ); @@ -225,12 +227,12 @@ String _formatSteam(String response) { return value; } -class _DesktopCredentialListNotifier extends OathCredentialListNotifier { +class DesktopCredentialListNotifier extends OathCredentialListNotifier { final WithContext _withContext; final RpcNodeSession _session; final bool _locked; Timer? _timer; - _DesktopCredentialListNotifier(this._withContext, this._session, this._locked) + DesktopCredentialListNotifier(this._withContext, this._session, this._locked) : super(); void _notifyWindowState(WindowState windowState) { @@ -251,7 +253,7 @@ class _DesktopCredentialListNotifier extends OathCredentialListNotifier { @override Future calculate(OathCredential credential, - {bool update = true}) async { + {bool update = true, bool headless = false}) async { var now = DateTime.now().millisecondsSinceEpoch ~/ 1000; if (update) { // Manually triggered, need to pad timer to avoid immediate expiration @@ -264,12 +266,16 @@ class _DesktopCredentialListNotifier extends OathCredentialListNotifier { signaler.signals.listen((signal) async { if (signal.status == 'touch') { controller = await _withContext( - (context) async => promptUserInteraction( - context, - icon: const Icon(Icons.touch_app), - title: 'Touch Required', - description: 'Touch the button on your YubiKey now.', - ), + (context) async { + final l10n = AppLocalizations.of(context)!; + return promptUserInteraction( + context, + icon: const Icon(Icons.touch_app), + title: l10n.oath_touch_required, + description: l10n.oath_touch_now, + headless: headless, + ); + }, ); } }); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 63332f39..784408d1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -25,6 +25,8 @@ "oath_scanned_qr": "Scanned QR code", "oath_scan_qr": "Scan QR code", "oath_require_touch": "Require touch", + "oath_touch_required": "Touch Required", + "oath_touch_now": "Touch the button on your YubiKey now", "oath_sec": "sec", "oath_digits": "digits", "oath_success_delete_account": "Account deleted", From 274989c22b3aaacb402a8db95b64717d28d8fe1d Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 24 Feb 2023 14:19:34 +0100 Subject: [PATCH 05/15] OATH cleanups. --- lib/oath/state.dart | 16 +++++++++------- lib/oath/views/delete_account_dialog.dart | 8 +++----- lib/oath/views/oath_screen.dart | 2 +- lib/oath/views/rename_account_dialog.dart | 7 ++----- lib/oath/views/utils.dart | 7 +++++++ 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/lib/oath/state.dart b/lib/oath/state.dart index 03751de7..b3824dc8 100755 --- a/lib/oath/state.dart +++ b/lib/oath/state.dart @@ -66,7 +66,14 @@ abstract class OathCredentialListNotifier @override @protected set state(List? value) { - super.state = value != null ? List.unmodifiable(value) : null; + super.state = value != null + ? List.unmodifiable(value + ..sort((a, b) { + String searchKey(OathCredential c) => + ((c.issuer ?? '') + c.name).toLowerCase(); + return searchKey(a.credential).compareTo(searchKey(b.credential)); + })) + : null; } Future calculate(OathCredential credential); @@ -193,11 +200,6 @@ class FilteredCredentialsNotifier extends StateNotifier> { .toLowerCase() .contains(query.toLowerCase())) .where((pair) => pair.credential.issuer != '_hidden') - .toList() - ..sort((a, b) { - String searchKey(OathCredential c) => - ((c.issuer ?? '') + c.name).toLowerCase(); - return searchKey(a.credential).compareTo(searchKey(b.credential)); - }), + .toList(), ); } diff --git a/lib/oath/views/delete_account_dialog.dart b/lib/oath/views/delete_account_dialog.dart index dd27a1e8..462523c9 100755 --- a/lib/oath/views/delete_account_dialog.dart +++ b/lib/oath/views/delete_account_dialog.dart @@ -26,6 +26,7 @@ import '../../widgets/responsive_dialog.dart'; import '../models.dart'; import '../state.dart'; import '../keys.dart' as keys; +import 'utils.dart'; class DeleteAccountDialog extends ConsumerWidget { final DeviceNode device; @@ -34,10 +35,6 @@ class DeleteAccountDialog extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final label = credential.issuer != null - ? '${credential.issuer} (${credential.name})' - : credential.name; - return ResponsiveDialog( title: Text(AppLocalizations.of(context)!.oath_delete_account), actions: [ @@ -75,7 +72,8 @@ class DeleteAccountDialog extends ConsumerWidget { AppLocalizations.of(context)!.oath_warning_disable_this_cred, style: Theme.of(context).textTheme.bodyLarge, ), - Text('${AppLocalizations.of(context)!.oath_account} $label'), + Text( + '${AppLocalizations.of(context)!.oath_account} ${getTextName(credential)}'), ] .map((e) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), diff --git a/lib/oath/views/oath_screen.dart b/lib/oath/views/oath_screen.dart index a8e527e0..e6d877ac 100755 --- a/lib/oath/views/oath_screen.dart +++ b/lib/oath/views/oath_screen.dart @@ -125,7 +125,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { } return Actions( actions: { - SearchIntent: CallbackAction(onInvoke: (_) { + SearchIntent: CallbackAction(onInvoke: (_) { searchController.selection = TextSelection( baseOffset: 0, extentOffset: searchController.text.length); searchFocus.requestFocus(); diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index bc8993b6..24555377 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -96,10 +96,6 @@ class _RenameAccountDialogState extends ConsumerState { Widget build(BuildContext context) { final credential = widget.credential; - final label = credential.issuer != null - ? '${credential.issuer} (${credential.name})' - : credential.name; - final remaining = getRemainingKeySpace( oathType: credential.oathType, period: credential.period, @@ -142,7 +138,8 @@ class _RenameAccountDialogState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(AppLocalizations.of(context)!.oath_rename(label)), + Text(AppLocalizations.of(context)! + .oath_rename(getTextName(credential))), Text(AppLocalizations.of(context)! .oath_warning_will_change_account_displayed), TextFormField( diff --git a/lib/oath/views/utils.dart b/lib/oath/views/utils.dart index 3c863346..8e59f8d4 100755 --- a/lib/oath/views/utils.dart +++ b/lib/oath/views/utils.dart @@ -47,3 +47,10 @@ Pair getRemainingKeySpace( remaining - issuerSpace, ); } + +/// Gets a textual name for the account, based on the issuer and name. +String getTextName(OathCredential credential) { + return credential.issuer != null + ? '${credential.issuer} (${credential.name})' + : credential.name; +} From 758776d864b117ba72350330a341631baf5ff4ad Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 24 Feb 2023 14:20:17 +0100 Subject: [PATCH 06/15] Add AppLocalizations Provider. --- lib/app/state.dart | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/app/state.dart b/lib/app/state.dart index 09b4c11b..9317a040 100755 --- a/lib/app/state.dart +++ b/lib/app/state.dart @@ -15,11 +15,13 @@ */ import 'dart:async'; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:yubico_authenticator/app/logging.dart'; import '../core/state.dart'; @@ -40,6 +42,32 @@ final supportedThemesProvider = StateProvider>( (ref) => throw UnimplementedError(), ); +final _l10nProvider = StateNotifierProvider<_L10nNotifier, AppLocalizations>( + (ref) => _L10nNotifier()); + +final l10nProvider = Provider( + (ref) => ref.watch(_l10nProvider), +); + +class _L10nNotifier extends StateNotifier + with WidgetsBindingObserver { + _L10nNotifier() : super(lookupAppLocalizations(window.locale)) { + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + @protected + void didChangeLocales(List? locales) { + state = lookupAppLocalizations(window.locale); + } +} + final themeModeProvider = StateNotifierProvider( (ref) => ThemeModeNotifier( ref.watch(prefProvider), ref.read(supportedThemesProvider)), From 15e310ca383d97374c05839ed3761d130151e175 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 24 Feb 2023 14:22:36 +0100 Subject: [PATCH 07/15] Add systray. --- lib/desktop/init.dart | 4 + lib/desktop/systray.dart | 221 +++++++++++++++++++++++++++++++++++++++ lib/l10n/app_en.arb | 16 ++- 3 files changed, 240 insertions(+), 1 deletion(-) create mode 100755 lib/desktop/systray.dart diff --git a/lib/desktop/init.dart b/lib/desktop/init.dart index e6ba6555..d90697ac 100755 --- a/lib/desktop/init.dart +++ b/lib/desktop/init.dart @@ -48,6 +48,7 @@ import 'rpc.dart'; import 'devices.dart'; import 'qr_scanner.dart'; import 'state.dart'; +import 'systray.dart'; final _log = Logger('desktop.init'); const String _keyWidth = 'DESKTOP_WINDOW_WIDTH'; @@ -178,6 +179,9 @@ Future initialize(List argv) async { ref.read(rpcProvider).valueOrNull?.setLogLevel(level); }); + // Initialize systray + ref.watch(systrayProvider); + // Show a loading or error page while the Helper isn't ready return ref.watch(rpcProvider).when( data: (data) => const MainPage(), diff --git a/lib/desktop/systray.dart b/lib/desktop/systray.dart new file mode 100755 index 00000000..eaa14aeb --- /dev/null +++ b/lib/desktop/systray.dart @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2023 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:local_notifier/local_notifier.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../app/models.dart'; +import '../app/shortcuts.dart'; +import '../app/state.dart'; +import '../core/models.dart'; +import '../exception/cancellation_exception.dart'; +import '../oath/models.dart'; +import '../oath/state.dart'; +import '../oath/views/utils.dart'; +import 'oath/state.dart'; +import 'state.dart'; + +final _favoriteAccounts = + Provider.autoDispose>>( + (ref) { + final devicePath = ref.watch(currentDeviceProvider)?.path; + if (devicePath != null) { + final credentials = + ref.watch(desktopOathCredentialListProvider(devicePath)); + final favorites = ref.watch(favoritesProvider); + final listed = credentials + ?.map((e) => e.credential) + .where((c) => favorites.contains(c.id)) + .toList() ?? + []; + return Pair(devicePath, listed); + } + return Pair(null, []); + }, +); + +final systrayProvider = Provider.autoDispose((ref) { + final systray = _Systray(ref, ref.watch(l10nProvider)); + + // Keep track of which accounts to show + ref.listen( + _favoriteAccounts, + (_, next) { + systray._updateCredentials(next); + }, + ); + + // Keep track of the shown/hidden state of the app + ref.listen(windowStateProvider.select((value) => value.hidden), (_, hidden) { + systray._setHidden(hidden); + }, fireImmediately: true); + + ref.onDispose(systray.dispose); + + return systray; +}); + +Future _calculateCode( + DevicePath devicePath, OathCredential credential, Ref ref) async { + try { + return await (ref + .read(desktopOathCredentialListProvider(devicePath).notifier)) + .calculate(credential, headless: true); + } on CancellationException catch (_) { + return null; + } +} + +String _getIcon() { + if (Platform.isMacOS) { + return 'resources/icons/systray-template.eps'; + } + if (Platform.isWindows) { + return 'resources/icons/com.yubico.yubioath.ico'; + } + return 'resources/icons/com.yubico.yubioath-32x32.png'; +} + +class _Systray extends TrayListener { + final Ref _ref; + final AppLocalizations _l10n; + int _lastClick = 0; + DevicePath _devicePath = DevicePath([]); + List _credentials = []; + bool _isHidden = false; + _Systray(this._ref, this._l10n) { + _init(); + } + + Future _init() async { + await trayManager.setIcon(_getIcon(), isTemplate: true); + if (!Platform.isLinux) { + await trayManager.setToolTip(_l10n.general_app_name); + } + await _updateContextMenu(); + + // Doesn't seem to work on Linux + trayManager.addListener(this); + } + + void dispose() { + trayManager.destroy(); + } + + void _updateCredentials(Pair> pair) { + if (!listEquals(_credentials, pair.second)) { + _devicePath = pair.first ?? _devicePath; + _credentials = pair.second; + _updateContextMenu(); + } + } + + Future _setHidden(bool hidden) async { + _isHidden = hidden; + await _updateContextMenu(); + } + + @override + void onTrayIconMouseDown() { + if (Platform.isMacOS) { + trayManager.popUpContextMenu(); + } else { + final now = DateTime.now().millisecondsSinceEpoch; + if (now - _lastClick < 500) { + _lastClick = 0; + if (_isHidden) { + _ref.read(desktopWindowStateProvider.notifier).setWindowHidden(false); + } else { + windowManager.focus(); + } + } else { + _lastClick = now; + } + } + } + + @override + void onTrayIconRightMouseDown() { + trayManager.popUpContextMenu(); + } + + Future _updateContextMenu() async { + await trayManager.setContextMenu( + Menu( + items: [ + ..._credentials.map( + (e) { + final label = getTextName(e); + return MenuItem( + label: label, + onClick: (_) async { + final code = await _calculateCode(_devicePath, e, _ref); + if (code != null) { + await _ref + .read(clipboardProvider) + .setText(code.value, isSensitive: true); + final notification = LocalNotification( + title: _l10n.systray_oath_copied, + body: _l10n.systray_oath_copied_to_clipboard(label), + silent: true, + ); + await notification.show(); + await Future.delayed(const Duration(seconds: 4)); + await notification.close(); + } + }, + ); + }, + ), + if (_credentials.isEmpty) + MenuItem( + label: _l10n.systray_no_pinned, + disabled: true, + ), + MenuItem.separator(), + MenuItem( + label: _isHidden + ? _l10n.general_show_window + : _l10n.general_hide_window, + onClick: (_) { + _ref + .read(desktopWindowStateProvider.notifier) + .setWindowHidden(!_isHidden); + }, + ), + MenuItem.separator(), + MenuItem( + label: _l10n.general_quit, + onClick: (_) { + _ref.read(withContextProvider)( + (context) async { + Actions.invoke(context, const CloseIntent()); + }, + ); + }), + ], + ), + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 784408d1..ddb8ad87 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -94,6 +94,7 @@ "mgmt_toggle_applications": "Toggle applications", "mgmt_save": "Save", + "general_app_name": "Yubico Authenticator", "general_about": "About", "general_terms_of_use": "Terms of use", "general_privacy_policy": "Privacy policy", @@ -119,6 +120,9 @@ "general_setup": "Setup", "general_manage": "Manage", "general_configure_yubikey": "Configure YubiKey", + "general_show_window": "Show window", + "general_hide_window": "Hide window", + "general_quit": "Quit", "fido_press_fingerprint_begin": "Press your finger against the YubiKey to begin.", "fido_keep_touching_yubikey": "Keep touching your YubiKey repeatedly...", @@ -261,5 +265,15 @@ "placeholders": { "version": {} } - } + }, + + + "systray_oath_copied": "Code copied", + "systray_oath_copied_to_clipboard": "{label} copied to clipboard.", + "@systray_oath_copied_to_clipboard" : { + "placeholders": { + "label": {} + } + }, + "systray_no_pinned": "No pinned accounts" } \ No newline at end of file From 68cbea833a4fdec29522a990cd339706a873cce2 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 24 Feb 2023 14:23:17 +0100 Subject: [PATCH 08/15] Fix copy to clipboard for Wayland. --- lib/desktop/state.dart | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/desktop/state.dart b/lib/desktop/state.dart index 1a7963e4..4b332cc5 100755 --- a/lib/desktop/state.dart +++ b/lib/desktop/state.dart @@ -159,7 +159,29 @@ class _DesktopClipboard extends AppClipboard { @override Future setText(String toClipboard, {bool isSensitive = false}) async { - await Clipboard.setData(ClipboardData(text: toClipboard)); + // Wayland requires the window to be focused to copy to clipboard + final needsFocus = Platform.isLinux && + Platform.environment['XDG_SESSION_TYPE'] == 'wayland'; + var hidden = false; + try { + if (needsFocus && !await windowManager.isFocused()) { + if (!await windowManager.isVisible()) { + hidden = true; + await windowManager.setOpacity(0.0); + await windowManager.show(); + } + await windowManager.focus(); + // Window focus isn't immediate, wait until focused with 10s timeout + await Future.doWhile(() async => !await windowManager.isFocused()) + .timeout(const Duration(seconds: 10)); + } + await Clipboard.setData(ClipboardData(text: toClipboard)); + } finally { + if (hidden) { + await windowManager.hide(); + await windowManager.setOpacity(1.0); + } + } } } From 6f7d15dfff8a1595f68e58af76b6541076b573b8 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 24 Feb 2023 14:23:43 +0100 Subject: [PATCH 09/15] Hide window on Ctrl/Cmd + W. --- lib/app/shortcuts.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/app/shortcuts.dart b/lib/app/shortcuts.dart index 625ebbf7..145249bd 100755 --- a/lib/app/shortcuts.dart +++ b/lib/app/shortcuts.dart @@ -24,6 +24,7 @@ import 'package:window_manager/window_manager.dart'; import '../about_page.dart'; import '../android/views/android_settings_page.dart'; import '../core/state.dart'; +import '../desktop/state.dart'; import '../oath/keys.dart'; import '../settings_page.dart'; import 'message.dart'; @@ -42,6 +43,10 @@ class CloseIntent extends Intent { const CloseIntent(); } +class HideIntent extends Intent { + const HideIntent(); +} + class SearchIntent extends Intent { const SearchIntent(); } @@ -77,6 +82,12 @@ Widget registerGlobalShortcuts( windowManager.close(); return null; }), + HideIntent: CallbackAction(onInvoke: (_) { + if (isDesktop) { + ref.read(desktopWindowStateProvider.notifier).setWindowHidden(true); + } + return null; + }), SearchIntent: CallbackAction(onInvoke: (intent) { // If the OATH view doesn't have focus, but is shown, find and select the search bar. final searchContext = searchAccountsField.currentContext; @@ -136,8 +147,7 @@ Widget registerGlobalShortcuts( child: Shortcuts( shortcuts: { LogicalKeySet(ctrlOrCmd, LogicalKeyboardKey.keyC): const CopyIntent(), - LogicalKeySet(ctrlOrCmd, LogicalKeyboardKey.keyW): - const CloseIntent(), + LogicalKeySet(ctrlOrCmd, LogicalKeyboardKey.keyW): const HideIntent(), LogicalKeySet(ctrlOrCmd, LogicalKeyboardKey.keyF): const SearchIntent(), if (isDesktop) ...{ From d4253d3e63295b2b5e34481b7d946f2921d58d5a Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 24 Feb 2023 14:24:13 +0100 Subject: [PATCH 10/15] Embed required libraries on Linux. --- .github/workflows/linux.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index fce292f3..3a5779b3 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -18,7 +18,7 @@ jobs: - name: Install dependencies run: | apt-get update - apt-get install -qq software-properties-common libnotify-dev libayatana-appindicator3-dev + apt-get install -qq software-properties-common libnotify-dev libayatana-appindicator3-dev patchelf add-apt-repository -y ppa:git-core/ppa add-apt-repository -y ppa:deadsnakes/ppa apt-get install -qq git python$PYVER-dev python$PYVER-venv @@ -82,6 +82,18 @@ jobs: - name: Check generated files run: git diff --exit-code + - name: Embedd appindicator + run: | + patchelf --set-rpath '$ORIGIN' build/linux/x64/release/bundle/lib/libtray_manager_plugin.so + cp -L /usr/lib/x86_64-linux-gnu/libayatana-appindicator3.so.1 build/linux/x64/release/bundle/lib/ + patchelf --set-rpath '$ORIGIN' build/linux/x64/release/bundle/lib/libayatana-appindicator3.so.1 + cp -L /usr/lib/x86_64-linux-gnu/libayatana-indicator3.so.7 build/linux/x64/release/bundle/lib/ + patchelf --set-rpath '$ORIGIN' build/linux/x64/release/bundle/lib/libayatana-indicator3.so.7 + cp -L /usr/lib/x86_64-linux-gnu/libdbusmenu-glib.so.4 build/linux/x64/release/bundle/lib/ + patchelf --set-rpath '$ORIGIN' build/linux/x64/release/bundle/lib/libdbusmenu-glib.so.4 + cp -L /usr/lib/x86_64-linux-gnu/libdbusmenu-gtk3.so.4 build/linux/x64/release/bundle/lib/ + patchelf --set-rpath '$ORIGIN' build/linux/x64/release/bundle/lib/libdbusmenu-gtk3.so.4 + - name: Rename and archive app run: | export REF=$(echo ${GITHUB_REF} | cut -d '/' -f 3) From 4491fb4b40d696aeb4d9967c27be7e5e1f30562a Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 24 Feb 2023 14:24:33 +0100 Subject: [PATCH 11/15] Add OS info to diagnostics output. --- lib/about_page.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/about_page.dart b/lib/about_page.dart index ebac783a..201c5d55 100755 --- a/lib/about_page.dart +++ b/lib/about_page.dart @@ -162,6 +162,8 @@ class AboutPage extends ConsumerWidget { data.insert(0, { 'app_version': version, 'dart': Platform.version, + 'os': Platform.operatingSystem, + 'os_version': Platform.operatingSystemVersion, }); final text = const JsonEncoder.withIndent(' ').convert(data); await ref.read(clipboardProvider).setText(text); From f840bd0dc1298ea7a7d75252e8a9c19f06f60a0e Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Fri, 24 Feb 2023 16:24:25 +0100 Subject: [PATCH 12/15] Don't try to open a session for NFC readers with no card. --- lib/desktop/systray.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/desktop/systray.dart b/lib/desktop/systray.dart index eaa14aeb..15dbcc58 100755 --- a/lib/desktop/systray.dart +++ b/lib/desktop/systray.dart @@ -39,17 +39,17 @@ import 'state.dart'; final _favoriteAccounts = Provider.autoDispose>>( (ref) { - final devicePath = ref.watch(currentDeviceProvider)?.path; - if (devicePath != null) { + final deviceData = ref.watch(currentDeviceDataProvider).valueOrNull; + if (deviceData != null) { final credentials = - ref.watch(desktopOathCredentialListProvider(devicePath)); + ref.watch(desktopOathCredentialListProvider(deviceData.node.path)); final favorites = ref.watch(favoritesProvider); final listed = credentials ?.map((e) => e.credential) .where((c) => favorites.contains(c.id)) .toList() ?? []; - return Pair(devicePath, listed); + return Pair(deviceData.node.path, listed); } return Pair(null, []); }, From 5e85c89c8fb6c2e72dc01a566237d47d39b1910a Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 27 Feb 2023 11:14:25 +0100 Subject: [PATCH 13/15] Fix opening OATH dialog in tests. --- integration_test/oath_test_util.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/integration_test/oath_test_util.dart b/integration_test/oath_test_util.dart index 8627c312..ed8844b6 100644 --- a/integration_test/oath_test_util.dart +++ b/integration_test/oath_test_util.dart @@ -112,6 +112,7 @@ extension OathFunctions on WidgetTester { } await shortWait(); + /// find an AccountView with issuer/name in the account list var matchingAccounts = find.descendant( of: findAccountList(), @@ -146,8 +147,11 @@ extension OathFunctions on WidgetTester { expect(accountView, isNotNull); if (accountView != null) { - await ensureVisible(find.byWidget(accountView)); - await tap(find.byWidget(accountView)); + final accountFinder = find.byWidget(accountView); + await ensureVisible(accountFinder); + final codeButtonFinder = find.descendant( + of: accountFinder, matching: find.bySubtype()); + await tap(codeButtonFinder); await shortWait(); } } From 4375f84a52b8043c8d9a3d33c46bbf81a5588d9b Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 27 Feb 2023 12:01:15 +0100 Subject: [PATCH 14/15] Avoid calling calculate on deleted/renamed credential from dialog. --- lib/oath/views/account_dialog.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/oath/views/account_dialog.dart b/lib/oath/views/account_dialog.dart index 4cd65efc..4c27f684 100755 --- a/lib/oath/views/account_dialog.dart +++ b/lib/oath/views/account_dialog.dart @@ -156,7 +156,10 @@ class AccountDialog extends ConsumerWidget { if (helper.code == null && (isDesktop || node.transport == Transport.usb)) { Timer.run(() { - Actions.invoke(context, const CalculateIntent()); + // Only call if credential hasn't been deleted/renamed + if (ref.read(credentialsProvider)?.contains(credential) == true) { + Actions.invoke(context, const CalculateIntent()); + } }); } return FocusScope( From cd458e1693af10f440edd28756c483354bb39688 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Mon, 27 Feb 2023 15:17:41 +0100 Subject: [PATCH 15/15] Update to Flutter 3.7.5 and latest dependencies. --- .github/workflows/android.yaml | 2 +- .github/workflows/linux.yml | 2 +- .github/workflows/macos.yml | 2 +- .github/workflows/windows.yml | 2 +- macos/Podfile.lock | 4 +- pubspec.lock | 96 +++++++++++++++++----------------- 6 files changed, 54 insertions(+), 54 deletions(-) diff --git a/.github/workflows/android.yaml b/.github/workflows/android.yaml index 7a8a3e9e..ad700fa6 100644 --- a/.github/workflows/android.yaml +++ b/.github/workflows/android.yaml @@ -17,7 +17,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.7.3' + flutter-version: '3.7.5' - run: | flutter config flutter --version diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 3a5779b3..998cc326 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest env: PYVER: 3.11 - FLUTTER: '3.7.3' + FLUTTER: '3.7.5' container: image: ubuntu:18.04 env: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 33c02e44..8eeaf139 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -49,7 +49,7 @@ jobs: with: channel: 'stable' architecture: 'x64' - flutter-version: '3.7.3' + flutter-version: '3.7.5' - run: flutter config --enable-macos-desktop - run: flutter --version diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 76a932c2..1c1acd55 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -45,7 +45,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.7.3' + flutter-version: '3.7.5' - run: flutter config --enable-windows-desktop - run: flutter --version diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 0aaa6955..f4a4b77c 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -49,9 +49,9 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca + shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472 tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 - url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2 + url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 diff --git a/pubspec.lock b/pubspec.lock index 588c408a..453b9cca 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "569ddca58d535e601dd1584afa117710abc999d036c0cd2c51777fb257df78e8" + sha256: e440ac42679dfc04bbbefb58ed225c994bc7e07fccc8a68ec7d3631a127e5da9 url: "https://pub.dev" source: hosted - version: "53.0.0" + version: "54.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "10927c4b7c7c88b1adbca278c3d5531db92e2f4b4abf04e2919a800af965f3f5" + sha256: "2c2e3721ee9fb36de92faa060f3480c81b23e904352b087e5c64224b1a044427" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.6.0" archive: dependency: transitive description: @@ -69,18 +69,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab" + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" build_runner: dependency: "direct dev" description: @@ -456,26 +456,26 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9" + sha256: "525ad5e07622d19447ad740b1ed5070031f7a5437f44355ae915ff56e986429a" url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.1.9" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + sha256: "642ddf65fde5404f83267e8459ddb4556316d3ee6d511ed193357e25caa3632d" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" platform: dependency: transitive description: @@ -488,10 +488,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pool: dependency: transitive description: @@ -551,58 +551,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9" + sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.18" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7" + sha256: a51a4f9375097f94df1c6e0a49c0374440d31ab026b59d58a7e7660675879db4 url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.16" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0" + sha256: "6b84fdf06b32bb336f972d373cd38b63734f3461ba56ac2ba01b56d052796259" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 + sha256: d7fb71e6e20cd3dfffcc823a28da3539b392e53ed5fc5c2b90b55fdaa8a7e8fa url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 + sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958 + sha256: "6737b757e49ba93de2a233df229d0b6a87728cea1684da828cbc718b65dcf9d7" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.5" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" + sha256: bd014168e8484837c39ef21065b78f305810ceabc1d4f90be6e3b392ce81b46d url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" shelf: dependency: transitive description: @@ -748,66 +748,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" url: "https://pub.dev" source: hosted - version: "6.1.9" + version: "6.1.10" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" + sha256: "1f4d9ebe86f333c15d318f81dcdc08b01d45da44af74552608455ebdc08d9732" url: "https://pub.dev" source: hosted - version: "6.0.23" + version: "6.0.24" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815" + sha256: c9cd648d2f7ab56968e049d4e9116f96a85517f1dd806b96a86ea1018a3a82e5 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.1.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" + sha256: e29039160ab3730e42f3d811dc2a6d5f2864b90a70fb765ea60144b03307f682 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" + sha256: "2dddb3291a57b074dade66b5e07e64401dd2487caefd4e9e2f467138d8c7eb06" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" + sha256: "574cfbe2390666003c3a1d129bdc4574aaa6728f0c00a4829a81c316de69dd9b" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.15" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 + sha256: "97c9067950a0d09cbd93e2e3f0383d1403989362b97102fbf446473a48079a4b" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" uuid: dependency: transitive description: @@ -868,10 +868,10 @@ packages: dependency: "direct main" description: name: window_manager - sha256: "5bdd29dc5f1f3185fc90696373a571d77968e03e5e820fb1ecdbdade3f5d8fff" + sha256: "492806c69879f0d28e95472bbe5e8d5940ac8c6e99cc07052fe14946974555ba" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.1" xdg_directories: dependency: transitive description: