Compare commits

..

144 Commits

Author SHA1 Message Date
Jonas Bark
f83defb37b introduce workaround for Zwift Click V2 (reset every minute) 2025-10-01 16:17:20 +02:00
Jonas Bark
5c8db11536 Merge branch 'web' 2025-10-01 15:42:43 +02:00
Jonas Bark
30aa5b33a3 fix issue #81 2025-10-01 15:42:20 +02:00
Jonas Bark
ca41e69a17 fix issue #81 2025-10-01 15:41:19 +02:00
Jonas Bark
af4d8ab183 Merge branch 'main' into web 2025-09-30 17:59:18 +02:00
Jonas Bark
c1a24cfbd1 some more experiments 2025-09-30 17:59:12 +02:00
Jonas Bark
86b406e2a4 adjust changelog 2025-09-30 15:19:35 +02:00
Jonas Bark
1ec93330b0 Merge branch 'web' 2025-09-30 15:18:54 +02:00
Jonas Bark
4ed3c5fefe adjust changelog 2025-09-30 15:18:38 +02:00
Jonas Bark
54d106ff4e some more experiments - make it clear how to properly use Zwift Click v2 2025-09-30 15:17:51 +02:00
Jonas Bark
996669ec44 implement updated protocol for Zwift Ride and Zwift Click V2 2025-09-30 11:33:54 +02:00
Jonas Bark
1d38ff521a misc changes 2025-09-30 10:01:01 +02:00
Jonas Bark
f0c1409da4 misc changes 2025-09-30 09:12:50 +02:00
Jonas Bark
9617198db7 adjust changelog 2025-09-30 09:01:54 +02:00
Jonas Bark
e4863b1ebd Merge branch 'web' of github.com:jonasbark/swiftcontrol into web 2025-09-30 09:00:45 +02:00
Jonas Bark
d51a4cc29d cleanup 2025-09-30 09:00:41 +02:00
Jonas Bark
dcbb225355 cleanup 2025-09-30 09:00:30 +02:00
jonasbark
cba449b493 Merge pull request #80 from jonasbark/copilot/fix-3ddab2b9-517e-451f-827c-78dff444def4
[WIP] Create a setting, visible only when connected to a Zwift Ride device, to enable or disable the vibration message. Default is on.
2025-09-30 09:00:07 +02:00
copilot-swe-agent[bot]
559fe1232b Add test for vibration setting functionality
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-30 06:59:18 +00:00
copilot-swe-agent[bot]
a7f9ca489e Add vibration toggle setting for Zwift Ride devices
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-30 06:57:43 +00:00
copilot-swe-agent[bot]
74bf75a82e Initial plan 2025-09-30 06:54:32 +00:00
Jonas Bark
747629cebf Merge branch 'web' of github.com:jonasbark/swiftcontrol into web 2025-09-30 08:53:40 +02:00
jonasbark
aca6e9272b Merge pull request #79 from jonasbark/copilot/fix-b3d14829-b03a-4383-a558-c58fb62f2f16
Integrate changelog: Add in-app changelog screen, update dialog, and Play Store automation
2025-09-30 08:53:34 +02:00
copilot-swe-agent[bot]
18e6f9a1b5 Update build.yml to use changelog script for Play Store uploads
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-30 06:51:21 +00:00
copilot-swe-agent[bot]
c3532d5c35 Add quick reference guide for changelog integration
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-30 06:46:31 +00:00
copilot-swe-agent[bot]
1a88f45c93 Add implementation summary for changelog integration
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-30 06:45:12 +00:00
copilot-swe-agent[bot]
b49eda7fc7 Add comprehensive documentation for changelog integration
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-30 06:44:18 +00:00
Jonas Bark
f0b3bc70b2 Android: stop foreground service when disconnecting, update dependencies 2025-09-30 08:42:27 +02:00
copilot-swe-agent[bot]
08700edc22 Implement changelog integration features
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-30 06:41:46 +00:00
copilot-swe-agent[bot]
d698c9bbea Initial plan 2025-09-30 06:35:36 +00:00
jonasbark
eea1b8eb40 Merge pull request #78 from jonasbark/copilot/fix-d9ff38c6-4028-48c0-83e5-8755ed4d98b0
Fix keymap editing orientation to prevent misplaced touch positions
2025-09-29 17:48:56 +02:00
copilot-swe-agent[bot]
0118c5a87c Update keymap editing instructions to clarify landscape orientation
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-29 15:31:50 +00:00
copilot-swe-agent[bot]
65a3374d9c Force landscape orientation during keymap editing
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-29 15:29:32 +00:00
copilot-swe-agent[bot]
536225bf12 Initial plan 2025-09-29 15:25:09 +00:00
Jonas Bark
e858d35617 web fixes 2025-09-29 13:07:11 +02:00
Jonas Bark
6d87e85353 web only pipeline 2025-09-29 12:57:11 +02:00
Jonas Bark
d1fed35c3e some experiments for Click V2 2025-09-29 12:56:04 +02:00
Jonas Bark
d9297bd40e make connection work on iOS 2025-09-29 10:14:19 +02:00
Jonas Bark
a1926dfc00 Revert "remove iOS folder as Apple does not support the core functionality"
This reverts commit d0291c68d7.
2025-09-29 09:54:43 +02:00
Jonas Bark
d55ba039af version++ 2025-09-28 15:45:20 +02:00
Jonas Bark
c9ebc5a9f6 don't show actual touches on screen for Android 2025-09-28 15:39:01 +02:00
Jonas Bark
be0c2d97ba show firmware version of connected device 2025-09-27 15:11:24 +02:00
Jonas Bark
a03cc76eaa show firmware version of connected device 2025-09-27 15:03:02 +02:00
Jonas Bark
504c71d5c4 refactor touch placements: show touches on screen, fix misplaced coordinates - should fix #64 2025-09-27 14:48:23 +02:00
Jonas Bark
d0291c68d7 remove iOS folder as Apple does not support the core functionality 2025-09-27 14:02:10 +02:00
Jonas Bark
33e5e41eff disconnect devices when still connected from previous session, don't show update app dialog during debugging 2025-09-27 12:34:40 +02:00
Jonas Bark
221d5a0b8d fix crashes on some Android devices 2025-09-27 12:18:50 +02:00
Jonas Bark
b899487ee9 Merge branch 'main' of github.com:jonasbark/swiftcontrol 2025-09-26 15:06:38 +02:00
jonasbark
ff0b724a73 Update README.md 2025-09-26 11:12:34 +02:00
jonasbark
647c20a6a3 Make Google Play badge clickable in README
Updated Google Play badge to be a clickable link.
2025-09-26 11:02:27 +02:00
jonasbark
c36e63aa8d Fix Google Play badge and update download info
Updated Google Play badge link and added image.
2025-09-26 11:01:54 +02:00
jonasbark
cb523ea656 Update README to specify Zwift Click v2 support
Clarify support status for Zwift Click v2.
2025-09-26 10:57:40 +02:00
Jonas Bark
22b99f4f6d Merge branch 'main' of github.com:jonasbark/swiftcontrol 2025-09-25 14:21:03 +02:00
Jonas Bark
05e681b59a use prerelease attribute in github actions 2025-09-25 14:19:42 +02:00
jonasbark
07ee91c17a Clarify download link for latest version
Updated the download link description for clarity.
2025-09-25 13:46:34 +02:00
Jonas Bark
323a344c3a actions test 2025-09-25 13:42:08 +02:00
Jonas Bark
0172b1cf90 actions test 2025-09-25 13:26:24 +02:00
Jonas Bark
5a5e4066f6 Merge remote-tracking branch 'origin/main' 2025-09-25 12:56:10 +02:00
Jonas Bark
3256f5aa15 actions test 2025-09-25 12:56:02 +02:00
Jonas Bark
476a9a337f actions test 2025-09-25 12:54:22 +02:00
jonasbark
1f1ce58bd9 Update CHANGELOG for version 2.5.0
Added note about voucher for donors
2025-09-25 11:34:40 +02:00
Jonas Bark
bbb3dd3397 increase version 2025-09-25 11:16:49 +02:00
Jonas Bark
d7cee77c8b improve usability 2025-09-25 11:03:33 +02:00
Jonas Bark
e2ac975c75 rename Android package name, revert Zwift Click V2 encryption support, add play store assets 2025-09-24 09:12:21 +02:00
Jonas Bark
5e9352316c offer to get app from Play Store 2025-09-24 08:51:19 +02:00
Jonas Bark
c73adb7c0d version++ 2025-09-24 08:47:44 +02:00
Jonas Bark
c3b41f56d4 Merge remote-tracking branch 'origin/copilot/fix-74' 2025-09-24 08:42:39 +02:00
copilot-swe-agent[bot]
6fe841af58 Enhance disclosure dialog with navigation prevention and Play Store description
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-24 06:28:31 +00:00
copilot-swe-agent[bot]
d97307de6f Add accessibility disclosure dialog with proper consent options
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-24 06:26:05 +00:00
copilot-swe-agent[bot]
826dc2327f Initial plan 2025-09-24 06:20:32 +00:00
Jonas Bark
3466e504e3 implement in app update for Android 2025-09-22 13:41:50 +02:00
Jonas Bark
ebd7f80947 upload app bundle to play store 2025-09-22 13:27:30 +02:00
Jonas Bark
43e827d8f5 build app bundle for play store 2025-09-22 10:11:25 +02:00
Jonas Bark
5d5dc2e152 build app bundle for play store 2025-09-22 09:53:25 +02:00
Jonas Bark
c0d2eaa897 adjust readme to ensure Windows users to not pair their Zwift device with Windows 2025-09-22 09:35:55 +02:00
Jonas Bark
13c70fc445 enable encryption for Zwift Click v2 to potentially fix #68 2025-09-22 09:28:35 +02:00
jonasbark
1e11d28765 Merge pull request #71 from jonasbark/copilot/fix-64
Fix Windows mouse clicks at wrong location due to display scaling
2025-09-17 08:49:53 +02:00
copilot-swe-agent[bot]
7ee9bc43a0 Fix changelog date to 2025-09-17
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-17 06:49:09 +00:00
copilot-swe-agent[bot]
372085ec0e Update version to 2.4.0+1 and add changelog entry
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-17 06:46:52 +00:00
copilot-swe-agent[bot]
e758b35837 Fix Windows mouse click scaling for high DPI displays
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-17 06:32:42 +00:00
copilot-swe-agent[bot]
dee7b86120 Initial plan 2025-09-17 06:28:06 +00:00
Jonas Bark
b3ec7e7a3a funding 2025-09-16 20:08:51 +02:00
Jonas Bark
bbd01d023a - Show an overview of the keymap bindings
- Allow customizing an existing keymap
2025-09-16 10:32:09 +02:00
Jonas Bark
36282c9fa9 better donate options 2025-09-16 08:59:50 +02:00
jonasbark
daea07c409 Clarify iOS not being supported 2025-09-15 08:08:07 +02:00
jonasbark
49d7445d0e Aktualisieren von README.md 2025-09-11 21:14:32 +02:00
jonasbark
9bb0e5616a Aktualisieren von pubspec.yaml 2025-09-11 19:27:47 +02:00
jonasbark
7e98f595ee Aktualisieren von CHANGELOG.md 2025-09-11 19:27:18 +02:00
Jonas Bark
a9fdc4b16e attempt to add support for Zwift Click v2 2025-09-10 17:40:14 +02:00
Jonas Bark
c06819b502 attempt to add support for Zwift Click v2 2025-09-10 08:42:55 +02:00
Jonas Bark
969faca658 attempt to add support for Zwift Click v2 2025-09-09 09:19:52 +02:00
Jonas Bark
61fbb099e2 actions fix 2025-09-08 16:55:28 +02:00
Jonas Bark
fbd6356be0 donate button change 2025-09-08 16:54:23 +02:00
Jonas Bark
1c40455bf3 update readme 2025-09-08 16:42:30 +02:00
Jonas Bark
15129634a6 update some libraries to ensure compatibility with latest Flutter 2025-09-08 16:23:20 +02:00
Jonas Bark
89d35d7734 update some libraries to ensure compatibility with latest Flutter 2025-09-08 15:49:31 +02:00
Jonas Bark
d959bfb4c9 Windows: adjust key sending method to improve compatibility with more apps (fixes #62) 2025-09-08 15:33:28 +02:00
Jonas Bark
9bc25514ae add launch.json for easier entry when using Visual Studio Code 2025-09-08 14:46:07 +02:00
Jonas Bark
25210b57ba try to add dlls to ZIP to potentially fix #54 2025-09-08 14:21:27 +02:00
jonasbark
c9317e369c Merge pull request #62 from jonasbark/copilot/fix-61
Add long press mode option for custom keymaps
2025-09-08 14:02:04 +02:00
Jonas Bark
2195c19ed9 allow long touches / keyboard presses (fixes #61) 2025-09-08 14:01:28 +02:00
Jonas Bark
d13a9d72c9 mark versions not ending with +0 as beta versions 2025-09-08 13:43:14 +02:00
Jonas Bark
55d230e41c Merge branch 'main' into copilot/fix-61 2025-09-08 12:59:14 +02:00
Jonas Bark
ffa604f921 fix logging messages 2025-09-08 12:59:04 +02:00
copilot-swe-agent[bot]
93bdfeeaa7 Refactor action method parameters from isPressed/isRepeated to isKeyDown/isKeyUp
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-07 10:18:20 +00:00
copilot-swe-agent[bot]
336c64e5a9 Update version to 2.2.0 in pubspec.yaml and CHANGELOG.md
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-06 07:03:19 +00:00
copilot-swe-agent[bot]
20a706d93d Address feedback: remove documentation file, revert README changes, bump version and update changelog
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-06 06:58:55 +00:00
copilot-swe-agent[bot]
21cb8844fc Complete long press feature implementation with cleanup, tests and documentation
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-06 06:48:13 +00:00
copilot-swe-agent[bot]
4bc1a3b1d0 Add long press functionality to KeyPair and update UI
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-09-06 06:46:23 +00:00
copilot-swe-agent[bot]
9df1f7cfa6 Initial plan 2025-09-06 06:39:18 +00:00
jonasbark
72cdf86802 Update README.md 2025-08-18 10:06:22 +02:00
jonasbark
9a53d5fdab Merge pull request #32 from jonasbark/copilot/fix-31
Fix Windows compilation error: FindTargetWindow identifier not found
2025-07-04 09:25:26 +02:00
copilot-swe-agent[bot]
458e6333a0 Fix C++ forward declaration error for FindTargetWindow
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-07-04 07:22:34 +00:00
copilot-swe-agent[bot]
f42e483260 Initial plan 2025-07-04 07:19:09 +00:00
jonasbark
dda2135129 Merge pull request #30 from jonasbark/copilot/fix-26
Implement window-focused key simulation for Windows applications
2025-07-04 08:51:42 +02:00
copilot-swe-agent[bot]
bc2831c17e Update version to 2.1.0 and add changelog entry for automatic window focusing feature
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-07-03 15:00:12 +00:00
copilot-swe-agent[bot]
310313c3b2 Add check to avoid focusing window if already in foreground
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-07-03 14:56:11 +00:00
copilot-swe-agent[bot]
2122568461 Remove documentation and utility files as requested, keep automatic window focusing in Windows C++ plugin
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-07-03 14:23:43 +00:00
copilot-swe-agent[bot]
144fd5b740 Remove Dart API changes and implement automatic window focusing in Windows SimulateKeyPress method
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-07-03 14:13:14 +00:00
copilot-swe-agent[bot]
5f7a1a8203 Complete window-focused key simulation implementation with utilities and README update
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-07-03 14:02:59 +00:00
copilot-swe-agent[bot]
258b396444 Add documentation and testing tools for window-focused key simulation feature
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-07-03 14:01:10 +00:00
copilot-swe-agent[bot]
5861533793 Improve window-focused key simulation with better error handling and SendInput fallback
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-07-03 13:59:47 +00:00
copilot-swe-agent[bot]
3106bd09e8 Implement window-focused key simulation for Windows applications
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-07-03 13:53:30 +00:00
copilot-swe-agent[bot]
a3475a02d2 Initial plan 2025-07-03 13:42:04 +00:00
Jonas Bark
fb1a1f35ad you can now assign Escape and arrow down key to your custom keymap (fixes #18) 2025-05-04 10:46:00 +02:00
Jonas Bark
71aadde901 more troubleshooting, always use light theme 2025-05-02 19:10:56 +02:00
Jonas Bark
f7bfd8c206 UX improvements 2025-04-25 09:23:58 +02:00
Jonas Bark
ff83e5271b add Biketerra keymap (fixes #17) 2025-04-23 08:29:34 +02:00
Jonas Bark
ec6edb2864 add Biketerra keymap (fixes #17) 2025-04-18 09:44:19 +02:00
Jonas Bark
4f4a6f60c5 fix MyWhoosh up / downshift button assignment (I key vs K key) 2025-04-15 11:18:42 +02:00
Jonas Bark
354e13678b fix Zwift Click button assignment #12 2025-04-13 20:47:42 +02:00
Jonas Bark
f1b8822e20 vibrate Zwift Play / Zwift Ride controller on gear shift (thanks @cagnulein, closes #16) 2025-04-10 13:31:55 +02:00
jonasbark
6bf83b1034 Aktualisieren von README.md 2025-04-09 17:43:06 +02:00
Jonas Bark
7b1e4ede2a version++ 2025-04-08 08:53:57 +02:00
Jonas Bark
a554820115 open menu to make it clear you can simulate touch *and* keyboard press 2025-04-08 08:42:43 +02:00
Jonas Bark
cb9f9ea5b3 reconnect device if connection is lost 2025-04-08 08:33:18 +02:00
Jonas Bark
4051553a56 fix button assignment and logging 2025-04-08 08:20:15 +02:00
Jonas Bark
01a213354b potentially fix #12 2025-04-08 08:05:37 +02:00
Jonas Bark
962abfb38e add some personal preference for MyWhoosh 2025-04-07 15:35:05 +02:00
Jonas Bark
ada4cf0dfd Android: better approximation of button placement for freeform windows 2025-04-07 15:21:45 +02:00
Jonas Bark
aff1137c3d fix bluetooth scan issues on older Android devices by asking for location permission 2025-04-07 12:48:20 +02:00
Jonas Bark
7f24c27201 update readme 2025-04-06 16:21:14 +02:00
Jonas Bark
51c5e34220 long pressing a button now repeats the action every 250ms until it's released 2025-04-06 14:57:50 +02:00
Jonas Bark
10c2cc64a2 don't build on Readme updates 2025-04-06 14:02:30 +02:00
94 changed files with 13215 additions and 750 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,2 @@
github: [jonasbark]
open_collective: jonas-bark1
custom: ["https://paypal.me/boni"]
custom: ["https://paypal.me/boni", "https://donate.stripe.com/9B6aEX0muajY8bZ1Kl6J200"]

View File

@@ -4,6 +4,12 @@ on:
push:
branches:
- main
paths:
- '.github/workflows/**'
- 'lib/**'
- 'accessibility/**'
- 'keypress_simulator/**'
- 'pubspec.yaml'
jobs:
build:
@@ -76,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/"
@@ -128,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 }}
@@ -140,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
@@ -173,6 +202,25 @@ jobs:
- name: Zip directory (Windows)
shell: pwsh
run: |
$source = "C:\Windows\System32"
$destination = "build\windows\x64\runner\Release"
# List of required DLLs
$dlls = @("msvcp140.dll", "vcruntime140.dll", "vcruntime140_1.dll")
# Copy each file
foreach ($dll in $dlls) {
$srcPath = Join-Path $source $dll
$destPath = Join-Path $destination $dll
if (Test-Path $srcPath) {
Copy-Item -Path $srcPath -Destination $destPath -Force
Write-Output "Copied $dll to $destination"
} else {
Write-Warning "$dll not found in $source"
}
}
Compress-Archive -Path "build/windows/x64/runner/Release/*" -DestinationPath "build/windows/x64/runner/Release/SwiftControl.windows.zip"
#9 Upload Artifacts

49
.github/workflows/web.yml vendored Normal file
View 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
View File

@@ -45,3 +45,5 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
service-account.json

23
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "swiftcontrol",
"request": "launch",
"type": "dart"
},
{
"name": "swiftcontrol (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "swiftcontrol (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

View File

@@ -1,4 +1,71 @@
#### 2.0.0 (2025-04-06)
### 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)
### 2.4.0 (2025-09-16)
- Show an overview of the keymap bindings
- Allow customizing an existing keymap
- Add more donation options
### 2.3.0 (2025-09-11)
- Add support for latest Zwift Click v2
### 2.2.0 (2025-09-08)
- Add Long Press Mode option for custom keymaps - buttons can now send sustained key presses instead of repeated taps, perfect for movement controls in games (fixes #61)
- Windows: adjust key sending method to improve compatibility with more apps (fixes #62)
### 2.1.0 (2025-07-03)
- Windows: automatically focus compatible training apps (MyWhoosh, IndieVelo, Biketerra) when sending keystrokes, enabling seamless multi-window usage
### 2.0.9 (2025-05-04)
- you can now assign Escape and arrow down key to your custom keymap (#18)
### 2.0.8 (2025-05-02)
- only use the light theme for the app
- more troubleshooting information
### 2.0.7 (2025-04-18)
- add Biketerra.com keymap
- some UX improvements
### 2.0.6 (2025-04-15)
- fix MyWhoosh up / downshift button assignment (I key vs K key)
### 2.0.5 (2025-04-13)
- fix Zwift Click button assignment (#12)
### 2.0.4 (2025-04-10)
- vibrate Zwift Play / Zwift Ride controller on gear shift (thanks @cagnulein, closes #16)
### 2.0.3 (2025-04-08)
- adjust TrainingPeaks Virtual key mapping (#12)
- attempt to reconnect device if connection is lost
- Android: detect freeform windows for MyWhoosh + TrainingPeaks Virtual keymaps
### 2.0.2 (2025-04-07)
- fix bluetooth scan issues on older Android devices by asking for location permission
### 2.0.1 (2025-04-06)
- long pressing a button will trigger the action again every 250ms
### 2.0.0 (2025-04-06)
- You can now customize the actions (touches, mouse clicks or keyboard keys) for all buttons on all supported Zwift devices
- now shows the battery level of the connected devices
- add more troubleshooting information

View File

@@ -4,7 +4,14 @@
## Description
With SwiftControl you can control your favorite trainer app using your Zwift Click, Zwift Ride or Zwift Play devices. Primarily useful to perform virtual gear shifting.
With SwiftControl you can **control your favorite trainer app** using your Zwift Click, Zwift Ride or Zwift Play devices. Here's what you can do with it, depending on your configuration:
- Virtual Gear shifting
- Steering / turning
- adjust workout intensity
- control music on your device
- more? If you can do it via keyboard, mouse or touch, you can do it with SwiftControl
**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
@@ -13,40 +20,53 @@ 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
- indieVelo / Training Peaks
- Biketerra.com
- any other:
- Android: you can customize simulated touch points of all your buttons in the app
- Desktop: you can customize keyboard shortcuts and mouse clicks in the app
## Supported Devices
- Zwift Click
- Zwift Click v2 (mostly, see #68)
- Zwift Ride
- Zwift Play
## Supported Platforms
- Android
- App is losing connection over time? Read about how to [keep the app alive](https://dontkillmyapp.com/).
- macOS
- Windows (make sure you have installed the "[Microsoft Visual C++ Runtime libraries](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)")
- Windows
- Windows may flag the app as virus. I think it does so because the app does control the mouse and keyboard.
- Bluetooth connection unstable? You may need to use an [external Bluetooth adapter](https://github.com/jonasbark/swiftcontrol/issues/14#issuecomment-3193839509).
- Make sure your Zwift device is not paired with Windows Bluetooth settings: [more information](https://github.com/jonasbark/swiftcontrol/issues/70).
- [Web](https://jonasbark.github.io/swiftcontrol/) (you won't be able to do much)
- NOT SUPPORTED: iOS (iPhone, iPad) as Apple does not provide any way to simulate touches or keyboard events
## Troubleshooting
Your Zwift device is found but connection does not work properly? You may need to update the firmware in Zwift Companion app.
- Your Zwift device is found but connection does not work properly? You may need to update the firmware in Zwift Companion app.
## How does it work?
The app connects to your Zwift device automatically.
The app connects to your Zwift device automatically. It does not connect to your trainer itself.
- When using Android a "click" 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 and indieVelo / Training Peaks
- there are predefined Keymaps for MyWhoosh, indieVelo / Training Peaks, and others
- you can also create your own Keymaps for any other app
- you can also use the mouse to click on a certain part of the screen, or use keyboard shortcuts
## Alternatives
- [qdomyos-zwift](https://www.qzfitness.com/) directly controls the trainer (as opposed to controlling the trainer app)
## Donate
Please consider donating to support the development of this app.
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/boni)
Please consider donating to support the development of this app :)
- [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)

View File

@@ -61,23 +61,29 @@ enum class MediaAction(val raw: Int) {
/** Generated class from Pigeon that represents data sent in messages. */
data class WindowEvent (
val packageName: String,
val windowHeight: Long,
val windowWidth: Long
val top: Long,
val bottom: Long,
val right: Long,
val left: Long
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): WindowEvent {
val packageName = pigeonVar_list[0] as String
val windowHeight = pigeonVar_list[1] as Long
val windowWidth = pigeonVar_list[2] as Long
return WindowEvent(packageName, windowHeight, windowWidth)
val top = pigeonVar_list[1] as Long
val bottom = pigeonVar_list[2] as Long
val right = pigeonVar_list[3] as Long
val left = pigeonVar_list[4] as Long
return WindowEvent(packageName, top, bottom, right, left)
}
}
fun toList(): List<Any?> {
return listOf(
packageName,
windowHeight,
windowWidth,
top,
bottom,
right,
left,
)
}
override fun equals(other: Any?): Boolean {
@@ -88,8 +94,10 @@ data class WindowEvent (
return true
}
return packageName == other.packageName
&& windowHeight == other.windowHeight
&& windowWidth == other.windowWidth
&& top == other.top
&& bottom == other.bottom
&& right == other.right
&& left == other.left
}
override fun hashCode(): Int = toList().hashCode()
@@ -131,7 +139,7 @@ val AccessibilityApiPigeonMethodCodec = StandardMethodCodec(AccessibilityApiPige
interface Accessibility {
fun hasPermission(): Boolean
fun openPermissions()
fun performTouch(x: Double, y: Double)
fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean)
fun controlMedia(action: MediaAction)
companion object {
@@ -181,8 +189,10 @@ interface Accessibility {
val args = message as List<Any?>
val xArg = args[0] as Double
val yArg = args[1] as Double
val isKeyDownArg = args[2] as Boolean
val isKeyUpArg = args[3] as Boolean
val wrapped: List<Any?> = try {
api.performTouch(xArg, yArg)
api.performTouch(xArg, yArg, isKeyDownArg, isKeyUpArg)
listOf(null)
} catch (exception: Throwable) {
wrapError(exception)

View File

@@ -7,6 +7,7 @@ import StreamEventsStreamHandler
import WindowEvent
import android.content.Context
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.provider.Settings
import androidx.core.content.ContextCompat.startActivity
@@ -50,23 +51,27 @@ class AccessibilityPlugin: FlutterPlugin, Accessibility {
}, Bundle.EMPTY)
}
override fun performTouch(x: Double, y: Double) {
Observable.toService?.performTouch(x = x, y = y) ?: error("Service not running")
override fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean) {
Observable.toService?.performTouch(x = x, y = y, isKeyUp = isKeyUp, isKeyDown = isKeyDown) ?: error("Service not running")
}
override fun controlMedia(action: MediaAction) {
val audioService = context.getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager
when (action) {
MediaAction.PLAY_PAUSE -> {
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
}
MediaAction.NEXT -> {
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
}
MediaAction.VOLUME_DOWN -> {
audioService.adjustVolume(android.media.AudioManager.ADJUST_LOWER, android.media.AudioManager.FLAG_SHOW_UI)
}
MediaAction.VOLUME_UP -> {
audioService.adjustVolume(android.media.AudioManager.ADJUST_RAISE, android.media.AudioManager.FLAG_SHOW_UI)
}
MediaAction.VOLUME_DOWN -> audioService.adjustVolume(android.media.AudioManager.ADJUST_LOWER, android.media.AudioManager.FLAG_SHOW_UI)
MediaAction.VOLUME_UP -> audioService.adjustVolume(android.media.AudioManager.ADJUST_RAISE, android.media.AudioManager.FLAG_SHOW_UI)
}
}
@@ -84,8 +89,8 @@ class EventListener : StreamEventsStreamHandler(), Receiver {
eventSink = null
}
override fun onChange(packageName: String, windowWidth: Int, windowHeight: Int) {
eventSink?.success(WindowEvent(packageName = packageName, windowWidth = windowWidth.toLong(), windowHeight = windowHeight.toLong()))
override fun onChange(packageName: String, window: Rect) {
eventSink?.success(WindowEvent(packageName = packageName, right = window.right.toLong(), left = window.left.toLong(), bottom = window.bottom.toLong(), top = window.top.toLong()))
}
}

View File

@@ -5,7 +5,6 @@ import android.accessibilityservice.GestureDescription
import android.accessibilityservice.GestureDescription.StrokeDescription
import android.graphics.Path
import android.graphics.Rect
import android.os.Build
import android.util.Log
import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityEvent
@@ -37,36 +36,29 @@ class AccessibilityService : AccessibilityService(), Listener {
}
val currentPackageName = event.packageName.toString()
val windowSize = getWindowSize()
Observable.fromService?.onChange(packageName = currentPackageName, windowHeight = windowSize.bottom, windowWidth = windowSize.right)
Observable.fromService?.onChange(packageName = currentPackageName, window = windowSize)
}
private fun getWindowSize(): Rect {
val outBounds = Rect()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
rootInActiveWindow.getBoundsInWindow(outBounds)
} else {
rootInActiveWindow.getBoundsInScreen(outBounds)
}
rootInActiveWindow?.getBoundsInScreen(outBounds)
return outBounds
}
private fun simulateTap(x: Double, y: Double) {
val gestureBuilder = GestureDescription.Builder()
val path = Path()
path.moveTo(x.toFloat(), y.toFloat())
path.lineTo(x.toFloat()+1, y.toFloat())
val stroke = StrokeDescription(path, 0, ViewConfiguration.getTapTimeout().toLong())
gestureBuilder.addStroke(stroke)
dispatchGesture(gestureBuilder.build(), null, null)
}
override fun onInterrupt() {
Log.d("AccessibilityService", "Service Interrupted")
}
override fun performTouch(x: Double, y: Double) {
simulateTap(x, y)
override fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean) {
val gestureBuilder = GestureDescription.Builder()
val path = Path()
path.moveTo(x.toFloat(), y.toFloat())
path.lineTo(x.toFloat()+1, y.toFloat())
val stroke = StrokeDescription(path, 0, ViewConfiguration.getTapTimeout().toLong(), isKeyDown)
gestureBuilder.addStroke(stroke)
dispatchGesture(gestureBuilder.build(), null, null)
}
}

View File

@@ -1,14 +1,16 @@
package de.jonasbark.accessibility
import android.graphics.Rect
object Observable {
var toService: Listener? = null
var fromService: Receiver? = null
}
interface Listener {
fun performTouch(x: Double, y: Double)
fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean)
}
interface Receiver {
fun onChange(packageName: String, windowWidth: Int, windowHeight: Int)
}
fun onChange(packageName: String, window: Rect)
}

View File

@@ -6,7 +6,7 @@ abstract class Accessibility {
void openPermissions();
void performTouch(double x, double y);
void performTouch(double x, double y, {bool isKeyDown = true, bool isKeyUp = false});
void controlMedia(MediaAction action);
}
@@ -15,10 +15,18 @@ enum MediaAction { playPause, next, volumeUp, volumeDown }
class WindowEvent {
final String packageName;
final int windowHeight;
final int windowWidth;
final int top;
final int bottom;
final int right;
final int left;
WindowEvent({required this.packageName, required this.windowHeight, required this.windowWidth});
WindowEvent({
required this.packageName,
required this.left,
required this.right,
required this.top,
required this.bottom,
});
}
@EventChannelApi()

View File

@@ -25,21 +25,29 @@ enum MediaAction {
class WindowEvent {
WindowEvent({
required this.packageName,
required this.windowHeight,
required this.windowWidth,
required this.top,
required this.bottom,
required this.right,
required this.left,
});
String packageName;
int windowHeight;
int top;
int windowWidth;
int bottom;
int right;
int left;
List<Object?> _toList() {
return <Object?>[
packageName,
windowHeight,
windowWidth,
top,
bottom,
right,
left,
];
}
@@ -50,8 +58,10 @@ class WindowEvent {
result as List<Object?>;
return WindowEvent(
packageName: result[0]! as String,
windowHeight: result[1]! as int,
windowWidth: result[2]! as int,
top: result[1]! as int,
bottom: result[2]! as int,
right: result[3]! as int,
left: result[4]! as int,
);
}
@@ -66,8 +76,10 @@ class WindowEvent {
}
return
packageName == other.packageName
&& windowHeight == other.windowHeight
&& windowWidth == other.windowWidth;
&& top == other.top
&& bottom == other.bottom
&& right == other.right
&& left == other.left;
}
@override
@@ -175,14 +187,14 @@ class Accessibility {
}
}
Future<void> performTouch(double x, double y) async {
Future<void> performTouch(double x, double y, {bool isKeyDown = true, bool isKeyUp = false, }) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.performTouch$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[x, y]);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[x, y, isKeyDown, isKeyUp]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package de.jonasbark.swift_play
package de.jonasbark.swiftcontrol
import io.flutter.embedding.android.FlutterActivity

View File

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

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>13.0</string>
</dict>
</plist>

View File

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

View File

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

View File

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

View File

@@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -45,5 +45,7 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>SwiftControl</string>
</dict>
</plist>

View File

@@ -20,8 +20,12 @@ class KeyPressSimulator {
return _platform.requestAccess(onlyOpenPrefPane: onlyOpenPrefPane);
}
Future<void> simulateMouseClick(Offset position) {
return _platform.simulateMouseClick(position);
Future<void> simulateMouseClickDown(Offset position) {
return _platform.simulateMouseClick(position, keyDown: true);
}
Future<void> simulateMouseClickUp(Offset position) {
return _platform.simulateMouseClick(position, keyDown: false);
}
/// Simulate key down.

View File

@@ -62,6 +62,7 @@ public class KeypressSimulatorMacosPlugin: NSObject, FlutterPlugin {
let x: Double = args["x"] as! Double
let y: Double = args["y"] as! Double
let keyDown: Bool = args["keyDown"] as! Bool
let point = CGPoint(x: x, y: y)
@@ -72,19 +73,21 @@ public class KeypressSimulatorMacosPlugin: NSObject, FlutterPlugin {
mouseButton: .left)
move?.post(tap: .cghidEventTap)*/
// Mouse down
let mouseDown = CGEvent(mouseEventSource: nil,
mouseType: .leftMouseDown,
mouseCursorPosition: point,
mouseButton: .left)
mouseDown?.post(tap: .cghidEventTap)
// Mouse up
let mouseUp = CGEvent(mouseEventSource: nil,
mouseType: .leftMouseUp,
mouseCursorPosition: point,
mouseButton: .left)
mouseUp?.post(tap: .cghidEventTap)
if (keyDown) {
// Mouse down
let mouseDown = CGEvent(mouseEventSource: nil,
mouseType: .leftMouseDown,
mouseCursorPosition: point,
mouseButton: .left)
mouseDown?.post(tap: .cghidEventTap)
} else {
// Mouse up
let mouseUp = CGEvent(mouseEventSource: nil,
mouseType: .leftMouseUp,
mouseCursorPosition: point,
mouseButton: .left)
mouseUp?.post(tap: .cghidEventTap)
}
result(true)
}

View File

@@ -53,10 +53,11 @@ class MethodChannelKeyPressSimulator extends KeyPressSimulatorPlatform {
}
@override
Future<void> simulateMouseClick(Offset position) async {
final Map<String, double> arguments = {
Future<void> simulateMouseClick(Offset position, {required bool keyDown}) async {
final Map<String, Object?> arguments = {
'x': position.dx,
'y': position.dy,
'keyDown': keyDown,
};
await methodChannel.invokeMethod('simulateMouseClick', arguments);
}

View File

@@ -41,7 +41,7 @@ abstract class KeyPressSimulatorPlatform extends PlatformInterface {
throw UnimplementedError('simulateKeyPress() has not been implemented.');
}
Future<void> simulateMouseClick(Offset position) {
Future<void> simulateMouseClick(Offset position, {required bool keyDown}) {
throw UnimplementedError('simulateKeyPress() has not been implemented.');
}
}

View File

@@ -2,6 +2,9 @@
// This must be included before many other Windows headers.
#include <windows.h>
#include <psapi.h>
#include <string.h>
#include <flutter_windows.h>
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
@@ -16,6 +19,16 @@ using flutter::EncodableValue;
namespace keypress_simulator_windows {
// Forward declarations
struct FindWindowData {
std::string targetProcessName;
std::string targetWindowTitle;
HWND foundWindow;
};
BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam);
HWND FindTargetWindow(const std::string& processName, const std::string& windowTitle);
// static
void KeypressSimulatorWindowsPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarWindows* registrar) {
@@ -54,33 +67,43 @@ void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
modifiers.push_back(key_modifier);
}
INPUT input[6];
// List of compatible training apps to look for
std::vector<std::string> compatibleApps = {
"MyWhooshHD.exe",
"indieVelo.exe",
"biketerra.exe"
};
for (int32_t i = 0; i < modifiers.size(); i++) {
if (modifiers[i].compare("shiftModifier") == 0) {
input[i].ki.wVk = VK_SHIFT;
} else if (modifiers[i].compare("controlModifier") == 0) {
input[i].ki.wVk = VK_CONTROL;
} else if (modifiers[i].compare("altModifier") == 0) {
input[i].ki.wVk = VK_MENU;
} else if (modifiers[i].compare("metaModifier") == 0) {
input[i].ki.wVk = VK_LWIN;
// Try to find and focus a compatible app
HWND targetWindow = NULL;
for (const std::string& processName : compatibleApps) {
targetWindow = FindTargetWindow(processName, "");
if (targetWindow != NULL) {
// Only focus the window if it's not already in the foreground
if (GetForegroundWindow() != targetWindow) {
SetForegroundWindow(targetWindow);
Sleep(50); // Brief delay to ensure window is focused
}
break;
}
input[i].ki.dwFlags = keyDown ? 0 : KEYEVENTF_KEYUP;
input[i].type = INPUT_KEYBOARD;
}
/*int keyIndex = static_cast<int>(modifiers.size());
input[keyIndex].ki.wVk = static_cast<WORD>(keyCode);
input[keyIndex].ki.dwFlags = keyDown ? 0 : KEYEVENTF_KEYUP;
input[keyIndex].type = INPUT_KEYBOARD;*/
WORD sc = (WORD)MapVirtualKey(keyCode, MAPVK_VK_TO_VSC);
// Send key sequence to system
//SendInput(static_cast<UINT>(std::size(input)), input, sizeof(INPUT));
INPUT in = {0};
in.type = INPUT_KEYBOARD;
in.ki.wVk = 0; // when using SCANCODE, set VK=0
in.ki.wScan = sc;
in.ki.dwFlags = KEYEVENTF_SCANCODE | (keyDown ? 0 : KEYEVENTF_KEYUP);
if (keyCode == VK_LEFT || keyCode == VK_RIGHT || keyCode == VK_UP || keyCode == VK_DOWN ||
keyCode == VK_INSERT || keyCode == VK_DELETE || keyCode == VK_HOME || keyCode == VK_END ||
keyCode == VK_PRIOR || keyCode == VK_NEXT) {
in.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
}
SendInput(1, &in, sizeof(INPUT));
BYTE byteValue = static_cast<BYTE>(keyCode);
keybd_event(byteValue, 0x45, keyDown ? 0 : KEYEVENTF_KEYUP, 0);
/*BYTE byteValue = static_cast<BYTE>(keyCode);
keybd_event(byteValue, 0x45, keyDown ? 0 : KEYEVENTF_KEYUP, 0);*/
result->Success(flutter::EncodableValue(true));
}
@@ -93,6 +116,7 @@ void KeypressSimulatorWindowsPlugin::SimulateMouseClick(
double x = 0;
double y = 0;
bool keyDown = std::get<bool>(args.at(EncodableValue("keyDown")));
auto it_x = args.find(EncodableValue("x"));
if (it_x != args.end() && std::holds_alternative<double>(it_x->second)) {
x = std::get<double>(it_x->second);
@@ -103,24 +127,97 @@ void KeypressSimulatorWindowsPlugin::SimulateMouseClick(
y = std::get<double>(it_y->second);
}
// Get the monitor containing the target point and its DPI
const POINT target_point = {static_cast<LONG>(x), static_cast<LONG>(y)};
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
// Scale the coordinates according to the DPI scaling
int scaled_x = static_cast<int>(x * scale_factor);
int scaled_y = static_cast<int>(y * scale_factor);
// Move the mouse to the specified coordinates
SetCursorPos(static_cast<int>(x), static_cast<int>(y));
SetCursorPos(scaled_x, scaled_y);
// Prepare input for mouse down and up
INPUT input = {0};
input.type = INPUT_MOUSE;
// Mouse left button down
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, &input, sizeof(INPUT));
if (keyDown) {
// Mouse left button down
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, &input, sizeof(INPUT));
// Mouse left button up
input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(1, &input, sizeof(INPUT));
} else {
// Mouse left button up
input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(1, &input, sizeof(INPUT));
}
result->Success(flutter::EncodableValue(true));
}
BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) {
FindWindowData* data = reinterpret_cast<FindWindowData*>(lParam);
// Check if window is visible and not minimized
if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) {
return TRUE; // Continue enumeration
}
// Get window title
char windowTitle[256];
GetWindowTextA(hwnd, windowTitle, sizeof(windowTitle));
// Get process name
DWORD processId;
GetWindowThreadProcessId(hwnd, &processId);
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
char processName[MAX_PATH];
if (hProcess) {
DWORD size = sizeof(processName);
if (QueryFullProcessImageNameA(hProcess, 0, processName, &size)) {
// Extract just the filename from the full path
char* filename = strrchr(processName, '\\');
if (filename) {
filename++; // Skip the backslash
} else {
filename = processName;
}
// Check if this matches our target
if (!data->targetProcessName.empty() &&
_stricmp(filename, data->targetProcessName.c_str()) == 0) {
data->foundWindow = hwnd;
return FALSE; // Stop enumeration
}
}
CloseHandle(hProcess);
}
// Check window title if process name didn't match
if (!data->targetWindowTitle.empty() &&
_stricmp(windowTitle, data->targetWindowTitle.c_str()) == 0) {
data->foundWindow = hwnd;
return FALSE; // Stop enumeration
}
return TRUE; // Continue enumeration
}
HWND FindTargetWindow(const std::string& processName, const std::string& windowTitle) {
FindWindowData data;
data.targetProcessName = processName;
data.targetWindowTitle = windowTitle;
data.foundWindow = NULL;
EnumWindows(EnumWindowsCallback, reinterpret_cast<LPARAM>(&data));
return data.foundWindow;
}
void KeypressSimulatorWindowsPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {

View File

@@ -30,6 +30,8 @@ class KeypressSimulatorWindowsPlugin : public flutter::Plugin {
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
// Called when a method is called on this plugin's channel from Dart.
void HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,

View File

@@ -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();
@@ -9,7 +15,7 @@ class BleUuid {
}
class Constants {
static const ZWIFT_MANUFACTURER_ID = 2378; // Zwift, Inc
static const ZWIFT_MANUFACTURER_ID = 2378; // Zwift, Inc => 0x094A
// Zwift Play = RC1
static const RC1_LEFT_SIDE = 0x03;
@@ -22,17 +28,57 @@ class Constants {
// Zwift Click = BC1
static const BC1 = 0x09;
// Zwift Click v2 Right (unconfirmed)
static const CLICK_V2_RIGHT_SIDE = 0x0A;
// Zwift Click v2 Right (unconfirmed)
static const CLICK_V2_LEFT_SIDE = 0x0B;
static final RIDE_ON = Uint8List.fromList([0x52, 0x69, 0x64, 0x65, 0x4f, 0x6e]);
static final VIBRATE_PATTERN = Uint8List.fromList([0x12, 0x12, 0x08, 0x0A, 0x06, 0x08, 0x02, 0x10, 0x00, 0x18]);
// these don't actually seem to matter, its just the header has to be 7 bytes RIDEON + 2
static final REQUEST_START = Uint8List.fromList([0, 9]); //byteArrayOf(1, 2)
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;
@@ -45,6 +91,8 @@ class Constants {
enum DeviceType {
click,
clickV2Right,
clickV2Left,
playLeft,
playRight,
rideRight,
@@ -60,6 +108,10 @@ enum DeviceType {
switch (data) {
case Constants.BC1:
return DeviceType.click;
case Constants.CLICK_V2_RIGHT_SIDE:
return DeviceType.clickV2Right;
case Constants.CLICK_V2_LEFT_SIDE:
return DeviceType.clickV2Left;
case Constants.RC1_LEFT_SIDE:
return DeviceType.playLeft;
case Constants.RC1_RIGHT_SIDE:

View File

@@ -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;
@@ -21,7 +24,7 @@ class Connection {
final StreamController<BaseNotification> _actionStreams = StreamController<BaseNotification>.broadcast();
Stream<BaseNotification> get actionStream => _actionStreams.stream;
final Map<BaseDevice, StreamSubscription<BleConnectionUpdate>> _connectionSubscriptions = {};
final Map<BaseDevice, StreamSubscription<bool>> _connectionSubscriptions = {};
final StreamController<BaseDevice> _connectionStreams = StreamController<BaseDevice>.broadcast();
Stream<BaseDevice> get connectionStream => _connectionStreams.stream;
@@ -34,11 +37,15 @@ class Connection {
if (_lastScanResult.none((e) => e.deviceId == result.deviceId)) {
_lastScanResult.add(result);
final scanResult = BaseDevice.fromScanResult(result);
_actionStreams.add(
LogNotification('Found new device: ${result.name ?? scanResult?.runtimeType ?? result.deviceId}'),
);
if (scanResult != null) {
_actionStreams.add(LogNotification('Found new device: ${scanResult.runtimeType}'));
_addDevices([scanResult]);
} else {
final manufacturerData = result.manufacturerDataList;
final data =
manufacturerData.firstOrNullWhere((e) => e.companyId == Constants.ZWIFT_MANUFACTURER_ID)?.payload;
_actionStreams.add(LogNotification('Found unknown device with identifier: ${data?.firstOrNull}'));
}
}
};
@@ -47,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);
@@ -56,6 +64,7 @@ class Connection {
Future<void> performScanning() async {
isScanning.value = true;
_actionStreams.add(LogNotification('Scanning for devices...'));
// does not work on web, may not work on Windows
if (!kIsWeb && !Platform.isWindows) {
@@ -89,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()));
});
@@ -126,11 +135,21 @@ class Connection {
final actionSubscription = bleDevice.actionStream.listen((data) {
_actionStreams.add(data);
});
final connectionStateSubscription = UniversalBle.connectionStream(bleDevice.device.deviceId).listen((
state,
) async {
bleDevice.isConnected = state.isConnected;
final connectionStateSubscription = UniversalBle.connectionStream(bleDevice.device.deviceId).listen((state) {
bleDevice.isConnected = state;
_connectionStreams.add(bleDevice);
if (!bleDevice.isConnected) {
devices.remove(bleDevice);
_streamSubscriptions[bleDevice]?.cancel();
_streamSubscriptions.remove(bleDevice);
_connectionSubscriptions[bleDevice]?.cancel();
_connectionSubscriptions.remove(bleDevice);
_lastScanResult.clear();
// try reconnect
if (!isScanning.value) {
performScanning();
}
}
});
_connectionSubscriptions[bleDevice] = connectionStateSubscription;
@@ -148,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) {

View File

@@ -1,43 +1,62 @@
import 'dart:async';
import 'dart:io';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:swift_control/bluetooth/ble.dart';
import 'package:swift_control/bluetooth/devices/zwift_click.dart';
import 'package:swift_control/bluetooth/devices/zwift_clickv2.dart';
import 'package:swift_control/bluetooth/devices/zwift_play.dart';
import 'package:swift_control/bluetooth/devices/zwift_ride.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/desktop.dart';
import 'package:swift_control/utils/crypto/local_key_provider.dart';
import 'package:swift_control/utils/crypto/zap_crypto.dart';
import 'package:swift_control/utils/single_line_exception.dart';
import 'package:universal_ble/universal_ble.dart';
import '../../utils/crypto/encryption_utils.dart';
import '../../utils/keymap/buttons.dart';
import '../messages/notification.dart';
abstract class BaseDevice {
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;
BleCharacteristic? syncRxCharacteristic;
Timer? _longPressTimer;
Set<ZwiftButton> _previouslyPressedButtons = <ZwiftButton>{};
List<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_CLICK;
String get customServiceId => BleUuid.ZWIFT_CUSTOM_SERVICE_UUID;
static BaseDevice? fromScanResult(BleDevice scanResult) {
// Use the name first as the "System Devices" and Web (android sometimes Windows) don't have manufacturer data
final device = switch (scanResult.name) {
//'Zwift Ride' => ZwiftRide(scanResult), special case for Zwift Ride: we must only connect to the left controller
// https://www.makinolo.com/blog/2024/07/26/zwift-ride-protocol/
'Zwift Play' => ZwiftPlay(scanResult),
'Zwift Click' => ZwiftClick(scanResult),
_ => null,
};
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;
@@ -55,8 +74,10 @@ abstract class BaseDevice {
DeviceType.click => ZwiftClick(scanResult),
DeviceType.playRight => ZwiftPlay(scanResult),
DeviceType.playLeft => ZwiftPlay(scanResult),
//DeviceType.rideRight => ZwiftRide(scanResult), // see comment above
DeviceType.rideLeft => ZwiftRide(scanResult),
//DeviceType.rideRight => ZwiftRide(scanResult), // see comment above
DeviceType.clickV2Left => ZwiftClickV2(scanResult),
//DeviceType.clickV2Right => ZwiftClickV2(scanResult), // see comment above
_ => null,
};
}
@@ -78,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 {
@@ -88,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);
@@ -105,13 +125,29 @@ 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,
);
final syncTxCharacteristic = customService.characteristics.firstOrNullWhere(
(characteristic) => characteristic.uuid == BleUuid.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID,
);
final syncRxCharacteristic = customService.characteristics.firstOrNullWhere(
syncRxCharacteristic = customService.characteristics.firstOrNullWhere(
(characteristic) => characteristic.uuid == BleUuid.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID,
);
@@ -119,52 +155,42 @@ abstract class BaseDevice {
throw Exception('Characteristics not found');
}
await UniversalBle.setNotifiable(
device.deviceId,
customService.uuid,
asyncCharacteristic.uuid,
BleInputProperty.notification,
);
await UniversalBle.setNotifiable(
device.deviceId,
customService.uuid,
syncTxCharacteristic.uuid,
BleInputProperty.indication,
);
await UniversalBle.subscribeNotifications(device.deviceId, customService.uuid, asyncCharacteristic.uuid);
await UniversalBle.subscribeIndications(device.deviceId, customService.uuid, syncTxCharacteristic.uuid);
await _setupHandshake(syncRxCharacteristic);
await setupHandshake();
}
Future<void> _setupHandshake(BleCharacteristic syncRxCharacteristic) async {
Future<void> setupHandshake() async {
if (supportsEncryption) {
await UniversalBle.writeValue(
await UniversalBle.write(
device.deviceId,
customServiceId,
syncRxCharacteristic.uuid,
syncRxCharacteristic!.uuid,
Uint8List.fromList([
...Constants.RIDE_ON,
...Constants.REQUEST_START,
...zapEncryption.localKeyProvider.getPublicKeyBytes(),
]),
BleOutputProperty.withoutResponse,
withoutResponse: true,
);
} else {
await UniversalBle.writeValue(
await UniversalBle.write(
device.deviceId,
customServiceId,
syncRxCharacteristic.uuid,
syncRxCharacteristic!.uuid,
Constants.RIDE_ON,
BleOutputProperty.withoutResponse,
withoutResponse: true,
);
}
}
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;
}
@@ -172,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");
@@ -190,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;
@@ -205,7 +229,11 @@ abstract class BaseDevice {
final payload = bytes.sublist(4);
if (zapEncryption.encryptionKeyBytes == null) {
actionStreamInternal.add(LogNotification('Encryption not initialized, yet.'));
actionStreamInternal.add(
LogNotification(
'Encryption not initialized, yet. You may need to update the firmware of your device with the Zwift Companion app.',
),
);
return;
}
@@ -229,13 +257,104 @@ abstract class BaseDevice {
break;
case Constants.CLICK_NOTIFICATION_MESSAGE_TYPE:
case Constants.PLAY_NOTIFICATION_MESSAGE_TYPE:
case Constants.RIDE_NOTIFICATION_MESSAGE_TYPE: // untested
processClickNotification(message).then((_) {}).catchError((e) {
actionStreamInternal.add(LogNotification(e.toString()));
});
case Constants.RIDE_NOTIFICATION_MESSAGE_TYPE:
processClickNotification(message)
.then((buttonsClicked) async {
return handleButtonsClicked(buttonsClicked);
})
.catchError((e) {
actionStreamInternal.add(LogNotification(e.toString()));
});
break;
}
}
Future<void> processClickNotification(Uint8List message);
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)) &&
settings.getVibrationEnabled()) {
await _vibrate();
}
for (final action in buttonsClicked) {
// For repeated actions, don't trigger key down/up events (useful for long press)
final isKeyDown = !repeated;
actionStreamInternal.add(
LogNotification(await actionHandler.performAction(action, isKeyDown: isKeyDown, isKeyUp: false)),
);
}
}
Future<void> _performRelease(List<ZwiftButton> buttonsReleased) async {
for (final action in buttonsReleased) {
actionStreamInternal.add(
LogNotification(await actionHandler.performAction(action, isKeyDown: false, isKeyUp: true)),
);
}
}
Future<void> _vibrate() async {
final vibrateCommand = Uint8List.fromList([...Constants.VIBRATE_PATTERN, 0x20]);
await UniversalBle.write(
device.deviceId,
customServiceId,
syncRxCharacteristic!.uuid,
supportsEncryption ? zapEncryption.encrypt(vibrateCommand) : vibrateCommand,
withoutResponse: true,
);
}
Future<void> disconnect() async {
_isInited = false;
_longPressTimer?.cancel();
_previouslyPressedButtons.clear();
// Release any held keys in long press mode
if (actionHandler is DesktopActions) {
await (actionHandler as DesktopActions).releaseAllHeldKeys();
}
await UniversalBle.disconnect(device.deviceId);
isConnected = false;
}
}

View File

@@ -1,29 +1,26 @@
import 'package:flutter/foundation.dart';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/bluetooth/messages/notification.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import '../messages/click_notification.dart';
class ZwiftClick extends BaseDevice {
ZwiftClick(super.scanResult);
ZwiftClick(super.scanResult) : super(availableButtons: [ZwiftButton.shiftUpRight, ZwiftButton.shiftDownLeft]);
ClickNotification? _lastClickNotification;
@override
Future<void> processClickNotification(Uint8List message) async {
Future<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
final ClickNotification clickNotification = ClickNotification(message);
if (_lastClickNotification == null || _lastClickNotification != clickNotification) {
_lastClickNotification = clickNotification;
if (clickNotification.buttonsClicked.isNotEmpty) {
actionStreamInternal.add(clickNotification);
}
final buttons = clickNotification.buttonsClicked;
for (final action in buttons) {
actionStreamInternal.add(LogNotification(await actionHandler.performAction(action)));
}
return clickNotification.buttonsClicked;
} else {
return null;
}
}
}

View File

@@ -0,0 +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 => 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,
]),
);*/
}
}

View File

@@ -1,13 +1,32 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/bluetooth/messages/play_notification.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import '../../main.dart';
import '../ble.dart';
import '../messages/notification.dart';
class ZwiftPlay extends BaseDevice {
ZwiftPlay(super.scanResult);
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;
@@ -15,7 +34,7 @@ class ZwiftPlay extends BaseDevice {
List<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_PLAY;
@override
Future<void> processClickNotification(Uint8List message) async {
Future<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
final PlayNotification clickNotification = PlayNotification(message);
if (_lastControllerNotification == null || _lastControllerNotification != clickNotification) {
_lastControllerNotification = clickNotification;
@@ -24,11 +43,9 @@ class ZwiftPlay extends BaseDevice {
actionStreamInternal.add(clickNotification);
}
final buttons = clickNotification.buttonsClicked;
for (final action in buttons) {
actionStreamInternal.add(LogNotification(await actionHandler.performAction(action)));
}
return clickNotification.buttonsClicked;
} else {
return null;
}
}
}

View File

@@ -1,14 +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/main.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;
@@ -19,19 +47,197 @@ class ZwiftRide extends BaseDevice {
RideNotification? _lastControllerNotification;
@override
Future<void> processClickNotification(Uint8List message) async {
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);
if (_lastControllerNotification == null || _lastControllerNotification != clickNotification) {
_lastControllerNotification = clickNotification;
if (clickNotification.buttonsClicked.isNotEmpty) {
actionStreamInternal.add(clickNotification);
}
final buttons = clickNotification.buttonsClicked;
for (final action in buttons) {
actionStreamInternal.add(LogNotification(await actionHandler.performAction(action)));
}
return clickNotification.buttonsClicked;
} else {
return null;
}
}
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,
);
}
}

View File

@@ -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';
@@ -12,14 +13,14 @@ class ClickNotification extends BaseNotification {
ClickNotification(Uint8List message) {
final status = ClickKeyPadStatus.fromBuffer(message);
buttonsClicked = [
if (status.buttonPlus == PlayButtonStatus.ON) ZwiftButton.shiftUpLeft,
if (status.buttonMinus == PlayButtonStatus.ON) ZwiftButton.shiftDownRight,
if (status.buttonPlus == PlayButtonStatus.ON) ZwiftButton.shiftUpRight,
if (status.buttonMinus == PlayButtonStatus.ON) ZwiftButton.shiftDownLeft,
];
}
@override
String toString() {
return 'Buttons: ${buttonsClicked.joinToString(transform: (e) => e.name)}';
return 'Buttons: ${buttonsClicked.joinToString(transform: (e) => e.name.splitByUpperCase())}';
}
@override

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View 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');

File diff suppressed because it is too large Load Diff

View 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';

View 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');

View 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');

View 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==');

View 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';

View File

@@ -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();
@@ -45,7 +45,7 @@ class SwiftPlayApp extends StatelessWidget {
title: 'SwiftControl',
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: ThemeMode.system,
themeMode: ThemeMode.light,
home: const RequirementsPage(),
);
}

98
lib/pages/changelog.dart Normal file
View 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,
),
),
],
),
),
),
],
),
),
);
},
),
);
}
}

