From accce97dc66edb2e636dbb1c42a4d805b76dba71 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sat, 23 Nov 2024 20:24:00 -0300 Subject: [PATCH 01/11] feat: Introduce global shortcuts --- lib/main.dart | 6 +++++- lib/utils/keyboard.dart | 44 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 lib/utils/keyboard.dart diff --git a/lib/main.dart b/lib/main.dart index 6b12a7a1..22f32436 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -42,6 +42,7 @@ import 'package:bluecherry_client/screens/multi_window/single_layout_window.dart import 'package:bluecherry_client/screens/multi_window/window.dart'; import 'package:bluecherry_client/screens/players/live_player.dart'; import 'package:bluecherry_client/utils/app_links/app_links.dart' as app_links; +import 'package:bluecherry_client/utils/keyboard.dart'; import 'package:bluecherry_client/utils/logging.dart' as logging; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/storage.dart'; @@ -416,7 +417,10 @@ class _UnityAppState extends State builder: (context, child) { Intl.defaultLocale = Localizations.localeOf(context).languageCode; - return child!; + return CallbackShortcuts( + bindings: globalShortcuts(context), + child: child!, + ); }, ); }), diff --git a/lib/utils/keyboard.dart b/lib/utils/keyboard.dart new file mode 100644 index 00000000..8022a173 --- /dev/null +++ b/lib/utils/keyboard.dart @@ -0,0 +1,44 @@ +import 'package:bluecherry_client/providers/home_provider.dart'; +import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +Map globalShortcuts(BuildContext context) { + final settings = context.read(); + final home = context.read(); + + void setTab(int index) { + index--; + if (index >= 0 && index < UnityTab.values.length) { + final tab = NavigatorData.of(context)[index]; + home.setTab(tab.tab); + } + } + + return { + SingleActivator(LogicalKeyboardKey.f11): () { + settings.kFullscreen.value = !settings.kFullscreen.value; + }, + SingleActivator(LogicalKeyboardKey.f12): () { + settings.kImmersiveMode.value = !settings.kImmersiveMode.value; + }, + SingleActivator(LogicalKeyboardKey.digit1, alt: true): () => setTab(1), + SingleActivator(LogicalKeyboardKey.digit2, alt: true): () => setTab(2), + SingleActivator(LogicalKeyboardKey.numpad2, alt: true): () => setTab(2), + SingleActivator(LogicalKeyboardKey.digit3, alt: true): () => setTab(3), + SingleActivator(LogicalKeyboardKey.numpad3, alt: true): () => setTab(3), + SingleActivator(LogicalKeyboardKey.digit4, alt: true): () => setTab(4), + SingleActivator(LogicalKeyboardKey.numpad4, alt: true): () => setTab(4), + SingleActivator(LogicalKeyboardKey.digit5, alt: true): () => setTab(5), + SingleActivator(LogicalKeyboardKey.numpad5, alt: true): () => setTab(5), + SingleActivator(LogicalKeyboardKey.digit6, alt: true): () => setTab(6), + SingleActivator(LogicalKeyboardKey.numpad6, alt: true): () => setTab(6), + SingleActivator(LogicalKeyboardKey.digit7, alt: true): () => setTab(7), + SingleActivator(LogicalKeyboardKey.numpad7, alt: true): () => setTab(7), + SingleActivator(LogicalKeyboardKey.digit8, alt: true): () => setTab(8), + SingleActivator(LogicalKeyboardKey.numpad8, alt: true): () => setTab(8), + SingleActivator(LogicalKeyboardKey.digit9, alt: true): () => setTab(9), + SingleActivator(LogicalKeyboardKey.numpad9, alt: true): () => setTab(9), + }; +} From 608099afc1bedc49cca74b7cf0a46b0743797948 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sat, 23 Nov 2024 20:24:30 -0300 Subject: [PATCH 02/11] chore: Move NavigationData to home provider --- lib/providers/home_provider.dart | 70 ++++++++++++++++++++++++++++++ lib/screens/home.dart | 73 -------------------------------- lib/widgets/desktop_buttons.dart | 1 - 3 files changed, 70 insertions(+), 74 deletions(-) diff --git a/lib/providers/home_provider.dart b/lib/providers/home_provider.dart index 34590579..d500d4ee 100644 --- a/lib/providers/home_provider.dart +++ b/lib/providers/home_provider.dart @@ -22,6 +22,7 @@ import 'package:bluecherry_client/providers/layouts_provider.dart'; import 'package:bluecherry_client/providers/server_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; +import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/video_player.dart'; import 'package:flutter/foundation.dart'; @@ -104,6 +105,75 @@ enum UnityLoadingReason { } } +class NavigatorData { + /// The tab that this navigator data represents. + final UnityTab tab; + final IconData icon; + final IconData selectedIcon; + final String text; + + const NavigatorData({ + required this.tab, + required this.icon, + required this.selectedIcon, + required this.text, + }); + + static List of(BuildContext context) { + final loc = AppLocalizations.of(context); + final screenSize = MediaQuery.sizeOf(context); + final layout = context.read().currentLayout; + + return [ + NavigatorData( + tab: UnityTab.deviceGrid, + icon: Icons.window_outlined, + selectedIcon: Icons.window, + text: loc.screens(layout.name), + ), + NavigatorData( + tab: UnityTab.eventsTimeline, + icon: Icons.subscriptions_outlined, + selectedIcon: Icons.subscriptions, + text: loc.eventsTimeline, + ), + if (screenSize.width <= kMobileBreakpoint.width || + Scaffold.hasDrawer(context)) + NavigatorData( + tab: UnityTab.directCameraScreen, + icon: Icons.videocam_outlined, + selectedIcon: Icons.videocam, + text: loc.directCamera, + ), + NavigatorData( + tab: UnityTab.eventsHistory, + icon: Icons.featured_play_list_outlined, + selectedIcon: Icons.featured_play_list, + text: loc.eventBrowser, + ), + NavigatorData( + tab: UnityTab.addServer, + icon: Icons.dns_outlined, + selectedIcon: Icons.dns, + text: loc.addServer, + ), + if (!kIsWeb) + NavigatorData( + tab: UnityTab.downloads, + icon: Icons.download_outlined, + selectedIcon: Icons.download, + text: loc.downloads, + ), + NavigatorData( + tab: UnityTab.settings, + icon: Icons.settings_outlined, + selectedIcon: Icons.settings, + text: loc.settings, + ), + ]; + } +} + class HomeProvider extends ChangeNotifier { static final _instance = HomeProvider(); static HomeProvider get instance => _instance; diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 81904f67..479143ae 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -19,7 +19,6 @@ import 'package:animations/animations.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; -import 'package:bluecherry_client/providers/layouts_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/direct_camera.dart'; import 'package:bluecherry_client/screens/downloads/downloads_manager.dart'; @@ -28,84 +27,12 @@ import 'package:bluecherry_client/screens/events_timeline/events_playback.dart'; import 'package:bluecherry_client/screens/layouts/device_grid.dart'; import 'package:bluecherry_client/screens/servers/wizard.dart'; import 'package:bluecherry_client/screens/settings/settings.dart'; -import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; import 'package:bluecherry_client/widgets/search.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; -class NavigatorData { - /// The tab that this navigator data represents. - final UnityTab tab; - final IconData icon; - final IconData selectedIcon; - final String text; - - const NavigatorData({ - required this.tab, - required this.icon, - required this.selectedIcon, - required this.text, - }); - - static List of(BuildContext context) { - final loc = AppLocalizations.of(context); - final screenSize = MediaQuery.sizeOf(context); - final layout = context.watch().currentLayout; - - return [ - NavigatorData( - tab: UnityTab.deviceGrid, - icon: Icons.window_outlined, - selectedIcon: Icons.window, - text: loc.screens(layout.name), - ), - NavigatorData( - tab: UnityTab.eventsTimeline, - icon: Icons.subscriptions_outlined, - selectedIcon: Icons.subscriptions, - text: loc.eventsTimeline, - ), - if (screenSize.width <= kMobileBreakpoint.width || - Scaffold.hasDrawer(context)) - NavigatorData( - tab: UnityTab.directCameraScreen, - icon: Icons.videocam_outlined, - selectedIcon: Icons.videocam, - text: loc.directCamera, - ), - NavigatorData( - tab: UnityTab.eventsHistory, - icon: Icons.featured_play_list_outlined, - selectedIcon: Icons.featured_play_list, - text: loc.eventBrowser, - ), - NavigatorData( - tab: UnityTab.addServer, - icon: Icons.dns_outlined, - selectedIcon: Icons.dns, - text: loc.addServer, - ), - if (!kIsWeb) - NavigatorData( - tab: UnityTab.downloads, - icon: Icons.download_outlined, - selectedIcon: Icons.download, - text: loc.downloads, - ), - NavigatorData( - tab: UnityTab.settings, - icon: Icons.settings_outlined, - selectedIcon: Icons.settings, - text: loc.settings, - ), - ]; - } -} - class Home extends StatefulWidget { const Home({super.key}); diff --git a/lib/widgets/desktop_buttons.dart b/lib/widgets/desktop_buttons.dart index 8c03898c..0f03da5a 100644 --- a/lib/widgets/desktop_buttons.dart +++ b/lib/widgets/desktop_buttons.dart @@ -28,7 +28,6 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; import 'package:bluecherry_client/screens/events_browser/events_screen.dart'; import 'package:bluecherry_client/screens/events_timeline/events_playback.dart'; -import 'package:bluecherry_client/screens/home.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/squared_icon_button.dart'; From 96900a609c20e1c15a867f8bd3fbbc04125ce55e Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sat, 23 Nov 2024 20:27:32 -0300 Subject: [PATCH 03/11] fix: Only mark keyboard event as valid if the Timeline is currently valid --- .../events_timeline/desktop/timeline.dart | 2 + .../events_timeline/events_playback.dart | 85 +++++++++++-------- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/lib/screens/events_timeline/desktop/timeline.dart b/lib/screens/events_timeline/desktop/timeline.dart index 63d16491..662a8017 100644 --- a/lib/screens/events_timeline/desktop/timeline.dart +++ b/lib/screens/events_timeline/desktop/timeline.dart @@ -375,6 +375,8 @@ class Timeline extends ChangeNotifier { }); } + // TODO(bdlukaa): Only make it possible to seek between bounds. + // Currently, is is possible to seek before and after the day. /// Seeks forward by [duration] void seekForward([Duration duration = const Duration(seconds: 15)]) => seekTo(currentPosition + duration); diff --git a/lib/screens/events_timeline/events_playback.dart b/lib/screens/events_timeline/events_playback.dart index 1decb5e5..0a309fd9 100644 --- a/lib/screens/events_timeline/events_playback.dart +++ b/lib/screens/events_timeline/events_playback.dart @@ -195,6 +195,8 @@ class _EventsPlaybackState extends EventsScreenState { '${event.physicalKey}${event.physicalKey.debugName}', ); + final isTimelineValid = timeline!.tiles.isNotEmpty; + switch (event.logicalKey) { case LogicalKeyboardKey.arrowRight: timeline!.seekForward(); @@ -230,26 +232,18 @@ class _EventsPlaybackState extends EventsScreenState { return KeyEventResult.handled; case LogicalKeyboardKey.mediaSkipForward: case LogicalKeyboardKey.mediaTrackNext: - timeline!.seekToNextEvent(); - return KeyEventResult.handled; + final nextEvent = timeline!.seekToNextEvent(); + if (nextEvent != null) return KeyEventResult.handled; case LogicalKeyboardKey.mediaSkipBackward: case LogicalKeyboardKey.mediaTrackPrevious: - timeline!.seekToPreviousEvent(); - return KeyEventResult.handled; + final previousEvent = timeline!.seekToPreviousEvent(); + if (previousEvent != null) return KeyEventResult.handled; case LogicalKeyboardKey.mediaStepForward: timeline!.stepForward(); return KeyEventResult.handled; case LogicalKeyboardKey.mediaStepBackward: timeline!.stepBackward(); return KeyEventResult.handled; - case LogicalKeyboardKey.home: - case LogicalKeyboardKey.numpad0: - case LogicalKeyboardKey.digit0: - timeline!.seekTo(Duration.zero); - return KeyEventResult.handled; - case LogicalKeyboardKey.end: - timeline!.seekTo(timeline!.endPosition); - return KeyEventResult.handled; case LogicalKeyboardKey.keyM: if (timeline!.isMuted) { timeline!.volume = 1.0; @@ -263,47 +257,70 @@ class _EventsPlaybackState extends EventsScreenState { case LogicalKeyboardKey.arrowDown: timeline!.volume -= 0.1; return KeyEventResult.handled; - + case LogicalKeyboardKey.home: + case LogicalKeyboardKey.numpad0: + case LogicalKeyboardKey.digit0: + timeline!.seekTo(Duration.zero); + return KeyEventResult.handled; + case LogicalKeyboardKey.end: + timeline!.seekTo(timeline!.endPosition); + return KeyEventResult.handled; case LogicalKeyboardKey.numpad1: case LogicalKeyboardKey.digit1: - timeline!.seekTo(timeline!.endPosition * 0.1); - return KeyEventResult.handled; + if (isTimelineValid) { + timeline!.seekTo(timeline!.endPosition * 0.1); + return KeyEventResult.handled; + } case LogicalKeyboardKey.numpad2: case LogicalKeyboardKey.digit2: - timeline!.seekTo(timeline!.endPosition * 0.2); - return KeyEventResult.handled; + if (isTimelineValid) { + timeline!.seekTo(timeline!.endPosition * 0.2); + return KeyEventResult.handled; + } case LogicalKeyboardKey.numpad3: case LogicalKeyboardKey.digit3: - timeline!.seekTo(timeline!.endPosition * 0.3); - return KeyEventResult.handled; + if (isTimelineValid) { + timeline!.seekTo(timeline!.endPosition * 0.3); + return KeyEventResult.handled; + } case LogicalKeyboardKey.numpad4: case LogicalKeyboardKey.digit4: - timeline!.seekTo(timeline!.endPosition * 0.4); - return KeyEventResult.handled; + if (isTimelineValid) { + timeline!.seekTo(timeline!.endPosition * 0.4); + return KeyEventResult.handled; + } case LogicalKeyboardKey.numpad5: case LogicalKeyboardKey.digit5: - timeline!.seekTo(timeline!.endPosition * 0.5); - return KeyEventResult.handled; + if (isTimelineValid) { + timeline!.seekTo(timeline!.endPosition * 0.5); + return KeyEventResult.handled; + } case LogicalKeyboardKey.numpad6: case LogicalKeyboardKey.digit6: - timeline!.seekTo(timeline!.endPosition * 0.6); - return KeyEventResult.handled; + if (isTimelineValid) { + timeline!.seekTo(timeline!.endPosition * 0.6); + return KeyEventResult.handled; + } case LogicalKeyboardKey.numpad7: case LogicalKeyboardKey.digit7: - timeline!.seekTo(timeline!.endPosition * 0.7); - return KeyEventResult.handled; + if (isTimelineValid) { + timeline!.seekTo(timeline!.endPosition * 0.7); + return KeyEventResult.handled; + } case LogicalKeyboardKey.numpad8: case LogicalKeyboardKey.digit8: - timeline!.seekTo(timeline!.endPosition * 0.8); - return KeyEventResult.handled; + if (isTimelineValid) { + timeline!.seekTo(timeline!.endPosition * 0.8); + return KeyEventResult.handled; + } case LogicalKeyboardKey.numpad9: case LogicalKeyboardKey.digit9: - timeline!.seekTo(timeline!.endPosition * 0.9); - return KeyEventResult.handled; - - default: - return KeyEventResult.ignored; + if (isTimelineValid) { + timeline!.seekTo(timeline!.endPosition * 0.9); + return KeyEventResult.handled; + } } + return KeyEventResult.ignored; }, child: LayoutBuilder(builder: (context, constraints) { final hasDrawer = Scaffold.hasDrawer(context); From 2c3386b543efe432cf6ffea137fce2173763e844 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 26 Nov 2024 18:04:07 -0300 Subject: [PATCH 04/11] feat: Add layouts shortcuts --- lib/main.dart | 7 +- lib/providers/layouts_provider.dart | 4 ++ .../layouts/desktop/layout_manager.dart | 14 ++-- lib/screens/layouts/desktop/layout_view.dart | 1 + lib/screens/layouts/device_grid.dart | 1 + lib/utils/keyboard.dart | 64 ++++++++++++++++++- lib/widgets/collapsable_sidebar.dart | 20 +++--- 7 files changed, 94 insertions(+), 17 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 22f32436..436ae6ce 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -417,8 +417,13 @@ class _UnityAppState extends State builder: (context, child) { Intl.defaultLocale = Localizations.localeOf(context).languageCode; + final home = context.watch(); + return CallbackShortcuts( - bindings: globalShortcuts(context), + bindings: { + ...globalShortcuts(), + if (home.tab == UnityTab.deviceGrid) ...layoutShortcuts(), + }, child: child!, ); }, diff --git a/lib/providers/layouts_provider.dart b/lib/providers/layouts_provider.dart index 7613853d..70d58dcc 100644 --- a/lib/providers/layouts_provider.dart +++ b/lib/providers/layouts_provider.dart @@ -28,7 +28,9 @@ import 'package:bluecherry_client/providers/app_provider_interface.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/storage.dart'; import 'package:bluecherry_client/utils/video_player.dart'; +import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:unity_video_player/unity_video_player.dart'; class LayoutsProvider extends UnityProvider { @@ -69,6 +71,8 @@ class LayoutsProvider extends UnityProvider { save(); } + final sidebarKey = GlobalKey(); + @override Future initialize() async { await initializeStorage(kStorageDesktopLayouts); diff --git a/lib/screens/layouts/desktop/layout_manager.dart b/lib/screens/layouts/desktop/layout_manager.dart index 1b5deaf6..e1882d84 100644 --- a/lib/screens/layouts/desktop/layout_manager.dart +++ b/lib/screens/layouts/desktop/layout_manager.dart @@ -143,12 +143,7 @@ class _LayoutManagerState extends State with Searchable { SquaredIconButton( icon: const Icon(Icons.add), tooltip: loc.newLayout, - onPressed: () { - showDialog( - context: context, - builder: (context) => const NewLayoutDialog(), - ); - }, + onPressed: () => showNewLayoutDialog(context), ), ]), ), @@ -389,6 +384,13 @@ class _LayoutTypeChooser extends StatelessWidget { } } +Future showNewLayoutDialog(BuildContext context) { + return showDialog( + context: context, + builder: (context) => const NewLayoutDialog(), + ); +} + class NewLayoutDialog extends StatefulWidget { const NewLayoutDialog({super.key}); diff --git a/lib/screens/layouts/desktop/layout_view.dart b/lib/screens/layouts/desktop/layout_view.dart index 05931923..c25da396 100644 --- a/lib/screens/layouts/desktop/layout_view.dart +++ b/lib/screens/layouts/desktop/layout_view.dart @@ -88,6 +88,7 @@ class _LargeDeviceGridState extends State final isReversed = widget.width <= _kReverseBreakpoint; final sidebar = CollapsableSidebar( + key: view.sidebarKey, initiallyClosed: app_links.openedFromFile || view.currentLayout.devices.isNotEmpty, left: !isReversed, diff --git a/lib/screens/layouts/device_grid.dart b/lib/screens/layouts/device_grid.dart index 9a25ab11..b8ba018c 100644 --- a/lib/screens/layouts/device_grid.dart +++ b/lib/screens/layouts/device_grid.dart @@ -39,6 +39,7 @@ import 'package:bluecherry_client/screens/multi_window/window.dart'; import 'package:bluecherry_client/utils/app_links/app_links.dart' as app_links; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/utils/keyboard.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/theme.dart'; import 'package:bluecherry_client/utils/video_player.dart'; diff --git a/lib/utils/keyboard.dart b/lib/utils/keyboard.dart index 8022a173..78a106ca 100644 --- a/lib/utils/keyboard.dart +++ b/lib/utils/keyboard.dart @@ -1,10 +1,14 @@ +import 'package:bluecherry_client/main.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; +import 'package:bluecherry_client/providers/layouts_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/layout_manager.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; -Map globalShortcuts(BuildContext context) { +Map globalShortcuts() { + final context = navigatorKey.currentContext!; final settings = context.read(); final home = context.read(); @@ -42,3 +46,61 @@ Map globalShortcuts(BuildContext context) { SingleActivator(LogicalKeyboardKey.numpad9, alt: true): () => setTab(9), }; } + +Map layoutShortcuts() { + final context = navigatorKey.currentContext!; + final settings = context.read(); + final layouts = context.read(); + + return { + for (var i = 1; i < layouts.layouts.length.clamp(1, 9); i++) + SingleActivator( + { + 1: LogicalKeyboardKey.digit1, + 2: LogicalKeyboardKey.digit2, + 3: LogicalKeyboardKey.digit3, + 4: LogicalKeyboardKey.digit4, + 5: LogicalKeyboardKey.digit5, + 6: LogicalKeyboardKey.digit6, + 7: LogicalKeyboardKey.digit7, + 8: LogicalKeyboardKey.digit8, + 9: LogicalKeyboardKey.digit9, + }[i]!, + control: true, + ): () { + layouts.updateCurrentLayout(i - 1); + }, + for (var i = 1; i < layouts.layouts.length.clamp(1, 9); i++) + SingleActivator( + { + 1: LogicalKeyboardKey.numpad1, + 2: LogicalKeyboardKey.numpad2, + 3: LogicalKeyboardKey.numpad3, + 4: LogicalKeyboardKey.numpad4, + 5: LogicalKeyboardKey.numpad5, + 6: LogicalKeyboardKey.numpad6, + 7: LogicalKeyboardKey.numpad7, + 8: LogicalKeyboardKey.numpad8, + 9: LogicalKeyboardKey.numpad9, + }[i]!, + control: true, + ): () { + layouts.updateCurrentLayout(i - 1); + }, + SingleActivator(LogicalKeyboardKey.keyC, control: true, shift: true): + settings.toggleCycling, + SingleActivator(LogicalKeyboardKey.keyB, control: true): () { + layouts.sidebarKey.currentState?.toggle(); + }, + SingleActivator(LogicalKeyboardKey.keyN, control: true): () { + showNewLayoutDialog(context); + }, + SingleActivator(LogicalKeyboardKey.tab, control: true): + layouts.switchToNextLayout, + SingleActivator(LogicalKeyboardKey.tab, control: true, shift: true): + layouts.switchToNextLayout, + SingleActivator(LogicalKeyboardKey.keyL, control: true): () { + layouts.toggleLayoutLock(layouts.currentLayout); + }, + }; +} diff --git a/lib/widgets/collapsable_sidebar.dart b/lib/widgets/collapsable_sidebar.dart index c3d415c3..ff543727 100644 --- a/lib/widgets/collapsable_sidebar.dart +++ b/lib/widgets/collapsable_sidebar.dart @@ -49,10 +49,10 @@ class CollapsableSidebar extends StatefulWidget { }); @override - State createState() => _CollapsableSidebarState(); + State createState() => CollapsableSidebarState(); } -class _CollapsableSidebarState extends State +class CollapsableSidebarState extends State with SingleTickerProviderStateMixin { late final AnimationController collapseController = AnimationController( vsync: this, @@ -89,6 +89,14 @@ class _CollapsableSidebarState extends State super.dispose(); } + void toggle() { + if (collapseController.isCompleted) { + collapseController.reverse(); + } else { + collapseController.forward(); + } + } + @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); @@ -126,13 +134,7 @@ class _CollapsableSidebarState extends State .animate(collapseAnimation), child: const Icon(Icons.keyboard_arrow_right), ), - onPressed: () { - if (collapseController.isCompleted) { - collapseController.reverse(); - } else { - collapseController.forward(); - } - }, + onPressed: toggle, ), ); From 31af006c86d02f0b4611d84647786596e43e4d1c Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 26 Nov 2024 18:12:59 -0300 Subject: [PATCH 05/11] feat: Add settings shortcuts --- lib/main.dart | 7 ++- lib/providers/settings_provider.dart | 7 +++ lib/screens/layouts/device_grid.dart | 1 - lib/screens/settings/settings_desktop.dart | 19 +++----- lib/utils/keyboard.dart | 53 ++++++++++++++++++++-- 5 files changed, 67 insertions(+), 20 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 436ae6ce..fb8c04f0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -421,8 +421,11 @@ class _UnityAppState extends State return CallbackShortcuts( bindings: { - ...globalShortcuts(), - if (home.tab == UnityTab.deviceGrid) ...layoutShortcuts(), + ...globalShortcuts(context), + if (home.tab == UnityTab.deviceGrid) + ...layoutShortcuts(context), + if (home.tab == UnityTab.settings) + ...settingsShortcuts(context), }, child: child!, ); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index ca0e71fa..f7d9ef66 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -710,6 +710,13 @@ class SettingsProvider extends UnityProvider { kShowNetworkUsage, ]; + int _settingsIndex = 0; + int get settingsIndex => _settingsIndex; + set settingsIndex(int value) { + _settingsIndex = value; + notifyListeners(); + } + /// Initializes the [SettingsProvider] instance & fetches state from `async` /// `package:hive` method-calls. Called before [runApp]. static Future ensureInitialized() async { diff --git a/lib/screens/layouts/device_grid.dart b/lib/screens/layouts/device_grid.dart index b8ba018c..9a25ab11 100644 --- a/lib/screens/layouts/device_grid.dart +++ b/lib/screens/layouts/device_grid.dart @@ -39,7 +39,6 @@ import 'package:bluecherry_client/screens/multi_window/window.dart'; import 'package:bluecherry_client/utils/app_links/app_links.dart' as app_links; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:bluecherry_client/utils/keyboard.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/theme.dart'; import 'package:bluecherry_client/utils/video_player.dart'; diff --git a/lib/screens/settings/settings_desktop.dart b/lib/screens/settings/settings_desktop.dart index ca77e81b..33cff374 100644 --- a/lib/screens/settings/settings_desktop.dart +++ b/lib/screens/settings/settings_desktop.dart @@ -17,6 +17,7 @@ * along with this program. If not, see . */ +import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/settings/advanced_options.dart'; import 'package:bluecherry_client/screens/settings/application.dart'; import 'package:bluecherry_client/screens/settings/events_and_downloads.dart'; @@ -26,8 +27,9 @@ import 'package:bluecherry_client/screens/settings/updates_and_help.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; -class DesktopSettings extends StatefulWidget { +class DesktopSettings extends StatelessWidget { const DesktopSettings({super.key}); static const horizontalPadding = @@ -35,17 +37,11 @@ class DesktopSettings extends StatefulWidget { static const verticalPadding = EdgeInsetsDirectional.symmetric(vertical: 16.0); - @override - State createState() => _DesktopSettingsState(); -} - -class _DesktopSettingsState extends State { - int currentIndex = 0; - @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); final theme = Theme.of(context); + final settings = context.watch(); return LayoutBuilder(builder: (context, constraints) { return Row(children: [ @@ -78,9 +74,8 @@ class _DesktopSettingsState extends State { label: Text(loc.advancedOptions), ), ], - selectedIndex: currentIndex, - onDestinationSelected: (index) => - setState(() => currentIndex = index), + selectedIndex: settings.settingsIndex, + onDestinationSelected: (index) => settings.settingsIndex = index, ), Expanded( child: Card( @@ -103,7 +98,7 @@ class _DesktopSettingsState extends State { ), child: AnimatedSwitcher( duration: kThemeChangeDuration, - child: switch (currentIndex) { + child: switch (settings.settingsIndex) { 0 => const GeneralSettings(), 1 => const ServerAndDevicesSettings(), 2 => const EventsAndDownloadsSettings(), diff --git a/lib/utils/keyboard.dart b/lib/utils/keyboard.dart index 78a106ca..ebf8ea74 100644 --- a/lib/utils/keyboard.dart +++ b/lib/utils/keyboard.dart @@ -7,8 +7,8 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; -Map globalShortcuts() { - final context = navigatorKey.currentContext!; +Map globalShortcuts(BuildContext context) { + context = navigatorKey.currentContext ?? context; final settings = context.read(); final home = context.read(); @@ -47,8 +47,8 @@ Map globalShortcuts() { }; } -Map layoutShortcuts() { - final context = navigatorKey.currentContext!; +Map layoutShortcuts(BuildContext context) { + context = navigatorKey.currentContext ?? context; final settings = context.read(); final layouts = context.read(); @@ -93,7 +93,8 @@ Map layoutShortcuts() { layouts.sidebarKey.currentState?.toggle(); }, SingleActivator(LogicalKeyboardKey.keyN, control: true): () { - showNewLayoutDialog(context); + if (navigatorKey.currentContext == null) return; + showNewLayoutDialog(navigatorKey.currentContext!); }, SingleActivator(LogicalKeyboardKey.tab, control: true): layouts.switchToNextLayout, @@ -104,3 +105,45 @@ Map layoutShortcuts() { }, }; } + +Map settingsShortcuts(BuildContext context) { + context = navigatorKey.currentContext ?? context; + final settings = context.read(); + + return { + for (var i = 0; i < 6; i++) + SingleActivator( + { + 1: LogicalKeyboardKey.digit1, + 2: LogicalKeyboardKey.digit2, + 3: LogicalKeyboardKey.digit3, + 4: LogicalKeyboardKey.digit4, + 5: LogicalKeyboardKey.digit5, + 6: LogicalKeyboardKey.digit6, + 7: LogicalKeyboardKey.digit7, + 8: LogicalKeyboardKey.digit8, + 9: LogicalKeyboardKey.digit9, + }[i + 1]!, + control: true, + ): () { + settings.settingsIndex = i; + }, + for (var i = 0; i < 6; i++) + SingleActivator( + { + 1: LogicalKeyboardKey.numpad1, + 2: LogicalKeyboardKey.numpad2, + 3: LogicalKeyboardKey.numpad3, + 4: LogicalKeyboardKey.numpad4, + 5: LogicalKeyboardKey.numpad5, + 6: LogicalKeyboardKey.numpad6, + 7: LogicalKeyboardKey.numpad7, + 8: LogicalKeyboardKey.numpad8, + 9: LogicalKeyboardKey.numpad9, + }[i + 1]!, + control: true, + ): () { + settings.settingsIndex = i; + }, + }; +} From f501705b4018c68c4f8ae463a69925b9e7df673a Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 26 Nov 2024 18:59:00 -0300 Subject: [PATCH 06/11] fix: Do not enable shortcuts when dialogs are presents --- lib/main.dart | 16 +++++++++------- lib/widgets/desktop_buttons.dart | 3 +++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index fb8c04f0..3ab142b2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -420,13 +420,15 @@ class _UnityAppState extends State final home = context.watch(); return CallbackShortcuts( - bindings: { - ...globalShortcuts(context), - if (home.tab == UnityTab.deviceGrid) - ...layoutShortcuts(context), - if (home.tab == UnityTab.settings) - ...settingsShortcuts(context), - }, + bindings: navigatorObserver.isDialog + ? {} + : { + ...globalShortcuts(context), + if (home.tab == UnityTab.deviceGrid) + ...layoutShortcuts(context), + if (home.tab == UnityTab.settings) + ...settingsShortcuts(context), + }, child: child!, ); }, diff --git a/lib/widgets/desktop_buttons.dart b/lib/widgets/desktop_buttons.dart index 0f03da5a..c9b861fb 100644 --- a/lib/widgets/desktop_buttons.dart +++ b/lib/widgets/desktop_buttons.dart @@ -44,14 +44,17 @@ final navigatorObserver = NObserver(); class NObserver extends NavigatorObserver { bool poppableRoute = false; + bool isDialog = false; void update(Route? route) { if (route == null || route is DialogRoute || route is PopupRoute) { + if (route is DialogRoute) isDialog = true; poppableRoute = false; return; } poppableRoute = true; + isDialog = false; navigationStream.add(route.settings.arguments); } From cbdb83b8ce8bbb91871029464453cdcdd7298de3 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 26 Nov 2024 18:59:11 -0300 Subject: [PATCH 07/11] feat: List shortcuts in settings --- lib/screens/settings/application.dart | 138 +++++++++++++++ lib/utils/keyboard.dart | 231 ++++++++++++++++++++++++++ 2 files changed, 369 insertions(+) diff --git a/lib/screens/settings/application.dart b/lib/screens/settings/application.dart index 2ee68493..b4b85dd2 100644 --- a/lib/screens/settings/application.dart +++ b/lib/screens/settings/application.dart @@ -21,10 +21,12 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/settings/settings_desktop.dart'; import 'package:bluecherry_client/screens/settings/shared/options_chooser_tile.dart'; import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/utils/keyboard.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:intl/intl.dart'; @@ -208,6 +210,11 @@ class ApplicationSettings extends StatelessWidget { ), ), ], + const SubHeader( + 'Keyboard Shortcuts', + padding: DesktopSettings.horizontalPadding, + ), + KeyboardSection(), ]); } @@ -372,3 +379,134 @@ class TimeFormatSection extends StatelessWidget { ); } } + +class KeyboardSection extends StatelessWidget { + const KeyboardSection({super.key}); + + @override + Widget build(BuildContext context) { + return DataTable( + columns: [ + DataColumn(label: Text('System')), + DataColumn(label: Text('Command')), + DataColumn(label: Text('Keybinding')), + ], + rows: [ + for (final keybinding in KeyboardBindings.all) + DataRow( + cells: [ + DataCell(Text(keybinding.system)), + DataCell(Text(keybinding.name)), + DataCell( + Text(keybinding.activator.debugDescribeKeys()), + showEditIcon: true, + onTap: () { + showDialog( + context: context, + builder: (context) => + KeybindingDialog(keybinding: keybinding), + ); + }, + ), + ], + ), + ], + ); + } +} + +class KeybindingDialog extends StatefulWidget { + final ({String name, String system, ShortcutActivator activator}) keybinding; + + const KeybindingDialog({super.key, required this.keybinding}); + + @override + State createState() => _KeybindingDialogState(); +} + +class _KeybindingDialogState extends State { + ShortcutActivator? _newActivator; + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + void _handleKeypress(KeyEvent event) { + if (event is KeyDownEvent) { + setState(() { + _newActivator = SingleActivator( + LogicalKeyboardKey.findKeyByKeyId(event.logicalKey.keyId)!, + control: HardwareKeyboard.instance.isControlPressed, + shift: HardwareKeyboard.instance.isShiftPressed, + alt: HardwareKeyboard.instance.isAltPressed, + ); + }); + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('Change Keybinding for "${widget.keybinding.name}"'), + content: KeyboardListener( + autofocus: true, + focusNode: _focusNode, + onKeyEvent: _handleKeypress, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Text('Press the new key combination you want to use.'), + const SizedBox(height: 20), + RichText( + text: TextSpan( + text: 'Current Keybinding: ', + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: widget.keybinding.activator.debugDescribeKeys(), + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + RichText( + text: TextSpan( + text: 'New Keybinding: ', + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: _newActivator?.debugDescribeKeys() ?? 'Empty', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ]), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + // TODO: Save the new keybinding + Navigator.pop(context); + }, + child: const Text('Save'), + ), + ], + ); + } +} diff --git a/lib/utils/keyboard.dart b/lib/utils/keyboard.dart index ebf8ea74..39463819 100644 --- a/lib/utils/keyboard.dart +++ b/lib/utils/keyboard.dart @@ -147,3 +147,234 @@ Map settingsShortcuts(BuildContext context) { }, }; } + +class KeyboardBindings { + // Global shortcuts + static const toggleFullscreen = SingleActivator(LogicalKeyboardKey.f11); + static const toggleImmersiveMode = SingleActivator(LogicalKeyboardKey.f12); + static const setTab1 = SingleActivator(LogicalKeyboardKey.digit1, alt: true); + static const setTab2 = SingleActivator(LogicalKeyboardKey.digit2, alt: true); + static const setTab3 = SingleActivator(LogicalKeyboardKey.digit3, alt: true); + static const setTab4 = SingleActivator(LogicalKeyboardKey.digit4, alt: true); + static const setTab5 = SingleActivator(LogicalKeyboardKey.digit5, alt: true); + static const setTab6 = SingleActivator(LogicalKeyboardKey.digit6, alt: true); + static const setTab7 = SingleActivator(LogicalKeyboardKey.digit7, alt: true); + static const setTab8 = SingleActivator(LogicalKeyboardKey.digit8, alt: true); + static const setTab9 = SingleActivator(LogicalKeyboardKey.digit9, alt: true); + + // Layout shortcuts + static const setLayout1 = + SingleActivator(LogicalKeyboardKey.digit1, control: true); + static const setLayout2 = + SingleActivator(LogicalKeyboardKey.digit2, control: true); + static const setLayout3 = + SingleActivator(LogicalKeyboardKey.digit3, control: true); + static const setLayout4 = + SingleActivator(LogicalKeyboardKey.digit4, control: true); + static const setLayout5 = + SingleActivator(LogicalKeyboardKey.digit5, control: true); + static const setLayout6 = + SingleActivator(LogicalKeyboardKey.digit6, control: true); + static const setLayout7 = + SingleActivator(LogicalKeyboardKey.digit7, control: true); + static const setLayout8 = + SingleActivator(LogicalKeyboardKey.digit8, control: true); + static const setLayout9 = + SingleActivator(LogicalKeyboardKey.digit9, control: true); + static const toggleLayoutCycling = + SingleActivator(LogicalKeyboardKey.keyC, control: true, shift: true); + static const toggleLayoutSidebar = + SingleActivator(LogicalKeyboardKey.keyB, control: true); + static const showNewLayoutDialog = + SingleActivator(LogicalKeyboardKey.keyN, control: true); + static const switchToNextLayout = + SingleActivator(LogicalKeyboardKey.tab, control: true); + static const switchToPreviousLayout = + SingleActivator(LogicalKeyboardKey.tab, control: true, shift: true); + static const toggleLayoutLock = + SingleActivator(LogicalKeyboardKey.keyL, control: true); + + // Settings shortcuts + static const setSettingsTab1 = + SingleActivator(LogicalKeyboardKey.digit1, control: true); + static const setSettingsTab2 = + SingleActivator(LogicalKeyboardKey.digit2, control: true); + static const setSettingsTab3 = + SingleActivator(LogicalKeyboardKey.digit3, control: true); + static const setSettingsTab4 = + SingleActivator(LogicalKeyboardKey.digit4, control: true); + static const setSettingsTab5 = + SingleActivator(LogicalKeyboardKey.digit5, control: true); + static const setSettingsTab6 = + SingleActivator(LogicalKeyboardKey.digit6, control: true); + + static List< + ({ + String name, + String system, + ShortcutActivator activator, + })> get all { + return [ + ( + name: 'Toggle Fullscreen', + system: 'Global', + activator: toggleFullscreen, + ), + ( + name: 'Toggle Immersive Mode', + system: 'Global', + activator: toggleImmersiveMode, + ), + ( + name: 'Set Layouts Tab', + system: 'Global', + activator: setTab1, + ), + ( + name: 'Set Events Timeline Tab', + system: 'Global', + activator: setTab2, + ), + ( + name: 'Set Add Server Tab', + system: 'Global', + activator: setTab3, + ), + ( + name: 'Set Downloads Tab', + system: 'Global', + activator: setTab4, + ), + ( + name: 'Set Settings Tab', + system: 'Global', + activator: setTab5, + ), + ( + name: 'Set Tab 6', + system: 'Global', + activator: setTab6, + ), + ( + name: 'Set Tab 7', + system: 'Global', + activator: setTab7, + ), + ( + name: 'Set Tab 8', + system: 'Global', + activator: setTab8, + ), + ( + name: 'Set Tab 9', + system: 'Global', + activator: setTab9, + ), + ( + name: 'Set Layout 1', + system: 'Layout', + activator: setLayout1, + ), + ( + name: 'Set Layout 2', + system: 'Layout', + activator: setLayout2, + ), + ( + name: 'Set Layout 3', + system: 'Layout', + activator: setLayout3, + ), + ( + name: 'Set Layout 4', + system: 'Layout', + activator: setLayout4, + ), + ( + name: 'Set Layout 5', + system: 'Layout', + activator: setLayout5, + ), + ( + name: 'Set Layout 6', + system: 'Layout', + activator: setLayout6, + ), + ( + name: 'Set Layout 7', + system: 'Layout', + activator: setLayout7, + ), + ( + name: 'Set Layout 8', + system: 'Layout', + activator: setLayout8, + ), + ( + name: 'Set Layout 9', + system: 'Layout', + activator: setLayout9, + ), + ( + name: 'Toggle Layout Cycling', + system: 'Layout', + activator: toggleLayoutCycling, + ), + ( + name: 'Toggle Layout Sidebar', + system: 'Layout', + activator: toggleLayoutSidebar, + ), + ( + name: 'Show New Layout Dialog', + system: 'Layout', + activator: showNewLayoutDialog, + ), + ( + name: 'Switch to Next Layout', + system: 'Layout', + activator: switchToNextLayout, + ), + ( + name: 'Switch to Previous Layout', + system: 'Layout', + activator: switchToPreviousLayout, + ), + ( + name: 'Toggle Layout Lock', + system: 'Layout', + activator: toggleLayoutLock, + ), + ( + name: 'Go to General Settings', + system: 'Settings', + activator: setSettingsTab1, + ), + ( + name: 'Go to Server and Devices Settings', + system: 'Settings', + activator: setSettingsTab2, + ), + ( + name: 'Go to Events and Downloads Settings', + system: 'Settings', + activator: setSettingsTab3, + ), + ( + name: 'Go to Application Settings', + system: 'Settings', + activator: setSettingsTab4, + ), + ( + name: 'Go to Updates and Help Settings', + system: 'Settings', + activator: setSettingsTab5, + ), + ( + name: 'Go to Advanced Options Settings', + system: 'Settings', + activator: setSettingsTab6, + ), + ]; + } +} From 272c37eee6d3ba030738223a35158e9ed9d269f6 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 28 Nov 2024 14:32:56 -0300 Subject: [PATCH 08/11] feat: Store keybindings in storage --- lib/main.dart | 11 +- lib/screens/settings/application.dart | 25 +- lib/utils/keyboard.dart | 861 ++++++++++++++++---------- 3 files changed, 564 insertions(+), 333 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 3ab142b2..ea455e7f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -116,6 +116,7 @@ Future main(List args) async { await LayoutsProvider.ensureInitialized(); await UpdateManager.ensureInitialized(); await EventsProvider.ensureInitialized(); + await KeyboardBindings.ensureInitialized(); }, onLayoutScreen: (layout, theme) { configureWindowTitle(layout.name); @@ -333,6 +334,9 @@ class _UnityAppState extends State ChangeNotifierProvider.value( value: EventsProvider.instance, ), + ChangeNotifierProvider.value( + value: KeyboardBindings.instance, + ), ], child: Consumer(builder: (context, settings, _) { return MaterialApp( @@ -418,16 +422,17 @@ class _UnityAppState extends State Intl.defaultLocale = Localizations.localeOf(context).languageCode; final home = context.watch(); + final bindings = context.watch(); return CallbackShortcuts( bindings: navigatorObserver.isDialog ? {} : { - ...globalShortcuts(context), + ...globalShortcuts(context, bindings), if (home.tab == UnityTab.deviceGrid) - ...layoutShortcuts(context), + ...layoutShortcuts(context, bindings), if (home.tab == UnityTab.settings) - ...settingsShortcuts(context), + ...settingsShortcuts(context, bindings), }, child: child!, ); diff --git a/lib/screens/settings/application.dart b/lib/screens/settings/application.dart index b4b85dd2..e4e21f68 100644 --- a/lib/screens/settings/application.dart +++ b/lib/screens/settings/application.dart @@ -385,6 +385,7 @@ class KeyboardSection extends StatelessWidget { @override Widget build(BuildContext context) { + final keyboard = context.watch(); return DataTable( columns: [ DataColumn(label: Text('System')), @@ -392,19 +393,20 @@ class KeyboardSection extends StatelessWidget { DataColumn(label: Text('Keybinding')), ], rows: [ - for (final keybinding in KeyboardBindings.all) + for (final keybinding in keyboard.all) DataRow( cells: [ DataCell(Text(keybinding.system)), DataCell(Text(keybinding.name)), DataCell( - Text(keybinding.activator.debugDescribeKeys()), + Text(keybinding.value.debugDescribeKeys()), showEditIcon: true, onTap: () { showDialog( context: context, - builder: (context) => - KeybindingDialog(keybinding: keybinding), + builder: (context) => KeybindingDialog( + keybinding: keybinding, + ), ); }, ), @@ -416,16 +418,19 @@ class KeyboardSection extends StatelessWidget { } class KeybindingDialog extends StatefulWidget { - final ({String name, String system, ShortcutActivator activator}) keybinding; + final KeybindingSetting keybinding; - const KeybindingDialog({super.key, required this.keybinding}); + const KeybindingDialog({ + super.key, + required this.keybinding, + }); @override State createState() => _KeybindingDialogState(); } class _KeybindingDialogState extends State { - ShortcutActivator? _newActivator; + SingleActivator? _newActivator; late FocusNode _focusNode; @override @@ -470,7 +475,7 @@ class _KeybindingDialogState extends State { style: DefaultTextStyle.of(context).style, children: [ TextSpan( - text: widget.keybinding.activator.debugDescribeKeys(), + text: widget.keybinding.value.debugDescribeKeys(), style: TextStyle( fontWeight: FontWeight.bold, ), @@ -501,7 +506,9 @@ class _KeybindingDialogState extends State { ), ElevatedButton( onPressed: () { - // TODO: Save the new keybinding + if (_newActivator != null) { + widget.keybinding.value = _newActivator!; + } Navigator.pop(context); }, child: const Text('Save'), diff --git a/lib/utils/keyboard.dart b/lib/utils/keyboard.dart index 39463819..71185f1f 100644 --- a/lib/utils/keyboard.dart +++ b/lib/utils/keyboard.dart @@ -1,13 +1,19 @@ import 'package:bluecherry_client/main.dart'; +import 'package:bluecherry_client/providers/app_provider_interface.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/layouts_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/screens/layouts/desktop/layout_manager.dart'; +import 'package:bluecherry_client/utils/logging.dart'; +import 'package:bluecherry_client/utils/storage.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; -Map globalShortcuts(BuildContext context) { +Map globalShortcuts( + BuildContext context, + KeyboardBindings bindings, +) { context = navigatorKey.currentContext ?? context; final settings = context.read(); final home = context.read(); @@ -21,360 +27,573 @@ Map globalShortcuts(BuildContext context) { } return { - SingleActivator(LogicalKeyboardKey.f11): () { + bindings.toggleFullscreen: () { settings.kFullscreen.value = !settings.kFullscreen.value; }, - SingleActivator(LogicalKeyboardKey.f12): () { + bindings.toggleImmersiveMode: () { settings.kImmersiveMode.value = !settings.kImmersiveMode.value; }, - SingleActivator(LogicalKeyboardKey.digit1, alt: true): () => setTab(1), - SingleActivator(LogicalKeyboardKey.digit2, alt: true): () => setTab(2), - SingleActivator(LogicalKeyboardKey.numpad2, alt: true): () => setTab(2), - SingleActivator(LogicalKeyboardKey.digit3, alt: true): () => setTab(3), - SingleActivator(LogicalKeyboardKey.numpad3, alt: true): () => setTab(3), - SingleActivator(LogicalKeyboardKey.digit4, alt: true): () => setTab(4), - SingleActivator(LogicalKeyboardKey.numpad4, alt: true): () => setTab(4), - SingleActivator(LogicalKeyboardKey.digit5, alt: true): () => setTab(5), - SingleActivator(LogicalKeyboardKey.numpad5, alt: true): () => setTab(5), - SingleActivator(LogicalKeyboardKey.digit6, alt: true): () => setTab(6), - SingleActivator(LogicalKeyboardKey.numpad6, alt: true): () => setTab(6), - SingleActivator(LogicalKeyboardKey.digit7, alt: true): () => setTab(7), - SingleActivator(LogicalKeyboardKey.numpad7, alt: true): () => setTab(7), - SingleActivator(LogicalKeyboardKey.digit8, alt: true): () => setTab(8), - SingleActivator(LogicalKeyboardKey.numpad8, alt: true): () => setTab(8), - SingleActivator(LogicalKeyboardKey.digit9, alt: true): () => setTab(9), - SingleActivator(LogicalKeyboardKey.numpad9, alt: true): () => setTab(9), + bindings.setTab1: () => setTab(1), + bindings.setTab2: () => setTab(2), + bindings.setTab3: () => setTab(3), + bindings.setTab4: () => setTab(4), + bindings.setTab5: () => setTab(5), + bindings.setTab6: () => setTab(6), + bindings.setTab7: () => setTab(7), + bindings.setTab8: () => setTab(8), + bindings.setTab9: () => setTab(9), }; } -Map layoutShortcuts(BuildContext context) { +Map layoutShortcuts( + BuildContext context, + KeyboardBindings bindings, +) { context = navigatorKey.currentContext ?? context; final settings = context.read(); final layouts = context.read(); return { - for (var i = 1; i < layouts.layouts.length.clamp(1, 9); i++) - SingleActivator( - { - 1: LogicalKeyboardKey.digit1, - 2: LogicalKeyboardKey.digit2, - 3: LogicalKeyboardKey.digit3, - 4: LogicalKeyboardKey.digit4, - 5: LogicalKeyboardKey.digit5, - 6: LogicalKeyboardKey.digit6, - 7: LogicalKeyboardKey.digit7, - 8: LogicalKeyboardKey.digit8, - 9: LogicalKeyboardKey.digit9, - }[i]!, - control: true, - ): () { - layouts.updateCurrentLayout(i - 1); - }, - for (var i = 1; i < layouts.layouts.length.clamp(1, 9); i++) - SingleActivator( - { - 1: LogicalKeyboardKey.numpad1, - 2: LogicalKeyboardKey.numpad2, - 3: LogicalKeyboardKey.numpad3, - 4: LogicalKeyboardKey.numpad4, - 5: LogicalKeyboardKey.numpad5, - 6: LogicalKeyboardKey.numpad6, - 7: LogicalKeyboardKey.numpad7, - 8: LogicalKeyboardKey.numpad8, - 9: LogicalKeyboardKey.numpad9, - }[i]!, - control: true, - ): () { - layouts.updateCurrentLayout(i - 1); - }, - SingleActivator(LogicalKeyboardKey.keyC, control: true, shift: true): - settings.toggleCycling, - SingleActivator(LogicalKeyboardKey.keyB, control: true): () { + bindings.setLayout1: () => layouts.updateCurrentLayout(0), + bindings.setLayout2: () => layouts.updateCurrentLayout(1), + bindings.setLayout3: () => layouts.updateCurrentLayout(2), + bindings.setLayout4: () => layouts.updateCurrentLayout(3), + bindings.setLayout5: () => layouts.updateCurrentLayout(4), + bindings.setLayout6: () => layouts.updateCurrentLayout(5), + bindings.setLayout7: () => layouts.updateCurrentLayout(6), + bindings.setLayout8: () => layouts.updateCurrentLayout(7), + bindings.setLayout9: () => layouts.updateCurrentLayout(8), + bindings.toggleLayoutCycling: settings.toggleCycling, + bindings.toggleLayoutSidebar: () { layouts.sidebarKey.currentState?.toggle(); }, - SingleActivator(LogicalKeyboardKey.keyN, control: true): () { + bindings.showNewLayoutDialog: () { if (navigatorKey.currentContext == null) return; showNewLayoutDialog(navigatorKey.currentContext!); }, - SingleActivator(LogicalKeyboardKey.tab, control: true): - layouts.switchToNextLayout, - SingleActivator(LogicalKeyboardKey.tab, control: true, shift: true): - layouts.switchToNextLayout, - SingleActivator(LogicalKeyboardKey.keyL, control: true): () { + bindings.switchToNextLayout: layouts.switchToNextLayout, + bindings.switchToPreviousLayout: layouts.switchToNextLayout, + bindings.toggleLayoutLock: () { layouts.toggleLayoutLock(layouts.currentLayout); }, }; } -Map settingsShortcuts(BuildContext context) { +Map settingsShortcuts( + BuildContext context, + KeyboardBindings bindings, +) { context = navigatorKey.currentContext ?? context; final settings = context.read(); return { - for (var i = 0; i < 6; i++) - SingleActivator( - { - 1: LogicalKeyboardKey.digit1, - 2: LogicalKeyboardKey.digit2, - 3: LogicalKeyboardKey.digit3, - 4: LogicalKeyboardKey.digit4, - 5: LogicalKeyboardKey.digit5, - 6: LogicalKeyboardKey.digit6, - 7: LogicalKeyboardKey.digit7, - 8: LogicalKeyboardKey.digit8, - 9: LogicalKeyboardKey.digit9, - }[i + 1]!, - control: true, - ): () { - settings.settingsIndex = i; - }, - for (var i = 0; i < 6; i++) - SingleActivator( - { - 1: LogicalKeyboardKey.numpad1, - 2: LogicalKeyboardKey.numpad2, - 3: LogicalKeyboardKey.numpad3, - 4: LogicalKeyboardKey.numpad4, - 5: LogicalKeyboardKey.numpad5, - 6: LogicalKeyboardKey.numpad6, - 7: LogicalKeyboardKey.numpad7, - 8: LogicalKeyboardKey.numpad8, - 9: LogicalKeyboardKey.numpad9, - }[i + 1]!, - control: true, - ): () { - settings.settingsIndex = i; - }, + bindings.setSettingsTab1: () => settings.settingsIndex = 0, + bindings.setSettingsTab2: () => settings.settingsIndex = 1, + bindings.setSettingsTab3: () => settings.settingsIndex = 2, + bindings.setSettingsTab4: () => settings.settingsIndex = 3, + bindings.setSettingsTab5: () => settings.settingsIndex = 4, + bindings.setSettingsTab6: () => settings.settingsIndex = 5, }; } -class KeyboardBindings { +class KeyboardBindings extends UnityProvider { + KeyboardBindings._(); + + static late KeyboardBindings instance; + // Global shortcuts - static const toggleFullscreen = SingleActivator(LogicalKeyboardKey.f11); - static const toggleImmersiveMode = SingleActivator(LogicalKeyboardKey.f12); - static const setTab1 = SingleActivator(LogicalKeyboardKey.digit1, alt: true); - static const setTab2 = SingleActivator(LogicalKeyboardKey.digit2, alt: true); - static const setTab3 = SingleActivator(LogicalKeyboardKey.digit3, alt: true); - static const setTab4 = SingleActivator(LogicalKeyboardKey.digit4, alt: true); - static const setTab5 = SingleActivator(LogicalKeyboardKey.digit5, alt: true); - static const setTab6 = SingleActivator(LogicalKeyboardKey.digit6, alt: true); - static const setTab7 = SingleActivator(LogicalKeyboardKey.digit7, alt: true); - static const setTab8 = SingleActivator(LogicalKeyboardKey.digit8, alt: true); - static const setTab9 = SingleActivator(LogicalKeyboardKey.digit9, alt: true); + final _toggleFullscreen = KeybindingSetting( + name: 'Toggle Fullscreen', + system: 'Global', + key: 'global.fullscreen', + def: SingleActivator(LogicalKeyboardKey.f11), + ); + + SingleActivator get toggleFullscreen => _toggleFullscreen.value; + + final _toggleImmersiveMode = KeybindingSetting( + name: 'Toggle Immersive Mode', + system: 'Global', + key: 'global.immersiveMode', + def: SingleActivator(LogicalKeyboardKey.f12), + ); + + SingleActivator get toggleImmersiveMode => _toggleImmersiveMode.value; + + final _setTab1 = KeybindingSetting( + name: 'Set Tab 1', + system: 'Global', + key: 'global.setTab1', + def: SingleActivator(LogicalKeyboardKey.digit1, alt: true), + ); + + SingleActivator get setTab1 => _setTab1.value; + + final _setTab2 = KeybindingSetting( + name: 'Set Tab 2', + system: 'Global', + key: 'global.setTab2', + def: SingleActivator(LogicalKeyboardKey.digit2, alt: true), + ); + + SingleActivator get setTab2 => _setTab2.value; + + final _setTab3 = KeybindingSetting( + name: 'Set Tab 3', + system: 'Global', + key: 'global.setTab3', + def: SingleActivator(LogicalKeyboardKey.digit3, alt: true), + ); + + SingleActivator get setTab3 => _setTab3.value; + + final _setTab4 = KeybindingSetting( + name: 'Set Tab 4', + system: 'Global', + key: 'global.setTab4', + def: SingleActivator(LogicalKeyboardKey.digit4, alt: true), + ); + + SingleActivator get setTab4 => _setTab4.value; + + final _setTab5 = KeybindingSetting( + name: 'Set Tab 5', + system: 'Global', + key: 'global.setTab5', + def: SingleActivator(LogicalKeyboardKey.digit5, alt: true), + ); + + SingleActivator get setTab5 => _setTab5.value; + + final _setTab6 = KeybindingSetting( + name: 'Set Tab 6', + system: 'Global', + key: 'global.setTab6', + def: SingleActivator(LogicalKeyboardKey.digit6, alt: true), + ); + + SingleActivator get setTab6 => _setTab6.value; + + final _setTab7 = KeybindingSetting( + name: 'Set Tab 7', + system: 'Global', + key: 'global.setTab7', + def: SingleActivator(LogicalKeyboardKey.digit7, alt: true), + ); + + SingleActivator get setTab7 => _setTab7.value; + + final _setTab8 = KeybindingSetting( + name: 'Set Tab 8', + system: 'Global', + key: 'global.setTab8', + def: SingleActivator(LogicalKeyboardKey.digit8, alt: true), + ); + + SingleActivator get setTab8 => _setTab8.value; + + final _setTab9 = KeybindingSetting( + name: 'Set Tab 9', + system: 'Global', + key: 'global.setTab9', + def: SingleActivator(LogicalKeyboardKey.digit9, alt: true), + ); + + SingleActivator get setTab9 => _setTab9.value; // Layout shortcuts - static const setLayout1 = - SingleActivator(LogicalKeyboardKey.digit1, control: true); - static const setLayout2 = - SingleActivator(LogicalKeyboardKey.digit2, control: true); - static const setLayout3 = - SingleActivator(LogicalKeyboardKey.digit3, control: true); - static const setLayout4 = - SingleActivator(LogicalKeyboardKey.digit4, control: true); - static const setLayout5 = - SingleActivator(LogicalKeyboardKey.digit5, control: true); - static const setLayout6 = - SingleActivator(LogicalKeyboardKey.digit6, control: true); - static const setLayout7 = - SingleActivator(LogicalKeyboardKey.digit7, control: true); - static const setLayout8 = - SingleActivator(LogicalKeyboardKey.digit8, control: true); - static const setLayout9 = - SingleActivator(LogicalKeyboardKey.digit9, control: true); - static const toggleLayoutCycling = - SingleActivator(LogicalKeyboardKey.keyC, control: true, shift: true); - static const toggleLayoutSidebar = - SingleActivator(LogicalKeyboardKey.keyB, control: true); - static const showNewLayoutDialog = - SingleActivator(LogicalKeyboardKey.keyN, control: true); - static const switchToNextLayout = - SingleActivator(LogicalKeyboardKey.tab, control: true); - static const switchToPreviousLayout = - SingleActivator(LogicalKeyboardKey.tab, control: true, shift: true); - static const toggleLayoutLock = - SingleActivator(LogicalKeyboardKey.keyL, control: true); + final _setLayout1 = KeybindingSetting( + name: 'Set Layout 1', + system: 'Layout', + key: 'layout.setLayout1', + def: SingleActivator(LogicalKeyboardKey.digit1, control: true), + ); + + SingleActivator get setLayout1 => _setLayout1.value; + + final _setLayout2 = KeybindingSetting( + name: 'Set Layout 2', + system: 'Layout', + key: 'layout.setLayout2', + def: SingleActivator(LogicalKeyboardKey.digit2, control: true), + ); + + SingleActivator get setLayout2 => _setLayout2.value; + + final _setLayout3 = KeybindingSetting( + name: 'Set Layout 3', + system: 'Layout', + key: 'layout.setLayout3', + def: SingleActivator(LogicalKeyboardKey.digit3, control: true), + ); + + SingleActivator get setLayout3 => _setLayout3.value; + + final _setLayout4 = KeybindingSetting( + name: 'Set Layout 4', + system: 'Layout', + key: 'layout.setLayout4', + def: SingleActivator(LogicalKeyboardKey.digit4, control: true), + ); + + SingleActivator get setLayout4 => _setLayout4.value; + + final _setLayout5 = KeybindingSetting( + name: 'Set Layout 5', + system: 'Layout', + key: 'layout.setLayout5', + def: SingleActivator(LogicalKeyboardKey.digit5, control: true), + ); + + SingleActivator get setLayout5 => _setLayout5.value; + + final _setLayout6 = KeybindingSetting( + name: 'Set Layout 6', + system: 'Layout', + key: 'layout.setLayout6', + def: SingleActivator(LogicalKeyboardKey.digit6, control: true), + ); + + SingleActivator get setLayout6 => _setLayout6.value; + + final _setLayout7 = KeybindingSetting( + name: 'Set Layout 7', + system: 'Layout', + key: 'layout.setLayout7', + def: SingleActivator(LogicalKeyboardKey.digit7, control: true), + ); + + SingleActivator get setLayout7 => _setLayout7.value; + + final _setLayout8 = KeybindingSetting( + name: 'Set Layout 8', + system: 'Layout', + key: 'layout.setLayout8', + def: SingleActivator(LogicalKeyboardKey.digit8, control: true), + ); + + SingleActivator get setLayout8 => _setLayout8.value; + + final _setLayout9 = KeybindingSetting( + name: 'Set Layout 9', + system: 'Layout', + key: 'layout.setLayout9', + def: SingleActivator(LogicalKeyboardKey.digit9, control: true), + ); + + SingleActivator get setLayout9 => _setLayout9.value; + + final _toggleLayoutCycling = KeybindingSetting( + name: 'Toggle Layout Cycling', + system: 'Layout', + key: 'layout.toggleLayoutCycling', + def: SingleActivator(LogicalKeyboardKey.keyC, control: true, shift: true), + ); + + SingleActivator get toggleLayoutCycling => _toggleLayoutCycling.value; + + final _toggleLayoutSidebar = KeybindingSetting( + name: 'Toggle Layout Sidebar', + system: 'Layout', + key: 'layout.toggleLayoutSidebar', + def: SingleActivator(LogicalKeyboardKey.keyB, control: true), + ); + + SingleActivator get toggleLayoutSidebar => _toggleLayoutSidebar.value; + + final _showNewLayoutDialog = KeybindingSetting( + name: 'Show New Layout Dialog', + system: 'Layout', + key: 'layout.showNewLayoutDialog', + def: SingleActivator(LogicalKeyboardKey.keyN, control: true), + ); + + SingleActivator get showNewLayoutDialog => _showNewLayoutDialog.value; + + final _switchToNextLayout = KeybindingSetting( + name: 'Switch to Next Layout', + system: 'Layout', + key: 'layout.switchToNextLayout', + def: SingleActivator(LogicalKeyboardKey.tab, control: true), + ); + + SingleActivator get switchToNextLayout => _switchToNextLayout.value; + + final _switchToPreviousLayout = KeybindingSetting( + name: 'Switch to Previous Layout', + system: 'Layout', + key: 'layout.switchToPreviousLayout', + def: SingleActivator(LogicalKeyboardKey.tab, control: true, shift: true), + ); + + SingleActivator get switchToPreviousLayout => _switchToPreviousLayout.value; + + final _toggleLayoutLock = KeybindingSetting( + name: 'Toggle Layout Lock', + system: 'Layout', + key: 'layout.toggleLayoutLock', + def: SingleActivator(LogicalKeyboardKey.keyL, control: true), + ); + + SingleActivator get toggleLayoutLock => _toggleLayoutLock.value; // Settings shortcuts - static const setSettingsTab1 = - SingleActivator(LogicalKeyboardKey.digit1, control: true); - static const setSettingsTab2 = - SingleActivator(LogicalKeyboardKey.digit2, control: true); - static const setSettingsTab3 = - SingleActivator(LogicalKeyboardKey.digit3, control: true); - static const setSettingsTab4 = - SingleActivator(LogicalKeyboardKey.digit4, control: true); - static const setSettingsTab5 = - SingleActivator(LogicalKeyboardKey.digit5, control: true); - static const setSettingsTab6 = - SingleActivator(LogicalKeyboardKey.digit6, control: true); - - static List< - ({ - String name, - String system, - ShortcutActivator activator, - })> get all { + final _setSettingsTab1 = KeybindingSetting( + name: 'Set Settings Tab 1', + system: 'Settings', + key: 'settings.setSettingsTab1', + def: SingleActivator(LogicalKeyboardKey.digit1, control: true), + ); + + SingleActivator get setSettingsTab1 => _setSettingsTab1.value; + + final _setSettingsTab2 = KeybindingSetting( + name: 'Set Settings Tab 2', + system: 'Settings', + key: 'settings.setSettingsTab2', + def: SingleActivator(LogicalKeyboardKey.digit2, control: true), + ); + + SingleActivator get setSettingsTab2 => _setSettingsTab2.value; + + final _setSettingsTab3 = KeybindingSetting( + name: 'Set Settings Tab 3', + system: 'Settings', + key: 'settings.setSettingsTab3', + def: SingleActivator(LogicalKeyboardKey.digit3, control: true), + ); + + SingleActivator get setSettingsTab3 => _setSettingsTab3.value; + + final _setSettingsTab4 = KeybindingSetting( + name: 'Set Settings Tab 4', + system: 'Settings', + key: 'settings.setSettingsTab4', + def: SingleActivator(LogicalKeyboardKey.digit4, control: true), + ); + + SingleActivator get setSettingsTab4 => _setSettingsTab4.value; + + final _setSettingsTab5 = KeybindingSetting( + name: 'Set Settings Tab 5', + system: 'Settings', + key: 'settings.setSettingsTab5', + def: SingleActivator(LogicalKeyboardKey.digit5, control: true), + ); + + SingleActivator get setSettingsTab5 => _setSettingsTab5.value; + + final _setSettingsTab6 = KeybindingSetting( + name: 'Set Settings Tab 6', + system: 'Settings', + key: 'settings.setSettingsTab6', + def: SingleActivator(LogicalKeyboardKey.digit6, control: true), + ); + + SingleActivator get setSettingsTab6 => _setSettingsTab6.value; + + List get all => [ + _toggleFullscreen, + _toggleImmersiveMode, + _setTab1, + _setTab2, + _setTab3, + _setTab4, + _setTab5, + _setTab6, + _setTab7, + _setTab8, + _setTab9, + _setLayout1, + _setLayout2, + _setLayout3, + _setLayout4, + _setLayout5, + _setLayout6, + _setLayout7, + _setLayout8, + _setLayout9, + _toggleLayoutCycling, + _toggleLayoutSidebar, + _showNewLayoutDialog, + _switchToNextLayout, + _switchToPreviousLayout, + _toggleLayoutLock, + _setSettingsTab1, + _setSettingsTab2, + _setSettingsTab3, + _setSettingsTab4, + _setSettingsTab5, + _setSettingsTab6, + ]; + + /// Initializes the [KeyboardBindings] instance & fetches state from `async` + /// `package:hive` method-calls. Called before [runApp]. + static Future ensureInitialized() async { + instance = KeyboardBindings._(); + await instance.initialize(); + debugPrint('KeyboardBindings initialized'); + return instance; + } + + @override + Future initialize() async { + try { + await initializeStorage('keybindings'); + } catch (error, stackTrace) { + handleError( + error, + stackTrace, + 'Error initializing keybindings storage. Fallback to memory', + ); + } + + for (final setting in _allSettings) { + await setting.loadData(); + } + + notifyListeners(); + } + + @override + Future save({bool notifyListeners = true}) async { + await write({ + for (final setting in _allSettings) + ...() { + try { + return {setting.key: setting.saveAs(setting.value)}; + } catch (error, stackTrace) { + handleError( + error, + stackTrace, + 'Error saving setting ${setting.key}', + ); + } + return {}; + }(), + }); + super.save(notifyListeners: notifyListeners); + } + + Future updateProperty(Future Function() update) async { + await update(); + save(); + } + + Future restoreDefaults() async { + for (final setting in _allSettings) { + setting._value = setting.def; + } + await save(); + await initialize(); + } + + // The list of all keybinding settings + late final List _allSettings = [ + _toggleFullscreen, + _toggleImmersiveMode, + _setTab1, + _setTab2, + _setTab3, + _setTab4, + _setTab5, + _setTab6, + _setTab7, + _setTab8, + _setTab9, + _setLayout1, + _setLayout2, + _setLayout3, + _setLayout4, + _setLayout5, + _setLayout6, + _setLayout7, + _setLayout8, + _setLayout9, + _toggleLayoutCycling, + _toggleLayoutSidebar, + _showNewLayoutDialog, + _switchToNextLayout, + _switchToPreviousLayout, + _toggleLayoutLock, + _setSettingsTab1, + _setSettingsTab2, + _setSettingsTab3, + _setSettingsTab4, + _setSettingsTab5, + _setSettingsTab6, + ]; +} + +class KeybindingSetting { + final String name; + final String system; + final String key; + final SingleActivator def; + + late SingleActivator _value; + + SingleActivator get value => _value; + + set value(SingleActivator newValue) { + _value = newValue; + KeyboardBindings.instance.save(); + } + + KeybindingSetting({ + required this.name, + required this.system, + required this.key, + required this.def, + }) { + Future.microtask(() async { + _value = await defaultValue; + }); + } + + String saveAs(SingleActivator activator) { return [ - ( - name: 'Toggle Fullscreen', - system: 'Global', - activator: toggleFullscreen, - ), - ( - name: 'Toggle Immersive Mode', - system: 'Global', - activator: toggleImmersiveMode, - ), - ( - name: 'Set Layouts Tab', - system: 'Global', - activator: setTab1, - ), - ( - name: 'Set Events Timeline Tab', - system: 'Global', - activator: setTab2, - ), - ( - name: 'Set Add Server Tab', - system: 'Global', - activator: setTab3, - ), - ( - name: 'Set Downloads Tab', - system: 'Global', - activator: setTab4, - ), - ( - name: 'Set Settings Tab', - system: 'Global', - activator: setTab5, - ), - ( - name: 'Set Tab 6', - system: 'Global', - activator: setTab6, - ), - ( - name: 'Set Tab 7', - system: 'Global', - activator: setTab7, - ), - ( - name: 'Set Tab 8', - system: 'Global', - activator: setTab8, - ), - ( - name: 'Set Tab 9', - system: 'Global', - activator: setTab9, - ), - ( - name: 'Set Layout 1', - system: 'Layout', - activator: setLayout1, - ), - ( - name: 'Set Layout 2', - system: 'Layout', - activator: setLayout2, - ), - ( - name: 'Set Layout 3', - system: 'Layout', - activator: setLayout3, - ), - ( - name: 'Set Layout 4', - system: 'Layout', - activator: setLayout4, - ), - ( - name: 'Set Layout 5', - system: 'Layout', - activator: setLayout5, - ), - ( - name: 'Set Layout 6', - system: 'Layout', - activator: setLayout6, - ), - ( - name: 'Set Layout 7', - system: 'Layout', - activator: setLayout7, - ), - ( - name: 'Set Layout 8', - system: 'Layout', - activator: setLayout8, - ), - ( - name: 'Set Layout 9', - system: 'Layout', - activator: setLayout9, - ), - ( - name: 'Toggle Layout Cycling', - system: 'Layout', - activator: toggleLayoutCycling, - ), - ( - name: 'Toggle Layout Sidebar', - system: 'Layout', - activator: toggleLayoutSidebar, - ), - ( - name: 'Show New Layout Dialog', - system: 'Layout', - activator: showNewLayoutDialog, - ), - ( - name: 'Switch to Next Layout', - system: 'Layout', - activator: switchToNextLayout, - ), - ( - name: 'Switch to Previous Layout', - system: 'Layout', - activator: switchToPreviousLayout, - ), - ( - name: 'Toggle Layout Lock', - system: 'Layout', - activator: toggleLayoutLock, - ), - ( - name: 'Go to General Settings', - system: 'Settings', - activator: setSettingsTab1, - ), - ( - name: 'Go to Server and Devices Settings', - system: 'Settings', - activator: setSettingsTab2, - ), - ( - name: 'Go to Events and Downloads Settings', - system: 'Settings', - activator: setSettingsTab3, - ), - ( - name: 'Go to Application Settings', - system: 'Settings', - activator: setSettingsTab4, - ), - ( - name: 'Go to Updates and Help Settings', - system: 'Settings', - activator: setSettingsTab5, - ), - ( - name: 'Go to Advanced Options Settings', - system: 'Settings', - activator: setSettingsTab6, - ), - ]; + if (activator.alt) 'alt', + if (activator.control) 'control', + if (activator.shift) 'shift', + activator.trigger.keyId, + ].join(','); + } + + SingleActivator loadFrom(String value) { + final keys = value.split(','); + return SingleActivator( + LogicalKeyboardKey.findKeyByKeyId(int.parse(keys.last))!, + control: keys.contains('control'), + shift: keys.contains('shift'), + alt: keys.contains('alt'), + ); + } + + Future get defaultValue async { + try { + final serializedData = await secureStorage.read(key: key); + if (serializedData != null) { + return loadFrom(serializedData); + } + } catch (error, stack) { + handleError(error, stack, 'Failed to get keybinding for $key'); + } + return def; + } + + Future loadData() async { + try { + var serializedData = await secureStorage.read(key: key); + serializedData ??= saveAs(def); + _value = loadFrom(serializedData); + } catch (error, stackTrace) { + handleError( + error, + stackTrace, + 'Error loading data for $key. Fallback to default value', + ); + _value = def; + } } } From 74bcd8c0597bd94cee693a44c3e8e028c2895c10 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 28 Nov 2024 14:39:01 -0300 Subject: [PATCH 09/11] fix: Do not update control --- lib/screens/settings/application.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/screens/settings/application.dart b/lib/screens/settings/application.dart index e4e21f68..7d178c01 100644 --- a/lib/screens/settings/application.dart +++ b/lib/screens/settings/application.dart @@ -448,12 +448,22 @@ class _KeybindingDialogState extends State { void _handleKeypress(KeyEvent event) { if (event is KeyDownEvent) { setState(() { + final key = LogicalKeyboardKey.findKeyByKeyId(event.logicalKey.keyId)!; + if (key == LogicalKeyboardKey.control || + key == LogicalKeyboardKey.alt || + key == LogicalKeyboardKey.shift) return; + _newActivator = SingleActivator( - LogicalKeyboardKey.findKeyByKeyId(event.logicalKey.keyId)!, + key, control: HardwareKeyboard.instance.isControlPressed, shift: HardwareKeyboard.instance.isShiftPressed, alt: HardwareKeyboard.instance.isAltPressed, ); + debugPrint( + 'New activator: ' + '${_newActivator!.debugDescribeKeys()}' + '/${_newActivator?.trigger.debugName}', + ); }); } } From 9785032d83e947f43b4b60cbe0832d3d17833f65 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 28 Nov 2024 14:51:03 -0300 Subject: [PATCH 10/11] feat: Only show on desktop and add reset button --- lib/screens/settings/application.dart | 42 +++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/lib/screens/settings/application.dart b/lib/screens/settings/application.dart index 7d178c01..c4fd3288 100644 --- a/lib/screens/settings/application.dart +++ b/lib/screens/settings/application.dart @@ -210,11 +210,43 @@ class ApplicationSettings extends StatelessWidget { ), ), ], - const SubHeader( - 'Keyboard Shortcuts', - padding: DesktopSettings.horizontalPadding, - ), - KeyboardSection(), + if (isDesktopPlatform) ...[ + SubHeader( + 'Keyboard Shortcuts', + padding: DesktopSettings.horizontalPadding, + trailing: TextButton( + child: Text( + 'Reset Defaults', + style: TextStyle(color: theme.colorScheme.error), + ), + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Reset Keyboard Shortcuts'), + content: Text( + 'Are you sure you want to reset all keyboard shortcuts to their default values?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + context.read().restoreDefaults(); + Navigator.pop(context); + }, + child: Text('Reset'), + ), + ], + ), + ); + }, + ), + ), + KeyboardSection(), + ], ]); } From f14cf16fe4967a9ce30f2dd5ebbabb9ecf246727 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 28 Nov 2024 14:55:16 -0300 Subject: [PATCH 11/11] chore: Reorganize settings/application --- lib/screens/settings/application.dart | 320 ++++++++++++++------------ 1 file changed, 175 insertions(+), 145 deletions(-) diff --git a/lib/screens/settings/application.dart b/lib/screens/settings/application.dart index c4fd3288..76464b17 100644 --- a/lib/screens/settings/application.dart +++ b/lib/screens/settings/application.dart @@ -70,7 +70,7 @@ class ApplicationSettings extends StatelessWidget { settings.kThemeMode.value = v; }, ), - if (isMobilePlatform) _buildImmersiveModeTile(), + if (isMobilePlatform) ImmersiveModeTile(), const LanguageSection(), SubHeader(loc.dateAndTime, padding: DesktopSettings.horizontalPadding), const DateFormatSection(), @@ -94,121 +94,14 @@ class ApplicationSettings extends StatelessWidget { ), if (isDesktopPlatform) ...[ const SubHeader('Window', padding: DesktopSettings.horizontalPadding), - if (canLaunchAtStartup) - CheckboxListTile.adaptive( - value: settings.kLaunchAppOnStartup.value, - onChanged: (v) { - if (v != null) { - settings.kLaunchAppOnStartup.value = v; - } - }, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.launch), - ), - title: const Text('Launch app on startup'), - subtitle: const Text( - 'Whether to launch the app when the system starts', - ), - ), - CheckboxListTile.adaptive( - value: settings.kFullscreen.value, - onChanged: (v) { - if (v != null) settings.kFullscreen.value = v; - }, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.fullscreen), - ), - title: const Text('Fullscreen Mode'), - subtitle: const Text('Whether the app is in fullscreen mode or not.'), - ), - _buildImmersiveModeTile(), - if (canUseSystemTray) - CheckboxListTile.adaptive( - value: settings.kMinimizeToTray.value, - onChanged: (v) { - if (v != null) { - settings.kMinimizeToTray.value = v; - } - }, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.sensor_door), - ), - title: const Text('Minimize to tray'), - subtitle: const Text( - 'Whether to minimize app to the system tray when the window is ' - 'closed. This will keep the app running in the background.', - ), - ), + WindowSection(), ], if (settings.kShowDebugInfo.value) ...[ const SubHeader( 'Acessibility', padding: DesktopSettings.horizontalPadding, ), - CheckboxListTile.adaptive( - value: settings.kAnimationsEnabled.value, - onChanged: (v) { - if (v != null) { - settings.kAnimationsEnabled.value = v; - } - }, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.animation), - ), - title: const Text('Animations'), - subtitle: const Text( - 'Disable animations on low-end devices to improve performance. This ' - 'will also disable some visual effects. ', - ), - ), - CheckboxListTile.adaptive( - value: settings.kHighContrast.value, - onChanged: (v) { - if (v != null) { - settings.kHighContrast.value = v; - } - }, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.filter_b_and_w), - ), - title: const Text('High contrast mode'), - subtitle: const Text( - 'Enable high contrast mode to make the app easier to read and use.', - ), - ), - CheckboxListTile.adaptive( - value: settings.kLargeFont.value, - onChanged: (v) { - if (v != null) { - settings.kLargeFont.value = v; - } - }, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.accessibility_new), - ), - title: const Text('Large Font'), - subtitle: const Text( - 'Increase the size of the text in the app to make it easier to read.', - ), - ), + AcessibilitySection(), ], if (isDesktopPlatform) ...[ SubHeader( @@ -249,41 +142,6 @@ class ApplicationSettings extends StatelessWidget { ], ]); } - - /// Creates the Immersive Mode tile. - /// - /// On Desktop, this is used alonside the Fullscreen mode tile. When in - /// fullscreen, the immersive mode hides the top bar and only shows it when - /// the user hovers over the top of the window. - /// - /// On Mobile, this makes the app full-screen and hides the system UI. - Widget _buildImmersiveModeTile() { - return Builder(builder: (context) { - final theme = Theme.of(context); - final settings = context.watch(); - - return CheckboxListTile.adaptive( - value: settings.kImmersiveMode.value, - onChanged: settings.kFullscreen.value || isMobilePlatform - ? (v) { - if (v != null) settings.kImmersiveMode.value = v; - } - : null, - contentPadding: DesktopSettings.horizontalPadding, - secondary: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundColor: theme.iconTheme.color, - child: const Icon(Icons.web_asset), - ), - title: const Text('Immersive Mode'), - subtitle: const Text( - 'This will hide the title bar and window controls. ' - 'To show the top bar, hover over the top of the window. ' - 'This only works in fullscreen mode.', - ), - ); - }); - } } class LanguageSection extends StatelessWidget { @@ -412,6 +270,178 @@ class TimeFormatSection extends StatelessWidget { } } +class WindowSection extends StatelessWidget { + const WindowSection({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final settings = context.watch(); + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (canLaunchAtStartup) + CheckboxListTile.adaptive( + value: settings.kLaunchAppOnStartup.value, + onChanged: (v) { + if (v != null) { + settings.kLaunchAppOnStartup.value = v; + } + }, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.launch), + ), + title: const Text('Launch app on startup'), + subtitle: const Text( + 'Whether to launch the app when the system starts', + ), + ), + CheckboxListTile.adaptive( + value: settings.kFullscreen.value, + onChanged: (v) { + if (v != null) settings.kFullscreen.value = v; + }, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.fullscreen), + ), + title: const Text('Fullscreen Mode'), + subtitle: const Text('Whether the app is in fullscreen mode or not.'), + ), + ImmersiveModeTile(), + if (canUseSystemTray) + CheckboxListTile.adaptive( + value: settings.kMinimizeToTray.value, + onChanged: (v) { + if (v != null) { + settings.kMinimizeToTray.value = v; + } + }, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.sensor_door), + ), + title: const Text('Minimize to tray'), + subtitle: const Text( + 'Whether to minimize app to the system tray when the window is ' + 'closed. This will keep the app running in the background.', + ), + ), + ]); + } +} + +/// Creates the Immersive Mode tile. +/// +/// On Desktop, this is used alonside the Fullscreen mode tile. When in +/// fullscreen, the immersive mode hides the top bar and only shows it when +/// the user hovers over the top of the window. +/// +/// On Mobile, this makes the app full-screen and hides the system UI. +class ImmersiveModeTile extends StatelessWidget { + const ImmersiveModeTile({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final settings = context.watch(); + + return CheckboxListTile.adaptive( + value: settings.kImmersiveMode.value, + onChanged: settings.kFullscreen.value || isMobilePlatform + ? (v) { + if (v != null) settings.kImmersiveMode.value = v; + } + : null, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.web_asset), + ), + title: const Text('Immersive Mode'), + subtitle: const Text( + 'This will hide the title bar and window controls. ' + 'To show the top bar, hover over the top of the window. ' + 'This only works in fullscreen mode.', + ), + ); + } +} + +class AcessibilitySection extends StatelessWidget { + const AcessibilitySection({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final settings = context.watch(); + + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + CheckboxListTile.adaptive( + value: settings.kAnimationsEnabled.value, + onChanged: (v) { + if (v != null) { + settings.kAnimationsEnabled.value = v; + } + }, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.animation), + ), + title: const Text('Animations'), + subtitle: const Text( + 'Disable animations on low-end devices to improve performance. This ' + 'will also disable some visual effects. ', + ), + ), + CheckboxListTile.adaptive( + value: settings.kHighContrast.value, + onChanged: (v) { + if (v != null) { + settings.kHighContrast.value = v; + } + }, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.filter_b_and_w), + ), + title: const Text('High contrast mode'), + subtitle: const Text( + 'Enable high contrast mode to make the app easier to read and use.', + ), + ), + CheckboxListTile.adaptive( + value: settings.kLargeFont.value, + onChanged: (v) { + if (v != null) { + settings.kLargeFont.value = v; + } + }, + contentPadding: DesktopSettings.horizontalPadding, + secondary: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundColor: theme.iconTheme.color, + child: const Icon(Icons.accessibility_new), + ), + title: const Text('Large Font'), + subtitle: const Text( + 'Increase the size of the text in the app to make it easier to read.', + ), + ), + ]); + } +} + class KeyboardSection extends StatelessWidget { const KeyboardSection({super.key});