Compare commits

..

49 Commits

Author SHA1 Message Date
Jonas Bark
2e69cdb593 version++ 2026-01-22 17:47:00 +01:00
Jonas Bark
6d6427f8cd fix macOS build issue 2026-01-22 17:31:52 +01:00
Jonas Bark
2d73d8577e fix macOS build issue 2026-01-22 17:11:17 +01:00
Jonas Bark
19e72af527 version++ 2026-01-22 16:44:48 +01:00
Jonas Bark
1692c584f4 update flutter 2026-01-22 16:44:26 +01:00
Jonas Bark
ab8e1b6afe fix build 2026-01-22 15:40:23 +01:00
Jonas Bark
9a7c4bb13a update changelog, translations 2026-01-22 14:47:17 +01:00
Jonas Bark
b97298099c Windows & macOS: allow configuration of volume keys on Bluetooth HID devices 2026-01-22 14:29:57 +01:00
Jonas Bark
a7a91e00a2 inform user to enable local connection method on Android 2026-01-22 14:10:15 +01:00
Jonas Bark
ad7d8276a2 update changelog 2026-01-22 08:22:55 +01:00
Jonas Bark
6d652ad70b show side identifier for Zwift Play 2026-01-21 13:06:26 +01:00
Jonas Bark
94d5b4ad92 improve default actions 2026-01-21 12:52:27 +01:00
Jonas Bark
792d2733ff add missing openbikecontrol actions 2026-01-21 10:33:17 +01:00
Jonas Bark
07e4b5e89f cleanup 2026-01-19 16:57:54 +01:00
Jonas Bark
fe271038cb fix media key buttons 2026-01-19 16:57:39 +01:00
Jonas Bark
51cb4859e3 fix import, adjust changelog 2026-01-19 11:18:43 +01:00
jonasbark
b4667ca894 Merge pull request #270 from jonasbark/copilot/add-device-button-config
Generalize per-device mappings for Bluetooth controllers and persist device IDs in keymaps
2026-01-19 11:02:00 +01:00
Jonas Bark
f79cfb6319 Merge branch 'main' of github.com:jonasbark/swiftcontrol 2026-01-19 11:01:45 +01:00
Jonas Bark
97279b7c16 update dependencies 2026-01-19 11:01:42 +01:00
copilot-swe-agent[bot]
5b8a64c356 refactor: generalize per-device button handling
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2026-01-19 09:55:00 +00:00
copilot-swe-agent[bot]
29777d86e0 feat: device-specific thinkrider mapping
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2026-01-19 09:37:14 +00:00
copilot-swe-agent[bot]
58440148b3 chore: update progress checklist
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2026-01-19 09:26:30 +00:00
copilot-swe-agent[bot]
bfcf43d428 feat: per-device cycplus mapping
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2026-01-19 09:23:06 +00:00
copilot-swe-agent[bot]
b8448d57e5 Initial plan 2026-01-19 09:13:25 +00:00
Jonas Bark
000104365c add OpenBikeControl protocol to onboarding 2026-01-19 08:23:30 +01:00
jonasbark
8141310f7b Update Windows Store version to 4.4.1 2026-01-17 19:08:48 +01:00
jonasbark
f432d9d187 Merge pull request #265 from jonasbark/copilot/add-special-keys-to-buttoneditor
Add Android global actions to local control button editor
2026-01-17 14:51:54 +01:00
Jonas Bark
5ab8ccc8c8 Merge branch 'copilot/add-special-keys-to-buttoneditor' of github.com:jonasbark/swiftcontrol into copilot/add-special-keys-to-buttoneditor
# Conflicts:
#	CHANGELOG.md
2026-01-17 14:48:25 +01:00
Jonas Bark
fd9818707e PR feedback 2026-01-17 14:46:22 +01:00
jonasbark
7422ff8624 Update CHANGELOG.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-17 14:39:56 +01:00
Jonas Bark
58cf7b5bf3 update changelog 2026-01-17 14:19:47 +01:00
Jonas Bark
87d71e1213 Android: simulate additional actions for local connection method (Left, Down, Right, Up, Down, Select, Back, Home, Recent Apps), allowing you to navigate in the trainer app, if supported 2026-01-17 13:52:27 +01:00
Jonas Bark
a0cde5352f Merge branch 'main' into copilot/add-special-keys-to-buttoneditor 2026-01-17 13:14:15 +01:00
copilot-swe-agent[bot]
b6ed1c047d Add Android global actions support
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2026-01-17 12:06:48 +00:00
Jonas Bark
8d8cb7381a fix configuration not updating for new buttons 2026-01-17 12:59:39 +01:00
copilot-swe-agent[bot]
f3bbf5e06c Initial plan 2026-01-17 11:50:53 +00:00
Jonas Bark
a5f9b42e6f Windows: fix media key detection 2026-01-17 12:11:14 +01:00
Jonas Bark
d053101c14 Merge branch 'main' of github.com:jonasbark/swiftcontrol 2026-01-17 12:01:26 +01:00
jonasbark
3022b15d3a Merge pull request #264 from jonasbark/copilot/fix-bikecontrol-gear-change-issue
Allow Rouvy gear changes while app is in background
2026-01-17 12:00:33 +01:00
jonas.bark@gmx.de
6b541e1d14 allow all apps to execute keyboard key in background 2026-01-17 11:58:15 +01:00
copilot-swe-agent[bot]
586b148ed3 Allow Rouvy gear changes while app is in background
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2026-01-17 09:11:34 +00:00
copilot-swe-agent[bot]
cf47a758ee Refine background window handling
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2026-01-17 09:10:56 +00:00
copilot-swe-agent[bot]
89230815a2 Allow background keypress for Rouvy
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2026-01-17 09:10:17 +00:00
copilot-swe-agent[bot]
60a176bba2 Initial plan 2026-01-17 09:07:01 +00:00
Jonas Bark
44b5c010ba cleanup 2026-01-16 13:29:44 +01:00
Jonas Bark
425c65528b change notification icon 2026-01-16 12:59:39 +01:00
Jonas Bark
8ac6f58d8e fix detection for hardware buttons when BikeControl is in foreground, change notification icon 2026-01-16 12:38:10 +01:00
Jonas Bark
f6ac724c60 fix iap logic on Android 2026-01-16 11:59:50 +01:00
Jonas Bark
9010346c0b cleanup 2026-01-16 00:44:08 +01:00
69 changed files with 1259 additions and 615 deletions

View File

@@ -36,12 +36,12 @@ on:
env:
SHOREBIRD_TOKEN: ${{ secrets.SHOREBIRD_TOKEN }}
FLUTTER_VERSION: 3.38.4
FLUTTER_VERSION: 3.38.7
jobs:
build:
name: Build & Release
runs-on: macos-latest
runs-on: macos-26
permissions:
id-token: write
@@ -53,6 +53,10 @@ jobs:
- name: Checkout Repository
uses: actions/checkout@v3
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Install certificates
if: inputs.build_mac || inputs.build_ios
env:
@@ -133,22 +137,7 @@ jobs:
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
platform: android
args: "--artifact=apk -- --dart-define=REVENUECAT_API_KEY_ANDROID=${{ secrets.REVENUECAT_API_KEY_ANDROID }}"
- name: Build Web
if: inputs.build_web
run: flutter build web --release --base-href "/swiftcontrol/"
- name: Upload static files as artifact
if: inputs.build_web
id: deployment
uses: actions/upload-pages-artifact@v3
with:
path: build/web
- name: Web Deploy
if: inputs.build_web
uses: actions/deploy-pages@v4
args: "-- --dart-define=REVENUECAT_API_KEY_ANDROID=${{ secrets.REVENUECAT_API_KEY_ANDROID }}"
- name: Extract latest changelog
id: changelog
@@ -207,41 +196,6 @@ jobs:
run: |
xcrun altool --upload-app -f build/ios/ipa/swift_play.ipa -t ios --apiKey "$APPSTORE_API_KEY" --apiIssuer "$APPSTORE_API_ISSUER_ID";
- name: Handle Android archives
if: inputs.build_android && inputs.build_github && false
run: |
cp build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/BikeControl.android.apk
- name: Code Signing of macOS app
if: inputs.build_mac && inputs.build_github && false
run: /usr/bin/codesign --deep --force -s "$DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY" --entitlements ../../../../../macos/Runner/Release.entitlements --options runtime BikeControl.app -v
working-directory: build/macos/Build/Products/Release
env:
DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY: ${{ secrets.DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY }}
- name: Handle macOS archives
if: inputs.build_mac && inputs.build_github && false
run: |
cd build/macos/Build/Products/Release/
zip -r BikeControl.macos.zip BikeControl.app/
- name: Upload Android Artifacts
if: inputs.build_android && inputs.build_github && false
uses: actions/upload-artifact@v4
with:
overwrite: true
name: Releases
path: |
build/app/outputs/flutter-apk/BikeControl.android.apk
- name: Upload macOS Artifacts
if: inputs.build_mac && inputs.build_github && false
uses: actions/upload-artifact@v4
with:
overwrite: true
name: Releases
path: |
build/macos/Build/Products/Release/BikeControl.macos.zip
#10 Extract Version
- name: Extract version from pubspec.yaml
@@ -347,14 +301,4 @@ jobs:
overwrite: true
name: Releases
path: |
build/windows/x64/runner/Release/bike_control.windows.zip
build/windows/x64/runner/Release/bike_control.msix
- name: Update Release
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: "build/windows/x64/runner/Release/bike_control.msix"
prerelease: true
tag: v${{ env.VERSION }}
token: ${{ secrets.TOKEN }}

View File

@@ -10,7 +10,6 @@ env:
jobs:
build:
name: Patch iOS, Android & macOS
if: false
runs-on: macos-latest
permissions:

View File

@@ -1,3 +1,12 @@
### 4.5.0 (22-01-2026)
**Features**:
- Android: simulate additional actions for local connection method (Left, Down, Right, Up, Select, Back, Home, Recent Apps)
- control your phone with your controller
- control UI within the trainer app (if supported)
- BikeControl now supports individual mapping when you use more than one Cycplus BC2 and ThinkRider VS200 controller
- Windows & macOS: allow configuration of volume keys on Bluetooth HID devices
### 4.4.0 (16-01-2026)
**Features**:
@@ -6,6 +15,8 @@
**Fixes**:
- Android: Local connection method allows passing keyboard events to the trainer app
- macOS: Compatibility with macOS Tahoe
- Windows: send keyboard events to the correct window when using multiple monitors or when another app is focused
- Windows: fix media key detection
### 4.3.0 (07-01-2026)

View File

@@ -1 +1 @@
4.3.0
4.4.1

View File

