mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c0e027abb | ||
|
|
0dcb666bbd | ||
|
|
00ebca7a01 | ||
|
|
f9c8820303 | ||
|
|
3d414edda9 | ||
|
|
0e3f6f1d5e | ||
|
|
c4b0ef38c0 | ||
|
|
2e800bb2de | ||
|
|
505b970497 | ||
|
|
986bfd481c | ||
|
|
ab8d480a01 | ||
|
|
7e19b76403 | ||
|
|
cbc2f103ac | ||
|
|
6a7922cf17 | ||
|
|
8e23de2718 | ||
|
|
11aec5fba1 | ||
|
|
c98f213e2e | ||
|
|
8c11cfcad6 | ||
|
|
5fe88ffc6a | ||
|
|
7a8c7a4ee1 | ||
|
|
3343325195 | ||
|
|
edda16dc06 | ||
|
|
7a3d120123 | ||
|
|
92419c9182 | ||
|
|
68bb5bf371 | ||
|
|
b0d8bfcadd | ||
|
|
a58ad1daf6 | ||
|
|
657c6056c4 | ||
|
|
84daba8902 | ||
|
|
3e37f8a269 | ||
|
|
28d178c4be | ||
|
|
f560cd5930 | ||
|
|
dbf24c6cd3 | ||
|
|
0a4989ca47 | ||
|
|
507dbf5d0f | ||
|
|
536f36f4e7 | ||
|
|
c523ba2287 | ||
|
|
a3f1cbb3b1 | ||
|
|
561bb2f0f4 | ||
|
|
dbbd1b5f2c | ||
|
|
7f6ec2f732 | ||
|
|
e8649203cf | ||
|
|
765b0c3d6d | ||
|
|
248c40731c | ||
|
|
cde01a4863 | ||
|
|
1ee8a0188b | ||
|
|
32e8fc9bf5 | ||
|
|
3c87d895c5 | ||
|
|
82dd8a9b48 | ||
|
|
0996506fd1 |
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -12,6 +12,8 @@
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
android/keystore.properties
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
[](https://paypal.me/boni)
|
||||
|
||||
## TODO
|
||||
- test Zwift Ride
|
||||
- confirm that Windows release works
|
||||
- implement more actions for Play + Ride
|
||||
|
||||
@@ -11,14 +11,11 @@ import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
|
||||
|
||||
/** AccessibilityPlugin */
|
||||
class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
|
||||
class AccessibilityPlugin: FlutterPlugin, Accessibility {
|
||||
/// The MethodChannel that will the communication between Flutter and native Android
|
||||
///
|
||||
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
||||
@@ -38,14 +35,6 @@ class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
|
||||
Observable.fromService = eventHandler
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
if (call.method == "getPlatformVersion") {
|
||||
result.success("Android ${android.os.Build.VERSION.RELEASE}")
|
||||
} else {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
}
|
||||
@@ -72,7 +61,10 @@ class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
|
||||
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
}
|
||||
MediaAction.NEXT -> audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
MediaAction.NEXT -> {
|
||||
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
}
|
||||
MediaAction.VOLUME_DOWN -> audioService.adjustVolume(android.media.AudioManager.ADJUST_LOWER, android.media.AudioManager.FLAG_SHOW_UI)
|
||||
MediaAction.VOLUME_UP -> audioService.adjustVolume(android.media.AudioManager.ADJUST_RAISE, android.media.AudioManager.FLAG_SHOW_UI)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
|
||||
12
keypress_simulator/.clang-format
Normal file
12
keypress_simulator/.clang-format
Normal 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
|
||||
1
keypress_simulator/.github/FUNDING.yml
vendored
Normal file
1
keypress_simulator/.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
liberapay: lijy91
|
||||
50
keypress_simulator/.github/workflows/build.yml
vendored
Normal file
50
keypress_simulator/.github/workflows/build.yml
vendored
Normal 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
|
||||
31
keypress_simulator/.github/workflows/lint.yml
vendored
Normal file
31
keypress_simulator/.github/workflows/lint.yml
vendored
Normal 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
|
||||
20
keypress_simulator/.github/workflows/test.yml
vendored
Normal file
20
keypress_simulator/.github/workflows/test.yml
vendored
Normal 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
6
keypress_simulator/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.dart_tool/
|
||||
.idea/
|
||||
|
||||
*.iml
|
||||
pubspec_overrides.yaml
|
||||
pubspec.lock
|
||||
21
keypress_simulator/LICENSE
Normal file
21
keypress_simulator/LICENSE
Normal 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.
|
||||
93
keypress_simulator/README-ZH.md
Normal file
93
keypress_simulator/README-ZH.md
Normal 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)
|
||||
93
keypress_simulator/README.md
Normal file
93
keypress_simulator/README.md
Normal 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)
|
||||
35
keypress_simulator/melos.yaml
Normal file
35
keypress_simulator/melos.yaml
Normal 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.
|
||||
29
keypress_simulator/packages/keypress_simulator_windows/.gitignore
vendored
Normal file
29
keypress_simulator/packages/keypress_simulator_windows/.gitignore
vendored
Normal 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/
|
||||
@@ -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'
|
||||
@@ -0,0 +1,3 @@
|
||||
## 0.2.0
|
||||
|
||||
* First release.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
include: package:mostly_reasonable_lints/flutter.yaml
|
||||
@@ -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
|
||||
17
keypress_simulator/packages/keypress_simulator_windows/windows/.gitignore
vendored
Normal file
17
keypress_simulator/packages/keypress_simulator_windows/windows/.gitignore
vendored
Normal 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/
|
||||
@@ -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()
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
9
keypress_simulator/pubspec.yaml
Normal file
9
keypress_simulator/pubspec.yaml
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -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);
|
||||
}
|
||||
26
lib/bluetooth/devices/zwift_click.dart
Normal file
26
lib/bluetooth/devices/zwift_click.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:swift_control/bluetooth/devices/base_device.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
|
||||
import '../messages/click_notification.dart';
|
||||
|
||||
class ZwiftClick extends BaseDevice {
|
||||
ZwiftClick(super.scanResult);
|
||||
|
||||
ClickNotification? _lastClickNotification;
|
||||
|
||||
@override
|
||||
void processClickNotification(Uint8List message) {
|
||||
final ClickNotification clickNotification = ClickNotification(message);
|
||||
if (_lastClickNotification == null || _lastClickNotification != clickNotification) {
|
||||
_lastClickNotification = clickNotification;
|
||||
actionStreamInternal.add(clickNotification);
|
||||
|
||||
if (clickNotification.buttonUp) {
|
||||
actionHandler.increaseGear();
|
||||
} else if (clickNotification.buttonDown) {
|
||||
actionHandler.decreaseGear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,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) {
|
||||
49
lib/bluetooth/devices/zwift_ride.dart
Normal file
49
lib/bluetooth/devices/zwift_ride.dart
Normal 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);
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:swift_control/utils/messages/notification.dart';
|
||||
|
||||
import '../../protocol/zwift.pb.dart';
|
||||
import '../protocol/zwift.pb.dart';
|
||||
import 'notification.dart';
|
||||
|
||||
class ClickNotification extends BaseNotification {
|
||||
static const int BTN_PRESSED = 0;
|
||||
|
||||
bool buttonUp = false;
|
||||
bool buttonDown = false;
|
||||
|
||||
ClickNotification(Uint8List message) {
|
||||
final status = ClickKeyPadStatus.fromBuffer(message);
|
||||
buttonUp = status.buttonPlus.value == BTN_PRESSED;
|
||||
buttonDown = status.buttonMinus.value == BTN_PRESSED;
|
||||
buttonUp = status.buttonPlus == PlayButtonStatus.ON;
|
||||
buttonDown = status.buttonMinus == PlayButtonStatus.ON;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1,25 +1,22 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:swift_control/utils/messages/notification.dart';
|
||||
|
||||
import '../../protocol/zwift.pb.dart';
|
||||
|
||||
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 &&
|
||||
140
lib/bluetooth/messages/ride_notification.dart
Normal file
140
lib/bluetooth/messages/ride_notification.dart
Normal 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;
|
||||
}
|
||||
@@ -25,8 +25,8 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage {
|
||||
PlayButtonStatus? buttonZLeft,
|
||||
PlayButtonStatus? buttonARight,
|
||||
PlayButtonStatus? buttonBDown,
|
||||
PlayButtonStatus? buttonOn,
|
||||
PlayButtonStatus? buttonShift,
|
||||
PlayButtonStatus? buttonOn,
|
||||
$core.int? analogLR,
|
||||
$core.int? analogUD,
|
||||
}) {
|
||||
@@ -46,12 +46,12 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage {
|
||||
if (buttonBDown != null) {
|
||||
$result.buttonBDown = buttonBDown;
|
||||
}
|
||||
if (buttonOn != null) {
|
||||
$result.buttonOn = buttonOn;
|
||||
}
|
||||
if (buttonShift != null) {
|
||||
$result.buttonShift = buttonShift;
|
||||
}
|
||||
if (buttonOn != null) {
|
||||
$result.buttonOn = buttonOn;
|
||||
}
|
||||
if (analogLR != null) {
|
||||
$result.analogLR = analogLR;
|
||||
}
|
||||
@@ -70,8 +70,8 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage {
|
||||
..e<PlayButtonStatus>(3, _omitFieldNames ? '' : 'ButtonZLeft', $pb.PbFieldType.OE, protoName: 'Button_Z_Left', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..e<PlayButtonStatus>(4, _omitFieldNames ? '' : 'ButtonARight', $pb.PbFieldType.OE, protoName: 'Button_A_Right', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..e<PlayButtonStatus>(5, _omitFieldNames ? '' : 'ButtonBDown', $pb.PbFieldType.OE, protoName: 'Button_B_Down', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..e<PlayButtonStatus>(6, _omitFieldNames ? '' : 'ButtonOn', $pb.PbFieldType.OE, protoName: 'Button_On', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..e<PlayButtonStatus>(7, _omitFieldNames ? '' : 'ButtonShift', $pb.PbFieldType.OE, protoName: 'Button_Shift', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..e<PlayButtonStatus>(6, _omitFieldNames ? '' : 'ButtonShift', $pb.PbFieldType.OE, protoName: 'Button_Shift', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..e<PlayButtonStatus>(7, _omitFieldNames ? '' : 'ButtonOn', $pb.PbFieldType.OE, protoName: 'Button_On', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
|
||||
..a<$core.int>(8, _omitFieldNames ? '' : 'AnalogLR', $pb.PbFieldType.OS3, protoName: 'Analog_LR')
|
||||
..a<$core.int>(9, _omitFieldNames ? '' : 'AnalogUD', $pb.PbFieldType.OS3, protoName: 'Analog_UD')
|
||||
..hasRequiredFields = false
|
||||
@@ -144,22 +144,22 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage {
|
||||
void clearButtonBDown() => clearField(5);
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
PlayButtonStatus get buttonOn => $_getN(5);
|
||||
PlayButtonStatus get buttonShift => $_getN(5);
|
||||
@$pb.TagNumber(6)
|
||||
set buttonOn(PlayButtonStatus v) { setField(6, v); }
|
||||
set buttonShift(PlayButtonStatus v) { setField(6, v); }
|
||||
@$pb.TagNumber(6)
|
||||
$core.bool hasButtonOn() => $_has(5);
|
||||
$core.bool hasButtonShift() => $_has(5);
|
||||
@$pb.TagNumber(6)
|
||||
void clearButtonOn() => clearField(6);
|
||||
void clearButtonShift() => clearField(6);
|
||||
|
||||
@$pb.TagNumber(7)
|
||||
PlayButtonStatus get buttonShift => $_getN(6);
|
||||
PlayButtonStatus get buttonOn => $_getN(6);
|
||||
@$pb.TagNumber(7)
|
||||
set buttonShift(PlayButtonStatus v) { setField(7, v); }
|
||||
set buttonOn(PlayButtonStatus v) { setField(7, v); }
|
||||
@$pb.TagNumber(7)
|
||||
$core.bool hasButtonShift() => $_has(6);
|
||||
$core.bool hasButtonOn() => $_has(6);
|
||||
@$pb.TagNumber(7)
|
||||
void clearButtonShift() => clearField(7);
|
||||
void clearButtonOn() => clearField(7);
|
||||
|
||||
@$pb.TagNumber(8)
|
||||
$core.int get analogLR => $_getIZ(7);
|
||||
@@ -82,8 +82,8 @@ const PlayKeyPadStatus$json = {
|
||||
{'1': 'Button_Z_Left', '3': 3, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonZLeft'},
|
||||
{'1': 'Button_A_Right', '3': 4, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonARight'},
|
||||
{'1': 'Button_B_Down', '3': 5, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonBDown'},
|
||||
{'1': 'Button_On', '3': 6, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonOn'},
|
||||
{'1': 'Button_Shift', '3': 7, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonShift'},
|
||||
{'1': 'Button_Shift', '3': 6, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonShift'},
|
||||
{'1': 'Button_On', '3': 7, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonOn'},
|
||||
{'1': 'Analog_LR', '3': 8, '4': 1, '5': 17, '10': 'AnalogLR'},
|
||||
{'1': 'Analog_UD', '3': 9, '4': 1, '5': 17, '10': 'AnalogUD'},
|
||||
],
|
||||
@@ -97,9 +97,9 @@ final $typed_data.Uint8List playKeyPadStatusDescriptor = $convert.base64Decode(
|
||||
'4uZGUuam9uYXNiYXJrLlBsYXlCdXR0b25TdGF0dXNSC0J1dHRvblpMZWZ0EkQKDkJ1dHRvbl9B'
|
||||
'X1JpZ2h0GAQgASgOMh4uZGUuam9uYXNiYXJrLlBsYXlCdXR0b25TdGF0dXNSDEJ1dHRvbkFSaW'
|
||||
'dodBJCCg1CdXR0b25fQl9Eb3duGAUgASgOMh4uZGUuam9uYXNiYXJrLlBsYXlCdXR0b25TdGF0'
|
||||
'dXNSC0J1dHRvbkJEb3duEjsKCUJ1dHRvbl9PbhgGIAEoDjIeLmRlLmpvbmFzYmFyay5QbGF5Qn'
|
||||
'V0dG9uU3RhdHVzUghCdXR0b25PbhJBCgxCdXR0b25fU2hpZnQYByABKA4yHi5kZS5qb25hc2Jh'
|
||||
'cmsuUGxheUJ1dHRvblN0YXR1c1ILQnV0dG9uU2hpZnQSGwoJQW5hbG9nX0xSGAggASgRUghBbm'
|
||||
'dXNSC0J1dHRvbkJEb3duEkEKDEJ1dHRvbl9TaGlmdBgGIAEoDjIeLmRlLmpvbmFzYmFyay5QbG'
|
||||
'F5QnV0dG9uU3RhdHVzUgtCdXR0b25TaGlmdBI7CglCdXR0b25fT24YByABKA4yHi5kZS5qb25h'
|
||||
'c2JhcmsuUGxheUJ1dHRvblN0YXR1c1IIQnV0dG9uT24SGwoJQW5hbG9nX0xSGAggASgRUghBbm'
|
||||
'Fsb2dMUhIbCglBbmFsb2dfVUQYCSABKBFSCEFuYWxvZ1VE');
|
||||
|
||||
@$core.Deprecated('Use playCommandParametersDescriptor instead')
|
||||
@@ -16,8 +16,8 @@ message PlayKeyPadStatus {
|
||||
optional PlayButtonStatus Button_Z_Left = 3;
|
||||
optional PlayButtonStatus Button_A_Right = 4;
|
||||
optional PlayButtonStatus Button_B_Down = 5;
|
||||
optional PlayButtonStatus Button_On = 6;
|
||||
optional PlayButtonStatus Button_Shift = 7;
|
||||
optional PlayButtonStatus Button_Shift = 6;
|
||||
optional PlayButtonStatus Button_On = 7;
|
||||
optional sint32 Analog_LR = 8;
|
||||
optional sint32 Analog_UD = 9;
|
||||
}
|
||||
@@ -1,18 +1,33 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:accessibility/accessibility.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:swift_control/pages/requirements.dart';
|
||||
import 'package:swift_control/theme.dart';
|
||||
import 'package:swift_control/utils/connection.dart';
|
||||
import 'package:swift_control/utils/actions/android.dart';
|
||||
import 'package:swift_control/utils/actions/desktop.dart';
|
||||
import 'package:swift_control/utils/settings/settings.dart';
|
||||
|
||||
import 'bluetooth/connection.dart';
|
||||
import 'utils/actions/base_actions.dart';
|
||||
|
||||
final connection = Connection();
|
||||
final actionHandler = ActionHandler();
|
||||
late final BaseActions actionHandler;
|
||||
final accessibilityHandler = Accessibility();
|
||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
final settings = Settings();
|
||||
|
||||
void main() {
|
||||
if (kIsWeb) {
|
||||
actionHandler = StubActions();
|
||||
} else if (Platform.isAndroid) {
|
||||
actionHandler = AndroidActions();
|
||||
} else {
|
||||
actionHandler = DesktopActions();
|
||||
}
|
||||
|
||||
runApp(const SwiftPlayApp());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/devices/base_device.dart';
|
||||
import 'package:swift_control/pages/touch_area.dart';
|
||||
import 'package:swift_control/widgets/logviewer.dart';
|
||||
import 'package:swift_control/widgets/title.dart';
|
||||
|
||||
import '../bluetooth/devices/base_device.dart';
|
||||
import '../widgets/menu.dart';
|
||||
|
||||
class DevicePage extends StatefulWidget {
|
||||
@@ -45,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()),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
186
lib/pages/touch_area.dart
Normal 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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:swift_control/utils/ble.dart';
|
||||
import 'package:swift_control/utils/crypto/local_key_provider.dart';
|
||||
import 'package:swift_control/utils/devices/zwift_click.dart';
|
||||
import 'package:swift_control/utils/devices/zwift_play.dart';
|
||||
import 'package:swift_control/utils/devices/zwift_ride.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
import '../crypto/zap_crypto.dart';
|
||||
import '../messages/notification.dart';
|
||||
|
||||
abstract class BaseDevice {
|
||||
final BleDevice scanResult;
|
||||
final zapEncryption = ZapCrypto(LocalKeyProvider());
|
||||
|
||||
bool isConnected = false;
|
||||
|
||||
bool supportsEncryption = true;
|
||||
|
||||
BaseDevice(this.scanResult);
|
||||
|
||||
static BaseDevice? fromScanResult(BleDevice scanResult) {
|
||||
if (scanResult.name == 'Zwift Ride') {
|
||||
return ZwiftRide(scanResult);
|
||||
}
|
||||
if (kIsWeb) {
|
||||
// manufacturer data is not available on web
|
||||
if (scanResult.name == 'Zwift Play') {
|
||||
return ZwiftPlay(scanResult);
|
||||
} else if (scanResult.name == 'Zwift Click') {
|
||||
return ZwiftClick(scanResult);
|
||||
}
|
||||
}
|
||||
final manufacturerData = scanResult.manufacturerDataList;
|
||||
final data = manufacturerData.firstOrNullWhere((e) => e.companyId == Constants.ZWIFT_MANUFACTURER_ID)?.payload;
|
||||
if (data == null || data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final type = DeviceType.fromManufacturerData(data.first);
|
||||
return switch (type) {
|
||||
DeviceType.click => ZwiftClick(scanResult),
|
||||
DeviceType.playRight => ZwiftPlay(scanResult),
|
||||
DeviceType.playLeft => ZwiftPlay(scanResult),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BaseDevice && runtimeType == other.runtimeType && scanResult == other.scanResult;
|
||||
|
||||
@override
|
||||
int get hashCode => scanResult.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return runtimeType.toString();
|
||||
}
|
||||
|
||||
BleDevice get device => scanResult;
|
||||
final StreamController<BaseNotification> actionStreamInternal = StreamController<BaseNotification>.broadcast();
|
||||
Stream<BaseNotification> get actionStream => actionStreamInternal.stream;
|
||||
|
||||
Future<void> connect() async {
|
||||
await UniversalBle.connect(device.deviceId, connectionTimeout: const Duration(seconds: 3));
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
//await UniversalBle.requestMtu(device.deviceId, 256);
|
||||
}
|
||||
|
||||
final services = await UniversalBle.discoverServices(device.deviceId);
|
||||
await handleServices(services);
|
||||
}
|
||||
|
||||
Future<void> handleServices(List<BleService> services);
|
||||
|
||||
void processCharacteristic(String tag, Uint8List bytes);
|
||||
}
|
||||
@@ -1,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;
|
||||
}
|
||||
@@ -1,12 +1,77 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
enum Keymap {
|
||||
myWhoosh(increase: PhysicalKeyboardKey.keyK, decrease: PhysicalKeyboardKey.keyI),
|
||||
indieVelo(increase: PhysicalKeyboardKey(0x70030), decrease: PhysicalKeyboardKey(0x70038)),
|
||||
plusMinus(increase: PhysicalKeyboardKey(0x70030), decrease: PhysicalKeyboardKey(0x70038));
|
||||
class Keymap {
|
||||
static Keymap myWhoosh = Keymap(
|
||||
'MyWhoosh',
|
||||
increase: KeyPair(physicalKey: PhysicalKeyboardKey.keyK, logicalKey: LogicalKeyboardKey.keyK),
|
||||
decrease: KeyPair(physicalKey: PhysicalKeyboardKey.keyI, logicalKey: LogicalKeyboardKey.keyI),
|
||||
);
|
||||
static Keymap custom = Keymap('Custom', increase: null, decrease: null);
|
||||
|
||||
final PhysicalKeyboardKey increase;
|
||||
final PhysicalKeyboardKey decrease;
|
||||
static List<Keymap> values = [myWhoosh, custom];
|
||||
|
||||
const Keymap({required this.increase, required this.decrease});
|
||||
KeyPair? increase;
|
||||
KeyPair? decrease;
|
||||
final String name;
|
||||
|
||||
Keymap(this.name, {required this.increase, required this.decrease});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (increase == null && decrease == null) {
|
||||
return name;
|
||||
}
|
||||
return "$name: ${increase?.logicalKey.keyLabel} + ${decrease?.logicalKey.keyLabel}";
|
||||
}
|
||||
|
||||
List<String> encode() {
|
||||
// encode to save in preferences
|
||||
return [
|
||||
name,
|
||||
increase?.logicalKey.keyId.toString() ?? '',
|
||||
increase?.physicalKey.usbHidUsage.toString() ?? '',
|
||||
decrease?.logicalKey.keyId.toString() ?? '',
|
||||
decrease?.physicalKey.usbHidUsage.toString() ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
static Keymap? decode(List<String> data) {
|
||||
// decode from preferences
|
||||
|
||||
if (data.length < 4) {
|
||||
return null;
|
||||
}
|
||||
final name = data[0];
|
||||
final keymap = values.firstOrNullWhere((element) => element.name == name);
|
||||
|
||||
if (keymap == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (keymap.name != custom.name) {
|
||||
return keymap;
|
||||
}
|
||||
|
||||
if (data.sublist(1).all((e) => e.isNotEmpty)) {
|
||||
keymap.increase = KeyPair(
|
||||
physicalKey: PhysicalKeyboardKey(int.parse(data[2])),
|
||||
logicalKey: LogicalKeyboardKey(int.parse(data[1])),
|
||||
);
|
||||
keymap.decrease = KeyPair(
|
||||
physicalKey: PhysicalKeyboardKey(int.parse(data[4])),
|
||||
logicalKey: LogicalKeyboardKey(int.parse(data[3])),
|
||||
);
|
||||
return keymap;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class KeyPair {
|
||||
final PhysicalKeyboardKey physicalKey;
|
||||
final LogicalKeyboardKey logicalKey;
|
||||
|
||||
KeyPair({required this.physicalKey, required this.logicalKey});
|
||||
}
|
||||
|
||||
@@ -1,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');
|
||||
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()];
|
||||
}
|
||||
|
||||
43
lib/utils/settings/settings.dart
Normal file
43
lib/utils/settings/settings.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../keymap/keymap.dart';
|
||||
|
||||
class Settings {
|
||||
late final SharedPreferences _prefs;
|
||||
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
|
||||
try {
|
||||
final keymapSetting = _prefs.getStringList("keymap");
|
||||
if (keymapSetting != null) {
|
||||
actionHandler.init(Keymap.decode(keymapSetting));
|
||||
}
|
||||
|
||||
final gearUpX = _prefs.getDouble("gearUpX");
|
||||
final gearUpY = _prefs.getDouble("gearUpY");
|
||||
final gearDownX = _prefs.getDouble("gearDownX");
|
||||
final gearDownY = _prefs.getDouble("gearDownY");
|
||||
if (gearUpX != null && gearUpY != null && gearDownX != null && gearDownY != null) {
|
||||
actionHandler.updateTouchPositions(Offset(gearUpX, gearUpY), Offset(gearDownX, gearDownY));
|
||||
}
|
||||
} catch (e) {
|
||||
// couldn't decode, reset
|
||||
await _prefs.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void setKeymap(Keymap keymap) {
|
||||
_prefs.setStringList("keymap", keymap.encode());
|
||||
}
|
||||
|
||||
void updateTouchPositions(Offset gearUp, Offset gearDown) {
|
||||
_prefs.setDouble("gearUpX", gearUp.dx);
|
||||
_prefs.setDouble("gearUpY", gearUp.dy);
|
||||
_prefs.setDouble("gearDownX", gearDown.dx);
|
||||
_prefs.setDouble("gearDownY", gearDown.dy);
|
||||
}
|
||||
}
|
||||
97
lib/widgets/custom_keymap_selector.dart
Normal file
97
lib/widgets/custom_keymap_selector.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:swift_control/utils/keymap/keymap.dart';
|
||||
|
||||
Future<Keymap?> showCustomKeymapDialog(BuildContext context, {required Keymap keymap}) {
|
||||
return showDialog<Keymap>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return GearHotkeyDialog(keymap: keymap);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class GearHotkeyDialog extends StatefulWidget {
|
||||
final Keymap keymap;
|
||||
const GearHotkeyDialog({super.key, required this.keymap});
|
||||
|
||||
@override
|
||||
State<GearHotkeyDialog> createState() => _GearHotkeyDialogState();
|
||||
}
|
||||
|
||||
class _GearHotkeyDialogState extends State<GearHotkeyDialog> {
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
KeyDownEvent? _pressedKey;
|
||||
KeyDownEvent? _gearUpHotkey;
|
||||
KeyDownEvent? _gearDownHotkey;
|
||||
|
||||
String _mode = 'up'; // 'up' or 'down'
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
void _onKey(KeyEvent event) {
|
||||
setState(() {
|
||||
if (event is KeyDownEvent) {
|
||||
_pressedKey = event;
|
||||
} else if (event is KeyUpEvent) {
|
||||
if (_pressedKey != null) {
|
||||
if (_mode == 'up') {
|
||||
_gearUpHotkey = _pressedKey;
|
||||
_mode = 'down';
|
||||
} else {
|
||||
_gearDownHotkey = _pressedKey;
|
||||
widget.keymap.increase = KeyPair(
|
||||
physicalKey: _gearUpHotkey!.physicalKey,
|
||||
logicalKey: _gearUpHotkey!.logicalKey,
|
||||
);
|
||||
widget.keymap.decrease = KeyPair(
|
||||
physicalKey: _gearDownHotkey!.physicalKey,
|
||||
logicalKey: _gearDownHotkey!.logicalKey,
|
||||
);
|
||||
Navigator.of(context).pop(widget.keymap);
|
||||
}
|
||||
_pressedKey = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String _formatKey(KeyDownEvent? key) {
|
||||
return key?.logicalKey.keyLabel ?? 'Not set';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Set Gear Hotkeys'),
|
||||
content: KeyboardListener(
|
||||
focusNode: _focusNode,
|
||||
autofocus: true,
|
||||
onKeyEvent: _onKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Step 1: Press a hotkey for **Gear Up**."),
|
||||
Text("Step 2: Press a hotkey for **Gear Down**."),
|
||||
SizedBox(height: 20),
|
||||
ListTile(
|
||||
leading: Icon(Icons.arrow_upward),
|
||||
title: Text("Gear Up Hotkey"),
|
||||
subtitle: Text(_formatKey(_gearUpHotkey)),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.arrow_downward),
|
||||
title: Text("Gear Down Hotkey"),
|
||||
subtitle: Text(_formatKey(_gearDownHotkey)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [TextButton(onPressed: () => Navigator.of(context).pop(null), child: Text("Cancel"))],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import 'dart:async';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../bluetooth/messages/notification.dart';
|
||||
import '../main.dart';
|
||||
import '../utils/messages/notification.dart';
|
||||
|
||||
class LogViewer extends StatefulWidget {
|
||||
const LogViewer({super.key});
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
List<Widget> buildMenuButtons() {
|
||||
return [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
launchUrlString('https://paypal.me/boni');
|
||||
},
|
||||
child: Text('Donate ♥'),
|
||||
),
|
||||
const MenuButton(),
|
||||
SizedBox(width: 8),
|
||||
];
|
||||
}
|
||||
|
||||
class MenuButton extends StatelessWidget {
|
||||
const MenuButton({super.key});
|
||||
|
||||
@@ -9,18 +24,31 @@ class MenuButton extends StatelessWidget {
|
||||
return PopupMenuButton(
|
||||
itemBuilder:
|
||||
(c) => [
|
||||
if (kDebugMode) ...[
|
||||
PopupMenuItem(
|
||||
child: Text('Gear up'),
|
||||
onTap: () {
|
||||
Future.delayed(Duration(seconds: 2)).then((_) {
|
||||
actionHandler.increaseGear();
|
||||
});
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text('Gear down'),
|
||||
onTap: () {
|
||||
Future.delayed(Duration(seconds: 2)).then((_) {
|
||||
actionHandler.decreaseGear();
|
||||
});
|
||||
},
|
||||
),
|
||||
PopupMenuItem(child: PopupMenuDivider()),
|
||||
],
|
||||
PopupMenuItem(
|
||||
child: Text('Feedback'),
|
||||
onTap: () {
|
||||
launchUrlString('https://github.com/jonasbark/swiftcontrol/issues');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text('Donate 🫶'),
|
||||
onTap: () {
|
||||
launchUrlString('https://paypal.me/boni');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text('License'),
|
||||
onTap: () {
|
||||
|
||||
94
lib/widgets/title.dart
Normal file
94
lib/widgets/title.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:swift_control/widgets/small_progress_indicator.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
String? _latestVersionUrlValue;
|
||||
PackageInfo? _packageInfoValue;
|
||||
|
||||
class AppTitle extends StatefulWidget {
|
||||
const AppTitle({super.key});
|
||||
|
||||
@override
|
||||
State<AppTitle> createState() => _AppTitleState();
|
||||
}
|
||||
|
||||
class _AppTitleState extends State<AppTitle> {
|
||||
Future<String?> getLatestVersionUrlIfNewer() async {
|
||||
final response = await http.get(Uri.parse('https://api.github.com/repos/jonasbark/swiftcontrol/releases/latest'));
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
final latestVersion = data['tag_name'].split('+').first;
|
||||
final currentVersion = 'v${_packageInfoValue!.version}';
|
||||
|
||||
if (latestVersion != null && latestVersion != currentVersion) {
|
||||
final assets = data['assets'] as List;
|
||||
if (Platform.isAndroid) {
|
||||
final apkUrl = assets.firstOrNullWhere((asset) => asset['name'].endsWith('.apk'))['browser_download_url'];
|
||||
return apkUrl;
|
||||
} else if (Platform.isMacOS) {
|
||||
final dmgUrl =
|
||||
assets.firstOrNullWhere((asset) => asset['name'].endsWith('.macos.zip'))['browser_download_url'];
|
||||
return dmgUrl;
|
||||
} else if (Platform.isWindows) {
|
||||
final appImageUrl =
|
||||
assets.firstOrNullWhere((asset) => asset['name'].endsWith('.windows.zip'))['browser_download_url'];
|
||||
return appImageUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_packageInfoValue == null) {
|
||||
PackageInfo.fromPlatform().then((value) {
|
||||
setState(() {
|
||||
_packageInfoValue = value;
|
||||
});
|
||||
_loadLatestVersionUrl();
|
||||
});
|
||||
} else {
|
||||
_loadLatestVersionUrl();
|
||||
}
|
||||
}
|
||||
|
||||
void _loadLatestVersionUrl() async {
|
||||
if (_latestVersionUrlValue == null && !kIsWeb) {
|
||||
final url = await getLatestVersionUrlIfNewer();
|
||||
if (url != null && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('New version available: ${url.split("/").takeLast(2).first.split('%').first}'),
|
||||
duration: Duration(seconds: 1337),
|
||||
action: SnackBarAction(
|
||||
label: 'Download',
|
||||
onPressed: () {
|
||||
launchUrlString(url);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text('SwiftControl'),
|
||||
if (_packageInfoValue != null) Text('v${_packageInfoValue!.version}') else SmallProgressIndicator(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,13 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
||||
@@ -5,14 +5,20 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import file_selector_macos
|
||||
import flutter_local_notifications
|
||||
import keypress_simulator_macos
|
||||
import package_info_plus
|
||||
import shared_preferences_foundation
|
||||
import universal_ble
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
KeypressSimulatorMacosPlugin.register(with: registry.registrar(forPlugin: "KeypressSimulatorMacosPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
PODS:
|
||||
- file_selector_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
- keypress_simulator_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- package_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- universal_ble (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -11,28 +18,40 @@ PODS:
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- keypress_simulator_macos (from `Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos`)
|
||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- universal_ble (from `Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
file_selector_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||
flutter_local_notifications:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
keypress_simulator_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos
|
||||
package_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
universal_ble:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
flutter_local_notifications: 4ccab5b7a22835214a6672e3f9c5e8ae207dab36
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
keypress_simulator_macos: f8556f9101f9f2f175652e0bceddf0fe82a4c6b2
|
||||
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
|
||||
|
||||
@@ -10,5 +10,9 @@
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
<dict>
|
||||
<key>com.apple.security.device.bluetooth</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
299
pubspec.lock
299
pubspec.lock
@@ -96,6 +96,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.4+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -136,6 +144,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+2"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+2"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_platform_interface
|
||||
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.2"
|
||||
file_selector_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+4"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -213,6 +261,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.27"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -232,7 +288,7 @@ packages:
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||
@@ -255,6 +311,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+22"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+2"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+2"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+2"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.1"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_windows
|
||||
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -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:
|
||||
|
||||
13
pubspec.yaml
13
pubspec.yaml
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
keypress_simulator_windows
|
||||
permission_handler_windows
|
||||
universal_ble
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user