Compare commits

...

50 Commits

Author SHA1 Message Date
Jonas Bark
1c0e027abb Android: make sure the touch reassignment page is fullscreen 2025-04-02 12:20:56 +02:00
Jonas Bark
0dcb666bbd update changelog 2025-04-01 21:21:58 +02:00
Jonas Bark
00ebca7a01 Merge branch 'windows' 2025-04-01 21:20:25 +02:00
Jonas Bark
f9c8820303 version++ 2025-04-01 21:20:20 +02:00
Jonas Bark
3d414edda9 Zwift Ride: cleanup, fix button 2025-04-01 21:17:46 +02:00
Jonas Bark
0e3f6f1d5e Windows: implement keybd_event 2025-04-01 19:54:41 +02:00
Jonas Bark
c4b0ef38c0 Zwift Ride: only connect to the left controller 2025-04-01 19:38:52 +02:00
Jonas Bark
2e800bb2de add hint to install runtime libraries for windows 2025-04-01 11:48:38 +02:00
Jonas Bark
505b970497 show the connected device type if the bluetooth name is null 2025-04-01 09:43:08 +02:00
Jonas Bark
986bfd481c show the connected device type if the bluetooth name is null 2025-04-01 09:42:40 +02:00
Jonas Bark
ab8d480a01 change SendInput to use keybd_event instead 2025-04-01 09:39:41 +02:00
Jonas Bark
7e19b76403 integrate keypress_simulator to make changes 2025-04-01 09:25:05 +02:00
Jonas Bark
cbc2f103ac run connection sequentially - may fix issues on Windows 2025-04-01 09:21:31 +02:00
Jonas Bark
6a7922cf17 version++ 2025-03-31 21:00:37 +02:00
Jonas Bark
8e23de2718 version update check => internet permission is required 2025-03-31 20:58:27 +02:00
Jonas Bark
11aec5fba1 Zwift Ride: add buttonPowerDown to shift gears
Zwift Play: Fix buttonShift assignment
Android: fix Media next button
2025-03-31 20:20:47 +02:00
Jonas Bark
c98f213e2e add some comments 2025-03-30 21:34:29 +02:00
Jonas Bark
8c11cfcad6 remove location requirement again 2025-03-30 21:21:31 +02:00
Jonas Bark
5fe88ffc6a version++ 2025-03-30 21:18:38 +02:00
Jonas Bark
7a8c7a4ee1 fix #6 for real this time 2025-03-30 21:18:18 +02:00
Jonas Bark
3343325195 adjust reinstating saved keymaps 2025-03-30 20:06:44 +02:00
Jonas Bark
edda16dc06 adjust reinstating saved keymaps 2025-03-30 19:30:34 +02:00
Jonas Bark
7a3d120123 Keyboard: no point in catching modifiers 2025-03-30 18:58:38 +02:00
Jonas Bark
92419c9182 potential fix for #6 2025-03-30 18:44:55 +02:00
Jonas Bark
68bb5bf371 Zwift Ride: adjust on off button detection 2025-03-30 18:30:15 +02:00
Jonas Bark
b0d8bfcadd potential fix for #7 2025-03-30 18:29:01 +02:00
Jonas Bark
a58ad1daf6 potential fix for Bluetooth device detection 2025-03-30 16:53:34 +02:00
Jonas Bark
657c6056c4 potential fix for Bluetooth device detection 2025-03-30 16:08:03 +02:00
Jonas Bark
84daba8902 potential fix for Bluetooth device detection 2025-03-30 16:03:01 +02:00
Jonas Bark
3e37f8a269 update readme 2025-03-30 15:17:52 +02:00
Jonas Bark
28d178c4be update changelog + version 2025-03-30 15:04:00 +02:00
Jonas Bark
f560cd5930 don't call getSystemDevices on Web 2025-03-30 15:02:04 +02:00
Jonas Bark
dbf24c6cd3 fix touch placement coordinate systems (closes #4) 2025-03-30 15:00:24 +02:00
Jonas Bark
0a4989ca47 fix touch placement coordinate systems (closes #4) 2025-03-30 14:59:42 +02:00
Jonas Bark
507dbf5d0f cleanup code 2025-03-30 14:40:08 +02:00
Jonas Bark
536f36f4e7 update Zwift Ride decoding based on Feedback from @JayyajGH
fixes #3
2025-03-30 14:30:38 +02:00
Jonas Bark
c523ba2287 Android: allow user to adjust touch placements 2025-03-30 14:26:51 +02:00
Jonas Bark
a3f1cbb3b1 reconnect to existing BLE connection, also fallback to name only if manufacturerData isn't available 2025-03-30 14:09:41 +02:00
Jonas Bark
561bb2f0f4 allow adding custom keymap and store it 2025-03-30 13:36:40 +02:00
Jonas Bark
dbbd1b5f2c another potential keyboard fix, Zwift Play adjustment 2025-03-29 21:15:43 +01:00
Jonas Bark
7f6ec2f732 revert keyboard change 2025-03-29 19:48:51 +01:00
Jonas Bark
e8649203cf Zwift Ride: more work on https://github.com/jonasbark/swiftcontrol/issues/3#issuecomment-2763633261 2025-03-29 17:42:08 +01:00
Jonas Bark
765b0c3d6d Zwift Ride: more work on https://github.com/jonasbark/swiftcontrol/issues/3#issuecomment-2763633261 2025-03-29 17:38:41 +01:00
Jonas Bark
248c40731c potential fix for https://github.com/jonasbark/swiftcontrol/issues/1#issuecomment-2763382744 2025-03-29 15:12:10 +01:00
Jonas Bark
cde01a4863 fix build 2025-03-29 15:07:01 +01:00
Jonas Bark
1ee8a0188b try to resolve https://github.com/jonasbark/swiftcontrol/issues/3#issuecomment-2763361122 2025-03-29 14:54:16 +01:00
Jonas Bark
32e8fc9bf5 try to resolve https://github.com/jonasbark/swiftcontrol/issues/3#issuecomment-2763361122 2025-03-29 14:37:07 +01:00
Jonas Bark
3c87d895c5 fix missing permissions for Android 2025-03-29 14:34:39 +01:00
Jonas Bark
82dd8a9b48 sign Android APK 2025-03-29 14:16:17 +01:00
Jonas Bark
0996506fd1 attempt to fix #3 2025-03-29 13:49:22 +01:00
77 changed files with 2390 additions and 328 deletions

View File

@@ -71,6 +71,11 @@ jobs:
env:
DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY: ${{ secrets.DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY }}
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/android.keystore;
echo "${{ secrets.KEYSTORE_PROPERTIES }}" > android/keystore.properties;
#6 Building APK
- name: Build APK
run: flutter build apk --release

2
.gitignore vendored
View File

@@ -12,6 +12,8 @@
.swiftpm/
migrate_working_dir/
android/keystore.properties
# IntelliJ related
*.iml
*.ipr

View File

@@ -1,3 +1,45 @@
### 1.1.8 (2025-04-02)
- Android: make sure the touch reassignment page is fullscreen
### 1.1.7 (2025-04-01)
- Zwift Ride: fix connection issues by connecting only to the left controller
- Windows: connect sequentially to fix (finally?) fix connection issues
- Windows: change the way keyboard is simulated, should fix glitches
### 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
### 1.0.5 (2025-03-29)
- Zwift Ride: remap the shifter buttons to the correct values
### 1.0.0+4 (2025-03-29)
- Zwift Ride: attempt to fix button parsing
- Android: fix missing permissions
- Windows: potential fix for key press issues
### 1.0.0+3 (2025-03-29)
- Windows: fix connection by using a different Bluetooth stack (issue #1)

View File

@@ -18,7 +18,9 @@ Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
## Supported Apps
- MyWhoosh
- indieVelo / Training Peaks
- let me know if you know others that can benefit
- any other:
- Android: you can customize the gear shifting touch points in the app
- Desktop: you can customize the keyboard shortcuts in the app
## Supported Devices
- Zwift Click
@@ -28,7 +30,7 @@ Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
## Supported Platforms
- Android
- macOS
- Windows
- 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)")
- [Web](https://jonasbark.github.io/swiftcontrol/) (you won't be able to do much)
## How does it work?
@@ -43,6 +45,4 @@ Please consider donating to support the development of this app.
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/boni)
## TODO
- test Zwift Ride
- confirm that Windows release works
- implement more actions for Play + Ride

View File

@@ -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)
}