@@ -90,6 +90,23 @@ enum class MediaAction(val raw: Int) {
}
}
enum class GlobalAction(val raw: Int) {
BACK(0),
DPAD_CENTER(1),
DOWN(2),
RIGHT(3),
UP(4),
LEFT(5),
HOME(6),
RECENTS(7);
companion object {
fun ofRaw(raw: Int): GlobalAction? {
return values().firstOrNull { it.raw == raw }
}
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class WindowEvent (
val packageName: String,
@@ -175,11 +192,16 @@ private open class AccessibilityApiPigeonCodec : StandardMessageCodec() {
}
}
130.toByte() -> {
return (readValue(buffer) as Long?)?.let {
GlobalAction.ofRaw(it.toInt())
}
}
131.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
WindowEvent.fromList(it)
}
}
131.toByte() -> {
132.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
AKeyEvent.fromList(it)
}
@@ -193,12 +215,16 @@ private open class AccessibilityApiPigeonCodec : StandardMessageCodec() {
stream.write(129)
writeValue(stream, value.raw)
}
is WindowEvent -> {
is GlobalAction -> {
stream.write(130)
writeValue(stream, value.raw)
}
is WindowEvent -> {
stream.write(131)
writeValue(stream, value.toList())
}
is AKeyEvent -> {
stream.write(131)
stream.write(132)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
@@ -213,6 +239,7 @@ interface Accessibility {
fun hasPermission(): Boolean
fun openPermissions()
fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean)
fun performGlobalAction(action: GlobalAction)
fun controlMedia(action: MediaAction)
fun isRunning(): Boolean
fun ignoreHidDevices()
@@ -279,6 +306,24 @@ interface Accessibility {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.accessibility.Accessibility.performGlobalAction$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val actionArg = args[0] as GlobalAction
val wrapped: List<Any?> = try {
api.performGlobalAction(actionArg)
listOf(null)
} catch (exception: Throwable) {
AccessibilityApiPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.accessibility.Accessibility.controlMedia$separatedMessageChannelSuffix", codec)
if (api != null) {

View File

@@ -2,6 +2,7 @@ package de.jonasbark.accessibility
import AKeyEvent
import Accessibility
import GlobalAction
import HidKeyPressedStreamHandler
import MediaAction
import PigeonEventSink
@@ -66,6 +67,10 @@ class AccessibilityPlugin: FlutterPlugin, Accessibility {
Observable.toService?.performTouch(x = x, y = y, isKeyUp = isKeyUp, isKeyDown = isKeyDown) ?: error("Service not running")
}
override fun performGlobalAction(action: GlobalAction) {
Observable.toService?.performGlobalAction(action) ?: error("Service not running")
}
override fun controlMedia(action: MediaAction) {
val audioService = context.getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager
when (action) {

View File

@@ -15,6 +15,7 @@ import android.view.KeyEvent
import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
import GlobalAction
class AccessibilityService : AccessibilityService(), Listener {
@@ -71,7 +72,8 @@ class AccessibilityService : AccessibilityService(), Listener {
override fun onKeyEvent(event: KeyEvent): Boolean {
val keyString = KeyEvent.keyCodeToString(event.keyCode)
if (!Observable.ignoreHidDevices && isBleRemote(event) && Observable.handledKeys.contains(keyString)) {
// 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))) {
// Handle keys that have a keymap defined
Log.d(
"AccessibilityService",
@@ -96,6 +98,20 @@ class AccessibilityService : AccessibilityService(), Listener {
}
}
override fun performGlobalAction(action: GlobalAction) {
val mappedAction = when (action) {
GlobalAction.BACK -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_BACK
GlobalAction.DPAD_CENTER -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_DPAD_CENTER
GlobalAction.DOWN -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_DPAD_DOWN
GlobalAction.RIGHT -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_DPAD_RIGHT
GlobalAction.UP -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_DPAD_UP
GlobalAction.LEFT -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_DPAD_LEFT
GlobalAction.HOME -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_HOME
GlobalAction.RECENTS -> android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS
}
performGlobalAction(mappedAction)
}
override fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean) {
val gestureBuilder = GestureDescription.Builder()
val path = Path()

View File

@@ -2,6 +2,7 @@ package de.jonasbark.accessibility
import android.graphics.Rect
import android.view.KeyEvent
import GlobalAction
import java.util.concurrent.ConcurrentHashMap
object Observable {
@@ -15,6 +16,7 @@ object Observable {
interface Listener {
fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean)
fun performGlobalAction(action: GlobalAction)
}
interface Receiver {

View File

@@ -8,6 +8,8 @@ abstract class Accessibility {
void performTouch(double x, double y, {bool isKeyDown = true, bool isKeyUp = false});
void performGlobalAction(GlobalAction action);
void controlMedia(MediaAction action);
bool isRunning();
@@ -19,6 +21,17 @@ abstract class Accessibility {
enum MediaAction { playPause, next, volumeUp, volumeDown }
enum GlobalAction {
back,
dpadCenter,
down,
right,
up,
left,
home,
recents,
}
class WindowEvent {
final String packageName;
final int top;

View File

@@ -36,6 +36,17 @@ enum MediaAction {
volumeDown,
}
enum GlobalAction {
back,
dpadCenter,
down,
right,
up,
left,
home,
recents,
}
class WindowEvent {
WindowEvent({
required this.packageName,
@@ -164,11 +175,14 @@ class _PigeonCodec extends StandardMessageCodec {
} else if (value is MediaAction) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else if (value is WindowEvent) {
} else if (value is GlobalAction) {
buffer.putUint8(130);
writeValue(buffer, value.index);
} else if (value is WindowEvent) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else if (value is AKeyEvent) {
buffer.putUint8(131);
buffer.putUint8(132);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
@@ -182,8 +196,11 @@ class _PigeonCodec extends StandardMessageCodec {
final int? value = readValue(buffer) as int?;
return value == null ? null : MediaAction.values[value];
case 130:
return WindowEvent.decode(readValue(buffer)!);
final int? value = readValue(buffer) as int?;
return value == null ? null : GlobalAction.values[value];
case 131:
return WindowEvent.decode(readValue(buffer)!);
case 132:
return AKeyEvent.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
@@ -280,6 +297,29 @@ class Accessibility {
}
}
Future<void> performGlobalAction(GlobalAction action) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.performGlobalAction$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[action]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
Future<void> controlMedia(MediaAction action) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.controlMedia$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(

View File

@@ -16,7 +16,7 @@ keystoreProperties.load(FileInputStream(keystorePropertiesFile))
android {
namespace = "de.jonasbark.swiftcontrol"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
ndkVersion = "28.2.13676358"
compileOptions {
// Flag to enable support for the new language APIs

View File

@@ -3,6 +3,7 @@
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/*" />

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip

View File

@@ -18,7 +18,7 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}

View File

@@ -10,6 +10,8 @@ PODS:
- flutter_secure_storage_darwin (10.0.0):
- Flutter
- FlutterMacOS
- flutter_volume_controller (0.0.1):
- Flutter
- gamepads_ios (0.1.1):
- Flutter
- image_picker_ios (0.0.1):
@@ -29,27 +31,24 @@ PODS:
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- purchases_flutter (9.10.2):
- purchases_flutter (9.10.6):
- Flutter
- PurchasesHybridCommon (= 17.25.0)
- purchases_ui_flutter (9.10.2):
- PurchasesHybridCommon (= 17.27.1)
- purchases_ui_flutter (9.10.6):
- Flutter
- PurchasesHybridCommonUI (= 17.25.0)
- PurchasesHybridCommon (17.25.0):
- RevenueCat (= 5.51.1)
- PurchasesHybridCommonUI (17.25.0):
- PurchasesHybridCommon (= 17.25.0)
- RevenueCatUI (= 5.51.1)
- PurchasesHybridCommonUI (= 17.27.1)
- PurchasesHybridCommon (17.27.1):
- RevenueCat (= 5.54.1)
- PurchasesHybridCommonUI (17.27.1):
- PurchasesHybridCommon (= 17.27.1)
- RevenueCatUI (= 5.54.1)
- restart_app (0.0.1):
- Flutter
- RevenueCat (5.51.1)
- RevenueCatUI (5.51.1):
- RevenueCat (= 5.51.1)
- RevenueCat (5.54.1)
- RevenueCatUI (5.54.1):
- RevenueCat (= 5.54.1)
- sensors_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -69,6 +68,7 @@ DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- 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`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
@@ -78,7 +78,6 @@ DEPENDENCIES:
- 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`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- purchases_flutter (from `.symlinks/plugins/purchases_flutter/ios`)
- purchases_ui_flutter (from `.symlinks/plugins/purchases_ui_flutter/ios`)
@@ -107,6 +106,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage_darwin:
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios"
gamepads_ios:
:path: ".symlinks/plugins/gamepads_ios/ios"
image_picker_ios:
@@ -125,8 +126,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/nsd_ios/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
purchases_flutter:
@@ -147,13 +146,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
bluetooth_low_energy_darwin: 764d8d1ae5abefbcdb839e812b4b25c0061fcf8b
bluetooth_low_energy_darwin: 50bc79258e60586e4c4bed5948bd31d925f37fac
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
flutter_secure_storage_darwin: 557817588b80e60213cbecb573c45c76b788018d
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
gamepads_ios: 1d2930c7a4450a9a1b57444ebf305a6a6cbeea0b
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
in_app_purchase_storekit: 2342c0a5da86593124d08dd13d920f39a52b273a
in_app_review: 436034b18594851a7328d7f1c2ed5ec235b79cfc
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
@@ -161,19 +161,18 @@ SPEC CHECKSUMS:
media_key_detector_ios: 7ff9aefdfea00bb7b71e184132381b7d0e7e1269
nsd_ios: 8c37babdc6538e3350dbed3a52674d2edde98173
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
purchases_flutter: 7cbb87481a018c1bb8b4966b7d446a7c19d96d87
purchases_ui_flutter: 1905cebf3f46e03aeefba0f6258000f3cdd32641
PurchasesHybridCommon: 6a79a873ab52f777bfa36e9516f3fcd84d3b3428
PurchasesHybridCommonUI: 3c1f78addfb3f470713548b4eac8c59254d0efe6
purchases_flutter: b3c0792197f69cd7af4c2449b71df6ac6378aace
purchases_ui_flutter: caae6d62ea23c6fe964992a28353211cc74b244a
PurchasesHybridCommon: 027f03312519c51056457eb2e4f7ee1c91b61b8f
PurchasesHybridCommonUI: 48afb5e29204958bff1276b0f7acb8e4b59fe99a
restart_app: 806659942bf932f6ce51c5372f91ce5e81c8c14a
RevenueCat: eab035bbab271faccfef5c36eaff2a1ffef14dc0
RevenueCatUI: c6acb3648fa58ccb4f0f178556ed0278b796234f
RevenueCat: ecbba580fa453b0d4a0475449b904196d74ef678
RevenueCatUI: ac7492873928e9e7f297e5e27a7c4f23f9008326
sensors_plus: 7229095999f30740798f0eeef5cd120357a8f4f2
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
PODFILE CHECKSUM: 7ebd5c9b932b3af79d5c67e3af873118b74e970f

View File

@@ -1,18 +1,20 @@
#include "keypress_simulator_windows_plugin.h"
// This must be included before many other Windows headers.
#include <windows.h>
#include <flutter_windows.h>
#include <psapi.h>
#include <string.h>
#include <flutter_windows.h>
#include <windows.h>
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <algorithm>
#include <memory>
#include <sstream>
#include <unordered_map>
#include <vector>
using flutter::EncodableList;
using flutter::EncodableMap;
@@ -28,7 +30,8 @@ struct FindWindowData {
};
BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam);
HWND FindTargetWindow(const std::string& processName, const std::string& windowTitle);
HWND FindTargetWindow(const std::string& processName,
const std::string& windowTitle);
// static
void KeypressSimulatorWindowsPlugin::RegisterWithRegistrar(
@@ -69,26 +72,91 @@ void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
}
// List of compatible training apps to look for
std::vector<std::string> compatibleApps = {
"MyWhooshHD.exe",
"indieVelo.exe",
"biketerra.exe"
};
std::vector<std::string> compatibleApps = {"MyWhooshHD.exe", "MyWhoosh.exe",
"indieVelo.exe", "biketerra.exe",
"Rouvy.exe"};
// Try to find and focus a compatible app
// Try to find and focus (or directly target) a compatible app
std::string foundProcessName;
bool supportsBackgroundInput = true;
HWND targetWindow = NULL;
for (const std::string& processName : compatibleApps) {
targetWindow = FindTargetWindow(processName, "");
if (targetWindow != NULL) {
// Only focus the window if it's not already in the foreground
if (GetForegroundWindow() != targetWindow) {
foundProcessName = processName;
if (!supportsBackgroundInput && GetForegroundWindow() != targetWindow) {
SetForegroundWindow(targetWindow);
Sleep(50); // Brief delay to ensure window is focused
Sleep(50); // Brief delay to ensure window is focused
}
break;
}
}
// If we found a target window that supports background input and it's not
// focused, send messages directly
auto postKeyMessage = [](HWND hwnd, UINT vkCode, bool down) {
const WORD scanCode =
static_cast<WORD>(MapVirtualKey(vkCode, MAPVK_VK_TO_VSC));
// Build lParam with repeat count 1 and scan code; set transition states for
// key up
LPARAM lParam = 1 | (static_cast<LPARAM>(scanCode) << 16);
if (vkCode == VK_LEFT || vkCode == VK_RIGHT || vkCode == VK_UP ||
vkCode == VK_DOWN || vkCode == VK_INSERT || vkCode == VK_DELETE ||
vkCode == VK_HOME || vkCode == VK_END || vkCode == VK_PRIOR ||
vkCode == VK_NEXT) {
lParam |= (1 << 24); // extended key
}
if (!down) {
lParam |= (1 << 30); // previous key state
lParam |= (1 << 31); // transition state
}
PostMessage(hwnd, down ? WM_KEYDOWN : WM_KEYUP, vkCode, lParam);
};
auto sendKeyToWindow = [&postKeyMessage](HWND hwnd,
const std::vector<std::string>& mods,
UINT keyCode, bool down) {
auto handleModifier = [&postKeyMessage, hwnd](UINT vk, bool press) {
postKeyMessage(hwnd, vk, press);
};
if (down) {
for (const std::string& modifier : mods) {
if (modifier == "shiftModifier") {
handleModifier(VK_SHIFT, true);
} else if (modifier == "controlModifier") {
handleModifier(VK_CONTROL, true);
} else if (modifier == "altModifier") {
handleModifier(VK_MENU, true);
} else if (modifier == "metaModifier") {
handleModifier(VK_LWIN, true);
}
}
postKeyMessage(hwnd, keyCode, true);
} else {
postKeyMessage(hwnd, keyCode, false);
// release modifiers
for (const std::string& modifier : mods) {
if (modifier == "shiftModifier") {
handleModifier(VK_SHIFT, false);
} else if (modifier == "controlModifier") {
handleModifier(VK_CONTROL, false);
} else if (modifier == "altModifier") {
handleModifier(VK_MENU, false);
} else if (modifier == "metaModifier") {
handleModifier(VK_LWIN, false);
}
}
}
};
if (targetWindow != NULL && !foundProcessName.empty() &&
supportsBackgroundInput && GetForegroundWindow() != targetWindow) {
sendKeyToWindow(targetWindow, modifiers, keyCode, keyDown);
result->Success(flutter::EncodableValue(true));
return;
}
// Helper function to send modifier key events
auto sendModifierKey = [](UINT vkCode, bool down) {
WORD sc = (WORD)MapVirtualKey(vkCode, MAPVK_VK_TO_VSC);
@@ -101,7 +169,8 @@ void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
};
// Helper function to process modifiers
auto processModifiers = [&sendModifierKey](const std::vector<std::string>& mods, bool down) {
auto processModifiers = [&sendModifierKey](
const std::vector<std::string>& mods, bool down) {
for (const std::string& modifier : mods) {
if (modifier == "shiftModifier") {
sendModifierKey(VK_SHIFT, down);
@@ -125,12 +194,13 @@ void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
INPUT in = {0};
in.type = INPUT_KEYBOARD;
in.ki.wVk = 0; // when using SCANCODE, set VK=0
in.ki.wVk = 0; // when using SCANCODE, set VK=0
in.ki.wScan = sc;
in.ki.dwFlags = KEYEVENTF_SCANCODE | (keyDown ? 0 : KEYEVENTF_KEYUP);
if (keyCode == VK_LEFT || keyCode == VK_RIGHT || keyCode == VK_UP || keyCode == VK_DOWN ||
keyCode == VK_INSERT || keyCode == VK_DELETE || keyCode == VK_HOME || keyCode == VK_END ||
keyCode == VK_PRIOR || keyCode == VK_NEXT) {
if (keyCode == VK_LEFT || keyCode == VK_RIGHT || keyCode == VK_UP ||
keyCode == VK_DOWN || keyCode == VK_INSERT || keyCode == VK_DELETE ||
keyCode == VK_HOME || keyCode == VK_END || keyCode == VK_PRIOR ||
keyCode == VK_NEXT) {
in.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
}
SendInput(1, &in, sizeof(INPUT));
@@ -149,7 +219,6 @@ void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
void KeypressSimulatorWindowsPlugin::SimulateMouseClick(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
const EncodableMap& args = std::get<EncodableMap>(*method_call.arguments());
double x = 0;
double y = 0;
@@ -157,12 +226,12 @@ void KeypressSimulatorWindowsPlugin::SimulateMouseClick(
bool keyDown = std::get<bool>(args.at(EncodableValue("keyDown")));
auto it_x = args.find(EncodableValue("x"));
if (it_x != args.end() && std::holds_alternative<double>(it_x->second)) {
x = std::get<double>(it_x->second);
x = std::get<double>(it_x->second);
}
auto it_y = args.find(EncodableValue("y"));
if (it_y != args.end() && std::holds_alternative<double>(it_y->second)) {
y = std::get<double>(it_y->second);
y = std::get<double>(it_y->second);
}
// Get the monitor containing the target point and its DPI
@@ -170,7 +239,7 @@ void KeypressSimulatorWindowsPlugin::SimulateMouseClick(
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
// Scale the coordinates according to the DPI scaling
int scaled_x = static_cast<int>(x * scale_factor);
int scaled_y = static_cast<int>(y * scale_factor);
@@ -183,14 +252,14 @@ void KeypressSimulatorWindowsPlugin::SimulateMouseClick(
input.type = INPUT_MOUSE;
if (keyDown) {
// Mouse left button down
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, &input, sizeof(INPUT));
// Mouse left button down
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, &input, sizeof(INPUT));
} else {
// Mouse left button up
input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(1, &input, sizeof(INPUT));
// Mouse left button up
input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(1, &input, sizeof(INPUT));
}
result->Success(flutter::EncodableValue(true));
@@ -201,7 +270,7 @@ BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) {
// Check if window is visible and not minimized
if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) {
return TRUE; // Continue enumeration
return TRUE; // Continue enumeration
}
// Get window title
@@ -211,7 +280,8 @@ BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) {
// Get process name
DWORD processId;
GetWindowThreadProcessId(hwnd, &processId);
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
HANDLE hProcess =
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
char processName[MAX_PATH];
if (hProcess) {
DWORD size = sizeof(processName);
@@ -219,7 +289,7 @@ BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) {
// Extract just the filename from the full path
char* filename = strrchr(processName, '\\');
if (filename) {
filename++; // Skip the backslash
filename++; // Skip the backslash
} else {
filename = processName;
}
@@ -228,7 +298,7 @@ BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) {
if (!data->targetProcessName.empty() &&
_stricmp(filename, data->targetProcessName.c_str()) == 0) {
data->foundWindow = hwnd;
return FALSE; // Stop enumeration
return FALSE; // Stop enumeration
}
}
CloseHandle(hProcess);
@@ -238,13 +308,14 @@ BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) {
if (!data->targetWindowTitle.empty() &&
_stricmp(windowTitle, data->targetWindowTitle.c_str()) == 0) {
data->foundWindow = hwnd;
return FALSE; // Stop enumeration
return FALSE; // Stop enumeration
}
return TRUE; // Continue enumeration
return TRUE; // Continue enumeration
}
HWND FindTargetWindow(const std::string& processName, const std::string& windowTitle) {
HWND FindTargetWindow(const std::string& processName,
const std::string& windowTitle) {
FindWindowData data;
data.targetProcessName = processName;
data.targetWindowTitle = windowTitle;
@@ -257,19 +328,15 @@ HWND FindTargetWindow(const std::string& processName, const std::string& windowT
void KeypressSimulatorWindowsPlugin::SimulateMediaKey(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
const EncodableMap& args = std::get<EncodableMap>(*method_call.arguments());
std::string keyIdentifier = std::get<std::string>(args.at(EncodableValue("key")));
std::string keyIdentifier =
std::get<std::string>(args.at(EncodableValue("key")));
// Map string identifier to Windows virtual key codes
static const std::unordered_map<std::string, UINT> keyMap = {
{"playPause", VK_MEDIA_PLAY_PAUSE},
{"stop", VK_MEDIA_STOP},
{"next", VK_MEDIA_NEXT_TRACK},
{"previous", VK_MEDIA_PREV_TRACK},
{"volumeUp", VK_VOLUME_UP},
{"volumeDown", VK_VOLUME_DOWN}
};
{"playPause", VK_MEDIA_PLAY_PAUSE}, {"stop", VK_MEDIA_STOP},
{"next", VK_MEDIA_NEXT_TRACK}, {"previous", VK_MEDIA_PREV_TRACK},
{"volumeUp", VK_VOLUME_UP}, {"volumeDown", VK_VOLUME_DOWN}};
auto it = keyMap.find(keyIdentifier);
if (it == keyMap.end()) {
@@ -282,7 +349,7 @@ void KeypressSimulatorWindowsPlugin::SimulateMediaKey(
INPUT inputs[2] = {};
inputs[0].type = INPUT_KEYBOARD;
inputs[0].ki.wVk = static_cast<WORD>(vkCode);
inputs[0].ki.dwFlags = 0; // Key down
inputs[0].ki.dwFlags = 0; // Key down
// Send key up event
inputs[1].type = INPUT_KEYBOARD;
@@ -298,7 +365,6 @@ void KeypressSimulatorWindowsPlugin::SimulateMediaKey(
result->Success(flutter::EncodableValue(true));
}
void KeypressSimulatorWindowsPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {

View File

@@ -65,6 +65,10 @@ class Connection {
lastLogEntries = lastLogEntries.takeLast(kIsWeb ? 1000 : 60).toList();
});
if (!kIsWeb && (Platform.isMacOS || Platform.isWindows)) {
core.mediaKeyHandler.initialize();
}
UniversalBle.onAvailabilityChange = (available) {
_actionStreams.add(BluetoothAvailabilityNotification(available == AvailabilityState.poweredOn));
if (available == AvailabilityState.poweredOn && !kIsWeb) {

View File

@@ -17,11 +17,11 @@ import '../../utils/keymap/buttons.dart';
import '../messages/notification.dart';
abstract class BaseDevice {
final String name;
final String? _name;
final bool isBeta;
final List<ControllerButton> availableButtons;
BaseDevice(this.name, {required this.availableButtons, this.isBeta = false}) {
BaseDevice(this._name, {required this.availableButtons, this.isBeta = false}) {
if (availableButtons.isEmpty && core.actionHandler.supportedApp is CustomApp) {
// TODO we should verify where the buttons came from
final allButtons = core.actionHandler.supportedApp!.keymap.keyPairs.flatMap((e) => e.buttons);
@@ -34,6 +34,8 @@ abstract class BaseDevice {
Timer? _longPressTimer;
Set<ControllerButton> _previouslyPressedButtons = <ControllerButton>{};
String get name => _name ?? runtimeType.toString();
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -43,9 +45,7 @@ abstract class BaseDevice {
int get hashCode => name.hashCode;
@override
String toString() {
return name;
}
String toString() => name;
final StreamController<BaseNotification> actionStreamInternal = StreamController<BaseNotification>.broadcast();

View File

@@ -18,6 +18,7 @@ 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';
import 'package:bike_control/utils/keymap/buttons.dart';
import 'package:bike_control/widgets/ui/beta_pill.dart';
import 'package:bike_control/widgets/ui/device_info.dart';
import 'package:bike_control/widgets/ui/loading_widget.dart';
@@ -36,8 +37,18 @@ import 'thinkrider/thinkrider_vs200.dart';
abstract class BluetoothDevice extends BaseDevice {
final BleDevice scanResult;
BluetoothDevice(this.scanResult, {required super.availableButtons, super.isBeta = false})
: super(scanResult.name ?? 'Unknown Device') {
BluetoothDevice(
this.scanResult, {
required List<ControllerButton> availableButtons,
bool allowMultiple = false,
bool isBeta = false,
}) : super(
scanResult.name,
availableButtons: allowMultiple
? availableButtons.map((b) => b.copyWith(sourceDeviceId: scanResult.deviceId)).toList()
: availableButtons,
isBeta: isBeta,
) {
rssi = scanResult.rssi;
}
@@ -73,7 +84,7 @@ abstract class BluetoothDevice extends BaseDevice {
if (kIsWeb) {
device = switch (scanResult.name) {
'Zwift Ride' => ZwiftRide(scanResult),
'Zwift Play' => ZwiftPlay(scanResult),
'Zwift Play' => ZwiftPlay(scanResult, deviceType: ZwiftDeviceType.playLeft),
'Zwift Click' => ZwiftClickV2(scanResult),
'SQUARE' => EliteSquare(scanResult),
'OpenBike' => OpenBikeControlDevice(scanResult),
@@ -94,7 +105,7 @@ abstract class BluetoothDevice extends BaseDevice {
null => null,
//'Zwift Ride' => ZwiftRide(scanResult), special case for Zwift Ride: we must only connect to the left controller
// https://www.makinolo.com/blog/2024/07/26/zwift-ride-protocol/
'Zwift Play' => ZwiftPlay(scanResult),
//'Zwift Play' => ZwiftPlay(scanResult),
//'Zwift Click' => ZwiftClick(scanResult), special case for Zwift Click v2: we must only connect to the left controller
_ when scanResult.name!.toUpperCase().startsWith('HEADWIND') => WahooKickrHeadwind(scanResult),
_ when scanResult.name!.toUpperCase().startsWith('SQUARE') => EliteSquare(scanResult),
@@ -104,7 +115,7 @@ abstract class BluetoothDevice extends BaseDevice {
_ when scanResult.name!.toUpperCase().startsWith('CYCPLUS') && scanResult.name!.toUpperCase().contains('BC2') =>
CycplusBc2(scanResult),
_ when scanResult.name!.toUpperCase().startsWith('THINK VS') => ThinkRiderVs200(scanResult),
_ when scanResult.services.contains(CycplusBc2Constants.SERVICE_UUID.toLowerCase()) => CycplusBc2(scanResult),
//_ when scanResult.services.contains(CycplusBc2Constants.SERVICE_UUID.toLowerCase()) => CycplusBc2(scanResult),
_ when scanResult.services.contains(ShimanoDi2Constants.SERVICE_UUID.toLowerCase()) => ShimanoDi2(scanResult),
_ when scanResult.services.contains(ShimanoDi2Constants.SERVICE_UUID_ALTERNATIVE.toLowerCase()) => ShimanoDi2(
scanResult,
@@ -141,8 +152,8 @@ abstract class BluetoothDevice extends BaseDevice {
final type = ZwiftDeviceType.fromManufacturerData(data.first);
device = switch (type) {
ZwiftDeviceType.click => ZwiftClick(scanResult),
ZwiftDeviceType.playRight => ZwiftPlay(scanResult),
ZwiftDeviceType.playLeft => ZwiftPlay(scanResult),
ZwiftDeviceType.playRight => ZwiftPlay(scanResult, deviceType: type!),
ZwiftDeviceType.playLeft => ZwiftPlay(scanResult, deviceType: type!),
ZwiftDeviceType.rideLeft => ZwiftRide(scanResult),
//DeviceType.rideRight => ZwiftRide(scanResult), // see comment above
ZwiftDeviceType.clickV2Left => ZwiftClickV2(scanResult),

View File

@@ -11,6 +11,7 @@ class CycplusBc2 extends BluetoothDevice {
CycplusBc2(super.scanResult)
: super(
availableButtons: CycplusBc2Buttons.values,
allowMultiple: true,
);
@override
@@ -40,7 +41,7 @@ class CycplusBc2 extends BluetoothDevice {
// Process index 6 (shift up)
final currentByte6 = bytes[6];
if (_shouldTriggerShift(currentByte6, _lastStateIndex6)) {
buttonsToPress.add(CycplusBc2Buttons.shiftUp);
buttonsToPress.add(availableButtons[0]);
_lastStateIndex6 = 0x00; // Reset after successful press
} else {
_updateState(currentByte6, (val) => _lastStateIndex6 = val);
@@ -49,7 +50,7 @@ class CycplusBc2 extends BluetoothDevice {
// Process index 7 (shift down)
final currentByte7 = bytes[7];
if (_shouldTriggerShift(currentByte7, _lastStateIndex7)) {
buttonsToPress.add(CycplusBc2Buttons.shiftDown);
buttonsToPress.add(availableButtons[1]);
_lastStateIndex7 = 0x00; // Reset after successful press
} else {
_updateState(currentByte7, (val) => _lastStateIndex7 = val);

View File

@@ -3,11 +3,13 @@ import 'dart:io';
import 'package:bike_control/bluetooth/devices/base_device.dart';
import 'package:bike_control/bluetooth/messages/notification.dart';
import 'package:bike_control/pages/device.dart';
import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/keymap/buttons.dart';
import 'package:bike_control/widgets/ui/beta_pill.dart';
import 'package:bike_control/widgets/ui/warning.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import 'package:gamepads/gamepads.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
class GamepadDevice extends BaseDevice {
final String id;
@@ -74,6 +76,14 @@ class GamepadDevice extends BaseDevice {
if (isBeta) BetaPill(),
],
),
if (Platform.isAndroid && !core.settings.getLocalEnabled())
Warning(
children: [
Text(
'For it to work properly, even when BikeControl is in the background, you need to enable the local connection method in the next tab.',
).small,
],
),
],
),
);

View File

@@ -1,7 +1,11 @@
import 'dart:io';
import 'package:bike_control/bluetooth/devices/base_device.dart';
import 'package:bike_control/utils/actions/android.dart';
import 'package:bike_control/utils/core.dart';
import 'package:flutter/material.dart';
import 'package:bike_control/widgets/ui/warning.dart';
import 'package:flutter/material.dart' show PopupMenuButton, PopupMenuItem;
import 'package:shadcn_flutter/shadcn_flutter.dart';
class HidDevice extends BaseDevice {
HidDevice(super.name) : super(availableButtons: []);
@@ -13,24 +17,36 @@ class HidDevice extends BaseDevice {
@override
Widget showInformation(BuildContext context) {
return Row(
return Column(
children: [
Expanded(child: Text(toString())),
PopupMenuButton(
itemBuilder: (c) => [
PopupMenuItem(
child: Text('Ignore'),
onTap: () {
core.connection.disconnect(this, forget: true, persistForget: true);
if (core.actionHandler is AndroidActions) {
(core.actionHandler as AndroidActions).ignoreHidDevices();
} else if (core.mediaKeyHandler.isMediaKeyDetectionEnabled.value) {
core.mediaKeyHandler.isMediaKeyDetectionEnabled.value = false;
}
},
Row(
children: [
Expanded(child: Text(toString())),
PopupMenuButton(
itemBuilder: (c) => [
PopupMenuItem(
child: Text('Ignore'),
onTap: () {
core.connection.disconnect(this, forget: true, persistForget: true);
if (core.actionHandler is AndroidActions) {
(core.actionHandler as AndroidActions).ignoreHidDevices();
} else if (core.mediaKeyHandler.isMediaKeyDetectionEnabled.value) {
core.mediaKeyHandler.isMediaKeyDetectionEnabled.value = false;
}
},
),
],
),
],
),
if (Platform.isAndroid && !core.settings.getLocalEnabled())
Warning(
children: [
Text(
'For it to work properly, even when BikeControl is in the background, you need to enable the local connection method in the next tab.',
).small,
],
),
],
);
}

View File

@@ -35,8 +35,8 @@ class OpenBikeProtocolParser {
0x13: ControllerButton('Right/Look Right', identifier: 0x13, action: InGameAction.navigateRight),
0x14: ControllerButton('Select/Confirm', identifier: 0x14, action: InGameAction.select),
0x15: ControllerButton('Back/Cancel', identifier: 0x15, action: InGameAction.back),
0x16: ControllerButton('Menu', identifier: 0x16),
0x17: ControllerButton('Home', identifier: 0x17),
0x16: ControllerButton('Menu', identifier: 0x16, action: InGameAction.menu),
0x17: ControllerButton('Home', identifier: 0x17, action: InGameAction.home),
0x18: ControllerButton('Steer Left', identifier: 0x18, action: InGameAction.steerLeft),
0x19: ControllerButton('Steer Right', identifier: 0x19, action: InGameAction.steerRight),
// Social/Emotes (0x20-0x2F)

View File

@@ -12,6 +12,7 @@ class ThinkRiderVs200 extends BluetoothDevice {
: super(
availableButtons: ThinkRiderVs200Buttons.values,
isBeta: true,
allowMultiple: true,
);
@override
@@ -43,11 +44,11 @@ class ThinkRiderVs200 extends BluetoothDevice {
if (hexValue == ThinkRiderVs200Constants.SHIFT_UP_PATTERN) {
// Plus button pressed
actionStreamInternal.add(LogNotification('Shift Up detected: $hexValue'));
handleButtonsClickedWithoutLongPressSupport([ThinkRiderVs200Buttons.shiftUp]);
handleButtonsClickedWithoutLongPressSupport([availableButtons[0]]);
} else if (hexValue == ThinkRiderVs200Constants.SHIFT_DOWN_PATTERN) {
// Minus button pressed
actionStreamInternal.add(LogNotification('Shift Down detected: $hexValue'));
handleButtonsClickedWithoutLongPressSupport([ThinkRiderVs200Buttons.shiftDown]);
handleButtonsClickedWithoutLongPressSupport([availableButtons[1]]);
}
}

View File

@@ -59,13 +59,13 @@ class ZwiftButtons {
// left controller
static const ControllerButton navigationUp = ControllerButton(
'navigationUp',
action: InGameAction.toggleUi,
action: InGameAction.up,
icon: Icons.keyboard_arrow_up,
color: Colors.black,
);
static const ControllerButton navigationDown = ControllerButton(
'navigationDown',
action: InGameAction.uturn,
action: InGameAction.down,
icon: Icons.keyboard_arrow_down,
color: Colors.black,
);
@@ -106,7 +106,7 @@ class ZwiftButtons {
action: InGameAction.rideOnBomb,
color: Colors.deepOrangeAccent,
);
static const ControllerButton y = ControllerButton('y', action: null, color: Colors.lightBlue);
static const ControllerButton y = ControllerButton('y', action: InGameAction.menu, color: Colors.lightBlue);
static const ControllerButton onOffRight = ControllerButton('onOffRight', action: InGameAction.toggleUi);
static const ControllerButton sideButtonRight = ControllerButton('sideButtonRight', action: InGameAction.shiftUp);
static const ControllerButton paddleRight = ControllerButton('paddleRight', action: InGameAction.shiftUp);

View File

@@ -2,10 +2,13 @@ import 'package:bike_control/bluetooth/devices/zwift/constants.dart';
import 'package:bike_control/bluetooth/devices/zwift/protocol/zwift.pb.dart';
import 'package:bike_control/bluetooth/devices/zwift/zwift_device.dart';
import 'package:bike_control/utils/keymap/buttons.dart';
import 'package:bike_control/widgets/keymap_explanation.dart';
import 'package:flutter/foundation.dart';
class ZwiftPlay extends ZwiftDevice {
ZwiftPlay(super.scanResult)
final ZwiftDeviceType deviceType;
ZwiftPlay(super.scanResult, {required this.deviceType})
: super(
availableButtons: [
ZwiftButtons.y,
@@ -31,6 +34,9 @@ class ZwiftPlay extends ZwiftDevice {
@override
bool get canVibrate => true;
@override
String get name => '${super.name} (${deviceType.name.splitByUpperCase().split(' ').last})';
@override
List<ControllerButton> processClickNotification(Uint8List message) {
final status = PlayKeyPadStatus.fromBuffer(message);

View File

@@ -23,6 +23,7 @@
"allowsRunningInBackground": "Ermöglicht es BikeControl, im Hintergrund weiterzulaufen.",
"alreadyBoughtTheApp": "App bereits gekauft? Dann musst Du BikeControl nicht erneut kaufen. Aus technischen Gründen lässt sich leider nicht feststellen, ob die App früher bereits erworben wurde. \n\nGebe Deine Play Store-Kauf-ID (z.B. GPA.3356-1337-1338-1339) ein, um die Vollversion freizuschalten. Falls Du diese nicht finden kannst, kontaktiere mich bitte direkt.",
"alreadyBoughtTheAppPreviously": "App zuvor bereits gekauft?",
"androidSystemAction": "Android-Systemaktion",
"appIdActions": "{appId} Aktionen",
"@appIdActions": {
"placeholders": {
@@ -45,7 +46,7 @@
"chooseBikeControlInConnectionScreen": "Wähle im Verbindungsbildschirm BikeControl aus.",
"clickAButtonOnYourController": "Klicke eine Controller-Taste, um deren Aktion zu bearbeiten, oder tippe auf das Bearbeitungssymbol.",
"clickV2EventInfo": "Dein Click V2 sendet möglicherweise keine Tastenereignisse mehr. Probier mal ein paar Tasten aus und schau, ob sie in BikeControl angezeigt werden.",
"clickV2Instructions": "Damit dein Zwift Click V2 optimal funktioniert, solltest du das Gerät vor jeder Trainings-Session in der Zwift-App verbinden.\nAnsonsten funktioniert der Click V2 nach einer Minute nicht mehr.\n\n1. Öffne die Zwift-App.\n2. Melde dich an (kein Abonnement nötig) und öffne den Bildschirm für die Geräteverbindung.\n3. Verbinde deinen Trainer und dann den Zwift Click V2.\n4. Schließe die Zwift-App wieder und verbinde dich erneut in BikeControl.",
"clickV2Instructions": "Damit dein Zwift Click V2 optimal funktioniert, solltest du das Gerät vor jeder Trainings-Session in der Zwift-App verbinden.\nAnsonsten funktioniert der Click V2 nach einer Minute nicht mehr.\n\n1. Öffne die Zwift-App (nicht Companion!).\n2. Melde dich an (kein Abonnement nötig) und öffne den Bildschirm für die Geräteverbindung.\n3. Verbinde deinen Trainer und dann den Zwift Click V2.\n4. Schließe die Zwift-App wieder und verbinde dich erneut in BikeControl.",
"close": "Schließen",
"commandsRemainingToday": "{commandsRemainingToday}/{dailyCommandLimit} verbleibende Befehle heute",
"configuration": "Konfiguration",
@@ -139,6 +140,7 @@
"duplicate": "Duplikat",
"enableAutoRotation": "Aktiviere die automatische Drehung auf Ihrem Gerät, um sicherzustellen, dass die App ordnungsgemäß funktioniert.",
"enableBluetooth": "Bluetooth aktivieren",
"enableItInTheConnectionSettingsFirst": "Aktiviere es zuerst in den Verbindungseinstellungen.",
"enableKeyboardAccessMessage": "Aktiviere im folgenden Bildschirm die Tastatursteuerung für BikeControl. Falls BikeControl nicht angezeigt wird, füge es bitte manuell hinzu.",
"enableKeyboardMouseControl": "BikeControl sendet Maus- oder Tastaturkommandos an {appName}",
"@enableKeyboardMouseControl": {
@@ -148,7 +150,9 @@
}
}
},
"enableLocalConnectionMethodFirst": "Aktivieren Sie zuerst die Methode „Lokale Verbindung“.",
"enableMediaKeyDetection": "Medientastenerkennung aktivieren",
"enableMywhooshLinkInTheConnectionSettingsFirst": "Aktiviere zuerst MyWhoosh Link in den Verbindungseinstellungen.",
"enablePairingProcess": "Kopplungsprozess aktivieren",
"enablePermissions": "Berechtigungen aktivieren",
"enableSteeringWithPhone": "Sensoren Ihres Telefons aktivieren z.B. zum Lenken",

View File

@@ -23,6 +23,7 @@
"allowsRunningInBackground": "Allows BikeControl to keep running in background",
"alreadyBoughtTheApp": "Already bought the app? You don't need to pay for BikeControl, again. Due to technical difficulties, it's not possible to determine if the app was bought already.\n\nEnter your Play Store purchase ID (e.g. GPA.3356-1337-1338-1339) to unlock the full version. If you can't find it, please get in touch with me directly.",
"alreadyBoughtTheAppPreviously": "Already bought the app previously?",
"androidSystemAction": "Android System Action",
"appIdActions": "{appId} actions",
"@appIdActions": {
"placeholders": {
@@ -45,7 +46,7 @@
"chooseBikeControlInConnectionScreen": "Choose BikeControl in the connection screen.",
"clickAButtonOnYourController": "Click a button on your controller to edit its action or tap the edit icon.",
"clickV2EventInfo": "Your Click V2 may no longer send button events. Please check by tapping a few buttons and see if they are visible in BikeControl.",
"clickV2Instructions": "To make your Zwift Click V2 work best, you should connect it in the Zwift app before each training session.\nIf you don't do that, the Click V2 will stop working after a minute.\n\n1. Open Zwift app\n2. Log in (subscription not required) and open the device connection screen\n3. Connect your Trainer, then connect the Zwift Click V2\n4. Close the Zwift app again and connect again in BikeControl",
"clickV2Instructions": "To make your Zwift Click V2 work best, you should connect it in the Zwift app before each training session.\nIf you don't do that, the Click V2 will stop working after a minute.\n\n1. Open Zwift app (not the Companion!)\n2. Log in (subscription not required) and open the device connection screen\n3. Connect your Trainer, then connect the Zwift Click V2\n4. Close the Zwift app again and connect again in BikeControl",
"close": "Close",
"commandsRemainingToday": "{commandsRemainingToday}/{dailyCommandLimit} commands remaining today",
"configuration": "Configuration",
@@ -139,6 +140,7 @@
"duplicate": "Duplicate",
"enableAutoRotation": "Enable auto-rotation on your device to make sure the app works correctly.",
"enableBluetooth": "Enable Bluetooth",
"enableItInTheConnectionSettingsFirst": "Enable it in the connection settings first.",
"enableKeyboardAccessMessage": "Enable keyboard access in the following screen for BikeControl. If you don't see BikeControl, please add it manually.",
"enableKeyboardMouseControl": "BikeControl will send mouse or keyboard actions to control {appName}.",
"@enableKeyboardMouseControl": {
@@ -148,7 +150,9 @@
}
}
},
"enableLocalConnectionMethodFirst": "Enable Local Connection method, first.",
"enableMediaKeyDetection": "Enable Media Key Detection",
"enableMywhooshLinkInTheConnectionSettingsFirst": "Enable MyWhoosh Link in the connection settings first.",
"enablePairingProcess": "Enable Pairing Process",
"enablePermissions": "Enable Permissions",
"enableSteeringWithPhone": "Enable Phones' sensors to enable e.g. steering",

View File

@@ -23,6 +23,7 @@
"allowsRunningInBackground": "Permet à BikeControl de continuer à fonctionner en arrière-plan",
"alreadyBoughtTheApp": "Vous avez déjà acheté l'application ? Vous n'avez pas besoin de payer BikeControl une seconde fois. En raison de difficultés techniques, il est impossible de déterminer si l'application a déjà été achetée. \n\nSaisissez votre identifiant d'achat Play Store (par exemple : GPA.3356-1337-1338-1339) pour débloquer la version complète. Si vous ne le trouvez pas, veuillez me contacter directement.",
"alreadyBoughtTheAppPreviously": "Vous avez déjà acheté l'application ?",
"androidSystemAction": "Actions système Android",
"appIdActions": "{appId} actions",
"@appIdActions": {
"placeholders": {
@@ -45,7 +46,7 @@
"chooseBikeControlInConnectionScreen": "Sélectionnez BikeControl dans l'écran de connexion.",
"clickAButtonOnYourController": "Cliquez sur un bouton de votre manette pour modifier son action ou appuyez sur l'icône de modification.",
"clickV2EventInfo": "Votre Click V2 n'envoie peut-être plus les événements liés aux boutons. Vérifiez en appuyant sur quelques boutons et voyez s'ils apparaissent dans BikeControl.",
"clickV2Instructions": "Pour que ton Zwift Click V2 marche super bien, tu dois le connecter à l'appli Zwift une fois par jour.\nSi tu ne le fais pas, le Click V2 s'arrêtera de fonctionner au bout d'une minute.\n\n1. Ouvre l'appli Zwift.\n2. Connecte-toi (pas besoin d'abonnement) et ouvre l'écran de connexion des appareils.\n3. Connecte ton home trainer, puis connecte le Zwift Click V2.\n4. Ferme l'appli Zwift et reconnecte-toi dans BikeControl.",
"clickV2Instructions": "Pour que ton Zwift Click V2 marche super bien, tu dois le connecter à l'appli Zwift une fois par jour.\nSi tu ne le fais pas, le Click V2 s'arrêtera de fonctionner au bout d'une minute.\n\n1. Ouvre l'appli Zwift (pas Companion!)\n2. Connecte-toi (pas besoin d'abonnement) et ouvre l'écran de connexion des appareils.\n3. Connecte ton home trainer, puis connecte le Zwift Click V2.\n4. Ferme l'appli Zwift et reconnecte-toi dans BikeControl.",
"close": "Fermer",
"commandsRemainingToday": "{commandsRemainingToday}/{dailyCommandLimit} commandes restantes aujourd'hui",
"configuration": "Configuration",
@@ -139,6 +140,7 @@
"duplicate": "Double",
"enableAutoRotation": "Activez la rotation automatique sur votre appareil pour vous assurer que l'application fonctionne correctement.",
"enableBluetooth": "Activer le Bluetooth",
"enableItInTheConnectionSettingsFirst": "Activez-le d'abord dans les paramètres de connexion.",
"enableKeyboardAccessMessage": "Activez l'accès au clavier dans l'écran suivant pour BikeControl. Si vous ne voyez pas BikeControl, veuillez l'ajouter manuellement.",
"enableKeyboardMouseControl": "BikeControl enverra des actions de souris ou de clavier pour contrôler {appName} .",
"@enableKeyboardMouseControl": {
@@ -148,7 +150,9 @@
}
}
},
"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",
"enablePermissions": "Activer les autorisations",
"enableSteeringWithPhone": "Activez les capteurs du téléphone pour permettre, par exemple, la direction.",

View File

@@ -14,7 +14,7 @@
"action": "Azione",
"adjustControllerButtons": "Regola i pulsanti del controller",
"afterDate": "Dopo il {date}",
"allow": "Consentire",
"allow": "Consenti",
"allowAccessibilityService": "Consenti servizio di accessibilità",
"allowBluetoothConnections": "Consenti connessioni Bluetooth",
"allowBluetoothScan": "Consenti scansione Bluetooth",
@@ -23,6 +23,7 @@
"allowsRunningInBackground": "Consente a BikeControl di continuare a funzionare in background",
"alreadyBoughtTheApp": "Hai già acquistato l'app? Non devi più pagare per BikeControl. A causa di problemi tecnici, non è possibile determinare se l'app è già stata acquistata.\n\nInserisci il tuo ID di acquisto sul Play Store (ad esempio GPA.3356-1337-1338-1339) per sbloccare la versione completa. Se non riesci a trovarlo, contattami direttamente.",
"alreadyBoughtTheAppPreviously": "Hai già acquistato l'app in precedenza?",
"androidSystemAction": "Azione del sistema Android",
"appIdActions": "{appId} azioni",
"@appIdActions": {
"placeholders": {
@@ -45,7 +46,7 @@
"chooseBikeControlInConnectionScreen": "Selezionare BikeControl nella schermata di connessione.",
"clickAButtonOnYourController": "Fai clic su un pulsante del controller per modificarne l'azione oppure tocca l'icona di modifica.",
"clickV2EventInfo": "Il tuo Click V2 potrebbe non inviare più eventi tramite i pulsanti. Verifica toccando alcuni pulsanti e verifica se sono visibili in BikeControl.",
"clickV2Instructions": "Per far funzionare al meglio il tuo Zwift Click V2, dovresti collegarlo all'app Zwift prima di ogni sessione di allenamento. In caso contrario, il Click V2 smetterà di funzionare dopo un minuto. \n1. Apri l'app Zwift \n2. Accedi (non richiesto un abbonamento attivo) e apri la schermata di connessione del dispositivo \n3. Collega il tuo trainer, quindi collega lo Zwift Click V2, verifica che i comandi del controller funzionino \n4. Chiudi nuovamente l'app Zwift e riconnettiti a BikeControl",
"clickV2Instructions": "Per far funzionare al meglio il tuo Zwift Click V2, dovresti collegarlo all'app Zwift prima di ogni sessione di allenamento. In caso contrario, il Click V2 smetterà di funzionare dopo un minuto. \n\n1. Apri l'app Zwift (non il Companion!)\n2. Accedi (non richiesto un abbonamento attivo) e apri la schermata di connessione del dispositivo \n3. Collega il tuo trainer, quindi collega lo Zwift Click V2, verifica che i comandi del controller funzionino \n4. Chiudi nuovamente l'app Zwift e riconnettiti a BikeControl",
"close": "Chiudi",
"commandsRemainingToday": "{commandsRemainingToday}/{dailyCommandLimit} comandi rimanenti oggi",
"configuration": "Configurazione",
@@ -139,6 +140,7 @@
"duplicate": "Duplica",
"enableAutoRotation": "Abilita la rotazione automatica sul tuo dispositivo per assicurarti che l'app funzioni correttamente.",
"enableBluetooth": "Abilita Bluetooth",
"enableItInTheConnectionSettingsFirst": "Per prima cosa abilitalo nelle impostazioni di connessione.",
"enableKeyboardAccessMessage": "Abilita l'accesso tramite tastiera nella schermata seguente per BikeControl. Se BikeControl non è presente, aggiungilo manualmente.",
"enableKeyboardMouseControl": "BikeControl invierà azioni del mouse o della tastiera per controllare {appName} .",
"@enableKeyboardMouseControl": {
@@ -148,7 +150,9 @@
}
}
},
"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",
"enablePermissions": "Abilita autorizzazioni",
"enableSteeringWithPhone": "Abilita i sensori dei telefoni per abilitare ad esempio lo sterzo",

View File

@@ -23,6 +23,7 @@
"allowsRunningInBackground": "Umożliwia działanie BikeControl w tle",
"alreadyBoughtTheApp": "Kupiłeś już aplikację? Nie musisz płacić za BikeControl. Z powodu problemów technicznych nie można ustalić, czy aplikacja została już zakupiona. \n\nWprowadź swój identyfikator zakupu w Sklepie Play (np. GPA.3356-1337-1338-1339), aby odblokować pełną wersję. Jeśli nie możesz jej znaleźć, skontaktuj się ze mną bezpośrednio.",
"alreadyBoughtTheAppPreviously": "Już wcześniej kupiłeś aplikację?",
"androidSystemAction": "Akcja systemu Android",
"appIdActions": "{appId} działania",
"@appIdActions": {
"placeholders": {
@@ -45,7 +46,7 @@
"chooseBikeControlInConnectionScreen": "Wybierz BikeControl na ekranie połączenia.",
"clickAButtonOnYourController": "Kliknij przycisk na kontrolerze, aby edytować jego akcję lub naciśnij ikonę edycji.",
"clickV2EventInfo": "Twóje Click V2 może już nie wysyłać zdarzeń przycisków. Sprawdź, naciskając kilka przycisków, czy są one widoczne w BikeControl.",
"clickV2Instructions": "Aby Twoje Zwift Click V2 działały optymalnie, należy połączyć je z aplikacją Zwift przed każdym treningiem.\nJeśli tego nie zrobisz, Click V2 przestanie działać po minucie.\n\n1. Otwórz aplikację Zwift\n2. Zaloguj się (nie wymaga subskrypcji) i otwórz ekran połączeń z urządzeniami\n3. Połącz się z trenażerem, a następnie z Zwift Click V2\n4. Zamknij aplikację Zwift i ponownie połącz się z BikeControl",
"clickV2Instructions": "Aby Twoje Zwift Click V2 działały optymalnie, należy połączyć je z aplikacją Zwift przed każdym treningiem.\nJeśli tego nie zrobisz, Click V2 przestanie działać po minucie.\n\n1. Otwórz aplikację Zwift (nie Companion!)\n2. Zaloguj się (nie wymaga subskrypcji) i otwórz ekran połączeń z urządzeniami\n3. Połącz się z trenażerem, a następnie z Zwift Click V2\n4. Zamknij aplikację Zwift i ponownie połącz się z BikeControl",
"close": "Zamknij",
"commandsRemainingToday": "{commandsRemainingToday}/{dailyCommandLimit} dzisiejsze pozostałe polecenia",
"configuration": "Konfiguracja",
@@ -139,6 +140,7 @@
"duplicate": "Duplikuj",
"enableAutoRotation": "Włącz funkcję automatycznego obracania na urządzeniu, aby mieć pewność, że aplikacja będzie działać prawidłowo.",
"enableBluetooth": "Włącz Bluetooth",
"enableItInTheConnectionSettingsFirst": "Najpierw włącz tę opcję w ustawieniach połączenia.",
"enableKeyboardAccessMessage": "Włącz dostęp do klawiatury dla BikeControl na poniższym ekranie. Jeśli nie widzisz BikeControl, dodaj go ręcznie.",
"enableKeyboardMouseControl": "BikeControl będzie wysyłał akcje myszy lub klawiatury do kontrolera {appName} .",
"@enableKeyboardMouseControl": {
@@ -148,7 +150,9 @@
}
}
},
"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",
"enablePermissions": "Nadaj uprawnienia",
"enableSteeringWithPhone": "Włącz czujniki telefonów, aby umożliwić np. sterowanie",

View File

@@ -225,7 +225,11 @@ class _BikeControlAppState extends State<BikeControlApp> {
? OnboardingPage(
onComplete: () {
setState(() {
_showPage = BCPage.trainer;
if (core.obpMdnsEmulator.connectedApp.value == null) {
_showPage = BCPage.trainer;
} else {
_showPage = BCPage.devices;
}
});
},
)

View File

@@ -2,6 +2,7 @@ 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';
@@ -14,6 +15,7 @@ import 'package:bike_control/widgets/custom_keymap_selector.dart';
import 'package:bike_control/widgets/ui/button_widget.dart';
import 'package:bike_control/widgets/ui/colored_title.dart';
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/services.dart';
@@ -109,7 +111,6 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 8,
children: [
Text('Editing').h3,
AnimatedContainer(
duration: const Duration(milliseconds: 600),
curve: Curves.easeOut,
@@ -154,15 +155,31 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
..._buildTrainerConnectionActions(core.logic.obpConnectedApp!.supportedActions),
],
if (core.settings.getMyWhooshLinkEnabled() && core.logic.showMyWhooshLink) ...[
if (core.logic.showMyWhooshLink) ...[
SizedBox(height: 8),
ColoredTitle(text: context.i18n.myWhooshDirectConnectAction),
..._buildTrainerConnectionActions(core.whooshLink.supportedActions),
if (!core.settings.getMyWhooshLinkEnabled())
Warning(
important: false,
children: [
Text(AppLocalizations.of(context).enableMywhooshLinkInTheConnectionSettingsFirst),
],
)
else
..._buildTrainerConnectionActions(core.whooshLink.supportedActions),
],
if (core.logic.isZwiftBleEnabled || core.logic.isZwiftMdnsEnabled) ...[
if (core.logic.showZwiftBleEmulator || core.logic.showZwiftMsdnEmulator) ...[
SizedBox(height: 8),
ColoredTitle(text: context.i18n.zwiftControllerAction),
..._buildTrainerConnectionActions(core.zwiftEmulator.supportedActions),
if (!core.settings.getZwiftBleEmulatorEnabled() && !core.settings.getZwiftMdnsEmulatorEnabled())
Warning(
important: false,
children: [
Text(AppLocalizations.of(context).enableItInTheConnectionSettingsFirst),
],
)
else
..._buildTrainerConnectionActions(core.zwiftEmulator.supportedActions),
],
if (core.logic.showLocalRemoteOptions) ...[
@@ -175,40 +192,48 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
title: Text(context.i18n.predefinedAction(trainerApp.name)),
isActive: false,
onPressed: () {
showDropdown(
context: context,
builder: (c) => DropdownMenu(
children: actionsWithInGameAction!.map((keyPairAction) {
return MenuButton(
leading: keyPairAction.inGameAction?.icon != null
? Icon(keyPairAction.inGameAction!.icon)
: null,
onPressed: (_) {
// Copy all properties from the selected predefined action
if (core.actionHandler.supportedModes.contains(SupportedMode.keyboard)) {
_keyPair.physicalKey = keyPairAction.physicalKey;
_keyPair.logicalKey = keyPairAction.logicalKey;
_keyPair.modifiers = List.of(keyPairAction.modifiers);
} else {
_keyPair.physicalKey = null;
_keyPair.logicalKey = null;
_keyPair.modifiers = [];
}
if (core.actionHandler.supportedModes.contains(SupportedMode.touch)) {
_keyPair.touchPosition = keyPairAction.touchPosition;
} else {
_keyPair.touchPosition = Offset.zero;
}
_keyPair.isLongPress = keyPairAction.isLongPress;
_keyPair.inGameAction = keyPairAction.inGameAction;
_keyPair.inGameActionValue = keyPairAction.inGameActionValue;
setState(() {});
},
child: Text(keyPairAction.toString()),
);
}).toList(),
),
);
if (!core.settings.getLocalEnabled()) {
buildToast(
navigatorKey.currentContext!,
title: AppLocalizations.of(context).enableLocalConnectionMethodFirst,
);
} else {
showDropdown(
context: context,
builder: (c) => DropdownMenu(
children: actionsWithInGameAction!.map((keyPairAction) {
return MenuButton(
leading: keyPairAction.inGameAction?.icon != null
? Icon(keyPairAction.inGameAction!.icon)
: null,
onPressed: (_) {
// Copy all properties from the selected predefined action
if (core.actionHandler.supportedModes.contains(SupportedMode.keyboard)) {
_keyPair.physicalKey = keyPairAction.physicalKey;
_keyPair.logicalKey = keyPairAction.logicalKey;
_keyPair.modifiers = List.of(keyPairAction.modifiers);
} else {
_keyPair.physicalKey = null;
_keyPair.logicalKey = null;
_keyPair.modifiers = [];
}
if (core.actionHandler.supportedModes.contains(SupportedMode.touch)) {
_keyPair.touchPosition = keyPairAction.touchPosition;
} else {
_keyPair.touchPosition = Offset.zero;
}
_keyPair.isLongPress = keyPairAction.isLongPress;
_keyPair.inGameAction = keyPairAction.inGameAction;
_keyPair.inGameActionValue = keyPairAction.inGameActionValue;
_keyPair.androidAction = null;
setState(() {});
},
child: Text(keyPairAction.toString()),
);
}).toList(),
),
);
}
},
),
),
@@ -217,42 +242,62 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
SelectableCard(
icon: RadixIcons.keyboard,
title: Text(context.i18n.simulateKeyboardShortcut),
isActive: _keyPair.physicalKey != null && !_keyPair.isSpecialKey,
isActive:
_keyPair.physicalKey != null && !_keyPair.isSpecialKey && core.settings.getLocalEnabled(),
value: _keyPair.toString(),
onPressed: () async {
await showDialog<void>(
context: context,
barrierDismissible: false, // enable Escape key
builder: (c) => HotKeyListenerDialog(
customApp: core.actionHandler.supportedApp! as CustomApp,
keyPair: _keyPair,
),
);
setState(() {});
widget.onUpdate();
if (!core.settings.getLocalEnabled()) {
buildToast(
navigatorKey.currentContext!,
title: AppLocalizations.of(context).enableLocalConnectionMethodFirst,
);
} else {
await showDialog<void>(
context: context,
barrierDismissible: false, // enable Escape key
builder: (c) => HotKeyListenerDialog(
customApp: core.actionHandler.supportedApp! as CustomApp,
keyPair: _keyPair,
),
);
_keyPair.androidAction = null;
setState(() {});
widget.onUpdate();
}
},
),
if (core.actionHandler.supportedModes.contains(SupportedMode.touch))
SelectableCard(
title: Text(context.i18n.simulateTouch),
icon: core.actionHandler is AndroidActions ? Icons.touch_app_outlined : BootstrapIcons.mouse,
isActive: _keyPair.physicalKey == null && _keyPair.touchPosition != Offset.zero,
isActive:
((core.actionHandler is AndroidActions || _keyPair.physicalKey == null) &&
_keyPair.touchPosition != Offset.zero) &&
core.settings.getLocalEnabled(),
value: _keyPair.toString(),
onPressed: () async {
if (_keyPair.touchPosition == Offset.zero) {
_keyPair.touchPosition = Offset(50, 50);
}
_keyPair.physicalKey = null;
_keyPair.logicalKey = null;
await Navigator.of(context).push<bool?>(
MaterialPageRoute(
builder: (c) => TouchAreaSetupPage(
keyPair: _keyPair,
if (!core.settings.getLocalEnabled()) {
buildToast(
navigatorKey.currentContext!,
title: AppLocalizations.of(context).enableLocalConnectionMethodFirst,
);
} else {
if (_keyPair.touchPosition == Offset.zero) {
_keyPair.touchPosition = Offset(50, 50);
}
_keyPair.physicalKey = null;
_keyPair.logicalKey = null;
_keyPair.androidAction = null;
await Navigator.of(context).push<bool?>(
MaterialPageRoute(
builder: (c) => TouchAreaSetupPage(
keyPair: _keyPair,
),
),
),
);
setState(() {});
widget.onUpdate();
);
setState(() {});
widget.onUpdate();
}
},
),
@@ -260,83 +305,141 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
Builder(
builder: (context) => SelectableCard(
icon: Icons.music_note_outlined,
isActive: _keyPair.isSpecialKey,
isActive: _keyPair.isSpecialKey && core.settings.getLocalEnabled(),
title: Text(context.i18n.simulateMediaKey),
value: _keyPair.toString(),
onPressed: () {
showDropdown(
context: context,
builder: (c) => DropdownMenu(
children: [
MenuButton(
leading: Icon(Icons.play_arrow_outlined),
onPressed: (c) {
_keyPair.physicalKey = PhysicalKeyboardKey.mediaPlayPause;
_keyPair.logicalKey = null;
if (!core.settings.getLocalEnabled()) {
buildToast(
navigatorKey.currentContext!,
title: AppLocalizations.of(context).enableLocalConnectionMethodFirst,
);
} else {
showDropdown(
context: context,
builder: (c) => DropdownMenu(
children: [
MenuButton(
leading: Icon(Icons.play_arrow_outlined),
onPressed: (c) {
_keyPair.physicalKey = PhysicalKeyboardKey.mediaPlayPause;
_keyPair.touchPosition = Offset.zero;
_keyPair.logicalKey = null;
_keyPair.androidAction = null;
setState(() {});
widget.onUpdate();
},
child: Text(context.i18n.playPause),
),
MenuButton(
leading: Icon(Icons.stop_outlined),
onPressed: (c) {
_keyPair.physicalKey = PhysicalKeyboardKey.mediaStop;
_keyPair.logicalKey = null;
setState(() {});
widget.onUpdate();
},
child: Text(context.i18n.playPause),
),
MenuButton(
leading: Icon(Icons.stop_outlined),
onPressed: (c) {
_keyPair.physicalKey = PhysicalKeyboardKey.mediaStop;
_keyPair.touchPosition = Offset.zero;
_keyPair.logicalKey = null;
_keyPair.androidAction = null;
setState(() {});
widget.onUpdate();
},
child: Text(context.i18n.stop),
),
MenuButton(
leading: Icon(Icons.skip_previous_outlined),
onPressed: (c) {
_keyPair.physicalKey = PhysicalKeyboardKey.mediaTrackPrevious;
_keyPair.logicalKey = null;
setState(() {});
widget.onUpdate();
},
child: Text(context.i18n.stop),
),
MenuButton(
leading: Icon(Icons.skip_previous_outlined),
onPressed: (c) {
_keyPair.physicalKey = PhysicalKeyboardKey.mediaTrackPrevious;
_keyPair.touchPosition = Offset.zero;
_keyPair.logicalKey = null;
_keyPair.androidAction = null;
setState(() {});
widget.onUpdate();
},
child: Text(context.i18n.previous),
),
MenuButton(
leading: Icon(Icons.skip_next_outlined),
onPressed: (c) {
_keyPair.physicalKey = PhysicalKeyboardKey.mediaTrackNext;
_keyPair.logicalKey = null;
setState(() {});
widget.onUpdate();
},
child: Text(context.i18n.previous),
),
MenuButton(
leading: Icon(Icons.skip_next_outlined),
onPressed: (c) {
_keyPair.physicalKey = PhysicalKeyboardKey.mediaTrackNext;
_keyPair.touchPosition = Offset.zero;
_keyPair.logicalKey = null;
_keyPair.androidAction = null;
setState(() {});
widget.onUpdate();
},
child: Text(context.i18n.next),
),
MenuButton(
leading: Icon(Icons.volume_up_outlined),
onPressed: (c) {
_keyPair.physicalKey = PhysicalKeyboardKey.audioVolumeUp;
_keyPair.logicalKey = null;
setState(() {});
widget.onUpdate();
},
child: Text(context.i18n.next),
),
MenuButton(
leading: Icon(Icons.volume_up_outlined),
onPressed: (c) {
_keyPair.physicalKey = PhysicalKeyboardKey.audioVolumeUp;
_keyPair.touchPosition = Offset.zero;
_keyPair.logicalKey = null;
_keyPair.androidAction = null;
setState(() {});
widget.onUpdate();
},
child: Text(context.i18n.volumeUp),
),
MenuButton(
leading: Icon(Icons.volume_down_outlined),
child: Text(context.i18n.volumeDown),
onPressed: (c) {
_keyPair.physicalKey = PhysicalKeyboardKey.audioVolumeDown;
_keyPair.logicalKey = null;
setState(() {});
widget.onUpdate();
},
child: Text(context.i18n.volumeUp),
),
MenuButton(
leading: Icon(Icons.volume_down_outlined),
child: Text(context.i18n.volumeDown),
onPressed: (c) {
_keyPair.physicalKey = PhysicalKeyboardKey.audioVolumeDown;
_keyPair.touchPosition = Offset.zero;
_keyPair.logicalKey = null;
_keyPair.androidAction = null;
setState(() {});
widget.onUpdate();
},
),
],
),
);
setState(() {});
widget.onUpdate();
},
),
],
),
);
}
},
),
),
if (core.logic.showLocalControl && core.actionHandler is AndroidActions)
Builder(
builder: (context) => SelectableCard(
icon: Icons.settings_remote_outlined,
isActive: _keyPair.androidAction != null && core.settings.getLocalEnabled(),
title: Text(AppLocalizations.of(context).androidSystemAction),
value: _keyPair.androidAction?.title,
onPressed: () {
if (!core.settings.getLocalEnabled()) {
buildToast(navigatorKey.currentContext!, title: 'Enable Local Connection method, first.');
} else {
showDropdown(
context: context,
builder: (c) => DropdownMenu(
children: AndroidSystemAction.values
.map(
(action) => MenuButton(
leading: Icon(action.icon),
onPressed: (_) {
_keyPair.androidAction = action;
_keyPair.physicalKey = null;
_keyPair.logicalKey = null;
_keyPair.modifiers = [];
_keyPair.touchPosition = Offset.zero;
_keyPair.inGameAction = null;
_keyPair.inGameActionValue = null;
setState(() {});
widget.onUpdate();
},
child: Text(action.title),
),
)
.toList(),
),
);
}
},
),
),
@@ -369,6 +472,7 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
onPressed: (_) {
_keyPair.inGameAction = InGameAction.headwindSpeed;
_keyPair.inGameActionValue = value;
_keyPair.androidAction = null;
widget.onUpdate();
setState(() {});
},
@@ -382,6 +486,7 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
onPressed: (_) {
_keyPair.inGameAction = InGameAction.headwindHeartRateMode;
_keyPair.inGameActionValue = null;
_keyPair.androidAction = null;
widget.onUpdate();
setState(() {});
},
@@ -416,6 +521,7 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
_keyPair.touchPosition = Offset.zero;
_keyPair.inGameAction = null;
_keyPair.inGameActionValue = null;
_keyPair.androidAction = null;
widget.onUpdate();
setState(() {});
},
@@ -456,6 +562,7 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
_keyPair.touchPosition = Offset.zero;
_keyPair.physicalKey = null;
_keyPair.logicalKey = null;
_keyPair.androidAction = null;
_keyPair.inGameAction = action;
_keyPair.inGameActionValue = ingame;
widget.onUpdate();
@@ -470,6 +577,7 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
_keyPair.touchPosition = Offset.zero;
_keyPair.physicalKey = null;
_keyPair.logicalKey = null;
_keyPair.androidAction = null;
_keyPair.inGameAction = action;
_keyPair.inGameActionValue = null;
widget.onUpdate();

View File

@@ -53,7 +53,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
spacing: 4,
children: [
Text(screenshotMode ? 'Trainer app' : app.name),
if (app.supportsOpenBikeProtocol) Icon(Icons.star),
if (app.supportsOpenBikeProtocol.isNotEmpty) Icon(Icons.star),
],
),
popup: SelectPopup(
@@ -65,7 +65,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
spacing: 4,
children: [
Text(app.name),
if (app.supportsOpenBikeProtocol) Icon(Icons.star),
if (app.supportsOpenBikeProtocol.isNotEmpty) Icon(Icons.star),
],
),
);
@@ -88,7 +88,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
core.zwiftEmulator.stopAdvertising();
}
}
if (!selectedApp.supportsOpenBikeProtocol) {
if (selectedApp.supportsOpenBikeProtocol.isEmpty) {
if (core.obpMdnsEmulator.isStarted.value) {
core.obpMdnsEmulator.stopServer();
}
@@ -113,7 +113,7 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
},
),
if (core.settings.getTrainerApp() != null) ...[
if (core.settings.getTrainerApp()!.supportsOpenBikeProtocol == true &&
if (core.settings.getTrainerApp()!.supportsOpenBikeProtocol.isNotEmpty &&
!screenshotMode &&
!widget.onboardingMode)
Text(
@@ -194,13 +194,13 @@ class _ConfigurationPageState extends State<ConfigurationPage> {
Future<void> _setTarget(BuildContext context, Target target) async {
await core.settings.setLastTarget(target);
if (core.settings.getTrainerApp()?.supportsOpenBikeProtocol == true && !core.logic.emulatorEnabled) {
if ((core.settings.getTrainerApp()?.supportsOpenBikeProtocol.isNotEmpty ?? false) && !core.logic.emulatorEnabled) {
core.settings.setObpMdnsEnabled(true);
}
// enable local connection on Windows if the app doesn't support OBP
if (target == Target.thisDevice &&
core.settings.getTrainerApp()?.supportsOpenBikeProtocol == false &&
core.settings.getTrainerApp()?.supportsOpenBikeProtocol.isEmpty == true &&
!kIsWeb &&
Platform.isWindows) {
core.settings.setLocalEnabled(true);

View File

@@ -65,7 +65,7 @@ class _CustomizeState extends State<CustomizePage> {
Expanded(child: Text(a.name)),
if (a is CustomApp)
BetaPill(text: 'CUSTOM')
else if (a.supportsOpenBikeProtocol)
else if (a.supportsOpenBikeProtocol.isNotEmpty)
Icon(Icons.star, size: 16),
],
),

View File

@@ -5,7 +5,11 @@ import 'package:bike_control/bluetooth/devices/zwift/zwift_device.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/apps/supported_app.dart';
import 'package:bike_control/utils/requirements/multi.dart';
import 'package:bike_control/utils/requirements/platform.dart';
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/scan.dart';
import 'package:bike_control/widgets/title.dart';
import 'package:bike_control/widgets/ui/help_button.dart';
@@ -29,6 +33,7 @@ enum _OnboardingStep {
permissions,
connect,
trainer,
openbikecontrol,
finish,
}
@@ -103,7 +108,14 @@ class _OnboardingPageState extends State<OnboardingPage> {
_OnboardingStep.trainer => _TrainerOnboardingStep(
onComplete: () {
setState(() {
_currentStep = _OnboardingStep.finish;
if (core.settings.getTrainerApp()?.supportsOpenBikeProtocol.contains(
OpenBikeProtocolSupport.network,
) ??
false) {
_currentStep = _OnboardingStep.openbikecontrol;
} else {
_currentStep = _OnboardingStep.finish;
}
});
},
),
@@ -115,12 +127,13 @@ class _OnboardingPageState extends State<OnboardingPage> {
SizedBox(height: 30),
Icon(Icons.check_circle, size: 58, color: Colors.green),
ColoredTitle(text: AppLocalizations.of(context).setupComplete),
Text(
AppLocalizations.of(
context,
).asAFinalStepYoullChooseHowToConnectTo(core.settings.getTrainerApp()?.name ?? 'your trainer'),
textAlign: TextAlign.center,
).small.muted,
if (core.obpMdnsEmulator.connectedApp.value == null)
Text(
AppLocalizations.of(
context,
).asAFinalStepYoullChooseHowToConnectTo(core.settings.getTrainerApp()?.name ?? 'your trainer'),
textAlign: TextAlign.center,
).small.muted,
SizedBox(height: 30),
PrimaryButton(
@@ -133,6 +146,13 @@ class _OnboardingPageState extends State<OnboardingPage> {
),
],
),
_OnboardingStep.openbikecontrol => _OpenBikeControlConnectPage(
onComplete: () {
setState(() {
_currentStep = _OnboardingStep.finish;
});
},
),
},
),
),
@@ -329,3 +349,49 @@ class _TrainerOnboardingStepState extends State<_TrainerOnboardingStep> {
);
}
}
class _OpenBikeControlConnectPage extends StatefulWidget {
final VoidCallback onComplete;
const _OpenBikeControlConnectPage({super.key, required this.onComplete});
@override
State<_OpenBikeControlConnectPage> createState() => _OpenBikeControlConnectPageState();
}
class _OpenBikeControlConnectPageState extends State<_OpenBikeControlConnectPage> {
@override
void initState() {
super.initState();
if (!core.obpMdnsEmulator.isStarted.value) {
if (!core.logic.isObpMdnsEnabled) {
core.settings.setObpMdnsEnabled(true);
}
core.logic.startEnabledConnectionMethod();
}
core.obpMdnsEmulator.connectedApp.addListener(() {
if (core.obpMdnsEmulator.connectedApp.value != null) {
widget.onComplete();
}
});
}
@override
Widget build(BuildContext context) {
return Column(
spacing: 22,
children: [
Text(
AppLocalizations.of(context).openBikeControlAnnouncement(core.settings.getTrainerApp()!.name),
).small,
OpenBikeControlMdnsTile(),
if (core.settings.getLastTarget() == Target.otherDevice &&
core.settings.getTrainerApp()?.supportsOpenBikeProtocol.contains(OpenBikeProtocolSupport.ble) == true) ...[
SizedBox(height: 20),
Text('If you have issues with your network connection, you can also connect via Bluetooth.').small.muted,
OpenBikeControlBluetoothTile(),
],
],
);
}
}

View File

@@ -159,7 +159,7 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
spacing: 4,
children: [
Expanded(child: Text(screenshotMode ? 'Trainer app' : app.name)),
if (app.supportsOpenBikeProtocol) Icon(Icons.star),
if (app.supportsOpenBikeProtocol.isNotEmpty) Icon(Icons.star),
],
),
popup: SelectPopup(
@@ -171,7 +171,7 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
spacing: 4,
children: [
Text(app.name),
if (app.supportsOpenBikeProtocol) Icon(Icons.star),
if (app.supportsOpenBikeProtocol.isNotEmpty) Icon(Icons.star),
],
),
);

View File

@@ -79,6 +79,15 @@ class AndroidActions extends BaseActions {
return Success("Key pressed: ${keyPair.toString()}");
}
if (keyPair.androidAction != null) {
if (!core.settings.getLocalEnabled() || !core.logic.showLocalControl || !isKeyDown) {
return Ignored('Global action ignored');
}
await accessibilityHandler.performGlobalAction(keyPair.androidAction!.globalAction);
await IAPManager.instance.incrementCommandCount();
return Success("Global action: ${keyPair.androidAction!.title}");
}
final point = await resolveTouchPosition(keyPair: keyPair, windowInfo: windowInfo);
if (point != Offset.zero) {
try {

View File

@@ -1,6 +1,5 @@
import 'dart:io';
import 'package:bike_control/bluetooth/devices/hid/hid_device.dart';
import 'package:bike_control/bluetooth/devices/openbikecontrol/obc_ble_emulator.dart';
import 'package:bike_control/bluetooth/devices/openbikecontrol/obc_mdns_emulator.dart';
import 'package:bike_control/bluetooth/devices/openbikecontrol/protocol_parser.dart';
@@ -15,22 +14,21 @@ import 'package:bike_control/utils/actions/android.dart';
import 'package:bike_control/utils/actions/base_actions.dart';
import 'package:bike_control/utils/actions/remote.dart';
import 'package:bike_control/utils/keymap/apps/my_whoosh.dart';
import 'package:bike_control/utils/keymap/buttons.dart';
import 'package:bike_control/utils/keymap/apps/supported_app.dart';
import 'package:bike_control/utils/requirements/android.dart';
import 'package:bike_control/utils/settings/settings.dart';
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:media_key_detector/media_key_detector.dart';
import 'package:universal_ble/universal_ble.dart';
import '../bluetooth/connection.dart';
import '../bluetooth/devices/mywhoosh/link.dart';
import 'keymap/apps/rouvy.dart';
import 'media_key_handler.dart';
import 'requirements/multi.dart';
import 'requirements/platform.dart';
import 'smtc_stub.dart' if (dart.library.io) 'package:smtc_windows/smtc_windows.dart';
final core = Core();
@@ -170,11 +168,11 @@ class CoreLogic {
}
bool get showObpMdnsEmulator {
return core.settings.getTrainerApp()?.supportsOpenBikeProtocol == true;
return core.settings.getTrainerApp()?.supportsOpenBikeProtocol.contains(OpenBikeProtocolSupport.network) == true;
}
bool get showObpBluetoothEmulator {
return (core.settings.getTrainerApp()?.supportsOpenBikeProtocol == true) &&
return (core.settings.getTrainerApp()?.supportsOpenBikeProtocol.contains(OpenBikeProtocolSupport.ble) == true) &&
core.settings.getLastTarget() != Target.thisDevice;
}
@@ -209,11 +207,10 @@ class CoreLogic {
bool get ignoreWarnings =>
core.settings.getTrainerApp()?.supportsZwiftEmulation == true ||
core.settings.getTrainerApp()?.supportsOpenBikeProtocol == true;
core.settings.getTrainerApp()?.supportsOpenBikeProtocol.isNotEmpty == true;
bool get showLocalRemoteOptions =>
core.actionHandler.supportedModes.isNotEmpty &&
((showLocalControl && core.settings.getLocalEnabled()) || (isRemoteControlEnabled));
core.actionHandler.supportedModes.isNotEmpty && ((showLocalControl) || (isRemoteControlEnabled));
bool get hasNoConnectionMethod =>
!screenshotMode &&
@@ -261,9 +258,7 @@ class CoreLogic {
Future<bool> isTrainerConnected() async {
if (screenshotMode) {
return true;
} else if (showLocalControl &&
core.settings.getLocalEnabled() &&
core.settings.getTrainerApp()?.supportsOpenBikeProtocol == false) {
} else if (showLocalControl && core.settings.getLocalEnabled()) {
if (canRunAndroidService) {
return isAndroidServiceRunning();
} else {
@@ -336,89 +331,3 @@ class CoreLogic {
}
}
}
class MediaKeyHandler {
final ValueNotifier<bool> isMediaKeyDetectionEnabled = ValueNotifier(false);
bool _smtcInitialized = false;
SMTCWindows? _smtc;
void initialize() {
isMediaKeyDetectionEnabled.addListener(() async {
if (!isMediaKeyDetectionEnabled.value) {
if (Platform.isWindows) {
_smtc?.disableSmtc();
} else {
mediaKeyDetector.setIsPlaying(isPlaying: false);
mediaKeyDetector.removeListener(_onMediaKeyDetectedListener);
}
} else {
if (Platform.isWindows) {
if (!_smtcInitialized) {
_smtcInitialized = true;
await SMTCWindows.initialize();
}
_smtc = SMTCWindows(
metadata: const MusicMetadata(
title: 'BikeControl Media Key Handler',
album: 'BikeControl',
albumArtist: 'BikeControl',
artist: 'BikeControl',
),
// Timeline info for the OS media player
timeline: const PlaybackTimeline(
startTimeMs: 0,
endTimeMs: 1000,
positionMs: 0,
minSeekTimeMs: 0,
maxSeekTimeMs: 1000,
),
config: const SMTCConfig(
fastForwardEnabled: true,
nextEnabled: true,
pauseEnabled: true,
playEnabled: true,
rewindEnabled: true,
prevEnabled: true,
stopEnabled: true,
),
);
_smtc!.buttonPressStream.listen(_onMediaKeyPressedListener);
} else {
mediaKeyDetector.addListener(_onMediaKeyDetectedListener);
mediaKeyDetector.setIsPlaying(isPlaying: true);
}
}
});
}
void _onMediaKeyDetectedListener(MediaKey mediaKey) {
_onMediaKeyPressedListener(switch (mediaKey) {
MediaKey.playPause => PressedButton.play,
MediaKey.rewind => PressedButton.rewind,
MediaKey.fastForward => PressedButton.fastForward,
MediaKey.volumeUp => PressedButton.channelUp,
MediaKey.volumeDown => PressedButton.channelDown,
});
}
Future<void> _onMediaKeyPressedListener(PressedButton mediaKey) async {
final hidDevice = HidDevice('HID Device');
final keyPressed = mediaKey.name;
final button = hidDevice.getOrAddButton(
keyPressed,
() => ControllerButton(keyPressed),
);
var availableDevice = core.connection.controllerDevices.firstOrNullWhere(
(e) => e.toString() == hidDevice.toString(),
);
if (availableDevice == null) {
core.connection.addDevices([hidDevice]);
availableDevice = hidDevice;
}
availableDevice.handleButtonsClickedWithoutLongPressSupport([button]);
}
}

View File

@@ -242,7 +242,7 @@ class RevenueCatService {
// Direct the user to the paywall for a better experience
if (Platform.isAndroid && !_isAndroidWorking) {
buildToast(
context,
navigatorKey.currentContext!,
title: AppLocalizations.of(context).unlockingNotPossible,
duration: Duration(seconds: 5),
);

View File

@@ -1,7 +1,7 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/services.dart';
import 'package:bike_control/utils/keymap/apps/supported_app.dart';
import 'package:bike_control/utils/requirements/multi.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/services.dart';
import '../buttons.dart';
import '../keymap.dart';
@@ -13,7 +13,6 @@ class Biketerra extends SupportedApp {
packageName: "biketerra",
compatibleTargets: Target.values,
supportsZwiftEmulation: true,
supportsOpenBikeProtocol: false,
keymap: Keymap(
keyPairs: [
...ControllerButton.values

View File

@@ -1,4 +1,3 @@
import 'package:bike_control/main.dart';
import 'package:bike_control/utils/keymap/apps/supported_app.dart';
import 'package:bike_control/utils/requirements/multi.dart';
import 'package:dartx/dartx.dart';
@@ -14,7 +13,6 @@ class MyWhoosh extends SupportedApp {
packageName: "com.mywhoosh.whooshgame",
compatibleTargets: Target.values,
supportsZwiftEmulation: false,
supportsOpenBikeProtocol: screenshotMode,
star: true,
keymap: Keymap(
keyPairs: [

View File

@@ -10,7 +10,7 @@ class OpenBikeControl extends SupportedApp {
packageName: "org.openbikecontrol",
compatibleTargets: Target.values,
supportsZwiftEmulation: false,
supportsOpenBikeProtocol: true,
supportsOpenBikeProtocol: OpenBikeProtocolSupport.values,
keymap: Keymap(
keyPairs: [],
),

View File

@@ -9,13 +9,18 @@ import '../keymap.dart';
import 'custom_app.dart';
import 'my_whoosh.dart';
enum OpenBikeProtocolSupport {
ble,
network,
}
abstract class SupportedApp {
final List<Target> compatibleTargets;
final String packageName;
final String name;
final Keymap keymap;
final bool supportsZwiftEmulation;
final bool supportsOpenBikeProtocol;
final List<OpenBikeProtocolSupport> supportsOpenBikeProtocol;
final bool star;
const SupportedApp({
@@ -24,7 +29,7 @@ abstract class SupportedApp {
required this.keymap,
required this.compatibleTargets,
required this.supportsZwiftEmulation,
this.supportsOpenBikeProtocol = false,
this.supportsOpenBikeProtocol = const [],
this.star = false,
});

View File

@@ -18,7 +18,6 @@ class TrainingPeaks extends SupportedApp {
packageName: "com.indieVelo.client",
compatibleTargets: !kIsWeb && Platform.isIOS ? [Target.otherDevice] : Target.values,
supportsZwiftEmulation: false,
supportsOpenBikeProtocol: false,
star: true,
keymap: Keymap(
keyPairs: [

View File

@@ -37,7 +37,9 @@ enum InGameAction {
// openbikecontrol
up('Up', icon: RadixIcons.arrowUp),
down('Down', icon: RadixIcons.arrowDown);
down('Down', icon: RadixIcons.arrowDown),
home('Home', icon: RadixIcons.home),
menu('Menu', icon: RadixIcons.dropdownMenu);
final String title;
final bool isLongPress;
@@ -54,11 +56,14 @@ enum InGameAction {
}
class ControllerButton {
static const int _deviceIdSuffixLength = 4;
static const _unset = Object();
final String name;
final int? identifier;
final InGameAction? action;
final Color? color;
final IconData? icon;
final String? sourceDeviceId;
const ControllerButton(
this.name, {
@@ -66,8 +71,40 @@ class ControllerButton {
this.icon,
this.identifier,
this.action,
this.sourceDeviceId,
});
ControllerButton copyWith({
String? name,
int? identifier,
InGameAction? action,
Color? color,
IconData? icon,
Object? sourceDeviceId = _unset,
}) {
final newSourceDeviceId = sourceDeviceId == _unset ? this.sourceDeviceId : sourceDeviceId as String?;
return ControllerButton(
name ?? this.name,
color: color ?? this.color,
icon: icon ?? this.icon,
identifier: identifier ?? this.identifier,
action: action ?? this.action,
sourceDeviceId: newSourceDeviceId,
);
}
String get displayName {
if (sourceDeviceId == null) {
return name;
}
final shortenedId = sourceDeviceId!.length <= _deviceIdSuffixLength
? sourceDeviceId!
: sourceDeviceId!.substring(sourceDeviceId!.length - _deviceIdSuffixLength);
return '$name (${shortenedId.toUpperCase()})';
}
@override
String toString() {
return name;
@@ -82,10 +119,11 @@ class ControllerButton {
identifier == other.identifier &&
action == other.action &&
color == other.color &&
icon == other.icon;
icon == other.icon &&
sourceDeviceId == other.sourceDeviceId;
@override
int get hashCode => Object.hash(name, action, identifier, color, icon);
int get hashCode => Object.hash(name, action, identifier, color, icon, sourceDeviceId);
static List<ControllerButton> get values => [
...SterzoButtons.values,

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:accessibility/accessibility.dart';
import 'package:bike_control/gen/l10n.dart';
import 'package:bike_control/main.dart';
import 'package:bike_control/utils/actions/android.dart';
@@ -13,6 +14,23 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import '../actions/base_actions.dart';
import 'apps/custom_app.dart';
enum AndroidSystemAction {
back('Back', Icons.arrow_back_ios, GlobalAction.back),
dpadCenter('Select', Icons.radio_button_checked_outlined, GlobalAction.dpadCenter),
down('Arrow Down', Icons.arrow_downward, GlobalAction.down),
right('Arrow Right', Icons.arrow_forward, GlobalAction.right),
up('Arrow Up', Icons.arrow_upward, GlobalAction.up),
left('Arrow Left', Icons.arrow_back, GlobalAction.left),
home('Home', Icons.home_outlined, GlobalAction.home),
recents('Recents', Icons.apps, GlobalAction.recents);
final String title;
final IconData icon;
final GlobalAction globalAction;
const AndroidSystemAction(this.title, this.icon, this.globalAction);
}
class Keymap {
static Keymap custom = Keymap(keyPairs: []);
@@ -50,6 +68,7 @@ class Keymap {
keyPair.isLongPress = false;
keyPair.inGameAction = null;
keyPair.inGameActionValue = null;
keyPair.androidAction = null;
}
_updateStream.add(null);
}
@@ -112,6 +131,7 @@ class KeyPair {
bool isLongPress;
InGameAction? inGameAction;
int? inGameActionValue;
AndroidSystemAction? androidAction;
KeyPair({
required this.buttons,
@@ -122,6 +142,7 @@ class KeyPair {
this.isLongPress = false,
this.inGameAction,
this.inGameActionValue,
this.androidAction,
});
bool get isSpecialKey =>
@@ -144,8 +165,14 @@ class KeyPair {
_ => Icons.keyboard,
},
//_ when inGameAction != null && core.logic.emulatorEnabled => Icons.link,
_ when inGameAction != null && inGameAction!.icon != null => inGameAction!.icon,
_ when inGameAction != null && inGameAction!.icon != null && core.logic.emulatorEnabled => inGameAction!.icon,
_
when androidAction != null &&
core.logic.showLocalControl &&
core.settings.getLocalEnabled() &&
core.actionHandler is AndroidActions =>
androidAction!.icon,
_ when physicalKey != null && core.actionHandler.supportedModes.contains(SupportedMode.keyboard) =>
RadixIcons.keyboard,
_
@@ -159,7 +186,11 @@ class KeyPair {
}
bool get hasNoAction =>
logicalKey == null && physicalKey == null && touchPosition == Offset.zero && inGameAction == null;
logicalKey == null &&
physicalKey == null &&
touchPosition == Offset.zero &&
inGameAction == null &&
androidAction == null;
bool get hasActiveAction =>
screenshotMode ||
@@ -167,6 +198,14 @@ class KeyPair {
core.logic.showLocalControl &&
core.settings.getLocalEnabled() &&
core.actionHandler.supportedModes.contains(SupportedMode.keyboard)) ||
(isSpecialKey &&
core.logic.showLocalControl &&
core.settings.getLocalEnabled() &&
core.actionHandler is AndroidActions) ||
(androidAction != null &&
core.logic.showLocalControl &&
core.settings.getLocalEnabled() &&
core.actionHandler is AndroidActions) ||
(touchPosition != Offset.zero &&
core.logic.showLocalRemoteOptions &&
core.actionHandler.supportedModes.contains(SupportedMode.touch)) ||
@@ -193,6 +232,8 @@ class KeyPair {
inGameAction!.title,
if (inGameActionValue != null) '$inGameActionValue',
].joinToString(separator: ': ')
: (androidAction != null && core.logic.showLocalControl && core.actionHandler is AndroidActions)
? androidAction!.title
: (isSpecialKey && core.actionHandler.supportedModes.contains(SupportedMode.media))
? switch (physicalKey) {
PhysicalKeyboardKey.mediaPlayPause => AppLocalizations.current.playPause,
@@ -239,7 +280,16 @@ class KeyPair {
// encode to save in preferences
return jsonEncode({
'actions': buttons.map((e) => e.name).toList(),
'actions': buttons
.map(
(e) => e.sourceDeviceId == null
? e.name
: {
'name': e.name,
'deviceId': e.sourceDeviceId,
},
)
.toList(),
if (logicalKey != null) 'logicalKey': logicalKey?.keyId.toString(),
if (physicalKey != null) 'physicalKey': physicalKey?.usbHidUsage.toString() ?? '0',
if (modifiers.isNotEmpty) 'modifiers': modifiers.map((e) => e.name).toList(),
@@ -247,6 +297,7 @@ class KeyPair {
'isLongPress': isLongPress,
'inGameAction': inGameAction?.name,
'inGameActionValue': inGameActionValue,
'androidAction': androidAction?.name,
});
}
@@ -262,11 +313,33 @@ class KeyPair {
)
: Offset.zero;
final buttons = decoded['actions']
.map<ControllerButton>(
(e) => ControllerButton.values.firstOrNullWhere((element) => element.name == e) ?? ControllerButton(e),
)
.cast<ControllerButton>()
ControllerButton? decodeButton(dynamic raw) {
String? name;
String? deviceId;
if (raw is String) {
name = raw;
} else if (raw is Map) {
name = raw['name']?.toString();
deviceId = raw['deviceId']?.toString();
}
if (name == null) {
return null;
}
final baseButton = ControllerButton.values.firstOrNullWhere((element) => element.name == name);
if (baseButton != null) {
return baseButton.copyWith(sourceDeviceId: deviceId);
}
return ControllerButton(name, sourceDeviceId: deviceId);
}
final buttons = (decoded['actions'] as List)
.map<ControllerButton?>(decodeButton)
.whereType<ControllerButton>()
.toList();
if (buttons.isEmpty) {
return null;
@@ -295,6 +368,9 @@ class KeyPair {
? InGameAction.values.firstOrNullWhere((element) => element.name == decoded['inGameAction'])
: null,
inGameActionValue: decoded['inGameActionValue'],
androidAction: decoded.containsKey('androidAction')
? AndroidSystemAction.values.firstOrNullWhere((element) => element.name == decoded['androidAction'])
: null,
);
}
@@ -309,7 +385,8 @@ class KeyPair {
touchPosition == other.touchPosition &&
isLongPress == other.isLongPress &&
inGameAction == other.inGameAction &&
inGameActionValue == other.inGameActionValue;
inGameActionValue == other.inGameActionValue &&
androidAction == other.androidAction;
@override
int get hashCode => Object.hash(
@@ -320,5 +397,6 @@ class KeyPair {
isLongPress,
inGameAction,
inGameActionValue,
androidAction,
);
}

View File

@@ -0,0 +1,127 @@
import 'dart:io';
import 'package:bike_control/bluetooth/devices/hid/hid_device.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_volume_controller/flutter_volume_controller.dart';
import 'package:media_key_detector/media_key_detector.dart';
import 'smtc_stub.dart' if (dart.library.io) 'package:smtc_windows/smtc_windows.dart';
class MediaKeyHandler {
final ValueNotifier<bool> isMediaKeyDetectionEnabled = ValueNotifier(false);
bool _smtcInitialized = false;
double? _lastVolume;
SMTCWindows? _smtc;
void initialize() {
isMediaKeyDetectionEnabled.addListener(() async {
if (!isMediaKeyDetectionEnabled.value) {
FlutterVolumeController.removeListener();
if (Platform.isWindows) {
_smtc?.disableSmtc();
} else {
mediaKeyDetector.setIsPlaying(isPlaying: false);
mediaKeyDetector.removeListener(_onMediaKeyDetectedListener);
}
} else {
FlutterVolumeController.addListener(
(volume) {
_lastVolume ??= volume;
if (volume != _lastVolume) {
final bool hasAction;
if (volume > _lastVolume!) {
hasAction = _onMediaKeyDetectedListener(MediaKey.volumeUp);
} else {
hasAction = _onMediaKeyDetectedListener(MediaKey.volumeDown);
}
if (hasAction) {
// revert volume
FlutterVolumeController.setVolume(_lastVolume!);
} else {
_lastVolume = volume;
}
}
},
);
if (Platform.isWindows) {
if (!_smtcInitialized) {
_smtcInitialized = true;
await SMTCWindows.initialize();
}
_smtc = SMTCWindows(
metadata: const MusicMetadata(
title: 'BikeControl Media Key Handler',
album: 'BikeControl',
albumArtist: 'BikeControl',
artist: 'BikeControl',
),
// Timeline info for the OS media player
timeline: const PlaybackTimeline(
startTimeMs: 0,
endTimeMs: 1000,
positionMs: 0,
minSeekTimeMs: 0,
maxSeekTimeMs: 1000,
),
config: const SMTCConfig(
fastForwardEnabled: true,
nextEnabled: true,
pauseEnabled: true,
playEnabled: true,
rewindEnabled: true,
prevEnabled: true,
stopEnabled: true,
),
);
_smtc!.buttonPressStream.listen(_onMediaKeyPressedListener);
} else {
mediaKeyDetector.addListener(_onMediaKeyDetectedListener);
mediaKeyDetector.setIsPlaying(isPlaying: true);
}
}
});
}
bool _onMediaKeyDetectedListener(MediaKey mediaKey) {
final hidDevice = HidDevice('HID Device');
var availableDevice = core.connection.controllerDevices.firstOrNullWhere(
(e) => e.toString() == hidDevice.toString(),
);
if (availableDevice == null) {
core.connection.addDevices([hidDevice]);
availableDevice = hidDevice;
}
final keyPressed = mediaKey.name;
final button = availableDevice.getOrAddButton(
keyPressed,
() => ControllerButton(keyPressed),
);
availableDevice.handleButtonsClickedWithoutLongPressSupport([button]);
return core.actionHandler.supportedApp?.keymap.getKeyPair(button)?.hasNoAction == false;
}
bool _onMediaKeyPressedListener(PressedButton mediaKey) {
return _onMediaKeyDetectedListener(switch (mediaKey) {
PressedButton.play => MediaKey.playPause,
PressedButton.pause => MediaKey.playPause,
PressedButton.next => MediaKey.fastForward,
PressedButton.previous => MediaKey.rewind,
PressedButton.stop => MediaKey.playPause,
PressedButton.fastForward => MediaKey.fastForward,
PressedButton.rewind => MediaKey.rewind,
PressedButton.record => throw UnimplementedError(),
PressedButton.channelUp => MediaKey.volumeUp,
PressedButton.channelDown => MediaKey.volumeDown,
});
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
@@ -114,6 +115,9 @@ class BluetoothConnectRequirement extends PlatformRequirement {
}
}
ReceivePort? _receivePort;
StreamSubscription? _sub;
class NotificationRequirement extends PlatformRequirement {
NotificationRequirement()
: super(
@@ -196,10 +200,11 @@ class NotificationRequirement extends PlatformRequirement {
}
static Future<void> setup() async {
print('NOTIFICATION SETUP');
await core.flutterLocalNotificationsPlugin.initialize(
InitializationSettings(
android: AndroidInitializationSettings(
'@mipmap/ic_launcher',
'@drawable/ic_notification',
),
iOS: DarwinInitializationSettings(
requestAlertPermission: false,
@@ -269,10 +274,17 @@ class NotificationRequirement extends PlatformRequirement {
),
);
final receivePort = ReceivePort();
IsolateNameServer.registerPortWithName(receivePort.sendPort, '_backgroundChannelKey');
final backgroundMessagePort = receivePort.asBroadcastStream();
backgroundMessagePort.listen((message) {
_receivePort = ReceivePort();
// If already registered, remove and re-register
IsolateNameServer.removePortNameMapping('_backgroundChannelKey');
final ok = IsolateNameServer.registerPortWithName(_receivePort!.sendPort, '_backgroundChannelKey');
if (!ok) {
// If this happens, something else re-registered immediately or youre in a weird state.
throw StateError('Failed to register port name');
}
final backgroundMessagePort = _receivePort!.asBroadcastStream();
_sub = backgroundMessagePort.listen((message) {
print('Background isolate received message: $message');
if (message == 'disconnect' || message == 'close') {
UniversalBle.onAvailabilityChange = null;
core.connection.disconnectAll();

View File

@@ -165,13 +165,13 @@ enum Target {
return switch (this) {
Target.thisDevice => false,
_ => supportedApp == null || supportedApp.supportsOpenBikeProtocol == false,
_ => supportedApp == null || supportedApp.supportsOpenBikeProtocol.isEmpty,
};
}
String getDescription(SupportedApp? app) {
final appName = app?.name ?? 'the Trainer app';
final preferredConnectionMethod = app?.supportsOpenBikeProtocol == true
final preferredConnectionMethod = app?.supportsOpenBikeProtocol.isNotEmpty == true
? AppLocalizations.current.openBikeControlConnection
: app is MyWhoosh
? AppLocalizations.current.myWhooshDirectConnection

View File

@@ -22,13 +22,20 @@ class ChangelogDialog extends StatelessWidget {
children: [
Text(context.i18n.whatsNew),
SizedBox(height: 4),
Text(context.i18n.version(entry.blocks.first.text)),
Text(context.i18n.version(entry.blocks.first.text)).small,
],
),
content: Container(
constraints: BoxConstraints(minWidth: 460, maxHeight: 500),
child: Scrollbar(
child: SingleChildScrollView(child: MarkdownWidget(markdown: latestVersion)),
child: SingleChildScrollView(
child: MarkdownWidget(
markdown: latestVersion,
theme: MarkdownThemeData(
textStyle: TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.primary),
),
),
),
),
),
actions: [

View File

@@ -3,6 +3,7 @@ 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';
@@ -25,7 +26,7 @@ class IAPStatusWidget extends StatefulWidget {
State<IAPStatusWidget> createState() => _IAPStatusWidgetState();
}
final _normalDate = DateTime(2026, 1, 15, 0, 0, 0, 0, 0);
final _normalDate = DateTime(2026, 2, 15, 0, 0, 0, 0, 0);
final _iapDate = DateTime(2025, 12, 21, 0, 0, 0, 0, 0);
enum AlreadyBoughtOption { fullPurchase, iap, no }
@@ -356,7 +357,8 @@ class _IAPStatusWidgetState extends State<IAPStatusWidget> {
],
),
),
] else if (_alreadyBoughtQuestion == AlreadyBoughtOption.no) ...[
] else if (_alreadyBoughtQuestion == AlreadyBoughtOption.no ||
DateTime.now().isAfter(_normalDate)) ...[
PrimaryButton(
onPressed: _isPurchasing ? null : () => _handlePurchase(context),
leading: Icon(Icons.star),
@@ -467,7 +469,7 @@ class _IAPStatusWidgetState extends State<IAPStatusWidget> {
} catch (e) {
if (mounted) {
buildToast(
context,
navigatorKey.currentContext!,
title: 'Error',
subtitle: 'An error occurred: $e',
);

View File

@@ -49,6 +49,7 @@ class _KeymapExplanationState extends State<KeymapExplanation> {
if (keyPair != null && !_isDrawerOpen) {
await _openKeyPairEditor(keyPair);
}
setState(() {});
}
});
}
@@ -78,7 +79,11 @@ class _KeymapExplanationState extends State<KeymapExplanation> {
.whereNot(
(keyPair) => keyPair.buttons.filter((b) => allAvailableButtons.contains(b)).isEmpty,
)
.sortedBy((k) => k.buttons.first.color != null ? 0 : 1);
.sortedBy(
(k) => k.buttons.first.color != null
? '0${(k.buttons.first.icon?.codePoint ?? 0 * -1)}'
: '1${(k.buttons.first.icon?.codePoint ?? 0 * -1)}',
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,

View File

@@ -33,7 +33,7 @@ class ButtonWidget extends StatelessWidget {
size: big && button.color != null ? null : 14,
)
: Text(
button.name.splitByUpperCase(),
button.displayName.splitByUpperCase(),
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: screenshotMode ? null : 'monospace',

View File

@@ -9,6 +9,7 @@
#include <bluetooth_low_energy_linux/bluetooth_low_energy_linux_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <flutter_volume_controller/flutter_volume_controller_plugin.h>
#include <gamepads_linux/gamepads_linux_plugin.h>
#include <gtk/gtk_plugin.h>
#include <media_key_detector_linux/media_key_detector_plugin.h>
@@ -27,6 +28,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_volume_controller_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterVolumeControllerPlugin");
flutter_volume_controller_plugin_register_with_registrar(flutter_volume_controller_registrar);
g_autoptr(FlPluginRegistrar) gamepads_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GamepadsLinuxPlugin");
gamepads_linux_plugin_register_with_registrar(gamepads_linux_registrar);

View File

@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
bluetooth_low_energy_linux
file_selector_linux
flutter_secure_storage_linux
flutter_volume_controller
gamepads_linux
gtk
media_key_detector_linux

View File

@@ -10,6 +10,7 @@ import device_info_plus
import file_selector_macos
import flutter_local_notifications
import flutter_secure_storage_darwin
import flutter_volume_controller
import gamepads_darwin
import in_app_purchase_storekit
import in_app_review
@@ -18,7 +19,6 @@ import keypress_simulator_macos
import media_key_detector_macos
import nsd_macos
import package_info_plus
import path_provider_foundation
import purchases_flutter
import screen_retriever_macos
import shared_preferences_foundation
@@ -33,6 +33,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
FlutterVolumeControllerPlugin.register(with: registry.registrar(forPlugin: "FlutterVolumeControllerPlugin"))
GamepadsDarwinPlugin.register(with: registry.registrar(forPlugin: "GamepadsDarwinPlugin"))
InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
@@ -41,7 +42,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
MediaKeyDetectorPlugin.register(with: registry.registrar(forPlugin: "MediaKeyDetectorPlugin"))
NsdMacosPlugin.register(with: registry.registrar(forPlugin: "NsdMacosPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
PurchasesFlutterPlugin.register(with: registry.registrar(forPlugin: "PurchasesFlutterPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View File

@@ -11,6 +11,8 @@ PODS:
- flutter_secure_storage_darwin (10.0.0):
- Flutter
- FlutterMacOS
- flutter_volume_controller (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- gamepads_darwin (0.1.1):
- FlutterMacOS
@@ -29,15 +31,12 @@ PODS:
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- purchases_flutter (9.10.6):
- FlutterMacOS
- purchases_flutter (9.10.2):
- FlutterMacOS
- PurchasesHybridCommon (= 17.25.0)
- PurchasesHybridCommon (17.25.0):
- RevenueCat (= 5.51.1)
- RevenueCat (5.51.1)
- PurchasesHybridCommon (= 17.27.1)
- PurchasesHybridCommon (17.27.1):
- RevenueCat (= 5.54.1)
- RevenueCat (5.54.1)
- screen_retriever_macos (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
@@ -59,6 +58,7 @@ DEPENDENCIES:
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- flutter_secure_storage_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
- 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`)
- in_app_purchase_storekit (from `Flutter/ephemeral/.symlinks/plugins/in_app_purchase_storekit/darwin`)
@@ -68,7 +68,6 @@ DEPENDENCIES:
- media_key_detector_macos (from `Flutter/ephemeral/.symlinks/plugins/media_key_detector_macos/macos`)
- nsd_macos (from `Flutter/ephemeral/.symlinks/plugins/nsd_macos/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- 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`)
@@ -93,6 +92,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
flutter_secure_storage_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_darwin/darwin
flutter_volume_controller:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_volume_controller/macos
FlutterMacOS:
:path: Flutter/ephemeral
gamepads_darwin:
@@ -111,8 +112,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/nsd_macos/macos
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
purchases_flutter:
:path: Flutter/ephemeral/.symlinks/plugins/purchases_flutter/macos
screen_retriever_macos:
@@ -129,11 +128,12 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
SPEC CHECKSUMS:
bluetooth_low_energy_darwin: 764d8d1ae5abefbcdb839e812b4b25c0061fcf8b
bluetooth_low_energy_darwin: 50bc79258e60586e4c4bed5948bd31d925f37fac
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
file_selector_macos: 3e56eaea051180007b900eacb006686fd54da150
flutter_local_notifications: 4ccab5b7a22835214a6672e3f9c5e8ae207dab36
flutter_secure_storage_darwin: 557817588b80e60213cbecb573c45c76b788018d
flutter_volume_controller: 25d09126b0d695560f11c80b1311d5063fed882f
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
gamepads_darwin: 07af6c60c282902b66574c800e20b2b26e68fda8
in_app_purchase_storekit: 2342c0a5da86593124d08dd13d920f39a52b273a
@@ -143,14 +143,13 @@ SPEC CHECKSUMS:
media_key_detector_macos: a93757a483b4b47283ade432b1af9e427c47329f
nsd_macos: 1a38a38a33adbb396b4c6f303bc076073514cadc
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
purchases_flutter: 777401787df16312c7b8b53b2d7144d26b6da0f0
PurchasesHybridCommon: 6a79a873ab52f777bfa36e9516f3fcd84d3b3428
RevenueCat: eab035bbab271faccfef5c36eaff2a1ffef14dc0
purchases_flutter: 55ed35144683d8673f0a4b5077e8f4d8291f291e
PurchasesHybridCommon: 027f03312519c51056457eb2e4f7ee1c91b61b8f
RevenueCat: ecbba580fa453b0d4a0475449b904196d74ef678
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
url_launcher_macos: 175a54c831f4375a6cf895875f716ee5af3888ce
wakelock_plus: 9d63063ffb7af1c215209769067c57103bde719d
window_manager: e25faf20d88283a0d46e7b1a759d07261ca27575

0
macos/Runner.xcodeproj/project.pbxproj Executable file → Normal file
View File

View File

@@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d
sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e"
url: "https://pub.dev"
source: hosted
version: "91.0.0"
version: "92.0.0"
accessibility:
dependency: "direct main"
description:
@@ -20,10 +20,10 @@ packages:
dependency: transitive
description:
name: analyzer
sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08
sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e"
url: "https://pub.dev"
source: hosted
version: "8.4.1"
version: "9.0.0"
animated_vector:
dependency: transitive
description:
@@ -84,50 +84,50 @@ packages:
dependency: "direct main"
description:
name: bluetooth_low_energy
sha256: "5dec5831412c7d82b77df878dd3e08a82132426d2fb4c5d7c98c9a8cd0ed79e0"
sha256: ac70d6f95c54df943b40d58b0be6cce1ed49ae8b76219d03386cb6c56e06ba5a
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "6.2.1"
bluetooth_low_energy_android:
dependency: transitive
description:
name: bluetooth_low_energy_android
sha256: "32c0f84f88770845e3189e04b0ddf4780dc8743fd7a8ade60b99b6cb414b8a89"
sha256: "1a1ad8aa3016b6ae843eeaa86c0f4eab7eda6669c06066a6da2494ffe6350853"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "6.2.1"
bluetooth_low_energy_darwin:
dependency: transitive
description:
name: bluetooth_low_energy_darwin
sha256: fbbe3be175cb54093884a84f6f0826d6e8a2a2e29dfeae9b367d5e8e9ee1db38
sha256: "63243add2f57ad201f09d42878a31bad6e5b93291c6ae3c36de24b5213178a0d"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "6.2.1"
bluetooth_low_energy_linux:
dependency: transitive
description:
name: bluetooth_low_energy_linux
sha256: a5c740f445dc8d2e940767fa94ed3bb24c32e77bc962a67ab23cb1f218180705
sha256: "95ae6d4f598595af61b73a32e1dc7a49d95081526c0c6b17205e852332845a5a"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "6.2.1"
bluetooth_low_energy_platform_interface:
dependency: transitive
description:
name: bluetooth_low_energy_platform_interface
sha256: dd76c0f8e31dcfb984059b03e73cb2998c29cffd17425f4ce946365b63abb3dc
sha256: "430a137fe0a662e2dd632390aa6e020a1c91335a5821ae07dacc9f319f9e590a"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "6.2.1"
bluetooth_low_energy_windows:
dependency: transitive
description:
name: bluetooth_low_energy_windows
sha256: "7a651259f7bc3ae2bb042c21e15e1e4f88acea57da1f69b3165f239124724791"
sha256: a235b4e368f8e45ad1712c8c1c9a138b4edb2ff98a2f1fbf3208fffeb9a27e3a
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "6.2.1"
bluez:
dependency: transitive
description:
@@ -176,6 +176,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
@@ -204,26 +212,26 @@ packages:
dependency: transitive
description:
name: country_flags
sha256: "714f2d415e74828eb08787d552a05e94cdf2cbe0607a5656f3e70087cd7bb7e0"
sha256: "457c545369eaddcaa47dc351ea97f00dbe2ee74ee7b487a805b86c10d1dfd880"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "4.1.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
version: "0.3.5+1"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.6"
version: "3.0.7"
dart_style:
dependency: transitive
description:
@@ -244,10 +252,10 @@ packages:
dependency: transitive
description:
name: data_widget
sha256: "95388df890189014f702b7e93f9de6bcf7d45143a99f6288f31899f10be441ba"
sha256: "4947aae3c50635496d56f94ad18de98e19015c5ebf01abee0f39a2c098c7021a"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
version: "0.0.3"
dbus:
dependency: transitive
description:
@@ -269,10 +277,10 @@ packages:
dependency: "direct main"
description:
name: device_info_plus
sha256: "49413c8ca514dea7633e8def233b25efdf83ec8522955cc2c0e3ad802927e7c6"
sha256: "4df8babf73058181227e18b08e6ea3520cf5fc5d796888d33b7cb0f33f984b7c"
url: "https://pub.dev"
source: hosted
version: "12.1.0"
version: "12.3.0"
device_info_plus_platform_interface:
dependency: transitive
description:
@@ -293,10 +301,10 @@ packages:
dependency: transitive
description:
name: equatable
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
version: "2.0.8"
expressions:
dependency: transitive
description:
@@ -317,10 +325,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.5"
file:
dependency: transitive
description:
@@ -333,34 +341,34 @@ packages:
dependency: transitive
description:
name: file_selector_linux
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
source: hosted
version: "0.9.3+2"
version: "0.9.4"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.4+4"
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.6.2"
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+4"
version: "0.9.3+5"
fixnum:
dependency: transitive
description:
@@ -428,18 +436,18 @@ packages:
dependency: "direct main"
description:
name: flutter_md
sha256: b5a67ae49135f7a76a0cc6f938ee3e8754e71d8448b97cf99c11512877f1d055
sha256: "3d726f06573397c66543a8ee229c86a263e8330768485a317f3ccd99c7794b6b"
url: "https://pub.dev"
source: hosted
version: "0.0.7"
version: "0.0.8"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
url: "https://pub.dev"
source: hosted
version: "2.0.30"
version: "2.0.33"
flutter_rust_bridge:
dependency: transitive
description:
@@ -501,6 +509,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_volume_controller:
dependency: "direct main"
description:
name: flutter_volume_controller
sha256: "22edb0993ad03ecbc8d1164daeb5b39d798d409625db692675a86889403b1532"
url: "https://pub.dev"
source: hosted
version: "1.3.4"
flutter_web_bluetooth:
dependency: transitive
description:
@@ -523,10 +539,10 @@ packages:
dependency: "direct main"
description:
name: gamepads
sha256: "5f8f1f31e241bce1c77e7bc5dcf5c03d9ea51a04ed775c8c299e788c512e0b54"
sha256: "3a8a35502f0b3d28ea55e2c6c42320583389c0514a2bdd92ba0440d0cadd628e"
url: "https://pub.dev"
source: hosted
version: "0.1.8+2"
version: "0.1.9"
gamepads_android:
dependency: transitive
description:
@@ -567,6 +583,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.2+1"
gamepads_web:
dependency: transitive
description:
name: gamepads_web
sha256: "4885a792f16de023c4975854ffe17ffccacb46beb1e78ac1ecde76fd10edcb22"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
gamepads_windows:
dependency: "direct overridden"
description:
@@ -588,10 +612,10 @@ packages:
dependency: transitive
description:
name: get_it
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
sha256: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9
url: "https://pub.dev"
source: hosted
version: "8.2.0"
version: "8.3.0"
glob:
dependency: transitive
description:
@@ -604,10 +628,10 @@ packages:
dependency: "direct dev"
description:
name: golden_screenshot
sha256: "4e56399d43000fad8db1992c0482cc192aa88a2bad76d4a16f09f1e0086781dd"
sha256: "048ae833c8a1c5b52d96484fd94a75ae28b185b93ba32cf3fd4cdedf69131018"
url: "https://pub.dev"
source: hosted
version: "8.1.1"
version: "8.2.0"
gsettings:
dependency: transitive
description:
@@ -624,14 +648,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
hooks:
dependency: transitive
description:
name: hooks
sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
http:
dependency: "direct main"
description:
name: http
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "1.6.0"
http_parser:
dependency: transitive
description:
@@ -640,54 +672,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
hybrid_logging:
dependency: transitive
description:
name: hybrid_logging
sha256: "54248d52ce68c14702a42fbc4083bac5c6be30f6afad8a41be4bbadd197b8af5"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
image:
dependency: transitive
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
version: "4.7.2"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041"
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.2.1"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "8dfe08ea7fcf7467dbaf6889e72eebd5e0d6711caae201fdac780eb45232cd02"
sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677"
url: "https://pub.dev"
source: hosted
version: "0.8.13+3"
version: "0.8.13+10"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.1"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad"
url: "https://pub.dev"
source: hosted
version: "0.8.13"
version: "0.8.13+3"
image_picker_linux:
dependency: transitive
description:
@@ -700,18 +724,18 @@ packages:
dependency: transitive
description:
name: image_picker_macos
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
version: "0.2.2+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665"
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
version: "2.11.1"
image_picker_windows:
dependency: transitive
description:
@@ -793,10 +817,10 @@ packages:
dependency: "direct dev"
description:
name: intl_utils
sha256: f597a982a5f2aab63ac47e893de47a7042aa89f2e82d96ed7f2f12f804669a56
sha256: fd84aea0e75b41ce36d40aa8dfc94a59fd63cb6582018f3c78c86163157a309f
url: "https://pub.dev"
source: hosted
version: "2.8.12"
version: "2.8.13"
ios_receipt:
dependency: "direct main"
description:
@@ -820,6 +844,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.28"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
js_interop:
dependency: transitive
description:
name: js_interop
sha256: "7ec859c296958ccea34dc770504bd3ff4ae52fdd9e7eeb2bacc7081ad476a1f5"
url: "https://pub.dev"
source: hosted
version: "0.0.1"
json_annotation:
dependency: transitive
description:
@@ -986,6 +1026,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.16.12"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
url: "https://pub.dev"
source: hosted
version: "0.17.4"
nested:
dependency: transitive
description:
@@ -998,18 +1046,18 @@ packages:
dependency: "direct main"
description:
name: nsd
sha256: cae71ee9c23ea7f75d4610efe7ff335b1f575fb93ef7b7f9a5c6183a091cbb74
sha256: "1611a5c9f61d56ff2973e1488ae04112103e5203b4a7a1fb594b48cfb366fc14"
url: "https://pub.dev"
source: hosted
version: "4.0.3"
version: "4.1.0"
nsd_android:
dependency: transitive
description:
name: nsd_android
sha256: "1309cd47d02c99bd305219f0a226644f9f4a964d341c3d6730cebb906bb1ec78"
sha256: "96d2d451c5db0319c37b1b2a38f2d55eb56ae54c0b0d3144c03c19c97436de4a"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.2.0"
nsd_ios:
dependency: transitive
description:
@@ -1030,10 +1078,10 @@ packages:
dependency: transitive
description:
name: nsd_platform_interface
sha256: "7220c8e0beeacd06c180fefcd6bb708415ed889c76f656e75ac633d09ceaa761"
sha256: b1a5ace6f01ea2ce37f373e52c3b7af4fd7c11de2582ddcc89f4fc00615d9dff
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.2.0"
nsd_windows:
dependency: transitive
description:
@@ -1042,6 +1090,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.1"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "9922a1ad59ac5afb154cc948aa6ded01987a75003651d0a2866afc23f4da624e"
url: "https://pub.dev"
source: hosted
version: "9.2.3"
package_config:
dependency: transitive
description:
@@ -1094,18 +1150,18 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
url: "https://pub.dev"
source: hosted
version: "2.2.20"
version: "2.2.22"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
@@ -1270,18 +1326,18 @@ packages:
dependency: "direct main"
description:
name: purchases_flutter
sha256: "0a8ce3855dacb8c28e1e8de99cfe5592be2cb350c1210259d6fa4d9d0b152f89"
sha256: "1a7132bac772d90e9b959364369beddfc7fca8a3d2909d6e4d818cef7fd64a8f"
url: "https://pub.dev"
source: hosted
version: "9.10.2"
version: "9.10.6"
purchases_ui_flutter:
dependency: "direct main"
description:
name: purchases_ui_flutter
sha256: cb9eac034536fa2c62beb0c4a9e921a09e505ee7fbe304370a6764044ed9aef2
sha256: "350550b6fe8cf91a8de9e613ed38829fcdd5e098f0ae1ae25d5b2d45c680c842"
url: "https://pub.dev"
source: hosted
version: "9.10.2"
version: "9.10.6"
quiver:
dependency: transitive
description:
@@ -1368,7 +1424,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: "3e49867dadd504ccf5c036adc967ef9229643111"
resolved-ref: "12e747e43b31b828c182a6d25acd1d599a10aca9"
url: "https://github.com/sunarya-thito/shadcn_flutter.git"
source: git
version: "0.0.48"
@@ -1376,26 +1432,26 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
version: "2.5.4"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "0b0f98d535319cb5cdd4f65783c2a54ee6d417a2f093dbb18be3e36e4c3d181f"
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
url: "https://pub.dev"
source: hosted
version: "2.4.14"
version: "2.4.18"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
@@ -1440,10 +1496,10 @@ packages:
dependency: transitive
description:
name: skeletonizer
sha256: eebc03dc86b298e2d7f61e0ebce5713e9dbbc3e786f825909b4591756f196eb6
sha256: "83157d8e2e41f0252079cfec496281c16e4c63660052dab8d4cd72a206bb7109"
url: "https://pub.dev"
source: hosted
version: "2.1.0+1"
version: "2.1.2"
sky_engine:
dependency: transitive
description: flutter
@@ -1517,10 +1573,10 @@ packages:
dependency: transitive
description:
name: time
sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461"
sha256: "46187cf30bffdab28c56be9a63861b36e4ab7347bf403297595d6a97e10c789f"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.1.6"
timezone:
dependency: transitive
description:
@@ -1566,34 +1622,34 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: c0fb544b9ac7efa10254efaf00a951615c362d1ea1877472f8f6c0fa00fcf15b
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
url: "https://pub.dev"
source: hosted
version: "6.3.23"
version: "6.3.28"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
url: "https://pub.dev"
source: hosted
version: "6.3.4"
version: "6.3.6"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.3"
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
@@ -1606,18 +1662,18 @@ packages:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.2"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
version: "3.1.5"
uuid:
dependency: transitive
description:
@@ -1670,10 +1726,10 @@ packages:
dependency: transitive
description:
name: watcher
sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a"
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.1.4"
version: "1.2.1"
web:
dependency: transitive
description:
@@ -1749,10 +1805,10 @@ packages:
dependency: transitive
description:
name: yaru
sha256: "704e10633b173d8f5308677d0879d31339204c5ae063aa904e46d8182e1cf191"
sha256: "67ac8c3dc52a5d69c049056d5fa40b909973e10b36df3cffeb666de867532d79"
url: "https://pub.dev"
source: hosted
version: "9.0.0"
version: "8.3.0"
yaru_window:
dependency: transitive
description:
@@ -1794,5 +1850,5 @@ packages:
source: hosted
version: "0.0.4"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0"
dart: ">=3.10.3 <4.0.0"
flutter: ">=3.38.4"

View File

@@ -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.4.0+77
version: 4.5.0+83
environment:
sdk: ^3.9.0
@@ -21,6 +21,7 @@ dependencies:
path_provider: ^2.1.5
smtc_windows: ^1.1.0
flutter_volume_controller: ^1.3.4
intl: any
version: ^3.0.0
bluetooth_low_energy: ^6.1.0

View File

@@ -9,6 +9,7 @@
#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>
#include <flutter_volume_controller/flutter_volume_controller_plugin_c_api.h>
#include <gamepads_windows/gamepads_windows_plugin_c_api.h>
#include <keypress_simulator_windows/keypress_simulator_windows_plugin_c_api.h>
#include <media_key_detector_windows/media_key_detector_windows.h>
@@ -27,6 +28,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
FlutterVolumeControllerPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterVolumeControllerPluginCApi"));
GamepadsWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GamepadsWindowsPluginCApi"));
KeypressSimulatorWindowsPluginCApiRegisterWithRegistrar(

View File

@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
bluetooth_low_energy_windows
file_selector_windows
flutter_secure_storage_windows
flutter_volume_controller
gamepads_windows
keypress_simulator_windows
media_key_detector_windows