Compare commits

...

6 Commits

Author SHA1 Message Date
Jonas Bark
edda16dc06 adjust reinstating saved keymaps 2025-03-30 19:30:34 +02:00
Jonas Bark
7a3d120123 Keyboard: no point in catching modifiers 2025-03-30 18:58:38 +02:00
Jonas Bark
92419c9182 potential fix for #6 2025-03-30 18:44:55 +02:00
Jonas Bark
68bb5bf371 Zwift Ride: adjust on off button detection 2025-03-30 18:30:15 +02:00
Jonas Bark
b0d8bfcadd potential fix for #7 2025-03-30 18:29:01 +02:00
Jonas Bark
a58ad1daf6 potential fix for Bluetooth device detection 2025-03-30 16:53:34 +02:00
9 changed files with 112 additions and 52 deletions

View File

@@ -1,4 +1,9 @@
### 1.1.0 (2025-03-30)
### 1.1.3 (2025-03-30)
- Windows: fix custom keyboard profile recreation after restart, also warn when choosing MyWhoosh profile (may fix #7)
- Zwift Ride: button map adjustments to prevent double shifting
- potential fix for #6
### 1.1.1 (2025-03-30)
- potential fix for Bluetooth device detection
### 1.1.0 (2025-03-30)

View File

@@ -53,11 +53,12 @@ class Connection {
Future<void> performScanning() async {
isScanning.value = true;
if (!kIsWeb) {
// does not work on web, may not work on Windows
if (!kIsWeb && !Platform.isWindows) {
UniversalBle.getSystemDevices(
withServices: [BleUuid.ZWIFT_CUSTOM_SERVICE_UUID, BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID],
).then((devices) {
final baseDevices = devices.map((device) => BaseDevice.fromScanResult(device)).whereNotNull().toList();
final baseDevices = devices.mapNotNull(BaseDevice.fromScanResult).toList();
if (baseDevices.isNotEmpty) {
_addDevices(baseDevices);
}

View File

@@ -28,18 +28,23 @@ abstract class BaseDevice {
String get customServiceId => BleUuid.ZWIFT_CUSTOM_SERVICE_UUID;
static BaseDevice? fromScanResult(BleDevice scanResult) {
final manufacturerData = scanResult.manufacturerDataList;
final data = manufacturerData.firstOrNullWhere((e) => e.companyId == Constants.ZWIFT_MANUFACTURER_ID)?.payload;
// Use the name first, probably safest method on all platforms
final device = switch (scanResult.name) {
'Zwift Ride' => ZwiftRide(scanResult),
'Zwift Play' => ZwiftPlay(scanResult),
'Zwift Click' => ZwiftClick(scanResult),
_ => null,
};
// otherwise use the manufacturer data, which doesn't exist on Web and "System Devices"
if (device == null) {
final manufacturerData = scanResult.manufacturerDataList;
final data = manufacturerData.firstOrNullWhere((e) => e.companyId == Constants.ZWIFT_MANUFACTURER_ID)?.payload;
if (data == null || data.isEmpty) {
return null;
}
// Web does not support manufacturer data, also the "system devices" don't return any, so use name fallback
if (data == null || data.isEmpty) {
return switch (scanResult.name) {
'Zwift Ride' => ZwiftRide(scanResult),
'Zwift Play' => ZwiftPlay(scanResult),
'Zwift Click' => ZwiftClick(scanResult),
_ => null,
};
} else {
final type = DeviceType.fromManufacturerData(data.first);
return switch (type) {
DeviceType.click => ZwiftClick(scanResult),

View File

@@ -20,9 +20,9 @@ enum _RideButtonMask {
SHFT_DN_R_BTN(0x02000),
POWERUP_L_BTN(0x00400),
ONOFF_L_BTN(0x01000),
POWERUP_R_BTN(0x04000),
ONOFF_R_BTN(0x20000);
ONOFF_L_BTN(0x00800),
ONOFF_R_BTN(0x08000);
final int mask;

View File

@@ -19,8 +19,8 @@ class DesktopActions extends BaseActions {
if (keymap == null) {
throw Exception('Keymap is not set');
}
await keyPressSimulator.simulateKeyDown(_keymap!.decrease);
await keyPressSimulator.simulateKeyUp(_keymap!.decrease);
await keyPressSimulator.simulateKeyDown(_keymap!.decrease?.physicalKey);
await keyPressSimulator.simulateKeyUp(_keymap!.decrease?.physicalKey);
}
@override
@@ -28,7 +28,7 @@ class DesktopActions extends BaseActions {
if (keymap == null) {
throw Exception('Keymap is not set');
}
await keyPressSimulator.simulateKeyDown(_keymap!.increase);
await keyPressSimulator.simulateKeyUp(_keymap!.increase);
await keyPressSimulator.simulateKeyDown(_keymap!.increase?.physicalKey);
await keyPressSimulator.simulateKeyUp(_keymap!.increase?.physicalKey);
}
}

View File

@@ -1,13 +1,18 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/services.dart';
class Keymap {
static Keymap myWhoosh = Keymap('MyWhoosh', increase: PhysicalKeyboardKey.keyK, decrease: PhysicalKeyboardKey.keyI);
static Keymap myWhoosh = Keymap(
'MyWhoosh',
increase: KeyPair(physicalKey: PhysicalKeyboardKey.keyK, logicalKey: LogicalKeyboardKey.keyK),
decrease: KeyPair(physicalKey: PhysicalKeyboardKey.keyI, logicalKey: LogicalKeyboardKey.keyI),
);
static Keymap custom = Keymap('Custom', increase: null, decrease: null);
static List<Keymap> values = [myWhoosh, custom];
PhysicalKeyboardKey? increase;
PhysicalKeyboardKey? decrease;
KeyPair? increase;
KeyPair? decrease;
final String name;
Keymap(this.name, {required this.increase, required this.decrease});
@@ -17,25 +22,56 @@ class Keymap {
if (increase == null && decrease == null) {
return name;
}
return "$name: ${increase?.debugName} + ${decrease?.debugName}";
return "$name: ${increase?.logicalKey.keyLabel} + ${decrease?.logicalKey.keyLabel}";
}
List<String> encode() {
// encode to save in preferences
return [name, increase?.usbHidUsage.toString() ?? '', decrease?.usbHidUsage.toString() ?? ''];
return [
name,
increase?.logicalKey.keyId.toString() ?? '',
increase?.physicalKey.usbHidUsage.toString() ?? '',
decrease?.logicalKey.keyId.toString() ?? '',
decrease?.physicalKey.usbHidUsage.toString() ?? '',
];
}
static Keymap decode(List<String> data) {
static Keymap? decode(List<String> data) {
// decode from preferences
if (data.length < 4) {
return null;
}
final name = data[0];
final keymap = values.firstWhere((element) => element.name == name, orElse: () => custom);
final keymap = values.firstOrNullWhere((element) => element.name == name);
if (keymap == null) {
return null;
}
if (keymap.name != custom.name) {
return keymap;
}
keymap.increase = data[1].isNotEmpty ? PhysicalKeyboardKey(int.parse(data[1])) : null;
keymap.decrease = data[2].isNotEmpty ? PhysicalKeyboardKey(int.parse(data[2])) : null;
return keymap;
if (data.sublist(1).all((e) => e.isNotEmpty)) {
keymap.increase = KeyPair(
physicalKey: PhysicalKeyboardKey(int.parse(data[2])),
logicalKey: LogicalKeyboardKey(int.parse(data[1])),
);
keymap.decrease = KeyPair(
physicalKey: PhysicalKeyboardKey(int.parse(data[4])),
logicalKey: LogicalKeyboardKey(int.parse(data[3])),
);
return keymap;
} else {
return null;
}
}
}
class KeyPair {
final PhysicalKeyboardKey physicalKey;
final LogicalKeyboardKey logicalKey;
KeyPair({required this.physicalKey, required this.logicalKey});
}

View File

@@ -1,3 +1,6 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:keypress_simulator/keypress_simulator.dart';
import 'package:swift_control/pages/scan.dart';
@@ -35,15 +38,25 @@ class KeymapRequirement extends PlatformRequirement {
@override
Widget? build(BuildContext context, VoidCallback onUpdate) {
final controller = TextEditingController(text: actionHandler.keymap?.name);
return DropdownMenu<Keymap>(
controller: controller,
dropdownMenuEntries:
Keymap.values.map((key) => DropdownMenuEntry<Keymap>(value: key, label: key.toString())).toList(),
onSelected: (keymap) async {
if (keymap!.name == Keymap.custom.name) {
keymap = await showCustomKeymapDialog(context, keymap: keymap);
} else if (keymap.name == Keymap.myWhoosh.name && (!kIsWeb && Platform.isWindows)) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Use a Custom Keymap if you experience any issues on Windows')));
}
controller.text = keymap?.name ?? '';
if (keymap == null) {
return;
}
actionHandler.init(keymap);
settings.setKeymap(keymap!);
settings.setKeymap(keymap);
onUpdate();
},
initialSelection: actionHandler.keymap,

View File

@@ -21,9 +21,9 @@ class GearHotkeyDialog extends StatefulWidget {
class _GearHotkeyDialogState extends State<GearHotkeyDialog> {
final FocusNode _focusNode = FocusNode();
final Set<PhysicalKeyboardKey> _pressedKeys = {};
Set<PhysicalKeyboardKey>? _gearUpHotkey;
Set<PhysicalKeyboardKey>? _gearDownHotkey;
KeyDownEvent? _pressedKey;
KeyDownEvent? _gearUpHotkey;
KeyDownEvent? _gearDownHotkey;
String _mode = 'up'; // 'up' or 'down'
@@ -36,27 +36,32 @@ class _GearHotkeyDialogState extends State<GearHotkeyDialog> {
void _onKey(KeyEvent event) {
setState(() {
if (event is KeyDownEvent) {
_pressedKeys.add(event.physicalKey);
_pressedKey = event;
} else if (event is KeyUpEvent) {
if (_pressedKeys.isNotEmpty) {
if (_pressedKey != null) {
if (_mode == 'up') {
_gearUpHotkey = {..._pressedKeys};
_gearUpHotkey = _pressedKey;
_mode = 'down';
} else {
_gearDownHotkey = {..._pressedKeys};
widget.keymap.increase = _gearUpHotkey!.first;
widget.keymap.decrease = _gearDownHotkey!.first;
_gearDownHotkey = _pressedKey;
widget.keymap.increase = KeyPair(
physicalKey: _gearUpHotkey!.physicalKey,
logicalKey: _gearUpHotkey!.logicalKey,
);
widget.keymap.decrease = KeyPair(
physicalKey: _gearDownHotkey!.physicalKey,
logicalKey: _gearDownHotkey!.logicalKey,
);
Navigator.of(context).pop(widget.keymap);
}
_pressedKeys.clear();
_pressedKey = null;
}
}
});
}
String _formatKeys(Set<PhysicalKeyboardKey>? keys) {
if (keys == null || keys.isEmpty) return 'Not set';
return keys.map((k) => k.debugName ?? k).join(' + ');
String _formatKey(KeyDownEvent? key) {
return key?.logicalKey.keyLabel ?? 'Not set';
}
@override
@@ -76,18 +81,13 @@ class _GearHotkeyDialogState extends State<GearHotkeyDialog> {
ListTile(
leading: Icon(Icons.arrow_upward),
title: Text("Gear Up Hotkey"),
subtitle: Text(_formatKeys(_gearUpHotkey)),
subtitle: Text(_formatKey(_gearUpHotkey)),
),
ListTile(
leading: Icon(Icons.arrow_downward),
title: Text("Gear Down Hotkey"),
subtitle: Text(_formatKeys(_gearDownHotkey)),
subtitle: Text(_formatKey(_gearDownHotkey)),
),
if (_pressedKeys.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text("Recording: ${_formatKeys(_pressedKeys)}", style: TextStyle(color: Colors.blue)),
),
],
),
),

View File

@@ -1,7 +1,7 @@
name: swift_control
description: "SwiftControl - Control your virtual riding"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.1.1+0
version: 1.1.3+0
environment:
sdk: ^3.7.0