View File

@@ -9,8 +9,6 @@ import android.os.Build
import android.util.Log
import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED
import android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED
import android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
@@ -30,7 +28,6 @@ class AccessibilityService : AccessibilityService(), Listener {
private val ignorePackages = listOf("com.android.systemui", "com.android.launcher", "com.android.settings")
override fun onAccessibilityEvent(event: AccessibilityEvent) {
Log.w("Acc", "onAccessibilityEvent: ${event.packageName} ${event.eventType} ${event.contentChangeTypes}")
if (event.packageName == null || rootInActiveWindow == null) {
return
}

View File

@@ -1,3 +1,6 @@
import java.io.FileInputStream
import java.util.*
plugins {
id("com.android.application")
id("kotlin-android")
@@ -5,6 +8,11 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
android {
namespace = "de.jonasbark.swift_play"
compileSdk = flutter.compileSdkVersion
@@ -33,11 +41,18 @@ android {
versionName = flutter.versionName
}
signingConfigs {
create("config") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = file("../${keystoreProperties["storeFile"] as String}")
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
signingConfig = signingConfigs.getByName("config")
}
}
}

View File

@@ -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}"

View File

@@ -0,0 +1,12 @@
# Defines the Chromium style for automatic reformatting.
# http://clang.llvm.org/docs/ClangFormatStyleOptions.html
BasedOnStyle: Chromium
# This defaults to 'Auto'. Explicitly set it for a while, so that
# 'vector<vector<int> >' in existing files gets formatted to
# 'vector<vector<int>>'. ('Auto' means that clang-format will only use
# 'int>>' if the file already contains at least one such instance.)
Standard: Cpp11
SortIncludes: true
---
Language: ObjC
ColumnLimit: 100

View File

@@ -0,0 +1 @@
liberapay: lijy91

View File

@@ -0,0 +1,50 @@
name: build
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
jobs:
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.8"
channel: "stable"
- uses: bluefireteam/melos-action@v2
- working-directory: ./packages/keypress_simulator/example
run: |
melos bs
flutter build macos --release
build-web:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.8"
channel: "stable"
- uses: bluefireteam/melos-action@v2
- working-directory: ./packages/keypress_simulator/example
run: |
melos bs
flutter build web --release
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.8"
channel: "stable"
- uses: bluefireteam/melos-action@v2
- working-directory: ./packages/keypress_simulator/example
run: |
melos bs
flutter build windows --release

View File

@@ -0,0 +1,31 @@
name: lint
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.8"
channel: "stable"
- uses: bluefireteam/melos-action@v2
- run: melos run analyze
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.8"
channel: "stable"
cache: true
- uses: bluefireteam/melos-action@v2
- run: melos run format-check

View File

@@ -0,0 +1,20 @@
name: test
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.8"
channel: "stable"
cache: true
- uses: bluefireteam/melos-action@v2
- run: melos run test --no-select

6
keypress_simulator/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.dart_tool/
.idea/
*.iml
pubspec_overrides.yaml
pubspec.lock

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-2024 LiJianying <lijy91@foxmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,93 @@
> **🚀 快速发布您的应用**: 试试 [Fastforge](https://fastforge.dev) - 构建、打包和分发您的 Flutter 应用最简单的方式。
# keypress_simulator
[![pub version][pub-image]][pub-url] [![][discord-image]][discord-url]
[pub-image]: https://img.shields.io/pub/v/keypress_simulator.svg
[pub-url]: https://pub.dev/packages/keypress_simulator
[discord-image]: https://img.shields.io/discord/884679008049037342.svg
[discord-url]: https://discord.gg/zPa6EZ2jqb
这个插件允许 Flutter 桌面应用模拟按键操作。
---
[English](./README.md) | 简体中文
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [平台支持](#%E5%B9%B3%E5%8F%B0%E6%94%AF%E6%8C%81)
- [快速开始](#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
- [安装](#%E5%AE%89%E8%A3%85)
- [用法](#%E7%94%A8%E6%B3%95)
- [谁在用使用它?](#%E8%B0%81%E5%9C%A8%E7%94%A8%E4%BD%BF%E7%94%A8%E5%AE%83)
- [许可证](#%E8%AE%B8%E5%8F%AF%E8%AF%81)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 平台支持
| Linux | macOS | Windows |
| :---: | :---: | :-----: |
| | ✔️ | ✔️ |
## 快速开始
### 安装
将此添加到你的软件包的 pubspec.yaml 文件:
```yaml
dependencies:
keypress_simulator: ^0.2.0
```
### 用法
```dart
import 'package:keypress_simulator/keypress_simulator.dart';
// 1. Simulate pressing ⌘ + C
// 1.1 Simulate key down
await keyPressSimulator.simulateKeyDown(
PhysicalKeyboardKey.keyC,
[ModifierKey.metaModifier],
);
// 1.2 Simulate key up
await keyPressSimulator.simulateKeyUp(
PhysicalKeyboardKey.keyC,
[ModifierKey.metaModifier],
);
// 2. Simulate long pressing ⌘ + space
// 2.1. Simulate key down
await keyPressSimulator.simulateKeyDown(
PhysicalKeyboardKey.space,
[ModifierKey.metaModifier],
);
await Future.delayed(const Duration(seconds: 5));
// 2.2. Simulate key up
await keyPressSimulator.simulateKeyUp(
PhysicalKeyboardKey.space,
[ModifierKey.metaModifier],
);
```
> 请看这个插件的示例应用,以了解完整的例子。
## 谁在用使用它?
- [Biyi (比译)](https://biyidev.com/) - 一个便捷的翻译和词典应用程序。
## 许可证
[MIT](./LICENSE)

View File

@@ -0,0 +1,93 @@
> **🚀 Ship Your App Faster**: Try [Fastforge](https://fastforge.dev) - The simplest way to build, package and distribute your Flutter apps.
# keypress_simulator
[![pub version][pub-image]][pub-url] [![][discord-image]][discord-url]
[pub-image]: https://img.shields.io/pub/v/keypress_simulator.svg
[pub-url]: https://pub.dev/packages/keypress_simulator
[discord-image]: https://img.shields.io/discord/884679008049037342.svg
[discord-url]: https://discord.gg/zPa6EZ2jqb
This plugin allows Flutter desktop apps to simulate key presses.
---
English | [简体中文](./README-ZH.md)
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Platform Support](#platform-support)
- [Quick Start](#quick-start)
- [Installation](#installation)
- [Usage](#usage)
- [Who's using it?](#whos-using-it)
- [License](#license)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Platform Support
| Linux | macOS | Windows |
| :---: | :---: | :-----: |
| | ✔️ | ✔️ |
## Quick Start
### Installation
Add this to your package's pubspec.yaml file:
```yaml
dependencies:
keypress_simulator: ^0.2.0
```
### Usage
```dart
import 'package:keypress_simulator/keypress_simulator.dart';
// 1. Simulate pressing ⌘ + C
// 1.1 Simulate key down
await keyPressSimulator.simulateKeyDown(
PhysicalKeyboardKey.keyC,
[ModifierKey.metaModifier],
);
// 1.2 Simulate key up
await keyPressSimulator.simulateKeyUp(
PhysicalKeyboardKey.keyC,
[ModifierKey.metaModifier],
);
// 2. Simulate long pressing ⌘ + space
// 2.1. Simulate key down
await keyPressSimulator.simulateKeyDown(
PhysicalKeyboardKey.space,
[ModifierKey.metaModifier],
);
await Future.delayed(const Duration(seconds: 5));
// 2.2. Simulate key up
await keyPressSimulator.simulateKeyUp(
PhysicalKeyboardKey.space,
[ModifierKey.metaModifier],
);
```
> Please see the example app of this plugin for a full example.
## Who's using it?
- [Biyi (比译)](https://biyidev.com/) - A convenient translation and dictionary app.
## License
[MIT](./LICENSE)

View File

@@ -0,0 +1,35 @@
name: keypress_simulator_workspace
repository: https://github.com/leanflutter/keypress_simulator
packages:
- examples/**
- packages/**
command:
bootstrap:
# Uses the pubspec_overrides.yaml instead of having Melos modifying the lock file.
usePubspecOverrides: true
scripts:
analyze:
exec: flutter analyze --fatal-infos
description: Run `flutter analyze` for all packages.
test:
exec: flutter test
description: Run `flutter test` for a specific package.
packageFilters:
dirExists:
- test
format:
exec: dart format . --fix
description: Run `dart format` for all packages.
format-check:
exec: dart format . --fix --set-exit-if-changed
description: Run `dart format` checks for all packages.
fix:
exec: dart fix . --apply
description: Run `dart fix` for all packages.

View File

@@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "67457e669f79e9f8d13d7a68fe09775fefbb79f4"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
- platform: windows
create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -0,0 +1,3 @@
## 0.2.0
* First release.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-2024 LiJianying <lijy91@foxmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,12 @@
# keypress_simulator_windows
[![pub version][pub-image]][pub-url]
[pub-image]: https://img.shields.io/pub/v/keypress_simulator_windows.svg
[pub-url]: https://pub.dev/packages/keypress_simulator_windows
The Windows implementation of [keypress_simulator](https://pub.dev/packages/keypress_simulator).
## License
[MIT](./LICENSE)

View File

@@ -0,0 +1 @@
include: package:mostly_reasonable_lints/flutter.yaml

View File

@@ -0,0 +1,25 @@
name: keypress_simulator_windows
description: Windows implementation of the keypress_simulator plugin.
version: 0.2.0
repository: https://github.com/leanflutter/keypress_simulator/tree/main/packages/keypress_simulator_windows
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
keypress_simulator_platform_interface: ^0.2.0
dev_dependencies:
flutter_test:
sdk: flutter
mostly_reasonable_lints: ^0.1.1
flutter:
plugin:
implements: keypress_simulator
platforms:
windows:
pluginClass: KeypressSimulatorWindowsPluginCApi

View File

@@ -0,0 +1,17 @@
flutter/
# Visual Studio user-specific files.
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio build-related files.
x64/
x86/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/

View File

@@ -0,0 +1,100 @@
# The Flutter tooling requires that developers have a version of Visual Studio
# installed that includes CMake 3.14 or later. You should not increase this
# version, as doing so will cause the plugin to fail to compile for some
# customers of the plugin.
cmake_minimum_required(VERSION 3.14)
# Project-level configuration.
set(PROJECT_NAME "keypress_simulator_windows")
project(${PROJECT_NAME} LANGUAGES CXX)
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(VERSION 3.14...3.25)
# This value is used when generating builds using this plugin, so it must
# not be changed
set(PLUGIN_NAME "keypress_simulator_windows_plugin")
# Any new source files that you add to the plugin should be added here.
list(APPEND PLUGIN_SOURCES
"keypress_simulator_windows_plugin.cpp"
"keypress_simulator_windows_plugin.h"
)
# Define the plugin library target. Its name must not be changed (see comment
# on PLUGIN_NAME above).
add_library(${PLUGIN_NAME} SHARED
"include/keypress_simulator_windows/keypress_simulator_windows_plugin_c_api.h"
"keypress_simulator_windows_plugin_c_api.cpp"
${PLUGIN_SOURCES}
)
# Apply a standard set of build settings that are configured in the
# application-level CMakeLists.txt. This can be removed for plugins that want
# full control over build settings.
apply_standard_settings(${PLUGIN_NAME})
# Symbols are hidden by default to reduce the chance of accidental conflicts
# between plugins. This should not be removed; any symbols that should be
# exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro.
set_target_properties(${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
# Source include directories and library dependencies. Add any plugin-specific
# dependencies here.
target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)
# List of absolute paths to libraries that should be bundled with the plugin.
# This list could contain prebuilt libraries, or libraries created by an
# external build triggered from this build file.
set(keypress_simulator_windows_bundled_libraries
""
PARENT_SCOPE
)
# === Tests ===
# These unit tests can be run from a terminal after building the example, or
# from Visual Studio after opening the generated solution file.
# Only enable test builds when building the example (which sets this variable)
# so that plugin clients aren't building the tests.
if (${include_${PROJECT_NAME}_tests})
set(TEST_RUNNER "${PROJECT_NAME}_test")
enable_testing()
# Add the Google Test dependency.
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/release-1.11.0.zip
)
# Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
# Disable install commands for gtest so it doesn't end up in the bundle.
set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE)
FetchContent_MakeAvailable(googletest)
# The plugin's C API is not very useful for unit testing, so build the sources
# directly into the test binary rather than using the DLL.
add_executable(${TEST_RUNNER}
test/keypress_simulator_windows_plugin_test.cpp
${PLUGIN_SOURCES}
)
apply_standard_settings(${TEST_RUNNER})
target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin)
target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock)
# flutter_wrapper_plugin has link dependencies on the Flutter DLL.
add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${FLUTTER_LIBRARY}" $<TARGET_FILE_DIR:${TEST_RUNNER}>
)
# Enable automatic test discovery.
include(GoogleTest)
gtest_discover_tests(${TEST_RUNNER})
endif()

View File

@@ -0,0 +1,23 @@
#ifndef FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_C_API_H_
#define FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_C_API_H_
#include <flutter_plugin_registrar.h>
#ifdef FLUTTER_PLUGIN_IMPL
#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport)
#else
#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport)
#endif
#if defined(__cplusplus)
extern "C" {
#endif
FLUTTER_PLUGIN_EXPORT void KeypressSimulatorWindowsPluginCApiRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar);
#if defined(__cplusplus)
} // extern "C"
#endif
#endif // FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_C_API_H_

View File

@@ -0,0 +1,98 @@
#include "keypress_simulator_windows_plugin.h"
// This must be included before many other Windows headers.
#include <windows.h>
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <memory>
#include <sstream>
using flutter::EncodableList;
using flutter::EncodableMap;
using flutter::EncodableValue;
namespace keypress_simulator_windows {
// static
void KeypressSimulatorWindowsPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarWindows* registrar) {
auto channel =
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
registrar->messenger(), "dev.leanflutter.plugins/keypress_simulator",
&flutter::StandardMethodCodec::GetInstance());
auto plugin = std::make_unique<KeypressSimulatorWindowsPlugin>();
channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto& call, auto result) {
plugin_pointer->HandleMethodCall(call, std::move(result));
});
registrar->AddPlugin(std::move(plugin));
}
KeypressSimulatorWindowsPlugin::KeypressSimulatorWindowsPlugin() {}
KeypressSimulatorWindowsPlugin::~KeypressSimulatorWindowsPlugin() {}
void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
const EncodableMap& args = std::get<EncodableMap>(*method_call.arguments());
UINT keyCode = std::get<int>(args.at(EncodableValue("keyCode")));
std::vector<std::string> modifiers;
bool keyDown = std::get<bool>(args.at(EncodableValue("keyDown")));
EncodableList key_modifier_list =
std::get<EncodableList>(args.at(EncodableValue("modifiers")));
for (flutter::EncodableValue key_modifier_value : key_modifier_list) {
std::string key_modifier = std::get<std::string>(key_modifier_value);
modifiers.push_back(key_modifier);
}
INPUT input[6];
for (int32_t i = 0; i < modifiers.size(); i++) {
if (modifiers[i].compare("shiftModifier") == 0) {
input[i].ki.wVk = VK_SHIFT;
} else if (modifiers[i].compare("controlModifier") == 0) {
input[i].ki.wVk = VK_CONTROL;
} else if (modifiers[i].compare("altModifier") == 0) {
input[i].ki.wVk = VK_MENU;
} else if (modifiers[i].compare("metaModifier") == 0) {
input[i].ki.wVk = VK_LWIN;
}
input[i].ki.dwFlags = keyDown ? 0 : KEYEVENTF_KEYUP;
input[i].type = INPUT_KEYBOARD;
}
/*int keyIndex = static_cast<int>(modifiers.size());
input[keyIndex].ki.wVk = static_cast<WORD>(keyCode);
input[keyIndex].ki.dwFlags = keyDown ? 0 : KEYEVENTF_KEYUP;
input[keyIndex].type = INPUT_KEYBOARD;*/
// Send key sequence to system
//SendInput(static_cast<UINT>(std::size(input)), input, sizeof(INPUT));
BYTE byteValue = static_cast<BYTE>(keyCode);
keybd_event(byteValue, 0x45, keyDown ? 0 : KEYEVENTF_KEYUP, 0);
result->Success(flutter::EncodableValue(true));
}
void KeypressSimulatorWindowsPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
if (method_call.method_name().compare("simulateKeyPress") == 0) {
SimulateKeyPress(method_call, std::move(result));
} else {
result->NotImplemented();
}
}
} // namespace keypress_simulator_windows

View File

@@ -0,0 +1,37 @@
#ifndef FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_H_
#define FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_H_
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <memory>
namespace keypress_simulator_windows {
class KeypressSimulatorWindowsPlugin : public flutter::Plugin {
public:
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar);
KeypressSimulatorWindowsPlugin();
virtual ~KeypressSimulatorWindowsPlugin();
// Disallow copy and assign.
KeypressSimulatorWindowsPlugin(const KeypressSimulatorWindowsPlugin&) =
delete;
KeypressSimulatorWindowsPlugin& operator=(
const KeypressSimulatorWindowsPlugin&) = delete;
void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
// Called when a method is called on this plugin's channel from Dart.
void HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
};
} // namespace keypress_simulator_windows
#endif // FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_H_

View File

@@ -0,0 +1,12 @@
#include "include/keypress_simulator_windows/keypress_simulator_windows_plugin_c_api.h"
#include <flutter/plugin_registrar_windows.h>
#include "keypress_simulator_windows_plugin.h"
void KeypressSimulatorWindowsPluginCApiRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar) {
keypress_simulator_windows::KeypressSimulatorWindowsPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarManager::GetInstance()
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
}

View File

@@ -0,0 +1,43 @@
#include <flutter/method_call.h>
#include <flutter/method_result_functions.h>
#include <flutter/standard_method_codec.h>
#include <gtest/gtest.h>
#include <windows.h>
#include <memory>
#include <string>
#include <variant>
#include "keypress_simulator_windows_plugin.h"
namespace keypress_simulator_windows {
namespace test {
namespace {
using flutter::EncodableMap;
using flutter::EncodableValue;
using flutter::MethodCall;
using flutter::MethodResultFunctions;
} // namespace
TEST(KeypressSimulatorWindowsPlugin, GetPlatformVersion) {
KeypressSimulatorWindowsPlugin plugin;
// Save the reply value from the success callback.
std::string result_string;
plugin.HandleMethodCall(
MethodCall("getPlatformVersion", std::make_unique<EncodableValue>()),
std::make_unique<MethodResultFunctions<>>(
[&result_string](const EncodableValue* result) {
result_string = std::get<std::string>(*result);
},
nullptr, nullptr));
// Since the exact string varies by host, just ensure that it's a string
// with the expected format.
EXPECT_TRUE(result_string.rfind("Windows ", 0) == 0);
}
} // namespace test
} // namespace keypress_simulator_windows

View File

@@ -0,0 +1,9 @@
name: keypress_simulator_workspace
homepage: https://github.com/leanflutter/keypress_simulator
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
dev_dependencies:
melos: ^3.1.0

View File

@@ -15,6 +15,10 @@ class Constants {
static const RC1_LEFT_SIDE = 0x03;
static const RC1_RIGHT_SIDE = 0x02;
// Zwift Ride
static const RIDE_RIGHT_SIDE = 0x07;
static const RIDE_LEFT_SIDE = 0x08;
// Zwift Click = BC1
static const BC1 = 0x09;
@@ -33,7 +37,7 @@ class Constants {
// not figured out the protobuf type this really is, the content is just two varints.
static const int CLICK_NOTIFICATION_MESSAGE_TYPE = 55;
static const int PLAY_NOTIFICATION_MESSAGE_TYPE = 7;
static const int RIDE_NOTIFICATION_MESSAGE_TYPE = 35;
static const int RIDE_NOTIFICATION_MESSAGE_TYPE = 35; // 0x23
// see this if connected to Core then Zwift connects to it. just one byte
static const DISCONNECT_MESSAGE_TYPE = 0xFE;
@@ -41,9 +45,10 @@ class Constants {
enum DeviceType {
click,
ride,
playLeft,
playRight;
playRight,
rideRight,
rideLeft;
@override
String toString() {
@@ -59,6 +64,10 @@ enum DeviceType {
return DeviceType.playLeft;
case Constants.RC1_RIGHT_SIDE:
return DeviceType.playRight;
case Constants.RIDE_RIGHT_SIDE:
return DeviceType.rideRight;
case Constants.RIDE_LEFT_SIDE:
return DeviceType.rideLeft;
}
return null;
}

View File

@@ -4,17 +4,20 @@ 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 {
final devices = <BaseDevice>[];
var androidNotificationsSetup = false;
final _connectionQueue = <BaseDevice>[];
var _handlingConnectionQueue = false;
final Map<BaseDevice, StreamSubscription<BaseNotification>> _streamSubscriptions = {};
final StreamController<BaseNotification> _actionStreams = StreamController<BaseNotification>.broadcast();
Stream<BaseNotification> get actionStream => _actionStreams.stream;
@@ -23,7 +26,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);
@@ -31,7 +34,7 @@ class Connection {
UniversalBle.onScanResult = (result) {
if (_lastScanResult.none((e) => e.deviceId == result.deviceId)) {
_lastScanResult.add(result);
_actionStreams.add(LogNotification('Found new devices: ${result.name}'));
_actionStreams.add(LogNotification('Found new device: ${result.name}'));
final scanResult = BaseDevice.fromScanResult(result);
if (scanResult != null) {
_addDevices([scanResult]);
@@ -53,6 +56,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) async {
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])),
@@ -69,9 +84,8 @@ class Connection {
final newDevices = dev.where((device) => !devices.contains(device)).toList();
devices.addAll(newDevices);
for (final device in newDevices) {
_connect(device).then((_) {});
}
_connectionQueue.addAll(newDevices);
_handleConnectionQueue();
hasDevices.value = devices.isNotEmpty;
if (devices.isNotEmpty && !androidNotificationsSetup && !kIsWeb && Platform.isAndroid) {
@@ -83,6 +97,30 @@ class Connection {
}
}
void _handleConnectionQueue() {
// windows apparently has issues when connecting to multiple devices at once, so don't
if (_connectionQueue.isNotEmpty && !_handlingConnectionQueue) {
_handlingConnectionQueue = true;
final device = _connectionQueue.removeAt(0);
_actionStreams.add(LogNotification('Connecting to: ${device.device.name ?? device.runtimeType}'));
_connect(device)
.then((_) {
_handlingConnectionQueue = false;
_actionStreams.add(LogNotification('Connection finished: ${device.device.name ?? device.runtimeType}'));
if (_connectionQueue.isNotEmpty) {
_handleConnectionQueue();
}
})
.catchError((e) {
_handlingConnectionQueue = false;
_actionStreams.add(LogNotification('Connection failed: ${device.device.name ?? device.runtimeType} - $e'));
if (_connectionQueue.isNotEmpty) {
_handleConnectionQueue();
}
});
}
}
Future<void> _connect(BaseDevice bleDevice) async {
try {
final actionSubscription = bleDevice.actionStream.listen((data) {

View File

@@ -1,26 +1,98 @@
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), special case for Zwift Ride: we must only connect to the left controller
// https://www.makinolo.com/blog/2024/07/26/zwift-ride-protocol/
'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), // see comment above
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 +149,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 +178,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 +217,5 @@ class ZwiftClick extends BaseDevice {
}
}
void _processDevicePublicKeyResponse(Uint8List bytes) {
final devicePublicKeyBytes = bytes.sublist(Constants.RIDE_ON.length + Constants.RESPONSE_START_CLICK.length);
zapEncryption.initialise(devicePublicKeyBytes);
if (kDebugMode) {
print("Device Public Key - ${devicePublicKeyBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}");
}
}
void processClickNotification(Uint8List message) {
final ClickNotification clickNotification = ClickNotification(message);
if (_lastClickNotification == null || _lastClickNotification != clickNotification) {
_lastClickNotification = clickNotification;
actionStreamInternal.add(clickNotification);
if (clickNotification.buttonUp) {
actionHandler.increaseGear();
} else if (clickNotification.buttonDown) {
actionHandler.decreaseGear();
}
}
}
void processClickNotification(Uint8List message);
}

View File

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

View File

@@ -1,29 +1,31 @@
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/controller_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);
ControllerNotification? _lastControllerNotification;
PlayNotification? _lastControllerNotification;
@override
List<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_PLAY;
@override
void processClickNotification(Uint8List message) {
final ControllerNotification clickNotification = ControllerNotification(message);
final PlayNotification clickNotification = PlayNotification(message);
if (_lastControllerNotification == null || _lastControllerNotification != clickNotification) {
_lastControllerNotification = clickNotification;
actionStreamInternal.add(clickNotification);
if (clickNotification.rightPad && clickNotification.analogLR.abs() == 100) {
if ((clickNotification.rightPad && clickNotification.buttonShift) ||
(clickNotification.rightPad && clickNotification.analogLR.abs() == 100)) {
actionHandler.increaseGear();
} else if (!clickNotification.rightPad && clickNotification.analogLR.abs() == 100) {
} else if ((!clickNotification.rightPad && clickNotification.buttonShift) ||
(!clickNotification.rightPad && clickNotification.analogLR.abs() == 100)) {
actionHandler.decreaseGear();
}
if (clickNotification.rightPad) {

View File

@@ -0,0 +1,49 @@
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 '../ble.dart';
class ZwiftRide extends BaseDevice {
ZwiftRide(super.scanResult);
@override
String get customServiceId => BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID;
@override
bool get supportsEncryption => false;
RideNotification? _lastControllerNotification;
@override
void processClickNotification(Uint8List message) {
final RideNotification clickNotification = RideNotification(message);
if (_lastControllerNotification == null || _lastControllerNotification != clickNotification) {
_lastControllerNotification = clickNotification;
actionStreamInternal.add(clickNotification);
if (clickNotification.buttonShiftDownLeft ||
clickNotification.buttonShiftUpLeft ||
clickNotification.buttonOnOffLeft ||
clickNotification.buttonPowerUpLeft) {
actionHandler.decreaseGear();
} else if (clickNotification.buttonShiftUpRight ||
clickNotification.buttonShiftDownRight ||
clickNotification.buttonOnOffRight ||
clickNotification.buttonPowerUpRight) {
actionHandler.increaseGear();
}
/*if (clickNotification.buttonA) {
actionHandler.controlMedia(MediaAction.next);
} else if (clickNotification.buttonY) {
actionHandler.controlMedia(MediaAction.volumeUp);
} else if (clickNotification.buttonB) {
actionHandler.controlMedia(MediaAction.volumeDown);
} else if (clickNotification.buttonZ) {
actionHandler.controlMedia(MediaAction.playPause);
}*/
}
}
}

View File

@@ -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

View File

@@ -1,25 +1,22 @@
import 'dart:typed_data';
import 'package:swift_control/utils/messages/notification.dart';
import '../../protocol/zwift.pb.dart';
class ControllerNotification extends BaseNotification {
static const int BTN_PRESSED = 0;
import 'package:swift_control/bluetooth/messages/notification.dart';
import 'package:swift_control/bluetooth/protocol/zwift.pb.dart';
class PlayNotification extends BaseNotification {
late bool rightPad, buttonY, buttonZ, buttonA, buttonB, buttonOn, buttonShift;
late int analogLR, analogUD;
ControllerNotification(Uint8List message) {
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;
}
@@ -41,7 +38,7 @@ class ControllerNotification extends BaseNotification {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ControllerNotification &&
other is PlayNotification &&
runtimeType == other.runtimeType &&
rightPad == other.rightPad &&
buttonY == other.buttonY &&

View File

@@ -0,0 +1,140 @@
import 'dart:typed_data';
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),
SHFT_UP_L_BTN(0x00100),
SHFT_DN_L_BTN(0x00200),
SHFT_UP_R_BTN(0x01000),
SHFT_DN_R_BTN(0x02000),
POWERUP_L_BTN(0x00400),
POWERUP_R_BTN(0x04000),
ONOFF_L_BTN(0x00800),
ONOFF_R_BTN(0x08000);
final int mask;
const _RideButtonMask(this.mask);
}
class RideNotification extends BaseNotification {
static const int BTN_PRESSED = 0;
late bool buttonLeft, buttonRight, buttonUp, buttonDown;
late bool buttonA, buttonB, buttonY, buttonZ;
late bool buttonShiftUpLeft, buttonShiftDownLeft;
late bool buttonShiftUpRight, buttonShiftDownRight;
late bool buttonPowerUpLeft, buttonPowerUpRight;
late bool buttonOnOffLeft, buttonOnOffRight;
int analogLR = 0, analogUD = 0;
RideNotification(Uint8List message) {
final status = RideKeyPadStatus.fromBuffer(message);
buttonLeft = status.buttonMap & _RideButtonMask.LEFT_BTN.mask == BTN_PRESSED;
buttonRight = status.buttonMap & _RideButtonMask.RIGHT_BTN.mask == BTN_PRESSED;
buttonUp = status.buttonMap & _RideButtonMask.UP_BTN.mask == BTN_PRESSED;
buttonDown = status.buttonMap & _RideButtonMask.DOWN_BTN.mask == BTN_PRESSED;
buttonA = status.buttonMap & _RideButtonMask.A_BTN.mask == BTN_PRESSED;
buttonB = status.buttonMap & _RideButtonMask.B_BTN.mask == BTN_PRESSED;
buttonY = status.buttonMap & _RideButtonMask.Y_BTN.mask == BTN_PRESSED;
buttonZ = status.buttonMap & _RideButtonMask.Z_BTN.mask == BTN_PRESSED;
buttonShiftUpLeft = status.buttonMap & _RideButtonMask.SHFT_UP_L_BTN.mask == BTN_PRESSED;
buttonShiftDownLeft = status.buttonMap & _RideButtonMask.SHFT_DN_L_BTN.mask == BTN_PRESSED;
buttonShiftUpRight = status.buttonMap & _RideButtonMask.SHFT_UP_R_BTN.mask == BTN_PRESSED;
buttonShiftDownRight = status.buttonMap & _RideButtonMask.SHFT_DN_R_BTN.mask == BTN_PRESSED;
buttonPowerUpLeft = status.buttonMap & _RideButtonMask.POWERUP_L_BTN.mask == BTN_PRESSED;
buttonPowerUpRight = status.buttonMap & _RideButtonMask.POWERUP_R_BTN.mask == BTN_PRESSED;
buttonOnOffLeft = status.buttonMap & _RideButtonMask.ONOFF_L_BTN.mask == BTN_PRESSED;
buttonOnOffRight = status.buttonMap & _RideButtonMask.ONOFF_R_BTN.mask == BTN_PRESSED;
for (final analogue in status.analogButtons.groupStatus) {
if (analogue.location == RideAnalogLocation.LEFT || analogue.location == RideAnalogLocation.RIGHT) {
analogLR = analogue.analogValue;
} else if (analogue.location == RideAnalogLocation.DOWN || analogue.location == RideAnalogLocation.UP) {
analogUD = analogue.analogValue;
}
}
}
@override
String toString() {
final allTrueParameters = [
if (buttonLeft) 'buttonLeft',
if (buttonRight) 'buttonRight',
if (buttonUp) 'buttonUp',
if (buttonDown) 'buttonDown',
if (buttonA) 'buttonA',
if (buttonB) 'buttonB',
if (buttonY) 'buttonY',
if (buttonZ) 'buttonZ',
if (buttonShiftUpLeft) 'buttonShiftUpLeft',
if (buttonShiftDownLeft) 'buttonShiftDownLeft',
if (buttonShiftUpRight) 'buttonShiftUpRight',
if (buttonShiftDownRight) 'buttonShiftDownRight',
if (buttonPowerUpLeft) 'buttonPowerUpLeft',
if (buttonPowerUpRight) 'buttonPowerUpRight',
if (buttonOnOffLeft) 'buttonOnOffLeft',
if (buttonOnOffRight) 'buttonOnOffRight',
];
return '{$allTrueParameters, analogLR: $analogLR, analogUD: $analogUD}';
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RideNotification &&
runtimeType == other.runtimeType &&
buttonLeft == other.buttonLeft &&
buttonRight == other.buttonRight &&
buttonUp == other.buttonUp &&
buttonDown == other.buttonDown &&
buttonA == other.buttonA &&
buttonB == other.buttonB &&
buttonY == other.buttonY &&
buttonZ == other.buttonZ &&
buttonShiftUpLeft == other.buttonShiftUpLeft &&
buttonShiftDownLeft == other.buttonShiftDownLeft &&
buttonShiftUpRight == other.buttonShiftUpRight &&
buttonShiftDownRight == other.buttonShiftDownRight &&
buttonPowerUpLeft == other.buttonPowerUpLeft &&
buttonPowerUpRight == other.buttonPowerUpRight &&
buttonOnOffLeft == other.buttonOnOffLeft &&
buttonOnOffRight == other.buttonOnOffRight &&
analogLR == other.analogLR &&
analogUD == other.analogUD;
@override
int get hashCode =>
buttonLeft.hashCode ^
buttonRight.hashCode ^
buttonUp.hashCode ^
buttonDown.hashCode ^
buttonA.hashCode ^
buttonB.hashCode ^
buttonY.hashCode ^
buttonZ.hashCode ^
buttonShiftUpLeft.hashCode ^
buttonShiftDownLeft.hashCode ^
buttonShiftUpRight.hashCode ^
buttonShiftDownRight.hashCode ^
buttonPowerUpLeft.hashCode ^
buttonPowerUpRight.hashCode ^
buttonOnOffLeft.hashCode ^
buttonOnOffRight.hashCode ^
analogLR.hashCode ^
analogUD.hashCode;
}

View File

@@ -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);

View File

@@ -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')

View File

@@ -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;
}

View File

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

View File

@@ -1,11 +1,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,21 +49,51 @@ 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) {
return "${it.device.name}: ${it.isConnected ? 'Connected' : 'Not connected'}";
return "${it.device.name ?? it.runtimeType}: ${it.isConnected ? 'Connected' : 'Not connected'}";
})}',
),
Divider(color: Theme.of(context).colorScheme.primary, height: 30),
if (!kIsWeb && (Platform.isAndroid || kDebugMode)) ...[
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(_) => TouchAreaSetupPage(
onSave: (gearUp, gearDown) {
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
final convertedGearUp =
gearUp.translate(touchAreaSize / 2, touchAreaSize / 2) * devicePixelRatio;
final convertedGearDown =
gearDown.translate(touchAreaSize / 2, touchAreaSize / 2) * devicePixelRatio;
print("Gear Up Position: $gearUp - converted: $convertedGearUp");
print("Gear Down Position: $gearDown - converted: $convertedGearDown");
actionHandler.updateTouchPositions(convertedGearUp, convertedGearDown);
settings.updateTouchPositions(convertedGearUp, convertedGearDown);
},
),
),
);
},
child: Text('Customize touch areas (optional)'),
),
],
Expanded(child: LogViewer()),
],
),

View File

@@ -4,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,
),
)

View File

@@ -30,7 +30,9 @@ class _ScanWidgetState extends State<ScanWidget> {
WidgetsBinding.instance.addPostFrameCallback((_) {
// must be called from a button
if (!kIsWeb) {
connection.performScanning();
Future.delayed(Duration(seconds: 1)).then((_) {
connection.performScanning();
});
}
});
}
@@ -57,11 +59,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"),
),
],
);
}
},

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

@@ -0,0 +1,186 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.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 dispose() {
super.dispose();
// Exit full screen
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
}
@override
void initState() {
super.initState();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky, overlays: []);
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: Padding(
padding: const EdgeInsets.all(16.0),
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.red,
label: "Gear ↑",
),
_buildDraggableArea(
position: _gearDownPos,
onPositionChanged: (newPos) => _gearDownPos = newPos,
color: Colors.green,
label: "Gear ↓",
),
Positioned(
top: 40,
right: 170,
child: ElevatedButton.icon(
onPressed: () {
_gearDownPos = Offset(100, 300);
_gearUpPos = Offset(200, 300);
setState(() {});
},
label: const Icon(Icons.lock_reset),
),
),
Positioned(
top: 40,
right: 20,
child: ElevatedButton.icon(
onPressed: _saveAndClose,
icon: const Icon(Icons.save),
label: const Text("Save & Close"),
),
),
],
),
);
}
}
class _TouchDot extends StatelessWidget {
final Color color;
final String label;
const _TouchDot({required this.color, required this.label});
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
width: touchAreaSize,
height: touchAreaSize,
decoration: BoxDecoration(
color: color.withOpacity(0.6),
shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: 2),
),
),
Text(label, style: TextStyle(color: Colors.black, fontSize: 12)),
],
);
}
}

