mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-17 16:07:41 +01:00
Merge remote-tracking branch 'origin/copilot/ensure-ble-hid-support-windows' into feature/openbikecontrol
# Conflicts: # .gitignore
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -8,4 +8,10 @@ enum MediaKey {
|
||||
|
||||
/// The fast-forward media button
|
||||
fastForward,
|
||||
|
||||
/// The volume up button
|
||||
volumeUp,
|
||||
|
||||
/// The volume down button
|
||||
volumeDown,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# 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
|
||||
- Added support for volume up and volume down hotkeys
|
||||
|
||||
# 0.0.1
|
||||
|
||||
|
||||
@@ -4,11 +4,60 @@
|
||||
|
||||
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, 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
|
||||
|
||||
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;
|
||||
case MediaKey.volumeUp:
|
||||
// Handle volume up
|
||||
break;
|
||||
case MediaKey.volumeDown:
|
||||
// Handle volume down
|
||||
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
|
||||
|
||||
@@ -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<bool> getIsPlaying() async {
|
||||
return _isPlaying;
|
||||
final isPlaying = await methodChannel.invokeMethod<bool>('getIsPlaying');
|
||||
return isPlaying ?? _isPlaying;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setIsPlaying({required bool isPlaying}) async {
|
||||
_isPlaying = isPlaying;
|
||||
await methodChannel.invokeMethod<void>('setIsPlaying', <String, dynamic>{'isPlaying': isPlaying});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,29 @@
|
||||
#include <flutter/method_channel.h>
|
||||
#include <flutter/plugin_registrar_windows.h>
|
||||
#include <flutter/standard_method_codec.h>
|
||||
#include <flutter/event_channel.h>
|
||||
#include <flutter/event_stream_handler_functions.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
|
||||
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;
|
||||
constexpr int HOTKEY_VOLUME_UP = 4;
|
||||
constexpr int HOTKEY_VOLUME_DOWN = 5;
|
||||
|
||||
class MediaKeyDetectorWindows : public flutter::Plugin {
|
||||
public:
|
||||
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
|
||||
|
||||
MediaKeyDetectorWindows();
|
||||
MediaKeyDetectorWindows(flutter::PluginRegistrarWindows *registrar);
|
||||
|
||||
virtual ~MediaKeyDetectorWindows();
|
||||
|
||||
@@ -27,6 +37,21 @@ class MediaKeyDetectorWindows : public flutter::Plugin {
|
||||
void HandleMethodCall(
|
||||
const flutter::MethodCall<flutter::EncodableValue> &method_call,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
|
||||
|
||||
// Register global hotkeys for media keys
|
||||
void RegisterHotkeys();
|
||||
|
||||
// Unregister global hotkeys
|
||||
void UnregisterHotkeys();
|
||||
|
||||
// Handle Windows messages
|
||||
std::optional<LRESULT> HandleWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
|
||||
|
||||
flutter::PluginRegistrarWindows *registrar_;
|
||||
std::unique_ptr<flutter::EventSink<>> event_sink_;
|
||||
std::atomic<bool> is_playing_{false};
|
||||
int window_proc_id_ = -1;
|
||||
bool hotkeys_registered_ = false;
|
||||
};
|
||||
|
||||
// static
|
||||
@@ -37,31 +62,163 @@ void MediaKeyDetectorWindows::RegisterWithRegistrar(
|
||||
registrar->messenger(), "media_key_detector_windows",
|
||||
&flutter::StandardMethodCodec::GetInstance());
|
||||
|
||||
auto plugin = std::make_unique<MediaKeyDetectorWindows>();
|
||||
auto plugin = std::make_unique<MediaKeyDetectorWindows>(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<flutter::EventChannel<flutter::EncodableValue>>(
|
||||
registrar->messenger(), "media_key_detector_windows_events",
|
||||
&flutter::StandardMethodCodec::GetInstance());
|
||||
|
||||
auto event_handler = std::make_unique<flutter::StreamHandlerFunctions<>>(
|
||||
[plugin_pointer = plugin.get()](
|
||||
const flutter::EncodableValue* arguments,
|
||||
std::unique_ptr<flutter::EventSink<>>&& events)
|
||||
-> std::unique_ptr<flutter::StreamHandlerError<>> {
|
||||
plugin_pointer->event_sink_ = std::move(events);
|
||||
return nullptr;
|
||||
},
|
||||
[plugin_pointer = plugin.get()](const flutter::EncodableValue* arguments)
|
||||
-> std::unique_ptr<flutter::StreamHandlerError<>> {
|
||||
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<flutter::EncodableValue> &method_call,
|
||||
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> 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<flutter::EncodableMap>(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<bool>(&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 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 && 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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
UnregisterHotKey(hwnd, HOTKEY_VOLUME_UP);
|
||||
UnregisterHotKey(hwnd, HOTKEY_VOLUME_DOWN);
|
||||
|
||||
hotkeys_registered_ = false;
|
||||
}
|
||||
|
||||
std::optional<LRESULT> 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;
|
||||
case HOTKEY_VOLUME_UP:
|
||||
key_index = 3; // MediaKey.volumeUp
|
||||
break;
|
||||
case HOTKEY_VOLUME_DOWN:
|
||||
key_index = 4; // MediaKey.volumeDown
|
||||
break;
|
||||
}
|
||||
|
||||
if (key_index >= 0) {
|
||||
event_sink_->Success(EncodableValue(key_index));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void MediaKeyDetectorWindowsRegisterWithRegistrar(
|
||||
|
||||
Reference in New Issue
Block a user