Compare commits

..

14 Commits

Author SHA1 Message Date
Jonas Bark
657c6056c4 potential fix for Bluetooth device detection 2025-03-30 16:08:03 +02:00
Jonas Bark
84daba8902 potential fix for Bluetooth device detection 2025-03-30 16:03:01 +02:00
Jonas Bark
3e37f8a269 update readme 2025-03-30 15:17:52 +02:00
Jonas Bark
28d178c4be update changelog + version 2025-03-30 15:04:00 +02:00
Jonas Bark
f560cd5930 don't call getSystemDevices on Web 2025-03-30 15:02:04 +02:00
Jonas Bark
dbf24c6cd3 fix touch placement coordinate systems (closes #4) 2025-03-30 15:00:24 +02:00
Jonas Bark
0a4989ca47 fix touch placement coordinate systems (closes #4) 2025-03-30 14:59:42 +02:00
Jonas Bark
507dbf5d0f cleanup code 2025-03-30 14:40:08 +02:00
Jonas Bark
536f36f4e7 update Zwift Ride decoding based on Feedback from @JayyajGH
fixes #3
2025-03-30 14:30:38 +02:00
Jonas Bark
c523ba2287 Android: allow user to adjust touch placements 2025-03-30 14:26:51 +02:00
Jonas Bark
a3f1cbb3b1 reconnect to existing BLE connection, also fallback to name only if manufacturerData isn't available 2025-03-30 14:09:41 +02:00
Jonas Bark
561bb2f0f4 allow adding custom keymap and store it 2025-03-30 13:36:40 +02:00
Jonas Bark
dbbd1b5f2c another potential keyboard fix, Zwift Play adjustment 2025-03-29 21:15:43 +01:00
Jonas Bark
7f6ec2f732 revert keyboard change 2025-03-29 19:48:51 +01:00
41 changed files with 939 additions and 255 deletions

View File

@@ -1,3 +1,15 @@
### 1.1.0 (2025-03-30)
- potential fix for Bluetooth device detection
### 1.1.0 (2025-03-30)
- Windows & macOS: allow setting custom keymap and store the setting
- Android: allow customizing the touch area, so it can work with any device without guesswork where the buttons are (#4)
- Zwift Ride: update Zwift Ride decoding based on Feedback from @JayyajGH (#3)
### 1.0.6 (2025-03-29)
- Another potential keyboard fix for Windows
- Zwift Play: actually also use the dedicated shift buttons
### 1.0.5 (2025-03-29)
- Zwift Ride: remap the shifter buttons to the correct values

View File

@@ -18,7 +18,9 @@ Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
## Supported Apps
- MyWhoosh
- indieVelo / Training Peaks
- let me know if you know others that can benefit
- any other:
- Android: you can customize the gear shifting touch points in the app
- Desktop: you can customize the keyboard shortcuts in the app
## Supported Devices
- Zwift Click
@@ -43,6 +45,4 @@ Please consider donating to support the development of this app.
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/boni)
## TODO
- test Zwift Ride
- confirm that Windows release works
- implement more actions for Play + Ride

View File

@@ -45,9 +45,10 @@ class Constants {
enum DeviceType {
click,
ride,
playLeft,
playRight;
playRight,
rideRight,
rideLeft;
@override
String toString() {
@@ -63,6 +64,10 @@ enum DeviceType {
return DeviceType.playLeft;
case Constants.RC1_RIGHT_SIDE:
return DeviceType.playRight;
case Constants.RIDE_RIGHT_SIDE:
return DeviceType.rideRight;
case Constants.RIDE_LEFT_SIDE:
return DeviceType.rideLeft;
}
return null;
}

View File

@@ -4,11 +4,11 @@ import 'dart:io';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/devices/base_device.dart';
import 'package:swift_control/utils/requirements/android.dart';
import 'package:universal_ble/universal_ble.dart';
import 'ble.dart';
import '../bluetooth/ble.dart';
import 'devices/base_device.dart';
import 'messages/notification.dart';
class Connection {
@@ -23,7 +23,7 @@ class Connection {
final StreamController<BaseDevice> _connectionStreams = StreamController<BaseDevice>.broadcast();
Stream<BaseDevice> get connectionStream => _connectionStreams.stream;
var _lastScanResult = <BleDevice>[];
final _lastScanResult = <BleDevice>[];
final ValueNotifier<bool> hasDevices = ValueNotifier(false);
final ValueNotifier<bool> isScanning = ValueNotifier(false);
@@ -53,6 +53,17 @@ class Connection {
Future<void> performScanning() async {
isScanning.value = true;
if (!kIsWeb) {
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();
if (baseDevices.isNotEmpty) {
_addDevices(baseDevices);
}
});
}
await UniversalBle.startScan(
scanFilter: ScanFilter(withServices: [BleUuid.ZWIFT_CUSTOM_SERVICE_UUID, BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID]),
platformConfig: PlatformConfig(web: WebOptions(optionalServices: [BleUuid.ZWIFT_CUSTOM_SERVICE_UUID])),

View File

@@ -1,26 +1,90 @@
import 'dart:async';
import 'dart:io';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/devices/base_device.dart';
import 'package:swift_control/utils/messages/notification.dart';
import 'package:swift_control/bluetooth/ble.dart';
import 'package:swift_control/bluetooth/devices/zwift_click.dart';
import 'package:swift_control/bluetooth/devices/zwift_play.dart';
import 'package:swift_control/bluetooth/devices/zwift_ride.dart';
import 'package:swift_control/utils/crypto/local_key_provider.dart';
import 'package:swift_control/utils/crypto/zap_crypto.dart';
import 'package:universal_ble/universal_ble.dart';
import '../ble.dart';
import '../crypto/encryption_utils.dart';
import '../messages/click_notification.dart';
import '../../utils/crypto/encryption_utils.dart';
import '../messages/notification.dart';
class ZwiftClick extends BaseDevice {
ZwiftClick(super.scanResult);
abstract class BaseDevice {
final BleDevice scanResult;
BaseDevice(this.scanResult);
final zapEncryption = ZapCrypto(LocalKeyProvider());
bool isConnected = false;
bool supportsEncryption = true;
List<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_CLICK;
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;
// 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),
DeviceType.playRight => ZwiftPlay(scanResult),
DeviceType.playLeft => ZwiftPlay(scanResult),
DeviceType.rideRight => ZwiftRide(scanResult),
DeviceType.rideLeft => ZwiftRide(scanResult),
_ => null,
};
}
}
@override
Future<void> handleServices(List<BleService> services) async {
bool operator ==(Object other) =>
identical(this, other) ||
other is BaseDevice && runtimeType == other.runtimeType && scanResult == other.scanResult;
@override
int get hashCode => scanResult.hashCode;
@override
String toString() {
return runtimeType.toString();
}
BleDevice get device => scanResult;
final StreamController<BaseNotification> actionStreamInternal = StreamController<BaseNotification>.broadcast();
Stream<BaseNotification> get actionStream => actionStreamInternal.stream;
Future<void> connect() async {
await UniversalBle.connect(device.deviceId, connectionTimeout: const Duration(seconds: 3));
if (!kIsWeb && Platform.isAndroid) {
//await UniversalBle.requestMtu(device.deviceId, 256);
}
final services = await UniversalBle.discoverServices(device.deviceId);
await _handleServices(services);
}
Future<void> _handleServices(List<BleService> services) async {
final customService = services.firstOrNullWhere((service) => service.uuid == customServiceId);
if (customService == null) {
throw Exception('Custom service not found');
throw Exception('Custom service $customServiceId not found for device $this ${device.name ?? device.rawName}');
}
final asyncCharacteristic = customService.characteristics.firstOrNullWhere(
@@ -77,7 +141,6 @@ class ZwiftClick extends BaseDevice {
}
}
@override
void processCharacteristic(String characteristic, Uint8List bytes) {
if (kDebugMode && false) {
print('Received $characteristic: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}');
@@ -107,7 +170,13 @@ class ZwiftClick extends BaseDevice {
}
}
ClickNotification? _lastClickNotification;
void _processDevicePublicKeyResponse(Uint8List bytes) {
final devicePublicKeyBytes = bytes.sublist(Constants.RIDE_ON.length + Constants.RESPONSE_START_CLICK.length);
zapEncryption.initialise(devicePublicKeyBytes);
if (kDebugMode) {
print("Device Public Key - ${devicePublicKeyBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}");
}
}
void _processData(Uint8List bytes) {
int type;
@@ -140,25 +209,5 @@ class ZwiftClick extends BaseDevice {
}
}
void _processDevicePublicKeyResponse(Uint8List bytes) {
final devicePublicKeyBytes = bytes.sublist(Constants.RIDE_ON.length + Constants.RESPONSE_START_CLICK.length);
zapEncryption.initialise(devicePublicKeyBytes);
if (kDebugMode) {
print("Device Public Key - ${devicePublicKeyBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}");
}
}
void processClickNotification(Uint8List message) {
final ClickNotification clickNotification = ClickNotification(message);
if (_lastClickNotification == null || _lastClickNotification != clickNotification) {
_lastClickNotification = clickNotification;
actionStreamInternal.add(clickNotification);
if (clickNotification.buttonUp) {
actionHandler.increaseGear();
} else if (clickNotification.buttonDown) {
actionHandler.decreaseGear();
}
}
}
void processClickNotification(Uint8List message);
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/foundation.dart';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/main.dart';
import '../messages/click_notification.dart';
class ZwiftClick extends BaseDevice {
ZwiftClick(super.scanResult);
ClickNotification? _lastClickNotification;
@override
void processClickNotification(Uint8List message) {
final ClickNotification clickNotification = ClickNotification(message);
if (_lastClickNotification == null || _lastClickNotification != clickNotification) {
_lastClickNotification = clickNotification;
actionStreamInternal.add(clickNotification);
if (clickNotification.buttonUp) {
actionHandler.increaseGear();
} else if (clickNotification.buttonDown) {
actionHandler.decreaseGear();
}
}
}
}

View File

@@ -1,12 +1,12 @@
import 'package:accessibility/accessibility.dart';
import 'package:flutter/foundation.dart';
import 'package:swift_control/utils/devices/zwift_click.dart';
import 'package:swift_control/utils/messages/play_notification.dart';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/bluetooth/messages/play_notification.dart';
import '../../main.dart';
import '../ble.dart';
class ZwiftPlay extends ZwiftClick {
class ZwiftPlay extends BaseDevice {
ZwiftPlay(super.scanResult);
PlayNotification? _lastControllerNotification;
@@ -21,9 +21,11 @@ class ZwiftPlay extends ZwiftClick {
_lastControllerNotification = clickNotification;
actionStreamInternal.add(clickNotification);
if (clickNotification.rightPad && clickNotification.analogLR.abs() == 100) {
if ((clickNotification.rightPad && clickNotification.buttonShift) ||
(clickNotification.rightPad && clickNotification.analogLR.abs() == 100)) {
actionHandler.increaseGear();
} else if (!clickNotification.rightPad && clickNotification.analogLR.abs() == 100) {
} else if ((!clickNotification.rightPad && clickNotification.buttonShift) ||
(!clickNotification.rightPad && clickNotification.analogLR.abs() == 100)) {
actionHandler.decreaseGear();
}
if (clickNotification.rightPad) {

View File

@@ -1,12 +1,12 @@
import 'dart:typed_data';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/bluetooth/messages/ride_notification.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/devices/zwift_click.dart';
import 'package:swift_control/utils/messages/ride_notification.dart';
import '../ble.dart';
class ZwiftRide extends ZwiftClick {
class ZwiftRide extends BaseDevice {
ZwiftRide(super.scanResult);
@override

View File

@@ -1,8 +1,7 @@
import 'dart:typed_data';
import 'package:swift_control/utils/messages/notification.dart';
import '../../protocol/zwift.pb.dart';
import '../protocol/zwift.pb.dart';
import 'notification.dart';
class ClickNotification extends BaseNotification {
static const int BTN_PRESSED = 0;

View File

@@ -1,8 +1,7 @@
import 'dart:typed_data';
import 'package:swift_control/utils/messages/notification.dart';
import '../../protocol/zwift.pb.dart';
import 'package:swift_control/bluetooth/messages/notification.dart';
import 'package:swift_control/bluetooth/protocol/zwift.pb.dart';
class PlayNotification extends BaseNotification {
static const int BTN_PRESSED = 0;

View File

@@ -1,27 +1,27 @@
import 'dart:typed_data';
import 'package:swift_control/utils/messages/notification.dart';
import '../../protocol/zwift.pb.dart';
import 'package:swift_control/bluetooth/messages/notification.dart';
import 'package:swift_control/bluetooth/protocol/zwift.pb.dart';
enum _RideButtonMask {
LEFT_BTN(0x00001),
UP_BTN(0x00002),
RIGHT_BTN(0x00004),
DOWN_BTN(0x00008),
A_BTN(0x00010),
B_BTN(0x00020),
Y_BTN(0x00040),
Z_BTN(0x00080),
Z_BTN(0x00100),
SHFT_UP_L_BTN(0x00200),
SHFT_DN_L_BTN(0x00400),
POWERUP_L_BTN(0x00800),
SHFT_UP_L_BTN(0x00100),
SHFT_DN_L_BTN(0x00200),
SHFT_UP_R_BTN(0x01000),
SHFT_DN_R_BTN(0x02000),
POWERUP_L_BTN(0x00400),
ONOFF_L_BTN(0x01000),
SHFT_UP_R_BTN(0x02000),
SHFT_DN_R_BTN(0x04000),
POWERUP_R_BTN(0x10000),
POWERUP_R_BTN(0x04000),
ONOFF_R_BTN(0x20000);
final int mask;

View File

@@ -1,18 +1,33 @@
import 'dart:io';
import 'package:accessibility/accessibility.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:swift_control/pages/requirements.dart';
import 'package:swift_control/theme.dart';
import 'package:swift_control/utils/connection.dart';
import 'package:swift_control/utils/actions/android.dart';
import 'package:swift_control/utils/actions/desktop.dart';
import 'package:swift_control/utils/settings/settings.dart';
import 'bluetooth/connection.dart';
import 'utils/actions/base_actions.dart';
final connection = Connection();
final actionHandler = ActionHandler();
late final BaseActions actionHandler;
final accessibilityHandler = Accessibility();
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final settings = Settings();
void main() {
if (kIsWeb) {
actionHandler = StubActions();
} else if (Platform.isAndroid) {
actionHandler = AndroidActions();
} else {
actionHandler = DesktopActions();
}
runApp(const SwiftPlayApp());
}

View File

@@ -1,11 +1,14 @@
import 'dart:async';
import 'dart:io';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/devices/base_device.dart';
import 'package:swift_control/pages/touch_area.dart';
import 'package:swift_control/widgets/logviewer.dart';
import '../bluetooth/devices/base_device.dart';
import '../widgets/menu.dart';
class DevicePage extends StatefulWidget {
@@ -46,13 +49,14 @@ class _DevicePageState extends State<DevicePage> {
child: Scaffold(
appBar: AppBar(
title: Text('SwiftControl'),
actions: [MenuButton()],
actions: buildMenuButtons(),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 10,
children: [
Text(
'Devices:\n${connection.devices.joinToString(separator: '\n', transform: (it) {
@@ -60,6 +64,35 @@ class _DevicePageState extends State<DevicePage> {
})}',
),
Divider(color: Theme.of(context).colorScheme.primary, height: 30),
if (!kIsWeb && (Platform.isAndroid || kDebugMode)) ...[
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(_) => TouchAreaSetupPage(
onSave: (gearUp, gearDown) {
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
final convertedGearUp =
gearUp.translate(touchAreaSize / 2, touchAreaSize / 2) * devicePixelRatio;
final convertedGearDown =
gearDown.translate(touchAreaSize / 2, touchAreaSize / 2) * devicePixelRatio;
print("Gear Up Position: $gearUp - converted: $convertedGearUp");
print("Gear Down Position: $gearDown - converted: $convertedGearDown");
actionHandler.updateTouchPositions(convertedGearUp, convertedGearDown);
settings.updateTouchPositions(convertedGearUp, convertedGearDown);
},
),
),
);
},
child: Text('Customize touch areas (optional)'),
),
],
Expanded(child: LogViewer()),
],
),

View File

@@ -4,6 +4,7 @@ import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/requirements/multi.dart';
import 'package:swift_control/utils/requirements/platform.dart';
import 'package:swift_control/widgets/menu.dart';
@@ -28,14 +29,16 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
// call after first frame
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!kIsWeb && Platform.isMacOS) {
// add more delay due tu CBManagerStateUnknown
Future.delayed(const Duration(seconds: 2), () {
settings.init().then((_) {
if (!kIsWeb && Platform.isMacOS) {
// add more delay due tu CBManagerStateUnknown
Future.delayed(const Duration(seconds: 2), () {
_reloadRequirements();
});
} else {
_reloadRequirements();
});
} else {
_reloadRequirements();
}
}
});
});
connection.hasDevices.addListener(() {
@@ -64,7 +67,7 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
appBar: AppBar(
title: Text('SwiftControl'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [MenuButton()],
actions: buildMenuButtons(),
),
body:
_requirements.isEmpty
@@ -83,7 +86,7 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
}
: null,
onStepTapped: (step) {
if (_requirements[step].status) {
if (_requirements[step].status && _requirements[step] is! KeymapRequirement) {
return;
}
final hasEarlierIncomplete = _requirements.indexWhere((req) => !req.status) < step;
@@ -100,16 +103,20 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
.mapIndexed(
(index, req) => Step(
title: Text(req.name),
content:
(index == _currentStep
? req.build(context, () {
_reloadRequirements();
})
: null) ??
ElevatedButton(
onPressed: req.status ? null : () => _callRequirement(req),
child: Text(req.name),
),
content: Container(
padding: const EdgeInsets.symmetric(vertical: 16.0),
alignment: Alignment.centerLeft,
child:
(index == _currentStep
? req.build(context, () {
_reloadRequirements();
})
: null) ??
ElevatedButton(
onPressed: req.status ? null : () => _callRequirement(req),
child: Text(req.name),
),
),
state: req.status ? StepState.complete : StepState.indexed,
),
)

View File

@@ -57,11 +57,15 @@ class _ScanWidgetState extends State<ScanWidget> {
],
);
} else {
return ElevatedButton(
onPressed: () {
connection.performScanning();
},
child: const Text("SCAN"),
return Row(
children: [
ElevatedButton(
onPressed: () {
connection.performScanning();
},
child: const Text("SCAN"),
),
],
);
}
},

174
lib/pages/touch_area.dart Normal file
View File

@@ -0,0 +1,174 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:swift_control/main.dart';
final touchAreaSize = 32.0;
class TouchAreaSetupPage extends StatefulWidget {
final void Function(Offset gearUp, Offset gearDown) onSave;
const TouchAreaSetupPage({required this.onSave, super.key});
@override
State<TouchAreaSetupPage> createState() => _TouchAreaSetupPageState();
}
class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
File? _backgroundImage;
Offset _gearUpPos = const Offset(200, 300);
Offset _gearDownPos = const Offset(100, 300);
Future<void> _pickScreenshot() async {
final picker = ImagePicker();
final result = await picker.pickImage(source: ImageSource.gallery);
if (result != null) {
setState(() {
_backgroundImage = File(result.path);
});
}
}
void _saveAndClose() {
widget.onSave(_gearUpPos, _gearDownPos);
Navigator.of(context).pop();
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
if (actionHandler.gearUpTouchPosition != null) {
_gearUpPos = actionHandler.gearUpTouchPosition!;
_gearUpPos = Offset(
_gearUpPos.dx / devicePixelRatio - touchAreaSize / 2,
_gearUpPos.dy / devicePixelRatio - touchAreaSize / 2,
);
}
if (actionHandler.gearDownTouchPosition != null) {
_gearDownPos = actionHandler.gearDownTouchPosition!;
_gearDownPos = Offset(
_gearDownPos.dx / devicePixelRatio - touchAreaSize / 2,
_gearDownPos.dy / devicePixelRatio - touchAreaSize / 2,
);
}
setState(() {});
});
}
Widget _buildDraggableArea({
required Offset position,
required void Function(Offset newPosition) onPositionChanged,
required Color color,
required String label,
}) {
return Positioned(
left: position.dx,
top: position.dy,
child: Draggable(
feedback: Material(color: Colors.transparent, child: _TouchDot(color: Colors.yellow, label: label)),
childWhenDragging: const SizedBox.shrink(),
onDraggableCanceled: (_, offset) {
setState(() => onPositionChanged(offset));
},
child: _TouchDot(color: color, label: label),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
if (_backgroundImage != null)
Positioned.fill(child: Opacity(opacity: 0.5, child: Image.file(_backgroundImage!, fit: BoxFit.cover)))
else
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Text('''1. Create an in-game screenshot of your app (e.g. within MyWhoosh)
2. Load the screenshot with the button below
3. Make sure the app is in the correct orientation (portrait or landscape)
4. Drag the touch areas to the correct position where the gear up / down buttons are located
5. Save and close this screen'''),
ElevatedButton(
onPressed: () {
_pickScreenshot();
},
child: Text('Load in-game screenshot for placement'),
),
],
),
),
// Touch Areas
_buildDraggableArea(
position: _gearUpPos,
onPositionChanged: (newPos) => _gearUpPos = newPos,
color: Colors.green,
label: "Gear ↑",
),
_buildDraggableArea(
position: _gearDownPos,
onPositionChanged: (newPos) => _gearDownPos = newPos,
color: Colors.red,
label: "Gear ↓",
),
Positioned(
top: 40,
right: 170,
child: ElevatedButton.icon(
onPressed: () {
_gearDownPos = Offset(100, 300);
_gearUpPos = Offset(200, 300);
setState(() {});
},
label: const Icon(Icons.lock_reset),
),
),
Positioned(
top: 40,
right: 20,
child: ElevatedButton.icon(
onPressed: _saveAndClose,
icon: const Icon(Icons.save),
label: const Text("Save & Close"),
),
),
],
),
);
}
}
class _TouchDot extends StatelessWidget {
final Color color;
final String label;
const _TouchDot({required this.color, required this.label});
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
width: touchAreaSize,
height: touchAreaSize,
decoration: BoxDecoration(
color: color.withOpacity(0.6),
shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: 2),
),
),
Text(label, style: TextStyle(color: Colors.black, fontSize: 12)),
],
);
}
}

View File

@@ -11,6 +11,14 @@ class AndroidActions extends BaseActions {
static const validPackageNames = [MYWHOOSH_APP_PACKAGE, TRAININGPEAKS_APP_PACKAGE];
WindowEvent? windowInfo;
Offset? _gearUpTouchPosition;
Offset? _gearDownTouchPosition;
@override
Offset? get gearUpTouchPosition => _gearUpTouchPosition;
@override
Offset? get gearDownTouchPosition => _gearDownTouchPosition;
@override
void init(Keymap? keymap) {
@@ -23,9 +31,10 @@ class AndroidActions extends BaseActions {
@override
void decreaseGear() {
if (windowInfo == null) {
throw Exception("Decrease gear: No window info");
} else {
if (_gearDownTouchPosition == null) {
if (windowInfo == null) {
throw Exception("Increasing gear: No window info");
}
final point = switch (windowInfo!.packageName) {
MYWHOOSH_APP_PACKAGE => Offset(windowInfo!.windowWidth * 0.80, windowInfo!.windowHeight * 0.94),
TRAININGPEAKS_APP_PACKAGE => Offset(windowInfo!.windowWidth / 2 * 1.15, windowInfo!.windowHeight * 0.74),
@@ -33,14 +42,17 @@ class AndroidActions extends BaseActions {
};
accessibilityHandler.performTouch(point.dx, point.dy);
} else {
accessibilityHandler.performTouch(_gearDownTouchPosition!.dx, _gearDownTouchPosition!.dy);
}
}
@override
void increaseGear() {
if (windowInfo == null) {
throw Exception("Increasing gear: No window info");
} else {
if (_gearUpTouchPosition == null) {
if (windowInfo == null) {
throw Exception("Increasing gear: No window info");
}
final point = switch (windowInfo!.packageName) {
MYWHOOSH_APP_PACKAGE => Offset(windowInfo!.windowWidth * 0.98, windowInfo!.windowHeight * 0.94),
TRAININGPEAKS_APP_PACKAGE => Offset(windowInfo!.windowWidth / 2 * 1.32, windowInfo!.windowHeight * 0.74),
@@ -48,6 +60,8 @@ class AndroidActions extends BaseActions {
};
accessibilityHandler.performTouch(point.dx, point.dy);
} else {
accessibilityHandler.performTouch(_gearUpTouchPosition!.dx, _gearUpTouchPosition!.dy);
}
}
@@ -55,4 +69,10 @@ class AndroidActions extends BaseActions {
void controlMedia(MediaAction action) {
accessibilityHandler.controlMedia(action);
}
@override
void updateTouchPositions(Offset gearUp, Offset gearDown) {
_gearUpTouchPosition = gearUp;
_gearDownTouchPosition = gearDown;
}
}

View File

@@ -1,14 +1,13 @@
import 'dart:io';
import 'dart:ui';
import 'package:accessibility/accessibility.dart';
import 'package:flutter/foundation.dart';
import '../keymap/keymap.dart';
import 'android.dart';
import 'desktop.dart';
abstract class BaseActions {
Keymap? get keymap => null;
Offset? get gearUpTouchPosition => null;
Offset? get gearDownTouchPosition => null;
void init(Keymap? keymap) {}
void increaseGear();
@@ -17,6 +16,8 @@ abstract class BaseActions {
void controlMedia(MediaAction action) {
throw UnimplementedError();
}
void updateTouchPositions(Offset gearUp, Offset gearDown) {}
}
class StubActions extends BaseActions {
@@ -30,35 +31,3 @@ class StubActions extends BaseActions {
print('Increase gear');
}
}
class ActionHandler {
late BaseActions actions;
ActionHandler() {
if (kIsWeb) {
actions = StubActions();
} else if (Platform.isAndroid) {
actions = AndroidActions();
} else {
actions = DesktopActions();
}
}
Keymap? get keymap => actions.keymap;
void init(Keymap? keymap) {
actions.init(keymap);
}
void increaseGear() {
actions.increaseGear();
}
void decreaseGear() {
actions.decreaseGear();
}
void controlMedia(MediaAction action) {
actions.controlMedia(action);
}
}

View File

@@ -15,22 +15,20 @@ class DesktopActions extends BaseActions {
}
@override
void decreaseGear() {
Future<void> decreaseGear() async {
if (keymap == null) {
throw Exception('Keymap is not set');
}
keyPressSimulator.simulateKeyDown(_keymap!.decrease).then((_) {
keyPressSimulator.simulateKeyUp(_keymap!.decrease);
});
await keyPressSimulator.simulateKeyDown(_keymap!.decrease);
await keyPressSimulator.simulateKeyUp(_keymap!.decrease);
}
@override
void increaseGear() {
Future<void> increaseGear() async {
if (keymap == null) {
throw Exception('Keymap is not set');
}
keyPressSimulator.simulateKeyDown(_keymap!.increase).then((_) {
keyPressSimulator.simulateKeyUp(_keymap!.increase);
});
await keyPressSimulator.simulateKeyDown(_keymap!.increase);
await keyPressSimulator.simulateKeyUp(_keymap!.increase);
}
}

View File

@@ -1,83 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:swift_control/utils/ble.dart';
import 'package:swift_control/utils/crypto/local_key_provider.dart';
import 'package:swift_control/utils/devices/zwift_click.dart';
import 'package:swift_control/utils/devices/zwift_play.dart';
import 'package:swift_control/utils/devices/zwift_ride.dart';
import 'package:universal_ble/universal_ble.dart';
import '../crypto/zap_crypto.dart';
import '../messages/notification.dart';
abstract class BaseDevice {
final BleDevice scanResult;
final zapEncryption = ZapCrypto(LocalKeyProvider());
bool isConnected = false;
bool supportsEncryption = true;
BaseDevice(this.scanResult);
static BaseDevice? fromScanResult(BleDevice scanResult) {
if (scanResult.name == 'Zwift Ride') {
return ZwiftRide(scanResult);
}
if (kIsWeb) {
// manufacturer data is not available on web
if (scanResult.name == 'Zwift Play') {
return ZwiftPlay(scanResult);
} else if (scanResult.name == 'Zwift Click') {
return ZwiftClick(scanResult);
}
}
final manufacturerData = scanResult.manufacturerDataList;
final data = manufacturerData.firstOrNullWhere((e) => e.companyId == Constants.ZWIFT_MANUFACTURER_ID)?.payload;
if (data == null || data.isEmpty) {
return null;
}
final type = DeviceType.fromManufacturerData(data.first);
return switch (type) {
DeviceType.click => ZwiftClick(scanResult),
DeviceType.playRight => ZwiftPlay(scanResult),
DeviceType.playLeft => ZwiftPlay(scanResult),
_ => null,
};
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BaseDevice && runtimeType == other.runtimeType && scanResult == other.scanResult;
@override
int get hashCode => scanResult.hashCode;
@override
String toString() {
return runtimeType.toString();
}
BleDevice get device => scanResult;
final StreamController<BaseNotification> actionStreamInternal = StreamController<BaseNotification>.broadcast();
Stream<BaseNotification> get actionStream => actionStreamInternal.stream;
Future<void> connect() async {
await UniversalBle.connect(device.deviceId, connectionTimeout: const Duration(seconds: 3));
if (!kIsWeb && Platform.isAndroid) {
//await UniversalBle.requestMtu(device.deviceId, 256);
}
final services = await UniversalBle.discoverServices(device.deviceId);
await handleServices(services);
}
Future<void> handleServices(List<BleService> services);
void processCharacteristic(String tag, Uint8List bytes);
}

View File

@@ -1,12 +1,41 @@
import 'package:flutter/services.dart';
enum Keymap {
myWhoosh(increase: PhysicalKeyboardKey.keyK, decrease: PhysicalKeyboardKey.keyI),
indieVelo(increase: PhysicalKeyboardKey(0x70030), decrease: PhysicalKeyboardKey(0x70038)),
plusMinus(increase: PhysicalKeyboardKey(0x70030), decrease: PhysicalKeyboardKey(0x70038));
class Keymap {
static Keymap myWhoosh = Keymap('MyWhoosh', increase: PhysicalKeyboardKey.keyK, decrease: PhysicalKeyboardKey.keyI);
static Keymap custom = Keymap('Custom', increase: null, decrease: null);
final PhysicalKeyboardKey increase;
final PhysicalKeyboardKey decrease;
static List<Keymap> values = [myWhoosh, custom];
const Keymap({required this.increase, required this.decrease});
PhysicalKeyboardKey? increase;
PhysicalKeyboardKey? decrease;
final String name;
Keymap(this.name, {required this.increase, required this.decrease});
@override
String toString() {
if (increase == null && decrease == null) {
return name;
}
return "$name: ${increase?.debugName} + ${decrease?.debugName}";
}
List<String> encode() {
// encode to save in preferences
return [name, increase?.usbHidUsage.toString() ?? '', decrease?.usbHidUsage.toString() ?? ''];
}
static Keymap decode(List<String> data) {
// decode from preferences
final name = data[0];
final keymap = values.firstWhere((element) => element.name == name, orElse: () => custom);
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;
}
}

View File

@@ -1,8 +1,8 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import 'package:keypress_simulator/keypress_simulator.dart';
import 'package:swift_control/pages/scan.dart';
import 'package:swift_control/utils/requirements/platform.dart';
import 'package:swift_control/widgets/custom_keymap_selector.dart';
import 'package:universal_ble/universal_ble.dart';
import '../../main.dart';
@@ -35,18 +35,19 @@ class KeymapRequirement extends PlatformRequirement {
@override
Widget? build(BuildContext context, VoidCallback onUpdate) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: DropdownMenu<Keymap>(
dropdownMenuEntries:
Keymap.values.map((key) => DropdownMenuEntry<Keymap>(value: key, label: key.name.capitalize())).toList(),
onSelected: (keymap) {
actionHandler.init(keymap);
onUpdate();
},
initialSelection: null,
hintText: 'Keymap',
),
return DropdownMenu<Keymap>(
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);
}
actionHandler.init(keymap);
settings.setKeymap(keymap!);
onUpdate();
},
initialSelection: actionHandler.keymap,
hintText: 'Keymap',
);
}
}

View File

@@ -0,0 +1,43 @@
import 'dart:ui';
import 'package:shared_preferences/shared_preferences.dart';
import '../../main.dart';
import '../keymap/keymap.dart';
class Settings {
late final SharedPreferences _prefs;
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
try {
final keymapSetting = _prefs.getStringList("keymap");
if (keymapSetting != null) {
actionHandler.init(Keymap.decode(keymapSetting));
}
final gearUpX = _prefs.getDouble("gearUpX");
final gearUpY = _prefs.getDouble("gearUpY");
final gearDownX = _prefs.getDouble("gearDownX");
final gearDownY = _prefs.getDouble("gearDownY");
if (gearUpX != null && gearUpY != null && gearDownX != null && gearDownY != null) {
actionHandler.updateTouchPositions(Offset(gearUpX, gearUpY), Offset(gearDownX, gearDownY));
}
} catch (e) {
// couldn't decode, reset
await _prefs.clear();
}
}
void setKeymap(Keymap keymap) {
_prefs.setStringList("keymap", keymap.encode());
}
void updateTouchPositions(Offset gearUp, Offset gearDown) {
_prefs.setDouble("gearUpX", gearUp.dx);
_prefs.setDouble("gearUpY", gearUp.dy);
_prefs.setDouble("gearDownX", gearDown.dx);
_prefs.setDouble("gearDownY", gearDown.dy);
}
}

View File

@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/utils/keymap/keymap.dart';
Future<Keymap?> showCustomKeymapDialog(BuildContext context, {required Keymap keymap}) {
return showDialog<Keymap>(
context: context,
builder: (context) {
return GearHotkeyDialog(keymap: keymap);
},
);
}
class GearHotkeyDialog extends StatefulWidget {
final Keymap keymap;
const GearHotkeyDialog({super.key, required this.keymap});
@override
State<GearHotkeyDialog> createState() => _GearHotkeyDialogState();
}
class _GearHotkeyDialogState extends State<GearHotkeyDialog> {
final FocusNode _focusNode = FocusNode();
final Set<PhysicalKeyboardKey> _pressedKeys = {};
Set<PhysicalKeyboardKey>? _gearUpHotkey;
Set<PhysicalKeyboardKey>? _gearDownHotkey;
String _mode = 'up'; // 'up' or 'down'
@override
void initState() {
super.initState();
_focusNode.requestFocus();
}
void _onKey(KeyEvent event) {
setState(() {
if (event is KeyDownEvent) {
_pressedKeys.add(event.physicalKey);
} else if (event is KeyUpEvent) {
if (_pressedKeys.isNotEmpty) {
if (_mode == 'up') {
_gearUpHotkey = {..._pressedKeys};
_mode = 'down';
} else {
_gearDownHotkey = {..._pressedKeys};
widget.keymap.increase = _gearUpHotkey!.first;
widget.keymap.decrease = _gearDownHotkey!.first;
Navigator.of(context).pop(widget.keymap);
}
_pressedKeys.clear();
}
}
});
}
String _formatKeys(Set<PhysicalKeyboardKey>? keys) {
if (keys == null || keys.isEmpty) return 'Not set';
return keys.map((k) => k.debugName ?? k).join(' + ');
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Set Gear Hotkeys'),
content: KeyboardListener(
focusNode: _focusNode,
autofocus: true,
onKeyEvent: _onKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Step 1: Press a hotkey for **Gear Up**."),
Text("Step 2: Press a hotkey for **Gear Down**."),
SizedBox(height: 20),
ListTile(
leading: Icon(Icons.arrow_upward),
title: Text("Gear Up Hotkey"),
subtitle: Text(_formatKeys(_gearUpHotkey)),
),
ListTile(
leading: Icon(Icons.arrow_downward),
title: Text("Gear Down Hotkey"),
subtitle: Text(_formatKeys(_gearDownHotkey)),
),
if (_pressedKeys.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text("Recording: ${_formatKeys(_pressedKeys)}", style: TextStyle(color: Colors.blue)),
),
],
),
),
actions: [TextButton(onPressed: () => Navigator.of(context).pop(null), child: Text("Cancel"))],
);
}
}

View File

@@ -3,8 +3,8 @@ import 'dart:async';
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import '../bluetooth/messages/notification.dart';
import '../main.dart';
import '../utils/messages/notification.dart';
class LogViewer extends StatefulWidget {
const LogViewer({super.key});

View File

@@ -1,6 +1,21 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/main.dart';
import 'package:url_launcher/url_launcher_string.dart';
List<Widget> buildMenuButtons() {
return [
TextButton(
onPressed: () {
launchUrlString('https://paypal.me/boni');
},
child: Text('Donate ♥'),
),
const MenuButton(),
SizedBox(width: 8),
];
}
class MenuButton extends StatelessWidget {
const MenuButton({super.key});
@@ -9,18 +24,31 @@ class MenuButton extends StatelessWidget {
return PopupMenuButton(
itemBuilder:
(c) => [
if (kDebugMode) ...[
PopupMenuItem(
child: Text('Gear up'),
onTap: () {
Future.delayed(Duration(seconds: 2)).then((_) {
actionHandler.increaseGear();
});
},
),
PopupMenuItem(
child: Text('Gear down'),
onTap: () {
Future.delayed(Duration(seconds: 2)).then((_) {
actionHandler.decreaseGear();
});
},
),
PopupMenuItem(child: PopupMenuDivider()),
],
PopupMenuItem(
child: Text('Feedback'),
onTap: () {
launchUrlString('https://github.com/jonasbark/swiftcontrol/issues');
},
),
PopupMenuItem(
child: Text('Donate 🫶'),
onTap: () {
launchUrlString('https://paypal.me/boni');
},
),
PopupMenuItem(
child: Text('License'),
onTap: () {

View File

@@ -6,9 +6,13 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
url_launcher_linux
)

View File

@@ -5,14 +5,18 @@
import FlutterMacOS
import Foundation
import file_selector_macos
import flutter_local_notifications
import keypress_simulator_macos
import shared_preferences_foundation
import universal_ble
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
KeypressSimulatorMacosPlugin.register(with: registry.registrar(forPlugin: "KeypressSimulatorMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@@ -1,9 +1,14 @@
PODS:
- file_selector_macos (0.0.1):
- FlutterMacOS
- flutter_local_notifications (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- keypress_simulator_macos (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- universal_ble (0.0.1):
- Flutter
- FlutterMacOS
@@ -11,28 +16,36 @@ PODS:
- FlutterMacOS
DEPENDENCIES:
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- keypress_simulator_macos (from `Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- universal_ble (from `Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES:
file_selector_macos:
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
flutter_local_notifications:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
FlutterMacOS:
:path: Flutter/ephemeral
keypress_simulator_macos:
:path: Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
universal_ble:
:path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
flutter_local_notifications: 4ccab5b7a22835214a6672e3f9c5e8ae207dab36
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
keypress_simulator_macos: f8556f9101f9f2f175652e0bceddf0fe82a4c6b2
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404

View File

@@ -10,5 +10,7 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>

View File

@@ -96,6 +96,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
crypto:
dependency: transitive
description:
@@ -136,6 +144,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.dev"
source: hosted
version: "0.9.3+2"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
url: "https://pub.dev"
source: hosted
version: "0.9.4+2"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev"
source: hosted
version: "2.6.2"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
url: "https://pub.dev"
source: hosted
version: "0.9.3+4"
fixnum:
dependency: transitive
description:
@@ -213,6 +261,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3"
url: "https://pub.dev"
source: hosted
version: "2.0.27"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -255,6 +311,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.4"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9"
url: "https://pub.dev"
source: hosted
version: "0.8.12+22"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
url: "https://pub.dev"
source: hosted
version: "0.8.12+2"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
url: "https://pub.dev"
source: hosted
version: "0.2.1+2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
url: "https://pub.dev"
source: hosted
version: "0.2.1+2"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
url: "https://pub.dev"
source: hosted
version: "2.10.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
json_annotation:
dependency: transitive
description:
@@ -359,6 +479,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
path:
dependency: transitive
description:
@@ -367,6 +495,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
@@ -423,6 +575,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
@@ -455,6 +615,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad"
url: "https://pub.dev"
source: hosted
version: "2.4.8"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter

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.0.5+0
version: 1.1.1+0
environment:
sdk: ^3.7.0
@@ -16,8 +16,10 @@ dependencies:
protobuf: ^3.1.0
permission_handler: ^11.4.0
dartx: any
image_picker: ^1.1.2
pointycastle: any
keypress_simulator: ^0.2.0
shared_preferences: ^2.5.3
flex_color_scheme: ^8.2.0
accessibility:
path: accessibility

View File

@@ -6,12 +6,15 @@
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <keypress_simulator_windows/keypress_simulator_windows_plugin_c_api.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <universal_ble/universal_ble_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
KeypressSimulatorWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("KeypressSimulatorWindowsPluginCApi"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
keypress_simulator_windows
permission_handler_windows
universal_ble