View File

@@ -8,21 +8,33 @@ import 'package:swift_control/utils/keymap/keymap.dart';
class AndroidActions extends BaseActions {
static const MYWHOOSH_APP_PACKAGE = "com.mywhoosh.whooshgame";
static const TRAININGPEAKS_APP_PACKAGE = "com.indieVelo.client";
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) {
streamEvents().listen((windowEvent) {
windowInfo = windowEvent;
if (validPackageNames.contains(windowEvent.packageName)) {
windowInfo = windowEvent;
}
});
}
@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),
@@ -30,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),
@@ -45,6 +60,8 @@ class AndroidActions extends BaseActions {
};
accessibilityHandler.performTouch(point.dx, point.dy);
} else {
accessibilityHandler.performTouch(_gearUpTouchPosition!.dx, _gearUpTouchPosition!.dy);
}
}
@@ -52,4 +69,10 @@ class AndroidActions extends BaseActions {
void controlMedia(MediaAction action) {
accessibilityHandler.controlMedia(action);
}
@override
void updateTouchPositions(Offset gearUp, Offset gearDown) {
_gearUpTouchPosition = gearUp;
_gearDownTouchPosition = gearDown;
}
}

View File

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

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
import 'package:swift_control/utils/devices/zwift_play.dart';
import '../ble.dart';
class ZwiftRide extends ZwiftPlay {
ZwiftRide(super.scanResult);
@override
String get customServiceId => BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID;
@override
bool get supportsEncryption => false;
}

