mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edda16dc06 | ||
|
|
7a3d120123 | ||
|
|
92419c9182 | ||
|
|
68bb5bf371 | ||
|
|
b0d8bfcadd | ||
|
|
a58ad1daf6 |
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user