View File

@@ -4,9 +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';
@@ -51,102 +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: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 10,
children: [
Text(
'Devices:\n${connection.devices.joinToString(separator: '\n', transform: (it) {
return "${it.device.name ?? it.runtimeType}: ${it.isConnected ? 'Connected' : 'Not connected'}${it.batteryLevel != null ? ' - Battery Level: ${it.batteryLevel}%' : ''}";
})}',
),
Divider(color: Theme.of(context).colorScheme.primary, height: 30),
if (!kIsWeb)
Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
DropdownMenu<SupportedApp>(
controller: controller,
dropdownMenuEntries:
SupportedApp.supportedApps
.map(
(app) => DropdownMenuEntry<SupportedApp>(
value: app,
label: app.name,
trailingIcon: IconButton(
icon: Icon(Icons.info_outline),
onPressed: () {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: Text(app.name),
content: SelectableText(app.keymap.toString()),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
),
);
},
),
),
)
.toList(),
label: Text('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(
'Use Custom keymap if you experience any issues (e.g. wrong keyboard output)',
),
),
);
}
},
initialSelection: actionHandler.supportedApp,
hintText: 'Select your Keymap',
),
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 (actionHandler.supportedApp is CustomApp)
ElevatedButton(
onPressed: () async {
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 (connection.devices.any((device) => (device is ZwiftClickV2) && device.isConnected))
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
],
),
Expanded(child: LogViewer()),
],
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,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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',
),
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'),
),
],
),
LogViewer(),
],
),
),
),
),
Positioned.fill(child: Testbed()),
],
),
),
);

