mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a7922cf17 | ||
|
|
8e23de2718 | ||
|
|
11aec5fba1 | ||
|
|
c98f213e2e | ||
|
|
8c11cfcad6 | ||
|
|
5fe88ffc6a | ||
|
|
7a8c7a4ee1 | ||
|
|
3343325195 | ||
|
|
edda16dc06 | ||
|
|
7a3d120123 | ||
|
|
92419c9182 | ||
|
|
68bb5bf371 | ||
|
|
b0d8bfcadd | ||
|
|
a58ad1daf6 | ||
|
|
657c6056c4 | ||
|
|
84daba8902 | ||
|
|
3e37f8a269 | ||
|
|
28d178c4be | ||
|
|
f560cd5930 | ||
|
|
dbf24c6cd3 | ||
|
|
0a4989ca47 | ||
|
|
507dbf5d0f | ||
|
|
536f36f4e7 | ||
|
|
c523ba2287 | ||
|
|
a3f1cbb3b1 | ||
|
|
561bb2f0f4 |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,3 +1,25 @@
|
||||
### 1.1.6 (2025-03-31)
|
||||
- Zwift Ride: add buttonPowerDown to shift gears
|
||||
- Zwift Play: Fix buttonShift assignment
|
||||
- Android: fix action to go to next song
|
||||
- App now checks if you run the latest available version
|
||||
|
||||
### 1.1.5 (2025-03-30)
|
||||
- fix bluetooth connection #6, also add missing entitlement on macOS
|
||||
|
||||
### 1.1.3 (2025-03-30)
|
||||
- Windows: fix custom keyboard profile recreation after restart, also warn when choosing MyWhoosh profile (may fix #7)
|
||||
- Zwift Ride: button map adjustments to prevent double shifting
|
||||
- potential fix for #6
|
||||
|
||||
### 1.1.1 (2025-03-30)
|
||||
- potential fix for Bluetooth device detection
|
||||
|
||||
### 1.1.0 (2025-03-30)
|
||||
- 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
|
||||
|
||||
@@ -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.
|
||||
[](https://paypal.me/boni)
|
||||
|
||||
## TODO
|
||||
- test Zwift Ride
|
||||
- confirm that Windows release works
|
||||
- implement more actions for Play + Ride
|
||||
|
||||
@@ -11,14 +11,11 @@ import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
|
||||
|
||||
/** AccessibilityPlugin */
|
||||
class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
|
||||
class AccessibilityPlugin: FlutterPlugin, Accessibility {
|
||||
/// The MethodChannel that will the communication between Flutter and native Android
|
||||
///
|
||||
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
||||
@@ -38,14 +35,6 @@ class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
|
||||
Observable.fromService = eventHandler
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
if (call.method == "getPlatformVersion") {
|
||||
result.success("Android ${android.os.Build.VERSION.RELEASE}")
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
}
|
||||
@@ -72,7 +61,10 @@ class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
|
||||
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
}
|
||||
MediaAction.NEXT -> audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
MediaAction.NEXT -> {
|
||||
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
}
|
||||
MediaAction.VOLUME_DOWN -> audioService.adjustVolume(android.media.AudioManager.ADJUST_LOWER, android.media.AudioManager.FLAG_SHOW_UI)
|
||||
MediaAction.VOLUME_UP -> audioService.adjustVolume(android.media.AudioManager.ADJUST_RAISE, android.media.AudioManager.FLAG_SHOW_UI)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- to check if you have the latest version -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:label="SwiftControl"
|
||||
android:name="${applicationName}"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,18 @@ class Connection {
|
||||
Future<void> performScanning() async {
|
||||
isScanning.value = true;
|
||||
|
||||
// does not work on web, may not work on Windows
|
||||
if (!kIsWeb && !Platform.isWindows) {
|
||||
UniversalBle.getSystemDevices(
|
||||
withServices: [BleUuid.ZWIFT_CUSTOM_SERVICE_UUID, BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID],
|
||||
).then((devices) {
|
||||
final baseDevices = devices.mapNotNull(BaseDevice.fromScanResult).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])),
|
||||
@@ -1,26 +1,97 @@
|
||||
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) {
|
||||
// Use the name first as the "System Devices" and Web (android sometimes Windows) don't have manufacturer data
|
||||
final device = switch (scanResult.name) {
|
||||
'Zwift Ride' => ZwiftRide(scanResult),
|
||||
'Zwift Play' => ZwiftPlay(scanResult),
|
||||
'Zwift Click' => ZwiftClick(scanResult),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (device != null) {
|
||||
return device;
|
||||
} else {
|
||||
// otherwise use the manufacturer data to identify the device
|
||||
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),
|
||||
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 +148,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 +177,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 +216,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);
|
||||
}
|
||||
26
lib/bluetooth/devices/zwift_click.dart
Normal file
26
lib/bluetooth/devices/zwift_click.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -24,12 +24,15 @@ class ZwiftRide extends ZwiftClick {
|
||||
_lastControllerNotification = clickNotification;
|
||||
actionStreamInternal.add(clickNotification);
|
||||
|
||||
if (clickNotification.buttonShiftDownLeft || clickNotification.buttonShiftUpLeft || clickNotification.buttonZ) {
|
||||
if (clickNotification.buttonShiftDownLeft ||
|
||||
clickNotification.buttonShiftUpLeft ||
|
||||
clickNotification.buttonOnOffLeft ||
|
||||
clickNotification.buttonPowerDownLeft) {
|
||||
actionHandler.decreaseGear();
|
||||
} else if (clickNotification.buttonShiftUpRight ||
|
||||
clickNotification.buttonShiftDownRight ||
|
||||
clickNotification.buttonOnOffLeft) {
|
||||
// TODO remove buttonZ once the assignment is fixed for real
|
||||
clickNotification.buttonOnOffRight ||
|
||||
clickNotification.buttonPowerUpLeft) {
|
||||
actionHandler.increaseGear();
|
||||
}
|
||||
/*if (clickNotification.buttonA) {
|
||||
@@ -1,19 +1,16 @@
|
||||
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;
|
||||
|
||||
bool buttonUp = false;
|
||||
bool buttonDown = false;
|
||||
|
||||
ClickNotification(Uint8List message) {
|
||||
final status = ClickKeyPadStatus.fromBuffer(message);
|
||||
buttonUp = status.buttonPlus.value == BTN_PRESSED;
|
||||
buttonDown = status.buttonMinus.value == BTN_PRESSED;
|
||||
buttonUp = status.buttonPlus == PlayButtonStatus.ON;
|
||||
buttonDown = status.buttonMinus == PlayButtonStatus.ON;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1,25 +1,22 @@
|
||||
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;
|
||||
|
||||
late bool rightPad, buttonY, buttonZ, buttonA, buttonB, buttonOn, buttonShift;
|
||||
late int analogLR, analogUD;
|
||||
|
||||
PlayNotification(Uint8List message) {
|
||||
final status = PlayKeyPadStatus.fromBuffer(message);
|
||||
|
||||
rightPad = status.rightPad.value == BTN_PRESSED;
|
||||
buttonY = status.buttonYUp.value == BTN_PRESSED;
|
||||
buttonZ = status.buttonZLeft.value == BTN_PRESSED;
|
||||
buttonA = status.buttonARight.value == BTN_PRESSED;
|
||||
buttonB = status.buttonBDown.value == BTN_PRESSED;
|
||||
buttonOn = status.buttonOn.value == BTN_PRESSED;
|
||||
buttonShift = status.buttonShift.value == BTN_PRESSED;
|
||||
rightPad = status.rightPad == PlayButtonStatus.ON;
|
||||
buttonY = status.buttonYUp == PlayButtonStatus.ON;
|
||||
buttonZ = status.buttonZLeft == PlayButtonStatus.ON;
|
||||
buttonA = status.buttonARight == PlayButtonStatus.ON;
|
||||
buttonB = status.buttonBDown == PlayButtonStatus.ON;
|
||||
buttonOn = status.buttonOn == PlayButtonStatus.ON;
|
||||
buttonShift = status.buttonShift == PlayButtonStatus.ON;
|
||||
analogLR = status.analogLR;
|
||||
analogUD = status.analogUD;
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
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),
|
||||
ONOFF_L_BTN(0x01000),
|
||||
SHFT_UP_R_BTN(0x02000),
|
||||
SHFT_DN_R_BTN(0x04000),
|
||||
SHFT_UP_L_BTN(0x00100),
|
||||
SHFT_DN_L_BTN(0x00200),
|
||||
SHFT_UP_R_BTN(0x01000),
|
||||
SHFT_DN_R_BTN(0x02000),
|
||||
|
||||
POWERUP_R_BTN(0x10000),
|
||||
ONOFF_R_BTN(0x20000);
|
||||
POWERUP_L_BTN(0x00400),
|
||||
POWERUP_R_BTN(0x04000),
|
||||
ONOFF_L_BTN(0x00800),
|
||||
ONOFF_R_BTN(0x08000);
|
||||
|
||||
final int mask;
|
||||
|
||||
@@ -25,8 +25,8 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage {
|
||||
PlayButtonStatus? buttonZLeft,
|
||||
PlayButtonStatus? buttonARight,
|
||||
PlayButtonStatus? buttonBDown,
|
||||
PlayButtonStatus? buttonOn,
|
||||
PlayButtonStatus? buttonShift,
|
||||
PlayButtonStatus? buttonOn,
|
||||
$core.int? analogLR,
|
||||
$core.int? analogUD,
|
||||
}) {
|
||||
@@ -46,12 +46,12 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage {
|
||||
if (buttonBDown != null) {
|
||||
$result.buttonBDown = buttonBDown;
|
||||
}
|
||||
if (buttonOn != null) {
|
||||
$result.buttonOn = buttonOn;
|
||||
}
|
||||
if (buttonShift != null) {
|
||||
$result.buttonShift = buttonShift;
|
||||
}
|
||||
if (buttonOn != null) {
|
||||
$result.buttonOn = buttonOn;
|
||||
}
|
||||
if (analogLR != null) {
|
||||
$result.analogLR = analogLR;
|
||||
}
|
||||
@@ -70,8 +70,8 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage {
|
||||
..e<PlayButtonStatus>(3, _omitFieldNames ? '' : 'ButtonZLeft', $pb.PbFieldType.OE, protoName: 'Button_Z_Left', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..e<PlayButtonStatus>(4, _omitFieldNames ? '' : 'ButtonARight', $pb.PbFieldType.OE, protoName: 'Button_A_Right', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..e<PlayButtonStatus>(5, _omitFieldNames ? '' : 'ButtonBDown', $pb.PbFieldType.OE, protoName: 'Button_B_Down', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..e<PlayButtonStatus>(6, _omitFieldNames ? '' : 'ButtonOn', $pb.PbFieldType.OE, protoName: 'Button_On', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..e<PlayButtonStatus>(7, _omitFieldNames ? '' : 'ButtonShift', $pb.PbFieldType.OE, protoName: 'Button_Shift', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..e<PlayButtonStatus>(6, _omitFieldNames ? '' : 'ButtonShift', $pb.PbFieldType.OE, protoName: 'Button_Shift', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..e<PlayButtonStatus>(7, _omitFieldNames ? '' : 'ButtonOn', $pb.PbFieldType.OE, protoName: 'Button_On', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..a<$core.int>(8, _omitFieldNames ? '' : 'AnalogLR', $pb.PbFieldType.OS3, protoName: 'Analog_LR')
|
||||
..a<$core.int>(9, _omitFieldNames ? '' : 'AnalogUD', $pb.PbFieldType.OS3, protoName: 'Analog_UD')
|
||||
..hasRequiredFields = false
|
||||
@@ -144,22 +144,22 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage {
|
||||
void clearButtonBDown() => clearField(5);
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
PlayButtonStatus get buttonOn => $_getN(5);
|
||||
PlayButtonStatus get buttonShift => $_getN(5);
|
||||
@$pb.TagNumber(6)
|
||||
set buttonOn(PlayButtonStatus v) { setField(6, v); }
|
||||
set buttonShift(PlayButtonStatus v) { setField(6, v); }
|
||||
@$pb.TagNumber(6)
|
||||
$core.bool hasButtonOn() => $_has(5);
|
||||
$core.bool hasButtonShift() => $_has(5);
|
||||
@$pb.TagNumber(6)
|
||||
void clearButtonOn() => clearField(6);
|
||||
void clearButtonShift() => clearField(6);
|
||||
|
||||
@$pb.TagNumber(7)
|
||||
PlayButtonStatus get buttonShift => $_getN(6);
|
||||
PlayButtonStatus get buttonOn => $_getN(6);
|
||||
@$pb.TagNumber(7)
|
||||
set buttonShift(PlayButtonStatus v) { setField(7, v); }
|
||||
set buttonOn(PlayButtonStatus v) { setField(7, v); }
|
||||
@$pb.TagNumber(7)
|
||||
$core.bool hasButtonShift() => $_has(6);
|
||||
$core.bool hasButtonOn() => $_has(6);
|
||||
@$pb.TagNumber(7)
|
||||
void clearButtonShift() => clearField(7);
|
||||
void clearButtonOn() => clearField(7);
|
||||
|
||||
@$pb.TagNumber(8)
|
||||
$core.int get analogLR => $_getIZ(7);
|
||||
@@ -82,8 +82,8 @@ const PlayKeyPadStatus$json = {
|
||||
{'1': 'Button_Z_Left', '3': 3, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonZLeft'},
|
||||
{'1': 'Button_A_Right', '3': 4, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonARight'},
|
||||
{'1': 'Button_B_Down', '3': 5, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonBDown'},
|
||||
{'1': 'Button_On', '3': 6, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonOn'},
|
||||
{'1': 'Button_Shift', '3': 7, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonShift'},
|
||||
{'1': 'Button_Shift', '3': 6, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonShift'},
|
||||
{'1': 'Button_On', '3': 7, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonOn'},
|
||||
{'1': 'Analog_LR', '3': 8, '4': 1, '5': 17, '10': 'AnalogLR'},
|
||||
{'1': 'Analog_UD', '3': 9, '4': 1, '5': 17, '10': 'AnalogUD'},
|
||||
],
|
||||
@@ -97,9 +97,9 @@ final $typed_data.Uint8List playKeyPadStatusDescriptor = $convert.base64Decode(
|
||||
'4uZGUuam9uYXNiYXJrLlBsYXlCdXR0b25TdGF0dXNSC0J1dHRvblpMZWZ0EkQKDkJ1dHRvbl9B'
|
||||
'X1JpZ2h0GAQgASgOMh4uZGUuam9uYXNiYXJrLlBsYXlCdXR0b25TdGF0dXNSDEJ1dHRvbkFSaW'
|
||||
'dodBJCCg1CdXR0b25fQl9Eb3duGAUgASgOMh4uZGUuam9uYXNiYXJrLlBsYXlCdXR0b25TdGF0'
|
||||
'dXNSC0J1dHRvbkJEb3duEjsKCUJ1dHRvbl9PbhgGIAEoDjIeLmRlLmpvbmFzYmFyay5QbGF5Qn'
|
||||
'V0dG9uU3RhdHVzUghCdXR0b25PbhJBCgxCdXR0b25fU2hpZnQYByABKA4yHi5kZS5qb25hc2Jh'
|
||||
'cmsuUGxheUJ1dHRvblN0YXR1c1ILQnV0dG9uU2hpZnQSGwoJQW5hbG9nX0xSGAggASgRUghBbm'
|
||||
'dXNSC0J1dHRvbkJEb3duEkEKDEJ1dHRvbl9TaGlmdBgGIAEoDjIeLmRlLmpvbmFzYmFyay5QbG'
|
||||
'F5QnV0dG9uU3RhdHVzUgtCdXR0b25TaGlmdBI7CglCdXR0b25fT24YByABKA4yHi5kZS5qb25h'
|
||||
'c2JhcmsuUGxheUJ1dHRvblN0YXR1c1IIQnV0dG9uT24SGwoJQW5hbG9nX0xSGAggASgRUghBbm'
|
||||
'Fsb2dMUhIbCglBbmFsb2dfVUQYCSABKBFSCEFuYWxvZ1VE');
|
||||
|
||||
@$core.Deprecated('Use playCommandParametersDescriptor instead')
|
||||
@@ -16,8 +16,8 @@ message PlayKeyPadStatus {
|
||||
optional PlayButtonStatus Button_Z_Left = 3;
|
||||
optional PlayButtonStatus Button_A_Right = 4;
|
||||
optional PlayButtonStatus Button_B_Down = 5;
|
||||
optional PlayButtonStatus Button_On = 6;
|
||||
optional PlayButtonStatus Button_Shift = 7;
|
||||
optional PlayButtonStatus Button_Shift = 6;
|
||||
optional PlayButtonStatus Button_On = 7;
|
||||
optional sint32 Analog_LR = 8;
|
||||
optional sint32 Analog_UD = 9;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
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 'package:swift_control/widgets/title.dart';
|
||||
|
||||
import '../bluetooth/devices/base_device.dart';
|
||||
import '../widgets/menu.dart';
|
||||
|
||||
class DevicePage extends StatefulWidget {
|
||||
@@ -45,14 +49,15 @@ class _DevicePageState extends State<DevicePage> {
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('SwiftControl'),
|
||||
actions: [MenuButton()],
|
||||
title: AppTitle(),
|
||||
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 +65,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()),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -4,8 +4,10 @@ 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';
|
||||
import 'package:swift_control/widgets/title.dart';
|
||||
|
||||
import 'device.dart';
|
||||
|
||||
@@ -28,14 +30,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(() {
|
||||
@@ -62,9 +66,9 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('SwiftControl'),
|
||||
title: AppTitle(),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
actions: [MenuButton()],
|
||||
actions: buildMenuButtons(),
|
||||
),
|
||||
body:
|
||||
_requirements.isEmpty
|
||||
@@ -83,7 +87,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 +104,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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
174
lib/pages/touch_area.dart
Normal 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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ class DesktopActions extends BaseActions {
|
||||
if (keymap == null) {
|
||||
throw Exception('Keymap is not set');
|
||||
}
|
||||
await keyPressSimulator.simulateKeyDown(_keymap!.decrease);
|
||||
await keyPressSimulator.simulateKeyUp(_keymap!.decrease);
|
||||
await keyPressSimulator.simulateKeyDown(_keymap!.decrease?.physicalKey);
|
||||
await keyPressSimulator.simulateKeyUp(_keymap!.decrease?.physicalKey);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -28,7 +28,7 @@ class DesktopActions extends BaseActions {
|
||||
if (keymap == null) {
|
||||
throw Exception('Keymap is not set');
|
||||
}
|
||||
await keyPressSimulator.simulateKeyDown(_keymap!.increase);
|
||||
await keyPressSimulator.simulateKeyUp(_keymap!.increase);
|
||||
await keyPressSimulator.simulateKeyDown(_keymap!.increase?.physicalKey);
|
||||
await keyPressSimulator.simulateKeyUp(_keymap!.increase?.physicalKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,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);
|
||||
}
|
||||
@@ -1,12 +1,77 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
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: KeyPair(physicalKey: PhysicalKeyboardKey.keyK, logicalKey: LogicalKeyboardKey.keyK),
|
||||
decrease: KeyPair(physicalKey: PhysicalKeyboardKey.keyI, logicalKey: LogicalKeyboardKey.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});
|
||||
KeyPair? increase;
|
||||
KeyPair? 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?.logicalKey.keyLabel} + ${decrease?.logicalKey.keyLabel}";
|
||||
}
|
||||
|
||||
List<String> encode() {
|
||||
// encode to save in preferences
|
||||
return [
|
||||
name,
|
||||
increase?.logicalKey.keyId.toString() ?? '',
|
||||
increase?.physicalKey.usbHidUsage.toString() ?? '',
|
||||
decrease?.logicalKey.keyId.toString() ?? '',
|
||||
decrease?.physicalKey.usbHidUsage.toString() ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
static Keymap? decode(List<String> data) {
|
||||
// decode from preferences
|
||||
|
||||
if (data.length < 4) {
|
||||
return null;
|
||||
}
|
||||
final name = data[0];
|
||||
final keymap = values.firstOrNullWhere((element) => element.name == name);
|
||||
|
||||
if (keymap == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (keymap.name != custom.name) {
|
||||
return keymap;
|
||||
}
|
||||
|
||||
if (data.sublist(1).all((e) => e.isNotEmpty)) {
|
||||
keymap.increase = KeyPair(
|
||||
physicalKey: PhysicalKeyboardKey(int.parse(data[2])),
|
||||
logicalKey: LogicalKeyboardKey(int.parse(data[1])),
|
||||
);
|
||||
keymap.decrease = KeyPair(
|
||||
physicalKey: PhysicalKeyboardKey(int.parse(data[4])),
|
||||
logicalKey: LogicalKeyboardKey(int.parse(data[3])),
|
||||
);
|
||||
return keymap;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class KeyPair {
|
||||
final PhysicalKeyboardKey physicalKey;
|
||||
final LogicalKeyboardKey logicalKey;
|
||||
|
||||
KeyPair({required this.physicalKey, required this.logicalKey});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:keypress_simulator/keypress_simulator.dart';
|
||||
import 'package:swift_control/pages/scan.dart';
|
||||
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 +38,29 @@ 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',
|
||||
),
|
||||
final controller = TextEditingController(text: actionHandler.keymap?.name);
|
||||
return DropdownMenu<Keymap>(
|
||||
controller: controller,
|
||||
dropdownMenuEntries:
|
||||
Keymap.values.map((key) => DropdownMenuEntry<Keymap>(value: key, label: key.toString())).toList(),
|
||||
onSelected: (keymap) async {
|
||||
if (keymap!.name == Keymap.custom.name) {
|
||||
keymap = await showCustomKeymapDialog(context, keymap: keymap);
|
||||
} else if (keymap.name == Keymap.myWhoosh.name && (!kIsWeb && Platform.isWindows)) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Use a Custom Keymap if you experience any issues on Windows')));
|
||||
}
|
||||
controller.text = keymap?.name ?? '';
|
||||
if (keymap == null) {
|
||||
return;
|
||||
}
|
||||
actionHandler.init(keymap);
|
||||
settings.setKeymap(keymap);
|
||||
onUpdate();
|
||||
},
|
||||
initialSelection: actionHandler.keymap,
|
||||
hintText: 'Keymap',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
43
lib/utils/settings/settings.dart
Normal file
43
lib/utils/settings/settings.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
97
lib/widgets/custom_keymap_selector.dart
Normal file
97
lib/widgets/custom_keymap_selector.dart
Normal 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();
|
||||
KeyDownEvent? _pressedKey;
|
||||
KeyDownEvent? _gearUpHotkey;
|
||||
KeyDownEvent? _gearDownHotkey;
|
||||
|
||||
String _mode = 'up'; // 'up' or 'down'
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
void _onKey(KeyEvent event) {
|
||||
setState(() {
|
||||
if (event is KeyDownEvent) {
|
||||
_pressedKey = event;
|
||||
} else if (event is KeyUpEvent) {
|
||||
if (_pressedKey != null) {
|
||||
if (_mode == 'up') {
|
||||
_gearUpHotkey = _pressedKey;
|
||||
_mode = 'down';
|
||||
} else {
|
||||
_gearDownHotkey = _pressedKey;
|
||||
widget.keymap.increase = KeyPair(
|
||||
physicalKey: _gearUpHotkey!.physicalKey,
|
||||
logicalKey: _gearUpHotkey!.logicalKey,
|
||||
);
|
||||
widget.keymap.decrease = KeyPair(
|
||||
physicalKey: _gearDownHotkey!.physicalKey,
|
||||
logicalKey: _gearDownHotkey!.logicalKey,
|
||||
);
|
||||
Navigator.of(context).pop(widget.keymap);
|
||||
}
|
||||
_pressedKey = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String _formatKey(KeyDownEvent? key) {
|
||||
return key?.logicalKey.keyLabel ?? 'Not set';
|
||||
}
|
||||
|
||||
@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(_formatKey(_gearUpHotkey)),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.arrow_downward),
|
||||
title: Text("Gear Down Hotkey"),
|
||||
subtitle: Text(_formatKey(_gearDownHotkey)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [TextButton(onPressed: () => Navigator.of(context).pop(null), child: Text("Cancel"))],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
|
||||
@@ -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: () {
|
||||
|
||||
94
lib/widgets/title.dart
Normal file
94
lib/widgets/title.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:swift_control/widgets/small_progress_indicator.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
String? _latestVersionUrlValue;
|
||||
PackageInfo? _packageInfoValue;
|
||||
|
||||
class AppTitle extends StatefulWidget {
|
||||
const AppTitle({super.key});
|
||||
|
||||
@override
|
||||
State<AppTitle> createState() => _AppTitleState();
|
||||
}
|
||||
|
||||
class _AppTitleState extends State<AppTitle> {
|
||||
Future<String?> getLatestVersionUrlIfNewer() async {
|
||||
final response = await http.get(Uri.parse('https://api.github.com/repos/jonasbark/swiftcontrol/releases/latest'));
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
final latestVersion = data['tag_name'].split('+').first;
|
||||
final currentVersion = 'v${_packageInfoValue!.version}';
|
||||
|
||||
if (latestVersion != null && latestVersion != currentVersion) {
|
||||
final assets = data['assets'] as List;
|
||||
if (Platform.isAndroid) {
|
||||
final apkUrl = assets.firstOrNullWhere((asset) => asset['name'].endsWith('.apk'))['browser_download_url'];
|
||||
return apkUrl;
|
||||
} else if (Platform.isMacOS) {
|
||||
final dmgUrl =
|
||||
assets.firstOrNullWhere((asset) => asset['name'].endsWith('.macos.zip'))['browser_download_url'];
|
||||
return dmgUrl;
|
||||
} else if (Platform.isWindows) {
|
||||
final appImageUrl =
|
||||
assets.firstOrNullWhere((asset) => asset['name'].endsWith('.windows.zip'))['browser_download_url'];
|
||||
return appImageUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_packageInfoValue == null) {
|
||||
PackageInfo.fromPlatform().then((value) {
|
||||
setState(() {
|
||||
_packageInfoValue = value;
|
||||
});
|
||||
_loadLatestVersionUrl();
|
||||
});
|
||||
} else {
|
||||
_loadLatestVersionUrl();
|
||||
}
|
||||
}
|
||||
|
||||
void _loadLatestVersionUrl() async {
|
||||
if (_latestVersionUrlValue == null && !kIsWeb) {
|
||||
final url = await getLatestVersionUrlIfNewer();
|
||||
if (url != null && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('New version available: ${url.split("/").takeLast(2).first.split('%').first}'),
|
||||
duration: Duration(seconds: 1337),
|
||||
action: SnackBarAction(
|
||||
label: 'Download',
|
||||
onPressed: () {
|
||||
launchUrlString(url);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text('SwiftControl'),
|
||||
if (_packageInfoValue != null) Text('v${_packageInfoValue!.version}') else SmallProgressIndicator(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
||||
@@ -5,14 +5,20 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import file_selector_macos
|
||||
import flutter_local_notifications
|
||||
import keypress_simulator_macos
|
||||
import package_info_plus
|
||||
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"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
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
|
||||
- package_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- universal_ble (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -11,28 +18,40 @@ 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`)
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/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
|
||||
package_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/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
|
||||
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
|
||||
|
||||
@@ -10,5 +10,9 @@
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
<dict>
|
||||
<key>com.apple.security.device.bluetooth</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
242
pubspec.lock
242
pubspec.lock
@@ -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
|
||||
@@ -232,7 +288,7 @@ packages:
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||
@@ -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,30 @@ 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"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -367,6 +511,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 +591,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 +631,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
|
||||
@@ -636,6 +868,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.12.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -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.6+0
|
||||
version: 1.1.6+0
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.0
|
||||
@@ -16,11 +16,15 @@ 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
|
||||
package_info_plus: ^8.3.0
|
||||
accessibility:
|
||||
path: accessibility
|
||||
http: ^1.3.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
keypress_simulator_windows
|
||||
permission_handler_windows
|
||||
universal_ble
|
||||
|
||||
Reference in New Issue
Block a user