mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Compare commits
61 Commits
copilot/ad
...
login
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa8089556a | ||
|
|
f572a5c300 | ||
|
|
dcd11e6c4b | ||
|
|
3f1a920888 | ||
|
|
ceeca9dd02 | ||
|
|
ab379cf74b | ||
|
|
ad7bd646f3 | ||
|
|
7ed1ba4397 | ||
|
|
a9cb929b01 | ||
|
|
8d056b526e | ||
|
|
7a77acaf94 | ||
|
|
216dc97517 | ||
|
|
746a680449 | ||
|
|
b1385e70cc | ||
|
|
aec24bba61 | ||
|
|
226824c14a | ||
|
|
8c4816ffd0 | ||
|
|
07423fc0f6 | ||
|
|
812a4efe13 | ||
|
|
8dadc07e07 | ||
|
|
15dc34b2ea | ||
|
|
d8a528017d | ||
|
|
d2a41fc2fa | ||
|
|
7b2f16772d | ||
|
|
edf19e3ffa | ||
|
|
ec4e4fc375 | ||
|
|
59141c81af | ||
|
|
732bb4a150 | ||
|
|
84d9d1e312 | ||
|
|
3b6f9f6f29 | ||
|
|
21f7636cee | ||
|
|
3eda3b590a | ||
|
|
f844681f4c | ||
|
|
5c22851d66 | ||
|
|
fb2068a08a | ||
|
|
f7a0b8dca8 | ||
|
|
76c59537c1 | ||
|
|
81f14f16fd | ||
|
|
c4a8d1ef9c | ||
|
|
a1cfe43ef9 | ||
|
|
14f5486ab6 | ||
|
|
a7e2b5bc26 | ||
|
|
8bea3b36cc | ||
|
|
2802ead254 | ||
|
|
5c2ae38951 | ||
|
|
0dc6ea7fd4 | ||
|
|
288fbed819 | ||
|
|
497528c75b | ||
|
|
d8ceea9c63 | ||
|
|
bed3dac98e | ||
|
|
9eaa9c53f9 | ||
|
|
ccd1d46128 | ||
|
|
79edebc8f9 | ||
|
|
dbf148c41f | ||
|
|
fe898cefda | ||
|
|
f662d0a36a | ||
|
|
5c8a5934f2 | ||
|
|
c7e845086a | ||
|
|
67a4144ab0 | ||
|
|
bfeb72a775 | ||
|
|
799234c323 |
30
.github/workflows/build.yml
vendored
30
.github/workflows/build.yml
vendored
@@ -31,8 +31,7 @@ on:
|
||||
|
||||
env:
|
||||
SHOREBIRD_TOKEN: ${{ secrets.SHOREBIRD_TOKEN }}
|
||||
FLUTTER_VERSION_MAC: 3.41.0-0.0.pre
|
||||
FLUTTER_VERSION: 3.38.7
|
||||
FLUTTER_VERSION: 3.41.0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -52,6 +51,14 @@ jobs:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
- name: rename pubspec_overrides_ci.yaml to pubspec_overrides.yaml
|
||||
run: |
|
||||
if [ -f pubspec_overrides_ci.yaml ]; then
|
||||
mv pubspec_overrides_ci.yaml pubspec_overrides.yaml
|
||||
else
|
||||
echo "No pubspec_overrides_ci.yaml found, skipping rename."
|
||||
fi
|
||||
|
||||
- name: Install certificates
|
||||
if: inputs.build_mac || inputs.build_ios
|
||||
env:
|
||||
@@ -105,8 +112,8 @@ jobs:
|
||||
- name: Set Up Flutter maCOS
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'beta'
|
||||
flutter-version: ${{ env.FLUTTER_VERSION_MAC }}
|
||||
channel: 'stable'
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Generate translation files
|
||||
run: |
|
||||
@@ -126,12 +133,6 @@ jobs:
|
||||
run:
|
||||
flutter build macos --release --obfuscate --split-debug-info=symbols --dart-define=VERIFYING_SHARED_SECRET=${{ secrets.VERIFYING_SHARED_SECRET }} --dart-define=REVENUECAT_API_KEY_IOS=${{ secrets.REVENUECAT_API_KEY_IOS }}
|
||||
|
||||
- name: Set Up Flutter Rest
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Decode Keystore
|
||||
if: inputs.build_android
|
||||
run: |
|
||||
@@ -244,6 +245,15 @@ jobs:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
- name: rename pubspec_overrides_ci.yaml to pubspec_overrides.yaml
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (Test-Path pubspec_overrides_ci.yaml) {
|
||||
Rename-Item -Path pubspec_overrides_ci.yaml -NewName pubspec_overrides.yaml
|
||||
} else {
|
||||
Write-Output "No pubspec_overrides_ci.yaml found, skipping rename."
|
||||
}
|
||||
|
||||
- name: Extract version from pubspec.yaml (Windows)
|
||||
shell: pwsh
|
||||
run: |
|
||||
|
||||
19
.github/workflows/patch.yml
vendored
19
.github/workflows/patch.yml
vendored
@@ -25,6 +25,14 @@ jobs:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
- name: rename pubspec_overrides_ci.yaml to pubspec_overrides.yaml
|
||||
run: |
|
||||
if [ -f pubspec_overrides_ci.yaml ]; then
|
||||
mv pubspec_overrides_ci.yaml pubspec_overrides.yaml
|
||||
else
|
||||
echo "No pubspec_overrides_ci.yaml found, skipping rename."
|
||||
fi
|
||||
|
||||
- name: 🐦 Setup Shorebird
|
||||
uses: shorebirdtech/setup-shorebird@v1
|
||||
with:
|
||||
@@ -104,7 +112,6 @@ jobs:
|
||||
args: '--allow-asset-diffs --allow-native-diffs -- --obfuscate --split-debug-info=symbols --dart-define=REVENUECAT_API_KEY_ANDROID=${{ secrets.REVENUECAT_API_KEY_ANDROID }}'
|
||||
|
||||
- name: 🚀 Shorebird Patch iOS
|
||||
if: false
|
||||
uses: shorebirdtech/shorebird-patch@v1
|
||||
with:
|
||||
platform: ios
|
||||
@@ -113,7 +120,6 @@ jobs:
|
||||
|
||||
windows:
|
||||
name: Patch Windows
|
||||
if: false
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
@@ -124,6 +130,15 @@ jobs:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
|
||||
- name: rename pubspec_overrides_ci.yaml to pubspec_overrides.yaml
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (Test-Path pubspec_overrides_ci.yaml) {
|
||||
Rename-Item -Path pubspec_overrides_ci.yaml -NewName pubspec_overrides.yaml
|
||||
} else {
|
||||
Write-Output "No pubspec_overrides_ci.yaml found, skipping rename."
|
||||
}
|
||||
|
||||
- name: 🐦 Setup Shorebird
|
||||
uses: shorebirdtech/setup-shorebird@v1
|
||||
with:
|
||||
|
||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,10 +1,23 @@
|
||||
### Unreleased
|
||||
### 4.8.0 (15-02-2026)
|
||||
|
||||
**Features**:
|
||||
- Windows: Add device source detection for media hotkeys
|
||||
- Distinguish between multiple bluetooth media controllers
|
||||
- Configure different actions for each controller even when they have the same buttons
|
||||
- Uses Windows Raw Input API to identify device sources
|
||||
- Bluetooth media buttons are now supported on iOS
|
||||
- Shimano Di2: long press and double clicks are now supported:
|
||||
- perform steering using long presses
|
||||
- gear changes are now reflected properly without losing any button presses
|
||||
|
||||
### 4.7.0 (04-02-2026)
|
||||
|
||||
**Features**:
|
||||
- new connection method: act as Bluetooth Keyboard:
|
||||
Your device can now act as Bluetooth keyboard, allowing you to send keyboard shortcuts (e.g. for virtual shifting) directly to your connected device. Especially useful for tablets / iPads.
|
||||
- added new keyboard shortcuts for Rouvy (Kudos, Pause workout)
|
||||
|
||||
**Fixes**:
|
||||
- you can now finally buy the full version on Android :)
|
||||
- save "Enable Media Key detection" setting across app restarts
|
||||
- UI adjustments and fixes in the controller configuration screen
|
||||
- iOS: Remote pairing now works again
|
||||
|
||||
### 4.6.0 (28-01-2026)
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
4.6.2
|
||||
4.7.2
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
.
|
||||
@@ -73,7 +73,7 @@ class AccessibilityService : AccessibilityService(), Listener {
|
||||
override fun onKeyEvent(event: KeyEvent): Boolean {
|
||||
val keyString = KeyEvent.keyCodeToString(event.keyCode)
|
||||
// if currently active app is BikeControl => handle it, so keymap can be created
|
||||
if (!Observable.ignoreHidDevices && isBleRemote(event) && (rootInActiveWindow.packageName == "de.jonasbark.swiftcontrol" || Observable.handledKeys.contains(keyString))) {
|
||||
if (!Observable.ignoreHidDevices && isBleRemote(event) && (rootInActiveWindow?.packageName == "de.jonasbark.swiftcontrol" || Observable.handledKeys.contains(keyString))) {
|
||||
// Handle keys that have a keymap defined
|
||||
Log.d(
|
||||
"AccessibilityService",
|
||||
|
||||
@@ -51,6 +51,13 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
|
||||
<data android:scheme="bikecontrol" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
|
||||
BIN
assets/silence.mp3
Normal file
BIN
assets/silence.mp3
Normal file
Binary file not shown.
@@ -20,7 +20,5 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
PODS:
|
||||
- app_links (6.4.1):
|
||||
- Flutter
|
||||
- AppAuth (2.0.0):
|
||||
- AppAuth/Core (= 2.0.0)
|
||||
- AppAuth/ExternalUserAgent (= 2.0.0)
|
||||
- AppAuth/Core (2.0.0)
|
||||
- AppAuth/ExternalUserAgent (2.0.0):
|
||||
- AppAuth/Core
|
||||
- AppCheckCore (11.2.0):
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- audio_session (0.0.1):
|
||||
- Flutter
|
||||
- bluetooth_low_energy_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -14,6 +28,33 @@ PODS:
|
||||
- Flutter
|
||||
- gamepads_ios (0.1.1):
|
||||
- Flutter
|
||||
- google_sign_in_ios (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleSignIn (~> 9.0)
|
||||
- GTMSessionFetcher (>= 3.4.0)
|
||||
- GoogleSignIn (9.1.0):
|
||||
- AppAuth (~> 2.0)
|
||||
- AppCheckCore (~> 11.0)
|
||||
- GTMAppAuth (~> 5.0)
|
||||
- GTMSessionFetcher/Core (~> 3.3)
|
||||
- GoogleUtilities/Environment (8.1.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Logger (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (8.1.0)
|
||||
- GoogleUtilities/UserDefaults (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GTMAppAuth (5.0.0):
|
||||
- AppAuth/Core (~> 2.0)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
|
||||
- GTMSessionFetcher (3.5.0):
|
||||
- GTMSessionFetcher/Full (= 3.5.0)
|
||||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- GTMSessionFetcher/Full (3.5.0):
|
||||
- GTMSessionFetcher/Core
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- in_app_purchase_storekit (0.0.1):
|
||||
@@ -25,6 +66,9 @@ PODS:
|
||||
- Flutter
|
||||
- ios_receipt (0.0.1):
|
||||
- Flutter
|
||||
- just_audio (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- media_key_detector_ios (0.0.1):
|
||||
- Flutter
|
||||
- nsd_ios (0.0.1):
|
||||
@@ -33,6 +77,7 @@ PODS:
|
||||
- Flutter
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
- purchases_flutter (9.10.6):
|
||||
- Flutter
|
||||
- PurchasesHybridCommon (= 17.27.1)
|
||||
@@ -54,6 +99,8 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sign_in_with_apple (0.0.1):
|
||||
- Flutter
|
||||
- universal_ble (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -63,6 +110,8 @@ PODS:
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||
- bluetooth_low_energy_darwin (from `.symlinks/plugins/bluetooth_low_energy_darwin/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
@@ -70,11 +119,13 @@ DEPENDENCIES:
|
||||
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
||||
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
|
||||
- gamepads_ios (from `.symlinks/plugins/gamepads_ios/ios`)
|
||||
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- ios_receipt (from `.symlinks/plugins/ios_receipt/ios`)
|
||||
- just_audio (from `.symlinks/plugins/just_audio/darwin`)
|
||||
- media_key_detector_ios (from `.symlinks/plugins/media_key_detector_ios/ios`)
|
||||
- nsd_ios (from `.symlinks/plugins/nsd_ios/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
@@ -84,18 +135,30 @@ DEPENDENCIES:
|
||||
- restart_app (from `.symlinks/plugins/restart_app/ios`)
|
||||
- sensors_plus (from `.symlinks/plugins/sensors_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
|
||||
- universal_ble (from `.symlinks/plugins/universal_ble/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- AppAuth
|
||||
- AppCheckCore
|
||||
- GoogleSignIn
|
||||
- GoogleUtilities
|
||||
- GTMAppAuth
|
||||
- GTMSessionFetcher
|
||||
- PromisesObjC
|
||||
- PurchasesHybridCommon
|
||||
- PurchasesHybridCommonUI
|
||||
- RevenueCat
|
||||
- RevenueCatUI
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: ".symlinks/plugins/app_links/ios"
|
||||
audio_session:
|
||||
:path: ".symlinks/plugins/audio_session/ios"
|
||||
bluetooth_low_energy_darwin:
|
||||
:path: ".symlinks/plugins/bluetooth_low_energy_darwin/darwin"
|
||||
device_info_plus:
|
||||
@@ -110,6 +173,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
||||
gamepads_ios:
|
||||
:path: ".symlinks/plugins/gamepads_ios/ios"
|
||||
google_sign_in_ios:
|
||||
:path: ".symlinks/plugins/google_sign_in_ios/darwin"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
in_app_purchase_storekit:
|
||||
@@ -120,6 +185,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
ios_receipt:
|
||||
:path: ".symlinks/plugins/ios_receipt/ios"
|
||||
just_audio:
|
||||
:path: ".symlinks/plugins/just_audio/darwin"
|
||||
media_key_detector_ios:
|
||||
:path: ".symlinks/plugins/media_key_detector_ios/ios"
|
||||
nsd_ios:
|
||||
@@ -138,6 +205,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/sensors_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sign_in_with_apple:
|
||||
:path: ".symlinks/plugins/sign_in_with_apple/ios"
|
||||
universal_ble:
|
||||
:path: ".symlinks/plugins/universal_ble/darwin"
|
||||
url_launcher_ios:
|
||||
@@ -146,6 +215,10 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 585674be3c6661708e6cd794ab4f39fb9d8356f9
|
||||
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
|
||||
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
|
||||
audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe
|
||||
bluetooth_low_energy_darwin: 50bc79258e60586e4c4bed5948bd31d925f37fac
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
@@ -153,15 +226,22 @@ SPEC CHECKSUMS:
|
||||
flutter_secure_storage_darwin: 557817588b80e60213cbecb573c45c76b788018d
|
||||
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
|
||||
gamepads_ios: 1d2930c7a4450a9a1b57444ebf305a6a6cbeea0b
|
||||
google_sign_in_ios: 7336a3372ea93ea56a21e126a0055ffca3723601
|
||||
GoogleSignIn: fcee2257188d5eda57a5e2b6a715550ffff9206d
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
|
||||
in_app_purchase_storekit: 2342c0a5da86593124d08dd13d920f39a52b273a
|
||||
in_app_review: 436034b18594851a7328d7f1c2ed5ec235b79cfc
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
ios_receipt: c2d5b4c36953c377a024992393976214ce6951e6
|
||||
just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79
|
||||
media_key_detector_ios: 7ff9aefdfea00bb7b71e184132381b7d0e7e1269
|
||||
nsd_ios: 8c37babdc6538e3350dbed3a52674d2edde98173
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
purchases_flutter: b3c0792197f69cd7af4c2449b71df6ac6378aace
|
||||
purchases_ui_flutter: caae6d62ea23c6fe964992a28353211cc74b244a
|
||||
PurchasesHybridCommon: 027f03312519c51056457eb2e4f7ee1c91b61b8f
|
||||
@@ -171,6 +251,7 @@ SPEC CHECKSUMS:
|
||||
RevenueCatUI: ac7492873928e9e7f297e5e27a7c4f23f9008326
|
||||
sensors_plus: 7229095999f30740798f0eeef5cd120357a8f4f2
|
||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
|
||||
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
|
||||
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
|
||||
wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
|
||||
|
||||
@@ -22,6 +22,19 @@
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.709945926587-0iierajthibf4vhqf85fc7bbpgbdgua2</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>GIDClientID</key>
|
||||
<string>709945926587-0iierajthibf4vhqf85fc7bbpgbdgua2.apps.googleusercontent.com</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
@@ -30,21 +43,22 @@
|
||||
<true/>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>BikeControl uses Bluetooth to connect to accessories.</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_wahoo-fitness-tnp._tcp</string>
|
||||
<string>_openbikecontrol._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>This app connects to your trainer app on your local network.</string>
|
||||
<key>NSMotionUsageDescription</key>
|
||||
<string>Access your accelerometer and gyroscope for steering support via your phone.</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_wahoo-fitness-tnp._tcp</string>
|
||||
<string>_openbikecontrol._tcp</string>
|
||||
</array>
|
||||
<string>Access your accelerometer and gyroscope for steering support via your phone.</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>bluetooth-peripheral</string>
|
||||
<string>bluetooth-central</string>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array/>
|
||||
</dict>
|
||||
|
||||
@@ -64,8 +64,10 @@ class Connection {
|
||||
lastLogEntries = lastLogEntries.takeLast(kIsWeb ? 1000 : 60).toList();
|
||||
});
|
||||
|
||||
if (!kIsWeb && (Platform.isMacOS || Platform.isWindows)) {
|
||||
if (!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isIOS)) {
|
||||
core.mediaKeyHandler.initialize();
|
||||
// Load saved media key detection state
|
||||
core.mediaKeyHandler.isMediaKeyDetectionEnabled.value = core.settings.getMediaKeyDetectionEnabled();
|
||||
}
|
||||
|
||||
UniversalBle.onAvailabilityChange = (available) {
|
||||
|
||||
@@ -69,18 +69,24 @@ abstract class BaseDevice {
|
||||
Future<void> connect();
|
||||
|
||||
Future<void> handleButtonsClickedWithoutLongPressSupport(List<ControllerButton> clickedButtons) async {
|
||||
await handleButtonsClicked(clickedButtons, longPress: true);
|
||||
if (clickedButtons.length == 1) {
|
||||
final keyPair = core.actionHandler.supportedApp?.keymap.getKeyPair(clickedButtons.single);
|
||||
if (keyPair != null && (keyPair.isLongPress || keyPair.inGameAction?.isLongPress == true)) {
|
||||
// simulate release after click
|
||||
// For long press actions: perform down, wait, then release
|
||||
await handleButtonsClicked(clickedButtons, longPress: true);
|
||||
_longPressTimer?.cancel();
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
await handleButtonsClicked([], longPress: true);
|
||||
} else {
|
||||
await handleButtonsClicked([], longPress: true);
|
||||
// For non-long-press actions: perform a single click
|
||||
// First call performs the click action (isKeyDown: true, isKeyUp: true)
|
||||
await handleButtonsClicked(clickedButtons);
|
||||
// Second call cleans up state (clears timer, logs release, clears _previouslyPressedButtons)
|
||||
// but doesn't perform a release action since longPress: false
|
||||
await handleButtonsClicked([]);
|
||||
}
|
||||
} else {
|
||||
await handleButtonsClicked(clickedButtons);
|
||||
await handleButtonsClicked([]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import 'package:bike_control/bluetooth/devices/zwift/zwift_clickv2.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/zwift_device.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/zwift_play.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/zwift_ride.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/pages/device.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/i18n_extension.dart';
|
||||
@@ -62,6 +61,7 @@ abstract class BluetoothDevice extends BaseDevice {
|
||||
|
||||
static List<String> servicesToScan = [
|
||||
ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID,
|
||||
ZwiftConstants.ZWIFT_CUSTOM_SERVICE_SHORT_UUID,
|
||||
ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID,
|
||||
SquareConstants.SERVICE_UUID,
|
||||
WahooKickrBikeShiftConstants.SERVICE_UUID,
|
||||
@@ -141,6 +141,7 @@ abstract class BluetoothDevice extends BaseDevice {
|
||||
return device;
|
||||
} else if (scanResult.services.containsAny([
|
||||
ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID.toLowerCase(),
|
||||
ZwiftConstants.ZWIFT_CUSTOM_SERVICE_SHORT_UUID.toLowerCase(),
|
||||
ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID.toLowerCase(),
|
||||
])) {
|
||||
// otherwise use the manufacturer data to identify the device
|
||||
@@ -169,13 +170,10 @@ abstract class BluetoothDevice extends BaseDevice {
|
||||
device == null &&
|
||||
core.connection.controllerDevices.none((d) => d is ZwiftRide)) {
|
||||
// Fallback for Zwift Ride if nothing else matched => old firmware
|
||||
if (navigatorKey.currentContext?.mounted ?? false) {
|
||||
buildToast(
|
||||
navigatorKey.currentContext!,
|
||||
title: 'You may need to update your Zwift Ride firmware.',
|
||||
duration: Duration(seconds: 6),
|
||||
);
|
||||
}
|
||||
buildToast(
|
||||
title: 'You may need to update your Zwift Ride firmware.',
|
||||
duration: Duration(seconds: 6),
|
||||
);
|
||||
}
|
||||
return device;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ class HidDevice extends BaseDevice {
|
||||
(core.actionHandler as AndroidActions).ignoreHidDevices();
|
||||
} else if (core.mediaKeyHandler.isMediaKeyDetectionEnabled.value) {
|
||||
core.mediaKeyHandler.isMediaKeyDetectionEnabled.value = false;
|
||||
core.settings.setMediaKeyDetectionEnabled(false);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
@@ -11,6 +10,7 @@ import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bike_control/utils/keymap/keymap.dart';
|
||||
import 'package:bike_control/utils/requirements/multi.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
|
||||
class WhooshLink extends TrainerConnection {
|
||||
Socket? _socket;
|
||||
@@ -66,16 +66,17 @@ class WhooshLink extends TrainerConnection {
|
||||
|
||||
// Accept connection
|
||||
_server!.listen(
|
||||
(Socket socket) {
|
||||
(Socket socket) async {
|
||||
if (kDebugMode) {
|
||||
print('Client connected: ${socket.remoteAddress.address}:${socket.remotePort}');
|
||||
}
|
||||
|
||||
SharedLogic.keepAlive();
|
||||
_socket = socket;
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, AppLocalizations.current.myWhooshLinkConnected),
|
||||
);
|
||||
isConnected.value = true;
|
||||
if (kDebugMode) {
|
||||
print('Client connected: ${socket.remoteAddress.address}:${socket.remotePort}');
|
||||
}
|
||||
|
||||
// Listen for data from the client
|
||||
socket.listen(
|
||||
(List<int> data) {
|
||||
@@ -89,6 +90,8 @@ class WhooshLink extends TrainerConnection {
|
||||
},
|
||||
onDone: () {
|
||||
print('Client disconnected: $socket');
|
||||
|
||||
SharedLogic.stopKeepAlive();
|
||||
isConnected.value = false;
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_WARNING, 'MyWhoosh Link disconnected'),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart' show AlertNotification, LogNotification;
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/keymap/apps/training_peaks.dart';
|
||||
import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bike_control/utils/keymap/keymap.dart';
|
||||
import 'package:bike_control/widgets/title.dart';
|
||||
@@ -104,17 +105,28 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
'Notify state changed for characteristic: ${char.characteristic.uuid}: ${char.state}',
|
||||
);
|
||||
});
|
||||
|
||||
Uint8List? firstAppInfoMessage;
|
||||
|
||||
_peripheralManager.characteristicWriteRequested.forEach((eventArgs) async {
|
||||
final characteristic = eventArgs.characteristic;
|
||||
final request = eventArgs.request;
|
||||
final value = request.value;
|
||||
print(
|
||||
'Write request for characteristic: ${characteristic.uuid}',
|
||||
);
|
||||
var value = request.value;
|
||||
if (kDebugMode) {
|
||||
print('Write request for characteristic: ${characteristic.uuid}: ${bytesToReadableHex(value)}');
|
||||
}
|
||||
|
||||
switch (eventArgs.characteristic.uuid.toString().toLowerCase()) {
|
||||
case OpenBikeControlConstants.APPINFO_CHARACTERISTIC_UUID:
|
||||
try {
|
||||
if (core.settings.getTrainerApp() is TrainingPeaks) {
|
||||
if (firstAppInfoMessage == null) {
|
||||
firstAppInfoMessage = value;
|
||||
return;
|
||||
} else {
|
||||
value = Uint8List.fromList([...firstAppInfoMessage!, ...value]);
|
||||
}
|
||||
}
|
||||
final appInfo = OpenBikeProtocolParser.parseAppInfo(value);
|
||||
isConnected.value = true;
|
||||
connectedApp.value = appInfo;
|
||||
@@ -228,6 +240,8 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
if (kDebugMode) {
|
||||
print('Stopping OpenBikeControl BLE server...');
|
||||
}
|
||||
await _peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
await _peripheralManager.stopAdvertising();
|
||||
isStarted.value = false;
|
||||
isConnected.value = false;
|
||||
|
||||
39
lib/bluetooth/devices/openbikecontrol/obc_dircon.dart
Normal file
39
lib/bluetooth/devices/openbikecontrol/obc_dircon.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/openbikecontrol_device.dart';
|
||||
import 'package:prop/emulators/dircon/dircon.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
abstract class OnMessage {
|
||||
void onMessage(List<int> message);
|
||||
}
|
||||
|
||||
class ObcDircon extends DirCon {
|
||||
final OnMessage onMessageCallback;
|
||||
ObcDircon({required super.socket, required this.onMessageCallback});
|
||||
|
||||
@override
|
||||
List<BleCharacteristic> getCharacteristics(String serviceUUID) {
|
||||
if (serviceUUID.toLowerCase() == OpenBikeControlConstants.SERVICE_UUID) {
|
||||
return [
|
||||
BleCharacteristic(
|
||||
OpenBikeControlConstants.BUTTON_STATE_CHARACTERISTIC_UUID,
|
||||
[CharacteristicProperty.notify],
|
||||
),
|
||||
BleCharacteristic(
|
||||
OpenBikeControlConstants.APPINFO_CHARACTERISTIC_UUID,
|
||||
[CharacteristicProperty.writeWithoutResponse, CharacteristicProperty.write],
|
||||
),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
void processWriteCallback(String characteristicUUID, List<int> characteristicData) {
|
||||
if (characteristicUUID.toLowerCase() == OpenBikeControlConstants.APPINFO_CHARACTERISTIC_UUID) {
|
||||
onMessageCallback.onMessage(characteristicData);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<String> get serviceUUIDs => [OpenBikeControlConstants.SERVICE_UUID];
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/obc_dircon.dart';
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/openbikecontrol_device.dart';
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/protocol_parser.dart';
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/keymap/apps/supported_app.dart';
|
||||
import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bike_control/utils/keymap/keymap.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
@@ -13,7 +15,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:nsd/nsd.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
|
||||
class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
class OpenBikeControlMdnsEmulator extends TrainerConnection implements OnMessage {
|
||||
ServerSocket? _server;
|
||||
Registration? _mdnsRegistration;
|
||||
|
||||
@@ -22,6 +24,7 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
final ValueNotifier<AppInfo?> connectedApp = ValueNotifier(null);
|
||||
|
||||
Socket? _socket;
|
||||
ObcDircon? _dirCon;
|
||||
|
||||
OpenBikeControlMdnsEmulator()
|
||||
: super(
|
||||
@@ -29,6 +32,9 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
supportedActions: InGameAction.values,
|
||||
);
|
||||
|
||||
bool get _useDirCon =>
|
||||
core.settings.getTrainerApp()?.supportsOpenBikeProtocol.contains(OpenBikeProtocolSupport.dircon) ?? false;
|
||||
|
||||
Future<void> startServer() async {
|
||||
print('Starting mDNS server...');
|
||||
isStarted.value = true;
|
||||
@@ -64,18 +70,23 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
_mdnsRegistration = await register(
|
||||
Service(
|
||||
name: 'BikeControl',
|
||||
type: '_openbikecontrol._tcp',
|
||||
type: _useDirCon ? '_wahoo-fitness-tnp._tcp' : '_openbikecontrol._tcp',
|
||||
port: 36867,
|
||||
//hostName: 'KICKR BIKE SHIFT B84D.local',
|
||||
addresses: [localIP],
|
||||
txt: {
|
||||
'version': Uint8List.fromList([0x01]),
|
||||
'id': Uint8List.fromList('1337'.codeUnits),
|
||||
'name': Uint8List.fromList('BikeControl'.codeUnits),
|
||||
'service-uuids': Uint8List.fromList(OpenBikeControlConstants.SERVICE_UUID.codeUnits),
|
||||
'manufacturer': Uint8List.fromList('OpenBikeControl'.codeUnits),
|
||||
'model': Uint8List.fromList('BikeControl app'.codeUnits),
|
||||
},
|
||||
txt: _useDirCon
|
||||
? {
|
||||
'ble-service-uuids': Uint8List.fromList(OpenBikeControlConstants.SERVICE_UUID.codeUnits),
|
||||
'mac-address': Uint8List.fromList('00:11:22:33:44:55'.codeUnits),
|
||||
'serial-number': Uint8List.fromList('1234567890'.codeUnits),
|
||||
}
|
||||
: {
|
||||
'version': Uint8List.fromList([0x01]),
|
||||
'id': Uint8List.fromList('1337'.codeUnits),
|
||||
'name': Uint8List.fromList('BikeControl'.codeUnits),
|
||||
'service-uuids': Uint8List.fromList(OpenBikeControlConstants.SERVICE_UUID.codeUnits),
|
||||
'manufacturer': Uint8List.fromList('OpenBikeControl'.codeUnits),
|
||||
'model': Uint8List.fromList('BikeControl app'.codeUnits),
|
||||
},
|
||||
),
|
||||
);
|
||||
print('Service: ${_mdnsRegistration!.id} at ${localIP.address}:$_mdnsRegistration');
|
||||
@@ -104,7 +115,7 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
Future<void> _createTcpServer() async {
|
||||
try {
|
||||
_server = await ServerSocket.bind(
|
||||
InternetAddress.anyIPv6,
|
||||
InternetAddress.anyIPv4,
|
||||
36867,
|
||||
shared: true,
|
||||
v6Only: false,
|
||||
@@ -119,40 +130,33 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
|
||||
// Accept connection
|
||||
_server!.listen(
|
||||
(Socket socket) {
|
||||
(Socket socket) async {
|
||||
SharedLogic.keepAlive();
|
||||
_socket = socket;
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Client connected: ${socket.remoteAddress.address}:${socket.remotePort}');
|
||||
}
|
||||
|
||||
if (_useDirCon) {
|
||||
_dirCon = ObcDircon(socket: socket, onMessageCallback: this);
|
||||
}
|
||||
|
||||
// Listen for data from the client
|
||||
socket.listen(
|
||||
(List<int> data) {
|
||||
if (kDebugMode) {
|
||||
print('Received message: ${bytesToHex(data)}');
|
||||
}
|
||||
final messageType = data[0];
|
||||
switch (messageType) {
|
||||
case OpenBikeProtocolParser.MSG_TYPE_APP_INFO:
|
||||
try {
|
||||
final appInfo = OpenBikeProtocolParser.parseAppInfo(Uint8List.fromList(data));
|
||||
isConnected.value = true;
|
||||
connectedApp.value = appInfo;
|
||||
|
||||
supportedActions = appInfo.supportedButtons.mapNotNull((b) => b.action).toList();
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, 'Connected to app: ${appInfo.appId}'),
|
||||
);
|
||||
} catch (e) {
|
||||
core.connection.signalNotification(LogNotification('Failed to parse app info: $e'));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
print('Unknown message type: $messageType');
|
||||
if (_dirCon != null) {
|
||||
_dirCon!.handleIncomingData(data);
|
||||
return;
|
||||
}
|
||||
onMessage(data);
|
||||
},
|
||||
onDone: () {
|
||||
_dirCon = null;
|
||||
SharedLogic.stopKeepAlive();
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, 'Disconnected from app: ${connectedApp.value?.appId}'),
|
||||
);
|
||||
@@ -205,6 +209,40 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
|
||||
void _write(Socket socket, List<int> responseData) {
|
||||
debugPrint('Sending response: ${bytesToHex(responseData)}');
|
||||
socket.add(responseData);
|
||||
if (_dirCon != null) {
|
||||
_dirCon!.sendCharacteristicNotification(OpenBikeControlConstants.BUTTON_STATE_CHARACTERISTIC_UUID, responseData);
|
||||
return;
|
||||
} else {
|
||||
socket.add(responseData);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onMessage(List<int> message) {
|
||||
if (kDebugMode) {
|
||||
print('Received message from OBC: ${bytesToHex(message)}');
|
||||
}
|
||||
final messageType = message[0];
|
||||
switch (messageType) {
|
||||
case OpenBikeProtocolParser.MSG_TYPE_APP_INFO:
|
||||
try {
|
||||
final appInfo = OpenBikeProtocolParser.parseAppInfo(Uint8List.fromList(message));
|
||||
isConnected.value = true;
|
||||
connectedApp.value = appInfo;
|
||||
|
||||
supportedActions = appInfo.supportedButtons.mapNotNull((b) => b.action).toList();
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, 'Connected to app: ${appInfo.appId}'),
|
||||
);
|
||||
} catch (e) {
|
||||
core.connection.signalNotification(LogNotification('Failed to parse app info: $e'));
|
||||
}
|
||||
break;
|
||||
case OpenBikeProtocolParser.MSG_TYPE_HAPTIC_FEEDBACK:
|
||||
// noop
|
||||
break;
|
||||
default:
|
||||
print('Unknown message type: $messageType');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
import '../bluetooth_device.dart';
|
||||
@@ -25,7 +26,7 @@ class ShimanoDi2 extends BluetoothDevice {
|
||||
await UniversalBle.subscribeIndications(device.deviceId, service.uuid, characteristic.uuid);
|
||||
}
|
||||
|
||||
final _lastButtons = <int, int>{};
|
||||
final _lastButtons = <int, ({int value, _Di2State type})>{};
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
@@ -33,6 +34,7 @@ class ShimanoDi2 extends BluetoothDevice {
|
||||
|
||||
@override
|
||||
Future<void> processCharacteristic(String characteristic, Uint8List bytes) async {
|
||||
Logger.info('Received data from $characteristic: ${bytesToReadableHex(bytes)}');
|
||||
if (characteristic.toLowerCase() == ShimanoDi2Constants.D_FLY_CHANNEL_UUID) {
|
||||
final channels = bytes.sublist(1);
|
||||
|
||||
@@ -40,7 +42,7 @@ class ShimanoDi2 extends BluetoothDevice {
|
||||
if (!_isInitialized) {
|
||||
channels.forEachIndexed((int value, int index) {
|
||||
final readableIndex = index + 1;
|
||||
_lastButtons[index] = value;
|
||||
_lastButtons[index] = (value: value, type: _Di2State.released);
|
||||
|
||||
getOrAddButton(
|
||||
'D-Fly Channel $readableIndex',
|
||||
@@ -51,28 +53,61 @@ class ShimanoDi2 extends BluetoothDevice {
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
final clickedButtons = <ControllerButton>[];
|
||||
|
||||
var actualChange = false;
|
||||
channels.forEachIndexed((int value, int index) {
|
||||
final didChange = _lastButtons[index] != value;
|
||||
_lastButtons[index] = value;
|
||||
final didChange = _lastButtons[index]?.value != value;
|
||||
|
||||
final readableIndex = index + 1;
|
||||
|
||||
final button = getOrAddButton(
|
||||
'D-Fly Channel $readableIndex',
|
||||
() => ControllerButton('D-Fly Channel $readableIndex', sourceDeviceId: device.deviceId),
|
||||
);
|
||||
if (didChange) {
|
||||
clickedButtons.add(button);
|
||||
if ((value & 0x10) != 0) {
|
||||
if (_lastButtons[index]?.type == _Di2State.longPress || _lastButtons[index]?.type == _Di2State.keep) {
|
||||
// short press is triggered after long press, until it's released later on
|
||||
_lastButtons[index] = (value: value, type: _Di2State.keep);
|
||||
Logger.info('Button $readableIndex still long pressed');
|
||||
} else {
|
||||
_lastButtons[index] = (value: value, type: _Di2State.shortPress);
|
||||
actualChange = true;
|
||||
Logger.info('Button $readableIndex short pressed');
|
||||
}
|
||||
} else if ((value & 0x20) != 0) {
|
||||
_lastButtons[index] = (value: value, type: _Di2State.longPress);
|
||||
actualChange = true;
|
||||
Logger.info('Button $readableIndex long pressed');
|
||||
} else if ((value & 0x40) != 0) {
|
||||
_lastButtons[index] = (value: value, type: _Di2State.doublePress);
|
||||
actualChange = true;
|
||||
Logger.info('Button $readableIndex double pressed');
|
||||
} else {
|
||||
_lastButtons[index] = (value: value, type: _Di2State.released);
|
||||
actualChange = true;
|
||||
Logger.info('Button $readableIndex released');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (clickedButtons.isNotEmpty) {
|
||||
await handleButtonsClickedWithoutLongPressSupport(clickedButtons);
|
||||
if (actualChange) {
|
||||
final buttonsToTrigger = _lastButtons.entries
|
||||
.where((entry) {
|
||||
final type = entry.value.type;
|
||||
return type != _Di2State.released;
|
||||
})
|
||||
.map((entry) => availableButtons.firstWhere((button) => button.name == 'D-Fly Channel ${entry.key + 1}'))
|
||||
.toList();
|
||||
|
||||
Logger.debug('Buttons to trigger: ${buttonsToTrigger.map((b) => b.name).join(', ')}');
|
||||
handleButtonsClicked(buttonsToTrigger);
|
||||
|
||||
final doublePress = _lastButtons.entries
|
||||
.filter((entry) => entry.value.type == _Di2State.doublePress)
|
||||
.map((entry) => availableButtons.firstWhere((button) => button.name == 'D-Fly Channel ${entry.key + 1}'))
|
||||
.toList();
|
||||
if (doublePress.isNotEmpty) {
|
||||
Logger.debug('Buttons to still trigger: ${doublePress.map((b) => b.name).join(', ')}');
|
||||
handleButtonsClicked(doublePress);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -97,3 +132,11 @@ class ShimanoDi2Constants {
|
||||
|
||||
static const String D_FLY_CHANNEL_UUID = "00002ac2-5348-494d-414e-4f5f424c4500";
|
||||
}
|
||||
|
||||
enum _Di2State {
|
||||
shortPress,
|
||||
longPress,
|
||||
keep,
|
||||
doublePress,
|
||||
released,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bike_control/utils/keymap/keymap.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
import '../bluetooth_device.dart';
|
||||
@@ -38,7 +36,6 @@ class WahooKickrHeadwind extends BluetoothDevice {
|
||||
@override
|
||||
Future<void> processCharacteristic(String characteristic, Uint8List bytes) {
|
||||
// Analyze the received bytes to determine current state
|
||||
actionStreamInternal.add(LogNotification('Received ${bytesToHex(bytes)} from Headwind $characteristic'));
|
||||
if (bytes.length >= 4 && bytes[0] == 0xFD && bytes[1] == 0x01) {
|
||||
final mode = bytes[3];
|
||||
final speed = bytes[2];
|
||||
@@ -83,6 +80,9 @@ class WahooKickrHeadwind extends BluetoothDevice {
|
||||
withoutResponse: true,
|
||||
);
|
||||
_currentMode = HeadwindMode.manual;
|
||||
|
||||
// Small delay to ensure mode change is processed before speed command
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
// Command format: [0x02, speed_value]
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class ZwiftConstants {
|
||||
static const ZWIFT_CUSTOM_SERVICE_UUID = "00000001-19CA-4651-86E5-FA29DCDD09D1";
|
||||
static const ZWIFT_CUSTOM_SERVICE_SHORT_UUID = "0001";
|
||||
static const ZWIFT_RIDE_CUSTOM_SERVICE_UUID = "0000fc82-0000-1000-8000-00805f9b34fb";
|
||||
static const ZWIFT_RIDE_CUSTOM_SERVICE_UUID_SHORT = "fc82";
|
||||
static const ZWIFT_ASYNC_CHARACTERISTIC_UUID = "00000002-19CA-4651-86E5-FA29DCDD09D1";
|
||||
|
||||
@@ -60,6 +60,7 @@ class ZwiftClickV2 extends ZwiftRide {
|
||||
Future<void> setupHandshake() async {
|
||||
if (isUnlocked) {
|
||||
super.setupHandshake();
|
||||
await sendCommandBuffer(Uint8List.fromList([0xFF, 0x04, 0x00]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +128,15 @@ class ZwiftClickV2 extends ZwiftRide {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (kDebugMode)
|
||||
Button(
|
||||
onPressed: () {
|
||||
test();
|
||||
},
|
||||
leading: const Icon(Icons.translate_sharp),
|
||||
style: ButtonStyle.primary(size: ButtonSize.small),
|
||||
child: Text('Reset'),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
@@ -168,6 +178,15 @@ class ZwiftClickV2 extends ZwiftRide {
|
||||
style: ButtonStyle.primary(size: ButtonSize.small),
|
||||
child: Text('Handshake'),
|
||||
),
|
||||
if (kDebugMode)
|
||||
Button(
|
||||
onPressed: () {
|
||||
test();
|
||||
},
|
||||
leading: const Icon(Icons.translate_sharp),
|
||||
style: ButtonStyle.primary(size: ButtonSize.small),
|
||||
child: Text('Reset'),
|
||||
),
|
||||
],
|
||||
),
|
||||
/*else
|
||||
|
||||
@@ -298,6 +298,8 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
}
|
||||
|
||||
Future<void> stopAdvertising() async {
|
||||
await _peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
await _peripheralManager.stopAdvertising();
|
||||
isStarted.value = false;
|
||||
isConnected.value = false;
|
||||
|
||||
@@ -11,9 +11,7 @@ class LogNotification extends BaseNotification {
|
||||
final String message;
|
||||
|
||||
LogNotification(this.message) {
|
||||
if (kDebugMode) {
|
||||
//print('LogNotification: $message');
|
||||
}
|
||||
Logger.debug('LogNotification: $message');
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
418
lib/bluetooth/remote_keyboard_pairing.dart
Normal file
418
lib/bluetooth/remote_keyboard_pairing.dart
Normal file
@@ -0,0 +1,418 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bike_control/utils/requirements/multi.dart';
|
||||
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
|
||||
import '../utils/keymap/keymap.dart';
|
||||
|
||||
class RemoteKeyboardPairing extends TrainerConnection {
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
late final _peripheralManager = PeripheralManager();
|
||||
bool _isLoading = false;
|
||||
bool _isServiceAdded = false;
|
||||
bool _isSubscribedToEvents = false;
|
||||
|
||||
Central? _central;
|
||||
GATTCharacteristic? _inputReport;
|
||||
|
||||
static const String connectionTitle = 'Keyboard Remote Control';
|
||||
|
||||
RemoteKeyboardPairing()
|
||||
: super(
|
||||
title: connectionTitle,
|
||||
supportedActions: InGameAction.values,
|
||||
);
|
||||
|
||||
Future<void> reconnect() async {
|
||||
await _peripheralManager.stopAdvertising();
|
||||
await _peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
startAdvertising().catchError((e) {
|
||||
core.settings.setRemoteControlEnabled(false);
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_WARNING, 'Failed to start Remote Control pairing: $e'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> startAdvertising() async {
|
||||
_isLoading = true;
|
||||
isStarted.value = true;
|
||||
|
||||
_peripheralManager.stateChanged.forEach((state) {
|
||||
print('Peripheral manager state: ${state.state}');
|
||||
});
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
_peripheralManager.connectionStateChanged.forEach((state) {
|
||||
print('Peripheral connection state: ${state.state} of ${state.central.uuid}');
|
||||
if (state.state == ConnectionState.connected) {
|
||||
} else if (state.state == ConnectionState.disconnected) {
|
||||
_central = null;
|
||||
isConnected.value = false;
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, AppLocalizations.current.disconnected),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
final status = await Permission.bluetoothAdvertise.request();
|
||||
if (!status.isGranted) {
|
||||
print('Bluetooth advertise permission not granted');
|
||||
isStarted.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (_peripheralManager.state != BluetoothLowEnergyState.poweredOn && core.settings.getRemoteControlEnabled()) {
|
||||
print('Waiting for peripheral manager to be powered on...');
|
||||
if (core.settings.getLastTarget() == Target.thisDevice) {
|
||||
return;
|
||||
}
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
final inputReport = GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString('2A4D'),
|
||||
permissions: [GATTCharacteristicPermission.read],
|
||||
properties: [GATTCharacteristicProperty.notify, GATTCharacteristicProperty.read],
|
||||
descriptors: [
|
||||
GATTDescriptor.immutable(
|
||||
// Report Reference: ID=1, Type=Input(1)
|
||||
uuid: UUID.fromString('2908'),
|
||||
value: Uint8List.fromList([0x01, 0x01]),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (!_isServiceAdded) {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
|
||||
final reportMapDataAbsolute = Uint8List.fromList([
|
||||
// Keyboard Report (Report ID 1)
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x06, // Usage (Keyboard)
|
||||
0xA1, 0x01, // Collection (Application)
|
||||
0x85, 0x01, // Report ID (1)
|
||||
0x05, 0x07, // Usage Page (Keyboard/Keypad)
|
||||
0x19, 0xE0, // Usage Minimum (Left Control)
|
||||
0x29, 0xE7, // Usage Maximum (Right GUI)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x08, // Report Count (8)
|
||||
0x81, 0x02, // Input (Data,Var,Abs) - Modifier byte
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x81, 0x01, // Input (Const) - Reserved byte
|
||||
0x95, 0x06, // Report Count (6)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x65, // Logical Maximum (101)
|
||||
0x05, 0x07, // Usage Page (Keyboard/Keypad)
|
||||
0x19, 0x00, // Usage Minimum (0)
|
||||
0x29, 0x65, // Usage Maximum (101)
|
||||
0x81, 0x00, // Input (Data,Array) - Key array (6 keys)
|
||||
0xC0, // End Collection
|
||||
]);
|
||||
|
||||
// 1) Build characteristics
|
||||
final hidInfo = GATTCharacteristic.immutable(
|
||||
uuid: UUID.fromString('2A4A'),
|
||||
value: Uint8List.fromList([0x11, 0x01, 0x00, 0x02]),
|
||||
descriptors: [], // HID v1.11, country=0, flags=2
|
||||
);
|
||||
|
||||
final reportMap = GATTCharacteristic.immutable(
|
||||
uuid: UUID.fromString('2A4B'),
|
||||
//properties: [GATTCharacteristicProperty.read],
|
||||
//permissions: [GATTCharacteristicPermission.read],
|
||||
value: reportMapDataAbsolute,
|
||||
descriptors: [
|
||||
GATTDescriptor.immutable(uuid: UUID.fromString('2908'), value: Uint8List.fromList([0x0, 0x0])),
|
||||
],
|
||||
);
|
||||
|
||||
final protocolMode = GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString('2A4E'),
|
||||
properties: [GATTCharacteristicProperty.read, GATTCharacteristicProperty.writeWithoutResponse],
|
||||
permissions: [GATTCharacteristicPermission.read, GATTCharacteristicPermission.write],
|
||||
descriptors: [],
|
||||
);
|
||||
|
||||
final hidControlPoint = GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString('2A4C'),
|
||||
properties: [GATTCharacteristicProperty.writeWithoutResponse],
|
||||
permissions: [GATTCharacteristicPermission.write],
|
||||
descriptors: [],
|
||||
);
|
||||
|
||||
// 2) HID service
|
||||
final hidService = GATTService(
|
||||
uuid: UUID.fromString(Platform.isIOS ? '1812' : '00001812-0000-1000-8000-00805F9B34FB'),
|
||||
isPrimary: true,
|
||||
characteristics: [
|
||||
hidInfo,
|
||||
reportMap,
|
||||
protocolMode,
|
||||
hidControlPoint,
|
||||
inputReport,
|
||||
],
|
||||
includedServices: [],
|
||||
);
|
||||
|
||||
if (!_isSubscribedToEvents) {
|
||||
_isSubscribedToEvents = true;
|
||||
_peripheralManager.characteristicReadRequested.forEach((char) {
|
||||
print('Read request for characteristic: ${char}');
|
||||
// You can respond to read requests here if needed
|
||||
});
|
||||
|
||||
_peripheralManager.characteristicNotifyStateChanged.forEach((char) {
|
||||
// Check if this is the input report characteristic (2A4D)
|
||||
if (char.characteristic.uuid == inputReport.uuid) {
|
||||
if (char.state) {
|
||||
_central = char.central;
|
||||
_inputReport = char.characteristic;
|
||||
isConnected.value = true;
|
||||
print('Input report subscribed');
|
||||
} else {
|
||||
_inputReport = null;
|
||||
_central = null;
|
||||
isConnected.value = false;
|
||||
print('Input report unsubscribed');
|
||||
}
|
||||
}
|
||||
print('Notify state changed for characteristic: ${char.characteristic.uuid}: ${char.state}');
|
||||
});
|
||||
}
|
||||
await _peripheralManager.addService(hidService);
|
||||
|
||||
// 3) Optional Battery service
|
||||
await _peripheralManager.addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString('180F'),
|
||||
isPrimary: true,
|
||||
characteristics: [
|
||||
GATTCharacteristic.immutable(
|
||||
uuid: UUID.fromString('2A19'),
|
||||
value: Uint8List.fromList([100]),
|
||||
descriptors: [],
|
||||
),
|
||||
],
|
||||
includedServices: [],
|
||||
),
|
||||
);
|
||||
_isServiceAdded = true;
|
||||
}
|
||||
|
||||
final advertisement = Advertisement(
|
||||
name:
|
||||
'BikeControl ${Platform.isIOS
|
||||
? 'iOS'
|
||||
: Platform.isAndroid
|
||||
? 'Android'
|
||||
: ''}',
|
||||
serviceUUIDs: [UUID.fromString(Platform.isIOS ? '1812' : '00001812-0000-1000-8000-00805F9B34FB')],
|
||||
);
|
||||
print('Starting advertising with Remote service...');
|
||||
|
||||
try {
|
||||
await _peripheralManager.startAdvertising(advertisement);
|
||||
} catch (e) {
|
||||
if (e.toString().contains("Advertising has already started")) {
|
||||
print('Advertising already started, ignoring error');
|
||||
return;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
Future<void> stopAdvertising() async {
|
||||
await _peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
await _peripheralManager.stopAdvertising();
|
||||
isStarted.value = false;
|
||||
isConnected.value = false;
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
Future<void> notifyCharacteristic(Uint8List value) async {
|
||||
if (_inputReport != null && _central != null) {
|
||||
await _peripheralManager.notifyCharacteristic(_central!, _inputReport!, value: value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActionResult> sendAction(KeyPair keyPair, {required bool isKeyDown, required bool isKeyUp}) async {
|
||||
if (isKeyDown && isKeyUp) {
|
||||
await sendKeyPress(keyPair);
|
||||
return Success('Key ${keyPair.toString()} press sent');
|
||||
} else if (isKeyDown) {
|
||||
await sendKeyDown(keyPair);
|
||||
return Success('Key ${keyPair.toString()} down sent');
|
||||
} else if (isKeyUp) {
|
||||
await sendKeyUp();
|
||||
return Success('Key ${keyPair.toString()} up sent');
|
||||
}
|
||||
return NotHandled('Illegal combination');
|
||||
}
|
||||
|
||||
/// USB HID Keyboard scan codes for common keys
|
||||
static const Map<String, int> hidKeyCodes = {
|
||||
'a': 0x04,
|
||||
'b': 0x05,
|
||||
'c': 0x06,
|
||||
'd': 0x07,
|
||||
'e': 0x08,
|
||||
'f': 0x09,
|
||||
'g': 0x0A,
|
||||
'h': 0x0B,
|
||||
'i': 0x0C,
|
||||
'j': 0x0D,
|
||||
'k': 0x0E,
|
||||
'l': 0x0F,
|
||||
'm': 0x10,
|
||||
'n': 0x11,
|
||||
'o': 0x12,
|
||||
'p': 0x13,
|
||||
'q': 0x14,
|
||||
'r': 0x15,
|
||||
's': 0x16,
|
||||
't': 0x17,
|
||||
'u': 0x18,
|
||||
'v': 0x19,
|
||||
'w': 0x1A,
|
||||
'x': 0x1B,
|
||||
'y': 0x1C,
|
||||
'z': 0x1D,
|
||||
'1': 0x1E,
|
||||
'2': 0x1F,
|
||||
'3': 0x20,
|
||||
'4': 0x21,
|
||||
'5': 0x22,
|
||||
'6': 0x23,
|
||||
'7': 0x24,
|
||||
'8': 0x25,
|
||||
'9': 0x26,
|
||||
'0': 0x27,
|
||||
'enter': 0x28,
|
||||
'escape': 0x29,
|
||||
'backspace': 0x2A,
|
||||
'tab': 0x2B,
|
||||
'space': 0x2C,
|
||||
'minus': 0x2D,
|
||||
'equals': 0x2E,
|
||||
'leftbracket': 0x2F,
|
||||
'rightbracket': 0x30,
|
||||
'backslash': 0x31,
|
||||
'semicolon': 0x33,
|
||||
'quote': 0x34,
|
||||
'grave': 0x35,
|
||||
'comma': 0x36,
|
||||
'period': 0x37,
|
||||
'slash': 0x38,
|
||||
'capslock': 0x39,
|
||||
'f1': 0x3A,
|
||||
'f2': 0x3B,
|
||||
'f3': 0x3C,
|
||||
'f4': 0x3D,
|
||||
'f5': 0x3E,
|
||||
'f6': 0x3F,
|
||||
'f7': 0x40,
|
||||
'f8': 0x41,
|
||||
'f9': 0x42,
|
||||
'f10': 0x43,
|
||||
'f11': 0x44,
|
||||
'f12': 0x45,
|
||||
'printscreen': 0x46,
|
||||
'scrolllock': 0x47,
|
||||
'pause': 0x48,
|
||||
'insert': 0x49,
|
||||
'home': 0x4A,
|
||||
'pageup': 0x4B,
|
||||
'delete': 0x4C,
|
||||
'end': 0x4D,
|
||||
'pagedown': 0x4E,
|
||||
'right': 0x4F,
|
||||
'left': 0x50,
|
||||
'down': 0x51,
|
||||
'up': 0x52,
|
||||
};
|
||||
|
||||
/// Modifier key bit masks
|
||||
static const int modLeftCtrl = 0x01;
|
||||
static const int modLeftShift = 0x02;
|
||||
static const int modLeftAlt = 0x04;
|
||||
static const int modLeftGui = 0x08;
|
||||
static const int modRightCtrl = 0x10;
|
||||
static const int modRightShift = 0x20;
|
||||
static const int modRightAlt = 0x40;
|
||||
static const int modRightGui = 0x80;
|
||||
|
||||
/// Create a keyboard HID report
|
||||
/// [modifiers] - bit mask for modifier keys (Ctrl, Shift, Alt, GUI)
|
||||
/// [keyCodes] - list of up to 6 key codes to send
|
||||
Uint8List keyboardReport(int modifiers, List<int> keyCodes) {
|
||||
final keys = List<int>.filled(6, 0);
|
||||
for (var i = 0; i < keyCodes.length && i < 6; i++) {
|
||||
keys[i] = keyCodes[i];
|
||||
}
|
||||
// Report format: [modifiers, reserved, key1, key2, key3, key4, key5, key6]
|
||||
return Uint8List.fromList([modifiers, 0x00, ...keys]);
|
||||
}
|
||||
|
||||
/// Send a keyboard key press and release
|
||||
/// [key] - the key name (e.g., 'a', 'enter', 'space', 'f1', 'up', 'down')
|
||||
/// [modifiers] - optional modifier keys (use modLeftCtrl, modLeftShift, etc.)
|
||||
Future<void> sendKeyPress(KeyPair keyPair, {int modifiers = 0}) async {
|
||||
final usbHidUsage = keyPair.physicalKey!.usbHidUsage;
|
||||
final keyCode = usbHidUsage & 0xFF;
|
||||
|
||||
// Send key down
|
||||
final downReport = keyboardReport(modifiers, [keyCode]);
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
'Sending keyboard key down: $keyPair (0x${keyCode.toRadixString(16)}) with modifiers: 0x${modifiers.toRadixString(16)}',
|
||||
);
|
||||
}
|
||||
await notifyCharacteristic(downReport);
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 20));
|
||||
|
||||
// Send key up (empty report)
|
||||
final upReport = keyboardReport(0, []);
|
||||
if (kDebugMode) {
|
||||
print('Sending keyboard key up');
|
||||
}
|
||||
await notifyCharacteristic(upReport);
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
|
||||
/// Send a key down event only (for holding keys)
|
||||
Future<void> sendKeyDown(KeyPair keyPair, {int modifiers = 0}) async {
|
||||
final usbHidUsage = keyPair.physicalKey!.usbHidUsage;
|
||||
final keyCode = usbHidUsage & 0xFF;
|
||||
|
||||
final report = keyboardReport(modifiers, [keyCode]);
|
||||
await notifyCharacteristic(report);
|
||||
}
|
||||
|
||||
/// Send a key up event (release all keys)
|
||||
Future<void> sendKeyUp() async {
|
||||
final report = keyboardReport(0, []);
|
||||
await notifyCharacteristic(report);
|
||||
}
|
||||
}
|
||||
@@ -159,37 +159,6 @@ class RemotePairing extends TrainerConnection {
|
||||
descriptors: [],
|
||||
);
|
||||
|
||||
// Input report characteristic (notify)
|
||||
final keyboardInputReport = GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString('2A4D'),
|
||||
permissions: [GATTCharacteristicPermission.read],
|
||||
properties: [GATTCharacteristicProperty.notify, GATTCharacteristicProperty.read],
|
||||
descriptors: [
|
||||
GATTDescriptor.immutable(
|
||||
// Report Reference: ID=1, Type=Input(1)
|
||||
uuid: UUID.fromString('2908'),
|
||||
value: Uint8List.fromList([0x02, 0x01]),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final outputReport = GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString('2A4D'),
|
||||
permissions: [GATTCharacteristicPermission.read, GATTCharacteristicPermission.write],
|
||||
properties: [
|
||||
GATTCharacteristicProperty.read,
|
||||
GATTCharacteristicProperty.write,
|
||||
GATTCharacteristicProperty.writeWithoutResponse,
|
||||
],
|
||||
descriptors: [
|
||||
GATTDescriptor.immutable(
|
||||
// Report Reference: ID=1, Type=Input(1)
|
||||
uuid: UUID.fromString('2908'),
|
||||
value: Uint8List.fromList([0x02, 0x02]),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// 2) HID service
|
||||
final hidService = GATTService(
|
||||
uuid: UUID.fromString(Platform.isIOS ? '1812' : '00001812-0000-1000-8000-00805F9B34FB'),
|
||||
@@ -198,9 +167,7 @@ class RemotePairing extends TrainerConnection {
|
||||
hidInfo,
|
||||
reportMap,
|
||||
protocolMode,
|
||||
outputReport,
|
||||
hidControlPoint,
|
||||
keyboardInputReport,
|
||||
inputReport,
|
||||
],
|
||||
includedServices: [],
|
||||
@@ -214,18 +181,21 @@ class RemotePairing extends TrainerConnection {
|
||||
});
|
||||
|
||||
_peripheralManager.characteristicNotifyStateChanged.forEach((char) {
|
||||
// Check if this is the input report characteristic (2A4D)
|
||||
if (char.characteristic.uuid == inputReport.uuid) {
|
||||
if (char.state) {
|
||||
_inputReport = char.characteristic;
|
||||
_central = char.central;
|
||||
_inputReport = char.characteristic;
|
||||
isConnected.value = true;
|
||||
print('Input report subscribed');
|
||||
} else {
|
||||
_inputReport = null;
|
||||
_central = null;
|
||||
isConnected.value = false;
|
||||
print('Input report unsubscribed');
|
||||
}
|
||||
}
|
||||
print(
|
||||
'Notify state changed for characteristic: ${char.characteristic.uuid} vs ${char.characteristic.uuid == inputReport.uuid}: ${char.state}',
|
||||
);
|
||||
print('Notify state changed for characteristic: ${char.characteristic.uuid}: ${char.state}');
|
||||
});
|
||||
}
|
||||
await _peripheralManager.addService(hidService);
|
||||
@@ -259,11 +229,22 @@ class RemotePairing extends TrainerConnection {
|
||||
);
|
||||
print('Starting advertising with Remote service...');
|
||||
|
||||
await _peripheralManager.startAdvertising(advertisement);
|
||||
try {
|
||||
await _peripheralManager.startAdvertising(advertisement);
|
||||
} catch (e) {
|
||||
if (e.toString().contains("Advertising has already started")) {
|
||||
print('Advertising already started, ignoring error');
|
||||
return;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
Future<void> stopAdvertising() async {
|
||||
await _peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
await _peripheralManager.stopAdvertising();
|
||||
isStarted.value = false;
|
||||
isConnected.value = false;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"accessibilityUsageMonitor": "• Die App überwacht, welches Trainings-App-Fenster aktiv ist, um sicherzustellen, dass Gesten an die richtige App gesendet werden.",
|
||||
"accessibilityUsageNoData": "• Über diesen Dienst werden keine personenbezogenen Daten abgerufen oder erfasst.",
|
||||
"accessories": "Zubehör",
|
||||
"actAsBluetoothKeyboard": "Als Bluetooth-Tastatur fungieren",
|
||||
"action": "Aktion",
|
||||
"adjustControllerButtons": "Controller-Tasten anpassen",
|
||||
"afterDate": "Nach dem {date}",
|
||||
@@ -36,6 +37,7 @@
|
||||
"battery": "Batterie",
|
||||
"beforeDate": "Vor dem {date}",
|
||||
"bluetoothAdvertiseAccess": "Bluetooth-Zugriff",
|
||||
"bluetoothKeyboardExplanation": "So können Sie Ihr Smartphone als Bluetooth-Tastatur zur Steuerung kompatibler Apps verwenden. Nach der Kopplung können Sie die Tasten Ihres Fahrradcontrollers nutzen, um Tastatureingaben an die App zu senden.",
|
||||
"bluetoothTurnedOn": "Bluetooth ist eingeschaltet",
|
||||
"browserNotSupported": "Dieser Browser unterstützt kein Web-Bluetooth und die Plattform wird nicht unterstützt :(",
|
||||
"button": "Taste.",
|
||||
@@ -153,7 +155,7 @@
|
||||
"enableLocalConnectionMethodFirst": "Aktivieren Sie zuerst die Methode „Lokale Verbindung“.",
|
||||
"enableMediaKeyDetection": "Medientastenerkennung aktivieren",
|
||||
"enableMywhooshLinkInTheConnectionSettingsFirst": "Aktiviere zuerst MyWhoosh Link in den Verbindungseinstellungen.",
|
||||
"enablePairingProcess": "Kopplungsprozess aktivieren",
|
||||
"enablePairingProcess": "Als Bluetooth-Maus fungieren",
|
||||
"enablePermissions": "Berechtigungen aktivieren",
|
||||
"enableSteeringWithPhone": "Lenkung über Handy-Sensoren aktivieren",
|
||||
"enableVibrationFeedback": "Vibrationsfeedback beim Gangwechsel aktivieren",
|
||||
@@ -279,7 +281,7 @@
|
||||
"openBikeControlAnnouncement": "Tolle Neuigkeiten! {trainerApp} unterstützt das OpenBikeControl-Protokoll für eine bestmögliche Benutzererfahrung.",
|
||||
"openBikeControlConnection": " z. B. durch Verwendung einer OpenBikeControl-Verbindung",
|
||||
"otherConnectionMethods": "Andere Verbindungsmethoden",
|
||||
"pairingDescription": "Die Kopplung ermöglicht volle Anpassungsmöglichkeiten, funktioniert aber möglicherweise nicht auf allen Geräten.",
|
||||
"pairingDescription": "So können Sie Ihr Smartphone als Bluetooth-Maus zur Steuerung von Apps verwenden. Nach der Kopplung können Sie die Tasten Ihres Fahrradcontrollers nutzen, um Mausbewegungen an die App zu senden.",
|
||||
"pairingInstructions": "Gehe auf Deinem {targetName} in die Bluetooth-Einstellungen und suche nach BikeControl oder dem Namen Ihres Geräts. Wenn Du die Fernbedienungsfunktion nutzen möchtest, ist eine Kopplung erforderlich.",
|
||||
"@pairingInstructions": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"accessibilityUsageMonitor": "• The app monitors which training app window is active to ensure gestures are sent to the correct app",
|
||||
"accessibilityUsageNoData": "• No personal data is accessed or collected through this service",
|
||||
"accessories": "Accessories",
|
||||
"actAsBluetoothKeyboard": "Act as Bluetooth Keyboard",
|
||||
"action": "Action",
|
||||
"adjustControllerButtons": "Adjust Controller Buttons",
|
||||
"afterDate": "After {date}",
|
||||
@@ -36,6 +37,7 @@
|
||||
"battery": "Battery",
|
||||
"beforeDate": "Before {date}",
|
||||
"bluetoothAdvertiseAccess": "Bluetooth Advertise access",
|
||||
"bluetoothKeyboardExplanation": "This will allow you to use your phone as a Bluetooth keyboard to control compatible apps. Once paired, you can use the buttons on your bike controller to send keyboard inputs to the app.",
|
||||
"bluetoothTurnedOn": "Bluetooth turned on",
|
||||
"browserNotSupported": "This Browser does not support Web Bluetooth and platform is not supported :(",
|
||||
"button": "button.",
|
||||
@@ -153,7 +155,7 @@
|
||||
"enableLocalConnectionMethodFirst": "Enable Local Connection method, first.",
|
||||
"enableMediaKeyDetection": "Enable Media Key Detection",
|
||||
"enableMywhooshLinkInTheConnectionSettingsFirst": "Enable MyWhoosh Link in the connection settings first.",
|
||||
"enablePairingProcess": "Enable Pairing Process",
|
||||
"enablePairingProcess": "Act as Bluetooth Mouse",
|
||||
"enablePermissions": "Enable Permissions",
|
||||
"enableSteeringWithPhone": "Enable Steering with your phone's sensors",
|
||||
"enableVibrationFeedback": "Enable vibration feedback when shifting gears",
|
||||
@@ -279,7 +281,7 @@
|
||||
"openBikeControlAnnouncement": "Great news - {trainerApp} supports the OpenBikeControl Protocol, so you'll have the best possible experience!",
|
||||
"openBikeControlConnection": " e.g. by using OpenBikeControl connection",
|
||||
"otherConnectionMethods": "Other Connection Methods",
|
||||
"pairingDescription": "Pairing allows full customizability, but may not work on all devices.",
|
||||
"pairingDescription": "This will allow you to use your phone as a Bluetooth mouse to control apps. Once paired, you can use the buttons on your bike controller to send mouse inputs to the app.",
|
||||
"pairingInstructions": "On your {targetName} go into Bluetooth settings and look for BikeControl or your machines name. Pairing is required if you want to use the remote control feature.",
|
||||
"@pairingInstructions": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"accessibilityUsageMonitor": "• L'application surveille quelle fenêtre de l'application d'entraînement est active afin de s'assurer que les gestes sont envoyés à la bonne application.",
|
||||
"accessibilityUsageNoData": "• Aucune donnée personnelle n'est consultée ou collectée par le biais de ce service.",
|
||||
"accessories": "Accessoires",
|
||||
"actAsBluetoothKeyboard": "Fonctionne comme un clavier Bluetooth",
|
||||
"action": "Action",
|
||||
"adjustControllerButtons": "Ajuster les boutons de la manette",
|
||||
"afterDate": "Après {date}",
|
||||
@@ -36,6 +37,7 @@
|
||||
"battery": "Batterie",
|
||||
"beforeDate": "Avant {date}",
|
||||
"bluetoothAdvertiseAccess": "Accès à la publicité Bluetooth",
|
||||
"bluetoothKeyboardExplanation": "Vous pourrez ainsi utiliser votre téléphone comme clavier Bluetooth pour contrôler les applications compatibles. Une fois l'appairage effectué, vous pourrez utiliser les boutons de votre contrôleur de vélo pour envoyer des commandes à l'application.",
|
||||
"bluetoothTurnedOn": "Bluetooth activé",
|
||||
"browserNotSupported": "Ce navigateur ne prend pas en charge Web Bluetooth et la plateforme n'est pas prise en charge :(",
|
||||
"button": "bouton.",
|
||||
@@ -153,7 +155,7 @@
|
||||
"enableLocalConnectionMethodFirst": "Activez d'abord le mode de connexion locale.",
|
||||
"enableMediaKeyDetection": "Activer la détection des touches multimédias",
|
||||
"enableMywhooshLinkInTheConnectionSettingsFirst": "Activez d'abord MyWhoosh Link dans les paramètres de connexion.",
|
||||
"enablePairingProcess": "Activer le processus d'appairage",
|
||||
"enablePairingProcess": "Fonctionne comme une souris Bluetooth",
|
||||
"enablePermissions": "Activer les autorisations",
|
||||
"enableSteeringWithPhone": "Activez la direction avec les capteurs de votre téléphone",
|
||||
"enableVibrationFeedback": "Activer le retour haptique par vibration lors du changement de vitesse",
|
||||
@@ -279,7 +281,7 @@
|
||||
"openBikeControlAnnouncement": "Excellente nouvelle! {trainerApp} Il prend en charge le protocole OpenBikeControl, vous bénéficierez donc de la meilleure expérience possible !",
|
||||
"openBikeControlConnection": " par exemple en utilisant la connexion OpenBikeControl",
|
||||
"otherConnectionMethods": "Autres méthodes de connexion",
|
||||
"pairingDescription": "Le jumelage permet une personnalisation complète, mais peut ne pas fonctionner sur tous les appareils.",
|
||||
"pairingDescription": "Cela vous permettra d'utiliser votre téléphone comme une souris Bluetooth pour contrôler des applications. Une fois l'appairage effectué, vous pourrez utiliser les boutons de votre contrôleur de vélo pour envoyer des commandes de souris à l'application.",
|
||||
"pairingInstructions": "Sur votre {targetName}, accédez aux paramètres Bluetooth et recherchez BikeControl ou le nom de votre appareil. L'appairage est nécessaire si vous souhaitez utiliser la fonction de commande à distance.",
|
||||
"@pairingInstructions": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"accessibilityUsageMonitor": "• L'applicazione monitora quale finestra dell'app di allenamento è attiva per garantire che i gesti vengano inviati all'app corretta",
|
||||
"accessibilityUsageNoData": "• Nessun dato personale viene raccolto o consultato tramite questo servizio",
|
||||
"accessories": "Accessori",
|
||||
"actAsBluetoothKeyboard": "Funziona come tastiera Bluetooth",
|
||||
"action": "Azione",
|
||||
"adjustControllerButtons": "Regola i pulsanti del controller",
|
||||
"afterDate": "Dopo il {date}",
|
||||
@@ -36,6 +37,7 @@
|
||||
"battery": "Batteria",
|
||||
"beforeDate": "Prima del {date}",
|
||||
"bluetoothAdvertiseAccess": "Accesso pubblicitario Bluetooth",
|
||||
"bluetoothKeyboardExplanation": "Questo ti permetterà di usare il tuo telefono come tastiera Bluetooth per controllare le app compatibili. Una volta associato, potrai usare i pulsanti del controller della tua bici per inviare input dalla tastiera all'app.",
|
||||
"bluetoothTurnedOn": "Bluetooth attivato",
|
||||
"browserNotSupported": "Questo browser non supporta il Web Bluetooth e la piattaforma non è supportata :(",
|
||||
"button": "pulsante.",
|
||||
@@ -153,7 +155,7 @@
|
||||
"enableLocalConnectionMethodFirst": "Abilitare prima il metodo di connessione locale.",
|
||||
"enableMediaKeyDetection": "Abilita rilevamento tasti multimediali",
|
||||
"enableMywhooshLinkInTheConnectionSettingsFirst": "Per prima cosa, abilita MyWhoosh Link nelle impostazioni di connessione.",
|
||||
"enablePairingProcess": "Abilita processo di associazione",
|
||||
"enablePairingProcess": "Funziona come un mouse Bluetooth",
|
||||
"enablePermissions": "Abilita autorizzazioni",
|
||||
"enableSteeringWithPhone": "Abilita lo sterzo con i sensori del tuo telefono",
|
||||
"enableVibrationFeedback": "Abilita il feedback delle vibrazioni durante il cambio marcia",
|
||||
@@ -279,7 +281,7 @@
|
||||
"openBikeControlAnnouncement": "Ottime notizie -{trainerApp} supporta il protocollo OpenBikeControl, così avrai la migliore esperienza possibile!",
|
||||
"openBikeControlConnection": " ad esempio utilizzando la connessione OpenBikeControl",
|
||||
"otherConnectionMethods": "Altri metodi di connessione",
|
||||
"pairingDescription": "L'associazione consente la personalizzazione completa, ma potrebbe non funzionare su tutti i dispositivi.",
|
||||
"pairingDescription": "Questo ti permetterà di usare il tuo telefono come mouse Bluetooth per controllare le app. Una volta associato, potrai usare i pulsanti del controller della tua bici per inviare input del mouse all'app.",
|
||||
"pairingInstructions": "Sul tuo{targetName} Accedi alle impostazioni Bluetooth e cerca BikeControl o il nome del tuo dispositivo. L'associazione è necessaria se vuoi utilizzare la funzione di controllo remoto.",
|
||||
"@pairingInstructions": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"accessibilityUsageMonitor": "• Aplikacja monitoruje, które okno aplikacji treningowej jest aktywne, aby zapewnić, że gesty są wysyłane do właściwej aplikacji",
|
||||
"accessibilityUsageNoData": "• Ta usługa nie uzyskuje dostępu do danych osobowych, ani ich nie gromadzi",
|
||||
"accessories": "Akcesoria",
|
||||
"actAsBluetoothKeyboard": "Działa jako klawiatura Bluetooth",
|
||||
"action": "Działanie",
|
||||
"adjustControllerButtons": "Dostosuj przyciski kontrolera",
|
||||
"afterDate": "Po {date}",
|
||||
@@ -36,6 +37,7 @@
|
||||
"battery": "Bateria",
|
||||
"beforeDate": "Zanim {date}",
|
||||
"bluetoothAdvertiseAccess": "Dostęp do reklamy Bluetooth",
|
||||
"bluetoothKeyboardExplanation": "Umożliwi to używanie telefonu jako klawiatury Bluetooth do sterowania kompatybilnymi aplikacjami. Po sparowaniu możesz używać przycisków na kontrolerze rowerowym do wysyłania poleceń z klawiatury do aplikacji.",
|
||||
"bluetoothTurnedOn": "Włączono Bluetooth",
|
||||
"browserNotSupported": "Ta przeglądarka nie obsługuje technologii Web Bluetooth i platforma nie jest obsługiwana :(",
|
||||
"button": "przycisk.",
|
||||
@@ -153,7 +155,7 @@
|
||||
"enableLocalConnectionMethodFirst": "Najpierw włącz metodę połączenia lokalnego.",
|
||||
"enableMediaKeyDetection": "Włącz rozpoznawanie klawiszy multimedialnych",
|
||||
"enableMywhooshLinkInTheConnectionSettingsFirst": "Najpierw włącz MyWhoosh Link w ustawieniach połączenia.",
|
||||
"enablePairingProcess": "Włącz proces parowania",
|
||||
"enablePairingProcess": "Działa jako mysz Bluetooth",
|
||||
"enablePermissions": "Nadaj uprawnienia",
|
||||
"enableSteeringWithPhone": "Włącz sterowanie za pomocą czujników telefonu",
|
||||
"enableVibrationFeedback": "Włącz wibracje podczas zmiany biegów",
|
||||
@@ -279,7 +281,7 @@
|
||||
"openBikeControlAnnouncement": "Świetna wiadomość - {trainerApp} obsługuje protokół OpenBikeControl, dzięki czemu uzyskasz najlepsze doświadczenia!",
|
||||
"openBikeControlConnection": " np. za pomocą połączenia OpenBikeControl",
|
||||
"otherConnectionMethods": "Inne metody połączenia",
|
||||
"pairingDescription": "Parowanie umożliwia pełną personalizację, jednak może nie działać na wszystkich urządzeniach.",
|
||||
"pairingDescription": "Umożliwi to używanie telefonu jako myszy Bluetooth do sterowania aplikacjami. Po sparowaniu możesz używać przycisków na kontrolerze rowerowym do wysyłania poleceń myszy do aplikacji.",
|
||||
"pairingInstructions": "Przejdź do ustawień Bluetooth na twoim {targetName} i wyszukaj BikeControl lub nazwę swojego urządzenia. Parowanie jest wymagane, jeśli chcesz korzystać z funkcji zdalnego sterowania.",
|
||||
"@pairingInstructions": {
|
||||
"placeholders": {
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/pages/touch_area.dart';
|
||||
import 'package:bike_control/utils/actions/android.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
@@ -18,6 +17,7 @@ import 'package:bike_control/widgets/ui/colors.dart';
|
||||
import 'package:bike_control/widgets/ui/toast.dart';
|
||||
import 'package:bike_control/widgets/ui/warning.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
@@ -180,14 +180,17 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
|
||||
SizedBox(height: 8),
|
||||
ColoredTitle(text: 'Local / Remote Setting'),
|
||||
|
||||
if (core.actionHandler.supportedModes.contains(SupportedMode.keyboard))
|
||||
if (core.actionHandler.supportedModes.contains(SupportedMode.keyboard) &&
|
||||
(core.settings.getLocalEnabled() || core.settings.getRemoteKeyboardControlEnabled()))
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return SelectableCard(
|
||||
icon: RadixIcons.keyboard,
|
||||
title: Text(context.i18n.simulateKeyboardShortcut),
|
||||
isActive:
|
||||
_keyPair.physicalKey != null && !_keyPair.isSpecialKey && core.settings.getLocalEnabled(),
|
||||
_keyPair.physicalKey != null &&
|
||||
!_keyPair.isSpecialKey &&
|
||||
(core.settings.getLocalEnabled() || core.settings.getRemoteKeyboardControlEnabled()),
|
||||
value: _keyPair.toString(),
|
||||
onPressed: () async {
|
||||
await _showModeDropdown(context, SupportedMode.keyboard);
|
||||
@@ -195,7 +198,8 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
if (core.actionHandler.supportedModes.contains(SupportedMode.touch))
|
||||
if (core.actionHandler.supportedModes.contains(SupportedMode.touch) &&
|
||||
(core.settings.getLocalEnabled() || core.settings.getRemoteControlEnabled()))
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return SelectableCard(
|
||||
@@ -204,7 +208,7 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
|
||||
isActive:
|
||||
((core.actionHandler is AndroidActions || _keyPair.physicalKey == null) &&
|
||||
_keyPair.touchPosition != Offset.zero) &&
|
||||
core.settings.getLocalEnabled(),
|
||||
(core.settings.getLocalEnabled() || core.settings.getRemoteControlEnabled()),
|
||||
value: _keyPair.toString(),
|
||||
trailing: IconButton.secondary(
|
||||
icon: Icon(Icons.ondemand_video),
|
||||
@@ -235,7 +239,6 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
|
||||
onPressed: () {
|
||||
if (!core.settings.getLocalEnabled()) {
|
||||
buildToast(
|
||||
navigatorKey.currentContext!,
|
||||
title: AppLocalizations.of(context).enableLocalConnectionMethodFirst,
|
||||
);
|
||||
} else {
|
||||
@@ -343,7 +346,7 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
|
||||
),
|
||||
onPressed: () {
|
||||
if (!core.settings.getLocalEnabled()) {
|
||||
buildToast(navigatorKey.currentContext!, title: 'Enable Local Connection method, first.');
|
||||
buildToast(title: 'Enable Local Connection method, first.');
|
||||
} else {
|
||||
showDropdown(
|
||||
context: context,
|
||||
@@ -495,6 +498,7 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
|
||||
_keyPair.androidAction = null;
|
||||
_keyPair.inGameAction = action;
|
||||
_keyPair.inGameActionValue = ingame;
|
||||
_keyPair.isLongPress = _keyPair.isLongPress ? true : action.isLongPress;
|
||||
widget.onUpdate();
|
||||
setState(() {});
|
||||
},
|
||||
@@ -510,6 +514,7 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
|
||||
_keyPair.androidAction = null;
|
||||
_keyPair.inGameAction = action;
|
||||
_keyPair.inGameActionValue = null;
|
||||
_keyPair.isLongPress = _keyPair.isLongPress ? true : action.isLongPress;
|
||||
widget.onUpdate();
|
||||
setState(() {});
|
||||
}
|
||||
@@ -536,9 +541,15 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
|
||||
.distinctBy((kp) => kp.inGameAction)
|
||||
.toList();
|
||||
|
||||
if (!core.settings.getLocalEnabled()) {
|
||||
final isEnabled =
|
||||
supportedMode == SupportedMode.keyboard &&
|
||||
(core.settings.getLocalEnabled() || core.settings.getRemoteKeyboardControlEnabled()) ||
|
||||
supportedMode == SupportedMode.touch &&
|
||||
(core.settings.getLocalEnabled() || core.settings.getRemoteControlEnabled()) ||
|
||||
supportedMode == SupportedMode.media && core.settings.getLocalEnabled();
|
||||
|
||||
if (!isEnabled) {
|
||||
return buildToast(
|
||||
navigatorKey.currentContext!,
|
||||
title: AppLocalizations.of(context).enableLocalConnectionMethodFirst,
|
||||
);
|
||||
} else if (actionsWithInGameAction != null && actionsWithInGameAction.isNotEmpty) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:bike_control/bluetooth/devices/openbikecontrol/obc_mdns_emulator
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/ftms_mdns_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/zwift_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/remote_keyboard_pairing.dart';
|
||||
import 'package:bike_control/bluetooth/remote_pairing.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/pages/touch_area.dart';
|
||||
@@ -22,7 +23,8 @@ import 'package:bike_control/widgets/apps/openbikecontrol_ble_tile.dart';
|
||||
import 'package:bike_control/widgets/apps/openbikecontrol_mdns_tile.dart';
|
||||
import 'package:bike_control/widgets/apps/zwift_mdns_tile.dart';
|
||||
import 'package:bike_control/widgets/apps/zwift_tile.dart';
|
||||
import 'package:bike_control/widgets/pair_widget.dart';
|
||||
import 'package:bike_control/widgets/keyboard_pair_widget.dart';
|
||||
import 'package:bike_control/widgets/mouse_pair_widget.dart';
|
||||
import 'package:bike_control/widgets/ui/gradient_text.dart';
|
||||
import 'package:bike_control/widgets/ui/toast.dart';
|
||||
import 'package:bike_control/widgets/ui/warning.dart';
|
||||
@@ -172,7 +174,7 @@ class _ButtonSimulatorState extends State<ButtonSimulator> {
|
||||
} else {
|
||||
_pressedAction = null;
|
||||
setState(() {});
|
||||
buildToast(context, title: 'No connected trainer.');
|
||||
buildToast(title: 'No connected trainer.');
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
@@ -232,12 +234,21 @@ class _ButtonSimulatorState extends State<ButtonSimulator> {
|
||||
),
|
||||
OpenBikeControlMdnsEmulator.connectionTitle => OpenBikeControlMdnsTile(),
|
||||
OpenBikeControlBluetoothEmulator.connectionTitle => OpenBikeControlBluetoothTile(),
|
||||
RemotePairing.connectionTitle => RemotePairingWidget(),
|
||||
RemotePairing.connectionTitle => RemoteMousePairingWidget(),
|
||||
RemoteKeyboardPairing.connectionTitle => RemoteKeyboardPairingWidget(),
|
||||
_ => SizedBox.shrink(),
|
||||
},
|
||||
...connectedTrainers.map(
|
||||
(connection) {
|
||||
final supportedActions = connection.supportedActions;
|
||||
final supportedActions = connection.supportedActions == InGameAction.values
|
||||
? core.settings
|
||||
.getTrainerApp()!
|
||||
.keymap
|
||||
.keyPairs
|
||||
.mapNotNull((k) => k.inGameAction)
|
||||
.distinct()
|
||||
.toList()
|
||||
: connection.supportedActions;
|
||||
|
||||
final actionGroups = {
|
||||
if (supportedActions.contains(InGameAction.shiftUp) &&
|
||||
@@ -443,7 +454,7 @@ class _ButtonSimulatorState extends State<ButtonSimulator> {
|
||||
}) async {
|
||||
if (!connection.isConnected.value) {
|
||||
if (down) {
|
||||
buildToast(context, title: 'No connected trainer.');
|
||||
buildToast(title: 'No connected trainer.');
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -499,7 +510,7 @@ class _ButtonSimulatorState extends State<ButtonSimulator> {
|
||||
);
|
||||
await IAPManager.instance.incrementCommandCount();
|
||||
if (result is! Success) {
|
||||
buildToast(context, title: result.message);
|
||||
buildToast(title: result.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,62 +110,6 @@ class _DevicePageState extends State<DevicePage> {
|
||||
),
|
||||
],
|
||||
|
||||
if (!kIsWeb && (Platform.isMacOS || Platform.isWindows))
|
||||
ValueListenableBuilder(
|
||||
valueListenable: core.mediaKeyHandler.isMediaKeyDetectionEnabled,
|
||||
builder: (context, value, child) {
|
||||
return SelectableCard(
|
||||
isActive: value,
|
||||
icon: value ? Icons.check_box : Icons.check_box_outline_blank,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(context.i18n.enableMediaKeyDetection),
|
||||
Text(
|
||||
context.i18n.mediaKeyDetectionTooltip,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
core.mediaKeyHandler.isMediaKeyDetectionEnabled.value =
|
||||
!core.mediaKeyHandler.isMediaKeyDetectionEnabled.value;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(),
|
||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS) && !core.settings.getShowOnboarding())
|
||||
SelectableCard(
|
||||
isActive: core.settings.getPhoneSteeringEnabled(),
|
||||
icon: core.settings.getPhoneSteeringEnabled() ? Icons.check_box : Icons.check_box_outline_blank,
|
||||
title: Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Icon(InGameAction.navigateRight.icon!, size: 16),
|
||||
Icon(InGameAction.navigateLeft.icon!, size: 16),
|
||||
SizedBox(),
|
||||
Expanded(child: Text(AppLocalizations.of(context).enableSteeringWithPhone)),
|
||||
IconButton.secondary(
|
||||
icon: Icon(Icons.ondemand_video),
|
||||
onPressed: () {
|
||||
launchUrlString('https://youtube.com/shorts/zqD5ARGIVmE?feature=share');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
final enable = !core.settings.getPhoneSteeringEnabled();
|
||||
core.settings.setPhoneSteeringEnabled(enable);
|
||||
core.connection.toggleGyroscopeSteering(enable);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
|
||||
Gap(12),
|
||||
if (!screenshotMode)
|
||||
Column(
|
||||
@@ -234,8 +178,66 @@ class _DevicePageState extends State<DevicePage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
Gap(12),
|
||||
SizedBox(),
|
||||
Gap(24),
|
||||
|
||||
if (!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isIOS))
|
||||
ValueListenableBuilder(
|
||||
valueListenable: core.mediaKeyHandler.isMediaKeyDetectionEnabled,
|
||||
builder: (context, value, child) {
|
||||
return SelectableCard(
|
||||
isActive: value,
|
||||
icon: value ? Icons.check_box : Icons.check_box_outline_blank,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(context.i18n.enableMediaKeyDetection),
|
||||
Text(
|
||||
context.i18n.mediaKeyDetectionTooltip,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
final newValue = !core.mediaKeyHandler.isMediaKeyDetectionEnabled.value;
|
||||
core.mediaKeyHandler.isMediaKeyDetectionEnabled.value = newValue;
|
||||
core.settings.setMediaKeyDetectionEnabled(newValue);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Gap(8),
|
||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS) && !core.settings.getShowOnboarding())
|
||||
SelectableCard(
|
||||
isActive: core.settings.getPhoneSteeringEnabled(),
|
||||
icon: core.settings.getPhoneSteeringEnabled() ? Icons.check_box : Icons.check_box_outline_blank,
|
||||
title: Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Icon(InGameAction.navigateRight.icon!, size: 16),
|
||||
Icon(InGameAction.navigateLeft.icon!, size: 16),
|
||||
SizedBox(),
|
||||
Expanded(child: Text(AppLocalizations.of(context).enableSteeringWithPhone)),
|
||||
IconButton.secondary(
|
||||
icon: Icon(Icons.ondemand_video),
|
||||
onPressed: () {
|
||||
launchUrlString('https://youtube.com/shorts/zqD5ARGIVmE?feature=share');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
final enable = !core.settings.getPhoneSteeringEnabled();
|
||||
core.settings.setPhoneSteeringEnabled(enable);
|
||||
core.connection.toggleGyroscopeSteering(enable);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
|
||||
SizedBox(height: 16),
|
||||
if (core.connection.controllerDevices.isNotEmpty)
|
||||
Row(
|
||||
spacing: 8,
|
||||
|
||||
131
lib/pages/login.dart
Normal file
131
lib/pages/login.dart
Normal file
@@ -0,0 +1,131 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/utils/requirements/windows.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:sign_in_button/sign_in_button.dart';
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../utils/core.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Please sign in to continue'),
|
||||
const SizedBox(height: 16),
|
||||
SignInButton(
|
||||
Buttons.google,
|
||||
onPressed: _nativeGoogleSignIn,
|
||||
),
|
||||
SignInButton(
|
||||
Buttons.apple,
|
||||
onPressed: _signInWithApple,
|
||||
),
|
||||
Button.secondary(
|
||||
child: Text('Logout'),
|
||||
onPressed: () {
|
||||
core.supabase.auth.signOut();
|
||||
},
|
||||
),
|
||||
if (kDebugMode && Platform.isWindows)
|
||||
Button.secondary(
|
||||
child: Text("Register"),
|
||||
onPressed: () {
|
||||
WindowsProtocolHandler().register("bikecontrol");
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<AuthResponse?> _nativeGoogleSignIn() async {
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
/// Web Client ID that you registered with Google Cloud.
|
||||
const webClientId = '709945926587-bgk7j9qc86t7nuemu100ngvl9c7irv9k.apps.googleusercontent.com';
|
||||
|
||||
/// iOS Client ID that you registered with Google Cloud.
|
||||
const iosClientId = '709945926587-0iierajthibf4vhqf85fc7bbpgbdgua2.apps.googleusercontent.com';
|
||||
final scopes = ['email'];
|
||||
final googleSignIn = GoogleSignIn.instance;
|
||||
await googleSignIn.initialize(
|
||||
serverClientId: webClientId,
|
||||
clientId: iosClientId,
|
||||
);
|
||||
GoogleSignInAccount? googleUser = await googleSignIn.attemptLightweightAuthentication(reportAllExceptions: true);
|
||||
googleUser ??= await googleSignIn.authenticate();
|
||||
|
||||
/// Authorization is required to obtain the access token with the appropriate scopes for Supabase authentication,
|
||||
/// while also granting permission to access user information.
|
||||
final authorization =
|
||||
await googleUser.authorizationClient.authorizationForScopes(scopes) ??
|
||||
await googleUser.authorizationClient.authorizeScopes(scopes);
|
||||
final idToken = googleUser.authentication.idToken;
|
||||
if (idToken == null) {
|
||||
throw AuthException('No ID Token found.');
|
||||
}
|
||||
final response = await core.supabase.auth.signInWithIdToken(
|
||||
provider: OAuthProvider.google,
|
||||
idToken: idToken,
|
||||
accessToken: authorization.accessToken,
|
||||
);
|
||||
return response;
|
||||
} else {
|
||||
await core.supabase.auth.signInWithOAuth(
|
||||
OAuthProvider.google,
|
||||
redirectTo: kIsWeb ? null : 'bikecontrol://login/',
|
||||
authScreenLaunchMode: kIsWeb
|
||||
? LaunchMode.platformDefault
|
||||
: LaunchMode.externalApplication, // Launch the auth screen in a new webview on mobile.
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs Apple sign in on iOS or macOS
|
||||
Future<AuthResponse?> _signInWithApple() async {
|
||||
if (Platform.isIOS || Platform.isMacOS) {
|
||||
final rawNonce = core.supabase.auth.generateRawNonce();
|
||||
final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString();
|
||||
|
||||
final credential = await SignInWithApple.getAppleIDCredential(
|
||||
scopes: [AppleIDAuthorizationScopes.email],
|
||||
nonce: hashedNonce,
|
||||
);
|
||||
final idToken = credential.identityToken;
|
||||
if (idToken == null) {
|
||||
throw const AuthException('Could not find ID Token from generated credential.');
|
||||
}
|
||||
final authResponse = await core.supabase.auth.signInWithIdToken(
|
||||
provider: OAuthProvider.apple,
|
||||
idToken: idToken,
|
||||
nonce: rawNonce,
|
||||
);
|
||||
return authResponse;
|
||||
} else {
|
||||
await core.supabase.auth.signInWithOAuth(
|
||||
OAuthProvider.apple,
|
||||
redirectTo: kIsWeb ? null : 'bikecontrol://login/',
|
||||
authScreenLaunchMode: kIsWeb ? LaunchMode.platformDefault : LaunchMode.externalApplication,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import 'package:bike_control/widgets/scan.dart';
|
||||
import 'package:bike_control/widgets/title.dart';
|
||||
import 'package:bike_control/widgets/ui/help_button.dart';
|
||||
import 'package:bike_control/widgets/ui/permissions_list.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
@@ -108,9 +109,10 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
_OnboardingStep.trainer => _TrainerOnboardingStep(
|
||||
onComplete: () {
|
||||
setState(() {
|
||||
if (core.settings.getTrainerApp()?.supportsOpenBikeProtocol.contains(
|
||||
if (core.settings.getTrainerApp()?.supportsOpenBikeProtocol.containsAny([
|
||||
OpenBikeProtocolSupport.network,
|
||||
) ??
|
||||
OpenBikeProtocolSupport.dircon,
|
||||
]) ??
|
||||
false) {
|
||||
_currentStep = _OnboardingStep.openbikecontrol;
|
||||
} else {
|
||||
|
||||
@@ -14,7 +14,8 @@ import 'package:bike_control/widgets/apps/openbikecontrol_mdns_tile.dart';
|
||||
import 'package:bike_control/widgets/apps/zwift_mdns_tile.dart';
|
||||
import 'package:bike_control/widgets/apps/zwift_tile.dart';
|
||||
import 'package:bike_control/widgets/iap_status_widget.dart';
|
||||
import 'package:bike_control/widgets/pair_widget.dart';
|
||||
import 'package:bike_control/widgets/keyboard_pair_widget.dart';
|
||||
import 'package:bike_control/widgets/mouse_pair_widget.dart';
|
||||
import 'package:bike_control/widgets/ui/colored_title.dart';
|
||||
import 'package:bike_control/widgets/ui/toast.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
@@ -25,6 +26,7 @@ import 'package:universal_ble/universal_ble.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../utils/keymap/apps/supported_app.dart';
|
||||
import '../utils/keymap/apps/zwift.dart';
|
||||
|
||||
class TrainerPage extends StatefulWidget {
|
||||
final bool isMobile;
|
||||
@@ -53,7 +55,7 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
|
||||
if (core.logic.showForegroundMessage) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// show snackbar to inform user that the app needs to stay in foreground
|
||||
buildToast(context, title: AppLocalizations.current.touchSimulationForegroundMessage);
|
||||
buildToast(title: AppLocalizations.current.touchSimulationForegroundMessage);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,7 +83,7 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
|
||||
UniversalBle.getBluetoothAvailabilityState().then((state) {
|
||||
if (state == AvailabilityState.poweredOn && mounted) {
|
||||
core.remotePairing.reconnect();
|
||||
buildToast(context, title: AppLocalizations.current.touchSimulationForegroundMessage);
|
||||
buildToast(title: AppLocalizations.current.touchSimulationForegroundMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -91,9 +93,8 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final showLocalAsOther =
|
||||
(core.logic.showObpBluetoothEmulator || core.logic.showObpMdnsEmulator) &&
|
||||
core.logic.showLocalControl &&
|
||||
!core.settings.getLocalEnabled();
|
||||
//(core.logic.showObpBluetoothEmulator || core.logic.showObpMdnsEmulator) &&
|
||||
false && core.logic.showLocalControl && !core.settings.getLocalEnabled();
|
||||
final showWhooshLinkAsOther =
|
||||
(core.logic.showObpBluetoothEmulator || core.logic.showObpMdnsEmulator) && core.logic.showMyWhooshLink;
|
||||
|
||||
@@ -122,11 +123,12 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
|
||||
),
|
||||
if (core.logic.showLocalControl && !showLocalAsOther) LocalTile(),
|
||||
if (core.logic.showMyWhooshLink && !showWhooshLinkAsOther) MyWhooshLinkTile(),
|
||||
if (core.logic.showRemote && core.settings.getTrainerApp() is! Zwift) RemoteKeyboardPairingWidget(),
|
||||
];
|
||||
|
||||
final otherTiles = [
|
||||
if (core.logic.showRemote) RemotePairingWidget(),
|
||||
if (showLocalAsOther) LocalTile(),
|
||||
if (core.logic.showRemote) RemoteMousePairingWidget(),
|
||||
if (core.logic.showLocalControl && showLocalAsOther) LocalTile(),
|
||||
if (showWhooshLinkAsOther) MyWhooshLinkTile(),
|
||||
];
|
||||
|
||||
@@ -202,8 +204,8 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
|
||||
),
|
||||
),
|
||||
if (core.settings.getTrainerApp() != null) ...[
|
||||
Gap(22),
|
||||
if (recommendedTiles.isNotEmpty) ...[
|
||||
Gap(22),
|
||||
ColoredTitle(text: context.i18n.recommendedConnectionMethods),
|
||||
Gap(12),
|
||||
],
|
||||
@@ -228,6 +230,8 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
|
||||
child: Accordion(
|
||||
items: [
|
||||
AccordionItem(
|
||||
expanded:
|
||||
recommendedTiles.isEmpty || (core.logic.showRemote && core.remotePairing.isStarted.value),
|
||||
trigger: AccordionTrigger(child: ColoredTitle(text: context.i18n.otherConnectionMethods)),
|
||||
content: Column(
|
||||
children: [
|
||||
@@ -269,7 +273,6 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
|
||||
onPressed: () {
|
||||
if (core.settings.getTrainerApp() == null) {
|
||||
buildToast(
|
||||
context,
|
||||
level: LogLevel.LOGLEVEL_WARNING,
|
||||
title: context.i18n.selectTrainerApp,
|
||||
);
|
||||
|
||||
@@ -178,14 +178,41 @@ abstract class BaseActions {
|
||||
class StubActions extends BaseActions {
|
||||
StubActions({super.supportedModes = const []});
|
||||
|
||||
final List<(ControllerButton button, bool isDown, bool isUp)> performedActions = [];
|
||||
final List<PerformedAction> performedActions = [];
|
||||
|
||||
@override
|
||||
Future<ActionResult> performAction(ControllerButton button, {bool isKeyDown = true, bool isKeyUp = false}) async {
|
||||
performedActions.add((button, isKeyDown, isKeyUp));
|
||||
performedActions.add(PerformedAction(button, isDown: isKeyDown, isUp: isKeyUp));
|
||||
return Future.value(Ignored('${button.name.splitByUpperCase()} clicked'));
|
||||
}
|
||||
|
||||
@override
|
||||
void cleanup() {}
|
||||
void cleanup() {
|
||||
performedActions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class PerformedAction {
|
||||
final ControllerButton button;
|
||||
final bool isDown;
|
||||
final bool isUp;
|
||||
|
||||
PerformedAction(this.button, {required this.isDown, required this.isUp});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PerformedAction &&
|
||||
runtimeType == other.runtimeType &&
|
||||
button.copyWith(sourceDeviceId: null) == other.button.copyWith(sourceDeviceId: null) &&
|
||||
isDown == other.isDown &&
|
||||
isUp == other.isUp;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(button, isDown, isUp);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '{button: $button, isDown: $isDown, isUp: $isUp}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,6 @@ class DesktopActions extends BaseActions {
|
||||
final label = keyPair.logicalKey!.keyLabel;
|
||||
final keyName = label.isNotEmpty ? label : keyPair.logicalKey!.debugName ?? 'Key';
|
||||
buildToast(
|
||||
navigatorKey.currentContext!,
|
||||
|
||||
location: ToastLocation.bottomLeft,
|
||||
title:
|
||||
'${isKeyDown
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bike_control/utils/keymap/keymap.dart';
|
||||
|
||||
class RemoteActions extends BaseActions {
|
||||
RemoteActions({super.supportedModes = const [SupportedMode.touch]});
|
||||
RemoteActions({super.supportedModes = const [SupportedMode.touch, SupportedMode.keyboard]});
|
||||
|
||||
@override
|
||||
Future<ActionResult> performAction(ControllerButton button, {required bool isKeyDown, required bool isKeyUp}) async {
|
||||
@@ -16,14 +16,22 @@ class RemoteActions extends BaseActions {
|
||||
return superResult;
|
||||
}
|
||||
final keyPair = supportedApp!.keymap.getKeyPair(button)!;
|
||||
if (!core.remotePairing.isConnected.value) {
|
||||
if (!core.remotePairing.isConnected.value && !core.remoteKeyboardPairing.isConnected.value) {
|
||||
return Error('Not connected to a ${core.settings.getLastTarget()?.name ?? 'remote'} device');
|
||||
}
|
||||
|
||||
if (keyPair.physicalKey != null && keyPair.touchPosition == Offset.zero) {
|
||||
return Error('Physical key actions are not supported, yet');
|
||||
} else {
|
||||
if (core.remotePairing.isConnected.value) {
|
||||
if (keyPair.touchPosition == Offset.zero) {
|
||||
return Error('Key $keyPair does not have a valid touch position');
|
||||
}
|
||||
return core.remotePairing.sendAction(keyPair, isKeyDown: isKeyDown, isKeyUp: isKeyUp);
|
||||
} else if (core.remoteKeyboardPairing.isConnected.value) {
|
||||
if (keyPair.physicalKey == null) {
|
||||
return Error('Key $keyPair does not have a valid physical key for keyboard actions');
|
||||
}
|
||||
return core.remoteKeyboardPairing.sendAction(keyPair, isKeyDown: isKeyDown, isKeyUp: isKeyUp);
|
||||
} else {
|
||||
return Error('Not connected to a ${core.settings.getLastTarget()?.name ?? 'remote'} device');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import 'package:bike_control/bluetooth/devices/openbikecontrol/obc_mdns_emulator
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/protocol_parser.dart';
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/ftms_mdns_emulator.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/zwift_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/bluetooth/remote_keyboard_pairing.dart';
|
||||
import 'package:bike_control/bluetooth/remote_pairing.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/utils/actions/android.dart';
|
||||
@@ -21,6 +21,8 @@ import 'package:dartx/dartx.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
import '../bluetooth/connection.dart';
|
||||
@@ -38,12 +40,14 @@ class Core {
|
||||
final settings = Settings();
|
||||
final connection = Connection();
|
||||
|
||||
late final supabase = Supabase.instance.client;
|
||||
late final whooshLink = WhooshLink();
|
||||
late final zwiftEmulator = ZwiftEmulator();
|
||||
late final zwiftMdnsEmulator = FtmsMdnsEmulator();
|
||||
late final obpMdnsEmulator = OpenBikeControlMdnsEmulator();
|
||||
late final obpBluetoothEmulator = OpenBikeControlBluetoothEmulator();
|
||||
late final remotePairing = RemotePairing();
|
||||
late final remoteKeyboardPairing = RemoteKeyboardPairing();
|
||||
|
||||
late final mediaKeyHandler = MediaKeyHandler();
|
||||
late final logic = CoreLogic();
|
||||
@@ -168,7 +172,11 @@ class CoreLogic {
|
||||
}
|
||||
|
||||
bool get showObpMdnsEmulator {
|
||||
return core.settings.getTrainerApp()?.supportsOpenBikeProtocol.contains(OpenBikeProtocolSupport.network) == true;
|
||||
return core.settings.getTrainerApp()?.supportsOpenBikeProtocol.containsAny([
|
||||
OpenBikeProtocolSupport.network,
|
||||
OpenBikeProtocolSupport.dircon,
|
||||
]) ==
|
||||
true;
|
||||
}
|
||||
|
||||
bool get showObpBluetoothEmulator {
|
||||
@@ -180,6 +188,10 @@ class CoreLogic {
|
||||
return core.settings.getRemoteControlEnabled() && showRemote;
|
||||
}
|
||||
|
||||
bool get isRemoteKeyboardControlEnabled {
|
||||
return core.settings.getRemoteKeyboardControlEnabled() && showRemote;
|
||||
}
|
||||
|
||||
bool get showMyWhooshLink =>
|
||||
core.settings.getTrainerApp() is MyWhoosh &&
|
||||
core.settings.getLastTarget() != null &&
|
||||
@@ -210,7 +222,8 @@ class CoreLogic {
|
||||
core.settings.getTrainerApp()?.supportsOpenBikeProtocol.isNotEmpty == true;
|
||||
|
||||
bool get showLocalRemoteOptions =>
|
||||
core.actionHandler.supportedModes.isNotEmpty && ((showLocalControl) || (isRemoteControlEnabled));
|
||||
core.actionHandler.supportedModes.isNotEmpty &&
|
||||
(showLocalControl || isRemoteControlEnabled || isRemoteKeyboardControlEnabled);
|
||||
|
||||
bool get hasNoConnectionMethod =>
|
||||
!screenshotMode &&
|
||||
@@ -235,6 +248,7 @@ class CoreLogic {
|
||||
if (isZwiftBleEnabled) core.zwiftEmulator,
|
||||
if (isZwiftMdnsEnabled) core.zwiftMdnsEmulator,
|
||||
if (isRemoteControlEnabled) core.remotePairing,
|
||||
if (isRemoteKeyboardControlEnabled) core.remoteKeyboardPairing,
|
||||
].filter((e) => e.isConnected.value).toList();
|
||||
|
||||
List<TrainerConnection> get enabledTrainerConnections => [
|
||||
@@ -244,6 +258,7 @@ class CoreLogic {
|
||||
if (isZwiftBleEnabled) core.zwiftEmulator,
|
||||
if (isZwiftMdnsEnabled) core.zwiftMdnsEmulator,
|
||||
if (isRemoteControlEnabled) core.remotePairing,
|
||||
if (isRemoteKeyboardControlEnabled) core.remoteKeyboardPairing,
|
||||
];
|
||||
|
||||
List<TrainerConnection> get trainerConnections => [
|
||||
@@ -253,6 +268,7 @@ class CoreLogic {
|
||||
if (showZwiftBleEmulator) core.zwiftEmulator,
|
||||
if (showZwiftMsdnEmulator) core.zwiftMdnsEmulator,
|
||||
if (showRemote) core.remotePairing,
|
||||
if (showRemote) core.remoteKeyboardPairing,
|
||||
];
|
||||
|
||||
Future<bool> isTrainerConnected() async {
|
||||
@@ -329,5 +345,15 @@ class CoreLogic {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (isRemoteKeyboardControlEnabled && !core.remoteKeyboardPairing.isStarted.value) {
|
||||
core.remoteKeyboardPairing.startAdvertising().catchError((e, s) {
|
||||
recordError(e, s, context: 'Remote Keyboard Pairing');
|
||||
core.settings.setRemoteKeyboardControlEnabled(false);
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_WARNING, 'Failed to start Remote Keyboard Control pairing: $e'),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +217,8 @@ class IAPManager {
|
||||
}
|
||||
|
||||
void setAttributes() {
|
||||
_revenueCatService?.setAttributes();
|
||||
if (!screenshotMode) {
|
||||
_revenueCatService?.setAttributes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/widgets/ui/toast.dart';
|
||||
@@ -11,9 +10,11 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:prop/prop.dart' as zp;
|
||||
import 'package:prop/prop.dart' hide LogLevel;
|
||||
import 'package:purchases_flutter/purchases_flutter.dart';
|
||||
import 'package:purchases_ui_flutter/purchases_ui_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
/// RevenueCat-based IAP service for iOS, macOS, and Android
|
||||
@@ -34,13 +35,14 @@ class RevenueCatService {
|
||||
final int Function() getDailyCommandLimit;
|
||||
final void Function(int limit) setDailyCommandLimit;
|
||||
|
||||
static const _isAndroidWorking = false;
|
||||
bool _isInitialized = false;
|
||||
String? _trialStartDate;
|
||||
String? _lastCommandDate;
|
||||
int? _dailyCommandCount;
|
||||
StreamSubscription<CustomerInfo>? _customerInfoSubscription;
|
||||
|
||||
late final StreamSubscription<AuthState> _authSubscription;
|
||||
|
||||
RevenueCatService(
|
||||
this._prefs, {
|
||||
required this.isPurchasedNotifier,
|
||||
@@ -119,11 +121,37 @@ class RevenueCatService {
|
||||
|
||||
_isInitialized = true;
|
||||
|
||||
if (Platform.isAndroid && !isPurchasedNotifier.value && !_isAndroidWorking) {
|
||||
setDailyCommandLimit(10000);
|
||||
} else if (!isTrialExpired && Platform.isAndroid) {
|
||||
if (!isTrialExpired && Platform.isAndroid) {
|
||||
setDailyCommandLimit(80);
|
||||
}
|
||||
|
||||
_authSubscription = core.supabase.auth.onAuthStateChange.listen((data) {
|
||||
final AuthChangeEvent event = data.event;
|
||||
final Session? session = data.session;
|
||||
|
||||
Logger.info('event: $event, session: ${session?.user.id} via ${session?.user.email}');
|
||||
|
||||
switch (event) {
|
||||
case AuthChangeEvent.initialSession:
|
||||
setAttributes();
|
||||
case AuthChangeEvent.signedIn:
|
||||
setAttributes();
|
||||
// handle signed in
|
||||
case AuthChangeEvent.signedOut:
|
||||
setAttributes();
|
||||
// handle signed out
|
||||
case AuthChangeEvent.passwordRecovery:
|
||||
// handle password recovery
|
||||
case AuthChangeEvent.tokenRefreshed:
|
||||
// handle token refreshed
|
||||
case AuthChangeEvent.userUpdated:
|
||||
// handle user updated
|
||||
case AuthChangeEvent.userDeleted:
|
||||
// handle user deleted
|
||||
case AuthChangeEvent.mfaChallengeVerified:
|
||||
// handle mfa challenge verified
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
recordError(e, s, context: 'Initializing RevenueCat Service');
|
||||
core.connection.signalNotification(
|
||||
@@ -240,16 +268,7 @@ class RevenueCatService {
|
||||
/// Purchase the full version (use paywall instead)
|
||||
Future<void> purchaseFullVersion(BuildContext context) async {
|
||||
// Direct the user to the paywall for a better experience
|
||||
if (Platform.isAndroid && !_isAndroidWorking) {
|
||||
_trialStartDate = null;
|
||||
await startTrial();
|
||||
buildToast(
|
||||
navigatorKey.currentContext!,
|
||||
title: AppLocalizations.of(context).unlockingNotPossible,
|
||||
duration: Duration(seconds: 5),
|
||||
);
|
||||
setDailyCommandLimit(10000);
|
||||
} else if (Platform.isMacOS) {
|
||||
if (Platform.isMacOS) {
|
||||
try {
|
||||
final offerings = await Purchases.getOfferings();
|
||||
final purchaseParams = PurchaseParams.package(offerings.current!.availablePackages.first);
|
||||
@@ -260,7 +279,7 @@ class RevenueCatService {
|
||||
} on PlatformException catch (e) {
|
||||
var errorCode = PurchasesErrorHelper.getErrorCode(e);
|
||||
if (errorCode != PurchasesErrorCode.purchaseCancelledError) {
|
||||
buildToast(context, title: e.message);
|
||||
buildToast(title: e.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -316,6 +335,9 @@ class RevenueCatService {
|
||||
|
||||
/// Increment the daily command count
|
||||
Future<void> incrementCommandCount() async {
|
||||
if (isPurchasedNotifier.value) {
|
||||
return; // No need to track for purchased users
|
||||
}
|
||||
try {
|
||||
final today = DateTime.now().toIso8601String().split('T')[0];
|
||||
final lastDate = await _prefs.read(key: _lastCommandDateKey);
|
||||
@@ -380,8 +402,11 @@ class RevenueCatService {
|
||||
}
|
||||
|
||||
Future<void> setAttributes() async {
|
||||
final Session? session = core.supabase.auth.currentSession;
|
||||
|
||||
// attributes are fully anonymous
|
||||
await Purchases.setAttributes({
|
||||
if (session?.user.id != null) "bikecontrol_user": session!.user.id,
|
||||
"bikecontrol_trainer": core.settings.getTrainerApp()?.name ?? '-',
|
||||
"bikecontrol_target": core.settings.getLastTarget()?.name ?? '-',
|
||||
if (core.connection.controllerDevices.isNotEmpty)
|
||||
|
||||
@@ -8,6 +8,9 @@ import 'package:bike_control/utils/windows_store_environment.dart';
|
||||
import 'package:bike_control/widgets/ui/toast.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:gotrue/src/types/auth_state.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:windows_iap/windows_iap.dart';
|
||||
|
||||
/// Windows-specific IAP service
|
||||
@@ -44,6 +47,32 @@ class WindowsIAPService {
|
||||
_lastCommandDate = await _prefs.read(key: _lastCommandDateKey);
|
||||
_dailyCommandCount = int.tryParse(await _prefs.read(key: _dailyCommandCountKey) ?? '0');
|
||||
_isInitialized = true;
|
||||
|
||||
|
||||
_authSubscription = core.supabase.auth.onAuthStateChange.listen((data) {
|
||||
final AuthChangeEvent event = data.event;
|
||||
final Session? session = data.session;
|
||||
|
||||
Logger.info('event: $event, session: ${session?.user.id} via ${session?.user.email}');
|
||||
|
||||
switch (event) {
|
||||
case AuthChangeEvent.initialSession:
|
||||
case AuthChangeEvent.signedIn:
|
||||
// handle signed in
|
||||
case AuthChangeEvent.signedOut:
|
||||
// handle signed out
|
||||
case AuthChangeEvent.passwordRecovery:
|
||||
// handle password recovery
|
||||
case AuthChangeEvent.tokenRefreshed:
|
||||
// handle token refreshed
|
||||
case AuthChangeEvent.userUpdated:
|
||||
// handle user updated
|
||||
case AuthChangeEvent.userDeleted:
|
||||
// handle user deleted
|
||||
case AuthChangeEvent.mfaChallengeVerified:
|
||||
// handle mfa challenge verified
|
||||
}
|
||||
});
|
||||
} catch (e, s) {
|
||||
recordError(e, s, context: 'Initializing');
|
||||
debugPrint('Failed to initialize Windows IAP: $e');
|
||||
@@ -92,7 +121,6 @@ class WindowsIAPService {
|
||||
if (status == StorePurchaseStatus.succeeded || status == StorePurchaseStatus.alreadyPurchased) {
|
||||
IAPManager.instance.isPurchased.value = true;
|
||||
buildToast(
|
||||
navigatorKey.currentContext!,
|
||||
title: 'Purchase Successful',
|
||||
subtitle: 'Thank you for your purchase! You now have unlimited access.',
|
||||
);
|
||||
@@ -109,6 +137,8 @@ class WindowsIAPService {
|
||||
/// Get the number of days remaining in the trial
|
||||
int trialDaysRemaining = 0;
|
||||
|
||||
late final StreamSubscription<AuthState> _authSubscription;
|
||||
|
||||
/// Check if the trial has expired
|
||||
bool get isTrialExpired {
|
||||
return !IAPManager.instance.isPurchased.value && hasTrialStarted && trialDaysRemaining <= 0;
|
||||
|
||||
@@ -10,7 +10,7 @@ class OpenBikeControl extends SupportedApp {
|
||||
packageName: "org.openbikecontrol",
|
||||
compatibleTargets: Target.values,
|
||||
supportsZwiftEmulation: false,
|
||||
supportsOpenBikeProtocol: OpenBikeProtocolSupport.values,
|
||||
supportsOpenBikeProtocol: [OpenBikeProtocolSupport.network, OpenBikeProtocolSupport.ble],
|
||||
keymap: Keymap(
|
||||
keyPairs: [],
|
||||
),
|
||||
|
||||
@@ -50,6 +50,18 @@ class Rouvy extends SupportedApp {
|
||||
logicalKey: LogicalKeyboardKey.keyB,
|
||||
inGameAction: InGameAction.back,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.a],
|
||||
physicalKey: PhysicalKeyboardKey.keyY,
|
||||
logicalKey: LogicalKeyboardKey.keyY,
|
||||
inGameAction: InGameAction.kudos,
|
||||
),
|
||||
KeyPair(
|
||||
buttons: [ZwiftButtons.y],
|
||||
physicalKey: PhysicalKeyboardKey.keyZ,
|
||||
logicalKey: LogicalKeyboardKey.keyZ,
|
||||
inGameAction: InGameAction.pause,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'my_whoosh.dart';
|
||||
enum OpenBikeProtocolSupport {
|
||||
ble,
|
||||
network,
|
||||
dircon,
|
||||
}
|
||||
|
||||
abstract class SupportedApp {
|
||||
|
||||
@@ -18,6 +18,7 @@ class TrainingPeaks extends SupportedApp {
|
||||
packageName: "com.indieVelo.client",
|
||||
compatibleTargets: !kIsWeb && Platform.isIOS ? [Target.otherDevice] : Target.values,
|
||||
supportsZwiftEmulation: false,
|
||||
supportsOpenBikeProtocol: [OpenBikeProtocolSupport.ble, OpenBikeProtocolSupport.dircon],
|
||||
star: true,
|
||||
keymap: Keymap(
|
||||
keyPairs: [
|
||||
|
||||
@@ -12,8 +12,8 @@ enum InGameAction {
|
||||
shiftUp('Shift Up', icon: BootstrapIcons.patchPlus),
|
||||
shiftDown('Shift Down', icon: BootstrapIcons.patchMinus),
|
||||
uturn('U-Turn', alternativeTitle: 'Down', icon: BootstrapIcons.arrowDownUp),
|
||||
steerLeft('Steer Left', alternativeTitle: 'Left', icon: RadixIcons.doubleArrowLeft),
|
||||
steerRight('Steer Right', alternativeTitle: 'Right', icon: RadixIcons.doubleArrowRight),
|
||||
steerLeft('Steer Left', alternativeTitle: 'Left', icon: RadixIcons.doubleArrowLeft, isLongPress: true),
|
||||
steerRight('Steer Right', alternativeTitle: 'Right', icon: RadixIcons.doubleArrowRight, isLongPress: true),
|
||||
|
||||
// mywhoosh
|
||||
cameraAngle('Change Camera Angle', possibleValues: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], icon: BootstrapIcons.cameraReels),
|
||||
@@ -31,9 +31,13 @@ enum InGameAction {
|
||||
back('Back', icon: BootstrapIcons.arrowLeft),
|
||||
rideOnBomb('Ride On Bomb', icon: LucideIcons.bomb, isLongPress: true),
|
||||
|
||||
// rouvy
|
||||
kudos('Kudos', icon: BootstrapIcons.handThumbsUp),
|
||||
pause('Pause/Resume', icon: BootstrapIcons.pause, isLongPress: true),
|
||||
|
||||
// headwind
|
||||
headwindSpeed('Headwind Speed', possibleValues: [0, 25, 50, 75, 100]),
|
||||
headwindHeartRateMode('Headwind HR Mode'),
|
||||
headwindSpeed('Headwind Speed', possibleValues: [0, 25, 50, 75, 100], icon: Icons.air),
|
||||
headwindHeartRateMode('Headwind HR Mode', icon: Icons.favorite),
|
||||
|
||||
// openbikecontrol
|
||||
up('Up', icon: RadixIcons.arrowUp),
|
||||
@@ -95,7 +99,7 @@ class ControllerButton {
|
||||
}
|
||||
|
||||
String get displayName {
|
||||
if (sourceDeviceId == null || true) {
|
||||
if (sourceDeviceId == null) {
|
||||
return name;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:bike_control/utils/actions/android.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
@@ -168,7 +169,12 @@ class KeyPair {
|
||||
_ => Icons.keyboard,
|
||||
},
|
||||
//_ when inGameAction != null && core.logic.emulatorEnabled => Icons.link,
|
||||
_ when inGameAction != null && inGameAction!.icon != null && core.logic.emulatorEnabled => inGameAction!.icon,
|
||||
_
|
||||
when inGameAction != null &&
|
||||
inGameAction!.icon != null &&
|
||||
(core.logic.emulatorEnabled ||
|
||||
[InGameAction.headwindHeartRateMode, InGameAction.headwindSpeed].contains(inGameAction!)) =>
|
||||
inGameAction!.icon,
|
||||
|
||||
_
|
||||
when androidAction != null &&
|
||||
@@ -197,10 +203,9 @@ class KeyPair {
|
||||
|
||||
bool get hasActiveAction =>
|
||||
screenshotMode ||
|
||||
(physicalKey != null &&
|
||||
core.logic.showLocalControl &&
|
||||
core.settings.getLocalEnabled() &&
|
||||
core.actionHandler.supportedModes.contains(SupportedMode.keyboard)) ||
|
||||
(physicalKey != null && (core.logic.showLocalControl && core.settings.getLocalEnabled()) ||
|
||||
(core.logic.showRemote && core.settings.getRemoteKeyboardControlEnabled()) &&
|
||||
core.actionHandler.supportedModes.contains(SupportedMode.keyboard)) ||
|
||||
(isSpecialKey &&
|
||||
core.logic.showLocalControl &&
|
||||
core.settings.getLocalEnabled() &&
|
||||
@@ -226,11 +231,17 @@ class KeyPair {
|
||||
(inGameAction != null &&
|
||||
core.logic.showZwiftMsdnEmulator &&
|
||||
core.settings.getZwiftMdnsEmulatorEnabled() &&
|
||||
core.zwiftMdnsEmulator.supportedActions.contains(inGameAction));
|
||||
core.zwiftMdnsEmulator.supportedActions.contains(inGameAction)) ||
|
||||
(inGameAction != null &&
|
||||
[InGameAction.headwindHeartRateMode, InGameAction.headwindSpeed].contains(inGameAction) &&
|
||||
(core.connection.accessories.isNotEmpty || kDebugMode));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final text = (inGameAction != null && core.logic.emulatorEnabled)
|
||||
final text =
|
||||
(inGameAction != null &&
|
||||
(core.logic.emulatorEnabled ||
|
||||
[InGameAction.headwindHeartRateMode, InGameAction.headwindSpeed].contains(inGameAction!)))
|
||||
? [
|
||||
inGameAction!.title,
|
||||
if (inGameActionValue != null) '$inGameActionValue',
|
||||
|
||||
@@ -91,9 +91,9 @@ class KeymapManager {
|
||||
if (jsonData != null && jsonData.isNotEmpty) {
|
||||
final success = await core.settings.importCustomAppProfile(jsonData);
|
||||
if (success) {
|
||||
buildToast(context, title: context.i18n.profileImportedSuccessfully);
|
||||
buildToast(title: context.i18n.profileImportedSuccessfully);
|
||||
} else {
|
||||
buildToast(context, title: context.i18n.failedToImportProfile);
|
||||
buildToast(title: context.i18n.failedToImportProfile);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -107,7 +107,7 @@ class KeymapManager {
|
||||
if (jsonData != null) {
|
||||
Clipboard.setData(ClipboardData(text: jsonData));
|
||||
|
||||
buildToast(context, title: context.i18n.profileExportedToClipboard(currentProfile));
|
||||
buildToast(title: context.i18n.profileExportedToClipboard(currentProfile));
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -25,7 +25,7 @@ class MediaKeyHandler {
|
||||
_smtc?.disableSmtc();
|
||||
} else {
|
||||
mediaKeyDetector.setIsPlaying(isPlaying: false);
|
||||
mediaKeyDetector.removeListenerWithDevice(_onMediaKeyDetectedListenerWithDevice);
|
||||
mediaKeyDetector.removeListener(_onMediaKeyDetectedListener);
|
||||
}
|
||||
} else {
|
||||
FlutterVolumeController.addListener(
|
||||
@@ -80,7 +80,7 @@ class MediaKeyHandler {
|
||||
);
|
||||
_smtc!.buttonPressStream.listen(_onMediaKeyPressedListener);
|
||||
} else {
|
||||
mediaKeyDetector.addListenerWithDevice(_onMediaKeyDetectedListenerWithDevice);
|
||||
mediaKeyDetector.addListener(_onMediaKeyDetectedListener);
|
||||
mediaKeyDetector.setIsPlaying(isPlaying: true);
|
||||
}
|
||||
}
|
||||
@@ -88,11 +88,7 @@ class MediaKeyHandler {
|
||||
}
|
||||
|
||||
bool _onMediaKeyDetectedListener(MediaKey mediaKey) {
|
||||
return _onMediaKeyDetectedListenerWithDevice(mediaKey, 'HID Device');
|
||||
}
|
||||
|
||||
bool _onMediaKeyDetectedListenerWithDevice(MediaKey mediaKey, String deviceId) {
|
||||
final hidDevice = HidDevice(deviceId);
|
||||
final hidDevice = HidDevice('HID Device');
|
||||
|
||||
var availableDevice = core.connection.controllerDevices.firstOrNullWhere(
|
||||
(e) => e.toString() == hidDevice.toString(),
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/utils/actions/android.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/requirements/platform.dart';
|
||||
@@ -133,7 +132,6 @@ class NotificationRequirement extends PlatformRequirement {
|
||||
?.requestNotificationsPermission();
|
||||
if (result == false) {
|
||||
buildToast(
|
||||
navigatorKey.currentContext!,
|
||||
title: 'Enable notifications for BikeControl in Android Settings',
|
||||
);
|
||||
}
|
||||
@@ -148,7 +146,6 @@ class NotificationRequirement extends PlatformRequirement {
|
||||
core.settings.setHasAskedPermissions(true);
|
||||
if (result == false) {
|
||||
buildToast(
|
||||
navigatorKey.currentContext!,
|
||||
title: 'Enable notifications for BikeControl in System Preferences → Notifications → Bike Control',
|
||||
);
|
||||
launchUrlString('x-apple.systempreferences:com.apple.preference.notifications');
|
||||
@@ -164,7 +161,6 @@ class NotificationRequirement extends PlatformRequirement {
|
||||
core.settings.setHasAskedPermissions(true);
|
||||
if (result == false) {
|
||||
buildToast(
|
||||
navigatorKey.currentContext!,
|
||||
title: 'Enable notifications for BikeControl in System Preferences → Notifications → Bike Control',
|
||||
);
|
||||
launchUrlString('x-apple.systempreferences:com.apple.preference.notifications');
|
||||
|
||||
@@ -22,7 +22,6 @@ class KeyboardRequirement extends PlatformRequirement {
|
||||
@override
|
||||
Future<void> call(BuildContext context, VoidCallback onUpdate) async {
|
||||
buildToast(
|
||||
context,
|
||||
title: AppLocalizations.current.enableKeyboardAccessMessage,
|
||||
);
|
||||
await keyPressSimulator.requestAccess(onlyOpenPrefPane: Platform.isMacOS);
|
||||
@@ -62,7 +61,7 @@ class BluetoothTurnedOn extends PlatformRequirement {
|
||||
await PeripheralManager().showAppSettings();
|
||||
} else if (currentState == AvailabilityState.poweredOff) {
|
||||
if (Platform.isMacOS) {
|
||||
buildToast(context, title: name);
|
||||
buildToast(title: name);
|
||||
} else {
|
||||
await UniversalBle.enableBluetooth();
|
||||
}
|
||||
|
||||
70
lib/utils/requirements/windows.dart
Normal file
70
lib/utils/requirements/windows.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
const _hive = HKEY_CURRENT_USER;
|
||||
|
||||
class WindowsProtocolHandler {
|
||||
List<String> getArguments(List<String>? arguments) {
|
||||
if (arguments == null) return ['%s'];
|
||||
|
||||
if (arguments.isEmpty && !arguments.any((e) => e.contains('%s'))) {
|
||||
throw ArgumentError('arguments must contain at least 1 instance of "%s"');
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
|
||||
void register(String scheme, {String? executable, List<String>? arguments}) {
|
||||
if (defaultTargetPlatform != TargetPlatform.windows) return;
|
||||
|
||||
final prefix = _regPrefix(scheme);
|
||||
final capitalized = scheme[0].toUpperCase() + scheme.substring(1);
|
||||
final args = getArguments(arguments).map((a) => _sanitize(a));
|
||||
final cmd = '${executable ?? Platform.resolvedExecutable} ${args.join(' ')}';
|
||||
|
||||
_regCreateStringKey(_hive, prefix, '', 'URL:$capitalized');
|
||||
_regCreateStringKey(_hive, prefix, 'URL Protocol', '');
|
||||
_regCreateStringKey(_hive, '$prefix\\shell\\open\\command', '', cmd);
|
||||
}
|
||||
|
||||
void unregister(String scheme) {
|
||||
if (defaultTargetPlatform != TargetPlatform.windows) return;
|
||||
|
||||
final txtKey = TEXT(_regPrefix(scheme));
|
||||
try {
|
||||
RegDeleteTree(HKEY_CURRENT_USER, txtKey);
|
||||
} finally {
|
||||
free(txtKey);
|
||||
}
|
||||
}
|
||||
|
||||
String _regPrefix(String scheme) => 'SOFTWARE\\Classes\\$scheme';
|
||||
|
||||
int _regCreateStringKey(int hKey, String key, String valueName, String data) {
|
||||
final txtKey = TEXT(key);
|
||||
final txtValue = TEXT(valueName);
|
||||
final txtData = TEXT(data);
|
||||
try {
|
||||
return RegSetKeyValue(
|
||||
hKey,
|
||||
txtKey,
|
||||
txtValue,
|
||||
REG_SZ,
|
||||
txtData,
|
||||
txtData.length * 2 + 2,
|
||||
);
|
||||
} finally {
|
||||
free(txtKey);
|
||||
free(txtValue);
|
||||
free(txtData);
|
||||
}
|
||||
}
|
||||
|
||||
String _sanitize(String value) {
|
||||
value = value.replaceAll(r'%s', '%1').replaceAll(r'"', '\\"');
|
||||
return '"$value"';
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import 'package:path_provider_windows/path_provider_windows.dart';
|
||||
import 'package:prop/emulators/prefs.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:shared_preferences_windows/shared_preferences_windows.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
@@ -49,6 +50,15 @@ class Settings {
|
||||
final app = getKeyMap();
|
||||
core.actionHandler.init(app);
|
||||
|
||||
try {
|
||||
await Supabase.initialize(
|
||||
url: 'https://pikrcyynovdvogrldfnw.supabase.co',
|
||||
anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY'),
|
||||
);
|
||||
} catch (e, s) {
|
||||
recordError(e, s, context: 'Supabase initialization');
|
||||
}
|
||||
|
||||
// Initialize IAP manager
|
||||
await IAPManager.instance.initialize();
|
||||
|
||||
@@ -319,6 +329,14 @@ class Settings {
|
||||
return prefs.getBool('remote_control_enabled') ?? false;
|
||||
}
|
||||
|
||||
void setRemoteKeyboardControlEnabled(bool value) {
|
||||
prefs.setBool('remote_keyboard_control_enabled', value);
|
||||
}
|
||||
|
||||
bool getRemoteKeyboardControlEnabled() {
|
||||
return prefs.getBool('remote_keyboard_control_enabled') ?? false;
|
||||
}
|
||||
|
||||
bool getLocalEnabled() {
|
||||
return prefs.getBool('local_control_enabled') ?? false;
|
||||
}
|
||||
@@ -406,4 +424,12 @@ class Settings {
|
||||
Future<void> setHasAskedPermissions(bool asked) async {
|
||||
await prefs.setBool('asked_permissions', asked);
|
||||
}
|
||||
|
||||
bool getMediaKeyDetectionEnabled() {
|
||||
return prefs.getBool('media_key_detection_enabled') ?? false;
|
||||
}
|
||||
|
||||
Future<void> setMediaKeyDetectionEnabled(bool enabled) async {
|
||||
await prefs.setBool('media_key_detection_enabled', enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/pages/markdown.dart';
|
||||
@@ -6,6 +5,7 @@ import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/i18n_extension.dart';
|
||||
import 'package:bike_control/widgets/ui/connection_method.dart';
|
||||
import 'package:bike_control/widgets/ui/toast.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
class MyWhooshLinkTile extends StatefulWidget {
|
||||
@@ -43,7 +43,6 @@ class _MywhooshLinkTileState extends State<MyWhooshLinkTile> {
|
||||
core.whooshLink.stopServer();
|
||||
} else if (value) {
|
||||
buildToast(
|
||||
navigatorKey.currentContext!,
|
||||
title: AppLocalizations.of(context).myWhooshLinkInfo,
|
||||
level: LogLevel.LOGLEVEL_INFO,
|
||||
duration: Duration(seconds: 6),
|
||||
@@ -60,7 +59,6 @@ class _MywhooshLinkTileState extends State<MyWhooshLinkTile> {
|
||||
recordError(e, s, context: 'MyWhoosh Link Server');
|
||||
core.settings.setMyWhooshLinkEnabled(false);
|
||||
buildToast(
|
||||
context,
|
||||
title: context.i18n.errorStartingMyWhooshLink,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/i18n_extension.dart';
|
||||
import 'package:bike_control/widgets/ui/connection_method.dart';
|
||||
import 'package:bike_control/widgets/ui/toast.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
class OpenBikeControlBluetoothTile extends StatefulWidget {
|
||||
@@ -45,7 +45,6 @@ class _OpenBikeProtocolTileState extends State<OpenBikeControlBluetoothTile> {
|
||||
recordError(e, s, context: 'OBP BLE Emulator');
|
||||
core.settings.setObpBleEnabled(false);
|
||||
buildToast(
|
||||
context,
|
||||
level: LogLevel.LOGLEVEL_WARNING,
|
||||
title: context.i18n.errorStartingOpenBikeControlBluetoothServer,
|
||||
);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bike_control/utils/keymap/keymap.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../utils/keymap/apps/custom_app.dart';
|
||||
|
||||
@@ -152,8 +154,48 @@ class _HotKeyListenerState extends State<HotKeyListenerDialog> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 20,
|
||||
children: [
|
||||
Text(AppLocalizations.current.pressKeyToAssign(_pressedButton.toString())),
|
||||
Text(
|
||||
AppLocalizations.current.pressKeyToAssign(_pressedButton?.displayName ?? _pressedButton.toString()),
|
||||
),
|
||||
Text(_formatKey(_pressedKey)),
|
||||
if (kDebugMode && (Platform.isAndroid || Platform.isIOS))
|
||||
SizedBox(
|
||||
height: 300,
|
||||
width: 300,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: LogicalKeyboardKey.knownLogicalKeys
|
||||
.map(
|
||||
(key) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
minVerticalPadding: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
Chip(label: Text(key.keyLabel)),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_pressedKey = KeyDownEvent(
|
||||
physicalKey: PhysicalKeyboardKey(0x80),
|
||||
logicalKey: key,
|
||||
character: null,
|
||||
timeStamp: Duration.zero,
|
||||
);
|
||||
widget.customApp.setKey(
|
||||
_pressedButton!,
|
||||
physicalKey: _pressedKey!.physicalKey,
|
||||
logicalKey: key,
|
||||
modifiers: _activeModifiers.toList(),
|
||||
touchPosition: widget.keyPair?.touchPosition,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/i18n_extension.dart';
|
||||
import 'package:bike_control/utils/iap/iap_manager.dart';
|
||||
@@ -280,7 +279,6 @@ class _IAPStatusWidgetState extends State<IAPStatusWidget> {
|
||||
if (redeemed) {
|
||||
await IAPManager.instance.redeem(purchaseId);
|
||||
buildToast(
|
||||
context,
|
||||
title: 'Success',
|
||||
subtitle: 'Purchase redeemed successfully!',
|
||||
);
|
||||
@@ -469,7 +467,6 @@ class _IAPStatusWidgetState extends State<IAPStatusWidget> {
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
buildToast(
|
||||
navigatorKey.currentContext!,
|
||||
title: 'Error',
|
||||
subtitle: 'An error occurred: $e',
|
||||
);
|
||||
|
||||
53
lib/widgets/keyboard_pair_widget.dart
Normal file
53
lib/widgets/keyboard_pair_widget.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/widgets/ui/connection_method.dart';
|
||||
import 'package:prop/prop.dart' show LogLevel;
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
class RemoteKeyboardPairingWidget extends StatefulWidget {
|
||||
const RemoteKeyboardPairingWidget({super.key});
|
||||
|
||||
@override
|
||||
State<RemoteKeyboardPairingWidget> createState() => _PairWidgetState();
|
||||
}
|
||||
|
||||
class _PairWidgetState extends State<RemoteKeyboardPairingWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: core.remoteKeyboardPairing.isStarted,
|
||||
builder: (context, isStarted, child) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: core.remoteKeyboardPairing.isConnected,
|
||||
builder: (context, isConnected, child) {
|
||||
return ConnectionMethod(
|
||||
supportedActions: null,
|
||||
isEnabled: core.logic.isRemoteKeyboardControlEnabled,
|
||||
isStarted: isStarted,
|
||||
showTroubleshooting: true,
|
||||
type: ConnectionMethodType.bluetooth,
|
||||
instructionLink: 'https://youtube.com/shorts/qalBSiAz7wg',
|
||||
title: AppLocalizations.of(context).actAsBluetoothKeyboard,
|
||||
description: AppLocalizations.of(context).bluetoothKeyboardExplanation,
|
||||
isConnected: isConnected,
|
||||
requirements: core.permissions.getRemoteControlRequirements(),
|
||||
onChange: (value) async {
|
||||
core.settings.setRemoteKeyboardControlEnabled(value);
|
||||
if (!value) {
|
||||
core.remoteKeyboardPairing.stopAdvertising();
|
||||
} else {
|
||||
core.remoteKeyboardPairing.startAdvertising().catchError((e) {
|
||||
core.settings.setRemoteControlEnabled(false);
|
||||
core.connection.signalNotification(AlertNotification(LogLevel.LOGLEVEL_ERROR, e.toString()));
|
||||
});
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class _KeymapExplanationState extends State<KeymapExplanation> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final keyButtonMap = core.connection.devices.associateWith((d) {
|
||||
final keyButtonMap = core.connection.controllerDevices.associateWith((d) {
|
||||
final allButtons = d.availableButtons;
|
||||
return widget.keymap.keyPairs
|
||||
.whereNot(
|
||||
@@ -175,7 +175,7 @@ class _KeymapExplanationState extends State<KeymapExplanation> {
|
||||
skipName: '$currentProfile (Copy)',
|
||||
);
|
||||
if (newName != null && context.mounted) {
|
||||
buildToast(context, title: context.i18n.createdNewCustomProfile(newName));
|
||||
buildToast(title: context.i18n.createdNewCustomProfile(newName));
|
||||
selectedKeyPair = core.actionHandler.supportedApp!.keymap.keyPairs.firstWhere(
|
||||
(e) => e == keyPair,
|
||||
);
|
||||
|
||||
@@ -69,7 +69,7 @@ class _LogviewerState extends State<LogViewer> {
|
||||
.join('\n');
|
||||
Clipboard.setData(ClipboardData(text: logText));
|
||||
|
||||
buildToast(context, title: context.i18n.logsHaveBeenCopiedToClipboard);
|
||||
buildToast(title: context.i18n.logsHaveBeenCopiedToClipboard);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/devices/zwift/zwift_clickv2.dart';
|
||||
import 'package:bike_control/pages/login.dart';
|
||||
import 'package:bike_control/pages/markdown.dart';
|
||||
import 'package:bike_control/pages/navigation.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
@@ -149,6 +150,16 @@ class BKMenuButton extends StatelessWidget {
|
||||
core.connection.disconnectAll();
|
||||
},
|
||||
),
|
||||
MenuButton(
|
||||
child: Text('Login'),
|
||||
onPressed: (c) async {
|
||||
openDrawer(
|
||||
context: context,
|
||||
builder: (c) => LoginPage(),
|
||||
position: OverlayPosition.bottom,
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuDivider(),
|
||||
],
|
||||
if (currentPage == BCPage.logs) ...[
|
||||
|
||||
@@ -9,14 +9,14 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
import '../utils/requirements/multi.dart';
|
||||
|
||||
class RemotePairingWidget extends StatefulWidget {
|
||||
const RemotePairingWidget({super.key});
|
||||
class RemoteMousePairingWidget extends StatefulWidget {
|
||||
const RemoteMousePairingWidget({super.key});
|
||||
|
||||
@override
|
||||
State<RemotePairingWidget> createState() => _PairWidgetState();
|
||||
State<RemoteMousePairingWidget> createState() => _PairWidgetState();
|
||||
}
|
||||
|
||||
class _PairWidgetState extends State<RemotePairingWidget> {
|
||||
class _PairWidgetState extends State<RemoteMousePairingWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart' as actions;
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/i18n_extension.dart';
|
||||
@@ -12,6 +11,7 @@ import 'package:bike_control/widgets/ui/button_widget.dart';
|
||||
import 'package:bike_control/widgets/ui/toast.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
import '../bluetooth/messages/notification.dart';
|
||||
@@ -104,13 +104,12 @@ class _TestbedState extends State<Testbed> with SingleTickerProviderStateMixin,
|
||||
_keys.removeLast();
|
||||
}
|
||||
} else if (core.actionHandler.supportedApp == null) {
|
||||
buildToast(context, level: LogLevel.LOGLEVEL_WARNING, title: context.i18n.selectTrainerAppAndTarget);
|
||||
buildToast(level: LogLevel.LOGLEVEL_WARNING, title: context.i18n.selectTrainerAppAndTarget);
|
||||
} else {
|
||||
final button = data.buttonsClicked.first;
|
||||
if (core.actionHandler.supportedApp is! CustomApp &&
|
||||
core.actionHandler.supportedApp?.keymap.getKeyPair(button) == null) {
|
||||
buildToast(
|
||||
context,
|
||||
level: LogLevel.LOGLEVEL_WARNING,
|
||||
titleWidget: Text.rich(
|
||||
TextSpan(
|
||||
@@ -146,7 +145,6 @@ class _TestbedState extends State<Testbed> with SingleTickerProviderStateMixin,
|
||||
}
|
||||
} else if (data is ActionNotification && data.result is! actions.Ignored) {
|
||||
buildToast(
|
||||
context,
|
||||
location: ToastLocation.bottomLeft,
|
||||
level: data.result is actions.Error ? LogLevel.LOGLEVEL_WARNING : LogLevel.LOGLEVEL_INFO,
|
||||
title: data.result.message,
|
||||
@@ -154,7 +152,6 @@ class _TestbedState extends State<Testbed> with SingleTickerProviderStateMixin,
|
||||
);
|
||||
} else if (data is AlertNotification) {
|
||||
buildToast(
|
||||
context,
|
||||
location: ToastLocation.bottomRight,
|
||||
level: data.level,
|
||||
title: data.alertMessage,
|
||||
|
||||
@@ -91,23 +91,29 @@ class _AppTitleState extends State<AppTitle> with WidgetsBindingObserver {
|
||||
return;
|
||||
} else if (updater.isAvailable) {
|
||||
final updateStatus = await updater.checkForUpdate();
|
||||
core.connection.signalNotification(LogNotification('Shorebird update status: $updateStatus'));
|
||||
if (updateStatus == UpdateStatus.outdated) {
|
||||
updater
|
||||
.update()
|
||||
.then((value) {
|
||||
.then((value) async {
|
||||
setState(() {
|
||||
_updateType = UpdateType.shorebird;
|
||||
});
|
||||
final nextPatch = await updater.readNextPatch();
|
||||
final currentVersion = Version.parse(packageInfoValue!.version);
|
||||
setState(() {
|
||||
_newVersion = Version(
|
||||
currentVersion.major,
|
||||
currentVersion.minor,
|
||||
currentVersion.patch,
|
||||
build: nextPatch?.number.toString() ?? '',
|
||||
);
|
||||
});
|
||||
})
|
||||
.catchError((e) {
|
||||
buildToast(context, title: AppLocalizations.current.failedToUpdate(e.toString()));
|
||||
buildToast(title: AppLocalizations.current.failedToUpdate(e.toString()));
|
||||
});
|
||||
} else if (updateStatus == UpdateStatus.restartRequired) {
|
||||
if (Platform.isIOS) {
|
||||
// TODO other platforms can't be trusted https://github.com/shorebirdtech/shorebird/issues/3498
|
||||
_updateType = UpdateType.shorebird;
|
||||
}
|
||||
_updateType = UpdateType.shorebird;
|
||||
}
|
||||
if (_updateType == UpdateType.shorebird) {
|
||||
final nextPatch = await updater.readNextPatch();
|
||||
@@ -212,7 +218,7 @@ class _AppTitleState extends State<AppTitle> with WidgetsBindingObserver {
|
||||
await _shorebirdRestart();
|
||||
} else if (_updateType == UpdateType.playStore) {
|
||||
await launchUrlString(
|
||||
'https://play.google.com/store/apps/details?id=org.jonasbark.swiftcontrol',
|
||||
'https://play.google.com/store/apps/details?id=de.jonasbark.swiftcontrol',
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
} else if (_updateType == UpdateType.appStore) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:bike_control/widgets/ui/toast.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
enum ConnectionMethodType {
|
||||
bluetooth,
|
||||
@@ -91,7 +92,7 @@ class _ConnectionMethodState extends State<ConnectionMethod> with WidgetsBinding
|
||||
return SelectableCard(
|
||||
onPressed: () {
|
||||
if (kIsWeb) {
|
||||
buildToast(context, title: 'Not Supported on Web :)');
|
||||
buildToast(title: 'Not Supported on Web :)');
|
||||
} else if (widget.requirements.isEmpty) {
|
||||
widget.onChange(!widget.isEnabled);
|
||||
} else {
|
||||
@@ -163,13 +164,19 @@ class _ConnectionMethodState extends State<ConnectionMethod> with WidgetsBinding
|
||||
style: widget.isEnabled && Theme.of(context).brightness == Brightness.light
|
||||
? ButtonStyle.outline().withBorder(border: Border.all(color: Colors.gray.shade500))
|
||||
: ButtonStyle.outline(),
|
||||
leading: Icon(Icons.help_outline),
|
||||
leading: Icon(
|
||||
widget.instructionLink!.contains("youtube") ? Icons.ondemand_video : Icons.help_outline,
|
||||
),
|
||||
onPressed: () {
|
||||
openDrawer(
|
||||
context: context,
|
||||
position: OverlayPosition.bottom,
|
||||
builder: (c) => MarkdownPage(assetPath: widget.instructionLink!),
|
||||
);
|
||||
if (widget.instructionLink!.contains("youtube")) {
|
||||
launchUrlString(widget.instructionLink!);
|
||||
} else {
|
||||
openDrawer(
|
||||
context: context,
|
||||
position: OverlayPosition.bottom,
|
||||
builder: (c) => MarkdownPage(assetPath: widget.instructionLink!),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).instructions),
|
||||
),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/widgets/ui/button_widget.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
void buildToast(
|
||||
BuildContext context, {
|
||||
void buildToast({
|
||||
LogLevel level = LogLevel.LOGLEVEL_INFO,
|
||||
String? title,
|
||||
Widget? titleWidget,
|
||||
@@ -13,45 +13,47 @@ void buildToast(
|
||||
String? subtitle,
|
||||
Duration? duration,
|
||||
}) {
|
||||
showToast(
|
||||
context: context,
|
||||
location: location,
|
||||
showDuration: switch (level) {
|
||||
LogLevel.LOGLEVEL_DEBUG => const Duration(seconds: 2),
|
||||
LogLevel.LOGLEVEL_INFO => duration ?? const Duration(seconds: 3),
|
||||
LogLevel.LOGLEVEL_WARNING => duration ?? const Duration(seconds: 5),
|
||||
LogLevel.LOGLEVEL_ERROR => duration ?? const Duration(seconds: 7),
|
||||
_ => duration ?? const Duration(seconds: 3),
|
||||
},
|
||||
builder: (context, overlay) => SurfaceCard(
|
||||
filled: switch (level) {
|
||||
LogLevel.LOGLEVEL_WARNING => true,
|
||||
LogLevel.LOGLEVEL_ERROR => true,
|
||||
_ => false,
|
||||
if (navigatorKey.currentContext?.mounted ?? false) {
|
||||
showToast(
|
||||
context: navigatorKey.currentContext!,
|
||||
location: location,
|
||||
showDuration: switch (level) {
|
||||
LogLevel.LOGLEVEL_DEBUG => const Duration(seconds: 2),
|
||||
LogLevel.LOGLEVEL_INFO => duration ?? const Duration(seconds: 3),
|
||||
LogLevel.LOGLEVEL_WARNING => duration ?? const Duration(seconds: 5),
|
||||
LogLevel.LOGLEVEL_ERROR => duration ?? const Duration(seconds: 7),
|
||||
_ => duration ?? const Duration(seconds: 3),
|
||||
},
|
||||
fillColor: switch (level) {
|
||||
LogLevel.LOGLEVEL_DEBUG => null,
|
||||
LogLevel.LOGLEVEL_INFO => null,
|
||||
LogLevel.LOGLEVEL_WARNING => Theme.of(context).colorScheme.chart1,
|
||||
LogLevel.LOGLEVEL_ERROR => Theme.of(context).colorScheme.destructive,
|
||||
_ => null,
|
||||
},
|
||||
child: Basic(
|
||||
title: titleWidget ?? Text(title ?? ''),
|
||||
subtitle: subtitle != null ? Text(subtitle) : null,
|
||||
trailing: titleWidget is ButtonWidget
|
||||
? null
|
||||
: PrimaryButton(
|
||||
size: ButtonSize.small,
|
||||
onPressed: () {
|
||||
// Close the toast programmatically when clicking Undo.
|
||||
overlay.close();
|
||||
onClose?.call();
|
||||
},
|
||||
child: Text(closeTitle),
|
||||
),
|
||||
trailingAlignment: Alignment.center,
|
||||
builder: (context, overlay) => SurfaceCard(
|
||||
filled: switch (level) {
|
||||
LogLevel.LOGLEVEL_WARNING => true,
|
||||
LogLevel.LOGLEVEL_ERROR => true,
|
||||
_ => false,
|
||||
},
|
||||
fillColor: switch (level) {
|
||||
LogLevel.LOGLEVEL_DEBUG => null,
|
||||
LogLevel.LOGLEVEL_INFO => null,
|
||||
LogLevel.LOGLEVEL_WARNING => Theme.of(context).colorScheme.chart1,
|
||||
LogLevel.LOGLEVEL_ERROR => Theme.of(context).colorScheme.destructive,
|
||||
_ => null,
|
||||
},
|
||||
child: Basic(
|
||||
title: titleWidget ?? Text(title ?? ''),
|
||||
subtitle: subtitle != null ? Text(subtitle) : null,
|
||||
trailing: titleWidget is ButtonWidget
|
||||
? null
|
||||
: PrimaryButton(
|
||||
size: ButtonSize.small,
|
||||
onPressed: () {
|
||||
// Close the toast programmatically when clicking Undo.
|
||||
overlay.close();
|
||||
onClose?.call();
|
||||
},
|
||||
child: Text(closeTitle),
|
||||
),
|
||||
trailingAlignment: Alignment.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import app_links
|
||||
import audio_session
|
||||
import bluetooth_low_energy_darwin
|
||||
import device_info_plus
|
||||
import file_selector_macos
|
||||
@@ -12,9 +14,11 @@ import flutter_local_notifications
|
||||
import flutter_secure_storage_darwin
|
||||
import flutter_volume_controller
|
||||
import gamepads_darwin
|
||||
import google_sign_in_ios
|
||||
import in_app_purchase_storekit
|
||||
import in_app_review
|
||||
import ios_receipt
|
||||
import just_audio
|
||||
import keypress_simulator_macos
|
||||
import media_key_detector_macos
|
||||
import nsd_macos
|
||||
@@ -22,12 +26,15 @@ import package_info_plus
|
||||
import purchases_flutter
|
||||
import screen_retriever_macos
|
||||
import shared_preferences_foundation
|
||||
import sign_in_with_apple
|
||||
import universal_ble
|
||||
import url_launcher_macos
|
||||
import wakelock_plus
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||
BluetoothLowEnergyDarwinPlugin.register(with: registry.registrar(forPlugin: "BluetoothLowEnergyDarwinPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
@@ -35,9 +42,11 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||
FlutterVolumeControllerPlugin.register(with: registry.registrar(forPlugin: "FlutterVolumeControllerPlugin"))
|
||||
GamepadsDarwinPlugin.register(with: registry.registrar(forPlugin: "GamepadsDarwinPlugin"))
|
||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||
InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin"))
|
||||
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
||||
IosReceiptPlugin.register(with: registry.registrar(forPlugin: "IosReceiptPlugin"))
|
||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||
KeypressSimulatorMacosPlugin.register(with: registry.registrar(forPlugin: "KeypressSimulatorMacosPlugin"))
|
||||
MediaKeyDetectorPlugin.register(with: registry.registrar(forPlugin: "MediaKeyDetectorPlugin"))
|
||||
NsdMacosPlugin.register(with: registry.registrar(forPlugin: "NsdMacosPlugin"))
|
||||
@@ -45,6 +54,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
PurchasesFlutterPlugin.register(with: registry.registrar(forPlugin: "PurchasesFlutterPlugin"))
|
||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
|
||||
UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
PODS:
|
||||
- app_links (6.4.1):
|
||||
- FlutterMacOS
|
||||
- AppAuth (2.0.0):
|
||||
- AppAuth/Core (= 2.0.0)
|
||||
- AppAuth/ExternalUserAgent (= 2.0.0)
|
||||
- AppAuth/Core (2.0.0)
|
||||
- AppAuth/ExternalUserAgent (2.0.0):
|
||||
- AppAuth/Core
|
||||
- AppCheckCore (11.2.0):
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- audio_session (0.0.1):
|
||||
- FlutterMacOS
|
||||
- bluetooth_low_energy_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -16,6 +30,33 @@ PODS:
|
||||
- FlutterMacOS (1.0.0)
|
||||
- gamepads_darwin (0.1.1):
|
||||
- FlutterMacOS
|
||||
- google_sign_in_ios (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleSignIn (~> 9.0)
|
||||
- GTMSessionFetcher (>= 3.4.0)
|
||||
- GoogleSignIn (9.1.0):
|
||||
- AppAuth (~> 2.0)
|
||||
- AppCheckCore (~> 11.0)
|
||||
- GTMAppAuth (~> 5.0)
|
||||
- GTMSessionFetcher/Core (~> 3.3)
|
||||
- GoogleUtilities/Environment (8.1.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Logger (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (8.1.0)
|
||||
- GoogleUtilities/UserDefaults (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GTMAppAuth (5.0.0):
|
||||
- AppAuth/Core (~> 2.0)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
|
||||
- GTMSessionFetcher (3.5.0):
|
||||
- GTMSessionFetcher/Full (= 3.5.0)
|
||||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- GTMSessionFetcher/Full (3.5.0):
|
||||
- GTMSessionFetcher/Core
|
||||
- in_app_purchase_storekit (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -23,6 +64,9 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- ios_receipt (0.0.1):
|
||||
- FlutterMacOS
|
||||
- just_audio (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- keypress_simulator_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- media_key_detector_macos (0.0.1):
|
||||
@@ -31,6 +75,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- package_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.0)
|
||||
- purchases_flutter (9.10.6):
|
||||
- FlutterMacOS
|
||||
- PurchasesHybridCommon (= 17.27.1)
|
||||
@@ -42,6 +87,8 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sign_in_with_apple (0.0.1):
|
||||
- FlutterMacOS
|
||||
- universal_ble (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -53,6 +100,8 @@ PODS:
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
|
||||
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
|
||||
- bluetooth_low_energy_darwin (from `Flutter/ephemeral/.symlinks/plugins/bluetooth_low_energy_darwin/darwin`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
@@ -61,9 +110,11 @@ DEPENDENCIES:
|
||||
- flutter_volume_controller (from `Flutter/ephemeral/.symlinks/plugins/flutter_volume_controller/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- gamepads_darwin (from `Flutter/ephemeral/.symlinks/plugins/gamepads_darwin/macos`)
|
||||
- google_sign_in_ios (from `Flutter/ephemeral/.symlinks/plugins/google_sign_in_ios/darwin`)
|
||||
- in_app_purchase_storekit (from `Flutter/ephemeral/.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
||||
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
||||
- ios_receipt (from `Flutter/ephemeral/.symlinks/plugins/ios_receipt/macos`)
|
||||
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`)
|
||||
- keypress_simulator_macos (from `Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos`)
|
||||
- media_key_detector_macos (from `Flutter/ephemeral/.symlinks/plugins/media_key_detector_macos/macos`)
|
||||
- nsd_macos (from `Flutter/ephemeral/.symlinks/plugins/nsd_macos/macos`)
|
||||
@@ -71,6 +122,7 @@ DEPENDENCIES:
|
||||
- purchases_flutter (from `Flutter/ephemeral/.symlinks/plugins/purchases_flutter/macos`)
|
||||
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sign_in_with_apple (from `Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos`)
|
||||
- universal_ble (from `Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
@@ -78,10 +130,21 @@ DEPENDENCIES:
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- AppAuth
|
||||
- AppCheckCore
|
||||
- GoogleSignIn
|
||||
- GoogleUtilities
|
||||
- GTMAppAuth
|
||||
- GTMSessionFetcher
|
||||
- PromisesObjC
|
||||
- PurchasesHybridCommon
|
||||
- RevenueCat
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
|
||||
audio_session:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
|
||||
bluetooth_low_energy_darwin:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/bluetooth_low_energy_darwin/darwin
|
||||
device_info_plus:
|
||||
@@ -98,12 +161,16 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral
|
||||
gamepads_darwin:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/gamepads_darwin/macos
|
||||
google_sign_in_ios:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/google_sign_in_ios/darwin
|
||||
in_app_purchase_storekit:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/in_app_purchase_storekit/darwin
|
||||
in_app_review:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
|
||||
ios_receipt:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/ios_receipt/macos
|
||||
just_audio:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin
|
||||
keypress_simulator_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos
|
||||
media_key_detector_macos:
|
||||
@@ -118,6 +185,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
sign_in_with_apple:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos
|
||||
universal_ble:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin
|
||||
url_launcher_macos:
|
||||
@@ -128,6 +197,10 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: c3185399a5cabc2e610ee5ad52fb7269b84ff869
|
||||
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
|
||||
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
|
||||
audio_session: 728ae3823d914f809c485d390274861a24b0904e
|
||||
bluetooth_low_energy_darwin: 50bc79258e60586e4c4bed5948bd31d925f37fac
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
file_selector_macos: 3e56eaea051180007b900eacb006686fd54da150
|
||||
@@ -136,18 +209,26 @@ SPEC CHECKSUMS:
|
||||
flutter_volume_controller: 25d09126b0d695560f11c80b1311d5063fed882f
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
gamepads_darwin: 07af6c60c282902b66574c800e20b2b26e68fda8
|
||||
google_sign_in_ios: 7336a3372ea93ea56a21e126a0055ffca3723601
|
||||
GoogleSignIn: fcee2257188d5eda57a5e2b6a715550ffff9206d
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
in_app_purchase_storekit: 2342c0a5da86593124d08dd13d920f39a52b273a
|
||||
in_app_review: 866c9b17c87a7b46a395bda43f5d3ca02deb585a
|
||||
ios_receipt: 8741a75f39e6ca0866313b73c69a5b674cf5c98c
|
||||
just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79
|
||||
keypress_simulator_macos: f8556f9101f9f2f175652e0bceddf0fe82a4c6b2
|
||||
media_key_detector_macos: a93757a483b4b47283ade432b1af9e427c47329f
|
||||
nsd_macos: 1a38a38a33adbb396b4c6f303bc076073514cadc
|
||||
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
purchases_flutter: 55ed35144683d8673f0a4b5077e8f4d8291f291e
|
||||
PurchasesHybridCommon: 027f03312519c51056457eb2e4f7ee1c91b61b8f
|
||||
RevenueCat: ecbba580fa453b0d4a0475449b904196d74ef678
|
||||
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
|
||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||
sign_in_with_apple: a9e97e744e8edc36aefc2723111f652102a7a727
|
||||
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
|
||||
url_launcher_macos: 175a54c831f4375a6cf895875f716ee5af3888ce
|
||||
wakelock_plus: 9d63063ffb7af1c215209769067c57103bde719d
|
||||
|
||||
@@ -240,6 +240,7 @@
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||
416B733CA2212067A6157BC2 /* [CP] Embed Pods Frameworks */,
|
||||
64E1C1C2337BD399A29F09AF /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -398,6 +399,23 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
64E1C1C2337BD399A29F09AF /* [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;
|
||||
};
|
||||
DDE99C1FE0195AFA9D281938 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
import app_links
|
||||
|
||||
@main
|
||||
class AppDelegate: FlutterAppDelegate {
|
||||
|
||||
public override func application(_ application: NSApplication,
|
||||
continue userActivity: NSUserActivity,
|
||||
restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void) -> Bool {
|
||||
|
||||
guard let url = AppLinks.shared.getUniversalLink(userActivity) else {
|
||||
return false
|
||||
}
|
||||
|
||||
AppLinks.shared.handleLink(link: url.absoluteString)
|
||||
|
||||
return false // Returning true will stop the propagation to other packages
|
||||
}
|
||||
|
||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -2,19 +2,25 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.bluetooth</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.google.GIDSignIn</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -48,5 +48,19 @@
|
||||
<string>NSApplication</string>
|
||||
<key>NSAccessibilityUsageDescription</key>
|
||||
<string>BikeControl needs to send keys to your trainer app.</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.709945926587-0iierajthibf4vhqf85fc7bbpgbdgua2</string>
|
||||
<string>bikecontrol</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>GIDClientID</key>
|
||||
<string>709945926587-0iierajthibf4vhqf85fc7bbpgbdgua2.apps.googleusercontent.com</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -2,19 +2,25 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.bluetooth</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.google.GIDSignIn</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -21,24 +21,12 @@ class MediaKeyDetector {
|
||||
_platform.addListener(listener);
|
||||
}
|
||||
|
||||
/// Listen for the media key event with device information
|
||||
void addListenerWithDevice(void Function(MediaKey mediaKey, String deviceId) listener) {
|
||||
_lazilyInitialize();
|
||||
_platform.addListenerWithDevice(listener);
|
||||
}
|
||||
|
||||
/// Remove the previously registered listener
|
||||
void removeListener(void Function(MediaKey mediaKey) listener) {
|
||||
_lazilyInitialize();
|
||||
_platform.removeListener(listener);
|
||||
}
|
||||
|
||||
/// Remove the previously registered listener with device information
|
||||
void removeListenerWithDevice(void Function(MediaKey mediaKey, String deviceId) listener) {
|
||||
_lazilyInitialize();
|
||||
_platform.removeListenerWithDevice(listener);
|
||||
}
|
||||
|
||||
void _lazilyInitialize() {
|
||||
if (!_initialized) {
|
||||
_platform.initialize();
|
||||
|
||||
@@ -50,7 +50,6 @@ abstract class MediaKeyDetectorPlatform extends PlatformInterface {
|
||||
void initialize();
|
||||
|
||||
final List<void Function(MediaKey mediaKey)> _listeners = [];
|
||||
final List<void Function(MediaKey mediaKey, String deviceId)> _listenersWithDevice = [];
|
||||
|
||||
/// Listen for the media key event
|
||||
void addListener(void Function(MediaKey mediaKey) listener) {
|
||||
@@ -59,33 +58,16 @@ abstract class MediaKeyDetectorPlatform extends PlatformInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/// Listen for the media key event with device information
|
||||
void addListenerWithDevice(void Function(MediaKey mediaKey, String deviceId) listener) {
|
||||
if (!_listenersWithDevice.contains(listener)) {
|
||||
_listenersWithDevice.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the previously registered listener
|
||||
void removeListener(void Function(MediaKey mediaKey) listener) {
|
||||
_listeners.remove(listener);
|
||||
}
|
||||
|
||||
/// Remove the previously registered listener with device information
|
||||
void removeListenerWithDevice(void Function(MediaKey mediaKey, String deviceId) listener) {
|
||||
_listenersWithDevice.remove(listener);
|
||||
}
|
||||
|
||||
/// Trigger all listeners to indicate that the specified media key was pressed
|
||||
void triggerListeners(MediaKey mediaKey, [String? deviceId]) {
|
||||
void triggerListeners(MediaKey mediaKey) {
|
||||
for (final l in _listeners) {
|
||||
l(mediaKey);
|
||||
}
|
||||
if (deviceId != null) {
|
||||
for (final l in _listenersWithDevice) {
|
||||
l(mediaKey, deviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Map<LogicalKeyboardKey, MediaKey> _keyMap = {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export './media_key.dart' show MediaKey;
|
||||
export './media_key_event.dart' show MediaKeyEvent;
|
||||
export './method_channel_media_key_detector.dart'
|
||||
show MethodChannelMediaKeyDetector;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/// Represents a media key event with device information
|
||||
class MediaKeyEvent {
|
||||
/// Creates a media key event
|
||||
const MediaKeyEvent({
|
||||
required this.key,
|
||||
required this.deviceId,
|
||||
});
|
||||
|
||||
/// The media key that was pressed
|
||||
final String key;
|
||||
|
||||
/// The unique identifier of the device that sent the event
|
||||
final String deviceId;
|
||||
|
||||
@override
|
||||
String toString() => 'MediaKeyEvent(key: $key, deviceId: $deviceId)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is MediaKeyEvent &&
|
||||
runtimeType == other.runtimeType &&
|
||||
key == other.key &&
|
||||
deviceId == other.deviceId;
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ deviceId.hashCode;
|
||||
}
|
||||
@@ -1,12 +1,3 @@
|
||||
# 0.0.3
|
||||
|
||||
- **NEW**: Add device source detection using Windows Raw Input API
|
||||
- Media key events now include unique device identifier
|
||||
- Enables distinguishing between multiple bluetooth media controllers
|
||||
- Adds `addListenerWithDevice` API for device-aware event handling
|
||||
- Maintains backward compatibility with existing `addListener` API
|
||||
- Falls back to RegisterHotKey API for compatibility
|
||||
|
||||
# 0.0.2
|
||||
|
||||
- Implement global media key detection using Windows RegisterHotKey API
|
||||
|
||||
@@ -6,7 +6,7 @@ The windows implementation of `media_key_detector`.
|
||||
|
||||
## Features
|
||||
|
||||
This plugin provides global media key detection on Windows with device source identification. This allows your application to respond to media keys (play/pause, next track, previous track, volume up, volume down) from multiple devices and distinguish which device sent each event.
|
||||
This plugin provides global media key detection on Windows using the Windows `RegisterHotKey` API. This allows your application to respond to media keys (play/pause, next track, previous track, volume up, volume down) even when it's not the focused application.
|
||||
|
||||
### Supported Media Keys
|
||||
|
||||
@@ -19,33 +19,17 @@ This plugin provides global media key detection on Windows with device source id
|
||||
### Implementation Details
|
||||
|
||||
The plugin uses:
|
||||
- **Raw Input API** for device-specific media key detection (primary method)
|
||||
- `RegisterHotKey` Windows API for global hotkey registration (fallback)
|
||||
- Event channels for communicating media key events with device information to Dart
|
||||
- Window message handlers to process WM_INPUT and WM_HOTKEY messages
|
||||
- `RegisterHotKey` Windows API for global hotkey registration
|
||||
- Event channels for communicating media key events to Dart
|
||||
- Window message handlers to process WM_HOTKEY messages
|
||||
|
||||
The Raw Input API allows the plugin to identify which physical device (e.g., keyboard, bluetooth remote) sent the media key event. This enables users with multiple media controllers to configure different actions for each device.
|
||||
|
||||
Hotkeys and raw input are registered when `setIsPlaying(true)` is called and automatically unregistered when `setIsPlaying(false)` is called or when the plugin is destroyed.
|
||||
|
||||
### Device Source Detection
|
||||
|
||||
When a media key is pressed, the plugin provides:
|
||||
- The media key that was pressed (e.g., playPause, fastForward)
|
||||
- The unique device identifier of the source device
|
||||
|
||||
This enables scenarios where:
|
||||
- A user has two bluetooth media remotes
|
||||
- Both remotes have a "play" button
|
||||
- Each remote can be configured to trigger different actions
|
||||
Hotkeys are registered when `setIsPlaying(true)` is called and automatically unregistered when `setIsPlaying(false)` is called or when the plugin is destroyed.
|
||||
|
||||
## Usage
|
||||
|
||||
This package is [endorsed][endorsed_link], which means you can simply use `media_key_detector`
|
||||
normally. This package will be automatically included in your app when you do.
|
||||
|
||||
### Basic Usage (without device information)
|
||||
|
||||
```dart
|
||||
import 'package:media_key_detector/media_key_detector.dart';
|
||||
|
||||
@@ -74,35 +58,6 @@ mediaKeyDetector.addListener((MediaKey key) {
|
||||
});
|
||||
```
|
||||
|
||||
### Advanced Usage (with device identification)
|
||||
|
||||
```dart
|
||||
import 'package:media_key_detector/media_key_detector.dart';
|
||||
|
||||
// Enable media key detection
|
||||
mediaKeyDetector.setIsPlaying(isPlaying: true);
|
||||
|
||||
// Listen for media key events with device information
|
||||
mediaKeyDetector.addListenerWithDevice((MediaKey key, String deviceId) {
|
||||
// deviceId contains the unique identifier of the device that sent the event
|
||||
// For example: "\\?\HID#VID_046D&PID_C52B&MI_00#..."
|
||||
|
||||
print('Media key $key pressed by device: $deviceId');
|
||||
|
||||
// Configure different actions based on device
|
||||
if (deviceId.contains('VID_046D')) {
|
||||
// Handle keys from Logitech device
|
||||
handleLogitechRemote(key);
|
||||
} else if (deviceId.contains('VID_05AC')) {
|
||||
// Handle keys from Apple device
|
||||
handleAppleKeyboard(key);
|
||||
} else {
|
||||
// Handle keys from other devices
|
||||
handleGenericDevice(key);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
[endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
|
||||
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
|
||||
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
|
||||
|
||||
@@ -19,26 +19,13 @@ class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform {
|
||||
@override
|
||||
void initialize() {
|
||||
_eventChannel.receiveBroadcastStream().listen((event) {
|
||||
final keyIdx = event as int;
|
||||
MediaKey? key;
|
||||
String? deviceId;
|
||||
|
||||
// Check if event is a map (new format with device info)
|
||||
if (event is Map) {
|
||||
final keyIdx = event['key'] as int?;
|
||||
deviceId = event['device'] as String?;
|
||||
|
||||
if (keyIdx != null && keyIdx > -1 && keyIdx < MediaKey.values.length) {
|
||||
key = MediaKey.values[keyIdx];
|
||||
}
|
||||
} else if (event is int) {
|
||||
// Backward compatibility: old format with just key index
|
||||
if (event > -1 && event < MediaKey.values.length) {
|
||||
key = MediaKey.values[event];
|
||||
}
|
||||
if (keyIdx > -1 && keyIdx < MediaKey.values.length) {
|
||||
key = MediaKey.values[keyIdx];
|
||||
}
|
||||
|
||||
if (key != null) {
|
||||
triggerListeners(key, deviceId);
|
||||
triggerListeners(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -48,15 +44,6 @@ class MediaKeyDetectorWindows : public flutter::Plugin {
|
||||
// Unregister global hotkeys
|
||||
void UnregisterHotkeys();
|
||||
|
||||
// Register for raw input from keyboard devices
|
||||
void RegisterRawInput(HWND hwnd);
|
||||
|
||||
// Unregister raw input
|
||||
void UnregisterRawInput(HWND hwnd);
|
||||
|
||||
// Get device identifier from device handle
|
||||
std::string GetDeviceIdentifier(HANDLE hDevice);
|
||||
|
||||
// Handle Windows messages
|
||||
std::optional<LRESULT> HandleWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
|
||||
|
||||
@@ -65,10 +52,6 @@ class MediaKeyDetectorWindows : public flutter::Plugin {
|
||||
std::atomic<bool> is_playing_{false};
|
||||
int window_proc_id_ = -1;
|
||||
bool hotkeys_registered_ = false;
|
||||
bool raw_input_registered_ = false;
|
||||
|
||||
// Cache for device identifiers
|
||||
std::map<HANDLE, std::string> device_cache_;
|
||||
};
|
||||
|
||||
// static
|
||||
@@ -121,8 +104,6 @@ MediaKeyDetectorWindows::MediaKeyDetectorWindows(flutter::PluginRegistrarWindows
|
||||
}
|
||||
|
||||
MediaKeyDetectorWindows::~MediaKeyDetectorWindows() {
|
||||
HWND hwnd = registrar_->GetView()->GetNativeWindow();
|
||||
UnregisterRawInput(hwnd);
|
||||
UnregisterHotkeys();
|
||||
if (window_proc_id_ != -1) {
|
||||
registrar_->UnregisterTopLevelWindowProcDelegate(window_proc_id_);
|
||||
@@ -143,13 +124,10 @@ void MediaKeyDetectorWindows::HandleMethodCall(
|
||||
if (is_playing_it != arguments->end()) {
|
||||
if (auto* is_playing = std::get_if<bool>(&is_playing_it->second)) {
|
||||
is_playing_.store(*is_playing);
|
||||
HWND hwnd = registrar_->GetView()->GetNativeWindow();
|
||||
if (*is_playing) {
|
||||
RegisterHotkeys();
|
||||
RegisterRawInput(hwnd);
|
||||
} else {
|
||||
UnregisterHotkeys();
|
||||
UnregisterRawInput(hwnd);
|
||||
}
|
||||
result->Success();
|
||||
return;
|
||||
@@ -207,131 +185,8 @@ void MediaKeyDetectorWindows::UnregisterHotkeys() {
|
||||
hotkeys_registered_ = false;
|
||||
}
|
||||
|
||||
void MediaKeyDetectorWindows::RegisterRawInput(HWND hwnd) {
|
||||
if (raw_input_registered_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register for raw input from keyboard devices
|
||||
RAWINPUTDEVICE rid[1];
|
||||
|
||||
// Keyboard devices
|
||||
rid[0].usUsagePage = 0x01; // Generic Desktop Controls
|
||||
rid[0].usUsage = 0x06; // Keyboard
|
||||
rid[0].dwFlags = RIDEV_INPUTSINK; // Receive input even when not in foreground
|
||||
rid[0].hwndTarget = hwnd;
|
||||
|
||||
if (RegisterRawInputDevices(rid, 1, sizeof(rid[0]))) {
|
||||
raw_input_registered_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void MediaKeyDetectorWindows::UnregisterRawInput(HWND hwnd) {
|
||||
if (!raw_input_registered_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unregister raw input
|
||||
RAWINPUTDEVICE rid[1];
|
||||
|
||||
rid[0].usUsagePage = 0x01;
|
||||
rid[0].usUsage = 0x06;
|
||||
rid[0].dwFlags = RIDEV_REMOVE;
|
||||
rid[0].hwndTarget = nullptr;
|
||||
|
||||
RegisterRawInputDevices(rid, 1, sizeof(rid[0]));
|
||||
raw_input_registered_ = false;
|
||||
device_cache_.clear();
|
||||
}
|
||||
|
||||
std::string MediaKeyDetectorWindows::GetDeviceIdentifier(HANDLE hDevice) {
|
||||
// Check cache first
|
||||
auto it = device_cache_.find(hDevice);
|
||||
if (it != device_cache_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Get device name
|
||||
UINT size = 0;
|
||||
GetRawInputDeviceInfoA(hDevice, RIDI_DEVICENAME, nullptr, &size);
|
||||
|
||||
if (size == 0) {
|
||||
return "Unknown Device";
|
||||
}
|
||||
|
||||
std::vector<char> name(size);
|
||||
if (GetRawInputDeviceInfoA(hDevice, RIDI_DEVICENAME, name.data(), &size) == static_cast<UINT>(-1)) {
|
||||
return "Unknown Device";
|
||||
}
|
||||
|
||||
std::string deviceName(name.data());
|
||||
|
||||
// Cache the result
|
||||
device_cache_[hDevice] = deviceName;
|
||||
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
std::optional<LRESULT> MediaKeyDetectorWindows::HandleWindowProc(
|
||||
HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
||||
|
||||
// Handle raw input messages for device-specific detection
|
||||
if (message == WM_INPUT && event_sink_) {
|
||||
UINT dwSize;
|
||||
GetRawInputData((HRAWINPUT)lparam, RID_INPUT, nullptr, &dwSize, sizeof(RAWINPUTHEADER));
|
||||
|
||||
std::vector<BYTE> buffer(dwSize);
|
||||
if (GetRawInputData((HRAWINPUT)lparam, RID_INPUT, buffer.data(), &dwSize, sizeof(RAWINPUTHEADER)) != dwSize) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
RAWINPUT* raw = (RAWINPUT*)buffer.data();
|
||||
|
||||
if (raw->header.dwType == RIM_TYPEKEYBOARD) {
|
||||
RAWKEYBOARD& keyboard = raw->data.keyboard;
|
||||
|
||||
// Check for media keys
|
||||
int key_index = -1;
|
||||
|
||||
// Media keys have VKey codes
|
||||
// Check for key down event (RI_KEY_MAKE is defined as 0, so we check both explicitly)
|
||||
if (keyboard.Flags == RI_KEY_MAKE) { // Key down event
|
||||
switch (keyboard.VKey) {
|
||||
case VK_MEDIA_PLAY_PAUSE:
|
||||
key_index = 0; // MediaKey.playPause
|
||||
break;
|
||||
case VK_MEDIA_PREV_TRACK:
|
||||
key_index = 1; // MediaKey.rewind
|
||||
break;
|
||||
case VK_MEDIA_NEXT_TRACK:
|
||||
key_index = 2; // MediaKey.fastForward
|
||||
break;
|
||||
case VK_VOLUME_UP:
|
||||
key_index = 3; // MediaKey.volumeUp
|
||||
break;
|
||||
case VK_VOLUME_DOWN:
|
||||
key_index = 4; // MediaKey.volumeDown
|
||||
break;
|
||||
}
|
||||
|
||||
if (key_index >= 0) {
|
||||
// Get device identifier
|
||||
std::string deviceId = GetDeviceIdentifier(raw->header.hDevice);
|
||||
|
||||
// Send event with both key index and device identifier
|
||||
flutter::EncodableMap event_data;
|
||||
event_data[EncodableValue("key")] = EncodableValue(key_index);
|
||||
event_data[EncodableValue("device")] = EncodableValue(deviceId);
|
||||
|
||||
event_sink_->Success(EncodableValue(event_data));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to hotkey messages (for compatibility)
|
||||
if (message == WM_HOTKEY && event_sink_) {
|
||||
int key_index = -1;
|
||||
|
||||
@@ -355,12 +210,7 @@ std::optional<LRESULT> MediaKeyDetectorWindows::HandleWindowProc(
|
||||
}
|
||||
|
||||
if (key_index >= 0) {
|
||||
// Send event with key index only (no device info for hotkey)
|
||||
flutter::EncodableMap event_data;
|
||||
event_data[EncodableValue("key")] = EncodableValue(key_index);
|
||||
event_data[EncodableValue("device")] = EncodableValue("HID Device");
|
||||
|
||||
event_sink_->Success(EncodableValue(event_data));
|
||||
event_sink_->Success(EncodableValue(key_index));
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
2
prop
2
prop
Submodule prop updated: 7517d9727b...9f4cefba4a
19
prop_public/lib/emulators/dircon/dircon.dart
Normal file
19
prop_public/lib/emulators/dircon/dircon.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
class DirCon {
|
||||
final Socket socket;
|
||||
|
||||
DirCon({required this.socket});
|
||||
|
||||
List<String> get serviceUUIDs => [];
|
||||
|
||||
List<BleCharacteristic> getCharacteristics(String serviceUUID) => [];
|
||||
|
||||
void processWriteCallback(String characteristicUUID, List<int> characteristicData) {}
|
||||
|
||||
void handleIncomingData(List<int> data) {}
|
||||
|
||||
void sendCharacteristicNotification(String uuid, List<int> responseData) {}
|
||||
}
|
||||
@@ -6,4 +6,34 @@ class SharedLogic {
|
||||
static Uint8List? handleWriteRequest(String characteristic, Uint8List value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<void> keepAlive() async {}
|
||||
|
||||
static void stopKeepAlive() {}
|
||||
}
|
||||
|
||||
class Logger {
|
||||
static void info(String text) {
|
||||
if (kDebugMode) {
|
||||
print('${DateTime.now()} \x1B[32m$text\x1B[0m');
|
||||
}
|
||||
}
|
||||
|
||||
static void warn(String text) {
|
||||
if (kDebugMode) {
|
||||
print('\x1B[33m$text\x1B[0m');
|
||||
}
|
||||
}
|
||||
|
||||
static void error(String text) {
|
||||
if (kDebugMode) {
|
||||
print('\x1B[31m$text\x1B[0m');
|
||||
}
|
||||
}
|
||||
|
||||
static void debug(String s) {
|
||||
if (kDebugMode) {
|
||||
print('\x1B[34m$s\x1B[0m');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
304
pubspec.lock
304
pubspec.lock
@@ -16,6 +16,14 @@ packages:
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
adaptive_number:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: adaptive_number
|
||||
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -48,6 +56,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.2"
|
||||
app_links:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links
|
||||
sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
app_links_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_linux
|
||||
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
app_links_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_platform_interface
|
||||
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
app_links_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_web
|
||||
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -80,6 +120,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
audio_session:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audio_session
|
||||
sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
bluetooth_low_energy:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -156,10 +204,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.4.1"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -232,6 +280,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
dart_jsonwebtoken:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_jsonwebtoken
|
||||
sha256: "0de65691c1d736e9459f22f654ddd6fd8368a271d4e41aa07e53e6301eff5075"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -289,6 +345,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.3"
|
||||
ed25519_edwards:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ed25519_edwards
|
||||
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
email_validator:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -530,11 +594,27 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
font_awesome_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: font_awesome_flutter
|
||||
sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.12.0"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
functions_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: functions_client
|
||||
sha256: "94074d62167ae634127ef6095f536835063a7dc80f2b1aa306d2346ff9023996"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
gamepads:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -632,6 +712,62 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.0"
|
||||
google_identity_services_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_identity_services_web
|
||||
sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+1"
|
||||
google_sign_in:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_sign_in
|
||||
sha256: "521031b65853b4409b8213c0387d57edaad7e2a949ce6dea0d8b2afc9cb29763"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.0"
|
||||
google_sign_in_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_android
|
||||
sha256: "5ec98ab35387c68c0050495bb211bd88375873723a80fae7c2e9266ea0bdd8bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.7"
|
||||
google_sign_in_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_ios
|
||||
sha256: ac1e4c1205267cb7999d1d81333fccffdfda29e853f434bbaf71525498bb6950
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
google_sign_in_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_platform_interface
|
||||
sha256: "7f59208c42b415a3cca203571128d6f84f885fead2d5b53eb65a9e27f2965bb5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
google_sign_in_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_web
|
||||
sha256: "2fc1f941e6443b2d6984f4056a727a3eaeab15d8ee99ba7125d79029be75a1da"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
gotrue:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gotrue
|
||||
sha256: f7b52008311941a7c3e99f9590c4ee32dfc102a5442e43abf1b287d9f8cc39b2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.18.0"
|
||||
gsettings:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -868,6 +1004,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
just_audio:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: just_audio
|
||||
sha256: "9694e4734f515f2a052493d1d7e0d6de219ee0427c7c29492e246ff32a219908"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.5"
|
||||
just_audio_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: just_audio_platform_interface
|
||||
sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
just_audio_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: just_audio_web
|
||||
sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.16"
|
||||
jwt_decode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jwt_decode
|
||||
sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
keypress_simulator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -940,18 +1108,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
version: "0.12.18"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
version: "0.13.0"
|
||||
matrix4_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1274,6 +1442,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1282,6 +1458,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
postgrest:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: postgrest
|
||||
sha256: f4b6bb24b465c47649243ef0140475de8a0ec311dc9c75ebe573b2dcabb10460
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1353,6 +1537,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
realtime_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: realtime_client
|
||||
sha256: "5268afc208d02fb9109854d262c1ebf6ece224cd285199ae1d2f92d2ff49dbf1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
restart_app:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1362,6 +1554,14 @@ packages:
|
||||
url: "https://github.com/maple135790/restart_app.git"
|
||||
source: git
|
||||
version: "1.4.0"
|
||||
retry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: retry
|
||||
sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1499,6 +1699,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
sign_in_button:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sign_in_button
|
||||
sha256: "7bcd5e3ca5f80578da6a92b8749badf4003cf4dc578b5cb737b9082871354ff8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
sign_in_with_apple:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sign_in_with_apple
|
||||
sha256: "8bd875c8e8748272749eb6d25b896f768e7e9d60988446d543fe85a37a2392b8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
sign_in_with_apple_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sign_in_with_apple_platform_interface
|
||||
sha256: "981bca52cf3bb9c3ad7ef44aace2d543e5c468bb713fd8dda4275ff76dfa6659"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
sign_in_with_apple_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sign_in_with_apple_web
|
||||
sha256: f316400827f52cafcf50d00e1a2e8a0abc534ca1264e856a81c5f06bd5b10fed
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
skeletonizer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1536,6 +1768,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
storage_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: storage_client
|
||||
sha256: "1c61b19ed9e78f37fdd1ca8b729ab8484e6c8fe82e15c87e070b861951183657"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1552,6 +1792,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
supabase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: supabase
|
||||
sha256: cc039f63a3168386b3a4f338f3bff342c860d415a3578f3fbe854024aee6f911
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.2"
|
||||
supabase_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: supabase_flutter
|
||||
sha256: "92b2416ecb6a5c3ed34cf6e382b35ce6cc8921b64f2a9299d5d28968d42b09bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1560,6 +1816,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1572,10 +1836,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
version: "0.7.8"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1745,6 +2009,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1856,6 +2136,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.4"
|
||||
yet_another_json_isolate:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yet_another_json_isolate
|
||||
sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
sdks:
|
||||
dart: ">=3.10.3 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
||||
10
pubspec.yaml
10
pubspec.yaml
@@ -1,7 +1,7 @@
|
||||
name: bike_control
|
||||
description: "BikeControl - Control your virtual riding"
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
version: 4.6.2+93
|
||||
version: 4.7.4+99
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.0
|
||||
@@ -30,6 +30,12 @@ dependencies:
|
||||
flutter_md: ^0.0.7
|
||||
permission_handler: ^12.0.1
|
||||
dartx: any
|
||||
|
||||
supabase_flutter: ^2.12.0
|
||||
sign_in_button: ^4.0.1
|
||||
google_sign_in: ^7.2.0
|
||||
sign_in_with_apple: ^7.0.1
|
||||
|
||||
nsd: ^4.0.3
|
||||
image_picker: ^1.1.2
|
||||
in_app_review: ^2.0.11
|
||||
@@ -104,6 +110,7 @@ flutter:
|
||||
- INSTRUCTIONS_ZWIFT.md
|
||||
- INSTRUCTIONS_LOCAL.md
|
||||
- shorebird.yaml
|
||||
- assets/silence.mp3
|
||||
- icon.png
|
||||
|
||||
flutter_intl:
|
||||
@@ -120,4 +127,5 @@ msix_config:
|
||||
store: true
|
||||
logo_path: web/icons/Icon-512.png
|
||||
build_windows: false
|
||||
protocol_activation: https, bikecontrol
|
||||
capabilities: internetClient,bluetooth,inputInjectionBrokered
|
||||
|
||||
9
pubspec_overrides_ci.yaml
Normal file
9
pubspec_overrides_ci.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
dependency_overrides:
|
||||
prop:
|
||||
path: prop
|
||||
|
||||
gamepads_windows:
|
||||
git:
|
||||
url: https://github.com/lea108/gamepads.git
|
||||
ref: windows-api-rework
|
||||
path: packages/gamepads_windows
|
||||
@@ -27,6 +27,7 @@ void main() {
|
||||
manufacturerData: [
|
||||
ManufacturerData(ZwiftConstants.ZWIFT_MANUFACTURER_ID, Uint8List.fromList([ZwiftConstants.RC1_RIGHT_SIDE])),
|
||||
],
|
||||
services: [ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID.toLowerCase()],
|
||||
);
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<ZwiftPlay>());
|
||||
});
|
||||
@@ -52,6 +53,17 @@ void main() {
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<ZwiftRide>());
|
||||
});
|
||||
|
||||
test('Detect Zwift Ride oldest firmware', () {
|
||||
final device = _createBleDevice(
|
||||
name: 'Zwift Ride',
|
||||
manufacturerData: [
|
||||
ManufacturerData(ZwiftConstants.ZWIFT_MANUFACTURER_ID, Uint8List.fromList([ZwiftConstants.RIDE_LEFT_SIDE])),
|
||||
],
|
||||
services: [ZwiftConstants.ZWIFT_CUSTOM_SERVICE_SHORT_UUID.toLowerCase()],
|
||||
);
|
||||
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<ZwiftRide>());
|
||||
});
|
||||
|
||||
test('Detect Zwift Click V1', () {
|
||||
final device = _createBleDevice(
|
||||
name: 'Zwift Click',
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/keymap/apps/custom_app.dart';
|
||||
import 'package:bike_control/utils/settings/settings.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
group('Custom Profile Tests', () {
|
||||
setUp(() async {
|
||||
// Initialize SharedPreferences with in-memory storage for testing
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
FlutterSecureStorage.setMockInitialValues({});
|
||||
await core.settings.init();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:bike_control/bluetooth/devices/cycplus/cycplus_bc2.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
void main() {
|
||||
@@ -28,7 +28,7 @@ void main() {
|
||||
_hexToUint8List('FEEFFFEE0206030398565E000158'),
|
||||
);
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.first, CycplusBc2Buttons.shiftUp);
|
||||
expect(stubActions.performedActions.first, PerformedAction(CycplusBc2Buttons.shiftUp, isDown: true, isUp: true));
|
||||
stubActions.performedActions.clear();
|
||||
|
||||
// Packet 2: [6]=03 [7]=01 -> Trigger: shiftDown
|
||||
@@ -37,7 +37,10 @@ void main() {
|
||||
_hexToUint8List('FEEFFFEE0206030198575E000157'),
|
||||
);
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.first, CycplusBc2Buttons.shiftDown);
|
||||
expect(
|
||||
stubActions.performedActions.first,
|
||||
PerformedAction(CycplusBc2Buttons.shiftDown, isDown: true, isUp: true),
|
||||
);
|
||||
stubActions.performedActions.clear();
|
||||
|
||||
// Packet 3: [6]=03 [7]=03 -> No trigger (lock state)
|
||||
@@ -53,7 +56,7 @@ void main() {
|
||||
_hexToUint8List('FEEFFFEE0206010399585E000159'),
|
||||
);
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.first, CycplusBc2Buttons.shiftUp);
|
||||
expect(stubActions.performedActions.first, PerformedAction(CycplusBc2Buttons.shiftUp, isDown: true, isUp: true));
|
||||
stubActions.performedActions.clear();
|
||||
});
|
||||
|
||||
@@ -89,7 +92,7 @@ void main() {
|
||||
_hexToUint8List('FEEFFFEE0206010300005E000100'),
|
||||
);
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.first, CycplusBc2Buttons.shiftUp);
|
||||
expect(stubActions.performedActions.first, PerformedAction(CycplusBc2Buttons.shiftUp, isDown: true, isUp: true));
|
||||
});
|
||||
|
||||
test('Test both buttons can trigger simultaneously', () {
|
||||
@@ -110,7 +113,10 @@ void main() {
|
||||
_hexToUint8List('FEEFFFEE0206020200005E000100'),
|
||||
);
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.contains(CycplusBc2Buttons.shiftUp), true);
|
||||
expect(
|
||||
stubActions.performedActions.contains(PerformedAction(CycplusBc2Buttons.shiftUp, isDown: true, isUp: true)),
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:bike_control/bluetooth/devices/zwift/constants.dart';
|
||||
import 'package:bike_control/main.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/keymap/apps/my_whoosh.dart';
|
||||
import 'package:bike_control/utils/keymap/keymap.dart';
|
||||
@@ -11,6 +12,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
Future<void> main() async {
|
||||
FlutterSecureStorage.setMockInitialValues({});
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
screenshotMode = true;
|
||||
|
||||
await core.settings.init();
|
||||
await core.settings.reset();
|
||||
|
||||
@@ -19,6 +19,7 @@ Future<void> main() async {
|
||||
|
||||
group('Shimano DI2 Tests', () {
|
||||
test('Should parse Di2 values correctly', () async {
|
||||
stubActions.cleanup();
|
||||
final instance = ShimanoDi2(BleDevice(name: 'Di2', deviceId: ''));
|
||||
await instance.processCharacteristic(
|
||||
ShimanoDi2Constants.D_FLY_CHANNEL_UUID,
|
||||
@@ -41,6 +42,7 @@ Future<void> main() async {
|
||||
});
|
||||
|
||||
test('should transmit all events', () async {
|
||||
stubActions.cleanup();
|
||||
final instance = ShimanoDi2(BleDevice(name: 'Di2', deviceId: ''));
|
||||
await instance.processCharacteristic(
|
||||
ShimanoDi2Constants.D_FLY_CHANNEL_UUID,
|
||||
@@ -60,7 +62,12 @@ Future<void> main() async {
|
||||
Uint8List.fromList([0x21, 0x14, 0xF0, 0xF0]),
|
||||
);
|
||||
final button = ControllerButton('D-Fly Channel 1');
|
||||
expect(stubActions.performedActions, equals([(button, true, false), (button, false, true)]));
|
||||
expect(
|
||||
stubActions.performedActions,
|
||||
equals([
|
||||
PerformedAction(button, isDown: true, isUp: true),
|
||||
]),
|
||||
);
|
||||
|
||||
await instance.processCharacteristic(
|
||||
ShimanoDi2Constants.D_FLY_CHANNEL_UUID,
|
||||
@@ -68,7 +75,10 @@ Future<void> main() async {
|
||||
);
|
||||
expect(
|
||||
stubActions.performedActions,
|
||||
equals([(button, true, false), (button, false, true), (button, true, false), (button, false, true)]),
|
||||
equals([
|
||||
PerformedAction(button, isDown: true, isUp: true),
|
||||
PerformedAction(button, isDown: true, isUp: true),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:bike_control/bluetooth/devices/thinkrider/thinkrider_vs200.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
void main() {
|
||||
@@ -21,7 +21,10 @@ void main() {
|
||||
_hexToUint8List('F3050301FC'),
|
||||
);
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.first, ThinkRiderVs200Buttons.shiftUp);
|
||||
expect(
|
||||
stubActions.performedActions.first,
|
||||
PerformedAction(ThinkRiderVs200Buttons.shiftUp, isDown: true, isUp: true),
|
||||
);
|
||||
});
|
||||
|
||||
test('Test shift down button press with correct pattern', () {
|
||||
@@ -35,7 +38,10 @@ void main() {
|
||||
_hexToUint8List('F3050300FB'),
|
||||
);
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.first, ThinkRiderVs200Buttons.shiftDown);
|
||||
expect(
|
||||
stubActions.performedActions.first,
|
||||
PerformedAction(ThinkRiderVs200Buttons.shiftDown, isDown: true, isUp: true),
|
||||
);
|
||||
});
|
||||
|
||||
test('Test multiple button presses', () {
|
||||
@@ -49,7 +55,10 @@ void main() {
|
||||
_hexToUint8List('F3050301FC'),
|
||||
);
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.first, ThinkRiderVs200Buttons.shiftUp);
|
||||
expect(
|
||||
stubActions.performedActions.first,
|
||||
PerformedAction(ThinkRiderVs200Buttons.shiftUp, isDown: true, isUp: true),
|
||||
);
|
||||
stubActions.performedActions.clear();
|
||||
|
||||
// Shift down
|
||||
@@ -58,7 +67,10 @@ void main() {
|
||||
_hexToUint8List('F3050300FB'),
|
||||
);
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(stubActions.performedActions.first, ThinkRiderVs200Buttons.shiftDown);
|
||||
expect(
|
||||
stubActions.performedActions.first,
|
||||
PerformedAction(ThinkRiderVs200Buttons.shiftDown, isDown: true, isUp: true),
|
||||
);
|
||||
});
|
||||
|
||||
test('Test incorrect pattern does not trigger action', () {
|
||||
@@ -73,6 +85,46 @@ void main() {
|
||||
);
|
||||
expect(stubActions.performedActions.isEmpty, true);
|
||||
});
|
||||
|
||||
test('Test shift up performs single click action (not double)', () {
|
||||
core.actionHandler = StubActions();
|
||||
final stubActions = core.actionHandler as StubActions;
|
||||
final device = ThinkRiderVs200(BleDevice(deviceId: 'deviceId', name: 'THINK VS01-0000285'));
|
||||
|
||||
// Send shift up pattern: F3-05-03-01-FC
|
||||
device.processCharacteristic(
|
||||
ThinkRiderVs200Constants.CHARACTERISTIC_UUID,
|
||||
_hexToUint8List('F3050301FC'),
|
||||
);
|
||||
|
||||
// Should have exactly 1 action (single click with isKeyDown: true, isKeyUp: true)
|
||||
// NOT 2 actions (down then up)
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(
|
||||
stubActions.performedActions.first,
|
||||
equals(PerformedAction(ThinkRiderVs200Buttons.shiftUp, isDown: true, isUp: true)),
|
||||
);
|
||||
});
|
||||
|
||||
test('Test shift down performs single click action (not double)', () {
|
||||
core.actionHandler = StubActions();
|
||||
final stubActions = core.actionHandler as StubActions;
|
||||
final device = ThinkRiderVs200(BleDevice(deviceId: 'deviceId', name: 'THINK VS01-0000285'));
|
||||
|
||||
// Send shift down pattern: F3-05-03-00-FB
|
||||
device.processCharacteristic(
|
||||
ThinkRiderVs200Constants.CHARACTERISTIC_UUID,
|
||||
_hexToUint8List('F3050300FB'),
|
||||
);
|
||||
|
||||
// Should have exactly 1 action (single click with isKeyDown: true, isKeyUp: true)
|
||||
// NOT 2 actions (down then up)
|
||||
expect(stubActions.performedActions.length, 1);
|
||||
expect(
|
||||
stubActions.performedActions.first,
|
||||
equals(PerformedAction(ThinkRiderVs200Buttons.shiftDown, isDown: true, isUp: true)),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <bluetooth_low_energy_windows/bluetooth_low_energy_windows_plugin_c_api.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
@@ -22,6 +23,8 @@
|
||||
#include <windows_iap/windows_iap_plugin_c_api.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AppLinksPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||
BluetoothLowEnergyWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("BluetoothLowEnergyWindowsPluginCApi"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
bluetooth_low_energy_windows
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user