View File

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

View File

@@ -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';
@@ -30,9 +31,13 @@ class _ScanWidgetState extends State<ScanWidget> {
WidgetsBinding.instance.addPostFrameCallback((_) {
// must be called from a button
if (!kIsWeb) {
Future.delayed(Duration(seconds: 1)).then((_) {
connection.performScanning();
});
Future.delayed(Duration(seconds: 1))
.then((_) {
return connection.performScanning();
})
.catchError((e) {
print(e);
});
}
});
}
@@ -41,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,
@@ -55,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(),
],
);

View File

@@ -6,7 +6,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:keypress_simulator/keypress_simulator.dart';
import 'package:swift_control/main.dart';
import 'package: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';
@@ -52,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);
}
@@ -61,11 +71,17 @@ 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);
}
_actionSubscription = connection.actionStream.listen((data) {
_actionSubscription = connection.actionStream.listen((data) async {
if (!mounted) {
return;
}
@@ -81,88 +97,191 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
if (_pressedButton != null) {
if (actionHandler.supportedApp!.keymap.getKeyPair(_pressedButton!) == null) {
final KeyPair keyPair;
actionHandler.supportedApp!.keymap.keyPairs.add(
KeyPair(
touchPosition: context.size!.center(Offset.zero),
keyPair = KeyPair(
touchPosition: context.size!
.center(Offset.zero)
.translate(actionHandler.supportedApp!.keymap.keyPairs.length * 40, 0),
buttons: [_pressedButton!],
physicalKey: null,
logicalKey: null,
isLongPress: false,
),
);
setState(() {});
// open menu
if (Platform.isMacOS || Platform.isWindows) {
await Future.delayed(Duration(milliseconds: 300));
await keyPressSimulator.simulateMouseClickDown(keyPair.touchPosition);
await keyPressSimulator.simulateMouseClickUp(keyPair.touchPosition);
}
}
}
});
}
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: const Text('Set Keyboard shortcut'),
onTap: () async {
await showDialog<void>(
context: context,
builder:
(c) =>
HotKeyListenerDialog(customApp: actionHandler.supportedApp! as CustomApp, keyPair: keyPair),
);
setState(() {});
},
),
if (keyPair.physicalKey != 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: const Text('Use as touch button'),
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(() {});
},
),
],
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),
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)'),
),
),
onDragUpdate: (details) {
print('Dragging: ${details.localPosition}');
},
childWhenDragging: const SizedBox.shrink(),
onDraggableCanceled: (_, offset) {
print('Drag canceled: ${offset}');
setState(() => onPositionChanged(offset));
},
child: _TouchDot(color: color, label: label, keyPair: keyPair),
),
],
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'),
),
),
),
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
@@ -182,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();
@@ -199,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,
@@ -224,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),
),
],
),
),
@@ -242,46 +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: Colors.black, width: 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.add,
),
),
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()}'),
Text(label, style: TextStyle(color: Colors.black, fontSize: 12)),
if (keyPair.physicalKey != null)
Text(switch (keyPair.physicalKey) {
PhysicalKeyboardKey.mediaPlayPause => 'Media: Play/Pause',
PhysicalKeyboardKey.mediaStop => 'Media: Stop',
PhysicalKeyboardKey.mediaTrackPrevious => 'Media: Previous',
PhysicalKeyboardKey.mediaTrackNext => 'Media: Next',
PhysicalKeyboardKey.audioVolumeUp => 'Media: Volume Up',
PhysicalKeyboardKey.audioVolumeDown => 'Media: Volume Down',
_ => keyPair.logicalKey?.keyLabel ?? 'Unknown',
}, style: TextStyle(color: Colors.grey, fontSize: 12)),
if (keyPair.isLongPress) Text('using long press'),
],
],
);
}