View File

@@ -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});
}

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/requirements/platform.dart';
@@ -18,6 +19,36 @@ class AccessibilityRequirement extends PlatformRequirement {
}
}
class BluetoothScanRequirement extends PlatformRequirement {
BluetoothScanRequirement() : super('Allow Bluetooth Scan');
@override
Future<void> call() async {
await Permission.bluetoothScan.request();
}
@override
Future<void> getStatus() async {
final state = await Permission.bluetoothScan.status;
status = state.isGranted || state.isLimited;
}
}
class BluetoothConnectRequirement extends PlatformRequirement {
BluetoothConnectRequirement() : super('Allow Bluetooth Connections');
@override
Future<void> call() async {
await Permission.bluetoothConnect.request();
}
@override
Future<void> getStatus() async {
final state = await Permission.bluetoothConnect.status;
status = state.isGranted || state.isLimited;
}
}
class NotificationRequirement extends PlatformRequirement {
NotificationRequirement() : super('Allow adding persistent Notification');

View File

@@ -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',
);
}
}

View File

@@ -29,7 +29,14 @@ Future<List<PlatformRequirement>> getRequirements() async {
} else if (Platform.isWindows) {
list = [BluetoothTurnedOn(), KeyboardRequirement(), KeymapRequirement(), BluetoothScanning()];
} else if (Platform.isAndroid) {
list = [BluetoothTurnedOn(), AccessibilityRequirement(), NotificationRequirement(), BluetoothScanning()];
list = [
BluetoothTurnedOn(),
AccessibilityRequirement(),
NotificationRequirement(),
BluetoothScanRequirement(),
BluetoothConnectRequirement(),
BluetoothScanning(),
];
} else {
list = [UnsupportedPlatform()];
}

