mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
@@ -1,4 +1,4 @@
|
||||
### 3.3.0 (unreleased)
|
||||
### 3.3.0 (31-10-2025)
|
||||
|
||||
**New Feature:**
|
||||
- Support for Elite Sterzo (thanks @michidk)
|
||||
@@ -6,11 +6,13 @@
|
||||
- Support for cheap bluetooth remotes (such as [these](https://www.amazon.com/s?k=bluetooth+remote))
|
||||
- you can now customize the Keymap right from the Customize section
|
||||
- show signal strength of connected devices (thanks @michidk)
|
||||
- Android and Windows only: simulate bluetooth controllers
|
||||
- enables gamepad and bluetooth remotes support for Zwift, Rouvy and Biketerra
|
||||
|
||||
**Fixes:**
|
||||
- fix firmware version display for Zwift Click V2 devices
|
||||
- fix touch position on some Android devices
|
||||
- Wahoo Kickr Bike Shift can now be connected correctly
|
||||
- Wahoo Kickr Bike Shift can now be connected
|
||||
- update default keymap for TrainingPeaks
|
||||
|
||||
### 3.2.0 (2025-10-22)
|
||||
|
||||
13
README.md
13
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
## Description
|
||||
|
||||
With SwiftControl you can **control your favorite trainer app** using your Zwift Click, Zwift Ride or Zwift Play devices. Here's what you can do with it, depending on your configuration:
|
||||
With SwiftControl you can **control your favorite trainer app** using your Zwift Click, Zwift Ride, Zwift Play, or other similar devices. Here's what you can do with it, depending on your configuration:
|
||||
- Virtual Gear shifting
|
||||
- Steering / turning
|
||||
- adjust workout intensity
|
||||
@@ -33,7 +33,11 @@ Check the compatibility matrix below!
|
||||
- TrainingPeaks Virtual / indieVelo
|
||||
- Biketerra.com
|
||||
- Rouvy
|
||||
- any other! You can add custom mapping and adjust touch points or keyboard shortcuts to your liking
|
||||
- Zwift
|
||||
- only Android and Windows support virtual shifting and in-app-navigation
|
||||
- iOS / macOS only support controlling Zwift via keyboard shortcuts or touch controls
|
||||
- any other!
|
||||
- you can add custom mapping and adjust touch points or keyboard shortcuts to your liking
|
||||
|
||||
## Supported Devices
|
||||
- Zwift Click
|
||||
@@ -44,7 +48,9 @@ Check the compatibility matrix below!
|
||||
- Elite Sterzo Smart (for steering support)
|
||||
- Elite Square Smart Frame (beta)
|
||||
- Gamepads (beta)
|
||||
- Cheap Bluetooth buttons such as [these](https://www.amazon.com/s?k=bluetooth+remote) (beta, confirmed on Android)
|
||||
- Cheap Bluetooth buttons such as [these](https://www.amazon.com/s?k=bluetooth+remote) (beta)
|
||||
- works on Android
|
||||
- on iOS it would require playing back an audio file - let me know if that is of interest to you
|
||||
|
||||
Support for other devices can be added - check the issues tab here on GithUb.
|
||||
|
||||
@@ -80,7 +86,6 @@ The app connects to your Controller devices (such as Zwift ones) automatically.
|
||||
- there are predefined Keymaps for MyWhoosh, indieVelo / Training Peaks, and others
|
||||
- you can also create your own Keymaps for any other app
|
||||
- you can also use the mouse to click on a certain part of the screen, or use keyboard shortcuts
|
||||
</details>
|
||||
|
||||
## Alternatives
|
||||
- [qdomyos-zwift](https://www.qzfitness.com/) directly controls the trainer (as opposed to controlling the trainer app). This can be useful if your trainer app does not support virtual shifting.
|
||||
|
||||
@@ -12,6 +12,13 @@ class MainActivity: FlutterActivity(), GamepadsCompatibleActivity {
|
||||
var keyListener: ((KeyEvent) -> Boolean)? = null
|
||||
var motionListener: ((MotionEvent) -> Boolean)? = null
|
||||
|
||||
override fun isGamepadsInputDevice(device: InputDevice): Boolean {
|
||||
return device.sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD
|
||||
|| device.sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
|
||||
// Some bluetooth keyboards are identified as GamePad. Check if it is ALPHABETIC keyboard.
|
||||
// && device.keyboardType != InputDevice.KEYBOARD_TYPE_ALPHABETIC
|
||||
}
|
||||
|
||||
override fun dispatchGenericMotionEvent(motionEvent: MotionEvent): Boolean {
|
||||
return motionListener?.invoke(motionEvent) ?: false
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
class BleUuid {
|
||||
static final DEVICE_INFORMATION_SERVICE_UUID = "0000180a-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION = "00002a26-0000-1000-8000-00805f9b34fb"
|
||||
.toLowerCase();
|
||||
static const DEVICE_INFORMATION_SERVICE_UUID = "0000180a-0000-1000-8000-00805f9b34fb";
|
||||
static const DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION = "00002a26-0000-1000-8000-00805f9b34fb";
|
||||
|
||||
static final DEVICE_BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final DEVICE_INFORMATION_CHARACTERISTIC_BATTERY_LEVEL = "00002a19-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static const DEVICE_BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb";
|
||||
static const DEVICE_INFORMATION_CHARACTERISTIC_BATTERY_LEVEL = "00002A19-0000-1000-8000-00805F9B34FB";
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ class Connection {
|
||||
_addDevices(pads);
|
||||
});
|
||||
|
||||
if (settings.getMyWhooshLinkEnabled() && settings.getTrainerApp() is MyWhoosh) {
|
||||
if (settings.getMyWhooshLinkEnabled() && settings.getTrainerApp() is MyWhoosh && !whooshLink.isStarted.value) {
|
||||
startMyWhooshServer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@ abstract class BluetoothDevice extends BaseDevice {
|
||||
if (device != null) {
|
||||
return device;
|
||||
} else if (scanResult.services.containsAny([
|
||||
ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID,
|
||||
ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID,
|
||||
ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID.toLowerCase(),
|
||||
ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID.toLowerCase(),
|
||||
])) {
|
||||
// otherwise use the manufacturer data to identify the device
|
||||
final manufacturerData = scanResult.manufacturerDataList;
|
||||
|
||||
@@ -10,6 +10,8 @@ import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/utils/keymap/keymap.dart';
|
||||
import 'package:swift_control/widgets/beta_pill.dart';
|
||||
|
||||
import '../../../widgets/warning.dart';
|
||||
|
||||
class GamepadDevice extends BaseDevice {
|
||||
final String id;
|
||||
|
||||
@@ -50,14 +52,29 @@ class GamepadDevice extends BaseDevice {
|
||||
|
||||
@override
|
||||
Widget showInformation(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
name.screenshot,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (isBeta) BetaPill(),
|
||||
],
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
name.screenshot,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (isBeta) BetaPill(),
|
||||
],
|
||||
),
|
||||
|
||||
if (actionHandler.supportedApp is! CustomApp)
|
||||
Warning(
|
||||
children: [
|
||||
Text('Use a custom keymap to use the buttons on $name.'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ class WhooshLink {
|
||||
InGameAction.navigateLeft => null,
|
||||
InGameAction.navigateRight => null,
|
||||
InGameAction.toggleUi => null,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (jsonObject != null) {
|
||||
|
||||
@@ -4,11 +4,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
|
||||
class ZwiftConstants {
|
||||
static final ZWIFT_CUSTOM_SERVICE_UUID = "00000001-19CA-4651-86E5-FA29DCDD09D1".toLowerCase();
|
||||
static final ZWIFT_RIDE_CUSTOM_SERVICE_UUID = "0000fc82-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final ZWIFT_ASYNC_CHARACTERISTIC_UUID = "00000002-19CA-4651-86E5-FA29DCDD09D1".toLowerCase();
|
||||
static final ZWIFT_SYNC_RX_CHARACTERISTIC_UUID = "00000003-19CA-4651-86E5-FA29DCDD09D1".toLowerCase();
|
||||
static final ZWIFT_SYNC_TX_CHARACTERISTIC_UUID = "00000004-19CA-4651-86E5-FA29DCDD09D1".toLowerCase();
|
||||
static const ZWIFT_CUSTOM_SERVICE_UUID = "00000001-19CA-4651-86E5-FA29DCDD09D1";
|
||||
static const ZWIFT_RIDE_CUSTOM_SERVICE_UUID = "0000fc82-0000-1000-8000-00805f9b34fb";
|
||||
static const ZWIFT_RIDE_CUSTOM_SERVICE_UUID_SHORT = "fc82";
|
||||
static const ZWIFT_ASYNC_CHARACTERISTIC_UUID = "00000002-19CA-4651-86E5-FA29DCDD09D1";
|
||||
static const ZWIFT_SYNC_RX_CHARACTERISTIC_UUID = "00000003-19CA-4651-86E5-FA29DCDD09D1";
|
||||
static const ZWIFT_SYNC_TX_CHARACTERISTIC_UUID = "00000004-19CA-4651-86E5-FA29DCDD09D1";
|
||||
|
||||
static const ZWIFT_MANUFACTURER_ID = 2378; // Zwift, Inc => 0x094A
|
||||
|
||||
@@ -32,21 +33,21 @@ class ZwiftConstants {
|
||||
static final VIBRATE_PATTERN = Uint8List.fromList([0x12, 0x12, 0x08, 0x0A, 0x06, 0x08, 0x02, 0x10, 0x00, 0x18]);
|
||||
|
||||
// these don't actually seem to matter, its just the header has to be 7 bytes RIDEON + 2
|
||||
static final REQUEST_START = Uint8List.fromList([0, 9]); //byteArrayOf(1, 2)
|
||||
static final RESPONSE_START_CLICK = Uint8List.fromList([1, 3]); // from device
|
||||
static final RESPONSE_START_PLAY = Uint8List.fromList([1, 4]); // from device
|
||||
static final REQUEST_START = Uint8List.fromList([0x00, 0x09]); //byteArrayOf(1, 2)
|
||||
static final RESPONSE_START_CLICK = Uint8List.fromList([0x01, 0x03]); // from device
|
||||
static final RESPONSE_START_PLAY = Uint8List.fromList([0x01, 0x04]); // from device
|
||||
static final RESPONSE_START_CLICK_V2 = Uint8List.fromList([0x02, 0x03]); // from device
|
||||
static final RESPONSE_STOPPED_CLICK_V2_VARIANT_1 = Uint8List.fromList([0xff, 0x05, 0x00, 0xea, 0x05]); // from device
|
||||
static final RESPONSE_STOPPED_CLICK_V2_VARIANT_2 = Uint8List.fromList([0xff, 0x05, 0x00, 0xfa, 0x05]); // from device
|
||||
|
||||
// Message types received from device
|
||||
static const CONTROLLER_NOTIFICATION_MESSAGE_TYPE = 07;
|
||||
static const EMPTY_MESSAGE_TYPE = 21;
|
||||
static const EMPTY_MESSAGE_TYPE = 21; // 0x15
|
||||
static const BATTERY_LEVEL_TYPE = 25;
|
||||
static const UNKNOWN_CLICKV2_TYPE = 0x3C;
|
||||
|
||||
// not figured out the protobuf type this really is, the content is just two varints.
|
||||
static const int CLICK_NOTIFICATION_MESSAGE_TYPE = 55;
|
||||
static const int CLICK_NOTIFICATION_MESSAGE_TYPE = 55; // 0x37
|
||||
static const int PLAY_NOTIFICATION_MESSAGE_TYPE = 7;
|
||||
static const int RIDE_NOTIFICATION_MESSAGE_TYPE = 35; // 0x23
|
||||
|
||||
@@ -58,7 +59,7 @@ class ZwiftButtons {
|
||||
// left controller
|
||||
static const ControllerButton navigationUp = ControllerButton(
|
||||
'navigationUp',
|
||||
action: null,
|
||||
action: InGameAction.toggleUi,
|
||||
icon: Icons.keyboard_arrow_up,
|
||||
color: Colors.black,
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ abstract class ZwiftDevice extends BluetoothDevice {
|
||||
|
||||
@override
|
||||
Future<void> handleServices(List<BleService> services) async {
|
||||
final customService = services.firstOrNullWhere((service) => service.uuid == customServiceId);
|
||||
final customService = services.firstOrNullWhere((service) => service.uuid == customServiceId.toLowerCase());
|
||||
|
||||
if (customService == null) {
|
||||
throw Exception(
|
||||
@@ -33,10 +33,10 @@ abstract class ZwiftDevice extends BluetoothDevice {
|
||||
}
|
||||
|
||||
final deviceInformationService = services.firstOrNullWhere(
|
||||
(service) => service.uuid == BleUuid.DEVICE_INFORMATION_SERVICE_UUID,
|
||||
(service) => service.uuid == BleUuid.DEVICE_INFORMATION_SERVICE_UUID.toLowerCase(),
|
||||
);
|
||||
final firmwareCharacteristic = deviceInformationService?.characteristics.firstOrNullWhere(
|
||||
(c) => c.uuid == BleUuid.DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION,
|
||||
(c) => c.uuid == BleUuid.DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION.toLowerCase(),
|
||||
);
|
||||
if (firmwareCharacteristic != null) {
|
||||
final firmwareData = await UniversalBle.read(
|
||||
@@ -56,13 +56,13 @@ abstract class ZwiftDevice extends BluetoothDevice {
|
||||
}
|
||||
|
||||
final asyncCharacteristic = customService.characteristics.firstOrNullWhere(
|
||||
(characteristic) => characteristic.uuid == ZwiftConstants.ZWIFT_ASYNC_CHARACTERISTIC_UUID,
|
||||
(characteristic) => characteristic.uuid == ZwiftConstants.ZWIFT_ASYNC_CHARACTERISTIC_UUID.toLowerCase(),
|
||||
);
|
||||
final syncTxCharacteristic = customService.characteristics.firstOrNullWhere(
|
||||
(characteristic) => characteristic.uuid == ZwiftConstants.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID,
|
||||
(characteristic) => characteristic.uuid == ZwiftConstants.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID.toLowerCase(),
|
||||
);
|
||||
syncRxCharacteristic = customService.characteristics.firstOrNullWhere(
|
||||
(characteristic) => characteristic.uuid == ZwiftConstants.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID,
|
||||
(characteristic) => characteristic.uuid == ZwiftConstants.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID.toLowerCase(),
|
||||
);
|
||||
|
||||
if (asyncCharacteristic == null || syncTxCharacteristic == null || syncRxCharacteristic == null) {
|
||||
@@ -87,7 +87,7 @@ abstract class ZwiftDevice extends BluetoothDevice {
|
||||
|
||||
@override
|
||||
Future<void> processCharacteristic(String characteristic, Uint8List bytes) async {
|
||||
if (kDebugMode && false) {
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
"${DateTime.now().toString().split(" ").last} Received data on $characteristic: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}",
|
||||
);
|
||||
@@ -98,7 +98,7 @@ abstract class ZwiftDevice extends BluetoothDevice {
|
||||
|
||||
try {
|
||||
if (bytes.startsWith(startCommand)) {
|
||||
_processDevicePublicKeyResponse(bytes);
|
||||
processDevicePublicKeyResponse(bytes);
|
||||
} else {
|
||||
processData(bytes);
|
||||
}
|
||||
@@ -113,7 +113,7 @@ abstract class ZwiftDevice extends BluetoothDevice {
|
||||
}
|
||||
}
|
||||
|
||||
void _processDevicePublicKeyResponse(Uint8List bytes) {
|
||||
void processDevicePublicKeyResponse(Uint8List bytes) {
|
||||
final devicePublicKeyBytes = bytes.sublist(
|
||||
ZwiftConstants.RIDE_ON.length + ZwiftConstants.RESPONSE_START_CLICK.length,
|
||||
);
|
||||
|
||||
367
lib/bluetooth/devices/zwift/zwift_emulator.dart
Normal file
367
lib/bluetooth/devices/zwift/zwift_emulator.dart
Normal file
@@ -0,0 +1,367 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide ConnectionState;
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:swift_control/bluetooth/ble.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/constants.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/protocol/zp.pbenum.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/utils/requirements/multi.dart';
|
||||
import 'package:swift_control/widgets/title.dart';
|
||||
|
||||
import 'protocol/zwift.pb.dart' show RideKeyPadStatus;
|
||||
|
||||
final zwiftEmulator = ZwiftEmulator();
|
||||
|
||||
class ZwiftEmulator {
|
||||
static final List<InGameAction> supportedActions = [
|
||||
InGameAction.shiftUp,
|
||||
InGameAction.shiftDown,
|
||||
InGameAction.uturn,
|
||||
InGameAction.steerLeft,
|
||||
InGameAction.steerRight,
|
||||
InGameAction.openActionBar,
|
||||
InGameAction.usePowerUp,
|
||||
InGameAction.select,
|
||||
InGameAction.back,
|
||||
InGameAction.rideOnBomb,
|
||||
];
|
||||
|
||||
ValueNotifier<bool> isConnected = ValueNotifier<bool>(false);
|
||||
bool get isAdvertising => _isAdvertising;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
final peripheralManager = PeripheralManager();
|
||||
bool _isAdvertising = false;
|
||||
bool _isLoading = false;
|
||||
bool _isServiceAdded = false;
|
||||
bool _isSubscribedToEvents = false;
|
||||
Central? _central;
|
||||
GATTCharacteristic? _asyncCharacteristic;
|
||||
|
||||
Future<void> reconnect() async {
|
||||
await peripheralManager.stopAdvertising();
|
||||
await peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
_isAdvertising = false;
|
||||
startAdvertising(() {});
|
||||
}
|
||||
|
||||
Future<void> startAdvertising(VoidCallback onUpdate) async {
|
||||
_isLoading = true;
|
||||
onUpdate();
|
||||
|
||||
peripheralManager.stateChanged.forEach((state) {
|
||||
print('Peripheral manager state: ${state.state}');
|
||||
});
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
if (Platform.isAndroid) {
|
||||
peripheralManager.connectionStateChanged.forEach((state) {
|
||||
print('Peripheral connection state: ${state.state} of ${state.central.uuid}');
|
||||
if (state.state == ConnectionState.connected) {
|
||||
} else if (state.state == ConnectionState.disconnected) {
|
||||
_central = null;
|
||||
isConnected.value = false;
|
||||
onUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final status = await Permission.bluetoothAdvertise.request();
|
||||
if (!status.isGranted) {
|
||||
print('Bluetooth advertise permission not granted');
|
||||
_isAdvertising = false;
|
||||
onUpdate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (peripheralManager.state != BluetoothLowEnergyState.poweredOn) {
|
||||
print('Waiting for peripheral manager to be powered on...');
|
||||
if (settings.getLastTarget() == Target.thisDevice) {
|
||||
return;
|
||||
}
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
|
||||
final syncTxCharacteristic = GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString(ZwiftConstants.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID),
|
||||
descriptors: [],
|
||||
properties: [
|
||||
GATTCharacteristicProperty.read,
|
||||
GATTCharacteristicProperty.indicate,
|
||||
],
|
||||
permissions: [
|
||||
GATTCharacteristicPermission.read,
|
||||
],
|
||||
);
|
||||
|
||||
_asyncCharacteristic = GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString(ZwiftConstants.ZWIFT_ASYNC_CHARACTERISTIC_UUID),
|
||||
descriptors: [],
|
||||
properties: [
|
||||
GATTCharacteristicProperty.notify,
|
||||
],
|
||||
permissions: [],
|
||||
);
|
||||
|
||||
if (!_isServiceAdded) {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
|
||||
if (!_isSubscribedToEvents) {
|
||||
_isSubscribedToEvents = true;
|
||||
peripheralManager.characteristicReadRequested.forEach((eventArgs) async {
|
||||
print('Read request for characteristic: ${eventArgs.characteristic.uuid}');
|
||||
|
||||
switch (eventArgs.characteristic.uuid.toString().toUpperCase()) {
|
||||
case ZwiftConstants.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID:
|
||||
print('Handling read request for SYNC TX characteristic');
|
||||
break;
|
||||
case BleUuid.DEVICE_INFORMATION_CHARACTERISTIC_BATTERY_LEVEL:
|
||||
await peripheralManager.respondReadRequestWithValue(
|
||||
eventArgs.request,
|
||||
value: Uint8List.fromList([100]),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
print('Unhandled read request for characteristic: ${eventArgs.characteristic.uuid}');
|
||||
}
|
||||
|
||||
final request = eventArgs.request;
|
||||
final trimmedValue = Uint8List.fromList([]);
|
||||
await peripheralManager.respondReadRequestWithValue(
|
||||
request,
|
||||
value: trimmedValue,
|
||||
);
|
||||
// You can respond to read requests here if needed
|
||||
});
|
||||
|
||||
peripheralManager.characteristicNotifyStateChanged.forEach((char) {
|
||||
print(
|
||||
'Notify state changed for characteristic: ${char.characteristic.uuid}: ${char.state}',
|
||||
);
|
||||
});
|
||||
peripheralManager.characteristicWriteRequested.forEach((eventArgs) async {
|
||||
_central = eventArgs.central;
|
||||
isConnected.value = true;
|
||||
|
||||
final characteristic = eventArgs.characteristic;
|
||||
final request = eventArgs.request;
|
||||
final value = request.value;
|
||||
print(
|
||||
'Write request for characteristic: ${characteristic.uuid}',
|
||||
);
|
||||
|
||||
switch (eventArgs.characteristic.uuid.toString().toUpperCase()) {
|
||||
case ZwiftConstants.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID:
|
||||
print(
|
||||
'Handling write request for SYNC RX characteristic, value: ${value.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}\n${String.fromCharCodes(value)}',
|
||||
);
|
||||
|
||||
final handshake = [...ZwiftConstants.RIDE_ON, ...ZwiftConstants.RESPONSE_START_CLICK_V2];
|
||||
final handshakeAlternative = ZwiftConstants.RIDE_ON; // e.g. Rouvy
|
||||
|
||||
if (value.contentEquals(handshake) || value.contentEquals(handshakeAlternative)) {
|
||||
print('Sending handshake');
|
||||
await peripheralManager.notifyCharacteristic(
|
||||
_central!,
|
||||
syncTxCharacteristic,
|
||||
value: ZwiftConstants.RIDE_ON,
|
||||
);
|
||||
onUpdate();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
print('Unhandled write request for characteristic: ${eventArgs.characteristic.uuid}');
|
||||
}
|
||||
|
||||
await peripheralManager.respondWriteRequest(request);
|
||||
});
|
||||
}
|
||||
|
||||
// Device Information
|
||||
await peripheralManager.addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString('180A'),
|
||||
isPrimary: true,
|
||||
characteristics: [
|
||||
GATTCharacteristic.immutable(
|
||||
uuid: UUID.fromString('2A29'),
|
||||
value: Uint8List.fromList('SwiftControl'.codeUnits),
|
||||
descriptors: [],
|
||||
),
|
||||
GATTCharacteristic.immutable(
|
||||
uuid: UUID.fromString('2A25'),
|
||||
value: Uint8List.fromList('09-B48123283828F1337'.codeUnits),
|
||||
descriptors: [],
|
||||
),
|
||||
GATTCharacteristic.immutable(
|
||||
uuid: UUID.fromString('2A27'),
|
||||
value: Uint8List.fromList('A.0'.codeUnits),
|
||||
descriptors: [],
|
||||
),
|
||||
GATTCharacteristic.immutable(
|
||||
uuid: UUID.fromString('2A26'),
|
||||
value: Uint8List.fromList((packageInfoValue?.version ?? '1.0.0').codeUnits),
|
||||
descriptors: [],
|
||||
),
|
||||
],
|
||||
includedServices: [],
|
||||
),
|
||||
);
|
||||
|
||||
// Battery Service
|
||||
await peripheralManager.addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString('180F'),
|
||||
isPrimary: true,
|
||||
characteristics: [
|
||||
GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString('2A19'),
|
||||
descriptors: [],
|
||||
properties: [
|
||||
GATTCharacteristicProperty.read,
|
||||
GATTCharacteristicProperty.notify,
|
||||
],
|
||||
permissions: [
|
||||
GATTCharacteristicPermission.read,
|
||||
],
|
||||
),
|
||||
],
|
||||
includedServices: [],
|
||||
),
|
||||
);
|
||||
|
||||
// Unknown Service
|
||||
await peripheralManager.addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString(ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID_SHORT),
|
||||
isPrimary: true,
|
||||
characteristics: [
|
||||
_asyncCharacteristic!,
|
||||
GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString(ZwiftConstants.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID),
|
||||
descriptors: [],
|
||||
properties: [
|
||||
GATTCharacteristicProperty.writeWithoutResponse,
|
||||
],
|
||||
permissions: [],
|
||||
),
|
||||
syncTxCharacteristic,
|
||||
GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString('00000005-19CA-4651-86E5-FA29DCDD09D1'),
|
||||
descriptors: [],
|
||||
properties: [
|
||||
GATTCharacteristicProperty.notify,
|
||||
],
|
||||
permissions: [],
|
||||
),
|
||||
GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString('00000006-19CA-4651-86E5-FA29DCDD09D1'),
|
||||
descriptors: [],
|
||||
properties: [
|
||||
GATTCharacteristicProperty.indicate,
|
||||
GATTCharacteristicProperty.read,
|
||||
GATTCharacteristicProperty.writeWithoutResponse,
|
||||
GATTCharacteristicProperty.write,
|
||||
],
|
||||
permissions: [
|
||||
GATTCharacteristicPermission.read,
|
||||
GATTCharacteristicPermission.write,
|
||||
],
|
||||
),
|
||||
],
|
||||
includedServices: [],
|
||||
),
|
||||
);
|
||||
_isServiceAdded = true;
|
||||
}
|
||||
|
||||
final advertisement = Advertisement(
|
||||
name: 'SwiftControl',
|
||||
serviceUUIDs: [UUID.fromString(ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID_SHORT)],
|
||||
serviceData: {
|
||||
UUID.fromString(ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID_SHORT): Uint8List.fromList([0x02]),
|
||||
},
|
||||
manufacturerSpecificData: [
|
||||
ManufacturerSpecificData(
|
||||
id: 0x094A,
|
||||
data: Uint8List.fromList([ZwiftConstants.CLICK_V2_LEFT_SIDE, 0x13, 0x37]),
|
||||
),
|
||||
],
|
||||
);
|
||||
print('Starting advertising with HID service...');
|
||||
|
||||
await peripheralManager.startAdvertising(advertisement);
|
||||
_isAdvertising = true;
|
||||
_isLoading = false;
|
||||
onUpdate();
|
||||
}
|
||||
|
||||
Future<void> stopAdvertising() async {
|
||||
await peripheralManager.stopAdvertising();
|
||||
_isAdvertising = false;
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
Future<String> sendAction(InGameAction inGameAction, int? inGameActionValue) async {
|
||||
final button = switch (inGameAction) {
|
||||
InGameAction.shiftUp => RideButtonMask.SHFT_UP_R_BTN,
|
||||
InGameAction.shiftDown => RideButtonMask.SHFT_UP_L_BTN,
|
||||
InGameAction.uturn => RideButtonMask.DOWN_BTN,
|
||||
InGameAction.steerLeft => RideButtonMask.LEFT_BTN,
|
||||
InGameAction.steerRight => RideButtonMask.RIGHT_BTN,
|
||||
InGameAction.openActionBar => RideButtonMask.UP_BTN,
|
||||
InGameAction.usePowerUp => RideButtonMask.Y_BTN,
|
||||
InGameAction.select => RideButtonMask.A_BTN,
|
||||
InGameAction.back => RideButtonMask.B_BTN,
|
||||
InGameAction.rideOnBomb => RideButtonMask.Z_BTN,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (button == null) {
|
||||
return 'Action ${inGameAction.name} not supported by Zwift Emulator';
|
||||
}
|
||||
|
||||
final status = RideKeyPadStatus()
|
||||
..buttonMap = (~button.mask) & 0xFFFFFFFF
|
||||
..analogPaddles.clear();
|
||||
|
||||
final bytes = status.writeToBuffer();
|
||||
|
||||
final commandProto = Uint8List.fromList([
|
||||
Opcode.CONTROLLER_NOTIFICATION.value,
|
||||
...bytes,
|
||||
]);
|
||||
|
||||
peripheralManager.notifyCharacteristic(_central!, _asyncCharacteristic!, value: commandProto);
|
||||
|
||||
final zero = Uint8List.fromList([0x23, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F]);
|
||||
peripheralManager.notifyCharacteristic(_central!, _asyncCharacteristic!, value: zero);
|
||||
return 'Sent action: ${inGameAction.name}';
|
||||
}
|
||||
}
|
||||
|
||||
class ZwiftEmulatorInformation extends StatelessWidget {
|
||||
const ZwiftEmulatorInformation({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: zwiftEmulator.isConnected,
|
||||
builder: (context, isConnected, _) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return Text('Zwift is ${isConnected ? 'connected' : 'not connected'}');
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -190,24 +190,23 @@ class ZwiftRide extends ZwiftDevice {
|
||||
|
||||
// Process DIGITAL buttons separately
|
||||
final buttonsClicked = [
|
||||
if (status.buttonMap & _RideButtonMask.LEFT_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.navigationLeft,
|
||||
if (status.buttonMap & _RideButtonMask.RIGHT_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.navigationRight,
|
||||
if (status.buttonMap & _RideButtonMask.UP_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.navigationUp,
|
||||
if (status.buttonMap & _RideButtonMask.DOWN_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.navigationDown,
|
||||
if (status.buttonMap & _RideButtonMask.A_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.a,
|
||||
if (status.buttonMap & _RideButtonMask.B_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.b,
|
||||
if (status.buttonMap & _RideButtonMask.Y_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.y,
|
||||
if (status.buttonMap & _RideButtonMask.Z_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.z,
|
||||
if (status.buttonMap & _RideButtonMask.SHFT_UP_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.shiftUpLeft,
|
||||
if (status.buttonMap & _RideButtonMask.SHFT_DN_L_BTN.mask == PlayButtonStatus.ON.value)
|
||||
ZwiftButtons.shiftDownLeft,
|
||||
if (status.buttonMap & _RideButtonMask.SHFT_UP_R_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.shiftUpRight,
|
||||
if (status.buttonMap & _RideButtonMask.SHFT_DN_R_BTN.mask == PlayButtonStatus.ON.value)
|
||||
if (status.buttonMap & RideButtonMask.LEFT_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.navigationLeft,
|
||||
if (status.buttonMap & RideButtonMask.RIGHT_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.navigationRight,
|
||||
if (status.buttonMap & RideButtonMask.UP_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.navigationUp,
|
||||
if (status.buttonMap & RideButtonMask.DOWN_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.navigationDown,
|
||||
if (status.buttonMap & RideButtonMask.A_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.a,
|
||||
if (status.buttonMap & RideButtonMask.B_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.b,
|
||||
if (status.buttonMap & RideButtonMask.Y_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.y,
|
||||
if (status.buttonMap & RideButtonMask.Z_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.z,
|
||||
if (status.buttonMap & RideButtonMask.SHFT_UP_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.shiftUpLeft,
|
||||
if (status.buttonMap & RideButtonMask.SHFT_DN_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.shiftDownLeft,
|
||||
if (status.buttonMap & RideButtonMask.SHFT_UP_R_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.shiftUpRight,
|
||||
if (status.buttonMap & RideButtonMask.SHFT_DN_R_BTN.mask == PlayButtonStatus.ON.value)
|
||||
ZwiftButtons.shiftDownRight,
|
||||
if (status.buttonMap & _RideButtonMask.POWERUP_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.powerUpLeft,
|
||||
if (status.buttonMap & _RideButtonMask.POWERUP_R_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.powerUpRight,
|
||||
if (status.buttonMap & _RideButtonMask.ONOFF_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.onOffLeft,
|
||||
if (status.buttonMap & _RideButtonMask.ONOFF_R_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.onOffRight,
|
||||
if (status.buttonMap & RideButtonMask.POWERUP_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.powerUpLeft,
|
||||
if (status.buttonMap & RideButtonMask.POWERUP_R_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.powerUpRight,
|
||||
if (status.buttonMap & RideButtonMask.ONOFF_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.onOffLeft,
|
||||
if (status.buttonMap & RideButtonMask.ONOFF_R_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.onOffRight,
|
||||
];
|
||||
|
||||
// Process ANALOG inputs separately - now properly separated from digital
|
||||
@@ -267,7 +266,7 @@ class ZwiftRide extends ZwiftDevice {
|
||||
}
|
||||
}
|
||||
|
||||
enum _RideButtonMask {
|
||||
enum RideButtonMask {
|
||||
LEFT_BTN(0x00001),
|
||||
UP_BTN(0x00002),
|
||||
RIGHT_BTN(0x00004),
|
||||
@@ -290,5 +289,5 @@ enum _RideButtonMask {
|
||||
|
||||
final int mask;
|
||||
|
||||
const _RideButtonMask(this.mask);
|
||||
const RideButtonMask(this.mask);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ Future<void> initializeActions(ConnectionType connectionType) async {
|
||||
ConnectionType.unknown => StubActions(),
|
||||
};
|
||||
}
|
||||
actionHandler.init(settings.getSupportedApp());
|
||||
actionHandler.init(settings.getKeyMap());
|
||||
}
|
||||
|
||||
class SwiftPlayApp extends StatelessWidget {
|
||||
|
||||
@@ -6,10 +6,12 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:swift_control/bluetooth/devices/link/link_device.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/actions/desktop.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/my_whoosh.dart';
|
||||
import 'package:swift_control/utils/keymap/manager.dart';
|
||||
import 'package:swift_control/utils/requirements/zwift.dart';
|
||||
import 'package:swift_control/widgets/beta_pill.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
import 'package:swift_control/widgets/logviewer.dart';
|
||||
@@ -59,6 +61,16 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
|
||||
zwiftEmulator.isConnected.addListener(() {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
|
||||
if (settings.getZwiftEmulatorEnabled() && actionHandler.supportedApp?.supportsZwiftEmulation == true) {
|
||||
zwiftEmulator.startAdvertising(() {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
if (actionHandler is RemoteActions && !kIsWeb && Platform.isIOS && (actionHandler as RemoteActions).isConnected) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// show snackbar to inform user that the app needs to stay in foreground
|
||||
@@ -175,12 +187,6 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
|
||||
Text('Enable auto-rotation on your device to make sure the app works correctly.'),
|
||||
],
|
||||
),
|
||||
if (connection.gamepadDevices.isNotEmpty && actionHandler.supportedApp is! CustomApp)
|
||||
Warning(
|
||||
children: [
|
||||
Text('Your gamepad requires a custom keymap to be able to use all buttons.'),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text('Connected Devices', style: Theme.of(context).textTheme.titleMedium),
|
||||
@@ -229,7 +235,8 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
|
||||
|
||||
if (connection.remoteDevices.isNotEmpty ||
|
||||
actionHandler is RemoteActions ||
|
||||
settings.getTrainerApp() is MyWhoosh)
|
||||
settings.getTrainerApp() is MyWhoosh ||
|
||||
actionHandler.supportedApp?.supportsZwiftEmulation == true)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 8.0),
|
||||
width: double.infinity,
|
||||
@@ -254,6 +261,10 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
|
||||
|
||||
if (settings.getTrainerApp() is MyWhoosh && !whooshLink.isConnected.value)
|
||||
LinkDevice('').showInformation(context),
|
||||
if (actionHandler.supportedApp?.supportsZwiftEmulation == true)
|
||||
ZwiftRequirement().build(context, () {
|
||||
setState(() {});
|
||||
})!,
|
||||
|
||||
if (actionHandler is RemoteActions)
|
||||
Row(
|
||||
@@ -336,14 +347,14 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
|
||||
if (profileName != null && profileName.isNotEmpty) {
|
||||
final customApp = CustomApp(profileName: profileName);
|
||||
actionHandler.init(customApp);
|
||||
await settings.setSupportedApp(customApp);
|
||||
await settings.setKeyMap(customApp);
|
||||
controller.text = profileName;
|
||||
setState(() {});
|
||||
}
|
||||
} else {
|
||||
controller.text = app.name ?? '';
|
||||
actionHandler.supportedApp = app;
|
||||
await settings.setSupportedApp(app);
|
||||
await settings.setKeyMap(app);
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
@@ -370,7 +381,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
|
||||
if (actionHandler.supportedApp is! CustomApp)
|
||||
Text(
|
||||
'Customize the keymap if you experience any issues (e.g. wrong keyboard output, or misaligned touch placements)',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
if (actionHandler.supportedApp != null && connection.controllerDevices.isNotEmpty)
|
||||
KeymapExplanation(
|
||||
@@ -381,7 +392,7 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
|
||||
controller.text = actionHandler.supportedApp?.name ?? '';
|
||||
|
||||
if (actionHandler.supportedApp is CustomApp) {
|
||||
settings.setSupportedApp(actionHandler.supportedApp!);
|
||||
settings.setKeyMap(actionHandler.supportedApp!);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -175,7 +175,9 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
|
||||
}
|
||||
|
||||
void _reloadRequirements() {
|
||||
getRequirements(settings.getLastTarget()?.connectionType ?? ConnectionType.unknown).then((req) {
|
||||
getRequirements(
|
||||
settings.getLastTarget()?.connectionType ?? ConnectionType.unknown,
|
||||
).then((req) {
|
||||
_requirements = req;
|
||||
final unresolvedIndex = req.indexWhere((req) => !req.status);
|
||||
if (unresolvedIndex != -1) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/widgets/button_widget.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
@@ -400,7 +401,7 @@ class KeypairExplanation extends StatelessWidget {
|
||||
)
|
||||
else
|
||||
Icon(keyPair.icon),
|
||||
if (keyPair.inGameAction != null && whooshLink.isConnected.value)
|
||||
if (keyPair.inGameAction != null && (whooshLink.isConnected.value || zwiftEmulator.isConnected.value))
|
||||
_KeyWidget(
|
||||
label: [
|
||||
keyPair.inGameAction.toString().split('.').last,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:accessibility/accessibility.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/actions/base_actions.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
@@ -37,6 +38,8 @@ class AndroidActions extends BaseActions {
|
||||
|
||||
if (keyPair.inGameAction != null && whooshLink.isConnected.value) {
|
||||
return whooshLink.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue);
|
||||
} else if (keyPair.inGameAction != null && zwiftEmulator.isConnected.value) {
|
||||
return zwiftEmulator.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue);
|
||||
} else if (keyPair.isSpecialKey) {
|
||||
await accessibilityHandler.controlMedia(switch (keyPair.physicalKey) {
|
||||
PhysicalKeyboardKey.mediaTrackNext => MediaAction.next,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:keypress_simulator/keypress_simulator.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/actions/base_actions.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
@@ -25,6 +26,8 @@ class DesktopActions extends BaseActions {
|
||||
// Handle regular key press mode (existing behavior)
|
||||
if (keyPair.inGameAction != null && whooshLink.isConnected.value) {
|
||||
return whooshLink.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue);
|
||||
} else if (keyPair.inGameAction != null && zwiftEmulator.isConnected.value) {
|
||||
return zwiftEmulator.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue);
|
||||
} else if (keyPair.physicalKey != null) {
|
||||
if (isKeyDown && isKeyUp) {
|
||||
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:accessibility/accessibility.dart';
|
||||
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_click.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/actions/base_actions.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
@@ -29,6 +30,8 @@ class RemoteActions extends BaseActions {
|
||||
|
||||
if (keyPair.inGameAction != null && whooshLink.isConnected.value) {
|
||||
return whooshLink.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue);
|
||||
} else if (keyPair.inGameAction != null && zwiftEmulator.isConnected.value) {
|
||||
return zwiftEmulator.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue);
|
||||
} else if (!(actionHandler as RemoteActions).isConnected) {
|
||||
return 'Not connected to a ${settings.getLastTarget()?.name ?? 'remote'} device';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
|
||||
import 'package:swift_control/utils/requirements/multi.dart';
|
||||
|
||||
import '../buttons.dart';
|
||||
import '../keymap.dart';
|
||||
@@ -10,6 +13,8 @@ class Biketerra extends SupportedApp {
|
||||
: super(
|
||||
name: 'Biketerra',
|
||||
packageName: "biketerra",
|
||||
compatibleTargets: Target.values,
|
||||
supportsZwiftEmulation: !(Platform.isIOS || Platform.isMacOS),
|
||||
keymap: Keymap(
|
||||
keyPairs: [
|
||||
KeyPair(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
|
||||
import 'package:swift_control/utils/requirements/multi.dart';
|
||||
|
||||
import '../buttons.dart';
|
||||
import '../keymap.dart';
|
||||
@@ -11,7 +14,15 @@ class CustomApp extends SupportedApp {
|
||||
CustomApp({this.profileName = 'Other'})
|
||||
: super(
|
||||
name: profileName,
|
||||
compatibleTargets: [
|
||||
if (!Platform.isIOS) Target.thisDevice,
|
||||
Target.macOS,
|
||||
Target.windows,
|
||||
Target.iOS,
|
||||
Target.android,
|
||||
],
|
||||
packageName: "custom_$profileName",
|
||||
supportsZwiftEmulation: !(Platform.isIOS || Platform.isMacOS),
|
||||
keymap: Keymap(keyPairs: []),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
|
||||
import 'package:swift_control/utils/requirements/multi.dart';
|
||||
|
||||
import '../buttons.dart';
|
||||
import '../keymap.dart';
|
||||
@@ -10,6 +11,8 @@ class MyWhoosh extends SupportedApp {
|
||||
: super(
|
||||
name: 'MyWhoosh',
|
||||
packageName: "com.mywhoosh.whooshgame",
|
||||
compatibleTargets: Target.values,
|
||||
supportsZwiftEmulation: false,
|
||||
keymap: Keymap(
|
||||
keyPairs: [
|
||||
KeyPair(
|
||||
@@ -17,12 +20,14 @@ class MyWhoosh extends SupportedApp {
|
||||
physicalKey: PhysicalKeyboardKey.keyI,
|
||||
logicalKey: LogicalKeyboardKey.keyI,
|
||||
touchPosition: Offset(80, 94),
|
||||
inGameAction: InGameAction.shiftDown,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(),
|
||||
physicalKey: PhysicalKeyboardKey.keyK,
|
||||
logicalKey: LogicalKeyboardKey.keyK,
|
||||
touchPosition: Offset(97, 94),
|
||||
inGameAction: InGameAction.shiftUp,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ControllerButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(),
|
||||
@@ -30,6 +35,7 @@ class MyWhoosh extends SupportedApp {
|
||||
logicalKey: LogicalKeyboardKey.arrowRight,
|
||||
touchPosition: Offset(60, 80),
|
||||
isLongPress: true,
|
||||
inGameAction: InGameAction.navigateRight,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ControllerButton.values.filter((e) => e.action == InGameAction.navigateLeft).toList(),
|
||||
@@ -37,11 +43,13 @@ class MyWhoosh extends SupportedApp {
|
||||
logicalKey: LogicalKeyboardKey.arrowLeft,
|
||||
touchPosition: Offset(32, 80),
|
||||
isLongPress: true,
|
||||
inGameAction: InGameAction.navigateLeft,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ControllerButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(),
|
||||
physicalKey: PhysicalKeyboardKey.keyH,
|
||||
logicalKey: LogicalKeyboardKey.keyH,
|
||||
inGameAction: InGameAction.toggleUi,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
32
lib/utils/keymap/apps/rouvy.dart
Normal file
32
lib/utils/keymap/apps/rouvy.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:dartx/dartx.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 '../keymap.dart';
|
||||
|
||||
class Rouvy extends SupportedApp {
|
||||
Rouvy()
|
||||
: super(
|
||||
name: 'Rouvy',
|
||||
packageName: "eu.virtualtraining.rouvy.android",
|
||||
compatibleTargets: Target.values,
|
||||
supportsZwiftEmulation: true,
|
||||
keymap: Keymap(
|
||||
keyPairs: [
|
||||
KeyPair(
|
||||
buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(),
|
||||
inGameAction: InGameAction.shiftDown,
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(),
|
||||
inGameAction: InGameAction.shiftUp,
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,36 @@
|
||||
import 'package:swift_control/utils/keymap/apps/biketerra.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/rouvy.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/training_peaks.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/zwift.dart';
|
||||
import 'package:swift_control/utils/requirements/multi.dart';
|
||||
|
||||
import '../keymap.dart';
|
||||
import 'custom_app.dart';
|
||||
import 'my_whoosh.dart';
|
||||
|
||||
abstract class SupportedApp {
|
||||
final List<Target> compatibleTargets;
|
||||
final String packageName;
|
||||
final String name;
|
||||
final Keymap keymap;
|
||||
final bool supportsZwiftEmulation;
|
||||
|
||||
const SupportedApp({required this.name, required this.packageName, required this.keymap});
|
||||
const SupportedApp({
|
||||
required this.name,
|
||||
required this.packageName,
|
||||
required this.keymap,
|
||||
required this.compatibleTargets,
|
||||
required this.supportsZwiftEmulation,
|
||||
});
|
||||
|
||||
static final List<SupportedApp> supportedApps = [MyWhoosh(), TrainingPeaks(), Biketerra(), CustomApp()];
|
||||
static final List<SupportedApp> supportedApps = [
|
||||
MyWhoosh(),
|
||||
Zwift(),
|
||||
TrainingPeaks(),
|
||||
Biketerra(),
|
||||
Rouvy(),
|
||||
CustomApp(),
|
||||
];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:swift_control/bluetooth/devices/elite/elite_square.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/constants.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 '../keymap.dart';
|
||||
|
||||
@@ -12,6 +13,8 @@ class TrainingPeaks extends SupportedApp {
|
||||
: super(
|
||||
name: 'TrainingPeaks Virtual / IndieVelo',
|
||||
packageName: "com.indieVelo.client",
|
||||
compatibleTargets: Target.values,
|
||||
supportsZwiftEmulation: false,
|
||||
keymap: Keymap(
|
||||
keyPairs: [
|
||||
// Explicit controller-button mappings with updated touch coordinates
|
||||
|
||||
113
lib/utils/keymap/apps/zwift.dart
Normal file
113
lib/utils/keymap/apps/zwift.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/constants.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 '../keymap.dart';
|
||||
|
||||
class Zwift extends SupportedApp {
|
||||
Zwift()
|
||||
: super(
|
||||
name: 'Zwift',
|
||||
packageName: "com.zwift.zwiftgame",
|
||||
supportsZwiftEmulation: !(Platform.isIOS || Platform.isMacOS),
|
||||
compatibleTargets: [
|
||||
if (!Platform.isIOS) Target.thisDevice,
|
||||
Target.macOS,
|
||||
Target.windows,
|
||||
Target.iOS,
|
||||
Target.android,
|
||||
],
|
||||
keymap: Keymap(
|
||||
keyPairs: [
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.navigationUp],
|
||||
physicalKey: PhysicalKeyboardKey.arrowUp,
|
||||
logicalKey: LogicalKeyboardKey.arrowUp,
|
||||
inGameAction: InGameAction.openActionBar,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.navigationDown],
|
||||
physicalKey: PhysicalKeyboardKey.arrowDown,
|
||||
logicalKey: LogicalKeyboardKey.arrowDown,
|
||||
inGameAction: InGameAction.uturn,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.navigationLeft],
|
||||
physicalKey: PhysicalKeyboardKey.arrowLeft,
|
||||
logicalKey: LogicalKeyboardKey.arrowLeft,
|
||||
inGameAction: InGameAction.steerLeft,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.navigationRight],
|
||||
physicalKey: PhysicalKeyboardKey.arrowRight,
|
||||
logicalKey: LogicalKeyboardKey.arrowRight,
|
||||
inGameAction: InGameAction.steerRight,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.shiftUpLeft],
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
inGameAction: InGameAction.shiftDown,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.shiftUpRight],
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
inGameAction: InGameAction.shiftUp,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.shiftDownLeft],
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
inGameAction: InGameAction.shiftDown,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.shiftDownRight],
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
inGameAction: InGameAction.shiftUp,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.paddleLeft],
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
inGameAction: InGameAction.shiftDown,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.paddleRight],
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
inGameAction: InGameAction.shiftUp,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.y],
|
||||
physicalKey: PhysicalKeyboardKey.space,
|
||||
logicalKey: LogicalKeyboardKey.space,
|
||||
inGameAction: InGameAction.usePowerUp,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.a],
|
||||
physicalKey: PhysicalKeyboardKey.enter,
|
||||
logicalKey: LogicalKeyboardKey.enter,
|
||||
inGameAction: InGameAction.select,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.b],
|
||||
physicalKey: PhysicalKeyboardKey.escape,
|
||||
logicalKey: LogicalKeyboardKey.escape,
|
||||
inGameAction: InGameAction.back,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.z],
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
inGameAction: InGameAction.rideOnBomb,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -8,16 +8,25 @@ import 'package:swift_control/bluetooth/devices/zwift/constants.dart';
|
||||
enum InGameAction {
|
||||
shiftUp('Shift Up'),
|
||||
shiftDown('Shift Down'),
|
||||
uturn('U-Turn'),
|
||||
steerLeft('Steer Left'),
|
||||
steerRight('Steer Right'),
|
||||
|
||||
// mywhoosh
|
||||
cameraAngle('Change Camera Angle', possibleValues: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
|
||||
emote('Emote', possibleValues: [1, 2, 3, 4, 5, 6]),
|
||||
toggleUi('Toggle UI'),
|
||||
navigateLeft('Navigate Left'),
|
||||
navigateRight('Navigate Right'),
|
||||
increaseResistance('Increase Resistance'),
|
||||
decreaseResistance('Decrease Resistance'),
|
||||
toggleUi('Toggle UI'),
|
||||
cameraAngle('Change Camera Angle', possibleValues: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
|
||||
emote('Emote', possibleValues: [1, 2, 3, 4, 5, 6]),
|
||||
uturn('U-Turn'),
|
||||
steerLeft('Steer Left'),
|
||||
steerRight('Steer Right');
|
||||
|
||||
// zwift
|
||||
openActionBar('Open Action Bar'),
|
||||
usePowerUp('Use Power-Up'),
|
||||
select('Select'),
|
||||
back('Back'),
|
||||
rideOnBomb('Ride On Bomb');
|
||||
|
||||
final String title;
|
||||
final List<int>? possibleValues;
|
||||
|
||||
@@ -49,7 +49,7 @@ class Keymap {
|
||||
_updateStream.add(null);
|
||||
|
||||
if (actionHandler.supportedApp is CustomApp) {
|
||||
settings.setSupportedApp(actionHandler.supportedApp!);
|
||||
settings.setKeyMap(actionHandler.supportedApp!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class KeymapManager {
|
||||
customApp.decodeKeymap(savedKeymap);
|
||||
}
|
||||
actionHandler.supportedApp = customApp;
|
||||
await settings.setSupportedApp(customApp);
|
||||
await settings.setKeyMap(customApp);
|
||||
}
|
||||
onDone();
|
||||
},
|
||||
@@ -244,7 +244,7 @@ class KeymapManager {
|
||||
customApp.decodeKeymap(savedKeymap);
|
||||
}
|
||||
actionHandler.supportedApp = customApp;
|
||||
await settings.setSupportedApp(customApp);
|
||||
await settings.setKeyMap(customApp);
|
||||
return newName;
|
||||
} else {
|
||||
final customApp = CustomApp(profileName: newName);
|
||||
@@ -266,7 +266,7 @@ class KeymapManager {
|
||||
});
|
||||
|
||||
actionHandler.supportedApp = customApp;
|
||||
await settings.setSupportedApp(customApp);
|
||||
await settings.setKeyMap(customApp);
|
||||
return newName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/custom_app.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/my_whoosh.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/zwift.dart';
|
||||
import 'package:swift_control/utils/requirements/platform.dart';
|
||||
import 'package:swift_control/utils/requirements/remote.dart';
|
||||
import 'package:swift_control/widgets/beta_pill.dart';
|
||||
@@ -86,49 +87,70 @@ typedef BoolFunction = bool Function();
|
||||
|
||||
enum Target {
|
||||
thisDevice(
|
||||
title: 'This device',
|
||||
description: 'Trainer app runs on this device',
|
||||
title: 'This Device',
|
||||
icon: Icons.devices,
|
||||
),
|
||||
iPad(
|
||||
title: 'iPad',
|
||||
description: 'Remotely control any trainer app on an iPad by acting as a Mouse, or directly via MyWhoosh Link',
|
||||
iOS(
|
||||
title: 'iPhone / iPad / Apple TV',
|
||||
icon: Icons.settings_remote_outlined,
|
||||
),
|
||||
android(
|
||||
title: 'Android Device',
|
||||
description: 'Remotely control any trainer app on another Android device, or directly via MyWhoosh Link',
|
||||
icon: Icons.settings_remote_outlined,
|
||||
isBeta: true,
|
||||
),
|
||||
macOS(
|
||||
title: 'Mac',
|
||||
description: 'Remotely control any trainer app on another Mac, or directly via MyWhoosh Link',
|
||||
icon: Icons.settings_remote_outlined,
|
||||
isBeta: true,
|
||||
),
|
||||
windows(
|
||||
title: 'Windows PC',
|
||||
description: 'Remotely control any trainer app on another Windows PC, or directly via MyWhoosh Link',
|
||||
icon: Icons.settings_remote_outlined,
|
||||
isBeta: true,
|
||||
);
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final IconData icon;
|
||||
final bool isBeta;
|
||||
|
||||
const Target({required this.title, required this.description, required this.icon, this.isBeta = false});
|
||||
const Target({required this.title, required this.icon});
|
||||
|
||||
bool get isCompatible {
|
||||
return settings.getTrainerApp()?.compatibleTargets.contains(this) == true;
|
||||
}
|
||||
|
||||
bool get isBeta {
|
||||
final supportedApp = settings.getTrainerApp();
|
||||
|
||||
if (supportedApp is Zwift && !(Platform.isIOS || Platform.isMacOS)) {
|
||||
// everything is supported, this device is not compatible anyway
|
||||
return false;
|
||||
}
|
||||
|
||||
return switch (this) {
|
||||
Target.thisDevice => !Platform.isIOS,
|
||||
Target.thisDevice => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
String getDescription(SupportedApp? app) {
|
||||
return switch (this) {
|
||||
Target.thisDevice when !isCompatible =>
|
||||
'Due to platform restrictions only controlling ${app?.name ?? 'the Trainer app'} on other devices is supported.',
|
||||
Target.thisDevice => 'Run ${app?.name ?? 'the Trainer app'} on this device.',
|
||||
Target.iOS =>
|
||||
'Run ${app?.name ?? 'the Trainer app'} on your Apple device and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Link method' : ''}.',
|
||||
Target.android =>
|
||||
'Run ${app?.name ?? 'the Trainer app'} on your Android device and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Link method' : ''}.',
|
||||
Target.macOS =>
|
||||
'Run ${app?.name ?? 'the Trainer app'} on your Mac and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Link method' : ''}.',
|
||||
Target.windows =>
|
||||
'Run ${app?.name ?? 'the Trainer app'} on your Windows PC and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Link method' : ''}.',
|
||||
};
|
||||
}
|
||||
|
||||
String? get warning {
|
||||
if (settings.getTrainerApp()?.supportsZwiftEmulation == true) {
|
||||
// no warnings for zwift emulation
|
||||
return null;
|
||||
}
|
||||
return switch (this) {
|
||||
Target.android when Platform.isAndroid =>
|
||||
"Select 'This device' unless you want to control another Android device. Are you sure?",
|
||||
@@ -170,86 +192,123 @@ class TargetRequirement extends PlatformRequirement {
|
||||
|
||||
@override
|
||||
Widget? build(BuildContext context, VoidCallback onUpdate) {
|
||||
return Column(
|
||||
spacing: 8,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Select Trainer App', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
DropdownMenu<SupportedApp>(
|
||||
dropdownMenuEntries: SupportedApp.supportedApps.map((app) {
|
||||
return DropdownMenuEntry(
|
||||
value: app,
|
||||
label: app.name,
|
||||
);
|
||||
}).toList(),
|
||||
hintText: 'Select Trainer app',
|
||||
initialSelection: settings.getTrainerApp(),
|
||||
onSelected: (selectedApp) async {
|
||||
if (settings.getTrainerApp() is MyWhoosh && selectedApp is! MyWhoosh && whooshLink.isStarted.value) {
|
||||
whooshLink.stopServer();
|
||||
}
|
||||
settings.setTrainerApp(selectedApp!);
|
||||
if (actionHandler.supportedApp == null ||
|
||||
(actionHandler.supportedApp is! CustomApp && selectedApp is! CustomApp)) {
|
||||
actionHandler.init(selectedApp);
|
||||
settings.setSupportedApp(selectedApp);
|
||||
}
|
||||
},
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Select Target where ${settings.getTrainerApp()?.name ?? 'the Trainer app'} runs on',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
DropdownMenu<Target>(
|
||||
dropdownMenuEntries: Target.values.map((target) {
|
||||
return DropdownMenuEntry(
|
||||
value: target,
|
||||
label: target.title,
|
||||
enabled: target.isCompatible,
|
||||
trailingIcon: Icon(target.icon),
|
||||
labelWidget: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(target.title, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
if (target.isBeta || (!Platform.isIOS && target == Target.iPad)) BetaPill(),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
target.isCompatible
|
||||
? target.description
|
||||
: 'Due to iOS restrictions only controlling trainer apps on other devices is supported.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
hintText: name,
|
||||
initialSelection: settings.getLastTarget(),
|
||||
onSelected: (target) async {
|
||||
if (target != null) {
|
||||
await settings.setLastTarget(target);
|
||||
initializeActions(target.connectionType);
|
||||
if (target.warning != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(target.warning!),
|
||||
duration: Duration(seconds: 10),
|
||||
),
|
||||
);
|
||||
return StatefulBuilder(
|
||||
builder: (c, setState) => Column(
|
||||
spacing: 8,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Select Trainer App', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
DropdownMenu<SupportedApp>(
|
||||
dropdownMenuEntries: SupportedApp.supportedApps.map((app) {
|
||||
return DropdownMenuEntry(
|
||||
value: app,
|
||||
label: app.name,
|
||||
labelWidget: app is Zwift && !(Platform.isWindows || Platform.isAndroid)
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(app.name),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'When running SwiftControl on Apple devices you are limited to on-screen controls (so no virtual shifting) only due to platform restrictions :(',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
Icon(Icons.warning_amber),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}).toList(),
|
||||
hintText: 'Select Trainer app',
|
||||
initialSelection: settings.getTrainerApp(),
|
||||
onSelected: (selectedApp) async {
|
||||
if (settings.getTrainerApp() is MyWhoosh && selectedApp is! MyWhoosh && whooshLink.isStarted.value) {
|
||||
whooshLink.stopServer();
|
||||
}
|
||||
onUpdate();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
settings.setTrainerApp(selectedApp!);
|
||||
if (actionHandler.supportedApp == null ||
|
||||
(actionHandler.supportedApp is! CustomApp && selectedApp is! CustomApp)) {
|
||||
actionHandler.init(selectedApp);
|
||||
settings.setKeyMap(selectedApp);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Select Target where ${settings.getTrainerApp()?.name ?? 'the Trainer app'} runs on',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
DropdownMenu<Target>(
|
||||
dropdownMenuEntries: Target.values.map((target) {
|
||||
return DropdownMenuEntry(
|
||||
value: target,
|
||||
label: target.title,
|
||||
enabled: target.isCompatible,
|
||||
trailingIcon: Icon(target.icon),
|
||||
labelWidget: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(target.title, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
if (target.isBeta) BetaPill(),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
target.getDescription(settings.getTrainerApp()),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
if (target == Target.thisDevice)
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 12),
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).dividerColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
hintText: 'Select Target device',
|
||||
initialSelection: settings.getLastTarget(),
|
||||
onSelected: (target) async {
|
||||
if (target != null) {
|
||||
await settings.setLastTarget(target);
|
||||
initializeActions(target.connectionType);
|
||||
if (target.warning != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(target.warning!),
|
||||
duration: Duration(seconds: 10),
|
||||
),
|
||||
);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: settings.getTrainerApp() != null && settings.getLastTarget() != null
|
||||
? () {
|
||||
onUpdate();
|
||||
}
|
||||
: null,
|
||||
child: Text('Continue'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ class RemoteRequirement extends PlatformRequirement {
|
||||
? null
|
||||
: Text(
|
||||
switch (settings.getLastTarget()) {
|
||||
Target.iPad =>
|
||||
Target.iOS =>
|
||||
'On your iPad go to Settings > Accessibility > Touch > AssistiveTouch > Pointer Devices > Devices and pair your device. Make sure AssistiveTouch is enabled.',
|
||||
_ =>
|
||||
'On your ${settings.getLastTarget()?.title} go into Bluetooth settings and look for SwiftControl or your machines name. Pairing is required to use the remote feature.',
|
||||
'On your ${settings.getLastTarget()?.title} go into Bluetooth settings and look for SwiftControl or your machines name. Pairing is required if you want to use the remote control feature.',
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -94,13 +94,13 @@ class RemoteRequirement extends PlatformRequirement {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (kDebugMode) {
|
||||
if (kDebugMode && false) {
|
||||
print('Continuing');
|
||||
return;
|
||||
}
|
||||
|
||||
while (peripheralManager.state != BluetoothLowEnergyState.poweredOn) {
|
||||
print('Waiting for peripheral manager to be powered on...');
|
||||
print('Waiting for peripheral manager to be powered on... ${peripheralManager.state}');
|
||||
if (settings.getLastTarget() == Target.thisDevice) {
|
||||
return;
|
||||
}
|
||||
@@ -283,7 +283,7 @@ class RemoteRequirement extends PlatformRequirement {
|
||||
|
||||
@override
|
||||
Future<void> getStatus() async {
|
||||
status = (actionHandler as RemoteActions).isConnected || screenshotMode;
|
||||
status = (actionHandler is RemoteActions && (actionHandler as RemoteActions).isConnected) || screenshotMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,9 @@ class _PairWidgetState extends State<_PairWidget> {
|
||||
super.initState();
|
||||
// after first frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
toggle();
|
||||
if (actionHandler.supportedApp?.supportsZwiftEmulation == false) {
|
||||
toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -319,7 +321,9 @@ class _PairWidgetState extends State<_PairWidget> {
|
||||
onPressed: () async {
|
||||
await toggle();
|
||||
},
|
||||
child: Text(_isAdvertising ? 'Stop Pairing' : 'Start Pairing'),
|
||||
child: Text(
|
||||
_isAdvertising ? 'Stop Pairing' : 'Start Pairing',
|
||||
),
|
||||
),
|
||||
if (_isAdvertising || _isLoading) SizedBox(height: 20, width: 20, child: SmallProgressIndicator()),
|
||||
],
|
||||
@@ -349,6 +353,24 @@ class _PairWidgetState extends State<_PairWidget> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (actionHandler.supportedApp?.supportsZwiftEmulation == true) ...[
|
||||
Text(
|
||||
'You can also skip pairing and directly connect to ${settings.getTrainerApp()?.name} by enabling the Zwift Controller.',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (c) => DevicePage(),
|
||||
settings: RouteSettings(name: '/device'),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text('Connect to ${settings.getTrainerApp()?.name} directly as controller'),
|
||||
),
|
||||
],
|
||||
if (_isAdvertising) ...[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
|
||||
83
lib/utils/requirements/zwift.dart
Normal file
83
lib/utils/requirements/zwift.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart' hide ConnectionState;
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/rouvy.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/zwift.dart';
|
||||
import 'package:swift_control/utils/requirements/platform.dart';
|
||||
import 'package:swift_control/widgets/small_progress_indicator.dart';
|
||||
|
||||
class ZwiftRequirement extends PlatformRequirement {
|
||||
ZwiftRequirement()
|
||||
: super(
|
||||
'Pair SwiftControl with Zwift',
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> call(BuildContext context, VoidCallback onUpdate) async {}
|
||||
|
||||
@override
|
||||
Widget? buildDescription() {
|
||||
return settings.getLastTarget() == null
|
||||
? null
|
||||
: Text(
|
||||
'In Zwift on your ${settings.getLastTarget()?.title} go into the Pairing settings and select SwiftControl from the list of available controllers.',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? build(BuildContext context, VoidCallback onUpdate) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: zwiftEmulator.isConnected,
|
||||
builder: (context, isConnected, _) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: settings.getZwiftEmulatorEnabled(),
|
||||
onChanged: (value) {
|
||||
settings.setZwiftEmulatorEnabled(value);
|
||||
if (!value) {
|
||||
zwiftEmulator.stopAdvertising();
|
||||
} else if (value) {
|
||||
zwiftEmulator.startAdvertising(onUpdate);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
title: Text('Enable Zwift Controller'),
|
||||
subtitle: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (!settings.getZwiftEmulatorEnabled())
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Disabled. ${settings.getTrainerApp() is Zwift
|
||||
? 'Virtual shifting and on screen navigation will not work.'
|
||||
: settings.getTrainerApp() is Rouvy
|
||||
? 'Virtual shifting will not work.'
|
||||
: ''}',
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
Expanded(
|
||||
child: Text(
|
||||
isConnected
|
||||
? "Connected"
|
||||
: "Waiting for connection. Choose SwiftControl in ${settings.getTrainerApp()?.name}'s controller pairing menu.",
|
||||
),
|
||||
),
|
||||
if (!isConnected) SmallProgressIndicator(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> getStatus() async {
|
||||
status = zwiftEmulator.isConnected.value || screenshotMode;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ class Settings {
|
||||
}
|
||||
|
||||
try {
|
||||
final app = getSupportedApp();
|
||||
final app = getKeyMap();
|
||||
actionHandler.init(app);
|
||||
} catch (e) {
|
||||
// couldn't decode, reset
|
||||
@@ -49,14 +49,14 @@ class Settings {
|
||||
return SupportedApp.supportedApps.firstOrNullWhere((e) => e.name == appName);
|
||||
}
|
||||
|
||||
Future<void> setSupportedApp(SupportedApp app) async {
|
||||
Future<void> setKeyMap(SupportedApp app) async {
|
||||
if (app is CustomApp) {
|
||||
await prefs.setStringList('customapp_${app.profileName}', app.encodeKeymap());
|
||||
}
|
||||
await prefs.setString('app', app.name);
|
||||
}
|
||||
|
||||
SupportedApp? getSupportedApp() {
|
||||
SupportedApp? getKeyMap() {
|
||||
final appName = prefs.getString('app');
|
||||
if (appName == null) {
|
||||
return null;
|
||||
@@ -165,4 +165,12 @@ class Settings {
|
||||
Future<void> setMyWhooshLinkEnabled(bool enabled) async {
|
||||
await prefs.setBool('mywhoosh_link_enabled', enabled);
|
||||
}
|
||||
|
||||
bool getZwiftEmulatorEnabled() {
|
||||
return prefs.getBool('zwift_emulator_enabled') ?? true;
|
||||
}
|
||||
|
||||
Future<void> setZwiftEmulatorEnabled(bool enabled) async {
|
||||
await prefs.setBool('zwift_emulator_enabled', enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/pages/device.dart';
|
||||
import 'package:swift_control/utils/actions/base_actions.dart';
|
||||
@@ -189,6 +190,52 @@ class _ButtonEditor extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (zwiftEmulator.isConnected.value)
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
child: PopupMenuButton(
|
||||
itemBuilder: (_) => ZwiftEmulator.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('Zwift Action')),
|
||||
Icon(Icons.arrow_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (actionHandler.supportedModes.contains(SupportedMode.keyboard))
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
@@ -303,6 +350,19 @@ class _ButtonEditor extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
onTap: () {
|
||||
keyPair.isLongPress = false;
|
||||
keyPair.physicalKey = null;
|
||||
keyPair.logicalKey = null;
|
||||
keyPair.touchPosition = Offset.zero;
|
||||
keyPair.inGameAction = null;
|
||||
keyPair.inGameActionValue = null;
|
||||
onUpdate();
|
||||
},
|
||||
child: const Text('Unassign action'),
|
||||
),
|
||||
];
|
||||
|
||||
return Container(
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:swift_control/widgets/small_progress_indicator.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
PackageInfo? _packageInfoValue;
|
||||
PackageInfo? packageInfoValue;
|
||||
bool? isFromPlayStore;
|
||||
|
||||
class AppTitle extends StatefulWidget {
|
||||
@@ -39,10 +39,10 @@ class _AppTitleState extends State<AppTitle> {
|
||||
});
|
||||
}
|
||||
|
||||
if (_packageInfoValue == null) {
|
||||
if (packageInfoValue == null) {
|
||||
PackageInfo.fromPlatform().then((value) {
|
||||
setState(() {
|
||||
_packageInfoValue = value;
|
||||
packageInfoValue = value;
|
||||
});
|
||||
_checkForUpdate();
|
||||
});
|
||||
@@ -143,9 +143,9 @@ class _AppTitleState extends State<AppTitle> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('SwiftControl', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
if (_packageInfoValue != null)
|
||||
if (packageInfoValue != null)
|
||||
Text(
|
||||
'v${_packageInfoValue!.version}${_shorebirdPatch != null ? '+${_shorebirdPatch!.number}' : ''}${kIsWeb || (Platform.isAndroid && isFromPlayStore == false) ? ' (sideloaded)' : ''}',
|
||||
'v${packageInfoValue!.version}${_shorebirdPatch != null ? '+${_shorebirdPatch!.number}' : ''}${kIsWeb || (Platform.isAndroid && isFromPlayStore == false) ? ' (sideloaded)' : ''}',
|
||||
style: TextStyle(fontFamily: "monospace", fontFamilyFallback: <String>["Courier"], fontSize: 12),
|
||||
)
|
||||
else
|
||||
@@ -177,7 +177,7 @@ class _AppTitleState extends State<AppTitle> {
|
||||
|
||||
void _compareVersion(String versionString) {
|
||||
final parsed = Version.parse(versionString);
|
||||
final current = Version.parse(_packageInfoValue!.version);
|
||||
final current = Version.parse(packageInfoValue!.version);
|
||||
if (parsed > current && mounted && !kDebugMode) {
|
||||
if (Platform.isAndroid) {
|
||||
_showUpdateSnackbar(parsed, 'https://play.google.com/store/apps/details?id=org.jonasbark.swiftcontrol');
|
||||
|
||||
@@ -28,7 +28,7 @@ void main() {
|
||||
|
||||
test('Should save and retrieve custom profile', () async {
|
||||
final customApp = CustomApp(profileName: 'Race');
|
||||
await settings.setSupportedApp(customApp);
|
||||
await settings.setKeyMap(customApp);
|
||||
|
||||
final profiles = settings.getCustomAppProfiles();
|
||||
expect(profiles.contains('Race'), true);
|
||||
@@ -39,9 +39,9 @@ void main() {
|
||||
final race = CustomApp(profileName: 'Race');
|
||||
final event = CustomApp(profileName: 'Event');
|
||||
|
||||
await settings.setSupportedApp(workout);
|
||||
await settings.setSupportedApp(race);
|
||||
await settings.setSupportedApp(event);
|
||||
await settings.setKeyMap(workout);
|
||||
await settings.setKeyMap(race);
|
||||
await settings.setKeyMap(event);
|
||||
|
||||
final profiles = settings.getCustomAppProfiles();
|
||||
expect(profiles.contains('Workout'), true);
|
||||
@@ -52,7 +52,7 @@ void main() {
|
||||
|
||||
test('Should duplicate custom profile', () async {
|
||||
final original = CustomApp(profileName: 'Original');
|
||||
await settings.setSupportedApp(original);
|
||||
await settings.setKeyMap(original);
|
||||
|
||||
await settings.duplicateCustomAppProfile('Original', 'Copy');
|
||||
|
||||
@@ -64,7 +64,7 @@ void main() {
|
||||
|
||||
test('Should delete custom profile', () async {
|
||||
final customApp = CustomApp(profileName: 'ToDelete');
|
||||
await settings.setSupportedApp(customApp);
|
||||
await settings.setKeyMap(customApp);
|
||||
|
||||
var profiles = settings.getCustomAppProfiles();
|
||||
expect(profiles.contains('ToDelete'), true);
|
||||
@@ -108,7 +108,7 @@ void main() {
|
||||
|
||||
test('Should export custom profile as JSON', () async {
|
||||
final customApp = CustomApp(profileName: 'TestProfile');
|
||||
await settings.setSupportedApp(customApp);
|
||||
await settings.setKeyMap(customApp);
|
||||
|
||||
final jsonData = settings.exportCustomAppProfile('TestProfile');
|
||||
expect(jsonData, isNotNull);
|
||||
@@ -120,7 +120,7 @@ void main() {
|
||||
test('Should import custom profile from JSON', () async {
|
||||
// First export a profile
|
||||
final customApp = CustomApp(profileName: 'ExportTest');
|
||||
await settings.setSupportedApp(customApp);
|
||||
await settings.setKeyMap(customApp);
|
||||
final jsonData = settings.exportCustomAppProfile('ExportTest');
|
||||
|
||||
// Import with a new name
|
||||
|
||||
Reference in New Issue
Block a user