View File

@@ -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';
@@ -22,9 +23,9 @@ class AndroidActions extends BaseActions {
}
@override
Future<String> performAction(ZwiftButton button) async {
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) {
@@ -41,7 +42,10 @@ class AndroidActions extends BaseActions {
}
}
final point = supportedApp!.resolveTouchPosition(action: button, windowInfo: windowInfo);
accessibilityHandler.performTouch(point.dx, point.dy);
return "Touch performed at: ${point.dx.toInt()}, ${point.dy.toInt()}";
if (point != Offset.zero) {
accessibilityHandler.performTouch(point.dx, point.dy, isKeyDown: isKeyDown, isKeyUp: isKeyUp);
return "Touch performed at: ${point.dx.toInt()}, ${point.dy.toInt()}";
}
return "No touch performed";
}
}

View File

@@ -9,12 +9,12 @@ abstract class BaseActions {
this.supportedApp = supportedApp;
}
Future<String> performAction(ZwiftButton action);
Future<String> performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false});
}
class StubActions extends BaseActions {
@override
Future<String> performAction(ZwiftButton action) {
Future<String> performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false}) {
return Future.value(action.name);
}
}

View File

@@ -1,27 +1,77 @@
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
final Set<ZwiftButton> _heldKeys = <ZwiftButton>{};
@override
Future<String> performAction(ZwiftButton action) async {
Future<String> performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false}) async {
if (supportedApp == null) {
return ('Supported app is not set');
}
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()}');
}
if (keyPair.physicalKey != null) {
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey);
await keyPressSimulator.simulateKeyUp(keyPair.physicalKey);
return 'Key pressed: ${keyPair.logicalKey?.keyLabel}';
// Handle long press mode
if (keyPair.isLongPress) {
if (isKeyDown && !isKeyUp) {
// Key press: start long press
if (!_heldKeys.contains(action)) {
_heldKeys.add(action);
if (keyPair.physicalKey != null) {
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey);
return 'Long press started: ${keyPair.logicalKey?.keyLabel}';
} else {
final point = supportedApp!.resolveTouchPosition(action: action, windowInfo: null);
await keyPressSimulator.simulateMouseClickDown(point);
return 'Long Mouse click started at: $point';
}
}
} else if (isKeyUp && !isKeyDown) {
// Key release: end long press
if (_heldKeys.contains(action)) {
_heldKeys.remove(action);
if (keyPair.physicalKey != null) {
await keyPressSimulator.simulateKeyUp(keyPair.physicalKey);
return 'Long press ended: ${keyPair.logicalKey?.keyLabel}';
} else {
final point = supportedApp!.resolveTouchPosition(action: action, windowInfo: null);
await keyPressSimulator.simulateMouseClickUp(point);
return 'Long Mouse click ended at: $point';
}
}
}
// Ignore other combinations in long press mode
return 'Long press active';
} else {
final point = supportedApp!.resolveTouchPosition(action: action, windowInfo: null);
await keyPressSimulator.simulateMouseClick(point);
return 'Mouse clicked at: $point';
// Handle regular key press mode (existing behavior)
if (keyPair.physicalKey != null) {
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey);
await keyPressSimulator.simulateKeyUp(keyPair.physicalKey);
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.dx} ${point.dy}';
}
}
}
// Release all held keys (useful for cleanup)
Future<void> releaseAllHeldKeys() async {
for (final action in _heldKeys.toList()) {
final keyPair = supportedApp?.keymap.getKeyPair(action);
if (keyPair?.physicalKey != null) {
await keyPressSimulator.simulateKeyUp(keyPair!.physicalKey);
}
}
_heldKeys.clear();
}
}

