mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
implement MyWhoosh link in the editor
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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<DevicePage> 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<DevicePage> 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<DevicePage> 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -146,7 +146,7 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
|
||||
currentPath = route.settings.name;
|
||||
return true;
|
||||
});
|
||||
if (currentPath == null || currentPath != '/device') {
|
||||
if (currentPath == '/') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -46,9 +46,8 @@ abstract class BaseActions {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Offset> resolveTouchPosition({required ControllerButton action, required WindowEvent? windowInfo}) async {
|
||||
final keyPair = supportedApp!.keymap.getKeyPair(action);
|
||||
if (keyPair != null && keyPair.touchPosition != Offset.zero) {
|
||||
Future<Offset> 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Offset> resolveTouchPosition({required ControllerButton action, required WindowEvent? windowInfo}) async {
|
||||
Future<Offset> 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) {
|
||||
|
||||
@@ -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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,51 +36,102 @@ class KeymapManager {
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> showManageProfileDialog(BuildContext context, String? currentProfile) async {
|
||||
return showDialog<String>(
|
||||
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<String> 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<String?> showRenameProfileDialog(BuildContext context, String currentName) async {
|
||||
Future<String?> _showRenameProfileDialog(BuildContext context, String currentName) async {
|
||||
final controller = TextEditingController(text: currentName);
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
@@ -99,7 +150,7 @@ class KeymapManager {
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> showDuplicateProfileDialog(BuildContext context, String currentName) async {
|
||||
Future<String?> _showDuplicateProfileDialog(BuildContext context, String currentName) async {
|
||||
final controller = TextEditingController(text: '$currentName (Copy)');
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
@@ -118,7 +169,7 @@ class KeymapManager {
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool?> showDeleteConfirmDialog(BuildContext context, String profileName) async {
|
||||
Future<bool?> _showDeleteConfirmDialog(BuildContext context, String profileName) async {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
@@ -136,7 +187,7 @@ class KeymapManager {
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> showImportDialog(BuildContext context) async {
|
||||
Future<String?> _showImportDialog(BuildContext context) async {
|
||||
final controller = TextEditingController();
|
||||
|
||||
// Try to get data from clipboard
|
||||
@@ -175,7 +226,7 @@ class KeymapManager {
|
||||
}
|
||||
|
||||
Future<String?> 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ class ChangelogDialog extends StatelessWidget {
|
||||
showDialog(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
routeSettings: RouteSettings(name: '/changelog'),
|
||||
builder: (context) => ChangelogDialog(entry: markdown),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<InGameActionsCustomizer> createState() => _InGameActionsCustomizerState();
|
||||
}
|
||||
|
||||
class _InGameActionsCustomizerState extends State<InGameActionsCustomizer> {
|
||||
@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 ?? <ControllerButton>[]) ...[
|
||||
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<InGameAction>(
|
||||
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<int>(
|
||||
items: value!.possibleValues!
|
||||
.map((val) => DropdownMenuItem<int>(value: val, child: Text(val.toString())))
|
||||
.toList(),
|
||||
value: settings.getInGameActionForButtonValue(button),
|
||||
onChanged: (val) {
|
||||
settings.setInGameActionForButtonValue(
|
||||
button,
|
||||
value,
|
||||
val!,
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
hint: Text('Value'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<PhysicalKeyboardKey>(
|
||||
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<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
@@ -227,12 +274,33 @@ class _ButtonEditor extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
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<PhysicalKeyboardKey>(
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user