From 61ad7c2eefdea587d5b4b2c97c767ba76a0ffdb1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 07:49:27 +0000 Subject: [PATCH 2/6] Implement global media key detection for Windows using RegisterHotKey API Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com> --- .../lib/media_key_detector_windows.dart | 16 +- .../media_key_detector_windows_plugin.cpp | 149 +++++++++++++++++- 2 files changed, 157 insertions(+), 8 deletions(-) diff --git a/media_key_detector/media_key_detector_windows/lib/media_key_detector_windows.dart b/media_key_detector/media_key_detector_windows/lib/media_key_detector_windows.dart index 6d9b07d..e856823 100644 --- a/media_key_detector/media_key_detector_windows/lib/media_key_detector_windows.dart +++ b/media_key_detector/media_key_detector_windows/lib/media_key_detector_windows.dart @@ -5,6 +5,7 @@ import 'package:media_key_detector_platform_interface/media_key_detector_platfor /// The Windows implementation of [MediaKeyDetectorPlatform]. class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform { bool _isPlaying = false; + final _eventChannel = const EventChannel('media_key_detector_windows_events'); /// The method channel used to interact with the native platform. @visibleForTesting @@ -17,7 +18,16 @@ class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform { @override void initialize() { - ServicesBinding.instance.keyboard.addHandler(defaultHandler); + _eventChannel.receiveBroadcastStream().listen((event) { + final keyIdx = event as int; + MediaKey? key; + if (keyIdx > -1 && keyIdx < MediaKey.values.length) { + key = MediaKey.values[keyIdx]; + } + if (key != null) { + triggerListeners(key); + } + }); } @override @@ -27,11 +37,13 @@ class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform { @override Future getIsPlaying() async { - return _isPlaying; + final isPlaying = await methodChannel.invokeMethod('getIsPlaying'); + return isPlaying ?? _isPlaying; } @override Future setIsPlaying({required bool isPlaying}) async { _isPlaying = isPlaying; + await methodChannel.invokeMethod('setIsPlaying', {'isPlaying': isPlaying}); } } diff --git a/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp b/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp index f3587ac..d4d3cff 100644 --- a/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp +++ b/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp @@ -6,19 +6,27 @@ #include #include #include +#include +#include #include #include +#include namespace { using flutter::EncodableValue; +// Hotkey IDs for media keys +constexpr int HOTKEY_PLAY_PAUSE = 1; +constexpr int HOTKEY_NEXT_TRACK = 2; +constexpr int HOTKEY_PREV_TRACK = 3; + class MediaKeyDetectorWindows : public flutter::Plugin { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); - MediaKeyDetectorWindows(); + MediaKeyDetectorWindows(flutter::PluginRegistrarWindows *registrar); virtual ~MediaKeyDetectorWindows(); @@ -27,6 +35,21 @@ class MediaKeyDetectorWindows : public flutter::Plugin { void HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result); + + // Register global hotkeys for media keys + void RegisterHotkeys(); + + // Unregister global hotkeys + void UnregisterHotkeys(); + + // Handle Windows messages + std::optional HandleWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + + flutter::PluginRegistrarWindows *registrar_; + std::unique_ptr> event_sink_; + std::atomic is_playing_{false}; + int window_proc_id_ = -1; + bool hotkeys_registered_ = false; }; // static @@ -37,31 +60,145 @@ void MediaKeyDetectorWindows::RegisterWithRegistrar( registrar->messenger(), "media_key_detector_windows", &flutter::StandardMethodCodec::GetInstance()); - auto plugin = std::make_unique(); + auto plugin = std::make_unique(registrar); channel->SetMethodCallHandler( [plugin_pointer = plugin.get()](const auto &call, auto result) { plugin_pointer->HandleMethodCall(call, std::move(result)); }); + // Set up event channel for media key events + auto event_channel = + std::make_unique>( + registrar->messenger(), "media_key_detector_windows_events", + &flutter::StandardMethodCodec::GetInstance()); + + auto event_handler = std::make_unique>( + [plugin_pointer = plugin.get()]( + const flutter::EncodableValue* arguments, + std::unique_ptr>&& events) + -> std::unique_ptr> { + plugin_pointer->event_sink_ = std::move(events); + return nullptr; + }, + [plugin_pointer = plugin.get()](const flutter::EncodableValue* arguments) + -> std::unique_ptr> { + plugin_pointer->event_sink_ = nullptr; + return nullptr; + }); + + event_channel->SetStreamHandler(std::move(event_handler)); + registrar->AddPlugin(std::move(plugin)); } -MediaKeyDetectorWindows::MediaKeyDetectorWindows() {} +MediaKeyDetectorWindows::MediaKeyDetectorWindows(flutter::PluginRegistrarWindows *registrar) + : registrar_(registrar) { + // Register a window procedure to handle hotkey messages + window_proc_id_ = registrar_->RegisterTopLevelWindowProcDelegate( + [this](HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + return HandleWindowProc(hwnd, message, wparam, lparam); + }); +} -MediaKeyDetectorWindows::~MediaKeyDetectorWindows() {} +MediaKeyDetectorWindows::~MediaKeyDetectorWindows() { + UnregisterHotkeys(); + if (window_proc_id_ != -1) { + registrar_->UnregisterTopLevelWindowProcDelegate(window_proc_id_); + } +} void MediaKeyDetectorWindows::HandleMethodCall( const flutter::MethodCall &method_call, std::unique_ptr> result) { if (method_call.method_name().compare("getPlatformName") == 0) { result->Success(EncodableValue("Windows")); - } - else { + } else if (method_call.method_name().compare("getIsPlaying") == 0) { + result->Success(EncodableValue(is_playing_.load())); + } else if (method_call.method_name().compare("setIsPlaying") == 0) { + const auto* arguments = std::get_if(method_call.arguments()); + if (arguments) { + auto is_playing_it = arguments->find(EncodableValue("isPlaying")); + if (is_playing_it != arguments->end()) { + if (auto* is_playing = std::get_if(&is_playing_it->second)) { + is_playing_.store(*is_playing); + if (*is_playing) { + RegisterHotkeys(); + } else { + UnregisterHotkeys(); + } + result->Success(); + return; + } + } + } + result->Error("INVALID_ARGUMENT", "isPlaying argument is required"); + } else { result->NotImplemented(); } } +void MediaKeyDetectorWindows::RegisterHotkeys() { + if (hotkeys_registered_) { + return; + } + + HWND hwnd = registrar_->GetView()->GetNativeWindow(); + + // Register global hotkeys for media keys + // MOD_NOREPEAT prevents the hotkey from repeating when held down + bool success = true; + success &= RegisterHotKey(hwnd, HOTKEY_PLAY_PAUSE, MOD_NOREPEAT, VK_MEDIA_PLAY_PAUSE); + success &= RegisterHotKey(hwnd, HOTKEY_NEXT_TRACK, MOD_NOREPEAT, VK_MEDIA_NEXT_TRACK); + success &= RegisterHotKey(hwnd, HOTKEY_PREV_TRACK, MOD_NOREPEAT, VK_MEDIA_PREV_TRACK); + + if (success) { + hotkeys_registered_ = true; + } +} + +void MediaKeyDetectorWindows::UnregisterHotkeys() { + if (!hotkeys_registered_) { + return; + } + + HWND hwnd = registrar_->GetView()->GetNativeWindow(); + + UnregisterHotKey(hwnd, HOTKEY_PLAY_PAUSE); + UnregisterHotKey(hwnd, HOTKEY_NEXT_TRACK); + UnregisterHotKey(hwnd, HOTKEY_PREV_TRACK); + + hotkeys_registered_ = false; +} + +std::optional MediaKeyDetectorWindows::HandleWindowProc( + HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + if (message == WM_HOTKEY && event_sink_) { + int key_index = -1; + + // Map hotkey ID to media key index + switch (wparam) { + case HOTKEY_PLAY_PAUSE: + key_index = 0; // MediaKey.playPause + break; + case HOTKEY_PREV_TRACK: + key_index = 1; // MediaKey.rewind + break; + case HOTKEY_NEXT_TRACK: + key_index = 2; // MediaKey.fastForward + break; + } + + if (key_index >= 0) { + event_sink_->Success(EncodableValue(key_index)); + } + + return 0; + } + + return std::nullopt; +} + } // namespace void MediaKeyDetectorWindowsRegisterWithRegistrar( From c356242a8f79d749b3ee12393c849c08265bd3e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 07:52:01 +0000 Subject: [PATCH 3/6] Improve hotkey registration error handling Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com> --- _codeql_detected_source_root | 1 + .../media_key_detector_windows_plugin.cpp | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) create mode 120000 _codeql_detected_source_root diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 0000000..945c9b4 --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp b/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp index d4d3cff..32f7fcf 100644 --- a/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp +++ b/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp @@ -147,13 +147,19 @@ void MediaKeyDetectorWindows::RegisterHotkeys() { // Register global hotkeys for media keys // MOD_NOREPEAT prevents the hotkey from repeating when held down - bool success = true; - success &= RegisterHotKey(hwnd, HOTKEY_PLAY_PAUSE, MOD_NOREPEAT, VK_MEDIA_PLAY_PAUSE); - success &= RegisterHotKey(hwnd, HOTKEY_NEXT_TRACK, MOD_NOREPEAT, VK_MEDIA_NEXT_TRACK); - success &= RegisterHotKey(hwnd, HOTKEY_PREV_TRACK, MOD_NOREPEAT, VK_MEDIA_PREV_TRACK); + bool play_pause_ok = RegisterHotKey(hwnd, HOTKEY_PLAY_PAUSE, MOD_NOREPEAT, VK_MEDIA_PLAY_PAUSE); + bool next_ok = RegisterHotKey(hwnd, HOTKEY_NEXT_TRACK, MOD_NOREPEAT, VK_MEDIA_NEXT_TRACK); + bool prev_ok = RegisterHotKey(hwnd, HOTKEY_PREV_TRACK, MOD_NOREPEAT, VK_MEDIA_PREV_TRACK); - if (success) { + // If all registrations succeeded, mark as registered + // If any failed, unregister the successful ones to maintain consistent state + if (play_pause_ok && next_ok && prev_ok) { hotkeys_registered_ = true; + } else { + // Clean up any successful registrations + if (play_pause_ok) UnregisterHotKey(hwnd, HOTKEY_PLAY_PAUSE); + if (next_ok) UnregisterHotKey(hwnd, HOTKEY_NEXT_TRACK); + if (prev_ok) UnregisterHotKey(hwnd, HOTKEY_PREV_TRACK); } } From 82cb41c20772d56e68473304d65a63edcc88ec10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 07:52:26 +0000 Subject: [PATCH 4/6] Remove CodeQL artifact from repository Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com> --- .gitignore | 1 + _codeql_detected_source_root | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 120000 _codeql_detected_source_root diff --git a/.gitignore b/.gitignore index 4131ec0..3c9055f 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ app.*.map.json service-account.json .env +_codeql_detected_source_root 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 From 7ecce43a3d6656d37bdb1b913d84202740680296 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 07:53:16 +0000 Subject: [PATCH 5/6] Add documentation for Windows global media key detection Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com> --- .../media_key_detector_windows/CHANGELOG.md | 5 ++- .../media_key_detector_windows/README.md | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/media_key_detector/media_key_detector_windows/CHANGELOG.md b/media_key_detector/media_key_detector_windows/CHANGELOG.md index 589555b..baba36f 100644 --- a/media_key_detector/media_key_detector_windows/CHANGELOG.md +++ b/media_key_detector/media_key_detector_windows/CHANGELOG.md @@ -1,6 +1,9 @@ # 0.0.2 -- TBD +- Implement global media key detection using Windows RegisterHotKey API +- Add event channel support for media key events +- Media keys now work even when app is not focused +- Improved error handling for hotkey registration # 0.0.1 diff --git a/media_key_detector/media_key_detector_windows/README.md b/media_key_detector/media_key_detector_windows/README.md index f64f638..dd12ff3 100644 --- a/media_key_detector/media_key_detector_windows/README.md +++ b/media_key_detector/media_key_detector_windows/README.md @@ -4,11 +4,52 @@ The windows implementation of `media_key_detector`. +## Features + +This plugin provides global media key detection on Windows using the Windows `RegisterHotKey` API. This allows your application to respond to media keys (play/pause, next track, previous track) even when it's not the focused application. + +### Supported Media Keys + +- Play/Pause (VK_MEDIA_PLAY_PAUSE) +- Next Track (VK_MEDIA_NEXT_TRACK) +- Previous Track (VK_MEDIA_PREV_TRACK) + +### Implementation Details + +The plugin uses: +- `RegisterHotKey` Windows API for global hotkey registration +- Event channels for communicating media key events to Dart +- Window message handlers to process WM_HOTKEY messages + +Hotkeys are registered when `setIsPlaying(true)` is called and automatically unregistered when `setIsPlaying(false)` is called or when the plugin is destroyed. + ## Usage This package is [endorsed][endorsed_link], which means you can simply use `media_key_detector` normally. This package will be automatically included in your app when you do. +```dart +import 'package:media_key_detector/media_key_detector.dart'; + +// Enable media key detection +mediaKeyDetector.setIsPlaying(isPlaying: true); + +// Listen for media key events +mediaKeyDetector.addListener((MediaKey key) { + switch (key) { + case MediaKey.playPause: + // Handle play/pause + break; + case MediaKey.fastForward: + // Handle next track + break; + case MediaKey.rewind: + // Handle previous track + break; + } +}); +``` + [endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin [very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg [very_good_analysis_link]: https://pub.dev/packages/very_good_analysis From cc16a2c8ded36d79824eccb66570c6473bd112d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:16:21 +0000 Subject: [PATCH 6/6] Add volume up/down hotkey support for Windows media key detection Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com> --- .../media_key_detector_platform_interface.dart | 2 ++ .../lib/src/media_key.dart | 6 ++++++ .../media_key_detector_windows/CHANGELOG.md | 1 + .../media_key_detector_windows/README.md | 10 +++++++++- .../media_key_detector_windows_plugin.cpp | 16 +++++++++++++++- 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/media_key_detector/media_key_detector_platform_interface/lib/media_key_detector_platform_interface.dart b/media_key_detector/media_key_detector_platform_interface/lib/media_key_detector_platform_interface.dart index 30e4279..a6d5572 100644 --- a/media_key_detector/media_key_detector_platform_interface/lib/media_key_detector_platform_interface.dart +++ b/media_key_detector/media_key_detector_platform_interface/lib/media_key_detector_platform_interface.dart @@ -74,6 +74,8 @@ abstract class MediaKeyDetectorPlatform extends PlatformInterface { LogicalKeyboardKey.mediaPlay: MediaKey.playPause, LogicalKeyboardKey.mediaRewind: MediaKey.rewind, LogicalKeyboardKey.mediaFastForward: MediaKey.fastForward, + LogicalKeyboardKey.audioVolumeUp: MediaKey.volumeUp, + LogicalKeyboardKey.audioVolumeDown: MediaKey.volumeDown, }; /// The default handler to use if this platform doesn't need to implement any diff --git a/media_key_detector/media_key_detector_platform_interface/lib/src/media_key.dart b/media_key_detector/media_key_detector_platform_interface/lib/src/media_key.dart index 6baa44e..1a2d45e 100644 --- a/media_key_detector/media_key_detector_platform_interface/lib/src/media_key.dart +++ b/media_key_detector/media_key_detector_platform_interface/lib/src/media_key.dart @@ -8,4 +8,10 @@ enum MediaKey { /// The fast-forward media button fastForward, + + /// The volume up button + volumeUp, + + /// The volume down button + volumeDown, } diff --git a/media_key_detector/media_key_detector_windows/CHANGELOG.md b/media_key_detector/media_key_detector_windows/CHANGELOG.md index baba36f..406c80b 100644 --- a/media_key_detector/media_key_detector_windows/CHANGELOG.md +++ b/media_key_detector/media_key_detector_windows/CHANGELOG.md @@ -4,6 +4,7 @@ - Add event channel support for media key events - Media keys now work even when app is not focused - Improved error handling for hotkey registration +- Added support for volume up and volume down hotkeys # 0.0.1 diff --git a/media_key_detector/media_key_detector_windows/README.md b/media_key_detector/media_key_detector_windows/README.md index dd12ff3..0eed398 100644 --- a/media_key_detector/media_key_detector_windows/README.md +++ b/media_key_detector/media_key_detector_windows/README.md @@ -6,13 +6,15 @@ The windows implementation of `media_key_detector`. ## Features -This plugin provides global media key detection on Windows using the Windows `RegisterHotKey` API. This allows your application to respond to media keys (play/pause, next track, previous track) even when it's not the focused application. +This plugin provides global media key detection on Windows using the Windows `RegisterHotKey` API. This allows your application to respond to media keys (play/pause, next track, previous track, volume up, volume down) even when it's not the focused application. ### Supported Media Keys - Play/Pause (VK_MEDIA_PLAY_PAUSE) - Next Track (VK_MEDIA_NEXT_TRACK) - Previous Track (VK_MEDIA_PREV_TRACK) +- Volume Up (VK_VOLUME_UP) +- Volume Down (VK_VOLUME_DOWN) ### Implementation Details @@ -46,6 +48,12 @@ mediaKeyDetector.addListener((MediaKey key) { case MediaKey.rewind: // Handle previous track break; + case MediaKey.volumeUp: + // Handle volume up + break; + case MediaKey.volumeDown: + // Handle volume down + break; } }); ``` diff --git a/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp b/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp index 32f7fcf..a1e9938 100644 --- a/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp +++ b/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp @@ -21,6 +21,8 @@ using flutter::EncodableValue; constexpr int HOTKEY_PLAY_PAUSE = 1; constexpr int HOTKEY_NEXT_TRACK = 2; constexpr int HOTKEY_PREV_TRACK = 3; +constexpr int HOTKEY_VOLUME_UP = 4; +constexpr int HOTKEY_VOLUME_DOWN = 5; class MediaKeyDetectorWindows : public flutter::Plugin { public: @@ -150,16 +152,20 @@ void MediaKeyDetectorWindows::RegisterHotkeys() { bool play_pause_ok = RegisterHotKey(hwnd, HOTKEY_PLAY_PAUSE, MOD_NOREPEAT, VK_MEDIA_PLAY_PAUSE); bool next_ok = RegisterHotKey(hwnd, HOTKEY_NEXT_TRACK, MOD_NOREPEAT, VK_MEDIA_NEXT_TRACK); bool prev_ok = RegisterHotKey(hwnd, HOTKEY_PREV_TRACK, MOD_NOREPEAT, VK_MEDIA_PREV_TRACK); + bool vol_up_ok = RegisterHotKey(hwnd, HOTKEY_VOLUME_UP, MOD_NOREPEAT, VK_VOLUME_UP); + bool vol_down_ok = RegisterHotKey(hwnd, HOTKEY_VOLUME_DOWN, MOD_NOREPEAT, VK_VOLUME_DOWN); // If all registrations succeeded, mark as registered // If any failed, unregister the successful ones to maintain consistent state - if (play_pause_ok && next_ok && prev_ok) { + if (play_pause_ok && next_ok && prev_ok && vol_up_ok && vol_down_ok) { hotkeys_registered_ = true; } else { // Clean up any successful registrations if (play_pause_ok) UnregisterHotKey(hwnd, HOTKEY_PLAY_PAUSE); if (next_ok) UnregisterHotKey(hwnd, HOTKEY_NEXT_TRACK); if (prev_ok) UnregisterHotKey(hwnd, HOTKEY_PREV_TRACK); + if (vol_up_ok) UnregisterHotKey(hwnd, HOTKEY_VOLUME_UP); + if (vol_down_ok) UnregisterHotKey(hwnd, HOTKEY_VOLUME_DOWN); } } @@ -173,6 +179,8 @@ void MediaKeyDetectorWindows::UnregisterHotkeys() { UnregisterHotKey(hwnd, HOTKEY_PLAY_PAUSE); UnregisterHotKey(hwnd, HOTKEY_NEXT_TRACK); UnregisterHotKey(hwnd, HOTKEY_PREV_TRACK); + UnregisterHotKey(hwnd, HOTKEY_VOLUME_UP); + UnregisterHotKey(hwnd, HOTKEY_VOLUME_DOWN); hotkeys_registered_ = false; } @@ -193,6 +201,12 @@ std::optional MediaKeyDetectorWindows::HandleWindowProc( case HOTKEY_NEXT_TRACK: key_index = 2; // MediaKey.fastForward break; + case HOTKEY_VOLUME_UP: + key_index = 3; // MediaKey.volumeUp + break; + case HOTKEY_VOLUME_DOWN: + key_index = 4; // MediaKey.volumeDown + break; } if (key_index >= 0) {