79
lib/utils/changelog.dart Normal file
View 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');
}
}

View File

@@ -0,0 +1,49 @@
import 'package:accessibility/accessibility.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
import '../buttons.dart';
import '../keymap.dart';
class Biketerra extends SupportedApp {
Biketerra()
: super(
name: 'Biketerra',
packageName: "biketerra",
keymap: Keymap(
keyPairs: [
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(),
physicalKey: PhysicalKeyboardKey.keyS,
logicalKey: LogicalKeyboardKey.keyS,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(),
physicalKey: PhysicalKeyboardKey.keyW,
logicalKey: LogicalKeyboardKey.keyW,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(),
physicalKey: PhysicalKeyboardKey.arrowRight,
logicalKey: LogicalKeyboardKey.arrowRight,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateLeft).toList(),
physicalKey: PhysicalKeyboardKey.arrowLeft,
logicalKey: LogicalKeyboardKey.arrowLeft,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(),
physicalKey: PhysicalKeyboardKey.keyU,
logicalKey: LogicalKeyboardKey.keyU,
),
],
),
);
}
extension WindowSize on WindowEvent {
int get width => right - left;
int get height => bottom - top;
}

View File

@@ -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;
}
@@ -42,14 +42,26 @@ class CustomApp extends SupportedApp {
ZwiftButton zwiftButton, {
required PhysicalKeyboardKey physicalKey,
required LogicalKeyboardKey? logicalKey,
bool isLongPress = false,
Offset? touchPosition,
}) {
// set the key for the zwift button
final keyPair = keymap.getKeyPair(zwiftButton);
if (keyPair != null) {
keyPair.physicalKey = physicalKey;
keyPair.logicalKey = logicalKey;
keyPair.isLongPress = isLongPress;
keyPair.touchPosition = touchPosition ?? Offset.zero;
} else {
keymap.keyPairs.add(KeyPair(buttons: [zwiftButton], physicalKey: physicalKey, logicalKey: logicalKey));
keymap.keyPairs.add(
KeyPair(
buttons: [zwiftButton],
physicalKey: physicalKey,
logicalKey: logicalKey,
isLongPress: isLongPress,
touchPosition: touchPosition ?? Offset.zero,
),
);
}
}
}

