From a5f9b42e6f116d136890de7b77f985be06fb2dbb Mon Sep 17 00:00:00 2001 From: Jonas Bark Date: Sat, 17 Jan 2026 12:11:14 +0100 Subject: [PATCH] Windows: fix media key detection --- CHANGELOG.md | 2 + _codeql_detected_source_root | 1 - lib/bluetooth/connection.dart | 4 + lib/utils/core.dart | 91 +------------------ lib/utils/media_key_handler.dart | 96 ++++++++++++++++++++ linux/flutter/generated_plugin_registrant.cc | 8 ++ linux/flutter/generated_plugins.cmake | 2 + pubspec.yaml | 2 +- 8 files changed, 114 insertions(+), 92 deletions(-) delete mode 120000 _codeql_detected_source_root create mode 100644 lib/utils/media_key_handler.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index dd99efc..a1790b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,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) diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root deleted file mode 120000 index 945c9b4..0000000 --- a/_codeql_detected_source_root +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/lib/bluetooth/connection.dart b/lib/bluetooth/connection.dart index 16f8922..a7ad993 100644 --- a/lib/bluetooth/connection.dart +++ b/lib/bluetooth/connection.dart @@ -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) { diff --git a/lib/utils/core.dart b/lib/utils/core.dart index fcbd2d7..5de0c25 100644 --- a/lib/utils/core.dart +++ b/lib/utils/core.dart @@ -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,20 @@ 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/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(); @@ -336,89 +333,3 @@ class CoreLogic { } } } - -class MediaKeyHandler { - final ValueNotifier 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 _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]); - } -} diff --git a/lib/utils/media_key_handler.dart b/lib/utils/media_key_handler.dart new file mode 100644 index 0000000..fcca0cb --- /dev/null +++ b/lib/utils/media_key_handler.dart @@ -0,0 +1,96 @@ +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: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 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 _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]); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index a99c31e..a8f1fa2 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -10,10 +10,12 @@ #include #include #include +#include #include #include #include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) bluetooth_low_energy_linux_registrar = @@ -28,6 +30,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) gamepads_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "GamepadsLinuxPlugin"); gamepads_linux_plugin_register_with_registrar(gamepads_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) media_key_detector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKeyDetectorPlugin"); media_key_detector_plugin_register_with_registrar(media_key_detector_linux_registrar); @@ -40,4 +45,7 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); + g_autoptr(FlPluginRegistrar) yaru_window_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "YaruWindowLinuxPlugin"); + yaru_window_linux_plugin_register_with_registrar(yaru_window_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 4b3e5a6..2f884b8 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -7,10 +7,12 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux flutter_secure_storage_linux gamepads_linux + gtk media_key_detector_linux screen_retriever_linux url_launcher_linux window_manager + yaru_window_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/pubspec.yaml b/pubspec.yaml index 101a565..414349f 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: bike_control description: "BikeControl - Control your virtual riding" publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 4.4.0+78 +version: 4.4.1+79 environment: sdk: ^3.9.0