View File

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

View File

@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/utils/keymap/keymap.dart';
Future<Keymap?> showCustomKeymapDialog(BuildContext context, {required Keymap keymap}) {
return showDialog<Keymap>(
context: context,
builder: (context) {
return GearHotkeyDialog(keymap: keymap);
},
);
}
class GearHotkeyDialog extends StatefulWidget {
final Keymap keymap;
const GearHotkeyDialog({super.key, required this.keymap});
@override
State<GearHotkeyDialog> createState() => _GearHotkeyDialogState();
}
class _GearHotkeyDialogState extends State<GearHotkeyDialog> {
final FocusNode _focusNode = FocusNode();
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"))],
);
}
}

View File

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

View File

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

94
lib/widgets/title.dart Normal file
View 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(),
],
);
}
}

View File

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

View File

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

View File

@@ -5,14 +5,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"))
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -96,6 +96,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
crypto:
dependency: transitive
description:
@@ -136,6 +144,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.dev"
source: hosted
version: "0.9.3+2"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
url: "https://pub.dev"
source: hosted
version: "0.9.4+2"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev"
source: hosted
version: "2.6.2"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
url: "https://pub.dev"
source: hosted
version: "0.9.3+4"
fixnum:
dependency: transitive
description:
@@ -213,6 +261,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3"
url: "https://pub.dev"
source: hosted
version: "2.0.27"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -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:
@@ -288,12 +408,11 @@ packages:
source: hosted
version: "0.2.0"
keypress_simulator_windows:
dependency: transitive
dependency: "direct overridden"
description:
name: keypress_simulator_windows
sha256: b4ff055131a2e5ea920eb3b6a185e1889fe00749b027df3b83aa726ed590a9b5
url: "https://pub.dev"
source: hosted
path: "keypress_simulator/packages/keypress_simulator_windows"
relative: true
source: path
version: "0.2.0"
leak_tracker:
dependency: transitive
@@ -359,6 +478,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 +510,78 @@ 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:
name: permission_handler
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
url: "https://pub.dev"
source: hosted
version: "11.4.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
url: "https://pub.dev"
source: hosted
version: "12.1.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98
url: "https://pub.dev"
source: hosted
version: "9.4.6"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
@@ -375,6 +590,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:
@@ -407,6 +630,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
@@ -588,6 +867,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:

View File

@@ -1,7 +1,7 @@
name: swift_control
description: "SwiftControl - Control your virtual riding"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+3
version: 1.1.8+0
environment:
sdk: ^3.7.0
@@ -14,12 +14,21 @@ dependencies:
flutter_local_notifications: ^19.0.0
universal_ble: any
protobuf: ^3.1.0
permission_handler: ^11.4.0
dartx: any
image_picker: ^1.1.2
pointycastle: any
keypress_simulator: ^0.2.0
keypress_simulator: any
shared_preferences: ^2.5.3
flex_color_scheme: ^8.2.0
package_info_plus: ^8.3.0
accessibility:
path: accessibility
http: ^1.3.0
dependency_overrides:
keypress_simulator_windows:
path: keypress_simulator/packages/keypress_simulator_windows
dev_dependencies:
flutter_test:

View File

@@ -6,13 +6,19 @@
#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(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
UniversalBlePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UniversalBlePluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(

View File

@@ -3,7 +3,9 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
keypress_simulator_windows
permission_handler_windows
universal_ble
url_launcher_windows
)