View File

@@ -1,6 +1,7 @@
import 'package:accessibility/accessibility.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
import '../../single_line_exception.dart';
@@ -16,13 +17,13 @@ class MyWhoosh extends SupportedApp {
keyPairs: [
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(),
physicalKey: PhysicalKeyboardKey.keyK,
logicalKey: LogicalKeyboardKey.keyK,
physicalKey: PhysicalKeyboardKey.keyI,
logicalKey: LogicalKeyboardKey.keyI,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(),
physicalKey: PhysicalKeyboardKey.keyI,
logicalKey: LogicalKeyboardKey.keyI,
physicalKey: PhysicalKeyboardKey.keyK,
logicalKey: LogicalKeyboardKey.keyK,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(),
@@ -52,10 +53,44 @@ class MyWhoosh extends SupportedApp {
if (windowInfo == null) {
throw SingleLineException("Window size not known - open $this first");
}
// just my personal preference
switch (action) {
case ZwiftButton.y:
accessibilityHandler.controlMedia(MediaAction.volumeUp);
return Offset.zero;
case ZwiftButton.b:
accessibilityHandler.controlMedia(MediaAction.volumeDown);
return Offset.zero;
case ZwiftButton.a:
accessibilityHandler.controlMedia(MediaAction.next);
return Offset.zero;
case ZwiftButton.z:
accessibilityHandler.controlMedia(MediaAction.playPause);
return Offset.zero;
default:
break;
}
return switch (action.action) {
InGameAction.shiftUp => Offset(windowInfo.windowWidth * 0.98, windowInfo.windowHeight * 0.94),
InGameAction.shiftDown => Offset(windowInfo.windowWidth * 0.80, windowInfo.windowHeight * 0.94),
InGameAction.shiftUp => Offset(
windowInfo.right - windowInfo.width * 0.02,
windowInfo.bottom - windowInfo.height * 0.06,
),
InGameAction.shiftDown => Offset(
windowInfo.right - windowInfo.width * 0.20,
windowInfo.bottom - windowInfo.height * 0.06,
),
InGameAction.navigateRight => Offset(
windowInfo.right - windowInfo.width * 0.02,
windowInfo.bottom - windowInfo.height * 0.20,
),
_ => throw SingleLineException("Unsupported action for MyWhoosh: $action"),
};
}
}
extension WindowSize on WindowEvent {
int get width => right - left;
int get height => bottom - top;
}

View File

@@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:accessibility/accessibility.dart';
import 'package:swift_control/utils/keymap/apps/biketerra.dart';
import 'package:swift_control/utils/keymap/apps/training_peaks.dart';
import '../../single_line_exception.dart';
@@ -27,7 +28,7 @@ abstract class SupportedApp {
const SupportedApp({required this.name, required this.packageName, required this.keymap});
static final List<SupportedApp> supportedApps = [MyWhoosh(), TrainingPeaks(), CustomApp()];
static final List<SupportedApp> supportedApps = [MyWhoosh(), TrainingPeaks(), Biketerra(), CustomApp()];
@override
String toString() {

View File

@@ -1,6 +1,7 @@
import 'package:accessibility/accessibility.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/utils/keymap/apps/my_whoosh.dart';
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:swift_control/utils/single_line_exception.dart';
@@ -17,13 +18,13 @@ class TrainingPeaks extends SupportedApp {
// https://help.trainingpeaks.com/hc/en-us/articles/31340399556877-TrainingPeaks-Virtual-Controls-and-Keyboard-Shortcuts
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(),
physicalKey: PhysicalKeyboardKey.minus,
logicalKey: LogicalKeyboardKey.minus,
physicalKey: PhysicalKeyboardKey.numpadSubtract,
logicalKey: LogicalKeyboardKey.numpadSubtract,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(),
physicalKey: PhysicalKeyboardKey.equal,
logicalKey: LogicalKeyboardKey.equal,
physicalKey: PhysicalKeyboardKey.numpadAdd,
logicalKey: LogicalKeyboardKey.numpadAdd,
),
KeyPair(
buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(),
@@ -64,8 +65,8 @@ class TrainingPeaks extends SupportedApp {
throw SingleLineException("Window size not known - open $this first");
}
return switch (action.action) {
InGameAction.shiftUp => Offset(windowInfo.windowWidth / 2 * 1.32, windowInfo.windowHeight * 0.74),
InGameAction.shiftDown => Offset(windowInfo.windowWidth / 2 * 1.15, windowInfo.windowHeight * 0.74),
InGameAction.shiftUp => Offset(windowInfo.width / 2 * 1.32, windowInfo.height * 0.74),
InGameAction.shiftDown => Offset(windowInfo.width / 2 * 1.15, windowInfo.height * 0.74),
_ => throw SingleLineException("Unsupported action for IndieVelo: $action"),
};
}

View File

@@ -17,7 +17,7 @@ class Keymap {
separator: ('\n---------\n'),
transform:
(k) =>
'''Button: ${k.buttons.joinToString(transform: (e) => e.name)}\nKeyboard key: ${k.logicalKey?.keyLabel ?? 'Not assigned'}\nAction: ${k.buttons.firstOrNull?.action}${k.touchPosition != Offset.zero ? '\nTouch Position: ${k.touchPosition.toString()}' : ''}''',
'''Button: ${k.buttons.joinToString(transform: (e) => e.name)}\nKeyboard key: ${k.logicalKey?.keyLabel ?? 'Not assigned'}\nAction: ${k.buttons.firstOrNull?.action}${k.touchPosition != Offset.zero ? '\nTouch Position: ${k.touchPosition.toString()}' : ''}${k.isLongPress ? '\nLong Press: Enabled' : ''}''',
);
}
@@ -41,12 +41,14 @@ class KeyPair {
PhysicalKeyboardKey? physicalKey;
LogicalKeyboardKey? logicalKey;
Offset touchPosition;
bool isLongPress;
KeyPair({
required this.buttons,
required this.physicalKey,
required this.logicalKey,
this.touchPosition = Offset.zero,
this.isLongPress = false,
});
bool get isSpecialKey =>
@@ -78,6 +80,7 @@ class KeyPair {
'logicalKey': logicalKey?.keyId.toString() ?? '0',
'physicalKey': physicalKey?.usbHidUsage.toString() ?? '0',
'touchPosition': {'x': touchPosition.dx, 'y': touchPosition.dy},
'isLongPress': isLongPress,
});
}
@@ -96,6 +99,7 @@ class KeyPair {
physicalKey:
int.parse(decoded['physicalKey']) != 0 ? PhysicalKeyboardKey(int.parse(decoded['physicalKey'])) : null,
touchPosition: Offset(decoded['touchPosition']['x'], decoded['touchPosition']['y']),
isLongPress: decoded['isLongPress'] ?? false,
);
}
}

