mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f83defb37b | ||
|
|
5c8db11536 | ||
|
|
30aa5b33a3 | ||
|
|
ca41e69a17 | ||
|
|
af4d8ab183 | ||
|
|
c1a24cfbd1 | ||
|
|
86b406e2a4 | ||
|
|
1ec93330b0 | ||
|
|
4ed3c5fefe | ||
|
|
54d106ff4e | ||
|
|
996669ec44 | ||
|
|
1d38ff521a | ||
|
|
f0c1409da4 | ||
|
|
9617198db7 | ||
|
|
e4863b1ebd | ||
|
|
d51a4cc29d | ||
|
|
dcbb225355 | ||
|
|
cba449b493 | ||
|
|
559fe1232b | ||
|
|
a7f9ca489e | ||
|
|
74bf75a82e | ||
|
|
747629cebf | ||
|
|
aca6e9272b | ||
|
|
18e6f9a1b5 | ||
|
|
c3532d5c35 | ||
|
|
1a88f45c93 | ||
|
|
b49eda7fc7 | ||
|
|
f0b3bc70b2 | ||
|
|
08700edc22 | ||
|
|
d698c9bbea | ||
|
|
eea1b8eb40 | ||
|
|
0118c5a87c | ||
|
|
65a3374d9c | ||
|
|
536225bf12 | ||
|
|
e858d35617 | ||
|
|
6d87e85353 | ||
|
|
d1fed35c3e | ||
|
|
d9297bd40e | ||
|
|
a1926dfc00 | ||
|
|
d55ba039af | ||
|
|
c9ebc5a9f6 | ||
|
|
be0c2d97ba | ||
|
|
a03cc76eaa | ||
|
|
504c71d5c4 | ||
|
|
d0291c68d7 | ||
|
|
33e5e41eff | ||
|
|
221d5a0b8d | ||
|
|
b899487ee9 | ||
|
|
ff0b724a73 | ||
|
|
647c20a6a3 | ||
|
|
c36e63aa8d | ||
|
|
cb523ea656 | ||
|
|
22b99f4f6d | ||
|
|
05e681b59a | ||
|
|
07ee91c17a | ||
|
|
323a344c3a | ||
|
|
0172b1cf90 | ||
|
|
5a5e4066f6 | ||
|
|
3256f5aa15 | ||
|
|
476a9a337f | ||
|
|
1f1ce58bd9 | ||
|
|
bbb3dd3397 | ||
|
|
d7cee77c8b | ||
|
|
e2ac975c75 | ||
|
|
5e9352316c | ||
|
|
c73adb7c0d | ||
|
|
c3b41f56d4 | ||
|
|
6fe841af58 | ||
|
|
d97307de6f | ||
|
|
826dc2327f | ||
|
|
3466e504e3 | ||
|
|
ebd7f80947 | ||
|
|
43e827d8f5 | ||
|
|
5d5dc2e152 |
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
@@ -82,10 +82,12 @@ jobs:
|
||||
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
|
||||
|
||||
- name: Build Bundle
|
||||
run: flutter build appbundle --release
|
||||
|
||||
- name: Build Web
|
||||
run: flutter build web --release --base-href "/swiftcontrol/"
|
||||
|
||||
@@ -134,6 +136,9 @@ jobs:
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "build/app/outputs/flutter-apk/SwiftControl.android.apk,build/macos/Build/Products/Release/SwiftControl.macos.zip"
|
||||
allowUpdates: true
|
||||
prerelease: ${{ endsWith(env.VERSION, '1337') }}
|
||||
body: "You can also download the Android version from the Play Store: https://play.google.com/store/apps/details?id=de.jonasbark.swiftcontrol"
|
||||
tag: v${{ env.VERSION }}
|
||||
token: ${{ secrets.TOKEN }}
|
||||
|
||||
@@ -146,6 +151,24 @@ jobs:
|
||||
- name: Web Deploy
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
- name: Extract latest changelog
|
||||
id: changelog
|
||||
run: |
|
||||
chmod +x scripts/get_latest_changelog.sh
|
||||
mkdir -p whatsnew
|
||||
./scripts/get_latest_changelog.sh > whatsnew/whatsnew-en-US
|
||||
|
||||
- name: Upload to Play Store
|
||||
# only upload when env.VERSION does not end with 1337, which is our indicator for beta releases
|
||||
if: "!endsWith(env.VERSION, '1337')"
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: de.jonasbark.swiftcontrol
|
||||
releaseFiles: build/app/outputs/bundle/release/app-release.aab
|
||||
track: production
|
||||
whatsNewDirectory: whatsnew
|
||||
|
||||
windows:
|
||||
needs: build
|
||||
name: Build & Release on Windows
|
||||
|
||||
49
.github/workflows/web.yml
vendored
Normal file
49
.github/workflows/web.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: "Build"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- web
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- 'lib/**'
|
||||
- 'accessibility/**'
|
||||
- 'keypress_simulator/**'
|
||||
- 'pubspec.yaml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Release
|
||||
runs-on: macos-latest
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
#1 Checkout Repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
#3 Setup Flutter
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
|
||||
#4 Install Dependencies
|
||||
- name: Install Dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Web
|
||||
run: flutter build web --release --base-href "/swiftcontrol/"
|
||||
|
||||
- name: Upload static files as artifact
|
||||
id: deployment
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: build/web
|
||||
|
||||
- name: Web Deploy
|
||||
uses: actions/deploy-pages@v4
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -45,3 +45,5 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
service-account.json
|
||||
|
||||
0
launch.json → .vscode/launch.json
vendored
0
launch.json → .vscode/launch.json
vendored
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,3 +1,21 @@
|
||||
### 2.6.1 (2025-10-01)
|
||||
- fix a few issues with the new touch placement feature
|
||||
- add a workaround for Zwift Click V2 which resets the device when button events are no longer sent
|
||||
|
||||
### 2.6.0 (2025-09-30)
|
||||
- refactor touch placements: show touches on screen, fix misplaced coordinates - should fix #64
|
||||
- show firmware version of connected device
|
||||
- Fix crashes on some Android devices
|
||||
- warn the user how to make Zwift Click V2 work properly
|
||||
- many UI improvements
|
||||
- add setting to enable or disable vibration on button press for Zwift Ride and Zwift Play controllers
|
||||
|
||||
### 2.5.0 (2025-09-25)
|
||||
- Improve usability
|
||||
- SwiftControl is now available via the Play Store: https://play.google.com/store/apps/details?id=de.jonasbark.swiftcontrol
|
||||
- SwiftControl will continue to be available to download for free on GitHub
|
||||
- contact me if you already donated and I'll get a voucher for you :)
|
||||
|
||||
### 2.4.0+1 (2025-09-17)
|
||||
- Windows: fix mouse clicks at wrong location due to display scaling (fixes #64)
|
||||
|
||||
|
||||
15
README.md
15
README.md
@@ -11,6 +11,8 @@ With SwiftControl you can **control your favorite trainer app** using your Zwift
|
||||
- control music on your device
|
||||
- more? If you can do it via keyboard, mouse or touch, you can do it with SwiftControl
|
||||
|
||||
**Android AccessibilityService Usage**: On Android, SwiftControl uses the AccessibilityService API to simulate touch gestures on your screen, allowing your Zwift devices to control training apps. This service only monitors which app window is active and performs touch gestures at the locations you configure. No personal data is accessed or collected.
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/1f81b674-1628-4763-ad66-5f3ed7a3f159
|
||||
|
||||
@@ -18,7 +20,9 @@ https://github.com/user-attachments/assets/1f81b674-1628-4763-ad66-5f3ed7a3f159
|
||||
|
||||
|
||||
## Downloads
|
||||
Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
|
||||
<a href="https://play.google.com/store/apps/details?id=de.jonasbark.swiftcontrol"><img width="270" height="80" alt="GetItOnGooglePlay_Badge_Web_color_English" src="https://github.com/user-attachments/assets/a059d5a1-2efb-4f65-8117-ef6a99823b21" /></a>
|
||||
|
||||
Get the latest version for free for Windows, macOS and Android here: https://github.com/jonasbark/swiftcontrol/releases
|
||||
|
||||
## Supported Apps
|
||||
- MyWhoosh
|
||||
@@ -30,7 +34,7 @@ Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
|
||||
|
||||
## Supported Devices
|
||||
- Zwift Click
|
||||
- Zwift Click v2
|
||||
- Zwift Click v2 (mostly, see #68)
|
||||
- Zwift Ride
|
||||
- Zwift Play
|
||||
|
||||
@@ -51,7 +55,7 @@ Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
|
||||
## How does it work?
|
||||
The app connects to your Zwift device automatically. It does not connect to your trainer itself.
|
||||
|
||||
- When using Android a touch on a certain part of the screen is simulated to trigger the action.
|
||||
- When using Android: SwiftControl uses the AccessibilityService API to simulate touch gestures on specific parts of your screen to trigger actions in training apps. The service monitors which training app window is currently active to ensure gestures are sent to the correct app.
|
||||
- When using macOS or Windows a keyboard or mouse click is used to trigger the action.
|
||||
- there are predefined Keymaps for MyWhoosh, indieVelo / Training Peaks, and others
|
||||
- you can also create your own Keymaps for any other app
|
||||
@@ -63,5 +67,6 @@ The app connects to your Zwift device automatically. It does not connect to your
|
||||
## Donate
|
||||
Please consider donating to support the development of this app :)
|
||||
|
||||
[](https://paypal.me/boni)
|
||||
|
||||
- [via PayPal](https://paypal.me/boni)
|
||||
- [via Credit Card, Google Pay, Apple Pay, etc (USD)](https://donate.stripe.com/8x24gzc5c4ZE3VJdt36J201)
|
||||
- [via Credit Card, Google Pay, Apple Pay, etc (EUR)](https://donate.stripe.com/9B6aEX0muajY8bZ1Kl6J200)
|
||||
|
||||
@@ -41,7 +41,7 @@ class AccessibilityService : AccessibilityService(), Listener {
|
||||
|
||||
private fun getWindowSize(): Rect {
|
||||
val outBounds = Rect()
|
||||
rootInActiveWindow.getBoundsInScreen(outBounds)
|
||||
rootInActiveWindow?.getBoundsInScreen(outBounds)
|
||||
return outBounds
|
||||
}
|
||||
|
||||
|
||||
@@ -26,3 +26,5 @@ linter:
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
formatter:
|
||||
page_width: 120
|
||||
|
||||
@@ -14,7 +14,7 @@ val keystoreProperties = Properties()
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
|
||||
android {
|
||||
namespace = "de.jonasbark.swift_play"
|
||||
namespace = "de.jonasbark.swiftcontrol"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
@@ -32,7 +32,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "de.jonasbark.swift_play"
|
||||
applicationId = "de.jonasbark.swiftcontrol"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = 24
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.jonasbark.swift_play
|
||||
package de.jonasbark.swiftcontrol
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
@@ -18,8 +18,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.7.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
|
||||
id("com.android.application") version "8.7.3" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '12.0'
|
||||
# platform :ios, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
66
ios/Podfile.lock
Normal file
66
ios/Podfile.lock
Normal file
@@ -0,0 +1,66 @@
|
||||
PODS:
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- universal_ble (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- universal_ble (from `.symlinks/plugins/universal_ble/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
universal_ble:
|
||||
:path: ".symlinks/plugins/universal_ble/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
|
||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -10,10 +10,12 @@
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
3E50CA021EFA25CF89FE46AB /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C0E42A04700D6B661C7EE82 /* Pods_RunnerTests.framework */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
9DEFD285994D09CFCE400F36 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE7ADD07A99710C0FB974A8 /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -40,14 +42,20 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0CF32F9ECDBEA4B014717FF8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
2C0E42A04700D6B661C7EE82 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
5CE7ADD07A99710C0FB974A8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
7D133E5D5548E2EF2879734F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
86D436F6DAF367742EF27F51 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
8AA6D129479129F106E2298A /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -55,19 +63,44 @@
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
DFFDC4B9C4D6EF6A3BDE2E73 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
EFDECED99A47773C293F8819 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
5046C8DCA17DB268ED17F005 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3E50CA021EFA25CF89FE46AB /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9DEFD285994D09CFCE400F36 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
31E2F9ED567016937E8AEA3B /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
86D436F6DAF367742EF27F51 /* Pods-Runner.debug.xcconfig */,
|
||||
0CF32F9ECDBEA4B014717FF8 /* Pods-Runner.release.xcconfig */,
|
||||
7D133E5D5548E2EF2879734F /* Pods-Runner.profile.xcconfig */,
|
||||
DFFDC4B9C4D6EF6A3BDE2E73 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
8AA6D129479129F106E2298A /* Pods-RunnerTests.release.xcconfig */,
|
||||
EFDECED99A47773C293F8819 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -76,6 +109,15 @@
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6A38311855DC1CB8C0E2FD04 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CE7ADD07A99710C0FB974A8 /* Pods_Runner.framework */,
|
||||
2C0E42A04700D6B661C7EE82 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -94,6 +136,8 @@
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
31E2F9ED567016937E8AEA3B /* Pods */,
|
||||
6A38311855DC1CB8C0E2FD04 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -128,8 +172,10 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
5E1D2B1ED00966C758CA2289 /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
5046C8DCA17DB268ED17F005 /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -145,12 +191,15 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
AF2FDC69578083D4D16AB4D6 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
EEF1FBDEE98BA93C4FBDB3AE /* [CP] Embed Pods Frameworks */,
|
||||
1F0C44A79AE73641A1C3FF47 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -222,6 +271,23 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
1F0C44A79AE73641A1C3FF47 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -238,6 +304,28 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
5E1D2B1ED00966C758CA2289 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -253,6 +341,45 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
AF2FDC69578083D4D16AB4D6 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
EEF1FBDEE98BA93C4FBDB3AE /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -346,7 +473,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -379,6 +506,7 @@
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = DFFDC4B9C4D6EF6A3BDE2E73 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -396,6 +524,7 @@
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 8AA6D129479129F106E2298A /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -411,6 +540,7 @@
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = EFDECED99A47773C293F8819 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -473,7 +603,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -524,7 +654,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
@@ -54,6 +55,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
3
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
3
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -45,5 +45,7 @@
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>SwiftControl</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
class BleUuid {
|
||||
static final DEVICE_INFORMATION_SERVICE_UUID = "0000180a-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION =
|
||||
"00002a26-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
|
||||
static final DEVICE_BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final DEVICE_INFORMATION_CHARACTERISTIC_BATTERY_LEVEL = "00002a19-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final ZWIFT_CUSTOM_SERVICE_UUID = "00000001-19CA-4651-86E5-FA29DCDD09D1".toLowerCase();
|
||||
static final ZWIFT_RIDE_CUSTOM_SERVICE_UUID = "0000fc82-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final ZWIFT_ASYNC_CHARACTERISTIC_UUID = "00000002-19CA-4651-86E5-FA29DCDD09D1".toLowerCase();
|
||||
@@ -34,11 +40,45 @@ class Constants {
|
||||
static final REQUEST_START = Uint8List.fromList([0, 9]); //byteArrayOf(1, 2)
|
||||
static final RESPONSE_START_CLICK = Uint8List.fromList([1, 3]); // from device
|
||||
static final RESPONSE_START_PLAY = Uint8List.fromList([1, 4]); // from device
|
||||
static final RESPONSE_START_CLICK_V2 = Uint8List.fromList([0x02, 0x03]); // from device
|
||||
static final RESPONSE_STOPPED_CLICK_V2 = Uint8List.fromList([
|
||||
0xff,
|
||||
0x05,
|
||||
0x00,
|
||||
0xea,
|
||||
0x05,
|
||||
0x19,
|
||||
0x0a,
|
||||
0x0c,
|
||||
0x35,
|
||||
0x38,
|
||||
0x44,
|
||||
0x31,
|
||||
0x35,
|
||||
0x41,
|
||||
0x42,
|
||||
0x42,
|
||||
0x34,
|
||||
0x33,
|
||||
0x36,
|
||||
0x33,
|
||||
0x10,
|
||||
0x01,
|
||||
0x18,
|
||||
0x84,
|
||||
0x07,
|
||||
0x20,
|
||||
0x08,
|
||||
0x28,
|
||||
0x09,
|
||||
0x30,
|
||||
]); // from device
|
||||
|
||||
// Message types received from device
|
||||
static const CONTROLLER_NOTIFICATION_MESSAGE_TYPE = 07;
|
||||
static const EMPTY_MESSAGE_TYPE = 21;
|
||||
static const BATTERY_LEVEL_TYPE = 25;
|
||||
static const UNKNOWN_CLICKV2_TYPE = 0x3C;
|
||||
|
||||
// not figured out the protobuf type this really is, the content is just two varints.
|
||||
static const int CLICK_NOTIFICATION_MESSAGE_TYPE = 55;
|
||||
|
||||
@@ -3,6 +3,9 @@ import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/actions/android.dart';
|
||||
import 'package:swift_control/utils/requirements/android.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
@@ -12,7 +15,7 @@ import 'messages/notification.dart';
|
||||
|
||||
class Connection {
|
||||
final devices = <BaseDevice>[];
|
||||
var androidNotificationsSetup = false;
|
||||
var _androidNotificationsSetup = false;
|
||||
|
||||
final _connectionQueue = <BaseDevice>[];
|
||||
var _handlingConnectionQueue = false;
|
||||
@@ -51,6 +54,7 @@ class Connection {
|
||||
final device = devices.firstOrNullWhere((e) => e.device.deviceId == deviceId);
|
||||
if (device == null) {
|
||||
_actionStreams.add(LogNotification('Device not found: $deviceId'));
|
||||
UniversalBle.disconnect(deviceId);
|
||||
return;
|
||||
} else {
|
||||
device.processCharacteristic(characteristicUuid, value);
|
||||
@@ -94,8 +98,8 @@ class Connection {
|
||||
_handleConnectionQueue();
|
||||
|
||||
hasDevices.value = devices.isNotEmpty;
|
||||
if (devices.isNotEmpty && !androidNotificationsSetup && !kIsWeb && Platform.isAndroid) {
|
||||
androidNotificationsSetup = true;
|
||||
if (devices.isNotEmpty && !_androidNotificationsSetup && !kIsWeb && Platform.isAndroid) {
|
||||
_androidNotificationsSetup = true;
|
||||
NotificationRequirement.setup().catchError((e) {
|
||||
_actionStreams.add(LogNotification(e.toString()));
|
||||
});
|
||||
@@ -163,6 +167,11 @@ class Connection {
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_actionStreams.add(LogNotification('Disconnecting all devices'));
|
||||
if (actionHandler is AndroidActions) {
|
||||
AndroidFlutterLocalNotificationsPlugin().stopForegroundService();
|
||||
_androidNotificationsSetup = false;
|
||||
}
|
||||
UniversalBle.stopScan();
|
||||
isScanning.value = false;
|
||||
for (var device in devices) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -21,11 +20,16 @@ import '../messages/notification.dart';
|
||||
|
||||
abstract class BaseDevice {
|
||||
final BleDevice scanResult;
|
||||
BaseDevice(this.scanResult);
|
||||
final List<ZwiftButton> availableButtons;
|
||||
|
||||
BaseDevice(this.scanResult, {required this.availableButtons});
|
||||
|
||||
final zapEncryption = ZapCrypto(LocalKeyProvider());
|
||||
|
||||
bool isConnected = false;
|
||||
bool _isInited = false;
|
||||
int? batteryLevel;
|
||||
String? firmwareVersion;
|
||||
|
||||
bool supportsEncryption = true;
|
||||
|
||||
@@ -38,13 +42,21 @@ abstract class BaseDevice {
|
||||
|
||||
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), special case for Zwift Click v2: we must only connect to the left controller
|
||||
_ => null,
|
||||
};
|
||||
final device =
|
||||
kIsWeb
|
||||
? switch (scanResult.name) {
|
||||
'Zwift Ride' => ZwiftRide(scanResult),
|
||||
'Zwift Play' => ZwiftPlay(scanResult),
|
||||
'Zwift Click' => ZwiftClickV2(scanResult),
|
||||
_ => null,
|
||||
}
|
||||
: 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), special case for Zwift Click v2: we must only connect to the left controller
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (device != null) {
|
||||
return device;
|
||||
@@ -87,7 +99,6 @@ abstract class BaseDevice {
|
||||
BleDevice get device => scanResult;
|
||||
final StreamController<BaseNotification> actionStreamInternal = StreamController<BaseNotification>.broadcast();
|
||||
|
||||
int? batteryLevel;
|
||||
Stream<BaseNotification> get actionStream => actionStreamInternal.stream;
|
||||
|
||||
Future<void> connect() async {
|
||||
@@ -97,8 +108,8 @@ abstract class BaseDevice {
|
||||
|
||||
await UniversalBle.connect(device.deviceId);
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
//await UniversalBle.requestMtu(device.deviceId, 256);
|
||||
if (!kIsWeb) {
|
||||
await UniversalBle.requestMtu(device.deviceId, 517);
|
||||
}
|
||||
|
||||
final services = await UniversalBle.discoverServices(device.deviceId);
|
||||
@@ -114,6 +125,22 @@ abstract class BaseDevice {
|
||||
);
|
||||
}
|
||||
|
||||
final deviceInformationService = services.firstOrNullWhere(
|
||||
(service) => service.uuid == BleUuid.DEVICE_INFORMATION_SERVICE_UUID,
|
||||
);
|
||||
final firmwareCharacteristic = deviceInformationService?.characteristics.firstOrNullWhere(
|
||||
(c) => c.uuid == BleUuid.DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION,
|
||||
);
|
||||
if (firmwareCharacteristic != null) {
|
||||
final firmwareData = await UniversalBle.read(
|
||||
device.deviceId,
|
||||
deviceInformationService!.uuid,
|
||||
firmwareCharacteristic.uuid,
|
||||
);
|
||||
firmwareVersion = String.fromCharCodes(firmwareData);
|
||||
connection.signalChange(this);
|
||||
}
|
||||
|
||||
final asyncCharacteristic = customService.characteristics.firstOrNullWhere(
|
||||
(characteristic) => characteristic.uuid == BleUuid.ZWIFT_ASYNC_CHARACTERISTIC_UUID,
|
||||
);
|
||||
@@ -131,10 +158,10 @@ abstract class BaseDevice {
|
||||
await UniversalBle.subscribeNotifications(device.deviceId, customService.uuid, asyncCharacteristic.uuid);
|
||||
await UniversalBle.subscribeIndications(device.deviceId, customService.uuid, syncTxCharacteristic.uuid);
|
||||
|
||||
await _setupHandshake();
|
||||
await setupHandshake();
|
||||
}
|
||||
|
||||
Future<void> _setupHandshake() async {
|
||||
Future<void> setupHandshake() async {
|
||||
if (supportsEncryption) {
|
||||
await UniversalBle.write(
|
||||
device.deviceId,
|
||||
@@ -158,12 +185,12 @@ abstract class BaseDevice {
|
||||
}
|
||||
}
|
||||
|
||||
void processCharacteristic(String characteristic, Uint8List bytes) {
|
||||
Future<void> processCharacteristic(String characteristic, Uint8List bytes) async {
|
||||
if (kDebugMode && false) {
|
||||
print('Received $characteristic: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}');
|
||||
print('Received $characteristic: ${String.fromCharCodes(bytes)}');
|
||||
print(
|
||||
"${DateTime.now().toString().split(" ").last} Received data on $characteristic: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}",
|
||||
);
|
||||
}
|
||||
|
||||
if (bytes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
@@ -171,10 +198,8 @@ abstract class BaseDevice {
|
||||
try {
|
||||
if (bytes.startsWith(startCommand)) {
|
||||
_processDevicePublicKeyResponse(bytes);
|
||||
} else if (bytes.startsWith(Constants.RIDE_ON)) {
|
||||
//print("Empty RideOn response - unencrypted mode");
|
||||
} else if (!supportsEncryption || (bytes.length > Int32List.bytesPerElement + EncryptionUtils.MAC_LENGTH)) {
|
||||
_processData(bytes);
|
||||
processData(bytes);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
print("Error processing data: $e");
|
||||
@@ -189,13 +214,13 @@ abstract class 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(' ')}");
|
||||
}
|
||||
zapEncryption.initialise(devicePublicKeyBytes);
|
||||
}
|
||||
|
||||
void _processData(Uint8List bytes) {
|
||||
Future<void> processData(Uint8List bytes) async {
|
||||
int type;
|
||||
Uint8List message;
|
||||
|
||||
@@ -232,47 +257,10 @@ abstract class BaseDevice {
|
||||
break;
|
||||
case Constants.CLICK_NOTIFICATION_MESSAGE_TYPE:
|
||||
case Constants.PLAY_NOTIFICATION_MESSAGE_TYPE:
|
||||
case Constants.RIDE_NOTIFICATION_MESSAGE_TYPE: // untested
|
||||
case Constants.RIDE_NOTIFICATION_MESSAGE_TYPE:
|
||||
processClickNotification(message)
|
||||
.then((buttonsClicked) async {
|
||||
if (buttonsClicked == null) {
|
||||
// ignore, no changes
|
||||
} else if (buttonsClicked.isEmpty) {
|
||||
actionStreamInternal.add(LogNotification('Buttons released'));
|
||||
_longPressTimer?.cancel();
|
||||
|
||||
// Handle release events for long press keys
|
||||
final buttonsReleased = _previouslyPressedButtons.toList();
|
||||
if (buttonsReleased.isNotEmpty) {
|
||||
await _performRelease(buttonsReleased);
|
||||
}
|
||||
_previouslyPressedButtons.clear();
|
||||
} else {
|
||||
// Handle release events for buttons that are no longer pressed
|
||||
final buttonsReleased = _previouslyPressedButtons.difference(buttonsClicked.toSet()).toList();
|
||||
if (buttonsReleased.isNotEmpty) {
|
||||
await _performRelease(buttonsReleased);
|
||||
}
|
||||
|
||||
final isLongPress =
|
||||
buttonsClicked.singleOrNull != null &&
|
||||
actionHandler.supportedApp?.keymap.getKeyPair(buttonsClicked.single)?.isLongPress == true;
|
||||
|
||||
if (!isLongPress &&
|
||||
!(buttonsClicked.singleOrNull == ZwiftButton.onOffLeft ||
|
||||
buttonsClicked.singleOrNull == ZwiftButton.onOffRight)) {
|
||||
// we don't want to trigger the long press timer for the on/off buttons, also not when it's a long press key
|
||||
_longPressTimer?.cancel();
|
||||
_longPressTimer = Timer.periodic(const Duration(milliseconds: 250), (timer) async {
|
||||
_performActions(buttonsClicked, true);
|
||||
});
|
||||
} else if (isLongPress) {
|
||||
// Update currently pressed buttons
|
||||
_previouslyPressedButtons = buttonsClicked.toSet();
|
||||
}
|
||||
|
||||
_performActions(buttonsClicked, false);
|
||||
}
|
||||
return handleButtonsClicked(buttonsClicked);
|
||||
})
|
||||
.catchError((e) {
|
||||
actionStreamInternal.add(LogNotification(e.toString()));
|
||||
@@ -283,9 +271,51 @@ abstract class BaseDevice {
|
||||
|
||||
Future<List<ZwiftButton>?> processClickNotification(Uint8List message);
|
||||
|
||||
Future<void> handleButtonsClicked(List<ZwiftButton>? buttonsClicked) async {
|
||||
if (buttonsClicked == null) {
|
||||
// ignore, no changes
|
||||
} else if (buttonsClicked.isEmpty) {
|
||||
actionStreamInternal.add(LogNotification('Buttons released'));
|
||||
_longPressTimer?.cancel();
|
||||
|
||||
// Handle release events for long press keys
|
||||
final buttonsReleased = _previouslyPressedButtons.toList();
|
||||
if (buttonsReleased.isNotEmpty) {
|
||||
await _performRelease(buttonsReleased);
|
||||
}
|
||||
_previouslyPressedButtons.clear();
|
||||
} else {
|
||||
// Handle release events for buttons that are no longer pressed
|
||||
final buttonsReleased = _previouslyPressedButtons.difference(buttonsClicked.toSet()).toList();
|
||||
if (buttonsReleased.isNotEmpty) {
|
||||
await _performRelease(buttonsReleased);
|
||||
}
|
||||
|
||||
final isLongPress =
|
||||
buttonsClicked.singleOrNull != null &&
|
||||
actionHandler.supportedApp?.keymap.getKeyPair(buttonsClicked.single)?.isLongPress == true;
|
||||
|
||||
if (!isLongPress &&
|
||||
!(buttonsClicked.singleOrNull == ZwiftButton.onOffLeft ||
|
||||
buttonsClicked.singleOrNull == ZwiftButton.onOffRight)) {
|
||||
// we don't want to trigger the long press timer for the on/off buttons, also not when it's a long press key
|
||||
_longPressTimer?.cancel();
|
||||
_longPressTimer = Timer.periodic(const Duration(milliseconds: 250), (timer) async {
|
||||
_performActions(buttonsClicked, true);
|
||||
});
|
||||
} else if (isLongPress) {
|
||||
// Update currently pressed buttons
|
||||
_previouslyPressedButtons = buttonsClicked.toSet();
|
||||
}
|
||||
|
||||
return _performActions(buttonsClicked, false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _performActions(List<ZwiftButton> buttonsClicked, bool repeated) async {
|
||||
if (!repeated &&
|
||||
buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp))) {
|
||||
buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp)) &&
|
||||
settings.getVibrationEnabled()) {
|
||||
await _vibrate();
|
||||
}
|
||||
for (final action in buttonsClicked) {
|
||||
@@ -317,6 +347,7 @@ abstract class BaseDevice {
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
_isInited = false;
|
||||
_longPressTimer?.cancel();
|
||||
_previouslyPressedButtons.clear();
|
||||
// Release any held keys in long press mode
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import '../messages/click_notification.dart';
|
||||
|
||||
class ZwiftClick extends BaseDevice {
|
||||
ZwiftClick(super.scanResult);
|
||||
ZwiftClick(super.scanResult) : super(availableButtons: [ZwiftButton.shiftUpRight, ZwiftButton.shiftDownLeft]);
|
||||
|
||||
ClickNotification? _lastClickNotification;
|
||||
|
||||
|
||||
@@ -1,8 +1,71 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:swift_control/bluetooth/devices/zwift_ride.dart';
|
||||
|
||||
import '../ble.dart';
|
||||
import '../protocol/zp.pbenum.dart';
|
||||
|
||||
class ZwiftClickV2 extends ZwiftRide {
|
||||
ZwiftClickV2(super.scanResult);
|
||||
|
||||
@override
|
||||
bool get supportsEncryption => true;
|
||||
bool get supportsEncryption => false;
|
||||
|
||||
@override
|
||||
List<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_CLICK_V2;
|
||||
|
||||
@override
|
||||
Future<void> setupHandshake() async {
|
||||
super.setupHandshake();
|
||||
await sendCommandBuffer(Uint8List.fromList([0xFF, 0x04, 0x00]));
|
||||
}
|
||||
|
||||
Future<void> test() async {
|
||||
await sendCommand(Opcode.RESET, null);
|
||||
//await sendCommand(Opcode.GET, Get(dataObjectId: VendorDO.PAGE_DEVICE_PAIRING.value)); // 0008 82E0 03
|
||||
|
||||
/*await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_DEV_INFO.value)); // 0008 00
|
||||
await sendCommand(Opcode.LOG_LEVEL_SET, LogLevelSet(logLevel: LogLevel.LOGLEVEL_TRACE)); // 4108 05
|
||||
|
||||
await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_CLIENT_SERVER_CONFIGURATION.value)); // 0008 10
|
||||
await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_CLIENT_SERVER_CONFIGURATION.value)); // 0008 10
|
||||
await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_CLIENT_SERVER_CONFIGURATION.value)); // 0008 10
|
||||
|
||||
await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_CONTROLLER_INPUT_CONFIG.value)); // 0008 80 08
|
||||
|
||||
await sendCommand(Opcode.GET, Get(dataObjectId: DO.BATTERY_STATE.value)); // 0008 83 06
|
||||
|
||||
// Value: FF04 000A 1540 E9D9 C96B 7463 C27F 1B4E 4D9F 1CB1 205D 882E D7CE
|
||||
// Value: FF04 000A 15B2 6324 0A31 D6C6 B81F C129 D6A4 E99D FFFC B9FC 418D
|
||||
await sendCommandBuffer(
|
||||
Uint8List.fromList([
|
||||
0xFF,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0A,
|
||||
0x15,
|
||||
0xC2,
|
||||
0x63,
|
||||
0x24,
|
||||
0x0A,
|
||||
0x31,
|
||||
0xD6,
|
||||
0xC6,
|
||||
0xB8,
|
||||
0x1F,
|
||||
0xC1,
|
||||
0x29,
|
||||
0xD6,
|
||||
0xA4,
|
||||
0xE9,
|
||||
0x9D,
|
||||
0xFF,
|
||||
0xFC,
|
||||
0xB9,
|
||||
0xFC,
|
||||
0x41,
|
||||
0x8D,
|
||||
]),
|
||||
);*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,25 @@ import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import '../ble.dart';
|
||||
|
||||
class ZwiftPlay extends BaseDevice {
|
||||
ZwiftPlay(super.scanResult);
|
||||
ZwiftPlay(super.scanResult)
|
||||
: super(
|
||||
availableButtons: [
|
||||
ZwiftButton.y,
|
||||
ZwiftButton.z,
|
||||
ZwiftButton.a,
|
||||
ZwiftButton.b,
|
||||
ZwiftButton.onOffRight,
|
||||
ZwiftButton.sideButtonRight,
|
||||
ZwiftButton.paddleRight,
|
||||
ZwiftButton.navigationUp,
|
||||
ZwiftButton.navigationLeft,
|
||||
ZwiftButton.navigationRight,
|
||||
ZwiftButton.navigationDown,
|
||||
ZwiftButton.onOffLeft,
|
||||
ZwiftButton.sideButtonLeft,
|
||||
ZwiftButton.paddleLeft,
|
||||
],
|
||||
);
|
||||
|
||||
PlayNotification? _lastControllerNotification;
|
||||
|
||||
|
||||
@@ -1,13 +1,42 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
import 'package:swift_control/bluetooth/devices/base_device.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift_clickv2.dart';
|
||||
import 'package:swift_control/bluetooth/messages/ride_notification.dart';
|
||||
import 'package:swift_control/bluetooth/protocol/zp_vendor.pb.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../ble.dart';
|
||||
import '../messages/notification.dart';
|
||||
import '../protocol/zp.pb.dart';
|
||||
|
||||
class ZwiftRide extends BaseDevice {
|
||||
ZwiftRide(super.scanResult);
|
||||
ZwiftRide(super.scanResult)
|
||||
: super(
|
||||
availableButtons: [
|
||||
ZwiftButton.navigationLeft,
|
||||
ZwiftButton.navigationRight,
|
||||
ZwiftButton.navigationUp,
|
||||
ZwiftButton.navigationDown,
|
||||
ZwiftButton.a,
|
||||
ZwiftButton.b,
|
||||
ZwiftButton.y,
|
||||
ZwiftButton.z,
|
||||
ZwiftButton.shiftUpLeft,
|
||||
ZwiftButton.shiftDownLeft,
|
||||
ZwiftButton.shiftUpRight,
|
||||
ZwiftButton.shiftDownRight,
|
||||
ZwiftButton.powerUpLeft,
|
||||
ZwiftButton.powerUpRight,
|
||||
ZwiftButton.onOffLeft,
|
||||
ZwiftButton.onOffRight,
|
||||
ZwiftButton.paddleLeft,
|
||||
ZwiftButton.paddleRight,
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
String get customServiceId => BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID;
|
||||
@@ -17,6 +46,158 @@ class ZwiftRide extends BaseDevice {
|
||||
|
||||
RideNotification? _lastControllerNotification;
|
||||
|
||||
@override
|
||||
Future<void> processData(Uint8List bytes) async {
|
||||
Opcode? opcode;
|
||||
Uint8List message;
|
||||
|
||||
if (supportsEncryption) {
|
||||
final counter = bytes.sublist(0, 4); // Int.SIZE_BYTES is 4
|
||||
final payload = bytes.sublist(4);
|
||||
|
||||
if (zapEncryption.encryptionKeyBytes == null) {
|
||||
actionStreamInternal.add(
|
||||
LogNotification(
|
||||
'Encryption not initialized, yet. You may need to update the firmware of your device with the Zwift Companion app.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final data = zapEncryption.decrypt(counter, payload);
|
||||
opcode = Opcode.valueOf(data[0]);
|
||||
message = data.sublist(1);
|
||||
} else {
|
||||
opcode = Opcode.valueOf(bytes[0]);
|
||||
message = bytes.sublist(1);
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
'${DateTime.now().toString().split(" ").last} Received $opcode: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')} => ${String.fromCharCodes(bytes)} ',
|
||||
);
|
||||
}
|
||||
|
||||
if (bytes.startsWith(Constants.RESPONSE_STOPPED_CLICK_V2) && this is ZwiftClickV2) {
|
||||
actionStreamInternal.add(
|
||||
LogNotification(
|
||||
'Your Zwift Click V2 no longer sends events. Connect it in the Zwift app once per day. Resetting the device now.',
|
||||
),
|
||||
);
|
||||
if (!kDebugMode) {
|
||||
sendCommand(Opcode.RESET, null);
|
||||
}
|
||||
}
|
||||
|
||||
switch (opcode) {
|
||||
case Opcode.RIDE_ON:
|
||||
//print("Empty RideOn response - unencrypted mode");
|
||||
|
||||
break;
|
||||
case Opcode.STATUS_RESPONSE:
|
||||
final status = StatusResponse.fromBuffer(message);
|
||||
if (kDebugMode) {
|
||||
print('StatusResponse: ${status.command} status: ${Status.valueOf(status.status)}');
|
||||
}
|
||||
break;
|
||||
case Opcode.GET_RESPONSE:
|
||||
final response = GetResponse.fromBuffer(message);
|
||||
final dataObjectType = DO.valueOf(response.dataObjectId);
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
'GetResponse: ${dataObjectType?.value.toRadixString(16).padLeft(4, '0') ?? response.dataObjectId} $dataObjectType',
|
||||
);
|
||||
}
|
||||
|
||||
switch (dataObjectType) {
|
||||
case DO.PAGE_DEV_INFO:
|
||||
final pageDevInfo = DevInfoPage.fromBuffer(response.dataObjectData);
|
||||
if (kDebugMode) {
|
||||
print('PageDevInfo: $pageDevInfo');
|
||||
}
|
||||
break;
|
||||
case DO.PAGE_DATE_TIME:
|
||||
final pageDateTime = DateTimePage.fromBuffer(response.dataObjectData);
|
||||
if (kDebugMode) {
|
||||
print('PageDateTime: $pageDateTime');
|
||||
}
|
||||
break;
|
||||
case DO.PAGE_CONTROLLER_INPUT_CONFIG:
|
||||
final pageDateTime = ControllerInputConfigPage.fromBuffer(response.dataObjectData);
|
||||
if (kDebugMode) {
|
||||
print('PageDateTime: $pageDateTime');
|
||||
}
|
||||
break;
|
||||
case null:
|
||||
final vendorDO = VendorDO.valueOf(response.dataObjectId);
|
||||
if (kDebugMode) {
|
||||
print('VendorDO: $vendorDO');
|
||||
}
|
||||
switch (vendorDO) {
|
||||
case VendorDO.DEVICE_COUNT:
|
||||
// TODO: Handle this case.
|
||||
break;
|
||||
case VendorDO.NO_CLUE:
|
||||
// TODO: Handle this case.
|
||||
break;
|
||||
case VendorDO.PAGE_DEVICE_PAIRING:
|
||||
final page = DevicePairingDataPage.fromBuffer(response.dataObjectData);
|
||||
if (kDebugMode) {
|
||||
// this should show the right click device
|
||||
// pairingStatus = 1 => connected and paired, otherwise it can be paired but not connected
|
||||
print(
|
||||
'PageDevicePairing: $page => ${page.pairingDevList.map((e) => e.device.reversed.map((d) => d.toRadixString(16).padLeft(2, '0'))).join(', ')}',
|
||||
);
|
||||
}
|
||||
break;
|
||||
case VendorDO.PAIRED_DEVICE:
|
||||
// TODO: Handle this case.
|
||||
break;
|
||||
case VendorDO.PAIRING_STATUS:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case Opcode.VENDOR_MESSAGE:
|
||||
final vendorOpCode = VendorOpcode.valueOf(message.second);
|
||||
print('VendorOpcode: $vendorOpCode');
|
||||
break;
|
||||
case Opcode.LOG_DATA:
|
||||
final logMessage = LogDataNotification.fromBuffer(message);
|
||||
if (kDebugMode) {
|
||||
actionStreamInternal.add(LogNotification(logMessage.toString()));
|
||||
}
|
||||
break;
|
||||
case Opcode.BATTERY_NOTIF:
|
||||
final notification = BatteryNotification.fromBuffer(message);
|
||||
if (batteryLevel != notification.newPercLevel) {
|
||||
batteryLevel = notification.newPercLevel;
|
||||
connection.signalChange(this);
|
||||
}
|
||||
break;
|
||||
case Opcode.CONTROLLER_NOTIFICATION:
|
||||
processClickNotification(message)
|
||||
.then((buttonsClicked) async {
|
||||
return handleButtonsClicked(buttonsClicked);
|
||||
})
|
||||
.catchError((e) {
|
||||
actionStreamInternal.add(LogNotification(e.toString()));
|
||||
});
|
||||
break;
|
||||
case null:
|
||||
if (bytes[0] == 0x1A) {
|
||||
final batteryStatus = BatteryStatus.fromBuffer(message);
|
||||
if (kDebugMode) {
|
||||
print('BatteryStatus: $batteryStatus');
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
|
||||
final RideNotification clickNotification = RideNotification(message);
|
||||
@@ -31,4 +212,32 @@ class ZwiftRide extends BaseDevice {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendCommand(Opcode opCode, $pb.GeneratedMessage? message) async {
|
||||
final buffer = Uint8List.fromList([opCode.value, ...message?.writeToBuffer() ?? []]);
|
||||
if (kDebugMode) {
|
||||
print("Sending $opCode: ${buffer.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}");
|
||||
}
|
||||
await UniversalBle.write(
|
||||
device.deviceId,
|
||||
customServiceId,
|
||||
syncRxCharacteristic!.uuid,
|
||||
buffer,
|
||||
withoutResponse: true,
|
||||
);
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
Future<void> sendCommandBuffer(Uint8List buffer) async {
|
||||
if (kDebugMode) {
|
||||
print("Sending ${buffer.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}");
|
||||
}
|
||||
await UniversalBle.write(
|
||||
device.deviceId,
|
||||
customServiceId,
|
||||
syncRxCharacteristic!.uuid,
|
||||
buffer,
|
||||
withoutResponse: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
|
||||
import '../protocol/zwift.pb.dart';
|
||||
import 'notification.dart';
|
||||
@@ -19,7 +20,7 @@ class ClickNotification extends BaseNotification {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Buttons: ${buttonsClicked.joinToString(transform: (e) => e.name)}';
|
||||
return 'Buttons: ${buttonsClicked.joinToString(transform: (e) => e.name.splitByUpperCase())}';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:dartx/dartx.dart';
|
||||
import 'package:swift_control/bluetooth/messages/notification.dart';
|
||||
import 'package:swift_control/bluetooth/protocol/zwift.pb.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
|
||||
class PlayNotification extends BaseNotification {
|
||||
late List<ZwiftButton> buttonsClicked;
|
||||
@@ -35,7 +36,7 @@ class PlayNotification extends BaseNotification {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Buttons: ${buttonsClicked.joinToString(transform: (e) => e.name)}';
|
||||
return 'Buttons: ${buttonsClicked.joinToString(transform: (e) => e.name.splitByUpperCase())}';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:dartx/dartx.dart';
|
||||
import 'package:swift_control/bluetooth/messages/notification.dart';
|
||||
import 'package:swift_control/bluetooth/protocol/zwift.pb.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
|
||||
enum _RideButtonMask {
|
||||
LEFT_BTN(0x00001),
|
||||
@@ -72,7 +73,7 @@ class RideNotification extends BaseNotification {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Buttons: ${buttonsClicked.joinToString(transform: (e) => e.name)}';
|
||||
return 'Buttons: ${buttonsClicked.joinToString(transform: (e) => e.name.splitByUpperCase())}';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
6146
lib/bluetooth/protocol/zp.pb.dart
Normal file
6146
lib/bluetooth/protocol/zp.pb.dart
Normal file
File diff suppressed because it is too large
Load Diff
583
lib/bluetooth/protocol/zp.pbenum.dart
Normal file
583
lib/bluetooth/protocol/zp.pbenum.dart
Normal file
@@ -0,0 +1,583 @@
|
||||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: zp.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
/// ///////////////////////////////////////////////////////////////
|
||||
/// Enumerations
|
||||
/// ///////////////////////////////////////////////////////////////
|
||||
class Opcode extends $pb.ProtobufEnum {
|
||||
static const Opcode GET = Opcode._(0, _omitEnumNames ? '' : 'GET');
|
||||
static const Opcode DEV_INFO_STATUS = Opcode._(1, _omitEnumNames ? '' : 'DEV_INFO_STATUS');
|
||||
static const Opcode BLE_SECURITY_REQUEST = Opcode._(2, _omitEnumNames ? '' : 'BLE_SECURITY_REQUEST');
|
||||
static const Opcode TRAINER_NOTIF = Opcode._(3, _omitEnumNames ? '' : 'TRAINER_NOTIF');
|
||||
static const Opcode TRAINER_CONFIG_SET = Opcode._(4, _omitEnumNames ? '' : 'TRAINER_CONFIG_SET');
|
||||
static const Opcode TRAINER_CONFIG_STATUS = Opcode._(5, _omitEnumNames ? '' : 'TRAINER_CONFIG_STATUS');
|
||||
static const Opcode DEV_INFO_SET = Opcode._(12, _omitEnumNames ? '' : 'DEV_INFO_SET');
|
||||
static const Opcode POWER_OFF = Opcode._(15, _omitEnumNames ? '' : 'POWER_OFF');
|
||||
static const Opcode RESET = Opcode._(24, _omitEnumNames ? '' : 'RESET');
|
||||
static const Opcode BATTERY_NOTIF = Opcode._(25, _omitEnumNames ? '' : 'BATTERY_NOTIF');
|
||||
static const Opcode CONTROLLER_NOTIFICATION = Opcode._(35, _omitEnumNames ? '' : 'CONTROLLER_NOTIFICATION');
|
||||
static const Opcode LOG_DATA = Opcode._(42, _omitEnumNames ? '' : 'LOG_DATA');
|
||||
static const Opcode SPINDOWN_REQUEST = Opcode._(58, _omitEnumNames ? '' : 'SPINDOWN_REQUEST');
|
||||
static const Opcode SPINDOWN_NOTIFICATION = Opcode._(59, _omitEnumNames ? '' : 'SPINDOWN_NOTIFICATION');
|
||||
static const Opcode GET_RESPONSE = Opcode._(60, _omitEnumNames ? '' : 'GET_RESPONSE');
|
||||
static const Opcode STATUS_RESPONSE = Opcode._(62, _omitEnumNames ? '' : 'STATUS_RESPONSE');
|
||||
static const Opcode SET = Opcode._(63, _omitEnumNames ? '' : 'SET');
|
||||
static const Opcode SET_RESPONSE = Opcode._(64, _omitEnumNames ? '' : 'SET_RESPONSE');
|
||||
static const Opcode LOG_LEVEL_SET = Opcode._(65, _omitEnumNames ? '' : 'LOG_LEVEL_SET');
|
||||
static const Opcode DATA_CHANGE_NOTIFICATION = Opcode._(66, _omitEnumNames ? '' : 'DATA_CHANGE_NOTIFICATION');
|
||||
static const Opcode GAME_STATE_NOTIFICATION = Opcode._(67, _omitEnumNames ? '' : 'GAME_STATE_NOTIFICATION');
|
||||
static const Opcode SENSOR_RELAY_CONFIG = Opcode._(68, _omitEnumNames ? '' : 'SENSOR_RELAY_CONFIG');
|
||||
static const Opcode SENSOR_RELAY_GET = Opcode._(69, _omitEnumNames ? '' : 'SENSOR_RELAY_GET');
|
||||
static const Opcode SENSOR_RELAY_RESPONSE = Opcode._(70, _omitEnumNames ? '' : 'SENSOR_RELAY_RESPONSE');
|
||||
static const Opcode SENSOR_RELAY_NOTIFICATION = Opcode._(71, _omitEnumNames ? '' : 'SENSOR_RELAY_NOTIFICATION');
|
||||
static const Opcode HRM_DATA_NOTIFICATION = Opcode._(72, _omitEnumNames ? '' : 'HRM_DATA_NOTIFICATION');
|
||||
static const Opcode WIFI_CONFIG_REQUEST = Opcode._(73, _omitEnumNames ? '' : 'WIFI_CONFIG_REQUEST');
|
||||
static const Opcode WIFI_NOTIFICATION = Opcode._(74, _omitEnumNames ? '' : 'WIFI_NOTIFICATION');
|
||||
static const Opcode POWER_METER_NOTIFICATION = Opcode._(75, _omitEnumNames ? '' : 'POWER_METER_NOTIFICATION');
|
||||
static const Opcode CADENCE_SENSOR_NOTIFICATION = Opcode._(76, _omitEnumNames ? '' : 'CADENCE_SENSOR_NOTIFICATION');
|
||||
static const Opcode DEVICE_UPDATE_REQUEST = Opcode._(77, _omitEnumNames ? '' : 'DEVICE_UPDATE_REQUEST');
|
||||
static const Opcode RELAY_ZP_MESSAGE = Opcode._(78, _omitEnumNames ? '' : 'RELAY_ZP_MESSAGE');
|
||||
static const Opcode RIDE_ON = Opcode._(82, _omitEnumNames ? '' : 'RIDE_ON');
|
||||
static const Opcode RESERVED = Opcode._(253, _omitEnumNames ? '' : 'RESERVED');
|
||||
static const Opcode LOST_CONTROL = Opcode._(254, _omitEnumNames ? '' : 'LOST_CONTROL');
|
||||
static const Opcode VENDOR_MESSAGE = Opcode._(255, _omitEnumNames ? '' : 'VENDOR_MESSAGE');
|
||||
|
||||
static const $core.List<Opcode> values = <Opcode> [
|
||||
GET,
|
||||
DEV_INFO_STATUS,
|
||||
BLE_SECURITY_REQUEST,
|
||||
TRAINER_NOTIF,
|
||||
TRAINER_CONFIG_SET,
|
||||
TRAINER_CONFIG_STATUS,
|
||||
DEV_INFO_SET,
|
||||
POWER_OFF,
|
||||
RESET,
|
||||
BATTERY_NOTIF,
|
||||
CONTROLLER_NOTIFICATION,
|
||||
LOG_DATA,
|
||||
SPINDOWN_REQUEST,
|
||||
SPINDOWN_NOTIFICATION,
|
||||
GET_RESPONSE,
|
||||
STATUS_RESPONSE,
|
||||
SET,
|
||||
SET_RESPONSE,
|
||||
LOG_LEVEL_SET,
|
||||
DATA_CHANGE_NOTIFICATION,
|
||||
GAME_STATE_NOTIFICATION,
|
||||
SENSOR_RELAY_CONFIG,
|
||||
SENSOR_RELAY_GET,
|
||||
SENSOR_RELAY_RESPONSE,
|
||||
SENSOR_RELAY_NOTIFICATION,
|
||||
HRM_DATA_NOTIFICATION,
|
||||
WIFI_CONFIG_REQUEST,
|
||||
WIFI_NOTIFICATION,
|
||||
POWER_METER_NOTIFICATION,
|
||||
CADENCE_SENSOR_NOTIFICATION,
|
||||
DEVICE_UPDATE_REQUEST,
|
||||
RELAY_ZP_MESSAGE,
|
||||
RIDE_ON,
|
||||
RESERVED,
|
||||
LOST_CONTROL,
|
||||
VENDOR_MESSAGE,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, Opcode> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static Opcode? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const Opcode._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
/// Data Objects
|
||||
class DO extends $pb.ProtobufEnum {
|
||||
static const DO PAGE_DEV_INFO = DO._(0, _omitEnumNames ? '' : 'PAGE_DEV_INFO');
|
||||
static const DO PROTOCOL_VERSION = DO._(1, _omitEnumNames ? '' : 'PROTOCOL_VERSION');
|
||||
static const DO SYSTEM_FW_VERSION = DO._(2, _omitEnumNames ? '' : 'SYSTEM_FW_VERSION');
|
||||
static const DO DEVICE_NAME = DO._(3, _omitEnumNames ? '' : 'DEVICE_NAME');
|
||||
static const DO SERIAL_NUMBER = DO._(5, _omitEnumNames ? '' : 'SERIAL_NUMBER');
|
||||
static const DO SYSTEM_HW_REVISION = DO._(6, _omitEnumNames ? '' : 'SYSTEM_HW_REVISION');
|
||||
static const DO DEVICE_CAPABILITIES = DO._(7, _omitEnumNames ? '' : 'DEVICE_CAPABILITIES');
|
||||
static const DO MANUFACTURER_ID = DO._(8, _omitEnumNames ? '' : 'MANUFACTURER_ID');
|
||||
static const DO PRODUCT_ID = DO._(9, _omitEnumNames ? '' : 'PRODUCT_ID');
|
||||
static const DO DEVICE_UID = DO._(10, _omitEnumNames ? '' : 'DEVICE_UID');
|
||||
static const DO PAGE_CLIENT_SERVER_CONFIGURATION = DO._(16, _omitEnumNames ? '' : 'PAGE_CLIENT_SERVER_CONFIGURATION');
|
||||
static const DO CLIENT_SERVER_NOTIFICATIONS = DO._(17, _omitEnumNames ? '' : 'CLIENT_SERVER_NOTIFICATIONS');
|
||||
static const DO PAGE_DEVICE_UPDATE_INFO = DO._(32, _omitEnumNames ? '' : 'PAGE_DEVICE_UPDATE_INFO');
|
||||
static const DO DEVICE_UPDATE_STATUS = DO._(33, _omitEnumNames ? '' : 'DEVICE_UPDATE_STATUS');
|
||||
static const DO DEVICE_UPDATE_NEW_VERSION = DO._(34, _omitEnumNames ? '' : 'DEVICE_UPDATE_NEW_VERSION');
|
||||
static const DO PAGE_DATE_TIME = DO._(48, _omitEnumNames ? '' : 'PAGE_DATE_TIME');
|
||||
static const DO UTC_DATE_TIME = DO._(49, _omitEnumNames ? '' : 'UTC_DATE_TIME');
|
||||
static const DO PAGE_BLE_SECURITY = DO._(64, _omitEnumNames ? '' : 'PAGE_BLE_SECURITY');
|
||||
static const DO BLE_SECURE_CONNECTION_STATUS = DO._(65, _omitEnumNames ? '' : 'BLE_SECURE_CONNECTION_STATUS');
|
||||
static const DO BLE_SECURE_CONNECTION_WINDOW_STATUS = DO._(66, _omitEnumNames ? '' : 'BLE_SECURE_CONNECTION_WINDOW_STATUS');
|
||||
static const DO PAGE_TRAINER_CONFIG = DO._(512, _omitEnumNames ? '' : 'PAGE_TRAINER_CONFIG');
|
||||
static const DO TRAINER_MODE = DO._(513, _omitEnumNames ? '' : 'TRAINER_MODE');
|
||||
static const DO CFG_RESISTANCE = DO._(514, _omitEnumNames ? '' : 'CFG_RESISTANCE');
|
||||
static const DO ERG_POWER = DO._(515, _omitEnumNames ? '' : 'ERG_POWER');
|
||||
static const DO AVERAGING_WINDOW = DO._(516, _omitEnumNames ? '' : 'AVERAGING_WINDOW');
|
||||
static const DO SIM_WIND = DO._(517, _omitEnumNames ? '' : 'SIM_WIND');
|
||||
static const DO SIM_GRADE = DO._(518, _omitEnumNames ? '' : 'SIM_GRADE');
|
||||
static const DO SIM_REAL_GEAR_RATIO = DO._(519, _omitEnumNames ? '' : 'SIM_REAL_GEAR_RATIO');
|
||||
static const DO SIM_VIRT_GEAR_RATIO = DO._(520, _omitEnumNames ? '' : 'SIM_VIRT_GEAR_RATIO');
|
||||
static const DO SIM_CW = DO._(521, _omitEnumNames ? '' : 'SIM_CW');
|
||||
static const DO SIM_WHEEL_DIAMETER = DO._(522, _omitEnumNames ? '' : 'SIM_WHEEL_DIAMETER');
|
||||
static const DO SIM_BIKE_MASS = DO._(523, _omitEnumNames ? '' : 'SIM_BIKE_MASS');
|
||||
static const DO SIM_RIDER_MASS = DO._(524, _omitEnumNames ? '' : 'SIM_RIDER_MASS');
|
||||
static const DO SIM_CRR = DO._(525, _omitEnumNames ? '' : 'SIM_CRR');
|
||||
static const DO SIM_RESERVED_FRONTAL_AREA = DO._(526, _omitEnumNames ? '' : 'SIM_RESERVED_FRONTAL_AREA');
|
||||
static const DO SIM_EBRAKE = DO._(527, _omitEnumNames ? '' : 'SIM_EBRAKE');
|
||||
static const DO PAGE_TRAINER_GEAR_INDEX_CONFIG = DO._(528, _omitEnumNames ? '' : 'PAGE_TRAINER_GEAR_INDEX_CONFIG');
|
||||
static const DO FRONT_GEAR_INDEX = DO._(529, _omitEnumNames ? '' : 'FRONT_GEAR_INDEX');
|
||||
static const DO FRONT_GEAR_INDEX_MAX = DO._(530, _omitEnumNames ? '' : 'FRONT_GEAR_INDEX_MAX');
|
||||
static const DO FRONT_GEAR_INDEX_MIN = DO._(531, _omitEnumNames ? '' : 'FRONT_GEAR_INDEX_MIN');
|
||||
static const DO REAR_GEAR_INDEX = DO._(532, _omitEnumNames ? '' : 'REAR_GEAR_INDEX');
|
||||
static const DO REAR_GEAR_INDEX_MAX = DO._(533, _omitEnumNames ? '' : 'REAR_GEAR_INDEX_MAX');
|
||||
static const DO REAR_GEAR_INDEX_MIN = DO._(534, _omitEnumNames ? '' : 'REAR_GEAR_INDEX_MIN');
|
||||
static const DO PAGE_TRAINER_CONFIG2 = DO._(544, _omitEnumNames ? '' : 'PAGE_TRAINER_CONFIG2');
|
||||
static const DO HIGH_SPEED_DATA = DO._(545, _omitEnumNames ? '' : 'HIGH_SPEED_DATA');
|
||||
static const DO ERG_POWER_SMOOTHING = DO._(546, _omitEnumNames ? '' : 'ERG_POWER_SMOOTHING');
|
||||
static const DO VIRTUAL_SHIFTING_MODE = DO._(547, _omitEnumNames ? '' : 'VIRTUAL_SHIFTING_MODE');
|
||||
static const DO PAGE_DEVICE_TILT_CONFIG = DO._(560, _omitEnumNames ? '' : 'PAGE_DEVICE_TILT_CONFIG');
|
||||
static const DO DEVICE_TILT_ENABLED = DO._(561, _omitEnumNames ? '' : 'DEVICE_TILT_ENABLED');
|
||||
static const DO DEVICE_TILT_GRADIENT_MIN = DO._(562, _omitEnumNames ? '' : 'DEVICE_TILT_GRADIENT_MIN');
|
||||
static const DO DEVICE_TILT_GRADIENT_MAX = DO._(563, _omitEnumNames ? '' : 'DEVICE_TILT_GRADIENT_MAX');
|
||||
static const DO DEVICE_TILT_GRADIENT = DO._(564, _omitEnumNames ? '' : 'DEVICE_TILT_GRADIENT');
|
||||
static const DO BATTERY_STATE = DO._(771, _omitEnumNames ? '' : 'BATTERY_STATE');
|
||||
static const DO PAGE_CONTROLLER_INPUT_CONFIG = DO._(1024, _omitEnumNames ? '' : 'PAGE_CONTROLLER_INPUT_CONFIG');
|
||||
static const DO INPUT_SUPPORTED_DIGITAL_INPUTS = DO._(1025, _omitEnumNames ? '' : 'INPUT_SUPPORTED_DIGITAL_INPUTS');
|
||||
static const DO INPUT_SUPPORTED_ANALOG_INPUTS = DO._(1026, _omitEnumNames ? '' : 'INPUT_SUPPORTED_ANALOG_INPUTS');
|
||||
static const DO INPUT_ANALOG_INPUT_RANGE = DO._(1027, _omitEnumNames ? '' : 'INPUT_ANALOG_INPUT_RANGE');
|
||||
static const DO INPUT_ANALOG_INPUT_DEADZONE = DO._(1028, _omitEnumNames ? '' : 'INPUT_ANALOG_INPUT_DEADZONE');
|
||||
static const DO PAGE_WIFI_CONFIGURATION = DO._(1056, _omitEnumNames ? '' : 'PAGE_WIFI_CONFIGURATION');
|
||||
static const DO WIFI_ENABLED = DO._(1057, _omitEnumNames ? '' : 'WIFI_ENABLED');
|
||||
static const DO WIFI_STATUS = DO._(1058, _omitEnumNames ? '' : 'WIFI_STATUS');
|
||||
static const DO WIFI_SSID = DO._(1059, _omitEnumNames ? '' : 'WIFI_SSID');
|
||||
static const DO WIFI_BAND = DO._(1060, _omitEnumNames ? '' : 'WIFI_BAND');
|
||||
static const DO WIFI_RSSI = DO._(1061, _omitEnumNames ? '' : 'WIFI_RSSI');
|
||||
static const DO WIFI_REGION_CODE = DO._(1062, _omitEnumNames ? '' : 'WIFI_REGION_CODE');
|
||||
static const DO SENSOR_RELAY_DATA_PAGE = DO._(1280, _omitEnumNames ? '' : 'SENSOR_RELAY_DATA_PAGE');
|
||||
static const DO SENSOR_RELAY_SUPPORTED_SENSORS = DO._(1281, _omitEnumNames ? '' : 'SENSOR_RELAY_SUPPORTED_SENSORS');
|
||||
static const DO SENSOR_RELAY_PAIRED_SENSORS = DO._(1282, _omitEnumNames ? '' : 'SENSOR_RELAY_PAIRED_SENSORS');
|
||||
|
||||
static const $core.List<DO> values = <DO> [
|
||||
PAGE_DEV_INFO,
|
||||
PROTOCOL_VERSION,
|
||||
SYSTEM_FW_VERSION,
|
||||
DEVICE_NAME,
|
||||
SERIAL_NUMBER,
|
||||
SYSTEM_HW_REVISION,
|
||||
DEVICE_CAPABILITIES,
|
||||
MANUFACTURER_ID,
|
||||
PRODUCT_ID,
|
||||
DEVICE_UID,
|
||||
PAGE_CLIENT_SERVER_CONFIGURATION,
|
||||
CLIENT_SERVER_NOTIFICATIONS,
|
||||
PAGE_DEVICE_UPDATE_INFO,
|
||||
DEVICE_UPDATE_STATUS,
|
||||
DEVICE_UPDATE_NEW_VERSION,
|
||||
PAGE_DATE_TIME,
|
||||
UTC_DATE_TIME,
|
||||
PAGE_BLE_SECURITY,
|
||||
BLE_SECURE_CONNECTION_STATUS,
|
||||
BLE_SECURE_CONNECTION_WINDOW_STATUS,
|
||||
PAGE_TRAINER_CONFIG,
|
||||
TRAINER_MODE,
|
||||
CFG_RESISTANCE,
|
||||
ERG_POWER,
|
||||
AVERAGING_WINDOW,
|
||||
SIM_WIND,
|
||||
SIM_GRADE,
|
||||
SIM_REAL_GEAR_RATIO,
|
||||
SIM_VIRT_GEAR_RATIO,
|
||||
SIM_CW,
|
||||
SIM_WHEEL_DIAMETER,
|
||||
SIM_BIKE_MASS,
|
||||
SIM_RIDER_MASS,
|
||||
SIM_CRR,
|
||||
SIM_RESERVED_FRONTAL_AREA,
|
||||
SIM_EBRAKE,
|
||||
PAGE_TRAINER_GEAR_INDEX_CONFIG,
|
||||
FRONT_GEAR_INDEX,
|
||||
FRONT_GEAR_INDEX_MAX,
|
||||
FRONT_GEAR_INDEX_MIN,
|
||||
REAR_GEAR_INDEX,
|
||||
REAR_GEAR_INDEX_MAX,
|
||||
REAR_GEAR_INDEX_MIN,
|
||||
PAGE_TRAINER_CONFIG2,
|
||||
HIGH_SPEED_DATA,
|
||||
ERG_POWER_SMOOTHING,
|
||||
VIRTUAL_SHIFTING_MODE,
|
||||
PAGE_DEVICE_TILT_CONFIG,
|
||||
DEVICE_TILT_ENABLED,
|
||||
DEVICE_TILT_GRADIENT_MIN,
|
||||
DEVICE_TILT_GRADIENT_MAX,
|
||||
DEVICE_TILT_GRADIENT,
|
||||
BATTERY_STATE,
|
||||
PAGE_CONTROLLER_INPUT_CONFIG,
|
||||
INPUT_SUPPORTED_DIGITAL_INPUTS,
|
||||
INPUT_SUPPORTED_ANALOG_INPUTS,
|
||||
INPUT_ANALOG_INPUT_RANGE,
|
||||
INPUT_ANALOG_INPUT_DEADZONE,
|
||||
PAGE_WIFI_CONFIGURATION,
|
||||
WIFI_ENABLED,
|
||||
WIFI_STATUS,
|
||||
WIFI_SSID,
|
||||
WIFI_BAND,
|
||||
WIFI_RSSI,
|
||||
WIFI_REGION_CODE,
|
||||
SENSOR_RELAY_DATA_PAGE,
|
||||
SENSOR_RELAY_SUPPORTED_SENSORS,
|
||||
SENSOR_RELAY_PAIRED_SENSORS,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, DO> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static DO? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const DO._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class Status extends $pb.ProtobufEnum {
|
||||
static const Status SUCCESS = Status._(0, _omitEnumNames ? '' : 'SUCCESS');
|
||||
static const Status FAILURE = Status._(1, _omitEnumNames ? '' : 'FAILURE');
|
||||
static const Status BUSY = Status._(2, _omitEnumNames ? '' : 'BUSY');
|
||||
static const Status INVALID_PARAM = Status._(3, _omitEnumNames ? '' : 'INVALID_PARAM');
|
||||
static const Status NOT_PERMITTED = Status._(4, _omitEnumNames ? '' : 'NOT_PERMITTED');
|
||||
static const Status NOT_SUPPORTED = Status._(5, _omitEnumNames ? '' : 'NOT_SUPPORTED');
|
||||
static const Status INVALID_MODE = Status._(6, _omitEnumNames ? '' : 'INVALID_MODE');
|
||||
|
||||
static const $core.List<Status> values = <Status> [
|
||||
SUCCESS,
|
||||
FAILURE,
|
||||
BUSY,
|
||||
INVALID_PARAM,
|
||||
NOT_PERMITTED,
|
||||
NOT_SUPPORTED,
|
||||
INVALID_MODE,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, Status> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static Status? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const Status._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class DeviceType extends $pb.ProtobufEnum {
|
||||
static const DeviceType UNDEFINED = DeviceType._(0, _omitEnumNames ? '' : 'UNDEFINED');
|
||||
static const DeviceType CYCLING_TURBO_TRAINER = DeviceType._(1, _omitEnumNames ? '' : 'CYCLING_TURBO_TRAINER');
|
||||
static const DeviceType USER_INPUT_DEVICE = DeviceType._(2, _omitEnumNames ? '' : 'USER_INPUT_DEVICE');
|
||||
static const DeviceType TREADMILL = DeviceType._(3, _omitEnumNames ? '' : 'TREADMILL');
|
||||
static const DeviceType SENSOR_RELAY = DeviceType._(4, _omitEnumNames ? '' : 'SENSOR_RELAY');
|
||||
static const DeviceType HEART_RATE_MONITOR = DeviceType._(5, _omitEnumNames ? '' : 'HEART_RATE_MONITOR');
|
||||
static const DeviceType POWER_METER = DeviceType._(6, _omitEnumNames ? '' : 'POWER_METER');
|
||||
static const DeviceType CADENCE_SENSOR = DeviceType._(7, _omitEnumNames ? '' : 'CADENCE_SENSOR');
|
||||
static const DeviceType WIFI = DeviceType._(8, _omitEnumNames ? '' : 'WIFI');
|
||||
|
||||
static const $core.List<DeviceType> values = <DeviceType> [
|
||||
UNDEFINED,
|
||||
CYCLING_TURBO_TRAINER,
|
||||
USER_INPUT_DEVICE,
|
||||
TREADMILL,
|
||||
SENSOR_RELAY,
|
||||
HEART_RATE_MONITOR,
|
||||
POWER_METER,
|
||||
CADENCE_SENSOR,
|
||||
WIFI,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, DeviceType> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static DeviceType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const DeviceType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class TrainerMode extends $pb.ProtobufEnum {
|
||||
static const TrainerMode MODE_UNKNOWN = TrainerMode._(0, _omitEnumNames ? '' : 'MODE_UNKNOWN');
|
||||
static const TrainerMode MODE_ERG = TrainerMode._(1, _omitEnumNames ? '' : 'MODE_ERG');
|
||||
static const TrainerMode MODE_RESISTANCE = TrainerMode._(2, _omitEnumNames ? '' : 'MODE_RESISTANCE');
|
||||
static const TrainerMode MODE_SIM = TrainerMode._(3, _omitEnumNames ? '' : 'MODE_SIM');
|
||||
|
||||
static const $core.List<TrainerMode> values = <TrainerMode> [
|
||||
MODE_UNKNOWN,
|
||||
MODE_ERG,
|
||||
MODE_RESISTANCE,
|
||||
MODE_SIM,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, TrainerMode> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static TrainerMode? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const TrainerMode._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class ChargingState extends $pb.ProtobufEnum {
|
||||
static const ChargingState CHARGING_IDLE = ChargingState._(0, _omitEnumNames ? '' : 'CHARGING_IDLE');
|
||||
static const ChargingState CHARGING_PROGRESS = ChargingState._(1, _omitEnumNames ? '' : 'CHARGING_PROGRESS');
|
||||
static const ChargingState CHARGING_DONE = ChargingState._(2, _omitEnumNames ? '' : 'CHARGING_DONE');
|
||||
|
||||
static const $core.List<ChargingState> values = <ChargingState> [
|
||||
CHARGING_IDLE,
|
||||
CHARGING_PROGRESS,
|
||||
CHARGING_DONE,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, ChargingState> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static ChargingState? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const ChargingState._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class SpindownStatus extends $pb.ProtobufEnum {
|
||||
static const SpindownStatus SPINDOWN_IDLE = SpindownStatus._(0, _omitEnumNames ? '' : 'SPINDOWN_IDLE');
|
||||
static const SpindownStatus SPINDOWN_REQUESTED = SpindownStatus._(1, _omitEnumNames ? '' : 'SPINDOWN_REQUESTED');
|
||||
static const SpindownStatus SPINDOWN_SUCCESS = SpindownStatus._(2, _omitEnumNames ? '' : 'SPINDOWN_SUCCESS');
|
||||
static const SpindownStatus SPINDOWN_ERROR = SpindownStatus._(3, _omitEnumNames ? '' : 'SPINDOWN_ERROR');
|
||||
static const SpindownStatus SPINDOWN_STOP_PEDALLING = SpindownStatus._(4, _omitEnumNames ? '' : 'SPINDOWN_STOP_PEDALLING');
|
||||
static const SpindownStatus SPINDOWN_ERROR_TIMEOUT = SpindownStatus._(5, _omitEnumNames ? '' : 'SPINDOWN_ERROR_TIMEOUT');
|
||||
static const SpindownStatus SPINDOWN_ERROR_TOSHORT = SpindownStatus._(6, _omitEnumNames ? '' : 'SPINDOWN_ERROR_TOSHORT');
|
||||
static const SpindownStatus SPINDOWN_ERROR_TOSLOW = SpindownStatus._(7, _omitEnumNames ? '' : 'SPINDOWN_ERROR_TOSLOW');
|
||||
static const SpindownStatus SPINDOWN_ERROR_TOFAST = SpindownStatus._(8, _omitEnumNames ? '' : 'SPINDOWN_ERROR_TOFAST');
|
||||
static const SpindownStatus SPINDOWN_ERROR_SAMPLEERROR = SpindownStatus._(9, _omitEnumNames ? '' : 'SPINDOWN_ERROR_SAMPLEERROR');
|
||||
static const SpindownStatus SPINDOWN_ERROR_ABORT = SpindownStatus._(10, _omitEnumNames ? '' : 'SPINDOWN_ERROR_ABORT');
|
||||
|
||||
static const $core.List<SpindownStatus> values = <SpindownStatus> [
|
||||
SPINDOWN_IDLE,
|
||||
SPINDOWN_REQUESTED,
|
||||
SPINDOWN_SUCCESS,
|
||||
SPINDOWN_ERROR,
|
||||
SPINDOWN_STOP_PEDALLING,
|
||||
SPINDOWN_ERROR_TIMEOUT,
|
||||
SPINDOWN_ERROR_TOSHORT,
|
||||
SPINDOWN_ERROR_TOSLOW,
|
||||
SPINDOWN_ERROR_TOFAST,
|
||||
SPINDOWN_ERROR_SAMPLEERROR,
|
||||
SPINDOWN_ERROR_ABORT,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, SpindownStatus> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static SpindownStatus? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const SpindownStatus._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class LogLevel extends $pb.ProtobufEnum {
|
||||
static const LogLevel LOGLEVEL_OFF = LogLevel._(0, _omitEnumNames ? '' : 'LOGLEVEL_OFF');
|
||||
static const LogLevel LOGLEVEL_ERROR = LogLevel._(1, _omitEnumNames ? '' : 'LOGLEVEL_ERROR');
|
||||
static const LogLevel LOGLEVEL_WARNING = LogLevel._(2, _omitEnumNames ? '' : 'LOGLEVEL_WARNING');
|
||||
static const LogLevel LOGLEVEL_INFO = LogLevel._(3, _omitEnumNames ? '' : 'LOGLEVEL_INFO');
|
||||
static const LogLevel LOGLEVEL_DEBUG = LogLevel._(4, _omitEnumNames ? '' : 'LOGLEVEL_DEBUG');
|
||||
static const LogLevel LOGLEVEL_TRACE = LogLevel._(5, _omitEnumNames ? '' : 'LOGLEVEL_TRACE');
|
||||
|
||||
static const $core.List<LogLevel> values = <LogLevel> [
|
||||
LOGLEVEL_OFF,
|
||||
LOGLEVEL_ERROR,
|
||||
LOGLEVEL_WARNING,
|
||||
LOGLEVEL_INFO,
|
||||
LOGLEVEL_DEBUG,
|
||||
LOGLEVEL_TRACE,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, LogLevel> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static LogLevel? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const LogLevel._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class RoadSurfaceType extends $pb.ProtobufEnum {
|
||||
static const RoadSurfaceType ROAD_SURFACE_SMOOTH_TARMAC = RoadSurfaceType._(0, _omitEnumNames ? '' : 'ROAD_SURFACE_SMOOTH_TARMAC');
|
||||
static const RoadSurfaceType ROAD_SURFACE_BRICK_ROAD = RoadSurfaceType._(1, _omitEnumNames ? '' : 'ROAD_SURFACE_BRICK_ROAD');
|
||||
static const RoadSurfaceType ROAD_SURFACE_HARD_COBBLES = RoadSurfaceType._(2, _omitEnumNames ? '' : 'ROAD_SURFACE_HARD_COBBLES');
|
||||
static const RoadSurfaceType ROAD_SURFACE_SOFT_COBBLES = RoadSurfaceType._(3, _omitEnumNames ? '' : 'ROAD_SURFACE_SOFT_COBBLES');
|
||||
static const RoadSurfaceType ROAD_SURFACE_NARROW_WOODEN_PLANKS = RoadSurfaceType._(4, _omitEnumNames ? '' : 'ROAD_SURFACE_NARROW_WOODEN_PLANKS');
|
||||
static const RoadSurfaceType ROAD_SURFACE_WIDE_WOODEN_PLANKS = RoadSurfaceType._(5, _omitEnumNames ? '' : 'ROAD_SURFACE_WIDE_WOODEN_PLANKS');
|
||||
static const RoadSurfaceType ROAD_SURFACE_DIRT = RoadSurfaceType._(6, _omitEnumNames ? '' : 'ROAD_SURFACE_DIRT');
|
||||
static const RoadSurfaceType ROAD_SURFACE_GRAVEL = RoadSurfaceType._(7, _omitEnumNames ? '' : 'ROAD_SURFACE_GRAVEL');
|
||||
static const RoadSurfaceType ROAD_SURFACE_CATTLE_GRID = RoadSurfaceType._(8, _omitEnumNames ? '' : 'ROAD_SURFACE_CATTLE_GRID');
|
||||
static const RoadSurfaceType ROAD_SURFACE_CONCRETE_FLAG_STONES = RoadSurfaceType._(9, _omitEnumNames ? '' : 'ROAD_SURFACE_CONCRETE_FLAG_STONES');
|
||||
static const RoadSurfaceType ROAD_SURFACE_ICE = RoadSurfaceType._(10, _omitEnumNames ? '' : 'ROAD_SURFACE_ICE');
|
||||
|
||||
static const $core.List<RoadSurfaceType> values = <RoadSurfaceType> [
|
||||
ROAD_SURFACE_SMOOTH_TARMAC,
|
||||
ROAD_SURFACE_BRICK_ROAD,
|
||||
ROAD_SURFACE_HARD_COBBLES,
|
||||
ROAD_SURFACE_SOFT_COBBLES,
|
||||
ROAD_SURFACE_NARROW_WOODEN_PLANKS,
|
||||
ROAD_SURFACE_WIDE_WOODEN_PLANKS,
|
||||
ROAD_SURFACE_DIRT,
|
||||
ROAD_SURFACE_GRAVEL,
|
||||
ROAD_SURFACE_CATTLE_GRID,
|
||||
ROAD_SURFACE_CONCRETE_FLAG_STONES,
|
||||
ROAD_SURFACE_ICE,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, RoadSurfaceType> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static RoadSurfaceType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const RoadSurfaceType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class WifiStatusCode extends $pb.ProtobufEnum {
|
||||
static const WifiStatusCode WIFI_STATUS_DISABLED = WifiStatusCode._(0, _omitEnumNames ? '' : 'WIFI_STATUS_DISABLED');
|
||||
static const WifiStatusCode WIFI_STATUS_NOT_PROVISIONED = WifiStatusCode._(1, _omitEnumNames ? '' : 'WIFI_STATUS_NOT_PROVISIONED');
|
||||
static const WifiStatusCode WIFI_STATUS_SCANNING = WifiStatusCode._(2, _omitEnumNames ? '' : 'WIFI_STATUS_SCANNING');
|
||||
static const WifiStatusCode WIFI_STATUS_DISCONNECTED = WifiStatusCode._(3, _omitEnumNames ? '' : 'WIFI_STATUS_DISCONNECTED');
|
||||
static const WifiStatusCode WIFI_STATUS_CONNECTING = WifiStatusCode._(4, _omitEnumNames ? '' : 'WIFI_STATUS_CONNECTING');
|
||||
static const WifiStatusCode WIFI_STATUS_CONNECTED = WifiStatusCode._(5, _omitEnumNames ? '' : 'WIFI_STATUS_CONNECTED');
|
||||
static const WifiStatusCode WIFI_STATUS_ERROR = WifiStatusCode._(6, _omitEnumNames ? '' : 'WIFI_STATUS_ERROR');
|
||||
|
||||
static const $core.List<WifiStatusCode> values = <WifiStatusCode> [
|
||||
WIFI_STATUS_DISABLED,
|
||||
WIFI_STATUS_NOT_PROVISIONED,
|
||||
WIFI_STATUS_SCANNING,
|
||||
WIFI_STATUS_DISCONNECTED,
|
||||
WIFI_STATUS_CONNECTING,
|
||||
WIFI_STATUS_CONNECTED,
|
||||
WIFI_STATUS_ERROR,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, WifiStatusCode> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static WifiStatusCode? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const WifiStatusCode._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class WifiErrorCode extends $pb.ProtobufEnum {
|
||||
static const WifiErrorCode WIFI_ERROR_UNKNOWN = WifiErrorCode._(0, _omitEnumNames ? '' : 'WIFI_ERROR_UNKNOWN');
|
||||
static const WifiErrorCode WIFI_ERROR_NO_MEMORY = WifiErrorCode._(1, _omitEnumNames ? '' : 'WIFI_ERROR_NO_MEMORY');
|
||||
static const WifiErrorCode WIFI_ERROR_INVALID_PARAMETERS = WifiErrorCode._(2, _omitEnumNames ? '' : 'WIFI_ERROR_INVALID_PARAMETERS');
|
||||
static const WifiErrorCode WIFI_ERROR_INVALID_STATE = WifiErrorCode._(3, _omitEnumNames ? '' : 'WIFI_ERROR_INVALID_STATE');
|
||||
static const WifiErrorCode WIFI_ERROR_NOT_FOUND = WifiErrorCode._(4, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_FOUND');
|
||||
static const WifiErrorCode WIFI_ERROR_NOT_SUPPORTED = WifiErrorCode._(5, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_SUPPORTED');
|
||||
static const WifiErrorCode WIFI_ERROR_NOT_ALLOWED = WifiErrorCode._(6, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_ALLOWED');
|
||||
static const WifiErrorCode WIFI_ERROR_NOT_INITIALISED = WifiErrorCode._(7, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_INITIALISED');
|
||||
static const WifiErrorCode WIFI_ERROR_NOT_STARTED = WifiErrorCode._(8, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_STARTED');
|
||||
static const WifiErrorCode WIFI_ERROR_TIMEOUT = WifiErrorCode._(9, _omitEnumNames ? '' : 'WIFI_ERROR_TIMEOUT');
|
||||
static const WifiErrorCode WIFI_ERROR_MODE = WifiErrorCode._(10, _omitEnumNames ? '' : 'WIFI_ERROR_MODE');
|
||||
static const WifiErrorCode WIFI_ERROR_SSID_INVALID = WifiErrorCode._(11, _omitEnumNames ? '' : 'WIFI_ERROR_SSID_INVALID');
|
||||
|
||||
static const $core.List<WifiErrorCode> values = <WifiErrorCode> [
|
||||
WIFI_ERROR_UNKNOWN,
|
||||
WIFI_ERROR_NO_MEMORY,
|
||||
WIFI_ERROR_INVALID_PARAMETERS,
|
||||
WIFI_ERROR_INVALID_STATE,
|
||||
WIFI_ERROR_NOT_FOUND,
|
||||
WIFI_ERROR_NOT_SUPPORTED,
|
||||
WIFI_ERROR_NOT_ALLOWED,
|
||||
WIFI_ERROR_NOT_INITIALISED,
|
||||
WIFI_ERROR_NOT_STARTED,
|
||||
WIFI_ERROR_TIMEOUT,
|
||||
WIFI_ERROR_MODE,
|
||||
WIFI_ERROR_SSID_INVALID,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, WifiErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static WifiErrorCode? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const WifiErrorCode._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class InterfaceType extends $pb.ProtobufEnum {
|
||||
static const InterfaceType INTERFACE_BLE = InterfaceType._(1, _omitEnumNames ? '' : 'INTERFACE_BLE');
|
||||
static const InterfaceType INTERFACE_ANT = InterfaceType._(2, _omitEnumNames ? '' : 'INTERFACE_ANT');
|
||||
static const InterfaceType INTERFACE_USB = InterfaceType._(3, _omitEnumNames ? '' : 'INTERFACE_USB');
|
||||
static const InterfaceType INTERFACE_ETH = InterfaceType._(4, _omitEnumNames ? '' : 'INTERFACE_ETH');
|
||||
static const InterfaceType INTERFACE_WIFI = InterfaceType._(5, _omitEnumNames ? '' : 'INTERFACE_WIFI');
|
||||
|
||||
static const $core.List<InterfaceType> values = <InterfaceType> [
|
||||
INTERFACE_BLE,
|
||||
INTERFACE_ANT,
|
||||
INTERFACE_USB,
|
||||
INTERFACE_ETH,
|
||||
INTERFACE_WIFI,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, InterfaceType> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static InterfaceType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const InterfaceType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class SensorConnectionStatus extends $pb.ProtobufEnum {
|
||||
static const SensorConnectionStatus SENSOR_STATUS_DISCOVERED = SensorConnectionStatus._(1, _omitEnumNames ? '' : 'SENSOR_STATUS_DISCOVERED');
|
||||
static const SensorConnectionStatus SENSOR_STATUS_DISCONNECTED = SensorConnectionStatus._(2, _omitEnumNames ? '' : 'SENSOR_STATUS_DISCONNECTED');
|
||||
static const SensorConnectionStatus SENSOR_STATUS_PAIRING = SensorConnectionStatus._(3, _omitEnumNames ? '' : 'SENSOR_STATUS_PAIRING');
|
||||
static const SensorConnectionStatus SENSOR_STATUS_CONNECTED = SensorConnectionStatus._(4, _omitEnumNames ? '' : 'SENSOR_STATUS_CONNECTED');
|
||||
|
||||
static const $core.List<SensorConnectionStatus> values = <SensorConnectionStatus> [
|
||||
SENSOR_STATUS_DISCOVERED,
|
||||
SENSOR_STATUS_DISCONNECTED,
|
||||
SENSOR_STATUS_PAIRING,
|
||||
SENSOR_STATUS_CONNECTED,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, SensorConnectionStatus> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static SensorConnectionStatus? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const SensorConnectionStatus._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class BleSecureConnectionStatus extends $pb.ProtobufEnum {
|
||||
static const BleSecureConnectionStatus BLE_CONNECTION_SECURITY_STATUS_NONE = BleSecureConnectionStatus._(0, _omitEnumNames ? '' : 'BLE_CONNECTION_SECURITY_STATUS_NONE');
|
||||
static const BleSecureConnectionStatus BLE_CONNECTION_SECURITY_STATUS_INPROGRESS = BleSecureConnectionStatus._(1, _omitEnumNames ? '' : 'BLE_CONNECTION_SECURITY_STATUS_INPROGRESS');
|
||||
static const BleSecureConnectionStatus BLE_CONNECTION_SECURITY_STATUS_ACTIVE = BleSecureConnectionStatus._(2, _omitEnumNames ? '' : 'BLE_CONNECTION_SECURITY_STATUS_ACTIVE');
|
||||
static const BleSecureConnectionStatus BLE_CONNECTION_SECURITY_STATUS_REJECTED = BleSecureConnectionStatus._(3, _omitEnumNames ? '' : 'BLE_CONNECTION_SECURITY_STATUS_REJECTED');
|
||||
|
||||
static const $core.List<BleSecureConnectionStatus> values = <BleSecureConnectionStatus> [
|
||||
BLE_CONNECTION_SECURITY_STATUS_NONE,
|
||||
BLE_CONNECTION_SECURITY_STATUS_INPROGRESS,
|
||||
BLE_CONNECTION_SECURITY_STATUS_ACTIVE,
|
||||
BLE_CONNECTION_SECURITY_STATUS_REJECTED,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, BleSecureConnectionStatus> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static BleSecureConnectionStatus? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const BleSecureConnectionStatus._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class BleSecureConnectionWindowStatus extends $pb.ProtobufEnum {
|
||||
static const BleSecureConnectionWindowStatus BLE_SECURE_CONNECTION_WINDOW_STATUS_CLOSED = BleSecureConnectionWindowStatus._(0, _omitEnumNames ? '' : 'BLE_SECURE_CONNECTION_WINDOW_STATUS_CLOSED');
|
||||
static const BleSecureConnectionWindowStatus BLE_SECURE_CONNECTION_WINDOW_STATUS_OPEN = BleSecureConnectionWindowStatus._(1, _omitEnumNames ? '' : 'BLE_SECURE_CONNECTION_WINDOW_STATUS_OPEN');
|
||||
|
||||
static const $core.List<BleSecureConnectionWindowStatus> values = <BleSecureConnectionWindowStatus> [
|
||||
BLE_SECURE_CONNECTION_WINDOW_STATUS_CLOSED,
|
||||
BLE_SECURE_CONNECTION_WINDOW_STATUS_OPEN,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, BleSecureConnectionWindowStatus> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static BleSecureConnectionWindowStatus? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const BleSecureConnectionWindowStatus._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class WifiRegionCode_RegionCodeType extends $pb.ProtobufEnum {
|
||||
static const WifiRegionCode_RegionCodeType ALPHA_2 = WifiRegionCode_RegionCodeType._(0, _omitEnumNames ? '' : 'ALPHA_2');
|
||||
static const WifiRegionCode_RegionCodeType ALPHA_3 = WifiRegionCode_RegionCodeType._(1, _omitEnumNames ? '' : 'ALPHA_3');
|
||||
static const WifiRegionCode_RegionCodeType NUMERIC = WifiRegionCode_RegionCodeType._(2, _omitEnumNames ? '' : 'NUMERIC');
|
||||
static const WifiRegionCode_RegionCodeType UNKNOWN = WifiRegionCode_RegionCodeType._(3, _omitEnumNames ? '' : 'UNKNOWN');
|
||||
|
||||
static const $core.List<WifiRegionCode_RegionCodeType> values = <WifiRegionCode_RegionCodeType> [
|
||||
ALPHA_2,
|
||||
ALPHA_3,
|
||||
NUMERIC,
|
||||
UNKNOWN,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, WifiRegionCode_RegionCodeType> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static WifiRegionCode_RegionCodeType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const WifiRegionCode_RegionCodeType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
|
||||
const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names');
|
||||
1692
lib/bluetooth/protocol/zp.pbjson.dart
Normal file
1692
lib/bluetooth/protocol/zp.pbjson.dart
Normal file
File diff suppressed because it is too large
Load Diff
14
lib/bluetooth/protocol/zp.pbserver.dart
Normal file
14
lib/bluetooth/protocol/zp.pbserver.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: zp.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'zp.pb.dart';
|
||||
|
||||
896
lib/bluetooth/protocol/zp_vendor.pb.dart
Normal file
896
lib/bluetooth/protocol/zp_vendor.pb.dart
Normal file
@@ -0,0 +1,896 @@
|
||||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: zp_vendor.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
import 'zp_vendor.pbenum.dart';
|
||||
|
||||
export 'zp_vendor.pbenum.dart';
|
||||
|
||||
class ControllerSync extends $pb.GeneratedMessage {
|
||||
factory ControllerSync({
|
||||
ControllerSyncStatus? status,
|
||||
$core.int? timeStamp,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (status != null) {
|
||||
$result.status = status;
|
||||
}
|
||||
if (timeStamp != null) {
|
||||
$result.timeStamp = timeStamp;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
ControllerSync._() : super();
|
||||
factory ControllerSync.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory ControllerSync.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ControllerSync', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create)
|
||||
..e<ControllerSyncStatus>(1, _omitFieldNames ? '' : 'status', $pb.PbFieldType.OE, defaultOrMaker: ControllerSyncStatus.NOT_CONNECTED, valueOf: ControllerSyncStatus.valueOf, enumValues: ControllerSyncStatus.values)
|
||||
..a<$core.int>(2, _omitFieldNames ? '' : 'timeStamp', $pb.PbFieldType.O3, protoName: 'timeStamp')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
ControllerSync clone() => ControllerSync()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
ControllerSync copyWith(void Function(ControllerSync) updates) => super.copyWith((message) => updates(message as ControllerSync)) as ControllerSync;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ControllerSync create() => ControllerSync._();
|
||||
ControllerSync createEmptyInstance() => create();
|
||||
static $pb.PbList<ControllerSync> createRepeated() => $pb.PbList<ControllerSync>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static ControllerSync getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ControllerSync>(create);
|
||||
static ControllerSync? _defaultInstance;
|
||||
|
||||
/// optional in code; proto3 treats as present when non-zero
|
||||
@$pb.TagNumber(1)
|
||||
ControllerSyncStatus get status => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set status(ControllerSyncStatus v) { setField(1, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasStatus() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearStatus() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.int get timeStamp => $_getIZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set timeStamp($core.int v) { $_setSignedInt32(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasTimeStamp() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearTimeStamp() => clearField(2);
|
||||
}
|
||||
|
||||
class EnableTestMode extends $pb.GeneratedMessage {
|
||||
factory EnableTestMode({
|
||||
$core.bool? enable,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (enable != null) {
|
||||
$result.enable = enable;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
EnableTestMode._() : super();
|
||||
factory EnableTestMode.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory EnableTestMode.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EnableTestMode', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create)
|
||||
..aOB(1, _omitFieldNames ? '' : 'enable')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
EnableTestMode clone() => EnableTestMode()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
EnableTestMode copyWith(void Function(EnableTestMode) updates) => super.copyWith((message) => updates(message as EnableTestMode)) as EnableTestMode;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EnableTestMode create() => EnableTestMode._();
|
||||
EnableTestMode createEmptyInstance() => create();
|
||||
static $pb.PbList<EnableTestMode> createRepeated() => $pb.PbList<EnableTestMode>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EnableTestMode getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<EnableTestMode>(create);
|
||||
static EnableTestMode? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool get enable => $_getBF(0);
|
||||
@$pb.TagNumber(1)
|
||||
set enable($core.bool v) { $_setBool(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasEnable() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearEnable() => clearField(1);
|
||||
}
|
||||
|
||||
class PairDevices extends $pb.GeneratedMessage {
|
||||
factory PairDevices({
|
||||
$core.bool? pair,
|
||||
PairDeviceType? type,
|
||||
$core.List<$core.int>? deviceId,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (pair != null) {
|
||||
$result.pair = pair;
|
||||
}
|
||||
if (type != null) {
|
||||
$result.type = type;
|
||||
}
|
||||
if (deviceId != null) {
|
||||
$result.deviceId = deviceId;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
PairDevices._() : super();
|
||||
factory PairDevices.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory PairDevices.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PairDevices', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create)
|
||||
..aOB(1, _omitFieldNames ? '' : 'pair')
|
||||
..e<PairDeviceType>(2, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: PairDeviceType.BLE, valueOf: PairDeviceType.valueOf, enumValues: PairDeviceType.values)
|
||||
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'deviceId', $pb.PbFieldType.OY, protoName: 'deviceId')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
PairDevices clone() => PairDevices()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
PairDevices copyWith(void Function(PairDevices) updates) => super.copyWith((message) => updates(message as PairDevices)) as PairDevices;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static PairDevices create() => PairDevices._();
|
||||
PairDevices createEmptyInstance() => create();
|
||||
static $pb.PbList<PairDevices> createRepeated() => $pb.PbList<PairDevices>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static PairDevices getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<PairDevices>(create);
|
||||
static PairDevices? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool get pair => $_getBF(0);
|
||||
@$pb.TagNumber(1)
|
||||
set pair($core.bool v) { $_setBool(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasPair() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearPair() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
PairDeviceType get type => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set type(PairDeviceType v) { setField(2, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasType() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearType() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.int> get deviceId => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set deviceId($core.List<$core.int> v) { $_setBytes(2, v); }
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasDeviceId() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearDeviceId() => clearField(3);
|
||||
}
|
||||
|
||||
class DevicePairingDataPage_PairedDevice extends $pb.GeneratedMessage {
|
||||
factory DevicePairingDataPage_PairedDevice({
|
||||
$core.List<$core.int>? device,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (device != null) {
|
||||
$result.device = device;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
DevicePairingDataPage_PairedDevice._() : super();
|
||||
factory DevicePairingDataPage_PairedDevice.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory DevicePairingDataPage_PairedDevice.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DevicePairingDataPage.PairedDevice', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create)
|
||||
..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'device', $pb.PbFieldType.OY)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
DevicePairingDataPage_PairedDevice clone() => DevicePairingDataPage_PairedDevice()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
DevicePairingDataPage_PairedDevice copyWith(void Function(DevicePairingDataPage_PairedDevice) updates) => super.copyWith((message) => updates(message as DevicePairingDataPage_PairedDevice)) as DevicePairingDataPage_PairedDevice;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static DevicePairingDataPage_PairedDevice create() => DevicePairingDataPage_PairedDevice._();
|
||||
DevicePairingDataPage_PairedDevice createEmptyInstance() => create();
|
||||
static $pb.PbList<DevicePairingDataPage_PairedDevice> createRepeated() => $pb.PbList<DevicePairingDataPage_PairedDevice>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static DevicePairingDataPage_PairedDevice getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DevicePairingDataPage_PairedDevice>(create);
|
||||
static DevicePairingDataPage_PairedDevice? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<$core.int> get device => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set device($core.List<$core.int> v) { $_setBytes(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasDevice() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearDevice() => clearField(1);
|
||||
}
|
||||
|
||||
class DevicePairingDataPage extends $pb.GeneratedMessage {
|
||||
factory DevicePairingDataPage({
|
||||
$core.int? devicesCount,
|
||||
$core.int? pairingStatus,
|
||||
$core.Iterable<DevicePairingDataPage_PairedDevice>? pairingDevList,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (devicesCount != null) {
|
||||
$result.devicesCount = devicesCount;
|
||||
}
|
||||
if (pairingStatus != null) {
|
||||
$result.pairingStatus = pairingStatus;
|
||||
}
|
||||
if (pairingDevList != null) {
|
||||
$result.pairingDevList.addAll(pairingDevList);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
DevicePairingDataPage._() : super();
|
||||
factory DevicePairingDataPage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory DevicePairingDataPage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DevicePairingDataPage', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create)
|
||||
..a<$core.int>(1, _omitFieldNames ? '' : 'devicesCount', $pb.PbFieldType.O3, protoName: 'devicesCount')
|
||||
..a<$core.int>(2, _omitFieldNames ? '' : 'pairingStatus', $pb.PbFieldType.O3, protoName: 'pairingStatus')
|
||||
..pc<DevicePairingDataPage_PairedDevice>(3, _omitFieldNames ? '' : 'pairingDevList', $pb.PbFieldType.PM, protoName: 'pairingDevList', subBuilder: DevicePairingDataPage_PairedDevice.create)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
DevicePairingDataPage clone() => DevicePairingDataPage()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
DevicePairingDataPage copyWith(void Function(DevicePairingDataPage) updates) => super.copyWith((message) => updates(message as DevicePairingDataPage)) as DevicePairingDataPage;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static DevicePairingDataPage create() => DevicePairingDataPage._();
|
||||
DevicePairingDataPage createEmptyInstance() => create();
|
||||
static $pb.PbList<DevicePairingDataPage> createRepeated() => $pb.PbList<DevicePairingDataPage>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static DevicePairingDataPage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<DevicePairingDataPage>(create);
|
||||
static DevicePairingDataPage? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.int get devicesCount => $_getIZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set devicesCount($core.int v) { $_setSignedInt32(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasDevicesCount() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearDevicesCount() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.int get pairingStatus => $_getIZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set pairingStatus($core.int v) { $_setSignedInt32(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasPairingStatus() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearPairingStatus() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<DevicePairingDataPage_PairedDevice> get pairingDevList => $_getList(2);
|
||||
}
|
||||
|
||||
enum SetDfuTest_TestCase {
|
||||
failedEnterDfu,
|
||||
failedStartAdvertising,
|
||||
crcFailure,
|
||||
notSet
|
||||
}
|
||||
|
||||
class SetDfuTest extends $pb.GeneratedMessage {
|
||||
factory SetDfuTest({
|
||||
$core.bool? failedEnterDfu,
|
||||
$core.bool? failedStartAdvertising,
|
||||
$core.int? crcFailure,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (failedEnterDfu != null) {
|
||||
$result.failedEnterDfu = failedEnterDfu;
|
||||
}
|
||||
if (failedStartAdvertising != null) {
|
||||
$result.failedStartAdvertising = failedStartAdvertising;
|
||||
}
|
||||
if (crcFailure != null) {
|
||||
$result.crcFailure = crcFailure;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
SetDfuTest._() : super();
|
||||
factory SetDfuTest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory SetDfuTest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static const $core.Map<$core.int, SetDfuTest_TestCase> _SetDfuTest_TestCaseByTag = {
|
||||
1 : SetDfuTest_TestCase.failedEnterDfu,
|
||||
2 : SetDfuTest_TestCase.failedStartAdvertising,
|
||||
3 : SetDfuTest_TestCase.crcFailure,
|
||||
0 : SetDfuTest_TestCase.notSet
|
||||
};
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetDfuTest', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create)
|
||||
..oo(0, [1, 2, 3])
|
||||
..aOB(1, _omitFieldNames ? '' : 'failedEnterDfu', protoName: 'failedEnterDfu')
|
||||
..aOB(2, _omitFieldNames ? '' : 'failedStartAdvertising', protoName: 'failedStartAdvertising')
|
||||
..a<$core.int>(3, _omitFieldNames ? '' : 'crcFailure', $pb.PbFieldType.O3, protoName: 'crcFailure')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
SetDfuTest clone() => SetDfuTest()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
SetDfuTest copyWith(void Function(SetDfuTest) updates) => super.copyWith((message) => updates(message as SetDfuTest)) as SetDfuTest;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SetDfuTest create() => SetDfuTest._();
|
||||
SetDfuTest createEmptyInstance() => create();
|
||||
static $pb.PbList<SetDfuTest> createRepeated() => $pb.PbList<SetDfuTest>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SetDfuTest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SetDfuTest>(create);
|
||||
static SetDfuTest? _defaultInstance;
|
||||
|
||||
SetDfuTest_TestCase whichTestCase() => _SetDfuTest_TestCaseByTag[$_whichOneof(0)]!;
|
||||
void clearTestCase() => clearField($_whichOneof(0));
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool get failedEnterDfu => $_getBF(0);
|
||||
@$pb.TagNumber(1)
|
||||
set failedEnterDfu($core.bool v) { $_setBool(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasFailedEnterDfu() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearFailedEnterDfu() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool get failedStartAdvertising => $_getBF(1);
|
||||
@$pb.TagNumber(2)
|
||||
set failedStartAdvertising($core.bool v) { $_setBool(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasFailedStartAdvertising() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearFailedStartAdvertising() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.int get crcFailure => $_getIZ(2);
|
||||
@$pb.TagNumber(3)
|
||||
set crcFailure($core.int v) { $_setSignedInt32(2, v); }
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasCrcFailure() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearCrcFailure() => clearField(3);
|
||||
}
|
||||
|
||||
class SetGearTestData extends $pb.GeneratedMessage {
|
||||
factory SetGearTestData({
|
||||
$core.int? frontGearIdx,
|
||||
$core.int? rearGearIdx,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (frontGearIdx != null) {
|
||||
$result.frontGearIdx = frontGearIdx;
|
||||
}
|
||||
if (rearGearIdx != null) {
|
||||
$result.rearGearIdx = rearGearIdx;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
SetGearTestData._() : super();
|
||||
factory SetGearTestData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory SetGearTestData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetGearTestData', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create)
|
||||
..a<$core.int>(1, _omitFieldNames ? '' : 'frontGearIdx', $pb.PbFieldType.O3, protoName: 'frontGearIdx')
|
||||
..a<$core.int>(2, _omitFieldNames ? '' : 'rearGearIdx', $pb.PbFieldType.O3, protoName: 'rearGearIdx')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
SetGearTestData clone() => SetGearTestData()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
SetGearTestData copyWith(void Function(SetGearTestData) updates) => super.copyWith((message) => updates(message as SetGearTestData)) as SetGearTestData;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SetGearTestData create() => SetGearTestData._();
|
||||
SetGearTestData createEmptyInstance() => create();
|
||||
static $pb.PbList<SetGearTestData> createRepeated() => $pb.PbList<SetGearTestData>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SetGearTestData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SetGearTestData>(create);
|
||||
static SetGearTestData? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.int get frontGearIdx => $_getIZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set frontGearIdx($core.int v) { $_setSignedInt32(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasFrontGearIdx() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearFrontGearIdx() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.int get rearGearIdx => $_getIZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set rearGearIdx($core.int v) { $_setSignedInt32(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasRearGearIdx() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearRearGearIdx() => clearField(2);
|
||||
}
|
||||
|
||||
class SetHrmTestData extends $pb.GeneratedMessage {
|
||||
factory SetHrmTestData({
|
||||
$core.bool? hrmPresent,
|
||||
$core.int? heartRate,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (hrmPresent != null) {
|
||||
$result.hrmPresent = hrmPresent;
|
||||
}
|
||||
if (heartRate != null) {
|
||||
$result.heartRate = heartRate;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
SetHrmTestData._() : super();
|
||||
factory SetHrmTestData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory SetHrmTestData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetHrmTestData', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create)
|
||||
..aOB(1, _omitFieldNames ? '' : 'hrmPresent', protoName: 'hrmPresent')
|
||||
..a<$core.int>(2, _omitFieldNames ? '' : 'heartRate', $pb.PbFieldType.O3, protoName: 'heartRate')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
SetHrmTestData clone() => SetHrmTestData()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
SetHrmTestData copyWith(void Function(SetHrmTestData) updates) => super.copyWith((message) => updates(message as SetHrmTestData)) as SetHrmTestData;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SetHrmTestData create() => SetHrmTestData._();
|
||||
SetHrmTestData createEmptyInstance() => create();
|
||||
static $pb.PbList<SetHrmTestData> createRepeated() => $pb.PbList<SetHrmTestData>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SetHrmTestData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SetHrmTestData>(create);
|
||||
static SetHrmTestData? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool get hrmPresent => $_getBF(0);
|
||||
@$pb.TagNumber(1)
|
||||
set hrmPresent($core.bool v) { $_setBool(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasHrmPresent() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearHrmPresent() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.int get heartRate => $_getIZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set heartRate($core.int v) { $_setSignedInt32(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasHeartRate() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearHeartRate() => clearField(2);
|
||||
}
|
||||
|
||||
class SetInputDeviceTestData_ControllerAnalogEvent extends $pb.GeneratedMessage {
|
||||
factory SetInputDeviceTestData_ControllerAnalogEvent({
|
||||
$core.int? sensorId,
|
||||
$core.int? value,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (sensorId != null) {
|
||||
$result.sensorId = sensorId;
|
||||
}
|
||||
if (value != null) {
|
||||
$result.value = value;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
SetInputDeviceTestData_ControllerAnalogEvent._() : super();
|
||||
factory SetInputDeviceTestData_ControllerAnalogEvent.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory SetInputDeviceTestData_ControllerAnalogEvent.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetInputDeviceTestData.ControllerAnalogEvent', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create)
|
||||
..a<$core.int>(1, _omitFieldNames ? '' : 'sensorId', $pb.PbFieldType.O3, protoName: 'sensorId')
|
||||
..a<$core.int>(2, _omitFieldNames ? '' : 'value', $pb.PbFieldType.O3)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
SetInputDeviceTestData_ControllerAnalogEvent clone() => SetInputDeviceTestData_ControllerAnalogEvent()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
SetInputDeviceTestData_ControllerAnalogEvent copyWith(void Function(SetInputDeviceTestData_ControllerAnalogEvent) updates) => super.copyWith((message) => updates(message as SetInputDeviceTestData_ControllerAnalogEvent)) as SetInputDeviceTestData_ControllerAnalogEvent;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SetInputDeviceTestData_ControllerAnalogEvent create() => SetInputDeviceTestData_ControllerAnalogEvent._();
|
||||
SetInputDeviceTestData_ControllerAnalogEvent createEmptyInstance() => create();
|
||||
static $pb.PbList<SetInputDeviceTestData_ControllerAnalogEvent> createRepeated() => $pb.PbList<SetInputDeviceTestData_ControllerAnalogEvent>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SetInputDeviceTestData_ControllerAnalogEvent getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SetInputDeviceTestData_ControllerAnalogEvent>(create);
|
||||
static SetInputDeviceTestData_ControllerAnalogEvent? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.int get sensorId => $_getIZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set sensorId($core.int v) { $_setSignedInt32(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasSensorId() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearSensorId() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.int get value => $_getIZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set value($core.int v) { $_setSignedInt32(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasValue() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearValue() => clearField(2);
|
||||
}
|
||||
|
||||
class SetInputDeviceTestData extends $pb.GeneratedMessage {
|
||||
factory SetInputDeviceTestData({
|
||||
$core.int? duration,
|
||||
$core.int? buttonEvent,
|
||||
$core.Iterable<SetInputDeviceTestData_ControllerAnalogEvent>? analogEventList,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (duration != null) {
|
||||
$result.duration = duration;
|
||||
}
|
||||
if (buttonEvent != null) {
|
||||
$result.buttonEvent = buttonEvent;
|
||||
}
|
||||
if (analogEventList != null) {
|
||||
$result.analogEventList.addAll(analogEventList);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
SetInputDeviceTestData._() : super();
|
||||
factory SetInputDeviceTestData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory SetInputDeviceTestData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetInputDeviceTestData', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create)
|
||||
..a<$core.int>(1, _omitFieldNames ? '' : 'duration', $pb.PbFieldType.O3)
|
||||
..a<$core.int>(2, _omitFieldNames ? '' : 'buttonEvent', $pb.PbFieldType.O3, protoName: 'buttonEvent')
|
||||
..pc<SetInputDeviceTestData_ControllerAnalogEvent>(3, _omitFieldNames ? '' : 'analogEventList', $pb.PbFieldType.PM, protoName: 'analogEventList', subBuilder: SetInputDeviceTestData_ControllerAnalogEvent.create)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
SetInputDeviceTestData clone() => SetInputDeviceTestData()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
SetInputDeviceTestData copyWith(void Function(SetInputDeviceTestData) updates) => super.copyWith((message) => updates(message as SetInputDeviceTestData)) as SetInputDeviceTestData;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SetInputDeviceTestData create() => SetInputDeviceTestData._();
|
||||
SetInputDeviceTestData createEmptyInstance() => create();
|
||||
static $pb.PbList<SetInputDeviceTestData> createRepeated() => $pb.PbList<SetInputDeviceTestData>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SetInputDeviceTestData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SetInputDeviceTestData>(create);
|
||||
static SetInputDeviceTestData? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.int get duration => $_getIZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set duration($core.int v) { $_setSignedInt32(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasDuration() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearDuration() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.int get buttonEvent => $_getIZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set buttonEvent($core.int v) { $_setSignedInt32(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasButtonEvent() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearButtonEvent() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<SetInputDeviceTestData_ControllerAnalogEvent> get analogEventList => $_getList(2);
|
||||
}
|
||||
|
||||
class SetTrainerTestData extends $pb.GeneratedMessage {
|
||||
factory SetTrainerTestData({
|
||||
$core.int? dataMode,
|
||||
$core.int? interfaces,
|
||||
TestTrainerData? testTrainerData,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (dataMode != null) {
|
||||
$result.dataMode = dataMode;
|
||||
}
|
||||
if (interfaces != null) {
|
||||
$result.interfaces = interfaces;
|
||||
}
|
||||
if (testTrainerData != null) {
|
||||
$result.testTrainerData = testTrainerData;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
SetTrainerTestData._() : super();
|
||||
factory SetTrainerTestData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory SetTrainerTestData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetTrainerTestData', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create)
|
||||
..a<$core.int>(1, _omitFieldNames ? '' : 'dataMode', $pb.PbFieldType.O3, protoName: 'dataMode')
|
||||
..a<$core.int>(2, _omitFieldNames ? '' : 'interfaces', $pb.PbFieldType.O3)
|
||||
..aOM<TestTrainerData>(3, _omitFieldNames ? '' : 'testTrainerData', protoName: 'testTrainerData', subBuilder: TestTrainerData.create)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
SetTrainerTestData clone() => SetTrainerTestData()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
SetTrainerTestData copyWith(void Function(SetTrainerTestData) updates) => super.copyWith((message) => updates(message as SetTrainerTestData)) as SetTrainerTestData;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SetTrainerTestData create() => SetTrainerTestData._();
|
||||
SetTrainerTestData createEmptyInstance() => create();
|
||||
static $pb.PbList<SetTrainerTestData> createRepeated() => $pb.PbList<SetTrainerTestData>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static SetTrainerTestData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<SetTrainerTestData>(create);
|
||||
static SetTrainerTestData? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.int get dataMode => $_getIZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set dataMode($core.int v) { $_setSignedInt32(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasDataMode() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearDataMode() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.int get interfaces => $_getIZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set interfaces($core.int v) { $_setSignedInt32(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasInterfaces() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearInterfaces() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
TestTrainerData get testTrainerData => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set testTrainerData(TestTrainerData v) { setField(3, v); }
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasTestTrainerData() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearTestTrainerData() => clearField(3);
|
||||
@$pb.TagNumber(3)
|
||||
TestTrainerData ensureTestTrainerData() => $_ensure(2);
|
||||
}
|
||||
|
||||
class TestTrainerData extends $pb.GeneratedMessage {
|
||||
factory TestTrainerData({
|
||||
$core.int? power,
|
||||
$core.int? cadence,
|
||||
$core.int? bikeSpeed,
|
||||
$core.int? averagedPower,
|
||||
$core.int? wheelSpeed,
|
||||
$core.int? calculatedRealGearRatio,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (power != null) {
|
||||
$result.power = power;
|
||||
}
|
||||
if (cadence != null) {
|
||||
$result.cadence = cadence;
|
||||
}
|
||||
if (bikeSpeed != null) {
|
||||
$result.bikeSpeed = bikeSpeed;
|
||||
}
|
||||
if (averagedPower != null) {
|
||||
$result.averagedPower = averagedPower;
|
||||
}
|
||||
if (wheelSpeed != null) {
|
||||
$result.wheelSpeed = wheelSpeed;
|
||||
}
|
||||
if (calculatedRealGearRatio != null) {
|
||||
$result.calculatedRealGearRatio = calculatedRealGearRatio;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
TestTrainerData._() : super();
|
||||
factory TestTrainerData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory TestTrainerData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TestTrainerData', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create)
|
||||
..a<$core.int>(1, _omitFieldNames ? '' : 'power', $pb.PbFieldType.O3)
|
||||
..a<$core.int>(2, _omitFieldNames ? '' : 'cadence', $pb.PbFieldType.O3)
|
||||
..a<$core.int>(3, _omitFieldNames ? '' : 'bikeSpeed', $pb.PbFieldType.O3, protoName: 'bikeSpeed')
|
||||
..a<$core.int>(4, _omitFieldNames ? '' : 'averagedPower', $pb.PbFieldType.O3, protoName: 'averagedPower')
|
||||
..a<$core.int>(5, _omitFieldNames ? '' : 'wheelSpeed', $pb.PbFieldType.O3, protoName: 'wheelSpeed')
|
||||
..a<$core.int>(6, _omitFieldNames ? '' : 'calculatedRealGearRatio', $pb.PbFieldType.O3, protoName: 'calculatedRealGearRatio')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
TestTrainerData clone() => TestTrainerData()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
TestTrainerData copyWith(void Function(TestTrainerData) updates) => super.copyWith((message) => updates(message as TestTrainerData)) as TestTrainerData;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static TestTrainerData create() => TestTrainerData._();
|
||||
TestTrainerData createEmptyInstance() => create();
|
||||
static $pb.PbList<TestTrainerData> createRepeated() => $pb.PbList<TestTrainerData>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static TestTrainerData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<TestTrainerData>(create);
|
||||
static TestTrainerData? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.int get power => $_getIZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set power($core.int v) { $_setSignedInt32(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasPower() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearPower() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.int get cadence => $_getIZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set cadence($core.int v) { $_setSignedInt32(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasCadence() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearCadence() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.int get bikeSpeed => $_getIZ(2);
|
||||
@$pb.TagNumber(3)
|
||||
set bikeSpeed($core.int v) { $_setSignedInt32(2, v); }
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasBikeSpeed() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearBikeSpeed() => clearField(3);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
$core.int get averagedPower => $_getIZ(3);
|
||||
@$pb.TagNumber(4)
|
||||
set averagedPower($core.int v) { $_setSignedInt32(3, v); }
|
||||
@$pb.TagNumber(4)
|
||||
$core.bool hasAveragedPower() => $_has(3);
|
||||
@$pb.TagNumber(4)
|
||||
void clearAveragedPower() => clearField(4);
|
||||
|
||||
@$pb.TagNumber(5)
|
||||
$core.int get wheelSpeed => $_getIZ(4);
|
||||
@$pb.TagNumber(5)
|
||||
set wheelSpeed($core.int v) { $_setSignedInt32(4, v); }
|
||||
@$pb.TagNumber(5)
|
||||
$core.bool hasWheelSpeed() => $_has(4);
|
||||
@$pb.TagNumber(5)
|
||||
void clearWheelSpeed() => clearField(5);
|
||||
|
||||
@$pb.TagNumber(6)
|
||||
$core.int get calculatedRealGearRatio => $_getIZ(5);
|
||||
@$pb.TagNumber(6)
|
||||
set calculatedRealGearRatio($core.int v) { $_setSignedInt32(5, v); }
|
||||
@$pb.TagNumber(6)
|
||||
$core.bool hasCalculatedRealGearRatio() => $_has(5);
|
||||
@$pb.TagNumber(6)
|
||||
void clearCalculatedRealGearRatio() => clearField(6);
|
||||
}
|
||||
|
||||
|
||||
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
|
||||
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');
|
||||
101
lib/bluetooth/protocol/zp_vendor.pbenum.dart
Normal file
101
lib/bluetooth/protocol/zp_vendor.pbenum.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: zp_vendor.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
class VendorOpcode extends $pb.ProtobufEnum {
|
||||
static const VendorOpcode UNDEFINED = VendorOpcode._(0, _omitEnumNames ? '' : 'UNDEFINED');
|
||||
static const VendorOpcode CONTROLLER_SYNC = VendorOpcode._(1, _omitEnumNames ? '' : 'CONTROLLER_SYNC');
|
||||
static const VendorOpcode PAIR_DEVICES = VendorOpcode._(2, _omitEnumNames ? '' : 'PAIR_DEVICES');
|
||||
static const VendorOpcode ENABLE_TEST_MODE = VendorOpcode._(65280, _omitEnumNames ? '' : 'ENABLE_TEST_MODE');
|
||||
static const VendorOpcode SET_DFU_TEST = VendorOpcode._(65281, _omitEnumNames ? '' : 'SET_DFU_TEST');
|
||||
static const VendorOpcode SET_TRAINER_TEST_DATA = VendorOpcode._(65282, _omitEnumNames ? '' : 'SET_TRAINER_TEST_DATA');
|
||||
static const VendorOpcode SET_INPUT_DEVICE_TEST_DATA = VendorOpcode._(65283, _omitEnumNames ? '' : 'SET_INPUT_DEVICE_TEST_DATA');
|
||||
static const VendorOpcode SET_GEAR_TEST_DATA = VendorOpcode._(65284, _omitEnumNames ? '' : 'SET_GEAR_TEST_DATA');
|
||||
static const VendorOpcode SET_HRM_TEST_DATA = VendorOpcode._(65285, _omitEnumNames ? '' : 'SET_HRM_TEST_DATA');
|
||||
static const VendorOpcode SET_TEST_DATA = VendorOpcode._(65286, _omitEnumNames ? '' : 'SET_TEST_DATA');
|
||||
|
||||
static const $core.List<VendorOpcode> values = <VendorOpcode> [
|
||||
UNDEFINED,
|
||||
CONTROLLER_SYNC,
|
||||
PAIR_DEVICES,
|
||||
ENABLE_TEST_MODE,
|
||||
SET_DFU_TEST,
|
||||
SET_TRAINER_TEST_DATA,
|
||||
SET_INPUT_DEVICE_TEST_DATA,
|
||||
SET_GEAR_TEST_DATA,
|
||||
SET_HRM_TEST_DATA,
|
||||
SET_TEST_DATA,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, VendorOpcode> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static VendorOpcode? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const VendorOpcode._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class PairDeviceType extends $pb.ProtobufEnum {
|
||||
static const PairDeviceType BLE = PairDeviceType._(0, _omitEnumNames ? '' : 'BLE');
|
||||
static const PairDeviceType ANT = PairDeviceType._(1, _omitEnumNames ? '' : 'ANT');
|
||||
|
||||
static const $core.List<PairDeviceType> values = <PairDeviceType> [
|
||||
BLE,
|
||||
ANT,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, PairDeviceType> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static PairDeviceType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const PairDeviceType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
/// Status used by ControllerSync
|
||||
class ControllerSyncStatus extends $pb.ProtobufEnum {
|
||||
static const ControllerSyncStatus NOT_CONNECTED = ControllerSyncStatus._(0, _omitEnumNames ? '' : 'NOT_CONNECTED');
|
||||
static const ControllerSyncStatus CONNECTED = ControllerSyncStatus._(1, _omitEnumNames ? '' : 'CONNECTED');
|
||||
|
||||
static const $core.List<ControllerSyncStatus> values = <ControllerSyncStatus> [
|
||||
NOT_CONNECTED,
|
||||
CONNECTED,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, ControllerSyncStatus> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static ControllerSyncStatus? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const ControllerSyncStatus._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
/// Looks like “data object / page” IDs used with pairing pages
|
||||
class VendorDO extends $pb.ProtobufEnum {
|
||||
static const VendorDO NO_CLUE = VendorDO._(0, _omitEnumNames ? '' : 'NO_CLUE');
|
||||
static const VendorDO PAGE_DEVICE_PAIRING = VendorDO._(61440, _omitEnumNames ? '' : 'PAGE_DEVICE_PAIRING');
|
||||
static const VendorDO DEVICE_COUNT = VendorDO._(61441, _omitEnumNames ? '' : 'DEVICE_COUNT');
|
||||
static const VendorDO PAIRING_STATUS = VendorDO._(61442, _omitEnumNames ? '' : 'PAIRING_STATUS');
|
||||
static const VendorDO PAIRED_DEVICE = VendorDO._(61443, _omitEnumNames ? '' : 'PAIRED_DEVICE');
|
||||
|
||||
static const $core.List<VendorDO> values = <VendorDO> [
|
||||
NO_CLUE,
|
||||
PAGE_DEVICE_PAIRING,
|
||||
DEVICE_COUNT,
|
||||
PAIRING_STATUS,
|
||||
PAIRED_DEVICE,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, VendorDO> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static VendorDO? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const VendorDO._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
|
||||
const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names');
|
||||
267
lib/bluetooth/protocol/zp_vendor.pbjson.dart
Normal file
267
lib/bluetooth/protocol/zp_vendor.pbjson.dart
Normal file
@@ -0,0 +1,267 @@
|
||||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: zp_vendor.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
import 'dart:convert' as $convert;
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:typed_data' as $typed_data;
|
||||
|
||||
@$core.Deprecated('Use vendorOpcodeDescriptor instead')
|
||||
const VendorOpcode$json = {
|
||||
'1': 'VendorOpcode',
|
||||
'2': [
|
||||
{'1': 'UNDEFINED', '2': 0},
|
||||
{'1': 'CONTROLLER_SYNC', '2': 1},
|
||||
{'1': 'PAIR_DEVICES', '2': 2},
|
||||
{'1': 'ENABLE_TEST_MODE', '2': 65280},
|
||||
{'1': 'SET_DFU_TEST', '2': 65281},
|
||||
{'1': 'SET_TRAINER_TEST_DATA', '2': 65282},
|
||||
{'1': 'SET_INPUT_DEVICE_TEST_DATA', '2': 65283},
|
||||
{'1': 'SET_GEAR_TEST_DATA', '2': 65284},
|
||||
{'1': 'SET_HRM_TEST_DATA', '2': 65285},
|
||||
{'1': 'SET_TEST_DATA', '2': 65286},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `VendorOpcode`. Decode as a `google.protobuf.EnumDescriptorProto`.
|
||||
final $typed_data.Uint8List vendorOpcodeDescriptor = $convert.base64Decode(
|
||||
'CgxWZW5kb3JPcGNvZGUSDQoJVU5ERUZJTkVEEAASEwoPQ09OVFJPTExFUl9TWU5DEAESEAoMUE'
|
||||
'FJUl9ERVZJQ0VTEAISFgoQRU5BQkxFX1RFU1RfTU9ERRCA/gMSEgoMU0VUX0RGVV9URVNUEIH+'
|
||||
'AxIbChVTRVRfVFJBSU5FUl9URVNUX0RBVEEQgv4DEiAKGlNFVF9JTlBVVF9ERVZJQ0VfVEVTVF'
|
||||
'9EQVRBEIP+AxIYChJTRVRfR0VBUl9URVNUX0RBVEEQhP4DEhcKEVNFVF9IUk1fVEVTVF9EQVRB'
|
||||
'EIX+AxITCg1TRVRfVEVTVF9EQVRBEIb+Aw==');
|
||||
|
||||
@$core.Deprecated('Use pairDeviceTypeDescriptor instead')
|
||||
const PairDeviceType$json = {
|
||||
'1': 'PairDeviceType',
|
||||
'2': [
|
||||
{'1': 'BLE', '2': 0},
|
||||
{'1': 'ANT', '2': 1},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `PairDeviceType`. Decode as a `google.protobuf.EnumDescriptorProto`.
|
||||
final $typed_data.Uint8List pairDeviceTypeDescriptor = $convert.base64Decode(
|
||||
'Cg5QYWlyRGV2aWNlVHlwZRIHCgNCTEUQABIHCgNBTlQQAQ==');
|
||||
|
||||
@$core.Deprecated('Use controllerSyncStatusDescriptor instead')
|
||||
const ControllerSyncStatus$json = {
|
||||
'1': 'ControllerSyncStatus',
|
||||
'2': [
|
||||
{'1': 'NOT_CONNECTED', '2': 0},
|
||||
{'1': 'CONNECTED', '2': 1},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `ControllerSyncStatus`. Decode as a `google.protobuf.EnumDescriptorProto`.
|
||||
final $typed_data.Uint8List controllerSyncStatusDescriptor = $convert.base64Decode(
|
||||
'ChRDb250cm9sbGVyU3luY1N0YXR1cxIRCg1OT1RfQ09OTkVDVEVEEAASDQoJQ09OTkVDVEVEEA'
|
||||
'E=');
|
||||
|
||||
@$core.Deprecated('Use vendorDODescriptor instead')
|
||||
const VendorDO$json = {
|
||||
'1': 'VendorDO',
|
||||
'2': [
|
||||
{'1': 'NO_CLUE', '2': 0},
|
||||
{'1': 'PAGE_DEVICE_PAIRING', '2': 61440},
|
||||
{'1': 'DEVICE_COUNT', '2': 61441},
|
||||
{'1': 'PAIRING_STATUS', '2': 61442},
|
||||
{'1': 'PAIRED_DEVICE', '2': 61443},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `VendorDO`. Decode as a `google.protobuf.EnumDescriptorProto`.
|
||||
final $typed_data.Uint8List vendorDODescriptor = $convert.base64Decode(
|
||||
'CghWZW5kb3JETxILCgdOT19DTFVFEAASGQoTUEFHRV9ERVZJQ0VfUEFJUklORxCA4AMSEgoMRE'
|
||||
'VWSUNFX0NPVU5UEIHgAxIUCg5QQUlSSU5HX1NUQVRVUxCC4AMSEwoNUEFJUkVEX0RFVklDRRCD'
|
||||
'4AM=');
|
||||
|
||||
@$core.Deprecated('Use controllerSyncDescriptor instead')
|
||||
const ControllerSync$json = {
|
||||
'1': 'ControllerSync',
|
||||
'2': [
|
||||
{'1': 'status', '3': 1, '4': 1, '5': 14, '6': '.com.zwift.protobuf.ControllerSyncStatus', '10': 'status'},
|
||||
{'1': 'timeStamp', '3': 2, '4': 1, '5': 5, '10': 'timeStamp'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `ControllerSync`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List controllerSyncDescriptor = $convert.base64Decode(
|
||||
'Cg5Db250cm9sbGVyU3luYxJACgZzdGF0dXMYASABKA4yKC5jb20uendpZnQucHJvdG9idWYuQ2'
|
||||
'9udHJvbGxlclN5bmNTdGF0dXNSBnN0YXR1cxIcCgl0aW1lU3RhbXAYAiABKAVSCXRpbWVTdGFt'
|
||||
'cA==');
|
||||
|
||||
@$core.Deprecated('Use enableTestModeDescriptor instead')
|
||||
const EnableTestMode$json = {
|
||||
'1': 'EnableTestMode',
|
||||
'2': [
|
||||
{'1': 'enable', '3': 1, '4': 1, '5': 8, '10': 'enable'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `EnableTestMode`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List enableTestModeDescriptor = $convert.base64Decode(
|
||||
'Cg5FbmFibGVUZXN0TW9kZRIWCgZlbmFibGUYASABKAhSBmVuYWJsZQ==');
|
||||
|
||||
@$core.Deprecated('Use pairDevicesDescriptor instead')
|
||||
const PairDevices$json = {
|
||||
'1': 'PairDevices',
|
||||
'2': [
|
||||
{'1': 'pair', '3': 1, '4': 1, '5': 8, '10': 'pair'},
|
||||
{'1': 'type', '3': 2, '4': 1, '5': 14, '6': '.com.zwift.protobuf.PairDeviceType', '10': 'type'},
|
||||
{'1': 'deviceId', '3': 3, '4': 1, '5': 12, '10': 'deviceId'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `PairDevices`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List pairDevicesDescriptor = $convert.base64Decode(
|
||||
'CgtQYWlyRGV2aWNlcxISCgRwYWlyGAEgASgIUgRwYWlyEjYKBHR5cGUYAiABKA4yIi5jb20uen'
|
||||
'dpZnQucHJvdG9idWYuUGFpckRldmljZVR5cGVSBHR5cGUSGgoIZGV2aWNlSWQYAyABKAxSCGRl'
|
||||
'dmljZUlk');
|
||||
|
||||
@$core.Deprecated('Use devicePairingDataPageDescriptor instead')
|
||||
const DevicePairingDataPage$json = {
|
||||
'1': 'DevicePairingDataPage',
|
||||
'2': [
|
||||
{'1': 'devicesCount', '3': 1, '4': 1, '5': 5, '10': 'devicesCount'},
|
||||
{'1': 'pairingStatus', '3': 2, '4': 1, '5': 5, '10': 'pairingStatus'},
|
||||
{'1': 'pairingDevList', '3': 3, '4': 3, '5': 11, '6': '.com.zwift.protobuf.DevicePairingDataPage.PairedDevice', '10': 'pairingDevList'},
|
||||
],
|
||||
'3': [DevicePairingDataPage_PairedDevice$json],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use devicePairingDataPageDescriptor instead')
|
||||
const DevicePairingDataPage_PairedDevice$json = {
|
||||
'1': 'PairedDevice',
|
||||
'2': [
|
||||
{'1': 'device', '3': 1, '4': 1, '5': 12, '10': 'device'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `DevicePairingDataPage`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List devicePairingDataPageDescriptor = $convert.base64Decode(
|
||||
'ChVEZXZpY2VQYWlyaW5nRGF0YVBhZ2USIgoMZGV2aWNlc0NvdW50GAEgASgFUgxkZXZpY2VzQ2'
|
||||
'91bnQSJAoNcGFpcmluZ1N0YXR1cxgCIAEoBVINcGFpcmluZ1N0YXR1cxJeCg5wYWlyaW5nRGV2'
|
||||
'TGlzdBgDIAMoCzI2LmNvbS56d2lmdC5wcm90b2J1Zi5EZXZpY2VQYWlyaW5nRGF0YVBhZ2UuUG'
|
||||
'FpcmVkRGV2aWNlUg5wYWlyaW5nRGV2TGlzdBomCgxQYWlyZWREZXZpY2USFgoGZGV2aWNlGAEg'
|
||||
'ASgMUgZkZXZpY2U=');
|
||||
|
||||
@$core.Deprecated('Use setDfuTestDescriptor instead')
|
||||
const SetDfuTest$json = {
|
||||
'1': 'SetDfuTest',
|
||||
'2': [
|
||||
{'1': 'failedEnterDfu', '3': 1, '4': 1, '5': 8, '9': 0, '10': 'failedEnterDfu'},
|
||||
{'1': 'failedStartAdvertising', '3': 2, '4': 1, '5': 8, '9': 0, '10': 'failedStartAdvertising'},
|
||||
{'1': 'crcFailure', '3': 3, '4': 1, '5': 5, '9': 0, '10': 'crcFailure'},
|
||||
],
|
||||
'8': [
|
||||
{'1': 'test_case'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `SetDfuTest`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List setDfuTestDescriptor = $convert.base64Decode(
|
||||
'CgpTZXREZnVUZXN0EigKDmZhaWxlZEVudGVyRGZ1GAEgASgISABSDmZhaWxlZEVudGVyRGZ1Ej'
|
||||
'gKFmZhaWxlZFN0YXJ0QWR2ZXJ0aXNpbmcYAiABKAhIAFIWZmFpbGVkU3RhcnRBZHZlcnRpc2lu'
|
||||
'ZxIgCgpjcmNGYWlsdXJlGAMgASgFSABSCmNyY0ZhaWx1cmVCCwoJdGVzdF9jYXNl');
|
||||
|
||||
@$core.Deprecated('Use setGearTestDataDescriptor instead')
|
||||
const SetGearTestData$json = {
|
||||
'1': 'SetGearTestData',
|
||||
'2': [
|
||||
{'1': 'frontGearIdx', '3': 1, '4': 1, '5': 5, '10': 'frontGearIdx'},
|
||||
{'1': 'rearGearIdx', '3': 2, '4': 1, '5': 5, '10': 'rearGearIdx'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `SetGearTestData`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List setGearTestDataDescriptor = $convert.base64Decode(
|
||||
'Cg9TZXRHZWFyVGVzdERhdGESIgoMZnJvbnRHZWFySWR4GAEgASgFUgxmcm9udEdlYXJJZHgSIA'
|
||||
'oLcmVhckdlYXJJZHgYAiABKAVSC3JlYXJHZWFySWR4');
|
||||
|
||||
@$core.Deprecated('Use setHrmTestDataDescriptor instead')
|
||||
const SetHrmTestData$json = {
|
||||
'1': 'SetHrmTestData',
|
||||
'2': [
|
||||
{'1': 'hrmPresent', '3': 1, '4': 1, '5': 8, '10': 'hrmPresent'},
|
||||
{'1': 'heartRate', '3': 2, '4': 1, '5': 5, '10': 'heartRate'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `SetHrmTestData`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List setHrmTestDataDescriptor = $convert.base64Decode(
|
||||
'Cg5TZXRIcm1UZXN0RGF0YRIeCgpocm1QcmVzZW50GAEgASgIUgpocm1QcmVzZW50EhwKCWhlYX'
|
||||
'J0UmF0ZRgCIAEoBVIJaGVhcnRSYXRl');
|
||||
|
||||
@$core.Deprecated('Use setInputDeviceTestDataDescriptor instead')
|
||||
const SetInputDeviceTestData$json = {
|
||||
'1': 'SetInputDeviceTestData',
|
||||
'2': [
|
||||
{'1': 'duration', '3': 1, '4': 1, '5': 5, '10': 'duration'},
|
||||
{'1': 'buttonEvent', '3': 2, '4': 1, '5': 5, '10': 'buttonEvent'},
|
||||
{'1': 'analogEventList', '3': 3, '4': 3, '5': 11, '6': '.com.zwift.protobuf.SetInputDeviceTestData.ControllerAnalogEvent', '10': 'analogEventList'},
|
||||
],
|
||||
'3': [SetInputDeviceTestData_ControllerAnalogEvent$json],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use setInputDeviceTestDataDescriptor instead')
|
||||
const SetInputDeviceTestData_ControllerAnalogEvent$json = {
|
||||
'1': 'ControllerAnalogEvent',
|
||||
'2': [
|
||||
{'1': 'sensorId', '3': 1, '4': 1, '5': 5, '10': 'sensorId'},
|
||||
{'1': 'value', '3': 2, '4': 1, '5': 5, '10': 'value'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `SetInputDeviceTestData`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List setInputDeviceTestDataDescriptor = $convert.base64Decode(
|
||||
'ChZTZXRJbnB1dERldmljZVRlc3REYXRhEhoKCGR1cmF0aW9uGAEgASgFUghkdXJhdGlvbhIgCg'
|
||||
'tidXR0b25FdmVudBgCIAEoBVILYnV0dG9uRXZlbnQSagoPYW5hbG9nRXZlbnRMaXN0GAMgAygL'
|
||||
'MkAuY29tLnp3aWZ0LnByb3RvYnVmLlNldElucHV0RGV2aWNlVGVzdERhdGEuQ29udHJvbGxlck'
|
||||
'FuYWxvZ0V2ZW50Ug9hbmFsb2dFdmVudExpc3QaSQoVQ29udHJvbGxlckFuYWxvZ0V2ZW50EhoK'
|
||||
'CHNlbnNvcklkGAEgASgFUghzZW5zb3JJZBIUCgV2YWx1ZRgCIAEoBVIFdmFsdWU=');
|
||||
|
||||
@$core.Deprecated('Use setTrainerTestDataDescriptor instead')
|
||||
const SetTrainerTestData$json = {
|
||||
'1': 'SetTrainerTestData',
|
||||
'2': [
|
||||
{'1': 'dataMode', '3': 1, '4': 1, '5': 5, '10': 'dataMode'},
|
||||
{'1': 'interfaces', '3': 2, '4': 1, '5': 5, '10': 'interfaces'},
|
||||
{'1': 'testTrainerData', '3': 3, '4': 1, '5': 11, '6': '.com.zwift.protobuf.TestTrainerData', '10': 'testTrainerData'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `SetTrainerTestData`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List setTrainerTestDataDescriptor = $convert.base64Decode(
|
||||
'ChJTZXRUcmFpbmVyVGVzdERhdGESGgoIZGF0YU1vZGUYASABKAVSCGRhdGFNb2RlEh4KCmludG'
|
||||
'VyZmFjZXMYAiABKAVSCmludGVyZmFjZXMSTQoPdGVzdFRyYWluZXJEYXRhGAMgASgLMiMuY29t'
|
||||
'Lnp3aWZ0LnByb3RvYnVmLlRlc3RUcmFpbmVyRGF0YVIPdGVzdFRyYWluZXJEYXRh');
|
||||
|
||||
@$core.Deprecated('Use testTrainerDataDescriptor instead')
|
||||
const TestTrainerData$json = {
|
||||
'1': 'TestTrainerData',
|
||||
'2': [
|
||||
{'1': 'power', '3': 1, '4': 1, '5': 5, '10': 'power'},
|
||||
{'1': 'cadence', '3': 2, '4': 1, '5': 5, '10': 'cadence'},
|
||||
{'1': 'bikeSpeed', '3': 3, '4': 1, '5': 5, '10': 'bikeSpeed'},
|
||||
{'1': 'averagedPower', '3': 4, '4': 1, '5': 5, '10': 'averagedPower'},
|
||||
{'1': 'wheelSpeed', '3': 5, '4': 1, '5': 5, '10': 'wheelSpeed'},
|
||||
{'1': 'calculatedRealGearRatio', '3': 6, '4': 1, '5': 5, '10': 'calculatedRealGearRatio'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `TestTrainerData`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List testTrainerDataDescriptor = $convert.base64Decode(
|
||||
'Cg9UZXN0VHJhaW5lckRhdGESFAoFcG93ZXIYASABKAVSBXBvd2VyEhgKB2NhZGVuY2UYAiABKA'
|
||||
'VSB2NhZGVuY2USHAoJYmlrZVNwZWVkGAMgASgFUgliaWtlU3BlZWQSJAoNYXZlcmFnZWRQb3dl'
|
||||
'chgEIAEoBVINYXZlcmFnZWRQb3dlchIeCgp3aGVlbFNwZWVkGAUgASgFUgp3aGVlbFNwZWVkEj'
|
||||
'gKF2NhbGN1bGF0ZWRSZWFsR2VhclJhdGlvGAYgASgFUhdjYWxjdWxhdGVkUmVhbEdlYXJSYXRp'
|
||||
'bw==');
|
||||
|
||||
14
lib/bluetooth/protocol/zp_vendor.pbserver.dart
Normal file
14
lib/bluetooth/protocol/zp_vendor.pbserver.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: zp_vendor.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'zp_vendor.pb.dart';
|
||||
|
||||
@@ -24,7 +24,7 @@ void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
if (kIsWeb) {
|
||||
actionHandler = StubActions();
|
||||
} else if (Platform.isAndroid) {
|
||||
} else if (Platform.isAndroid || Platform.isIOS) {
|
||||
actionHandler = AndroidActions();
|
||||
} else {
|
||||
actionHandler = DesktopActions();
|
||||
|
||||
98
lib/pages/changelog.dart
Normal file
98
lib/pages/changelog.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/utils/changelog.dart';
|
||||
|
||||
class ChangelogPage extends StatefulWidget {
|
||||
const ChangelogPage({super.key});
|
||||
|
||||
@override
|
||||
State<ChangelogPage> createState() => _ChangelogPageState();
|
||||
}
|
||||
|
||||
class _ChangelogPageState extends State<ChangelogPage> {
|
||||
List<ChangelogEntry>? _entries;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadChangelog();
|
||||
}
|
||||
|
||||
Future<void> _loadChangelog() async {
|
||||
try {
|
||||
final entries = await ChangelogParser.parse();
|
||||
setState(() {
|
||||
_entries = entries;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = 'Failed to load changelog: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Changelog'),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
body: _error != null
|
||||
? Center(child: Text(_error!))
|
||||
: _entries == null
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: ListView.builder(
|
||||
padding: EdgeInsets.all(16),
|
||||
itemCount: _entries!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = _entries![index];
|
||||
return Card(
|
||||
margin: EdgeInsets.only(bottom: 16),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Version ${entry.version}',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
entry.date,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
...entry.changes.map(
|
||||
(change) => Padding(
|
||||
padding: EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('• ', style: TextStyle(fontSize: 16)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
change,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import 'dart:io';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift_clickv2.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/pages/touch_area.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
import 'package:swift_control/widgets/logviewer.dart';
|
||||
import 'package:swift_control/widgets/testbed.dart';
|
||||
import 'package:swift_control/widgets/title.dart';
|
||||
|
||||
import '../bluetooth/devices/base_device.dart';
|
||||
@@ -52,114 +54,172 @@ class _DevicePageState extends State<DevicePage> {
|
||||
onPopInvokedWithResult: (hello, _) {
|
||||
connection.reset();
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: AppTitle(),
|
||||
actions: buildMenuButtons(),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
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.runtimeType}: ${it.isConnected ? 'Connected' : 'Not connected'}${it.batteryLevel != null ? ' - Battery Level: ${it.batteryLevel}%' : ''}";
|
||||
})}',
|
||||
),
|
||||
Divider(color: Theme.of(context).colorScheme.primary, height: 30),
|
||||
if (!kIsWeb)
|
||||
Column(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flex(
|
||||
child: Stack(
|
||||
children: [
|
||||
Scaffold(
|
||||
appBar: AppBar(
|
||||
title: AppTitle(),
|
||||
actions: buildMenuButtons(),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text('Connected Devices:', style: Theme.of(context).textTheme.titleMedium),
|
||||
|
||||
if (connection.devices.any((device) => (device is ZwiftClickV2) && device.isConnected))
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
'''To make your Zwift Click V2 work best you should connect it in the Zwift app once each day.\nIf you don't do that SwiftControl will need to reconnect every minute.
|
||||
|
||||
1. Open Zwift app
|
||||
2. Log in (subscription not required) and open the device connection screen
|
||||
3. Connect your Trainer, then connect the Zwift Click V2
|
||||
4. Close the Zwift app again and connect again in SwiftControl''',
|
||||
),
|
||||
),
|
||||
Text(
|
||||
connection.devices.joinToString(
|
||||
separator: '\n',
|
||||
transform: (it) {
|
||||
return """${it.device.name ?? it.runtimeType}: ${it.isConnected ? 'Connected' : 'Not connected'}
|
||||
${it.batteryLevel != null ? ' - Battery Level: ${it.batteryLevel}%' : ''}
|
||||
${it.firmwareVersion != null ? ' - Firmware Version: ${it.firmwareVersion}' : ''}""".trim();
|
||||
},
|
||||
),
|
||||
),
|
||||
Divider(color: Theme.of(context).colorScheme.primary, height: 30),
|
||||
if (!kIsWeb)
|
||||
Column(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
direction: MediaQuery.sizeOf(context).width > 600 ? Axis.horizontal : Axis.vertical,
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownMenu<SupportedApp>(
|
||||
controller: controller,
|
||||
dropdownMenuEntries:
|
||||
SupportedApp.supportedApps
|
||||
.map((app) => DropdownMenuEntry<SupportedApp>(value: app, label: app.name))
|
||||
.toList(),
|
||||
label: Text('Select Keymap'),
|
||||
onSelected: (app) async {
|
||||
if (app == null) {
|
||||
return;
|
||||
}
|
||||
controller.text = app.name ?? '';
|
||||
actionHandler.supportedApp = app;
|
||||
settings.setApp(app);
|
||||
setState(() {});
|
||||
if (app is! CustomApp && !kIsWeb && (Platform.isMacOS || Platform.isWindows)) {
|
||||
_snackBarMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Customize the keymap if you experience any issues (e.g. wrong keyboard output)',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
initialSelection: actionHandler.supportedApp,
|
||||
hintText: 'Select your Keymap',
|
||||
),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (actionHandler.supportedApp! is! CustomApp) {
|
||||
final customApp = CustomApp();
|
||||
|
||||
actionHandler.supportedApp!.keymap.keyPairs.forEachIndexed((pair, index) {
|
||||
pair.buttons.forEachIndexed((button, indexB) {
|
||||
customApp.setKey(
|
||||
button,
|
||||
physicalKey: pair.physicalKey!,
|
||||
logicalKey: pair.logicalKey,
|
||||
isLongPress: pair.isLongPress,
|
||||
touchPosition:
|
||||
pair.touchPosition != Offset.zero
|
||||
? pair.touchPosition
|
||||
: Offset(((indexB + 1)) * 100, 200 + (index * 100)),
|
||||
Flex(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
direction: MediaQuery.sizeOf(context).width > 600 ? Axis.horizontal : Axis.vertical,
|
||||
spacing: 8,
|
||||
children: [
|
||||
DropdownMenu<SupportedApp>(
|
||||
controller: controller,
|
||||
dropdownMenuEntries:
|
||||
SupportedApp.supportedApps
|
||||
.map((app) => DropdownMenuEntry<SupportedApp>(value: app, label: app.name))
|
||||
.toList(),
|
||||
label: Text('Select Keymap / app'),
|
||||
onSelected: (app) async {
|
||||
if (app == null) {
|
||||
return;
|
||||
}
|
||||
controller.text = app.name ?? '';
|
||||
actionHandler.supportedApp = app;
|
||||
await settings.setApp(app);
|
||||
setState(() {});
|
||||
if (app is! CustomApp && !kIsWeb && (Platform.isMacOS || Platform.isWindows)) {
|
||||
_snackBarMessengerKey.currentState!.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Customize the keymap if you experience any issues (e.g. wrong keyboard output)',
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
initialSelection: actionHandler.supportedApp,
|
||||
hintText: 'Select your Keymap',
|
||||
),
|
||||
|
||||
actionHandler.supportedApp = customApp;
|
||||
settings.setApp(customApp);
|
||||
}
|
||||
final result = await Navigator.of(
|
||||
context,
|
||||
).push<bool>(MaterialPageRoute(builder: (_) => TouchAreaSetupPage()));
|
||||
if (result == true && actionHandler.supportedApp is CustomApp) {
|
||||
settings.setApp(actionHandler.supportedApp!);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Text('Customize Keymap'),
|
||||
if (actionHandler.supportedApp != null)
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (actionHandler.supportedApp! is! CustomApp) {
|
||||
final customApp = CustomApp();
|
||||
|
||||
final connectedDevice = connection.devices.firstOrNull;
|
||||
actionHandler.supportedApp!.keymap.keyPairs.forEachIndexed((pair, index) {
|
||||
pair.buttons
|
||||
.filter(
|
||||
(button) => connectedDevice?.availableButtons.contains(button) == true,
|
||||
)
|
||||
.forEachIndexed((button, indexB) {
|
||||
customApp.setKey(
|
||||
button,
|
||||
physicalKey: pair.physicalKey!,
|
||||
logicalKey: pair.logicalKey,
|
||||
isLongPress: pair.isLongPress,
|
||||
touchPosition:
|
||||
pair.touchPosition != Offset.zero
|
||||
? pair.touchPosition
|
||||
: Offset(((indexB + 1)) * 100, 200 + (index * 100)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
actionHandler.supportedApp = customApp;
|
||||
await settings.setApp(customApp);
|
||||
}
|
||||
final result = await Navigator.of(
|
||||
context,
|
||||
).push<bool>(MaterialPageRoute(builder: (_) => TouchAreaSetupPage()));
|
||||
|
||||
if (result == true && actionHandler.supportedApp is CustomApp) {
|
||||
await settings.setApp(actionHandler.supportedApp!);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Text('Customize Keymap'),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (actionHandler.supportedApp != null)
|
||||
KeymapExplanation(
|
||||
key: Key(actionHandler.supportedApp!.keymap.runtimeType.toString()),
|
||||
keymap: actionHandler.supportedApp!.keymap,
|
||||
onUpdate: () {
|
||||
setState(() {});
|
||||
controller.text = actionHandler.supportedApp?.name ?? '';
|
||||
},
|
||||
),
|
||||
if (connection.devices.any(
|
||||
(device) =>
|
||||
(device.device.name == 'Zwift Ride' || device.device.name == 'Zwift Play') &&
|
||||
device.isConnected,
|
||||
))
|
||||
SwitchListTile(
|
||||
title: Text('Vibration on Shift'),
|
||||
subtitle: Text('Enable vibration feedback when shifting gears'),
|
||||
value: settings.getVibrationEnabled(),
|
||||
onChanged: (value) async {
|
||||
await settings.setVibrationEnabled(value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (kDebugMode &&
|
||||
connection.devices.any((device) => (device is ZwiftClickV2) && device.isConnected))
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
(connection.devices.first as ZwiftClickV2).test();
|
||||
},
|
||||
child: Text('Test'),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (actionHandler.supportedApp != null)
|
||||
KeymapExplanation(
|
||||
key: Key(actionHandler.supportedApp!.keymap.runtimeType.toString()),
|
||||
keymap: actionHandler.supportedApp!.keymap,
|
||||
onUpdate: () {
|
||||
setState(() {});
|
||||
controller.text = actionHandler.supportedApp?.name ?? '';
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 800, child: LogViewer()),
|
||||
],
|
||||
LogViewer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(child: Testbed()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,8 +3,10 @@ import 'dart:io';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/requirements/platform.dart';
|
||||
import 'package:swift_control/widgets/changelog_dialog.dart';
|
||||
import 'package:swift_control/widgets/menu.dart';
|
||||
import 'package:swift_control/widgets/title.dart';
|
||||
|
||||
@@ -30,8 +32,9 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
|
||||
// call after first frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
settings.init().then((_) {
|
||||
_checkAndShowChangelog();
|
||||
if (!kIsWeb && Platform.isMacOS) {
|
||||
// add more delay due tu CBManagerStateUnknown
|
||||
// add more delay due to CBManagerStateUnknown
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
_reloadRequirements();
|
||||
});
|
||||
@@ -48,6 +51,23 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkAndShowChangelog() async {
|
||||
try {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final currentVersion = packageInfo.version;
|
||||
final lastSeenVersion = settings.getLastSeenVersion();
|
||||
|
||||
if (mounted) {
|
||||
await ChangelogDialog.showIfNeeded(context, currentVersion, lastSeenVersion);
|
||||
}
|
||||
|
||||
// Update last seen version
|
||||
await settings.setLastSeenVersion(currentVersion);
|
||||
} catch (e) {
|
||||
print('Failed to check changelog: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
@@ -72,55 +92,68 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
|
||||
body:
|
||||
_requirements.isEmpty
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: Stepper(
|
||||
currentStep: _currentStep,
|
||||
connectorColor: WidgetStateProperty.resolveWith<Color>(
|
||||
(Set<WidgetState> states) => Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onStepContinue:
|
||||
_currentStep < _requirements.length
|
||||
? () {
|
||||
setState(() {
|
||||
_currentStep += 1;
|
||||
});
|
||||
: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0),
|
||||
child: Text(
|
||||
'Please complete the following requirements to make the app work correctly:',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Stepper(
|
||||
currentStep: _currentStep,
|
||||
connectorColor: WidgetStateProperty.resolveWith<Color>(
|
||||
(Set<WidgetState> states) => Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onStepContinue:
|
||||
_currentStep < _requirements.length
|
||||
? () {
|
||||
setState(() {
|
||||
_currentStep += 1;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
onStepTapped: (step) {
|
||||
if (_requirements[step].status) {
|
||||
return;
|
||||
}
|
||||
: null,
|
||||
onStepTapped: (step) {
|
||||
if (_requirements[step].status) {
|
||||
return;
|
||||
}
|
||||
final hasEarlierIncomplete = _requirements.indexWhere((req) => !req.status) < step;
|
||||
if (hasEarlierIncomplete) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_currentStep = step;
|
||||
});
|
||||
},
|
||||
controlsBuilder: (context, details) => Container(),
|
||||
steps:
|
||||
_requirements
|
||||
.mapIndexed(
|
||||
(index, req) => Step(
|
||||
title: 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),
|
||||
final hasEarlierIncomplete = _requirements.indexWhere((req) => !req.status) < step;
|
||||
if (hasEarlierIncomplete) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_currentStep = step;
|
||||
});
|
||||
},
|
||||
controlsBuilder: (context, details) => Container(),
|
||||
steps:
|
||||
_requirements
|
||||
.mapIndexed(
|
||||
(index, req) => Step(
|
||||
title: 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,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
state: req.status ? StepState.complete : StepState.indexed,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/widgets/small_progress_indicator.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import '../widgets/logviewer.dart';
|
||||
|
||||
@@ -45,9 +46,7 @@ class _ScanWidgetState extends State<ScanWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(minHeight: 200),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.all(16),
|
||||
shrinkWrap: true,
|
||||
child: Column(
|
||||
children: [
|
||||
ValueListenableBuilder(
|
||||
valueListenable: connection.isScanning,
|
||||
@@ -59,6 +58,14 @@ class _ScanWidgetState extends State<ScanWidget> {
|
||||
Text(
|
||||
'Scanning for devices... Make sure they are powered on and in range and not connected to another device.',
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
launchUrlString(
|
||||
'https://github.com/jonasbark/swiftcontrol/?tab=readme-ov-file#supported-platforms',
|
||||
);
|
||||
},
|
||||
child: const Text("Show Troubleshooting Guide"),
|
||||
),
|
||||
SmallProgressIndicator(),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:flutter/services.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:keypress_simulator/keypress_simulator.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
import 'package:swift_control/widgets/testbed.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../bluetooth/messages/click_notification.dart';
|
||||
@@ -53,6 +55,13 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
_actionSubscription.cancel();
|
||||
// Exit full screen
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
|
||||
// Reset orientation preferences to allow all orientations
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
windowManager.setFullScreen(false);
|
||||
}
|
||||
@@ -62,7 +71,13 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky, overlays: []);
|
||||
// Force landscape orientation during keymap editing
|
||||
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky, overlays: []).then((_) {
|
||||
// this will make sure the buttons are placed correctly after the transition
|
||||
setState(() {});
|
||||
});
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
windowManager.setFullScreen(true);
|
||||
}
|
||||
@@ -107,142 +122,166 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildDraggableArea({
|
||||
List<Widget> _buildDraggableArea({
|
||||
required Offset position,
|
||||
required bool enableTouch,
|
||||
required void Function(Offset newPosition) onPositionChanged,
|
||||
required Color color,
|
||||
required KeyPair keyPair,
|
||||
required String label,
|
||||
}) {
|
||||
return Positioned(
|
||||
left: position.dx,
|
||||
top: position.dy,
|
||||
child: PopupMenuButton<PhysicalKeyboardKey>(
|
||||
tooltip: 'Drag or click for special keys',
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.keyboard_alt_outlined),
|
||||
title: const Text('Simulate Keyboard shortcut'),
|
||||
),
|
||||
onTap: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false, // enable Escape key
|
||||
builder:
|
||||
(c) =>
|
||||
HotKeyListenerDialog(customApp: actionHandler.supportedApp! as CustomApp, keyPair: keyPair),
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
child: ListTile(title: const Text('Simulate Touch'), leading: Icon(Icons.touch_app_outlined)),
|
||||
onTap: () {
|
||||
keyPair.physicalKey = null;
|
||||
keyPair.logicalKey = null;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
onTap: () {
|
||||
keyPair.isLongPress = !keyPair.isLongPress;
|
||||
setState(() {});
|
||||
},
|
||||
child: CheckboxListTile(
|
||||
value: keyPair.isLongPress,
|
||||
onChanged: (value) {
|
||||
keyPair.isLongPress = value ?? false;
|
||||
setState(() {});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
title: const Text('Long Press Mode (vs. repeating)'),
|
||||
),
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
child: PopupMenuButton<PhysicalKeyboardKey>(
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaPlayPause,
|
||||
child: const Text('Media: Play/Pause'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaStop,
|
||||
child: const Text('Media: Stop'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaTrackPrevious,
|
||||
child: const Text('Media: Previous'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaTrackNext,
|
||||
child: const Text('Media: Next'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.audioVolumeUp,
|
||||
child: const Text('Media: Volume Up'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.audioVolumeDown,
|
||||
child: const Text('Media: Volume Down'),
|
||||
),
|
||||
],
|
||||
onSelected: (key) {
|
||||
keyPair.physicalKey = key;
|
||||
keyPair.logicalKey = null;
|
||||
final flutterView = WidgetsBinding.instance.platformDispatcher.views.first;
|
||||
|
||||
// figure out notch height for e.g. macOS. On Windows the display size is not available (0,0).
|
||||
final differenceInHeight =
|
||||
(flutterView.display.size.height > 0)
|
||||
? (flutterView.display.size.height - flutterView.physicalSize.height) / flutterView.devicePixelRatio
|
||||
: 0.0;
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Display Size: ${flutterView.display.size}');
|
||||
print('View size: ${flutterView.physicalSize}');
|
||||
print('Difference: $differenceInHeight');
|
||||
}
|
||||
return [
|
||||
Positioned(
|
||||
left: position.dx,
|
||||
top: position.dy - differenceInHeight,
|
||||
child: PopupMenuButton<PhysicalKeyboardKey>(
|
||||
enabled: enableTouch,
|
||||
tooltip: 'Drag to reposition. Tap to edit.',
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.keyboard_alt_outlined),
|
||||
title: const Text('Simulate Keyboard shortcut'),
|
||||
),
|
||||
onTap: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false, // enable Escape key
|
||||
builder:
|
||||
(c) => HotKeyListenerDialog(
|
||||
customApp: actionHandler.supportedApp! as CustomApp,
|
||||
keyPair: keyPair,
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.music_note_outlined),
|
||||
trailing: Icon(Icons.arrow_right),
|
||||
title: Text('Simulate Media key'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
child: ListTile(title: const Text('Simulate Touch'), leading: Icon(Icons.touch_app_outlined)),
|
||||
onTap: () {
|
||||
keyPair.physicalKey = null;
|
||||
keyPair.logicalKey = null;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
onTap: () {
|
||||
keyPair.isLongPress = !keyPair.isLongPress;
|
||||
setState(() {});
|
||||
},
|
||||
child: CheckboxListTile(
|
||||
value: keyPair.isLongPress,
|
||||
onChanged: (value) {
|
||||
keyPair.isLongPress = value ?? false;
|
||||
setState(() {});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
title: const Text('Long Press Mode (vs. repeating)'),
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
child: ListTile(title: const Text('Delete Keymap'), leading: Icon(Icons.delete, color: Colors.red)),
|
||||
onTap: () {
|
||||
actionHandler.supportedApp!.keymap.keyPairs.remove(keyPair);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
onSelected: (key) {
|
||||
keyPair.physicalKey = key;
|
||||
keyPair.logicalKey = null;
|
||||
setState(() {});
|
||||
},
|
||||
child: Container(
|
||||
color: kDebugMode && false ? Colors.yellow : null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Draggable(
|
||||
feedback: Material(
|
||||
color: Colors.transparent,
|
||||
child: _TouchDot(color: Colors.yellow, label: label, keyPair: keyPair),
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
child: PopupMenuButton<PhysicalKeyboardKey>(
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaPlayPause,
|
||||
child: const Text('Media: Play/Pause'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaStop,
|
||||
child: const Text('Media: Stop'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaTrackPrevious,
|
||||
child: const Text('Media: Previous'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.mediaTrackNext,
|
||||
child: const Text('Media: Next'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.audioVolumeUp,
|
||||
child: const Text('Media: Volume Up'),
|
||||
),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: PhysicalKeyboardKey.audioVolumeDown,
|
||||
child: const Text('Media: Volume Down'),
|
||||
),
|
||||
],
|
||||
onSelected: (key) {
|
||||
keyPair.physicalKey = key;
|
||||
keyPair.logicalKey = null;
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.music_note_outlined),
|
||||
trailing: Icon(Icons.arrow_right),
|
||||
title: Text('Simulate Media key'),
|
||||
),
|
||||
),
|
||||
),
|
||||
childWhenDragging: const SizedBox.shrink(),
|
||||
onDraggableCanceled: (_, offset) {
|
||||
setState(() => onPositionChanged(offset));
|
||||
},
|
||||
child: _TouchDot(color: color, label: label, keyPair: keyPair),
|
||||
),
|
||||
],
|
||||
PopupMenuDivider(),
|
||||
PopupMenuItem<PhysicalKeyboardKey>(
|
||||
value: null,
|
||||
child: ListTile(title: const Text('Delete Keymap'), leading: Icon(Icons.delete, color: Colors.red)),
|
||||
onTap: () {
|
||||
actionHandler.supportedApp!.keymap.keyPairs.remove(keyPair);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
onSelected: (key) {
|
||||
keyPair.physicalKey = key;
|
||||
keyPair.logicalKey = null;
|
||||
setState(() {});
|
||||
},
|
||||
child: Draggable(
|
||||
feedback: Material(color: Colors.transparent, child: KeypairExplanation(withKey: true, keyPair: keyPair)),
|
||||
childWhenDragging: const SizedBox.shrink(),
|
||||
onDraggableCanceled: (_, offset) {
|
||||
final fixedPosition = offset + Offset(0, differenceInHeight);
|
||||
setState(() => onPositionChanged(fixedPosition));
|
||||
},
|
||||
child: KeypairExplanation(withKey: true, keyPair: keyPair),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!keyPair.isSpecialKey && keyPair.physicalKey == null && keyPair.touchPosition != Offset.zero)
|
||||
Positioned(
|
||||
left: position.dx - 10,
|
||||
top: position.dy - 10 - differenceInHeight,
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
size: 20,
|
||||
shadows: [
|
||||
Shadow(color: Colors.white, offset: Offset(1, 1)),
|
||||
Shadow(color: Colors.white, offset: Offset(-1, -1)),
|
||||
Shadow(color: Colors.white, offset: Offset(-1, 1)),
|
||||
Shadow(color: Colors.white, offset: Offset(-1, 1)),
|
||||
Shadow(color: Colors.white, offset: Offset(1, -1)),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -262,12 +301,12 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text('''1. Create an in-game screenshot of your app (e.g. within MyWhoosh)
|
||||
Text('''1. Create an in-game screenshot of your app (e.g. within MyWhoosh) in landscape orientation
|
||||
2. Load the screenshot with the button below
|
||||
3. Make sure the app is in the correct orientation (portrait or landscape)
|
||||
3. The app is automatically set to landscape orientation for accurate mapping
|
||||
4. Press a button on your Zwift device to create a touch area
|
||||
5. Drag the touch areas to the desired position on the screenshot
|
||||
5. Save and close this screen'''),
|
||||
6. Save and close this screen'''),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_pickScreenshot();
|
||||
@@ -279,24 +318,26 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
),
|
||||
),
|
||||
// Touch Areas
|
||||
...?actionHandler.supportedApp?.keymap.keyPairs.map(
|
||||
(keyPair) => _buildDraggableArea(
|
||||
position: Offset(
|
||||
keyPair.touchPosition.dx / devicePixelRatio - touchAreaSize / 2,
|
||||
keyPair.touchPosition.dy / devicePixelRatio - touchAreaSize / 2 - (isDesktop ? touchAreaSize * 1.5 : 0),
|
||||
),
|
||||
keyPair: keyPair,
|
||||
onPositionChanged: (newPos) {
|
||||
final converted =
|
||||
newPos.translate(touchAreaSize / 2, touchAreaSize / 2 + (isDesktop ? touchAreaSize * 1.5 : 0)) *
|
||||
devicePixelRatio;
|
||||
keyPair.touchPosition = converted;
|
||||
setState(() {});
|
||||
},
|
||||
color: Colors.red,
|
||||
label: keyPair.buttons.joinToString(transform: (e) => e.name, separator: '\n'),
|
||||
),
|
||||
),
|
||||
...?actionHandler.supportedApp?.keymap.keyPairs
|
||||
.map(
|
||||
(keyPair) => _buildDraggableArea(
|
||||
enableTouch: true,
|
||||
position: Offset(
|
||||
keyPair.touchPosition.dx / devicePixelRatio,
|
||||
keyPair.touchPosition.dy / devicePixelRatio,
|
||||
),
|
||||
keyPair: keyPair,
|
||||
onPositionChanged: (newPos) {
|
||||
final converted = newPos * devicePixelRatio;
|
||||
keyPair.touchPosition = converted;
|
||||
setState(() {});
|
||||
},
|
||||
color: Colors.red,
|
||||
),
|
||||
)
|
||||
.flatten(),
|
||||
|
||||
Positioned.fill(child: Testbed()),
|
||||
|
||||
Positioned(
|
||||
top: 40,
|
||||
@@ -304,15 +345,20 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
actionHandler.supportedApp?.keymap.reset();
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.lock_reset),
|
||||
label: Text('Reset'),
|
||||
),
|
||||
ElevatedButton.icon(onPressed: _saveAndClose, icon: const Icon(Icons.save), label: const Text("Save")),
|
||||
PopupMenuButton(
|
||||
itemBuilder:
|
||||
(c) => [
|
||||
PopupMenuItem(
|
||||
child: Text('Reset'),
|
||||
onTap: () {
|
||||
actionHandler.supportedApp?.keymap.reset();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
icon: Icon(Icons.more_vert),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -322,59 +368,46 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class _TouchDot extends StatelessWidget {
|
||||
final Color color;
|
||||
final String label;
|
||||
class KeypairExplanation extends StatelessWidget {
|
||||
final bool withKey;
|
||||
final KeyPair keyPair;
|
||||
|
||||
const _TouchDot({required this.color, required this.label, required this.keyPair});
|
||||
const KeypairExplanation({super.key, required this.keyPair, this.withKey = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Container(
|
||||
width: touchAreaSize,
|
||||
height: touchAreaSize,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.6),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: keyPair.isLongPress ? Colors.green : Colors.black,
|
||||
width: keyPair.isLongPress ? 3 : 2,
|
||||
),
|
||||
if (withKey) KeyWidget(label: keyPair.buttons.joinToString(transform: (e) => e.name, separator: '\n')),
|
||||
if (keyPair.physicalKey != null) ...[
|
||||
Icon(switch (keyPair.physicalKey) {
|
||||
PhysicalKeyboardKey.mediaPlayPause ||
|
||||
PhysicalKeyboardKey.mediaStop ||
|
||||
PhysicalKeyboardKey.mediaTrackPrevious ||
|
||||
PhysicalKeyboardKey.mediaTrackNext ||
|
||||
PhysicalKeyboardKey.audioVolumeUp ||
|
||||
PhysicalKeyboardKey.audioVolumeDown => Icons.music_note_outlined,
|
||||
_ => Icons.keyboard,
|
||||
}, size: 16),
|
||||
KeyWidget(
|
||||
label: switch (keyPair.physicalKey) {
|
||||
PhysicalKeyboardKey.mediaPlayPause => 'Media: Play/Pause',
|
||||
PhysicalKeyboardKey.mediaStop => 'Media: Stop',
|
||||
PhysicalKeyboardKey.mediaTrackPrevious => 'Media: Previous',
|
||||
PhysicalKeyboardKey.mediaTrackNext => 'Media: Next',
|
||||
PhysicalKeyboardKey.audioVolumeUp => 'Media: Volume Up',
|
||||
PhysicalKeyboardKey.audioVolumeDown => 'Media: Volume Down',
|
||||
_ => keyPair.logicalKey?.keyLabel ?? 'Unknown',
|
||||
},
|
||||
),
|
||||
child: Icon(
|
||||
keyPair.isSpecialKey
|
||||
? Icons.music_note_outlined
|
||||
: keyPair.physicalKey != null
|
||||
? Icons.keyboard_alt_outlined
|
||||
: Icons.touch_app_outlined,
|
||||
),
|
||||
),
|
||||
if (keyPair.isLongPress) Text('using long press'),
|
||||
] else ...[
|
||||
Icon(Icons.touch_app, size: 16),
|
||||
KeyWidget(label: 'X: ${keyPair.touchPosition.dx.toInt()}, Y: ${keyPair.touchPosition.dy.toInt()}'),
|
||||
|
||||
Container(
|
||||
color: Colors.white.withAlpha(180),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: TextStyle(color: Colors.black, fontSize: 12)),
|
||||
if (keyPair.physicalKey != null)
|
||||
Text(switch (keyPair.physicalKey) {
|
||||
PhysicalKeyboardKey.mediaPlayPause => 'Media: Play/Pause',
|
||||
PhysicalKeyboardKey.mediaStop => 'Media: Stop',
|
||||
PhysicalKeyboardKey.mediaTrackPrevious => 'Media: Previous',
|
||||
PhysicalKeyboardKey.mediaTrackNext => 'Media: Next',
|
||||
PhysicalKeyboardKey.audioVolumeUp => 'Media: Volume Up',
|
||||
PhysicalKeyboardKey.audioVolumeDown => 'Media: Volume Down',
|
||||
_ => keyPair.logicalKey?.keyLabel ?? 'Unknown',
|
||||
}, style: TextStyle(color: Colors.black87, fontSize: 12)),
|
||||
if (keyPair.isLongPress)
|
||||
Text('Long Press', style: TextStyle(color: Colors.green, fontSize: 10, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (keyPair.isLongPress) Text('using long press'),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/actions/base_actions.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/custom_app.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
|
||||
import '../keymap/apps/supported_app.dart';
|
||||
import '../single_line_exception.dart';
|
||||
@@ -24,7 +25,7 @@ class AndroidActions extends BaseActions {
|
||||
@override
|
||||
Future<String> performAction(ZwiftButton button, {bool isKeyDown = true, bool isKeyUp = false}) async {
|
||||
if (supportedApp == null) {
|
||||
return ("Could not perform ${button.name}: No keymap set");
|
||||
return ("Could not perform ${button.name.splitByUpperCase()}: No keymap set");
|
||||
}
|
||||
|
||||
if (supportedApp is CustomApp) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:keypress_simulator/keypress_simulator.dart';
|
||||
import 'package:swift_control/utils/actions/base_actions.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
|
||||
class DesktopActions extends BaseActions {
|
||||
// Track keys that are currently held down in long press mode
|
||||
@@ -14,7 +15,7 @@ class DesktopActions extends BaseActions {
|
||||
|
||||
final keyPair = supportedApp!.keymap.getKeyPair(action);
|
||||
if (keyPair == null) {
|
||||
return ('Keymap entry not found for action: $action');
|
||||
return ('Keymap entry not found for action: ${action.toString().splitByUpperCase()}');
|
||||
}
|
||||
|
||||
// Handle long press mode
|
||||
@@ -53,12 +54,12 @@ class DesktopActions extends BaseActions {
|
||||
if (keyPair.physicalKey != null) {
|
||||
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey);
|
||||
await keyPressSimulator.simulateKeyUp(keyPair.physicalKey);
|
||||
return 'Key pressed: ${keyPair.logicalKey?.keyLabel}';
|
||||
return 'Key pressed: $keyPair';
|
||||
} else {
|
||||
final point = supportedApp!.resolveTouchPosition(action: action, windowInfo: null);
|
||||
await keyPressSimulator.simulateMouseClickDown(point);
|
||||
await keyPressSimulator.simulateMouseClickUp(point);
|
||||
return 'Mouse clicked at: $point';
|
||||
return 'Mouse clicked at: ${point.dx} ${point.dy}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
79
lib/utils/changelog.dart
Normal file
79
lib/utils/changelog.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class ChangelogEntry {
|
||||
final String version;
|
||||
final String date;
|
||||
final List<String> changes;
|
||||
|
||||
ChangelogEntry({
|
||||
required this.version,
|
||||
required this.date,
|
||||
required this.changes,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '### $version ($date)\n${changes.map((c) => '- $c').join('\n')}';
|
||||
}
|
||||
}
|
||||
|
||||
class ChangelogParser {
|
||||
static Future<List<ChangelogEntry>> parse() async {
|
||||
final content = await rootBundle.loadString('CHANGELOG.md');
|
||||
return parseContent(content);
|
||||
}
|
||||
|
||||
static List<ChangelogEntry> parseContent(String content) {
|
||||
final entries = <ChangelogEntry>[];
|
||||
final lines = content.split('\n');
|
||||
|
||||
ChangelogEntry? currentEntry;
|
||||
|
||||
for (var line in lines) {
|
||||
// Check if this is a version header (e.g., "### 2.6.0 (2025-09-28)")
|
||||
if (line.startsWith('### ')) {
|
||||
// Save previous entry if exists
|
||||
if (currentEntry != null) {
|
||||
entries.add(currentEntry);
|
||||
}
|
||||
|
||||
// Parse new entry
|
||||
final header = line.substring(4).trim();
|
||||
final match = RegExp(r'^(\S+)\s+\(([^)]+)\)').firstMatch(header);
|
||||
if (match != null) {
|
||||
currentEntry = ChangelogEntry(
|
||||
version: match.group(1)!,
|
||||
date: match.group(2)!,
|
||||
changes: [],
|
||||
);
|
||||
}
|
||||
} else if (line.startsWith('- ') && currentEntry != null) {
|
||||
// Add change to current entry
|
||||
currentEntry.changes.add(line.substring(2).trim());
|
||||
} else if (line.startsWith(' - ') && currentEntry != null) {
|
||||
// Sub-bullet point
|
||||
currentEntry.changes.add(line.substring(4).trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last entry
|
||||
if (currentEntry != null) {
|
||||
entries.add(currentEntry);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
static Future<ChangelogEntry?> getLatestEntry() async {
|
||||
final entries = await parse();
|
||||
return entries.isNotEmpty ? entries.first : null;
|
||||
}
|
||||
|
||||
static Future<String?> getLatestEntryForPlayStore() async {
|
||||
final entry = await getLatestEntry();
|
||||
if (entry == null) return null;
|
||||
|
||||
// Format for Play Store: just the changes, no version header
|
||||
return entry.changes.join('\n');
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class CustomApp extends SupportedApp {
|
||||
Offset resolveTouchPosition({required ZwiftButton action, required WindowEvent? windowInfo}) {
|
||||
final keyPair = keymap.getKeyPair(action);
|
||||
if (keyPair == null || keyPair.touchPosition == Offset.zero) {
|
||||
throw SingleLineException("No key pair found for action: $action");
|
||||
throw SingleLineException("No key pair found for action: $action. You may want to adjust the keymap.");
|
||||
}
|
||||
return keyPair.touchPosition;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
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';
|
||||
import 'package:swift_control/widgets/accessibility_disclosure_dialog.dart';
|
||||
|
||||
class AccessibilityRequirement extends PlatformRequirement {
|
||||
AccessibilityRequirement() : super('Allow Accessibility Service');
|
||||
@@ -17,6 +19,53 @@ class AccessibilityRequirement extends PlatformRequirement {
|
||||
Future<void> getStatus() async {
|
||||
status = await accessibilityHandler.hasPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? build(BuildContext context, VoidCallback onUpdate) {
|
||||
if (status) {
|
||||
return null; // Already granted, no need for disclosure
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'SwiftControl needs accessibility permission to control your training apps.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => _showDisclosureDialog(context, onUpdate),
|
||||
child: const Text('Show Permission Details'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showDisclosureDialog(BuildContext context, VoidCallback onUpdate) async {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false, // Prevent dismissing by tapping outside
|
||||
builder: (BuildContext context) {
|
||||
return AccessibilityDisclosureDialog(
|
||||
onAccept: () {
|
||||
Navigator.of(context).pop();
|
||||
// Open accessibility settings after user consents
|
||||
accessibilityHandler.openPermissions().then((_) {
|
||||
onUpdate();
|
||||
});
|
||||
},
|
||||
onDeny: () {
|
||||
Navigator.of(context).pop();
|
||||
// User denied, no action taken
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BluetoothScanRequirement extends PlatformRequirement {
|
||||
@@ -94,10 +143,7 @@ class NotificationRequirement extends PlatformRequirement {
|
||||
InitializationSettings(android: initializationSettingsAndroid),
|
||||
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
|
||||
onDidReceiveNotificationResponse: (n) {
|
||||
if (n.actionId != null) {
|
||||
connection.reset();
|
||||
exit(0);
|
||||
}
|
||||
notificationTapBackground(n);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -139,6 +185,8 @@ class NotificationRequirement extends PlatformRequirement {
|
||||
void notificationTapBackground(NotificationResponse notificationResponse) {
|
||||
if (notificationResponse.actionId != null) {
|
||||
connection.reset();
|
||||
exit(0);
|
||||
AndroidFlutterLocalNotificationsPlugin().stopForegroundService().then((_) {
|
||||
exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class UnsupportedPlatform extends PlatformRequirement {
|
||||
}
|
||||
|
||||
class BluetoothScanning extends PlatformRequirement {
|
||||
BluetoothScanning() : super('Bluetooth Scanning') {
|
||||
BluetoothScanning() : super('Finding your Zwift® controller...') {
|
||||
status = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Future<List<PlatformRequirement>> getRequirements() async {
|
||||
List<PlatformRequirement> list;
|
||||
if (kIsWeb) {
|
||||
list = [BluetoothTurnedOn(), BluetoothScanning()];
|
||||
} else if (Platform.isMacOS) {
|
||||
} else if (Platform.isMacOS || Platform.isIOS) {
|
||||
list = [BluetoothTurnedOn(), KeyboardRequirement(), BluetoothScanning()];
|
||||
} else if (Platform.isWindows) {
|
||||
list = [BluetoothTurnedOn(), KeyboardRequirement(), BluetoothScanning()];
|
||||
|
||||
@@ -32,10 +32,31 @@ class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
void setApp(SupportedApp app) {
|
||||
Future<void> reset() async {
|
||||
await _prefs.clear();
|
||||
actionHandler.init(null);
|
||||
}
|
||||
|
||||
Future<void> setApp(SupportedApp app) async {
|
||||
if (app is CustomApp) {
|
||||
_prefs.setStringList("customapp", app.encodeKeymap());
|
||||
await _prefs.setStringList("customapp", app.encodeKeymap());
|
||||
}
|
||||
_prefs.setString('app', app.name);
|
||||
await _prefs.setString('app', app.name);
|
||||
}
|
||||
|
||||
String? getLastSeenVersion() {
|
||||
return _prefs.getString('last_seen_version');
|
||||
}
|
||||
|
||||
Future<void> setLastSeenVersion(String version) async {
|
||||
await _prefs.setString('last_seen_version', version);
|
||||
}
|
||||
|
||||
bool getVibrationEnabled() {
|
||||
return _prefs.getBool('vibration_enabled') ?? true;
|
||||
}
|
||||
|
||||
Future<void> setVibrationEnabled(bool enabled) async {
|
||||
await _prefs.setBool('vibration_enabled', enabled);
|
||||
}
|
||||
}
|
||||
|
||||
77
lib/widgets/accessibility_disclosure_dialog.dart
Normal file
77
lib/widgets/accessibility_disclosure_dialog.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
|
||||
class AccessibilityDisclosureDialog extends StatelessWidget {
|
||||
final VoidCallback onAccept;
|
||||
final VoidCallback onDeny;
|
||||
|
||||
const AccessibilityDisclosureDialog({
|
||||
super.key,
|
||||
required this.onAccept,
|
||||
required this.onDeny,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false, // Prevent back navigation from dismissing dialog
|
||||
child: AlertDialog(
|
||||
title: const Text('Accessibility Service Permission Required'),
|
||||
content: const SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'SwiftControl needs to use Android\'s AccessibilityService API to function properly.',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text('Why is this permission needed?'),
|
||||
SizedBox(height: 8),
|
||||
Text('• To simulate touch gestures on your screen for controlling trainer apps'),
|
||||
Text('• To detect which training app window is currently active'),
|
||||
Text('• To enable you to control apps like MyWhoosh, IndieVelo, and others using your Zwift devices'),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'How does SwiftControl use this permission?',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text('• When you press buttons on your Zwift Click, Zwift Ride, or Zwift Play devices, SwiftControl simulates touch gestures at specific screen locations'),
|
||||
Text('• The app monitors which training app window is active to ensure gestures are sent to the correct app'),
|
||||
Text('• No personal data is accessed or collected through this service'),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'SwiftControl will only access your screen to perform the gestures you configure. No other accessibility features or personal information will be accessed.',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'You must choose to either Allow or Deny this permission to continue.',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, color: Colors.deepOrange),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: onDeny,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
child: const Text('Deny'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: onAccept,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Allow'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/widgets/changelog_dialog.dart
Normal file
59
lib/widgets/changelog_dialog.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/utils/changelog.dart';
|
||||
|
||||
class ChangelogDialog extends StatelessWidget {
|
||||
final ChangelogEntry entry;
|
||||
|
||||
const ChangelogDialog({super.key, required this.entry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('What\'s New'),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Version ${entry.version}',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.normal),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children:
|
||||
entry.changes
|
||||
.map(
|
||||
(change) => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('• ', style: TextStyle(fontSize: 16)),
|
||||
Expanded(child: Text(change, style: Theme.of(context).textTheme.bodyMedium)),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text('Got it!'))],
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> showIfNeeded(BuildContext context, String currentVersion, String? lastSeenVersion) async {
|
||||
// Show dialog if this is a new version
|
||||
if (lastSeenVersion != currentVersion) {
|
||||
try {
|
||||
final entry = await ChangelogParser.getLatestEntry();
|
||||
if (entry != null && context.mounted) {
|
||||
showDialog(context: context, builder: (context) => ChangelogDialog(entry: entry));
|
||||
}
|
||||
} catch (e) {
|
||||
print('Failed to load changelog for dialog: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/keymap/keymap.dart';
|
||||
|
||||
import '../pages/touch_area.dart';
|
||||
|
||||
class KeymapExplanation extends StatelessWidget {
|
||||
final Keymap keymap;
|
||||
final VoidCallback onUpdate;
|
||||
@@ -10,12 +12,18 @@ class KeymapExplanation extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final keyboardGroups = keymap.keyPairs
|
||||
final connectedDevice = connection.devices.firstOrNull;
|
||||
|
||||
final availableKeypairs = keymap.keyPairs.filter(
|
||||
(e) => connectedDevice?.availableButtons.containsAny(e.buttons) == true,
|
||||
);
|
||||
|
||||
final keyboardGroups = availableKeypairs
|
||||
.filter((e) => e.physicalKey != null)
|
||||
.groupBy((element) => '${element.physicalKey}-${element.isLongPress}');
|
||||
final touchGroups = keymap.keyPairs
|
||||
.groupBy((element) => '${element.physicalKey?.usbHidUsage}-${element.isLongPress}');
|
||||
final touchGroups = availableKeypairs
|
||||
.filter((e) => e.physicalKey == null && e.touchPosition != Offset.zero)
|
||||
.groupBy((element) => '${element.touchPosition}-${element.isLongPress}');
|
||||
.groupBy((element) => '${element.touchPosition.dx}-${element.touchPosition.dy}-${element.isLongPress}');
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -33,7 +41,7 @@ class KeymapExplanation extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Text(
|
||||
'Button on your ${connection.devices.firstOrNull?.device.name ?? connection.devices.firstOrNull?.runtimeType}',
|
||||
'Button on your ${connectedDevice?.device.name ?? connectedDevice?.runtimeType}',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
@@ -57,21 +65,12 @@ class KeymapExplanation extends StatelessWidget {
|
||||
children: [
|
||||
for (final keyPair in pair.value)
|
||||
for (final button in keyPair.buttons)
|
||||
IntrinsicWidth(child: _KeyWidget(label: button.name.splitByUpperCase())),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(Icons.keyboard, size: 16),
|
||||
_KeyWidget(label: pair.value.first.logicalKey?.keyLabel ?? ''),
|
||||
if (pair.value.first.isLongPress) Text('using long press'),
|
||||
if (connectedDevice?.availableButtons.contains(button) == true)
|
||||
IntrinsicWidth(child: KeyWidget(label: button.name)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(padding: const EdgeInsets.all(6), child: KeypairExplanation(keyPair: pair.value.first)),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -84,25 +83,13 @@ class KeymapExplanation extends StatelessWidget {
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (final keyPair in pair.value)
|
||||
for (final button in keyPair.buttons) _KeyWidget(label: button.name.splitByUpperCase()),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(Icons.touch_app, size: 16),
|
||||
_KeyWidget(
|
||||
label:
|
||||
'x: ${pair.value.first.touchPosition.dx.toInt()}, y: ${pair.value.first.touchPosition.dy.toInt()}',
|
||||
),
|
||||
|
||||
if (pair.value.first.isLongPress) Text('using long press'),
|
||||
for (final button in keyPair.buttons)
|
||||
if (connectedDevice?.availableButtons.contains(button) == true)
|
||||
KeyWidget(label: button.name),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(padding: const EdgeInsets.all(6), child: KeypairExplanation(keyPair: pair.value.first)),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -113,9 +100,9 @@ class KeymapExplanation extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _KeyWidget extends StatelessWidget {
|
||||
class KeyWidget extends StatelessWidget {
|
||||
final String label;
|
||||
const _KeyWidget({super.key, required this.label});
|
||||
const KeyWidget({super.key, required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -129,7 +116,7 @@ class _KeyWidget extends StatelessWidget {
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
label.splitByUpperCase(),
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
@@ -141,7 +128,7 @@ class _KeyWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
extension on String {
|
||||
extension SplitByUppercase on String {
|
||||
String splitByUpperCase() {
|
||||
return replaceAllMapped(RegExp(r'([a-z])([A-Z])'), (match) => '${match.group(1)} ${match.group(2)}').capitalize();
|
||||
}
|
||||
|
||||
@@ -48,52 +48,41 @@ class _LogviewerState extends State<LogViewer> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
SelectionArea(
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
children:
|
||||
_actions
|
||||
.map(
|
||||
(action) => Text.rich(
|
||||
return SelectionArea(
|
||||
child: ListView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
shrinkWrap: true,
|
||||
reverse: true,
|
||||
children:
|
||||
_actions
|
||||
.map(
|
||||
(action) => Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: action.date.toString().split(" ").last,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontFamily: "monospace",
|
||||
fontFamilyFallback: <String>["Courier"],
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: " ${action.entry}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
text: action.date.toString().split(" ").last,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontFamily: "monospace",
|
||||
fontFamilyFallback: <String>["Courier"],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
_actions.clear();
|
||||
setState(() {});
|
||||
},
|
||||
icon: Icon(Icons.clear),
|
||||
),
|
||||
),
|
||||
],
|
||||
TextSpan(
|
||||
text: " ${action.entry}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/widgets/title.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import '../pages/changelog.dart';
|
||||
import '../pages/device.dart';
|
||||
|
||||
List<Widget> buildMenuButtons() {
|
||||
@@ -25,6 +27,13 @@ List<Widget> buildMenuButtons() {
|
||||
launchUrlString(link);
|
||||
},
|
||||
),
|
||||
if (!kIsWeb && Platform.isAndroid && !isFromPlayStore)
|
||||
PopupMenuItem(
|
||||
child: Text('by buying the app from Play Store'),
|
||||
onTap: () {
|
||||
launchUrlString('https://play.google.com/store/apps/details?id=de.jonasbark.swiftcontrol');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text('via PayPal'),
|
||||
onTap: () {
|
||||
@@ -50,25 +59,45 @@ class MenuButton extends StatelessWidget {
|
||||
itemBuilder:
|
||||
(c) => [
|
||||
if (kDebugMode) ...[
|
||||
...ZwiftButton.values.map(
|
||||
(e) => PopupMenuItem(
|
||||
child: Text(e.name),
|
||||
onTap: () {
|
||||
Future.delayed(Duration(seconds: 2)).then((_) {
|
||||
actionHandler.performAction(e);
|
||||
});
|
||||
PopupMenuItem(
|
||||
child: PopupMenuButton(
|
||||
child: Text("Simulate buttons"),
|
||||
itemBuilder: (_) {
|
||||
return ZwiftButton.values
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
child: Text(e.name),
|
||||
onTap: () {
|
||||
Future.delayed(Duration(seconds: 2)).then((_) {
|
||||
actionHandler.performAction(e);
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
PopupMenuItem(child: PopupMenuDivider()),
|
||||
PopupMenuItem(
|
||||
child: Text('Continue'),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (c) => DevicePage()));
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text('Reset'),
|
||||
onTap: () async {
|
||||
await settings.reset();
|
||||
},
|
||||
),
|
||||
PopupMenuItem(child: PopupMenuDivider()),
|
||||
],
|
||||
PopupMenuItem(
|
||||
child: Text('Changelog'),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (c) => ChangelogPage()));
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text('Feedback'),
|
||||
onTap: () {
|
||||
|
||||
323
lib/widgets/testbed.dart
Normal file
323
lib/widgets/testbed.dart
Normal file
@@ -0,0 +1,323 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// A developer overlay that visualizes touches and keyboard events.
|
||||
/// - Touch dots appear where you touch and fade out over [touchRevealDuration].
|
||||
/// - Keyboard events are listed temporarily and fade out over [keyboardRevealDuration].
|
||||
class Testbed extends StatefulWidget {
|
||||
const Testbed({
|
||||
super.key,
|
||||
this.enabled = true,
|
||||
this.showTouches = true,
|
||||
this.showKeyboard = true,
|
||||
this.touchRevealDuration = const Duration(seconds: 2),
|
||||
this.keyboardRevealDuration = const Duration(seconds: 2),
|
||||
this.maxKeyboardEvents = 6,
|
||||
this.touchColor = const Color(0xFF00BCD4), // cyan-ish
|
||||
this.keyboardBadgeColor = const Color(0xCC000000), // translucent black
|
||||
this.keyboardTextStyle = const TextStyle(color: Colors.white, fontSize: 12),
|
||||
});
|
||||
|
||||
final bool enabled;
|
||||
final bool showTouches;
|
||||
final bool showKeyboard;
|
||||
|
||||
final Duration touchRevealDuration;
|
||||
final Duration keyboardRevealDuration;
|
||||
final int maxKeyboardEvents;
|
||||
|
||||
final Color touchColor;
|
||||
final Color keyboardBadgeColor;
|
||||
final TextStyle keyboardTextStyle;
|
||||
|
||||
@override
|
||||
State<Testbed> createState() => _TestbedState();
|
||||
}
|
||||
|
||||
class _TestbedState extends State<Testbed> with SingleTickerProviderStateMixin {
|
||||
late final Ticker _ticker;
|
||||
|
||||
// ----- Touch tracking -----
|
||||
final Map<int, _TouchSample> _active = <int, _TouchSample>{};
|
||||
final List<_TouchSample> _history = <_TouchSample>[];
|
||||
|
||||
// ----- Keyboard tracking -----
|
||||
final List<_KeySample> _keys = <_KeySample>[];
|
||||
|
||||
// Focus to receive key events without stealing focus from inputs.
|
||||
late final FocusNode _focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = FocusNode(debugLabel: 'TestbedFocus', canRequestFocus: true, skipTraversal: true);
|
||||
|
||||
_ticker = createTicker((_) {
|
||||
// Cull expired touch and key samples.
|
||||
final now = DateTime.now();
|
||||
_history.removeWhere((s) => now.difference(s.timestamp) > widget.touchRevealDuration);
|
||||
_keys.removeWhere((k) => now.difference(k.timestamp) > widget.keyboardRevealDuration);
|
||||
|
||||
if (mounted) setState(() {});
|
||||
})..start();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ticker.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onPointerDown(PointerDownEvent e) {
|
||||
if (!widget.enabled ||
|
||||
!widget.showTouches ||
|
||||
(e.kind != PointerDeviceKind.unknown && e.kind != PointerDeviceKind.mouse)) {
|
||||
return;
|
||||
}
|
||||
final sample = _TouchSample(
|
||||
pointer: e.pointer,
|
||||
position: e.position,
|
||||
timestamp: DateTime.now(),
|
||||
phase: _TouchPhase.down,
|
||||
);
|
||||
_active[e.pointer] = sample;
|
||||
_history.add(sample);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _onPointerCancel(PointerCancelEvent e) {
|
||||
if (!widget.enabled || !widget.showTouches || !mounted) return;
|
||||
_active.remove(e.pointer);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
KeyEventResult _onKey(FocusNode node, KeyEvent event) {
|
||||
if (!widget.enabled || !widget.showKeyboard || event is KeyUpEvent) return KeyEventResult.ignored;
|
||||
|
||||
final label = event.logicalKey.keyLabel;
|
||||
final keyName = label.isNotEmpty ? label : event.logicalKey.debugName ?? 'Key';
|
||||
final isDown = event is KeyDownEvent;
|
||||
final isUp = event is KeyUpEvent;
|
||||
|
||||
// Filter out repeat KeyDowns if desired (optional).
|
||||
// Here we keep them; comment this block in to drop repeats:
|
||||
// if (event.repeat) return KeyEventResult.handled;
|
||||
|
||||
final sample = _KeySample(
|
||||
text:
|
||||
'${isDown
|
||||
? "↓"
|
||||
: isUp
|
||||
? "↑"
|
||||
: "•"} $keyName',
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
_keys.insert(0, sample);
|
||||
if (_keys.length > widget.maxKeyboardEvents) {
|
||||
_keys.removeLast();
|
||||
}
|
||||
setState(() {});
|
||||
// We don't want to prevent normal text input, so we return ignored.
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
onPointerDown: _onPointerDown,
|
||||
onPointerCancel: _onPointerCancel,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Focus(
|
||||
focusNode: _focusNode,
|
||||
autofocus: true,
|
||||
canRequestFocus: true,
|
||||
descendantsAreFocusable: true,
|
||||
onKeyEvent: _onKey,
|
||||
child: Stack(
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
if (widget.showTouches)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: CustomPaint(
|
||||
painter: _TouchesPainter(
|
||||
now: DateTime.now(),
|
||||
samples: _history,
|
||||
duration: widget.touchRevealDuration,
|
||||
color: widget.touchColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.showKeyboard)
|
||||
Positioned(
|
||||
left: 12,
|
||||
bottom: 12,
|
||||
child: IgnorePointer(
|
||||
child: _KeyboardOverlay(
|
||||
items: _keys,
|
||||
duration: widget.keyboardRevealDuration,
|
||||
badgeColor: widget.keyboardBadgeColor,
|
||||
textStyle: widget.keyboardTextStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Touches =====
|
||||
|
||||
enum _TouchPhase { down, move, up }
|
||||
|
||||
class _TouchSample {
|
||||
_TouchSample({required this.pointer, required this.position, required this.timestamp, required this.phase});
|
||||
|
||||
final int pointer;
|
||||
final Offset position;
|
||||
final DateTime timestamp;
|
||||
final _TouchPhase phase;
|
||||
}
|
||||
|
||||
class _TouchesPainter extends CustomPainter {
|
||||
_TouchesPainter({required this.now, required this.samples, required this.duration, required this.color});
|
||||
|
||||
final DateTime now;
|
||||
final List<_TouchSample> samples;
|
||||
final Duration duration;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint =
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
|
||||
for (final s in samples) {
|
||||
final age = now.difference(s.timestamp);
|
||||
if (age > duration) continue;
|
||||
|
||||
final t = age.inMilliseconds / duration.inMilliseconds.clamp(1, 1 << 30);
|
||||
final fade = (1.0 - t).clamp(0.0, 1.0);
|
||||
|
||||
// Two concentric circles: inner filled pulse + outer ring.
|
||||
final baseRadius = 22.0;
|
||||
final pulse = 1.0 + 0.5 * math.sin(t * math.pi); // subtle pulsing
|
||||
final rOuter = baseRadius * (1.0 + 0.35 * t);
|
||||
final rInner = baseRadius * 0.5 * pulse;
|
||||
|
||||
// Outer ring (stroke, fading)
|
||||
paint
|
||||
..style = PaintingStyle.stroke
|
||||
..color = color.withOpacity(0.35 * fade);
|
||||
canvas.drawCircle(s.position, rOuter, paint);
|
||||
|
||||
// Inner fill (stronger)
|
||||
final fill =
|
||||
Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = color.withOpacity(0.35 + 0.35 * fade);
|
||||
canvas.drawCircle(s.position, rInner, fill);
|
||||
|
||||
// Tiny center dot for precision
|
||||
final center =
|
||||
Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = color.withOpacity(0.9 * fade);
|
||||
canvas.drawCircle(s.position, 2.5, center);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _TouchesPainter oldDelegate) {
|
||||
return oldDelegate.now != now ||
|
||||
oldDelegate.samples != samples ||
|
||||
oldDelegate.duration != duration ||
|
||||
oldDelegate.color != color;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Keyboard overlay =====
|
||||
|
||||
class _KeySample {
|
||||
_KeySample({required this.text, required this.timestamp});
|
||||
final String text;
|
||||
final DateTime timestamp;
|
||||
}
|
||||
|
||||
class _KeyboardOverlay extends StatelessWidget {
|
||||
const _KeyboardOverlay({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.duration,
|
||||
required this.badgeColor,
|
||||
required this.textStyle,
|
||||
});
|
||||
|
||||
final List<_KeySample> items;
|
||||
final Duration duration;
|
||||
final Color badgeColor;
|
||||
final TextStyle textStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final now = DateTime.now();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (final item in items)
|
||||
_KeyboardToast(
|
||||
text: item.text,
|
||||
age: now.difference(item.timestamp),
|
||||
duration: duration,
|
||||
badgeColor: badgeColor,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KeyboardToast extends StatelessWidget {
|
||||
const _KeyboardToast({
|
||||
required this.text,
|
||||
required this.age,
|
||||
required this.duration,
|
||||
required this.badgeColor,
|
||||
required this.textStyle,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final Duration age;
|
||||
final Duration duration;
|
||||
final Color badgeColor;
|
||||
final TextStyle textStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = (age.inMilliseconds / duration.inMilliseconds.clamp(1, 1 << 30)).clamp(0.0, 1.0);
|
||||
final fade = 1.0 - t;
|
||||
|
||||
return Material(
|
||||
child: Opacity(
|
||||
opacity: fade,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(color: badgeColor, borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(text, style: textStyle),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,14 @@ import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:in_app_update/in_app_update.dart';
|
||||
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;
|
||||
bool isFromPlayStore = true;
|
||||
|
||||
class AppTitle extends StatefulWidget {
|
||||
const AppTitle({super.key});
|
||||
@@ -20,16 +22,17 @@ class AppTitle extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AppTitleState extends State<AppTitle> {
|
||||
Future<String?> getLatestVersionUrlIfNewer() async {
|
||||
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 tagName = data['tag_name'] as String;
|
||||
final prerelase = data['prerelease'] as bool;
|
||||
final latestVersion = tagName.split('+').first;
|
||||
final currentVersion = 'v${_packageInfoValue!.version}';
|
||||
|
||||
// we anything but +0 is considered beta
|
||||
if (latestVersion != currentVersion && tagName.endsWith("+0")) {
|
||||
// +1337 releases are considered beta
|
||||
if (latestVersion != currentVersion && !prerelase) {
|
||||
final assets = data['assets'] as List;
|
||||
if (Platform.isAndroid) {
|
||||
final apkUrl = assets.firstOrNullWhere((asset) => asset['name'].endsWith('.apk'))['browser_download_url'];
|
||||
@@ -56,17 +59,40 @@ class _AppTitleState extends State<AppTitle> {
|
||||
setState(() {
|
||||
_packageInfoValue = value;
|
||||
});
|
||||
_loadLatestVersionUrl();
|
||||
_checkForUpdate();
|
||||
});
|
||||
} else {
|
||||
_loadLatestVersionUrl();
|
||||
_checkForUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void _loadLatestVersionUrl() async {
|
||||
void _checkForUpdate() async {
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
try {
|
||||
final appUpdateInfo = await InAppUpdate.checkForUpdate();
|
||||
if (context.mounted && appUpdateInfo.updateAvailability == UpdateAvailability.updateAvailable) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('New version available'),
|
||||
duration: Duration(seconds: 1337),
|
||||
action: SnackBarAction(
|
||||
label: 'Update',
|
||||
onPressed: () {
|
||||
InAppUpdate.performImmediateUpdate();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
} on Exception catch (e) {
|
||||
isFromPlayStore = false;
|
||||
print('Failed to check for update: $e');
|
||||
}
|
||||
}
|
||||
if (_latestVersionUrlValue == null && !kIsWeb) {
|
||||
final url = await getLatestVersionUrlIfNewer();
|
||||
if (url != null && mounted) {
|
||||
final url = await _getLatestVersionUrlIfNewer();
|
||||
if (url != null && mounted && !kDebugMode) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('New version available: ${url.split("/").takeLast(2).first.split('%').first}'),
|
||||
@@ -91,7 +117,7 @@ class _AppTitleState extends State<AppTitle> {
|
||||
Text('SwiftControl'),
|
||||
if (_packageInfoValue != null)
|
||||
Text(
|
||||
'v${_packageInfoValue!.version}+${_packageInfoValue!.buildNumber}',
|
||||
'v${_packageInfoValue!.version}',
|
||||
style: TextStyle(fontFamily: "monospace", fontFamilyFallback: <String>["Courier"], fontSize: 12),
|
||||
)
|
||||
else
|
||||
|
||||
@@ -20,7 +20,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- window_manager (0.2.0):
|
||||
- window_manager (0.5.0):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
@@ -71,7 +71,7 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
window_manager: e25faf20d88283a0d46e7b1a759d07261ca27575
|
||||
|
||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||
|
||||
|
||||
BIN
playstoreassets/appicon.png
Normal file
BIN
playstoreassets/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 517 KiB |
BIN
playstoreassets/logo.png
Normal file
BIN
playstoreassets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 309 KiB |
BIN
playstoreassets/mob1.png
Normal file
BIN
playstoreassets/mob1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
BIN
playstoreassets/mob2.png
Normal file
BIN
playstoreassets/mob2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
BIN
playstoreassets/tab1.png
Normal file
BIN
playstoreassets/tab1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
BIN
playstoreassets/tab2.png
Normal file
BIN
playstoreassets/tab2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
170
pubspec.lock
170
pubspec.lock
@@ -12,10 +12,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12"
|
||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.5"
|
||||
version: "4.0.7"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -60,10 +60,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.0.4"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -132,18 +132,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "306b78788d1bb569edb7c55d622953c2414ca12445b41c9117963e03afc5c513"
|
||||
sha256: "49413c8ca514dea7633e8def233b25efdf83ec8522955cc2c0e3ad802927e7c6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.3.3"
|
||||
version: "12.1.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
|
||||
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
version: "7.0.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -180,10 +180,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
|
||||
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+2"
|
||||
version: "0.9.4+4"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -233,26 +233,26 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
|
||||
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.3"
|
||||
version: "0.14.4"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
|
||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
version: "6.0.0"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: a9966c850de5e445331b854fa42df96a8020066d67f125a5964cbc6556643f68
|
||||
sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "19.4.1"
|
||||
version: "19.4.2"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -273,18 +273,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_windows
|
||||
sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98
|
||||
sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.0.3"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3"
|
||||
sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.27"
|
||||
version: "2.0.30"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -307,10 +307,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.5.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -331,66 +331,74 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
|
||||
sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.2.0"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9"
|
||||
sha256: "8dfe08ea7fcf7467dbaf6889e72eebd5e0d6711caae201fdac780eb45232cd02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+22"
|
||||
version: "0.8.13+3"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
|
||||
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.1.0"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
|
||||
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+2"
|
||||
version: "0.8.13"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
|
||||
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+2"
|
||||
version: "0.2.2"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
|
||||
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+2"
|
||||
version: "0.2.2"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
|
||||
sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.1"
|
||||
version: "2.11.0"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_windows
|
||||
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
||||
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
version: "0.2.2"
|
||||
in_app_update:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: in_app_update
|
||||
sha256: "9924a3efe592e1c0ec89dda3683b3cfec3d4cd02d908e6de00c24b759038ddb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.5"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -439,10 +447,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.1"
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -463,10 +471,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
|
||||
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
version: "6.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -511,18 +519,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
|
||||
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.0"
|
||||
version: "9.0.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -559,26 +567,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
|
||||
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.4.0"
|
||||
version: "12.0.1"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
|
||||
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.1.0"
|
||||
version: "13.0.1"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98
|
||||
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.6"
|
||||
version: "9.4.7"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -607,10 +615,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
version: "7.0.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -639,18 +647,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
version: "6.0.3"
|
||||
protobuf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||
sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "4.2.0"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -703,10 +711,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad"
|
||||
sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.8"
|
||||
version: "2.4.13"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -812,10 +820,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d
|
||||
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.0"
|
||||
version: "0.10.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -844,26 +852,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
version: "6.3.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4"
|
||||
sha256: "199bc33e746088546a39cc5f36bac5a278c5e53b40cb3196f99e7345fdcfae6b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.15"
|
||||
version: "6.3.22"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
||||
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
version: "6.3.4"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -876,10 +884,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
version: "3.2.3"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -892,10 +900,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
|
||||
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.1"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -916,10 +924,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
version: "15.0.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -932,10 +940,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
|
||||
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.12.0"
|
||||
version: "5.14.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -948,10 +956,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: window_manager
|
||||
sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059"
|
||||
sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
version: "0.5.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -964,10 +972,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -977,5 +985,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
|
||||
17
pubspec.yaml
17
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: 2.4.0+2
|
||||
version: 2.6.1+7
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.0
|
||||
@@ -14,18 +14,19 @@ dependencies:
|
||||
flutter_local_notifications: ^19.4.1
|
||||
universal_ble: ^0.21.1
|
||||
intl: any
|
||||
protobuf: ^3.1.0
|
||||
permission_handler: ^11.4.0
|
||||
protobuf: ^4.2.0
|
||||
permission_handler: ^12.0.1
|
||||
dartx: any
|
||||
image_picker: ^1.1.2
|
||||
pointycastle: any
|
||||
window_manager: ^0.4.3
|
||||
device_info_plus: ^11.3.3
|
||||
window_manager: ^0.5.1
|
||||
device_info_plus: ^12.1.0
|
||||
keypress_simulator:
|
||||
path: keypress_simulator/packages/keypress_simulator
|
||||
shared_preferences: ^2.5.3
|
||||
flex_color_scheme: ^8.3.0
|
||||
package_info_plus: ^8.3.0
|
||||
package_info_plus: ^9.0.0
|
||||
in_app_update: ^4.2.5
|
||||
accessibility:
|
||||
path: accessibility
|
||||
http: ^1.3.0
|
||||
@@ -35,7 +36,9 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
|
||||
flutter_launcher_icons: "^0.14.3"
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- CHANGELOG.md
|
||||
|
||||
16
scripts/get_latest_changelog.sh
Executable file
16
scripts/get_latest_changelog.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Script to extract the latest changelog entry for Play Store uploads
|
||||
# Usage: ./scripts/get_latest_changelog.sh
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
CHANGELOG_FILE="$PROJECT_ROOT/CHANGELOG.md"
|
||||
|
||||
if [ ! -f "$CHANGELOG_FILE" ]; then
|
||||
echo "Error: CHANGELOG.md not found at $CHANGELOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the first changelog entry (between first ### and second ###)
|
||||
awk '/^### / {if (count++) exit} count' "$CHANGELOG_FILE" | tail -n +2 | sed 's/^- /• /'
|
||||
86
test/accessibility_disclosure_test.dart
Normal file
86
test/accessibility_disclosure_test.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:swift_control/widgets/accessibility_disclosure_dialog.dart';
|
||||
|
||||
void main() {
|
||||
group('AccessibilityDisclosureDialog', () {
|
||||
testWidgets('shows proper consent options with two buttons', (WidgetTester tester) async {
|
||||
bool acceptCalled = false;
|
||||
bool denyCalled = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: AccessibilityDisclosureDialog(
|
||||
onAccept: () => acceptCalled = true,
|
||||
onDeny: () => denyCalled = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Verify dialog shows proper title
|
||||
expect(find.text('Accessibility Service Permission Required'), findsOneWidget);
|
||||
|
||||
// Verify both consent options are present
|
||||
expect(find.text('Allow'), findsOneWidget);
|
||||
expect(find.text('Deny'), findsOneWidget);
|
||||
|
||||
// Verify explanation text is present
|
||||
expect(find.textContaining('AccessibilityService API'), findsOneWidget);
|
||||
expect(find.textContaining('simulate touch gestures'), findsOneWidget);
|
||||
expect(find.textContaining('No personal data'), findsOneWidget);
|
||||
|
||||
// Test deny button
|
||||
await tester.tap(find.text('Deny'));
|
||||
await tester.pump();
|
||||
expect(denyCalled, isTrue);
|
||||
|
||||
// Reset and test accept button
|
||||
denyCalled = false;
|
||||
await tester.tap(find.text('Allow'));
|
||||
await tester.pump();
|
||||
expect(acceptCalled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('includes required disclosure information', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: AccessibilityDisclosureDialog(
|
||||
onAccept: () {},
|
||||
onDeny: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Check for key disclosure elements required by Play Store
|
||||
expect(find.textContaining('Why is this permission needed?'), findsOneWidget);
|
||||
expect(find.textContaining('How does SwiftControl use this permission?'), findsOneWidget);
|
||||
expect(find.textContaining('Zwift Click, Zwift Ride, or Zwift Play'), findsOneWidget);
|
||||
expect(find.textContaining('training app window is active'), findsOneWidget);
|
||||
expect(find.textContaining('You must choose to either Allow or Deny'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('prevents dismissal via back navigation', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: AccessibilityDisclosureDialog(
|
||||
onAccept: () {},
|
||||
onDeny: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Verify PopScope is present to prevent back navigation
|
||||
expect(find.byType(PopScope), findsOneWidget);
|
||||
|
||||
// Get the PopScope widget and verify canPop is false
|
||||
final popScope = tester.widget<PopScope>(find.byType(PopScope));
|
||||
expect(popScope.canPop, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
69
test/changelog_test.dart
Normal file
69
test/changelog_test.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:swift_control/utils/changelog.dart';
|
||||
|
||||
void main() {
|
||||
group('ChangelogParser', () {
|
||||
test('parses changelog entries correctly', () {
|
||||
const testContent = '''
|
||||
### 2.6.0 (2025-09-28)
|
||||
- Fix crashes on some Android devices
|
||||
- refactor touch placements: show touches on screen
|
||||
- show firmware version of connected device
|
||||
|
||||
### 2.5.0 (2025-09-25)
|
||||
- Improve usability
|
||||
- SwiftControl is now available via the Play Store
|
||||
- SwiftControl will continue to be available to download for free on GitHub
|
||||
''';
|
||||
|
||||
final entries = ChangelogParser.parseContent(testContent);
|
||||
|
||||
expect(entries.length, 2);
|
||||
|
||||
expect(entries[0].version, '2.6.0');
|
||||
expect(entries[0].date, '2025-09-28');
|
||||
expect(entries[0].changes.length, 3);
|
||||
expect(entries[0].changes[0], 'Fix crashes on some Android devices');
|
||||
|
||||
expect(entries[1].version, '2.5.0');
|
||||
expect(entries[1].date, '2025-09-25');
|
||||
expect(entries[1].changes.length, 3);
|
||||
expect(entries[1].changes[0], 'Improve usability');
|
||||
expect(entries[1].changes[1], 'SwiftControl is now available via the Play Store');
|
||||
expect(entries[1].changes[2], 'SwiftControl will continue to be available to download for free on GitHub');
|
||||
});
|
||||
|
||||
test('handles empty content', () {
|
||||
const testContent = '';
|
||||
final entries = ChangelogParser.parseContent(testContent);
|
||||
expect(entries.length, 0);
|
||||
});
|
||||
|
||||
test('handles single entry', () {
|
||||
const testContent = '''
|
||||
### 1.0.0 (2025-01-01)
|
||||
- Initial release
|
||||
''';
|
||||
|
||||
final entries = ChangelogParser.parseContent(testContent);
|
||||
|
||||
expect(entries.length, 1);
|
||||
expect(entries[0].version, '1.0.0');
|
||||
expect(entries[0].changes.length, 1);
|
||||
expect(entries[0].changes[0], 'Initial release');
|
||||
});
|
||||
|
||||
test('ChangelogEntry toString formats correctly', () {
|
||||
final entry = ChangelogEntry(
|
||||
version: '1.0.0',
|
||||
date: '2025-01-01',
|
||||
changes: ['Change 1', 'Change 2'],
|
||||
);
|
||||
|
||||
final result = entry.toString();
|
||||
expect(result, contains('### 1.0.0 (2025-01-01)'));
|
||||
expect(result, contains('- Change 1'));
|
||||
expect(result, contains('- Change 2'));
|
||||
});
|
||||
});
|
||||
}
|
||||
58
test/orientation_test.dart
Normal file
58
test/orientation_test.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:swift_control/pages/touch_area.dart';
|
||||
|
||||
void main() {
|
||||
group('TouchAreaSetupPage Orientation Tests', () {
|
||||
testWidgets('TouchAreaSetupPage should force landscape orientation on init', (WidgetTester tester) async {
|
||||
// Track system chrome method calls
|
||||
final List<MethodCall> systemChromeCalls = [];
|
||||
|
||||
// Mock SystemChrome.setPreferredOrientations
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||
systemChromeCalls.add(methodCall);
|
||||
return null;
|
||||
});
|
||||
|
||||
// Build the TouchAreaSetupPage
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: TouchAreaSetupPage(),
|
||||
),
|
||||
);
|
||||
|
||||
// Verify that setPreferredOrientations was called with landscape orientations
|
||||
final orientationCalls = systemChromeCalls
|
||||
.where((call) => call.method == 'SystemChrome.setPreferredOrientations')
|
||||
.toList();
|
||||
|
||||
expect(orientationCalls, isNotEmpty);
|
||||
|
||||
// Check if landscape orientations were set
|
||||
final lastOrientationCall = orientationCalls.last;
|
||||
final orientations = lastOrientationCall.arguments as List<String>;
|
||||
|
||||
expect(orientations, contains('DeviceOrientation.landscapeLeft'));
|
||||
expect(orientations, contains('DeviceOrientation.landscapeRight'));
|
||||
expect(orientations, hasLength(2)); // Only landscape orientations
|
||||
});
|
||||
|
||||
test('DeviceOrientation enum values are accessible', () {
|
||||
// Test that we can access the DeviceOrientation enum values
|
||||
final orientations = [
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
];
|
||||
|
||||
expect(orientations, hasLength(4));
|
||||
expect(orientations, contains(DeviceOrientation.landscapeLeft));
|
||||
expect(orientations, contains(DeviceOrientation.landscapeRight));
|
||||
expect(orientations, contains(DeviceOrientation.portraitUp));
|
||||
expect(orientations, contains(DeviceOrientation.portraitDown));
|
||||
});
|
||||
});
|
||||
}
|
||||
56
test/vibration_setting_test.dart
Normal file
56
test/vibration_setting_test.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:swift_control/utils/settings/settings.dart';
|
||||
|
||||
void main() {
|
||||
group('Vibration Setting Tests', () {
|
||||
late Settings settings;
|
||||
|
||||
setUp(() async {
|
||||
// Initialize SharedPreferences with in-memory storage for testing
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
settings = Settings();
|
||||
await settings.init();
|
||||
});
|
||||
|
||||
test('Vibration setting should default to true', () {
|
||||
expect(settings.getVibrationEnabled(), true);
|
||||
});
|
||||
|
||||
test('Vibration setting should persist when set to false', () async {
|
||||
await settings.setVibrationEnabled(false);
|
||||
expect(settings.getVibrationEnabled(), false);
|
||||
});
|
||||
|
||||
test('Vibration setting should persist when set to true', () async {
|
||||
await settings.setVibrationEnabled(true);
|
||||
expect(settings.getVibrationEnabled(), true);
|
||||
});
|
||||
|
||||
test('Vibration setting should toggle correctly', () async {
|
||||
// Start with default (true)
|
||||
expect(settings.getVibrationEnabled(), true);
|
||||
|
||||
// Toggle to false
|
||||
await settings.setVibrationEnabled(false);
|
||||
expect(settings.getVibrationEnabled(), false);
|
||||
|
||||
// Toggle back to true
|
||||
await settings.setVibrationEnabled(true);
|
||||
expect(settings.getVibrationEnabled(), true);
|
||||
});
|
||||
|
||||
test('Vibration setting should persist across Settings instances', () async {
|
||||
// Set vibration to false
|
||||
await settings.setVibrationEnabled(false);
|
||||
expect(settings.getVibrationEnabled(), false);
|
||||
|
||||
// Create a new Settings instance
|
||||
final newSettings = Settings();
|
||||
await newSettings.init();
|
||||
|
||||
// Should still be false
|
||||
expect(newSettings.getVibrationEnabled(), false);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user