From 828aa70a56114404e7e00471bb3f53dfeb616b3e Mon Sep 17 00:00:00 2001 From: Jonas Bark Date: Mon, 27 Oct 2025 12:14:41 +0100 Subject: [PATCH] implement MyWhoosh link in the editor --- lib/bluetooth/connection.dart | 2 +- lib/bluetooth/devices/link/link.dart | 2 +- lib/pages/device.dart | 122 +++---------------- lib/pages/requirements.dart | 2 +- lib/pages/touch_area.dart | 10 +- lib/utils/actions/android.dart | 33 ++--- lib/utils/actions/base_actions.dart | 5 +- lib/utils/actions/desktop.dart | 7 +- lib/utils/actions/remote.dart | 15 ++- lib/utils/keymap/keymap.dart | 17 ++- lib/utils/keymap/manager.dart | 141 +++++++++++++++------- lib/utils/settings/settings.dart | 23 ---- lib/widgets/changelog_dialog.dart | 1 + lib/widgets/ingameactions_customizer.dart | 133 -------------------- lib/widgets/keymap_explanation.dart | 87 ++++++++++--- 15 files changed, 240 insertions(+), 360 deletions(-) delete mode 100644 lib/widgets/ingameactions_customizer.dart diff --git a/lib/bluetooth/connection.dart b/lib/bluetooth/connection.dart index 9ee646d..9857667 100644 --- a/lib/bluetooth/connection.dart +++ b/lib/bluetooth/connection.dart @@ -60,7 +60,7 @@ class Connection { final existingDevice = bluetoothDevices.firstOrNullWhere( (e) => e.device.deviceId == result.deviceId, ); - if (existingDevice != null) { + if (existingDevice != null && existingDevice.rssi != result.rssi) { existingDevice.rssi = result.rssi; _connectionStreams.add(existingDevice); // Notify UI of update } diff --git a/lib/bluetooth/devices/link/link.dart b/lib/bluetooth/devices/link/link.dart index 8f38af4..602a1b2 100644 --- a/lib/bluetooth/devices/link/link.dart +++ b/lib/bluetooth/devices/link/link.dart @@ -132,7 +132,7 @@ class WhooshLink { if (jsonObject != null) { final jsonString = jsonEncode(jsonObject); _socket?.writeln(jsonString); - return 'Sent action to MyWhoosh: $action'; + return 'Sent action to MyWhoosh: $action ${value ?? ''}'; } else { return 'No action available for button: $action'; } diff --git a/lib/pages/device.dart b/lib/pages/device.dart index dc0b7bb..ead9ced 100644 --- a/lib/pages/device.dart +++ b/lib/pages/device.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:device_auto_rotate_checker/device_auto_rotate_checker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:swift_control/bluetooth/devices/zwift/protocol/zp.pbenum.dart'; import 'package:swift_control/bluetooth/devices/zwift/zwift_clickv2.dart'; @@ -13,10 +12,8 @@ import 'package:swift_control/pages/markdown.dart'; import 'package:swift_control/utils/actions/desktop.dart'; import 'package:swift_control/utils/keymap/manager.dart'; import 'package:swift_control/widgets/keymap_explanation.dart'; -import 'package:swift_control/widgets/loading_widget.dart'; import 'package:swift_control/widgets/logviewer.dart'; import 'package:swift_control/widgets/scan.dart'; -import 'package:swift_control/widgets/small_progress_indicator.dart'; import 'package:swift_control/widgets/testbed.dart'; import 'package:swift_control/widgets/title.dart'; import 'package:swift_control/widgets/warning.dart'; @@ -249,15 +246,16 @@ class _DevicePageState extends State with WidgetsBindingObserver { Text( 'Remote Control Mode: ${(actionHandler as RemoteActions).isConnected ? 'Connected' : 'Not connected'}', ), - LoadingWidget( - futureCallback: () async { - final requirement = RemoteRequirement(); - await requirement.reconnect(); - }, - renderChild: (isLoading, tap) => TextButton( - onPressed: tap, - child: isLoading ? SmallProgressIndicator() : Text('Reconnect'), - ), + PopupMenuButton( + itemBuilder: (_) => [ + PopupMenuItem( + child: Text('Reconnect'), + onTap: () async { + final requirement = RemoteRequirement(); + await requirement.reconnect(); + }, + ), + ], ), ], ), @@ -358,7 +356,7 @@ class _DevicePageState extends State with WidgetsBindingObserver { final profileName = await KeymapManager().showNewProfileDialog(context); if (profileName != null && profileName.isNotEmpty) { final customApp = CustomApp(profileName: profileName); - actionHandler.supportedApp = customApp; + actionHandler.init(customApp); await settings.setApp(customApp); controller.text = profileName; setState(() {}); @@ -388,99 +386,11 @@ class _DevicePageState extends State with WidgetsBindingObserver { Row( children: [ - IconButton( - onPressed: () async { - final currentProfile = actionHandler.supportedApp is CustomApp - ? actionHandler.supportedApp?.name - : null; - final action = await KeymapManager().showManageProfileDialog( - context, - currentProfile, - ); - if (action != null) { - if (action == 'rename') { - final newName = await KeymapManager().showRenameProfileDialog( - context, - currentProfile!, - ); - if (newName != null && newName.isNotEmpty && newName != currentProfile) { - await settings.duplicateCustomAppProfile(currentProfile, newName); - await settings.deleteCustomAppProfile(currentProfile); - final customApp = CustomApp(profileName: newName); - final savedKeymap = settings.getCustomAppKeymap(newName); - if (savedKeymap != null) { - customApp.decodeKeymap(savedKeymap); - } - actionHandler.supportedApp = customApp; - await settings.setApp(customApp); - controller.text = newName; - setState(() {}); - } - } else if (action == 'duplicate') { - final newName = await KeymapManager().duplicate( - context, - currentProfile!, - ); - - if (newName != null) { - controller.text = newName; - setState(() {}); - } - } else if (action == 'delete') { - final confirmed = await KeymapManager().showDeleteConfirmDialog( - context, - currentProfile!, - ); - if (confirmed == true) { - await settings.deleteCustomAppProfile(currentProfile); - controller.text = ''; - setState(() {}); - } - } else if (action == 'import') { - final jsonData = await KeymapManager().showImportDialog(context); - if (jsonData != null && jsonData.isNotEmpty) { - final success = await settings.importCustomAppProfile(jsonData); - if (mounted) { - if (success) { - _snackBarMessengerKey.currentState!.showSnackBar( - SnackBar( - content: Text('Profile imported successfully'), - duration: Duration(seconds: 5), - ), - ); - setState(() {}); - } else { - _snackBarMessengerKey.currentState!.showSnackBar( - SnackBar( - content: Text('Failed to import profile. Invalid format.'), - duration: Duration(seconds: 5), - backgroundColor: Colors.red, - ), - ); - } - } - } - } else if (action == 'export') { - final currentProfile = - (actionHandler.supportedApp as CustomApp).profileName; - final jsonData = settings.exportCustomAppProfile(currentProfile); - if (jsonData != null) { - Clipboard.setData(ClipboardData(text: jsonData)); - if (mounted) { - _snackBarMessengerKey.currentState!.showSnackBar( - SnackBar( - content: Text( - 'Profile "$currentProfile" exported to clipboard', - ), - duration: Duration(seconds: 5), - ), - ); - } - } - } - } - }, - icon: Icon(Icons.more_vert), + KeymapManager().getManageProfileDialog( + context, + actionHandler.supportedApp is CustomApp + ? actionHandler.supportedApp?.name + : null, ), ], ), diff --git a/lib/pages/requirements.dart b/lib/pages/requirements.dart index 04be538..1851839 100644 --- a/lib/pages/requirements.dart +++ b/lib/pages/requirements.dart @@ -146,7 +146,7 @@ class _RequirementsPageState extends State with WidgetsBinding currentPath = route.settings.name; return true; }); - if (currentPath == null || currentPath != '/device') { + if (currentPath == '/') { Navigator.push( context, MaterialPageRoute( diff --git a/lib/pages/touch_area.dart b/lib/pages/touch_area.dart index 51280b7..4159758 100644 --- a/lib/pages/touch_area.dart +++ b/lib/pages/touch_area.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:dartx/dartx.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -372,7 +373,14 @@ class KeypairExplanation extends StatelessWidget { ) else Icon(keyPair.icon), - if (keyPair.isSpecialKey && actionHandler.supportedModes.contains(SupportedMode.media)) + if (keyPair.inGameAction != null && whooshLink.isConnected.value) + _KeyWidget( + label: [ + keyPair.inGameAction.toString().split('.').last, + if (keyPair.inGameActionValue != null) ': ${keyPair.inGameActionValue}', + ].joinToString(separator: ''), + ) + else if (keyPair.isSpecialKey && actionHandler.supportedModes.contains(SupportedMode.media)) _KeyWidget( label: switch (keyPair.physicalKey) { PhysicalKeyboardKey.mediaPlayPause => 'Play/Pause', diff --git a/lib/utils/actions/android.dart b/lib/utils/actions/android.dart index bb12652..e20558a 100644 --- a/lib/utils/actions/android.dart +++ b/lib/utils/actions/android.dart @@ -2,7 +2,6 @@ import 'package:accessibility/accessibility.dart'; import 'package:flutter/services.dart'; import 'package:swift_control/main.dart'; import 'package:swift_control/utils/actions/base_actions.dart'; -import 'package:swift_control/utils/keymap/apps/custom_app.dart'; import 'package:swift_control/utils/keymap/buttons.dart'; import 'package:swift_control/widgets/keymap_explanation.dart'; @@ -30,20 +29,26 @@ class AndroidActions extends BaseActions { return ("Could not perform ${button.name.splitByUpperCase()}: No keymap set"); } - if (supportedApp is CustomApp) { - final keyPair = supportedApp!.keymap.getKeyPair(button); - if (keyPair != null && keyPair.isSpecialKey) { - await accessibilityHandler.controlMedia(switch (keyPair.physicalKey) { - PhysicalKeyboardKey.mediaTrackNext => MediaAction.next, - PhysicalKeyboardKey.mediaPlayPause => MediaAction.playPause, - PhysicalKeyboardKey.audioVolumeUp => MediaAction.volumeUp, - PhysicalKeyboardKey.audioVolumeDown => MediaAction.volumeDown, - _ => throw SingleLineException("No action for key: ${keyPair.physicalKey}"), - }); - return "Key pressed: ${keyPair.toString()}"; - } + final keyPair = supportedApp!.keymap.getKeyPair(button); + + if (keyPair == null) { + return ("Could not perform ${button.name.splitByUpperCase()}: No action assigned"); } - final point = await resolveTouchPosition(action: button, windowInfo: windowInfo); + + if (keyPair.inGameAction != null && whooshLink.isConnected.value) { + return whooshLink.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue); + } else if (keyPair.isSpecialKey) { + await accessibilityHandler.controlMedia(switch (keyPair.physicalKey) { + PhysicalKeyboardKey.mediaTrackNext => MediaAction.next, + PhysicalKeyboardKey.mediaPlayPause => MediaAction.playPause, + PhysicalKeyboardKey.audioVolumeUp => MediaAction.volumeUp, + PhysicalKeyboardKey.audioVolumeDown => MediaAction.volumeDown, + _ => throw SingleLineException("No action for key: ${keyPair.physicalKey}"), + }); + return "Key pressed: ${keyPair.toString()}"; + } + + final point = await resolveTouchPosition(keyPair: keyPair, windowInfo: windowInfo); if (point != Offset.zero) { try { await accessibilityHandler.performTouch(point.dx, point.dy, isKeyDown: isKeyDown, isKeyUp: isKeyUp); diff --git a/lib/utils/actions/base_actions.dart b/lib/utils/actions/base_actions.dart index 8650772..5e78b91 100644 --- a/lib/utils/actions/base_actions.dart +++ b/lib/utils/actions/base_actions.dart @@ -46,9 +46,8 @@ abstract class BaseActions { } } - Future resolveTouchPosition({required ControllerButton action, required WindowEvent? windowInfo}) async { - final keyPair = supportedApp!.keymap.getKeyPair(action); - if (keyPair != null && keyPair.touchPosition != Offset.zero) { + Future resolveTouchPosition({required KeyPair keyPair, required WindowEvent? windowInfo}) async { + if (keyPair.touchPosition != Offset.zero) { // convert relative position to absolute position based on window info // TODO support multiple screens diff --git a/lib/utils/actions/desktop.dart b/lib/utils/actions/desktop.dart index 78e5e7e..cb866c7 100644 --- a/lib/utils/actions/desktop.dart +++ b/lib/utils/actions/desktop.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:keypress_simulator/keypress_simulator.dart'; +import 'package:swift_control/main.dart'; import 'package:swift_control/utils/actions/base_actions.dart'; import 'package:swift_control/utils/keymap/buttons.dart'; import 'package:swift_control/widgets/keymap_explanation.dart'; @@ -22,7 +23,9 @@ class DesktopActions extends BaseActions { } // Handle regular key press mode (existing behavior) - if (keyPair.physicalKey != null) { + if (keyPair.inGameAction != null && whooshLink.isConnected.value) { + return whooshLink.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue); + } else if (keyPair.physicalKey != null) { if (isKeyDown && isKeyUp) { await keyPressSimulator.simulateKeyDown(keyPair.physicalKey); await keyPressSimulator.simulateKeyUp(keyPair.physicalKey); @@ -35,7 +38,7 @@ class DesktopActions extends BaseActions { return 'Key released: $keyPair'; } } else { - final point = await resolveTouchPosition(action: action, windowInfo: null); + final point = await resolveTouchPosition(keyPair: keyPair, windowInfo: null); if (point != Offset.zero) { if (isKeyDown && isKeyUp) { await keyPressSimulator.simulateMouseClickDown(point); diff --git a/lib/utils/actions/remote.dart b/lib/utils/actions/remote.dart index 5cd3810..993b5cf 100644 --- a/lib/utils/actions/remote.dart +++ b/lib/utils/actions/remote.dart @@ -7,6 +7,7 @@ import 'package:swift_control/bluetooth/devices/zwift/zwift_click.dart'; import 'package:swift_control/main.dart'; import 'package:swift_control/utils/actions/base_actions.dart'; import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/keymap/keymap.dart'; import 'package:swift_control/widgets/keymap_explanation.dart'; import 'package:universal_ble/universal_ble.dart'; @@ -26,14 +27,16 @@ class RemoteActions extends BaseActions { return 'Keymap entry not found for action: ${action.toString().splitByUpperCase()}'; } - if (!(actionHandler as RemoteActions).isConnected) { + if (keyPair.inGameAction != null && whooshLink.isConnected.value) { + return whooshLink.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue); + } else if (!(actionHandler as RemoteActions).isConnected) { return 'Not connected to a device'; } if (keyPair.physicalKey != null && keyPair.touchPosition == Offset.zero) { return ('Physical key actions are not supported, yet'); } else { - final point = await resolveTouchPosition(action: action, windowInfo: null); + final point = await resolveTouchPosition(keyPair: keyPair, windowInfo: null); final point2 = point; //Offset(100, 99.0); await sendAbsMouseReport(0, point2.dx.toInt(), point2.dy.toInt()); await sendAbsMouseReport(1, point2.dx.toInt(), point2.dy.toInt()); @@ -44,13 +47,9 @@ class RemoteActions extends BaseActions { } @override - Future resolveTouchPosition({required ControllerButton action, required WindowEvent? windowInfo}) async { + Future resolveTouchPosition({required KeyPair keyPair, required WindowEvent? windowInfo}) async { // for remote actions we use the relative position only - final keyPair = supportedApp!.keymap.getKeyPair(action); - if (keyPair != null && keyPair.touchPosition != Offset.zero) { - return keyPair.touchPosition; - } - return Offset.zero; + return keyPair.touchPosition; } Uint8List absMouseReport(int buttons3bit, int x, int y) { diff --git a/lib/utils/keymap/keymap.dart b/lib/utils/keymap/keymap.dart index 1802643..a7d9b14 100644 --- a/lib/utils/keymap/keymap.dart +++ b/lib/utils/keymap/keymap.dart @@ -60,6 +60,8 @@ class KeyPair { LogicalKeyboardKey? logicalKey; Offset touchPosition; bool isLongPress; + InGameAction? inGameAction; + int? inGameActionValue; KeyPair({ required this.buttons, @@ -67,6 +69,8 @@ class KeyPair { required this.logicalKey, this.touchPosition = Offset.zero, this.isLongPress = false, + this.inGameAction, + this.inGameActionValue, }); bool get isSpecialKey => @@ -85,10 +89,9 @@ class KeyPair { PhysicalKeyboardKey.mediaTrackNext || PhysicalKeyboardKey.audioVolumeUp || PhysicalKeyboardKey.audioVolumeDown => Icons.music_note_outlined, - _ => - physicalKey != null && actionHandler.supportedModes.contains(SupportedMode.keyboard) - ? Icons.keyboard - : Icons.touch_app, + _ when physicalKey != null && actionHandler.supportedModes.contains(SupportedMode.keyboard) => Icons.keyboard, + _ when inGameAction != null && whooshLink.isConnected.value => Icons.link, + _ => Icons.touch_app, }; } @@ -115,6 +118,8 @@ class KeyPair { if (physicalKey != null) 'physicalKey': physicalKey?.usbHidUsage.toString() ?? '0', if (touchPosition != Offset.zero) 'touchPosition': {'x': touchPosition.dx, 'y': touchPosition.dy}, 'isLongPress': isLongPress, + 'inGameAction': inGameAction?.name, + 'inGameActionValue': inGameActionValue, }); } @@ -149,6 +154,10 @@ class KeyPair { : null, touchPosition: touchPosition, isLongPress: decoded['isLongPress'] ?? false, + inGameAction: decoded.containsKey('inGameAction') + ? InGameAction.values.firstOrNullWhere((element) => element.name == decoded['inGameAction']) + : null, + inGameActionValue: decoded['inGameActionValue'], ); } } diff --git a/lib/utils/keymap/manager.dart b/lib/utils/keymap/manager.dart index 146ba64..190bed9 100644 --- a/lib/utils/keymap/manager.dart +++ b/lib/utils/keymap/manager.dart @@ -36,51 +36,102 @@ class KeymapManager { ); } - Future showManageProfileDialog(BuildContext context, String? currentProfile) async { - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('Manage Profile: ${currentProfile ?? ''}'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (currentProfile != null && actionHandler.supportedApp is CustomApp) - ListTile( - leading: Icon(Icons.edit), - title: Text('Rename'), - onTap: () => Navigator.pop(context, 'rename'), - ), - if (currentProfile != null) - ListTile( - leading: Icon(Icons.copy), - title: Text('Duplicate'), - onTap: () => Navigator.pop(context, 'duplicate'), - ), - ListTile( - leading: Icon(Icons.file_upload), - title: Text('Import'), - onTap: () => Navigator.pop(context, 'import'), - ), - if (currentProfile != null) - ListTile( - leading: Icon(Icons.share), - title: Text('Export'), - onTap: () => Navigator.pop(context, 'export'), - ), - if (currentProfile != null) - ListTile( - leading: Icon(Icons.delete, color: Theme.of(context).colorScheme.error), - title: Text('Delete', style: TextStyle(color: Theme.of(context).colorScheme.error)), - onTap: () => Navigator.pop(context, 'delete'), - ), - ], + PopupMenuButton getManageProfileDialog(BuildContext context, String? currentProfile) { + return PopupMenuButton( + itemBuilder: (context) => [ + if (currentProfile != null && actionHandler.supportedApp is CustomApp) + PopupMenuItem( + child: Text('Rename'), + onTap: () async { + final newName = await _showRenameProfileDialog( + context, + currentProfile, + ); + if (newName != null && newName.isNotEmpty && newName != currentProfile) { + await settings.duplicateCustomAppProfile(currentProfile, newName); + await settings.deleteCustomAppProfile(currentProfile); + final customApp = CustomApp(profileName: newName); + final savedKeymap = settings.getCustomAppKeymap(newName); + if (savedKeymap != null) { + customApp.decodeKeymap(savedKeymap); + } + actionHandler.supportedApp = customApp; + await settings.setApp(customApp); + } + }, + ), + if (currentProfile != null) + PopupMenuItem( + child: Text('Duplicate'), + onTap: () async { + final newName = await duplicate( + context, + currentProfile, + ); + }, + ), + PopupMenuItem( + child: Text('Import'), + onTap: () async { + final jsonData = await _showImportDialog(context); + if (jsonData != null && jsonData.isNotEmpty) { + final success = await settings.importCustomAppProfile(jsonData); + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Profile imported successfully'), + duration: Duration(seconds: 5), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to import profile. Invalid format.'), + duration: Duration(seconds: 5), + backgroundColor: Colors.red, + ), + ); + } + } + }, ), - actions: [TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel'))], - ), + if (currentProfile != null) + PopupMenuItem( + child: Text('Export'), + onTap: () { + final currentProfile = (actionHandler.supportedApp as CustomApp).profileName; + final jsonData = settings.exportCustomAppProfile(currentProfile); + if (jsonData != null) { + Clipboard.setData(ClipboardData(text: jsonData)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Profile "$currentProfile" exported to clipboard', + ), + duration: Duration(seconds: 5), + ), + ); + } + }, + ), + if (currentProfile != null) + PopupMenuItem( + child: Text('Delete', style: TextStyle(color: Theme.of(context).colorScheme.error)), + onTap: () async { + final confirmed = await _showDeleteConfirmDialog( + context, + currentProfile, + ); + if (confirmed == true) { + await settings.deleteCustomAppProfile(currentProfile); + } + }, + ), + ], ); } - Future showRenameProfileDialog(BuildContext context, String currentName) async { + Future _showRenameProfileDialog(BuildContext context, String currentName) async { final controller = TextEditingController(text: currentName); return showDialog( context: context, @@ -99,7 +150,7 @@ class KeymapManager { ); } - Future showDuplicateProfileDialog(BuildContext context, String currentName) async { + Future _showDuplicateProfileDialog(BuildContext context, String currentName) async { final controller = TextEditingController(text: '$currentName (Copy)'); return showDialog( context: context, @@ -118,7 +169,7 @@ class KeymapManager { ); } - Future showDeleteConfirmDialog(BuildContext context, String profileName) async { + Future _showDeleteConfirmDialog(BuildContext context, String profileName) async { return showDialog( context: context, builder: (context) => AlertDialog( @@ -136,7 +187,7 @@ class KeymapManager { ); } - Future showImportDialog(BuildContext context) async { + Future _showImportDialog(BuildContext context) async { final controller = TextEditingController(); // Try to get data from clipboard @@ -175,7 +226,7 @@ class KeymapManager { } Future duplicate(BuildContext context, String currentProfile) async { - final newName = await showDuplicateProfileDialog(context, currentProfile); + final newName = await _showDuplicateProfileDialog(context, currentProfile); if (newName != null && newName.isNotEmpty) { if (actionHandler.supportedApp is CustomApp) { await settings.duplicateCustomAppProfile(currentProfile, newName); diff --git a/lib/utils/settings/settings.dart b/lib/utils/settings/settings.dart index 25b6133..54afc29 100644 --- a/lib/utils/settings/settings.dart +++ b/lib/utils/settings/settings.dart @@ -4,7 +4,6 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:swift_control/utils/keymap/apps/supported_app.dart'; -import 'package:swift_control/utils/keymap/buttons.dart'; import 'package:swift_control/utils/requirements/multi.dart'; import 'package:window_manager/window_manager.dart'; @@ -203,26 +202,4 @@ class Settings { return migratedData; } - - void setInGameActionForButton(ControllerButton button, InGameAction inGameAction) { - final key = 'ingameaction_${button.name}'; - prefs.setString(key, inGameAction.name); - } - - InGameAction? getInGameActionForButton(ControllerButton button) { - final key = 'ingameaction_${button.name}'; - final actionName = prefs.getString(key); - if (actionName == null) return button.action; - return InGameAction.values.firstOrNullWhere((e) => e.name == actionName) ?? button.action; - } - - void setInGameActionForButtonValue(ControllerButton button, InGameAction inGameAction, int value) { - final key = 'ingameaction_${button.name}_value'; - prefs.setInt(key, value); - } - - int? getInGameActionForButtonValue(ControllerButton button) { - final key = 'ingameaction_${button.name}_value'; - return prefs.getInt(key); - } } diff --git a/lib/widgets/changelog_dialog.dart b/lib/widgets/changelog_dialog.dart index 84d84e9..5a66bb3 100644 --- a/lib/widgets/changelog_dialog.dart +++ b/lib/widgets/changelog_dialog.dart @@ -44,6 +44,7 @@ class ChangelogDialog extends StatelessWidget { showDialog( context: context, useRootNavigator: true, + routeSettings: RouteSettings(name: '/changelog'), builder: (context) => ChangelogDialog(entry: markdown), ); } diff --git a/lib/widgets/ingameactions_customizer.dart b/lib/widgets/ingameactions_customizer.dart deleted file mode 100644 index 95cb9a4..0000000 --- a/lib/widgets/ingameactions_customizer.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'package:dartx/dartx.dart'; -import 'package:flutter/material.dart'; -import 'package:swift_control/bluetooth/devices/link/link.dart'; -import 'package:swift_control/main.dart'; -import 'package:swift_control/pages/device.dart'; -import 'package:swift_control/utils/keymap/buttons.dart'; -import 'package:swift_control/widgets/button_widget.dart'; - -class InGameActionsCustomizer extends StatefulWidget { - const InGameActionsCustomizer({super.key}); - - @override - State createState() => _InGameActionsCustomizerState(); -} - -class _InGameActionsCustomizerState extends State { - @override - Widget build(BuildContext context) { - final connectedDevice = connection.devices.firstOrNull; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - spacing: 8, - children: [ - Table( - border: TableBorder.symmetric( - borderRadius: BorderRadius.circular(9), - inside: BorderSide( - color: Theme.of(context).colorScheme.primaryContainer, - ), - outside: BorderSide( - color: Theme.of(context).colorScheme.primaryContainer, - ), - ), - children: [ - TableRow( - children: [ - Padding( - padding: const EdgeInsets.all(6), - child: Text( - 'Button on your ${connectedDevice?.name.screenshot ?? connectedDevice?.runtimeType ?? 'device'}', - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), - ), - ), - Padding( - padding: const EdgeInsets.all(6), - child: Text( - 'Action on MyWhoosh', - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), - ), - ), - ], - ), - for (final button in connectedDevice?.availableButtons ?? []) ...[ - TableRow( - children: [ - Padding( - padding: const EdgeInsets.all(6), - child: Row( - children: [ - ButtonWidget(button: button), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(6), - child: Row( - children: [ - if (MediaQuery.sizeOf(context).width < 1800) - Expanded(child: _buildDropdownButton(button, true)) - else - _buildDropdownButton(button, false), - ], - ), - ), - ], - ), - ], - ], - ), - ], - ); - } - - Widget _buildDropdownButton(ControllerButton button, bool expand) { - final value = WhooshLink.supportedActions.contains(settings.getInGameActionForButton(button)) - ? settings.getInGameActionForButton(button) - : null; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DropdownButton( - isExpanded: expand, - items: WhooshLink.supportedActions - .map( - (ingame) => DropdownMenuItem( - value: ingame, - child: Text(ingame.toString()), - ), - ) - .toList(), - padding: EdgeInsets.zero, - menuWidth: 250, - value: value, - onChanged: (action) { - settings.setInGameActionForButton( - button, - action!, - ); - setState(() {}); - }, - ), - if (value?.possibleValues != null) - DropdownButton( - items: value!.possibleValues! - .map((val) => DropdownMenuItem(value: val, child: Text(val.toString()))) - .toList(), - value: settings.getInGameActionForButtonValue(button), - onChanged: (val) { - settings.setInGameActionForButtonValue( - button, - value, - val!, - ); - setState(() {}); - }, - hint: Text('Value'), - ), - ], - ); - } -} diff --git a/lib/widgets/keymap_explanation.dart b/lib/widgets/keymap_explanation.dart index 21e38d8..341d19b 100644 --- a/lib/widgets/keymap_explanation.dart +++ b/lib/widgets/keymap_explanation.dart @@ -12,6 +12,7 @@ import 'package:swift_control/utils/keymap/manager.dart'; import 'package:swift_control/widgets/button_widget.dart'; import 'package:swift_control/widgets/custom_keymap_selector.dart'; +import '../bluetooth/devices/link/link.dart'; import '../pages/touch_area.dart'; class KeymapExplanation extends StatefulWidget { @@ -133,6 +134,52 @@ class _ButtonEditor extends StatelessWidget { @override Widget build(BuildContext context) { final actions = [ + if (whooshLink.isConnected.value) + PopupMenuItem( + child: PopupMenuButton( + itemBuilder: (_) => WhooshLink.supportedActions.map( + (ingame) { + return PopupMenuItem( + value: ingame, + child: ingame.possibleValues != null + ? PopupMenuButton( + itemBuilder: (c) => ingame.possibleValues! + .map( + (value) => PopupMenuItem( + value: value, + child: Text(value.toString()), + onTap: () { + keyPair.inGameAction = ingame; + keyPair.inGameActionValue = value; + onUpdate(); + }, + ), + ) + .toList(), + child: Row( + children: [ + Expanded(child: Text(ingame.toString())), + Icon(Icons.arrow_right), + ], + ), + ) + : Text(ingame.toString()), + onTap: () { + keyPair.inGameAction = ingame; + keyPair.inGameActionValue = null; + onUpdate(); + }, + ); + }, + ).toList(), + child: Row( + children: [ + Expanded(child: Text('MyWhoosh Link Action')), + Icon(Icons.arrow_right), + ], + ), + ), + ), if (actionHandler.supportedModes.contains(SupportedMode.keyboard)) PopupMenuItem( value: null, @@ -227,12 +274,33 @@ class _ButtonEditor extends StatelessWidget { ), ), ), + + PopupMenuItem( + value: null, + onTap: () { + keyPair.isLongPress = !keyPair.isLongPress; + onUpdate(); + }, + child: ListTile( + title: const Text('Long Press Mode (vs. repeating)'), + trailing: Checkbox( + value: keyPair.isLongPress, + onChanged: (value) { + keyPair.isLongPress = value ?? false; + + onUpdate(); + Navigator.of(context).pop(); + }, + ), + ), + ), ]; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (keyPair.buttons.isNotEmpty && (keyPair.physicalKey != null || keyPair.touchPosition != Offset.zero)) + if (keyPair.buttons.isNotEmpty && + (keyPair.physicalKey != null || keyPair.touchPosition != Offset.zero || keyPair.inGameAction != null)) Expanded( child: KeypairExplanation( keyPair: keyPair, @@ -246,23 +314,6 @@ class _ButtonEditor extends StatelessWidget { enabled: true, itemBuilder: (context) => [ if (actions.length > 1) ...actions, - PopupMenuItem( - value: null, - onTap: () { - keyPair.isLongPress = !keyPair.isLongPress; - onUpdate(); - }, - child: CheckboxListTile( - value: keyPair.isLongPress, - onChanged: (value) { - keyPair.isLongPress = value ?? false; - - onUpdate(); - Navigator.of(context).pop(); - }, - title: const Text('Long Press Mode (vs. repeating)'), - ), - ), ], onSelected: (key) { keyPair.physicalKey = key;