View File

@@ -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 {
@@ -34,6 +83,21 @@ class BluetoothScanRequirement extends PlatformRequirement {
}
}
class LocationRequirement extends PlatformRequirement {
LocationRequirement() : super('Allow Location so Bluetooth scan works');
@override
Future<void> call() async {
await Permission.locationWhenInUse.request();
}
@override
Future<void> getStatus() async {
final state = await Permission.locationWhenInUse.status;
status = state.isGranted || state.isLimited;
}
}
class BluetoothConnectRequirement extends PlatformRequirement {
BluetoothConnectRequirement() : super('Allow Bluetooth Connections');
@@ -79,10 +143,7 @@ class NotificationRequirement extends PlatformRequirement {
InitializationSettings(android: initializationSettingsAndroid),
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
onDidReceiveNotificationResponse: (n) {
if (n.actionId != null) {
connection.reset();
exit(0);
}
notificationTapBackground(n);
},
);
@@ -124,6 +185,8 @@ class NotificationRequirement extends PlatformRequirement {
void notificationTapBackground(NotificationResponse notificationResponse) {
if (notificationResponse.actionId != null) {
connection.reset();
exit(0);
AndroidFlutterLocalNotificationsPlugin().stopForegroundService().then((_) {
exit(0);
});
}
}

View File

@@ -46,7 +46,7 @@ class UnsupportedPlatform extends PlatformRequirement {
}
class BluetoothScanning extends PlatformRequirement {
BluetoothScanning() : super('Bluetooth Scanning') {
BluetoothScanning() : super('Finding your Zwift® controller...') {
status = false;
}

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/utils/requirements/android.dart';
@@ -24,17 +25,23 @@ 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()];
} else if (Platform.isAndroid) {
final deviceInfoPlugin = DeviceInfoPlugin();
final deviceInfo = await deviceInfoPlugin.androidInfo;
list = [
BluetoothTurnedOn(),
AccessibilityRequirement(),
NotificationRequirement(),
BluetoothScanRequirement(),
BluetoothConnectRequirement(),
if (deviceInfo.version.sdkInt <= 30)
LocationRequirement()
else ...[
BluetoothScanRequirement(),
BluetoothConnectRequirement(),
],
BluetoothScanning(),
];
} else {

View File

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

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

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

View File

@@ -67,11 +67,11 @@ class _HotKeyListenerState extends State<HotKeyListenerDialog> {
setState(() {
if (event is KeyDownEvent) {
_pressedKey = event;
} else if (event is KeyUpEvent) {
widget.customApp.setKey(
_pressedButton!,
physicalKey: _pressedKey!.physicalKey,
logicalKey: _pressedKey!.logicalKey,
touchPosition: widget.keyPair?.touchPosition,
);
}
});
@@ -99,43 +99,6 @@ class _HotKeyListenerState extends State<HotKeyListenerDialog> {
children: [
Text("Press a key on your keyboard to assign to ${_pressedButton.toString()}"),
Text(_formatKey(_pressedKey)),
PopupMenuButton<PhysicalKeyboardKey>(
tooltip: 'Drag or click for special keys',
itemBuilder:
(context) => [
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.mediaPlayPause,
child: const Text('Media: Play/Pause'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.mediaStop,
child: const Text('Media: Stop'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.mediaTrackPrevious,
child: const Text('Media: Previous'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.mediaTrackNext,
child: const Text('Media: Next'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.audioVolumeUp,
child: const Text('Media: Volume Up'),
),
PopupMenuItem<PhysicalKeyboardKey>(
value: PhysicalKeyboardKey.audioVolumeDown,
child: const Text('Media: Volume Down'),
),
],
onSelected: (key) {
widget.customApp.setKey(_pressedButton!, physicalKey: key, logicalKey: null);
Navigator.pop(context, key);
},
child: IgnorePointer(
child: ElevatedButton(onPressed: () {}, child: Text('Or choose special key')),
),
),
],
),
),

View File

@@ -0,0 +1,135 @@
import 'package:dartx/dartx.dart';
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;
const KeymapExplanation({super.key, required this.keymap, required this.onUpdate});
@override
Widget build(BuildContext context) {
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?.usbHidUsage}-${element.isLongPress}');
final touchGroups = availableKeypairs
.filter((e) => e.physicalKey == null && e.touchPosition != Offset.zero)
.groupBy((element) => '${element.touchPosition.dx}-${element.touchPosition.dy}-${element.isLongPress}');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
if (keymap.keyPairs.isEmpty)
Text('No key mappings found. Please customize the keymap.')
else
Table(
border: TableBorder.all(color: Theme.of(context).colorScheme.primaryContainer),
children: [
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(6),
child: Text(
'Button on your ${connectedDevice?.device.name ?? connectedDevice?.runtimeType}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.all(6),
child: Text(
'Action',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
],
),
for (final pair in keyboardGroups.entries) ...[
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(6),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final keyPair in pair.value)
for (final button in keyPair.buttons)
if (connectedDevice?.availableButtons.contains(button) == true)
IntrinsicWidth(child: KeyWidget(label: button.name)),
],
),
),
Padding(padding: const EdgeInsets.all(6), child: KeypairExplanation(keyPair: pair.value.first)),
],
),
],
for (final pair in touchGroups.entries) ...[
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(6),
child: Row(
spacing: 8,
children: [
for (final keyPair in pair.value)
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)),
],
),
],
],
),
],
);
}
}
class KeyWidget extends StatelessWidget {
final String label;
const KeyWidget({super.key, required this.label});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
constraints: BoxConstraints(minWidth: 30),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.primary),
borderRadius: BorderRadius.circular(4),
color: Theme.of(context).colorScheme.primaryContainer,
),
child: Center(
child: Text(
label.splitByUpperCase(),
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
);
}
}
extension SplitByUppercase on String {
String splitByUpperCase() {
return replaceAllMapped(RegExp(r'([a-z])([A-Z])'), (match) => '${match.group(1)} ${match.group(2)}').capitalize();
}
}

