mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7bfd8c206 | ||
|
|
ff83e5271b | ||
|
|
ec6edb2864 | ||
|
|
4f4a6f60c5 | ||
|
|
354e13678b | ||
|
|
f1b8822e20 | ||
|
|
6bf83b1034 | ||
|
|
7b1e4ede2a | ||
|
|
a554820115 | ||
|
|
cb9f9ea5b3 | ||
|
|
4051553a56 | ||
|
|
01a213354b | ||
|
|
962abfb38e | ||
|
|
ada4cf0dfd | ||
|
|
aff1137c3d | ||
|
|
7f24c27201 | ||
|
|
51c5e34220 | ||
|
|
10c2cc64a2 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -4,6 +4,12 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- 'lib/**'
|
||||
- 'accessibility/**'
|
||||
- 'keypress_simulator/**'
|
||||
- 'pubspec.yaml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,4 +1,28 @@
|
||||
#### 2.0.0 (2025-04-06)
|
||||
### 2.0.7 (2025-04-18)
|
||||
- add Biketerra.com keymap
|
||||
- some UX improvements
|
||||
|
||||
### 2.0.6 (2025-04-15)
|
||||
- fix MyWhoosh up / downshift button assignment (I key vs K key)
|
||||
|
||||
### 2.0.5 (2025-04-13)
|
||||
- fix Zwift Click button assignment (#12)
|
||||
|
||||
### 2.0.4 (2025-04-10)
|
||||
- vibrate Zwift Play / Zwift Ride controller on gear shift (thanks @cagnulein, closes #16)
|
||||
|
||||
### 2.0.3 (2025-04-08)
|
||||
- adjust TrainingPeaks Virtual key mapping (#12)
|
||||
- attempt to reconnect device if connection is lost
|
||||
- Android: detect freeform windows for MyWhoosh + TrainingPeaks Virtual keymaps
|
||||
|
||||
### 2.0.2 (2025-04-07)
|
||||
- fix bluetooth scan issues on older Android devices by asking for location permission
|
||||
|
||||
### 2.0.1 (2025-04-06)
|
||||
- long pressing a button will trigger the action again every 250ms
|
||||
|
||||
### 2.0.0 (2025-04-06)
|
||||
- You can now customize the actions (touches, mouse clicks or keyboard keys) for all buttons on all supported Zwift devices
|
||||
- now shows the battery level of the connected devices
|
||||
- add more troubleshooting information
|
||||
|
||||
14
README.md
14
README.md
@@ -4,7 +4,12 @@
|
||||
|
||||
## Description
|
||||
|
||||
With SwiftControl you can control your favorite trainer app using your Zwift Click, Zwift Ride or Zwift Play devices. Primarily useful to perform virtual gear shifting.
|
||||
With SwiftControl you can control your favorite trainer app using your Zwift Click, Zwift Ride or Zwift Play devices. Here's what you can do with it, depending on your configuration:
|
||||
- Virtual Gear shifting
|
||||
- Steering / turning
|
||||
- adjust workout intensity
|
||||
- control music on your device
|
||||
- more? If you can do it via keyboard, mouse or touch, you can do it with SwiftControl
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/1f81b674-1628-4763-ad66-5f3ed7a3f159
|
||||
@@ -18,6 +23,7 @@ Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
|
||||
## Supported Apps
|
||||
- MyWhoosh
|
||||
- indieVelo / Training Peaks
|
||||
- Biketerra.com
|
||||
- any other:
|
||||
- Android: you can customize simulated touch points of all your buttons in the app
|
||||
- Desktop: you can customize keyboard shortcuts and mouse clicks in the app
|
||||
@@ -30,7 +36,9 @@ Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
|
||||
## Supported Platforms
|
||||
- Android
|
||||
- macOS
|
||||
- Windows (make sure you have installed the "[Microsoft Visual C++ Runtime libraries](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)")
|
||||
- Windows
|
||||
- make sure you have installed the "[Microsoft Visual C++ Runtime libraries](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)"
|
||||
- Windows may flag the app as virus. I think it does so because the app does control the mouse and keyboard.
|
||||
- [Web](https://jonasbark.github.io/swiftcontrol/) (you won't be able to do much)
|
||||
|
||||
## Troubleshooting
|
||||
@@ -46,7 +54,7 @@ The app connects to your Zwift device automatically.
|
||||
- you can also use the mouse to click on a certain part of the screen, or use keyboard shortcuts
|
||||
|
||||
## Donate
|
||||
Please consider donating to support the development of this app.
|
||||
Please consider donating to support the development of this app :)
|
||||
|
||||
[](https://paypal.me/boni)
|
||||
|
||||
|
||||
@@ -4,12 +4,7 @@
|
||||
|
||||
|
||||
import android.util.Log
|
||||
import io.flutter.plugin.common.BasicMessageChannel
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MessageCodec
|
||||
import io.flutter.plugin.common.StandardMethodCodec
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import io.flutter.plugin.common.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
@@ -61,23 +56,29 @@ enum class MediaAction(val raw: Int) {
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class WindowEvent (
|
||||
val packageName: String,
|
||||
val windowHeight: Long,
|
||||
val windowWidth: Long
|
||||
val top: Long,
|
||||
val bottom: Long,
|
||||
val right: Long,
|
||||
val left: Long
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): WindowEvent {
|
||||
val packageName = pigeonVar_list[0] as String
|
||||
val windowHeight = pigeonVar_list[1] as Long
|
||||
val windowWidth = pigeonVar_list[2] as Long
|
||||
return WindowEvent(packageName, windowHeight, windowWidth)
|
||||
val top = pigeonVar_list[1] as Long
|
||||
val bottom = pigeonVar_list[2] as Long
|
||||
val right = pigeonVar_list[3] as Long
|
||||
val left = pigeonVar_list[4] as Long
|
||||
return WindowEvent(packageName, top, bottom, right, left)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
packageName,
|
||||
windowHeight,
|
||||
windowWidth,
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
left,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -88,8 +89,10 @@ data class WindowEvent (
|
||||
return true
|
||||
}
|
||||
return packageName == other.packageName
|
||||
&& windowHeight == other.windowHeight
|
||||
&& windowWidth == other.windowWidth
|
||||
&& top == other.top
|
||||
&& bottom == other.bottom
|
||||
&& right == other.right
|
||||
&& left == other.left
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
@@ -232,9 +235,9 @@ private class AccessibilityApiPigeonStreamHandler<T>(
|
||||
}
|
||||
|
||||
interface AccessibilityApiPigeonEventChannelWrapper<T> {
|
||||
open fun onListen(p0: Any?, sink: PigeonEventSink<T>) {}
|
||||
fun onListen(p0: Any?, sink: PigeonEventSink<T>) {}
|
||||
|
||||
open fun onCancel(p0: Any?) {}
|
||||
fun onCancel(p0: Any?) {}
|
||||
}
|
||||
|
||||
class PigeonEventSink<T>(private val sink: EventChannel.EventSink) {
|
||||
@@ -250,7 +253,7 @@ class PigeonEventSink<T>(private val sink: EventChannel.EventSink) {
|
||||
sink.endOfStream()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
abstract class StreamEventsStreamHandler : AccessibilityApiPigeonEventChannelWrapper<WindowEvent> {
|
||||
companion object {
|
||||
fun register(messenger: BinaryMessenger, streamHandler: StreamEventsStreamHandler, instanceName: String = "") {
|
||||
@@ -263,4 +266,4 @@ abstract class StreamEventsStreamHandler : AccessibilityApiPigeonEventChannelWra
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import StreamEventsStreamHandler
|
||||
import WindowEvent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
@@ -84,8 +85,8 @@ class EventListener : StreamEventsStreamHandler(), Receiver {
|
||||
eventSink = null
|
||||
}
|
||||
|
||||
override fun onChange(packageName: String, windowWidth: Int, windowHeight: Int) {
|
||||
eventSink?.success(WindowEvent(packageName = packageName, windowWidth = windowWidth.toLong(), windowHeight = windowHeight.toLong()))
|
||||
override fun onChange(packageName: String, window: Rect) {
|
||||
eventSink?.success(WindowEvent(packageName = packageName, right = window.right.toLong(), left = window.left.toLong(), bottom = window.bottom.toLong(), top = window.top.toLong()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.accessibilityservice.GestureDescription
|
||||
import android.accessibilityservice.GestureDescription.StrokeDescription
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
@@ -37,16 +36,12 @@ class AccessibilityService : AccessibilityService(), Listener {
|
||||
}
|
||||
val currentPackageName = event.packageName.toString()
|
||||
val windowSize = getWindowSize()
|
||||
Observable.fromService?.onChange(packageName = currentPackageName, windowHeight = windowSize.bottom, windowWidth = windowSize.right)
|
||||
Observable.fromService?.onChange(packageName = currentPackageName, window = windowSize)
|
||||
}
|
||||
|
||||
private fun getWindowSize(): Rect {
|
||||
val outBounds = Rect()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
rootInActiveWindow.getBoundsInWindow(outBounds)
|
||||
} else {
|
||||
rootInActiveWindow.getBoundsInScreen(outBounds)
|
||||
}
|
||||
rootInActiveWindow.getBoundsInScreen(outBounds)
|
||||
return outBounds
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.jonasbark.accessibility
|
||||
|
||||
import android.graphics.Rect
|
||||
|
||||
object Observable {
|
||||
var toService: Listener? = null
|
||||
var fromService: Receiver? = null
|
||||
@@ -10,5 +12,5 @@ interface Listener {
|
||||
}
|
||||
|
||||
interface Receiver {
|
||||
fun onChange(packageName: String, windowWidth: Int, windowHeight: Int)
|
||||
fun onChange(packageName: String, window: Rect)
|
||||
}
|
||||
@@ -15,10 +15,18 @@ enum MediaAction { playPause, next, volumeUp, volumeDown }
|
||||
|
||||
class WindowEvent {
|
||||
final String packageName;
|
||||
final int windowHeight;
|
||||
final int windowWidth;
|
||||
final int top;
|
||||
final int bottom;
|
||||
final int right;
|
||||
final int left;
|
||||
|
||||
WindowEvent({required this.packageName, required this.windowHeight, required this.windowWidth});
|
||||
WindowEvent({
|
||||
required this.packageName,
|
||||
required this.left,
|
||||
required this.right,
|
||||
required this.top,
|
||||
required this.bottom,
|
||||
});
|
||||
}
|
||||
|
||||
@EventChannelApi()
|
||||
|
||||
@@ -25,21 +25,29 @@ enum MediaAction {
|
||||
class WindowEvent {
|
||||
WindowEvent({
|
||||
required this.packageName,
|
||||
required this.windowHeight,
|
||||
required this.windowWidth,
|
||||
required this.top,
|
||||
required this.bottom,
|
||||
required this.right,
|
||||
required this.left,
|
||||
});
|
||||
|
||||
String packageName;
|
||||
|
||||
int windowHeight;
|
||||
int top;
|
||||
|
||||
int windowWidth;
|
||||
int bottom;
|
||||
|
||||
int right;
|
||||
|
||||
int left;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
packageName,
|
||||
windowHeight,
|
||||
windowWidth,
|
||||
top,
|
||||
bottom,
|
||||
right,
|
||||
left,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -50,8 +58,10 @@ class WindowEvent {
|
||||
result as List<Object?>;
|
||||
return WindowEvent(
|
||||
packageName: result[0]! as String,
|
||||
windowHeight: result[1]! as int,
|
||||
windowWidth: result[2]! as int,
|
||||
top: result[1]! as int,
|
||||
bottom: result[2]! as int,
|
||||
right: result[3]! as int,
|
||||
left: result[4]! as int,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,8 +76,10 @@ class WindowEvent {
|
||||
}
|
||||
return
|
||||
packageName == other.packageName
|
||||
&& windowHeight == other.windowHeight
|
||||
&& windowWidth == other.windowWidth;
|
||||
&& top == other.top
|
||||
&& bottom == other.bottom
|
||||
&& right == other.right
|
||||
&& left == other.left;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -23,6 +23,7 @@ class Constants {
|
||||
static const BC1 = 0x09;
|
||||
|
||||
static final RIDE_ON = Uint8List.fromList([0x52, 0x69, 0x64, 0x65, 0x4f, 0x6e]);
|
||||
static final VIBRATE_PATTERN = Uint8List.fromList([0x12, 0x12, 0x08, 0x0A, 0x06, 0x08, 0x02, 0x10, 0x00, 0x18]);
|
||||
|
||||
// these don't actually seem to matter, its just the header has to be 7 bytes RIDEON + 2
|
||||
static final REQUEST_START = Uint8List.fromList([0, 9]); //byteArrayOf(1, 2)
|
||||
|
||||
@@ -56,6 +56,7 @@ class Connection {
|
||||
|
||||
Future<void> performScanning() async {
|
||||
isScanning.value = true;
|
||||
_actionStreams.add(LogNotification('Scanning for devices...'));
|
||||
|
||||
// does not work on web, may not work on Windows
|
||||
if (!kIsWeb && !Platform.isWindows) {
|
||||
@@ -126,11 +127,21 @@ class Connection {
|
||||
final actionSubscription = bleDevice.actionStream.listen((data) {
|
||||
_actionStreams.add(data);
|
||||
});
|
||||
final connectionStateSubscription = UniversalBle.connectionStream(bleDevice.device.deviceId).listen((
|
||||
state,
|
||||
) async {
|
||||
final connectionStateSubscription = UniversalBle.connectionStream(bleDevice.device.deviceId).listen((state) {
|
||||
bleDevice.isConnected = state.isConnected;
|
||||
_connectionStreams.add(bleDevice);
|
||||
if (!bleDevice.isConnected) {
|
||||
devices.remove(bleDevice);
|
||||
_streamSubscriptions[bleDevice]?.cancel();
|
||||
_streamSubscriptions.remove(bleDevice);
|
||||
_connectionSubscriptions[bleDevice]?.cancel();
|
||||
_connectionSubscriptions.remove(bleDevice);
|
||||
_lastScanResult.clear();
|
||||
// try reconnect
|
||||
if (!isScanning.value) {
|
||||
performScanning();
|
||||
}
|
||||
}
|
||||
});
|
||||
_connectionSubscriptions[bleDevice] = connectionStateSubscription;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:swift_control/utils/single_line_exception.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
import '../../utils/crypto/encryption_utils.dart';
|
||||
import '../../utils/keymap/buttons.dart';
|
||||
import '../messages/notification.dart';
|
||||
|
||||
abstract class BaseDevice {
|
||||
@@ -26,6 +27,9 @@ abstract class BaseDevice {
|
||||
|
||||
bool supportsEncryption = true;
|
||||
|
||||
BleCharacteristic? syncRxCharacteristic;
|
||||
Timer? _longPressTimer;
|
||||
|
||||
List<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_CLICK;
|
||||
String get customServiceId => BleUuid.ZWIFT_CUSTOM_SERVICE_UUID;
|
||||
|
||||
@@ -111,7 +115,7 @@ abstract class BaseDevice {
|
||||
final syncTxCharacteristic = customService.characteristics.firstOrNullWhere(
|
||||
(characteristic) => characteristic.uuid == BleUuid.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID,
|
||||
);
|
||||
final syncRxCharacteristic = customService.characteristics.firstOrNullWhere(
|
||||
syncRxCharacteristic = customService.characteristics.firstOrNullWhere(
|
||||
(characteristic) => characteristic.uuid == BleUuid.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID,
|
||||
);
|
||||
|
||||
@@ -132,15 +136,15 @@ abstract class BaseDevice {
|
||||
BleInputProperty.indication,
|
||||
);
|
||||
|
||||
await _setupHandshake(syncRxCharacteristic);
|
||||
await _setupHandshake();
|
||||
}
|
||||
|
||||
Future<void> _setupHandshake(BleCharacteristic syncRxCharacteristic) async {
|
||||
Future<void> _setupHandshake() async {
|
||||
if (supportsEncryption) {
|
||||
await UniversalBle.writeValue(
|
||||
device.deviceId,
|
||||
customServiceId,
|
||||
syncRxCharacteristic.uuid,
|
||||
syncRxCharacteristic!.uuid,
|
||||
Uint8List.fromList([
|
||||
...Constants.RIDE_ON,
|
||||
...Constants.REQUEST_START,
|
||||
@@ -152,7 +156,7 @@ abstract class BaseDevice {
|
||||
await UniversalBle.writeValue(
|
||||
device.deviceId,
|
||||
customServiceId,
|
||||
syncRxCharacteristic.uuid,
|
||||
syncRxCharacteristic!.uuid,
|
||||
Constants.RIDE_ON,
|
||||
BleOutputProperty.withoutResponse,
|
||||
);
|
||||
@@ -230,12 +234,53 @@ abstract class BaseDevice {
|
||||
case Constants.CLICK_NOTIFICATION_MESSAGE_TYPE:
|
||||
case Constants.PLAY_NOTIFICATION_MESSAGE_TYPE:
|
||||
case Constants.RIDE_NOTIFICATION_MESSAGE_TYPE: // untested
|
||||
processClickNotification(message).then((_) {}).catchError((e) {
|
||||
actionStreamInternal.add(LogNotification(e.toString()));
|
||||
});
|
||||
processClickNotification(message)
|
||||
.then((buttonsClicked) async {
|
||||
if (buttonsClicked == null) {
|
||||
// ignore, no changes
|
||||
} else if (buttonsClicked.isEmpty) {
|
||||
actionStreamInternal.add(LogNotification('Buttons released'));
|
||||
_longPressTimer?.cancel();
|
||||
} else {
|
||||
if (!(buttonsClicked.singleOrNull == ZwiftButton.onOffLeft ||
|
||||
buttonsClicked.singleOrNull == ZwiftButton.onOffRight)) {
|
||||
// we don't want to trigger the long press timer for the on/off buttons
|
||||
_longPressTimer?.cancel();
|
||||
_longPressTimer = Timer.periodic(const Duration(milliseconds: 250), (timer) async {
|
||||
_performActions(buttonsClicked, true);
|
||||
});
|
||||
}
|
||||
|
||||
_performActions(buttonsClicked, false);
|
||||
}
|
||||
})
|
||||
.catchError((e) {
|
||||
actionStreamInternal.add(LogNotification(e.toString()));
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> processClickNotification(Uint8List message);
|
||||
Future<List<ZwiftButton>?> processClickNotification(Uint8List message);
|
||||
|
||||
Future<void> _performActions(List<ZwiftButton> buttonsClicked, bool repeated) async {
|
||||
if (!repeated &&
|
||||
buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp))) {
|
||||
await _vibrate();
|
||||
}
|
||||
for (final action in buttonsClicked) {
|
||||
actionStreamInternal.add(LogNotification(await actionHandler.performAction(action)));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _vibrate() async {
|
||||
final vibrateCommand = Uint8List.fromList([...Constants.VIBRATE_PATTERN, 0x20]);
|
||||
await UniversalBle.writeValue(
|
||||
device.deviceId,
|
||||
customServiceId,
|
||||
syncRxCharacteristic!.uuid,
|
||||
supportsEncryption ? zapEncryption.encrypt(vibrateCommand) : vibrateCommand,
|
||||
BleOutputProperty.withoutResponse,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:swift_control/bluetooth/devices/base_device.dart';
|
||||
import 'package:swift_control/bluetooth/messages/notification.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
|
||||
import '../messages/click_notification.dart';
|
||||
|
||||
@@ -11,19 +10,17 @@ class ZwiftClick extends BaseDevice {
|
||||
ClickNotification? _lastClickNotification;
|
||||
|
||||
@override
|
||||
Future<void> processClickNotification(Uint8List message) async {
|
||||
Future<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
|
||||
final ClickNotification clickNotification = ClickNotification(message);
|
||||
if (_lastClickNotification == null || _lastClickNotification != clickNotification) {
|
||||
_lastClickNotification = clickNotification;
|
||||
|
||||
if (clickNotification.buttonsClicked.isNotEmpty) {
|
||||
actionStreamInternal.add(clickNotification);
|
||||
}
|
||||
|
||||
final buttons = clickNotification.buttonsClicked;
|
||||
|
||||
for (final action in buttons) {
|
||||
actionStreamInternal.add(LogNotification(await actionHandler.performAction(action)));
|
||||
}
|
||||
return clickNotification.buttonsClicked;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:swift_control/bluetooth/devices/base_device.dart';
|
||||
import 'package:swift_control/bluetooth/messages/play_notification.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../ble.dart';
|
||||
import '../messages/notification.dart';
|
||||
|
||||
class ZwiftPlay extends BaseDevice {
|
||||
ZwiftPlay(super.scanResult);
|
||||
@@ -15,7 +16,7 @@ class ZwiftPlay extends BaseDevice {
|
||||
List<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_PLAY;
|
||||
|
||||
@override
|
||||
Future<void> processClickNotification(Uint8List message) async {
|
||||
Future<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
|
||||
final PlayNotification clickNotification = PlayNotification(message);
|
||||
if (_lastControllerNotification == null || _lastControllerNotification != clickNotification) {
|
||||
_lastControllerNotification = clickNotification;
|
||||
@@ -24,11 +25,9 @@ class ZwiftPlay extends BaseDevice {
|
||||
actionStreamInternal.add(clickNotification);
|
||||
}
|
||||
|
||||
final buttons = clickNotification.buttonsClicked;
|
||||
|
||||
for (final action in buttons) {
|
||||
actionStreamInternal.add(LogNotification(await actionHandler.performAction(action)));
|
||||
}
|
||||
return clickNotification.buttonsClicked;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ 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/keymap/buttons.dart';
|
||||
|
||||
import '../ble.dart';
|
||||
import '../messages/notification.dart';
|
||||
|
||||
class ZwiftRide extends BaseDevice {
|
||||
ZwiftRide(super.scanResult);
|
||||
@@ -19,19 +18,17 @@ class ZwiftRide extends BaseDevice {
|
||||
RideNotification? _lastControllerNotification;
|
||||
|
||||
@override
|
||||
Future<void> processClickNotification(Uint8List message) async {
|
||||
Future<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
|
||||
final RideNotification clickNotification = RideNotification(message);
|
||||
if (_lastControllerNotification == null || _lastControllerNotification != clickNotification) {
|
||||
_lastControllerNotification = clickNotification;
|
||||
|
||||
if (clickNotification.buttonsClicked.isNotEmpty) {
|
||||
actionStreamInternal.add(clickNotification);
|
||||
}
|
||||
|
||||
final buttons = clickNotification.buttonsClicked;
|
||||
|
||||
for (final action in buttons) {
|
||||
actionStreamInternal.add(LogNotification(await actionHandler.performAction(action)));
|
||||
}
|
||||
return clickNotification.buttonsClicked;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ class ClickNotification extends BaseNotification {
|
||||
ClickNotification(Uint8List message) {
|
||||
final status = ClickKeyPadStatus.fromBuffer(message);
|
||||
buttonsClicked = [
|
||||
if (status.buttonPlus == PlayButtonStatus.ON) ZwiftButton.shiftUpLeft,
|
||||
if (status.buttonMinus == PlayButtonStatus.ON) ZwiftButton.shiftDownRight,
|
||||
if (status.buttonPlus == PlayButtonStatus.ON) ZwiftButton.shiftUpRight,
|
||||
if (status.buttonMinus == PlayButtonStatus.ON) ZwiftButton.shiftDownLeft,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class SwiftPlayApp extends StatelessWidget {
|
||||
title: 'SwiftControl',
|
||||
theme: AppTheme.light,
|
||||
darkTheme: AppTheme.dark,
|
||||
themeMode: ThemeMode.system,
|
||||
themeMode: ThemeMode.dark,
|
||||
home: const RequirementsPage(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,9 +30,13 @@ class _ScanWidgetState extends State<ScanWidget> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// must be called from a button
|
||||
if (!kIsWeb) {
|
||||
Future.delayed(Duration(seconds: 1)).then((_) {
|
||||
connection.performScanning();
|
||||
});
|
||||
Future.delayed(Duration(seconds: 1))
|
||||
.then((_) {
|
||||
return connection.performScanning();
|
||||
})
|
||||
.catchError((e) {
|
||||
print(e);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:keypress_simulator/keypress_simulator.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
@@ -65,7 +66,7 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
windowManager.setFullScreen(true);
|
||||
}
|
||||
_actionSubscription = connection.actionStream.listen((data) {
|
||||
_actionSubscription = connection.actionStream.listen((data) async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
@@ -81,15 +82,24 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
|
||||
if (_pressedButton != null) {
|
||||
if (actionHandler.supportedApp!.keymap.getKeyPair(_pressedButton!) == null) {
|
||||
final KeyPair keyPair;
|
||||
actionHandler.supportedApp!.keymap.keyPairs.add(
|
||||
KeyPair(
|
||||
touchPosition: context.size!.center(Offset.zero),
|
||||
keyPair = KeyPair(
|
||||
touchPosition: context.size!
|
||||
.center(Offset.zero)
|
||||
.translate(actionHandler.supportedApp!.keymap.keyPairs.length * 40, 0),
|
||||
buttons: [_pressedButton!],
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
|
||||
// open menu
|
||||
if (Platform.isMacOS || Platform.isWindows) {
|
||||
await Future.delayed(Duration(milliseconds: 300));
|
||||
await keyPressSimulator.simulateMouseClick(keyPair.touchPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -122,16 +132,66 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (keyPair.physicalKey != null)
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
child: const Text('Use as touch button'),
|
||||
onTap: () {
|
||||
keyPair.physicalKey = null;
|
||||
PopupMenuItem(
|
||||
child: PopupMenuButton<PhysicalKeyboardKey>(
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaPlayPause,
|
||||
child: const Text('Media: Play/Pause'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaStop,
|
||||
child: const Text('Media: Stop'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaTrackPrevious,
|
||||
child: const Text('Media: Previous'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaTrackNext,
|
||||
child: const Text('Media: Next'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.audioVolumeUp,
|
||||
child: const Text('Media: Volume Up'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.audioVolumeDown,
|
||||
child: const Text('Media: Volume Down'),
|
||||
),
|
||||
],
|
||||
onSelected: (key) {
|
||||
keyPair.physicalKey = key;
|
||||
keyPair.logicalKey = null;
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
width: 180,
|
||||
child: Align(alignment: Alignment.centerLeft, child: Text('Set Media key')),
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
child: const Text('Use as touch button'),
|
||||
onTap: () {
|
||||
keyPair.physicalKey = null;
|
||||
keyPair.logicalKey = null;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
child: const Text('Remove'),
|
||||
onTap: () {
|
||||
actionHandler.supportedApp!.keymap.keyPairs.remove(keyPair);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
onSelected: (key) {
|
||||
keyPair.physicalKey = key;
|
||||
@@ -148,12 +208,8 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
color: Colors.transparent,
|
||||
child: _TouchDot(color: Colors.yellow, label: label, keyPair: keyPair),
|
||||
),
|
||||
onDragUpdate: (details) {
|
||||
print('Dragging: ${details.localPosition}');
|
||||
},
|
||||
childWhenDragging: const SizedBox.shrink(),
|
||||
onDraggableCanceled: (_, offset) {
|
||||
print('Drag canceled: ${offset}');
|
||||
setState(() => onPositionChanged(offset));
|
||||
},
|
||||
child: _TouchDot(color: color, label: label, keyPair: keyPair),
|
||||
@@ -271,17 +327,25 @@ class _TouchDot extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
Text(label, style: TextStyle(color: Colors.black, fontSize: 12)),
|
||||
if (keyPair.physicalKey != null)
|
||||
Text(switch (keyPair.physicalKey) {
|
||||
PhysicalKeyboardKey.mediaPlayPause => 'Media: Play/Pause',
|
||||
PhysicalKeyboardKey.mediaStop => 'Media: Stop',
|
||||
PhysicalKeyboardKey.mediaTrackPrevious => 'Media: Previous',
|
||||
PhysicalKeyboardKey.mediaTrackNext => 'Media: Next',
|
||||
PhysicalKeyboardKey.audioVolumeUp => 'Media: Volume Up',
|
||||
PhysicalKeyboardKey.audioVolumeDown => 'Media: Volume Down',
|
||||
_ => keyPair.logicalKey?.keyLabel ?? 'Unknown',
|
||||
}, style: TextStyle(color: Colors.grey, fontSize: 12)),
|
||||
Container(
|
||||
color: Colors.white.withAlpha(180),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: TextStyle(color: Colors.black, fontSize: 12)),
|
||||
if (keyPair.physicalKey != null)
|
||||
Text(switch (keyPair.physicalKey) {
|
||||
PhysicalKeyboardKey.mediaPlayPause => 'Media: Play/Pause',
|
||||
PhysicalKeyboardKey.mediaStop => 'Media: Stop',
|
||||
PhysicalKeyboardKey.mediaTrackPrevious => 'Media: Previous',
|
||||
PhysicalKeyboardKey.mediaTrackNext => 'Media: Next',
|
||||
PhysicalKeyboardKey.audioVolumeUp => 'Media: Volume Up',
|
||||
PhysicalKeyboardKey.audioVolumeDown => 'Media: Volume Down',
|
||||
_ => keyPair.logicalKey?.keyLabel ?? 'Unknown',
|
||||
}, style: TextStyle(color: Colors.black87, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ class AndroidActions extends BaseActions {
|
||||
}
|
||||
}
|
||||
final point = supportedApp!.resolveTouchPosition(action: button, windowInfo: windowInfo);
|
||||
accessibilityHandler.performTouch(point.dx, point.dy);
|
||||
if (point != Offset.zero) {
|
||||
accessibilityHandler.performTouch(point.dx, point.dy);
|
||||
return "No touch performed";
|
||||
}
|
||||
return "Touch performed at: ${point.dx.toInt()}, ${point.dy.toInt()}";
|
||||
}
|
||||
}
|
||||
|
||||
49
lib/utils/keymap/apps/biketerra.dart
Normal file
49
lib/utils/keymap/apps/biketerra.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'package:accessibility/accessibility.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
|
||||
|
||||
import '../buttons.dart';
|
||||
import '../keymap.dart';
|
||||
|
||||
class Biketerra extends SupportedApp {
|
||||
Biketerra()
|
||||
: super(
|
||||
name: 'Biketerra',
|
||||
packageName: "biketerra",
|
||||
keymap: Keymap(
|
||||
keyPairs: [
|
||||
KeyPair(
|
||||
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(),
|
||||
physicalKey: PhysicalKeyboardKey.keyS,
|
||||
logicalKey: LogicalKeyboardKey.keyS,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(),
|
||||
physicalKey: PhysicalKeyboardKey.keyW,
|
||||
logicalKey: LogicalKeyboardKey.keyW,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(),
|
||||
physicalKey: PhysicalKeyboardKey.arrowRight,
|
||||
logicalKey: LogicalKeyboardKey.arrowRight,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateLeft).toList(),
|
||||
physicalKey: PhysicalKeyboardKey.arrowLeft,
|
||||
logicalKey: LogicalKeyboardKey.arrowLeft,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(),
|
||||
physicalKey: PhysicalKeyboardKey.keyU,
|
||||
logicalKey: LogicalKeyboardKey.keyU,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
extension WindowSize on WindowEvent {
|
||||
int get width => right - left;
|
||||
int get height => bottom - top;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:accessibility/accessibility.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
|
||||
|
||||
import '../../single_line_exception.dart';
|
||||
@@ -16,13 +17,13 @@ class MyWhoosh extends SupportedApp {
|
||||
keyPairs: [
|
||||
KeyPair(
|
||||
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(),
|
||||
physicalKey: PhysicalKeyboardKey.keyK,
|
||||
logicalKey: LogicalKeyboardKey.keyK,
|
||||
physicalKey: PhysicalKeyboardKey.keyI,
|
||||
logicalKey: LogicalKeyboardKey.keyI,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(),
|
||||
physicalKey: PhysicalKeyboardKey.keyI,
|
||||
logicalKey: LogicalKeyboardKey.keyI,
|
||||
physicalKey: PhysicalKeyboardKey.keyK,
|
||||
logicalKey: LogicalKeyboardKey.keyK,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(),
|
||||
@@ -52,10 +53,44 @@ class MyWhoosh extends SupportedApp {
|
||||
if (windowInfo == null) {
|
||||
throw SingleLineException("Window size not known - open $this first");
|
||||
}
|
||||
|
||||
// just my personal preference
|
||||
switch (action) {
|
||||
case ZwiftButton.y:
|
||||
accessibilityHandler.controlMedia(MediaAction.volumeUp);
|
||||
return Offset.zero;
|
||||
case ZwiftButton.b:
|
||||
accessibilityHandler.controlMedia(MediaAction.volumeDown);
|
||||
return Offset.zero;
|
||||
case ZwiftButton.a:
|
||||
accessibilityHandler.controlMedia(MediaAction.next);
|
||||
return Offset.zero;
|
||||
case ZwiftButton.z:
|
||||
accessibilityHandler.controlMedia(MediaAction.playPause);
|
||||
return Offset.zero;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return switch (action.action) {
|
||||
InGameAction.shiftUp => Offset(windowInfo.windowWidth * 0.98, windowInfo.windowHeight * 0.94),
|
||||
InGameAction.shiftDown => Offset(windowInfo.windowWidth * 0.80, windowInfo.windowHeight * 0.94),
|
||||
InGameAction.shiftUp => Offset(
|
||||
windowInfo.right - windowInfo.width * 0.02,
|
||||
windowInfo.bottom - windowInfo.height * 0.06,
|
||||
),
|
||||
InGameAction.shiftDown => Offset(
|
||||
windowInfo.right - windowInfo.width * 0.20,
|
||||
windowInfo.bottom - windowInfo.height * 0.06,
|
||||
),
|
||||
InGameAction.navigateRight => Offset(
|
||||
windowInfo.right - windowInfo.width * 0.02,
|
||||
windowInfo.bottom - windowInfo.height * 0.20,
|
||||
),
|
||||
_ => throw SingleLineException("Unsupported action for MyWhoosh: $action"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension WindowSize on WindowEvent {
|
||||
int get width => right - left;
|
||||
int get height => bottom - top;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:accessibility/accessibility.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/biketerra.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/training_peaks.dart';
|
||||
|
||||
import '../../single_line_exception.dart';
|
||||
@@ -27,7 +28,7 @@ abstract class SupportedApp {
|
||||
|
||||
const SupportedApp({required this.name, required this.packageName, required this.keymap});
|
||||
|
||||
static final List<SupportedApp> supportedApps = [MyWhoosh(), TrainingPeaks(), CustomApp()];
|
||||
static final List<SupportedApp> supportedApps = [MyWhoosh(), TrainingPeaks(), Biketerra(), CustomApp()];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:accessibility/accessibility.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/my_whoosh.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/utils/single_line_exception.dart';
|
||||
@@ -17,13 +18,13 @@ class TrainingPeaks extends SupportedApp {
|
||||
// https://help.trainingpeaks.com/hc/en-us/articles/31340399556877-TrainingPeaks-Virtual-Controls-and-Keyboard-Shortcuts
|
||||
KeyPair(
|
||||
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(),
|
||||
physicalKey: PhysicalKeyboardKey.minus,
|
||||
logicalKey: LogicalKeyboardKey.minus,
|
||||
physicalKey: PhysicalKeyboardKey.numpadSubtract,
|
||||
logicalKey: LogicalKeyboardKey.numpadSubtract,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(),
|
||||
physicalKey: PhysicalKeyboardKey.equal,
|
||||
logicalKey: LogicalKeyboardKey.equal,
|
||||
physicalKey: PhysicalKeyboardKey.numpadAdd,
|
||||
logicalKey: LogicalKeyboardKey.numpadAdd,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(),
|
||||
@@ -64,8 +65,8 @@ class TrainingPeaks extends SupportedApp {
|
||||
throw SingleLineException("Window size not known - open $this first");
|
||||
}
|
||||
return switch (action.action) {
|
||||
InGameAction.shiftUp => Offset(windowInfo.windowWidth / 2 * 1.32, windowInfo.windowHeight * 0.74),
|
||||
InGameAction.shiftDown => Offset(windowInfo.windowWidth / 2 * 1.15, windowInfo.windowHeight * 0.74),
|
||||
InGameAction.shiftUp => Offset(windowInfo.width / 2 * 1.32, windowInfo.height * 0.74),
|
||||
InGameAction.shiftDown => Offset(windowInfo.width / 2 * 1.15, windowInfo.height * 0.74),
|
||||
_ => throw SingleLineException("Unsupported action for IndieVelo: $action"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,6 +34,21 @@ class BluetoothScanRequirement extends PlatformRequirement {
|
||||
}
|
||||
}
|
||||
|
||||
class LocationRequirement extends PlatformRequirement {
|
||||
LocationRequirement() : super('Allow Location so Bluetooth scan works');
|
||||
|
||||
@override
|
||||
Future<void> call() async {
|
||||
await Permission.locationWhenInUse.request();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> getStatus() async {
|
||||
final state = await Permission.locationWhenInUse.status;
|
||||
status = state.isGranted || state.isLimited;
|
||||
}
|
||||
}
|
||||
|
||||
class BluetoothConnectRequirement extends PlatformRequirement {
|
||||
BluetoothConnectRequirement() : super('Allow Bluetooth Connections');
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/utils/requirements/android.dart';
|
||||
@@ -29,12 +30,18 @@ Future<List<PlatformRequirement>> getRequirements() async {
|
||||
} else if (Platform.isWindows) {
|
||||
list = [BluetoothTurnedOn(), KeyboardRequirement(), BluetoothScanning()];
|
||||
} else if (Platform.isAndroid) {
|
||||
final deviceInfoPlugin = DeviceInfoPlugin();
|
||||
final deviceInfo = await deviceInfoPlugin.androidInfo;
|
||||
list = [
|
||||
BluetoothTurnedOn(),
|
||||
AccessibilityRequirement(),
|
||||
NotificationRequirement(),
|
||||
BluetoothScanRequirement(),
|
||||
BluetoothConnectRequirement(),
|
||||
if (deviceInfo.version.sdkInt <= 30)
|
||||
LocationRequirement()
|
||||
else ...[
|
||||
BluetoothScanRequirement(),
|
||||
BluetoothConnectRequirement(),
|
||||
],
|
||||
BluetoothScanning(),
|
||||
];
|
||||
} else {
|
||||
|
||||
@@ -99,43 +99,6 @@ class _HotKeyListenerState extends State<HotKeyListenerDialog> {
|
||||
children: [
|
||||
Text("Press a key on your keyboard to assign to ${_pressedButton.toString()}"),
|
||||
Text(_formatKey(_pressedKey)),
|
||||
PopupMenuButton<PhysicalKeyboardKey>(
|
||||
tooltip: 'Drag or click for special keys',
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaPlayPause,
|
||||
child: const Text('Media: Play/Pause'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaStop,
|
||||
child: const Text('Media: Stop'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaTrackPrevious,
|
||||
child: const Text('Media: Previous'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaTrackNext,
|
||||
child: const Text('Media: Next'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.audioVolumeUp,
|
||||
child: const Text('Media: Volume Up'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.audioVolumeDown,
|
||||
child: const Text('Media: Volume Down'),
|
||||
),
|
||||
],
|
||||
onSelected: (key) {
|
||||
widget.customApp.setKey(_pressedButton!, physicalKey: key, logicalKey: null);
|
||||
Navigator.pop(context, key);
|
||||
},
|
||||
child: IgnorePointer(
|
||||
child: ElevatedButton(onPressed: () {}, child: Text('Or choose special key')),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import device_info_plus
|
||||
import file_selector_macos
|
||||
import flutter_local_notifications
|
||||
import keypress_simulator_macos
|
||||
@@ -16,6 +17,7 @@ import url_launcher_macos
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
KeypressSimulatorMacosPlugin.register(with: registry.registrar(forPlugin: "KeypressSimulatorMacosPlugin"))
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
PODS:
|
||||
- device_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_selector_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_local_notifications (0.0.1):
|
||||
@@ -22,6 +24,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- 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`)
|
||||
@@ -34,6 +37,8 @@ DEPENDENCIES:
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
device_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||
file_selector_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||
flutter_local_notifications:
|
||||
@@ -56,6 +61,7 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
flutter_local_notifications: 4ccab5b7a22835214a6672e3f9c5e8ae207dab36
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
|
||||
24
pubspec.lock
24
pubspec.lock
@@ -128,6 +128,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
device_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "306b78788d1bb569edb7c55d622953c2414ca12445b41c9117963e03afc5c513"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.3.3"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -912,6 +928,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.12.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
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: 2.0.0+0
|
||||
version: 2.0.7+0
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.0
|
||||
@@ -19,6 +19,7 @@ dependencies:
|
||||
image_picker: ^1.1.2
|
||||
pointycastle: any
|
||||
window_manager: ^0.4.3
|
||||
device_info_plus: ^11.3.3
|
||||
keypress_simulator:
|
||||
path: keypress_simulator/packages/keypress_simulator
|
||||
shared_preferences: ^2.5.3
|
||||
|
||||
Reference in New Issue
Block a user