View File

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

View File

@@ -1,17 +1,50 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
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() {
return [
TextButton(
onPressed: () {
launchUrlString('https://paypal.me/boni');
PopupMenuButton(
itemBuilder: (BuildContext context) {
return [
PopupMenuItem(
child: Text('via Credit Card, Google Pay, Apple Pay and others'),
onTap: () {
final currency = NumberFormat.simpleCurrency(locale: kIsWeb ? 'de_DE' : Platform.localeName);
final link = switch (currency.currencyName) {
'USD' => 'https://donate.stripe.com/8x24gzc5c4ZE3VJdt36J201',
_ => 'https://donate.stripe.com/9B6aEX0muajY8bZ1Kl6J200',
};
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: () {
launchUrlString('https://paypal.me/boni');
},
),
];
},
child: Text('Donate ♥'),
icon: Text('Donate ♥', style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)),
),
SizedBox(width: 8),
const MenuButton(),
SizedBox(width: 8),
];
@@ -26,18 +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: 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
View 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),
),
),
);
}
}

View File

@@ -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,14 +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 latestVersion = data['tag_name'].split('+').first;
final tagName = data['tag_name'] as String;
final prerelase = data['prerelease'] as bool;
final latestVersion = tagName.split('+').first;
final currentVersion = 'v${_packageInfoValue!.version}';
if (latestVersion != null && latestVersion != currentVersion) {
// +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'];
@@ -54,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}'),

View File

@@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
import device_info_plus
import file_selector_macos
import flutter_local_notifications
import keypress_simulator_macos
@@ -16,6 +17,7 @@ import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
KeypressSimulatorMacosPlugin.register(with: registry.registrar(forPlugin: "KeypressSimulatorMacosPlugin"))

View File

@@ -1,4 +1,4 @@
platform :osx, '10.14'
platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -1,4 +1,6 @@
PODS:
- device_info_plus (0.0.1):
- FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- flutter_local_notifications (0.0.1):
@@ -18,10 +20,11 @@ PODS:
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
- window_manager (0.2.0):
- window_manager (0.5.0):
- FlutterMacOS
DEPENDENCIES:
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
@@ -34,6 +37,8 @@ DEPENDENCIES:
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
EXTERNAL SOURCES:
device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
file_selector_macos:
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
flutter_local_notifications:
@@ -56,17 +61,18 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
SPEC CHECKSUMS:
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
flutter_local_notifications: 4ccab5b7a22835214a6672e3f9c5e8ae207dab36
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
keypress_simulator_macos: f8556f9101f9f2f175652e0bceddf0fe82a4c6b2
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
window_manager: e25faf20d88283a0d46e7b1a759d07261ca27575
PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
COCOAPODS: 1.16.2

View File

@@ -555,7 +555,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -643,7 +643,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@@ -693,7 +693,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;

BIN
playstoreassets/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

BIN
playstoreassets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

BIN
playstoreassets/mob1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
playstoreassets/mob2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
playstoreassets/tab1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

BIN
playstoreassets/tab2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

View File

@@ -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:
@@ -28,10 +28,10 @@ packages:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.12.0"
version: "2.13.0"
bluez:
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:
@@ -128,14 +128,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.11"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
sha256: "49413c8ca514dea7633e8def233b25efdf83ec8522955cc2c0e3ad802927e7c6"
url: "https://pub.dev"
source: hosted
version: "12.1.0"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
url: "https://pub.dev"
source: hosted
version: "7.0.3"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.3.3"
ffi:
dependency: transitive
description:
@@ -164,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:
@@ -196,10 +212,10 @@ packages:
dependency: "direct main"
description:
name: flex_color_scheme
sha256: "3344f8f6536c6ce0473b98e9f084ef80ca89024ad3b454f9c32cf840206f4387"
sha256: "034d5720747e6af39b2ad090d82dd92d33fde68e7964f1814b714c9d49ddbd64"
url: "https://pub.dev"
source: hosted
version: "8.2.0"
version: "8.3.0"
flex_seed_scheme:
dependency: transitive
description:
@@ -217,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: d59eeafd6df92174b1d5f68fc9d66634c97ce2e7cfe2293476236547bb19bbbd
sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5"
url: "https://pub.dev"
source: hosted
version: "19.0.0"
version: "19.4.2"
flutter_local_notifications_linux:
dependency: transitive
description:
@@ -249,26 +265,26 @@ packages:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "2569b973fc9d1f63a37410a9f7c1c552081226c597190cb359ef5d5762d1631c"
sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
url: "https://pub.dev"
source: hosted
version: "9.0.0"
version: "9.1.0"
flutter_local_notifications_windows:
dependency: transitive
description:
name: flutter_local_notifications_windows
sha256: f8fc0652a601f83419d623c85723a3e82ad81f92b33eaa9bcc21ea1b94773e6e
sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
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
@@ -278,10 +294,10 @@ packages:
dependency: transitive
description:
name: flutter_web_bluetooth
sha256: "1363831def5eed1e1064d1eca04e8ccb35446e8f758579c3c519e156b77926da"
sha256: ad26a1b3fef95b86ea5f63793b9a0cdc1a33490f35d754e4e711046cae3ebbf8
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.1.0"
flutter_web_plugins:
dependency: transitive
description: flutter
@@ -291,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:
@@ -315,66 +331,82 @@ 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:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
json_annotation:
dependency: transitive
description:
@@ -415,34 +447,34 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.8"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
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:
@@ -487,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:
@@ -535,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:
@@ -583,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:
@@ -615,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:
@@ -679,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:
@@ -772,10 +804,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.6"
time:
dependency: transitive
description:
@@ -788,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:
@@ -812,34 +844,34 @@ packages:
dependency: "direct main"
description:
name: universal_ble
sha256: "35d210e93a5938c6a6d1fd3c710cf4ac90b1bdd1b11c8eb2beeb32600672e6e6"
sha256: "6a5c6c1fb295015934a5aef3dc751ae7e00721535275f8478bfe74db77b238c5"
url: "https://pub.dev"
source: hosted
version: "0.17.0"
version: "0.21.1"
url_launcher:
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:
@@ -852,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:
@@ -868,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:
@@ -884,18 +916,18 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "14.3.1"
version: "15.0.2"
web:
dependency: transitive
description:
@@ -908,18 +940,26 @@ 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:
name: win32_registry
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
window_manager:
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:
@@ -932,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:
@@ -945,5 +985,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.29.0"
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0"

View File

@@ -1,7 +1,7 @@
name: swift_control
description: "SwiftControl - Control your virtual riding"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 2.0.0+0
version: 2.6.1+7
environment:
sdk: ^3.7.0
@@ -11,19 +11,22 @@ dependencies:
sdk: flutter
url_launcher: ^6.3.1
flutter_local_notifications: ^19.0.0
universal_ble: any
protobuf: ^3.1.0
permission_handler: ^11.4.0
flutter_local_notifications: ^19.4.1
universal_ble: ^0.21.1
intl: any
protobuf: ^4.2.0
permission_handler: ^12.0.1
dartx: any
image_picker: ^1.1.2
pointycastle: any
window_manager: ^0.4.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.2.0
package_info_plus: ^8.3.0
flex_color_scheme: ^8.3.0
package_info_plus: ^9.0.0
in_app_update: ^4.2.5
accessibility:
path: accessibility
http: ^1.3.0
@@ -33,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
View 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/^- /• /'

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

75
test/long_press_test.dart Normal file
View File

@@ -0,0 +1,75 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/utils/keymap/keymap.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
void main() {
group('Long Press KeyPair Tests', () {
test('KeyPair should encode and decode isLongPress property', () {
// Create a KeyPair with long press enabled
final keyPair = KeyPair(
buttons: [ZwiftButton.a],
physicalKey: PhysicalKeyboardKey.keyA,
logicalKey: LogicalKeyboardKey.keyA,
isLongPress: true,
);
// Encode the KeyPair
final encoded = keyPair.encode();
// Decode the KeyPair
final decoded = KeyPair.decode(encoded);
// Verify the decoded KeyPair has the correct properties
expect(decoded, isNotNull);
expect(decoded!.isLongPress, true);
expect(decoded.buttons, equals([ZwiftButton.a]));
expect(decoded.physicalKey, equals(PhysicalKeyboardKey.keyA));
expect(decoded.logicalKey, equals(LogicalKeyboardKey.keyA));
});
test('KeyPair should default isLongPress to false when not specified in decode', () {
// Create a legacy encoded KeyPair without isLongPress property
const legacyEncoded = '''
{
"actions": ["a"],
"logicalKey": "97",
"physicalKey": "458752",
"touchPosition": {"x": 0.0, "y": 0.0}
}
''';
// Decode the legacy KeyPair
final decoded = KeyPair.decode(legacyEncoded);
// Verify the decoded KeyPair defaults isLongPress to false
expect(decoded, isNotNull);
expect(decoded!.isLongPress, false);
});
test('KeyPair constructor should default isLongPress to false', () {
final keyPair = KeyPair(
buttons: [ZwiftButton.a],
physicalKey: PhysicalKeyboardKey.keyA,
logicalKey: LogicalKeyboardKey.keyA,
);
expect(keyPair.isLongPress, false);
});
test('KeyPair should correctly encode isLongPress false', () {
final keyPair = KeyPair(
buttons: [ZwiftButton.a],
physicalKey: PhysicalKeyboardKey.keyA,
logicalKey: LogicalKeyboardKey.keyA,
isLongPress: false,
);
final encoded = keyPair.encode();
final decoded = KeyPair.decode(encoded);
expect(decoded, isNotNull);
expect(decoded!.isLongPress, false);
});
});
}

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

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