Compare commits

...

35 Commits

Author SHA1 Message Date
Jonas Bark
6f5c6bf1d9 Merge remote-tracking branch 'origin/main' 2025-11-08 09:00:51 +01:00
Jonas Bark
8fc8f2dfda increase version 2025-11-08 09:00:42 +01:00
jonasbark
d36e031e87 Set release date for version 3.4.0
Updated the release date for version 3.4.0 in the changelog.
2025-11-08 08:55:56 +01:00
Jonas Bark
efac0af4b9 update changelog to include Rouvy keymap 2025-11-08 08:54:59 +01:00
Jonas Bark
d7e73524ad Merge branch 'feature/bluetoothmedia' 2025-11-08 08:49:23 +01:00
Jonas Bark
80998c955f media key detection on iOS and macOS 2025-11-08 08:49:06 +01:00
Jonas Bark
d824cb6207 integrate media_key_detector package, add iOS implementation #1 2025-11-07 21:55:07 +01:00
Jonas Bark
ab80d679e1 update Rouvy keymap 2025-11-07 20:53:44 +01:00
Jonas Bark
d4881faab1 code fix, update changelog and readme 2025-11-07 20:49:59 +01:00
Jonas Bark
7c74d61b43 Merge branch 'feature/mediabutton' 2025-11-07 20:45:30 +01:00
Jonas Bark
8ad2906a17 Merge branch 'feature/ble_hid' 2025-11-07 20:37:44 +01:00
Jonas Bark
0f4d19080a prefill mail text when support is used 2025-11-07 20:37:10 +01:00
Jonas Bark
a9a13be6ca fix modifier detection 2025-11-07 20:36:35 +01:00
Jonas Bark
c66badf39e Merge branch 'main' of github.com:jonasbark/swiftcontrol 2025-11-07 19:41:15 +01:00
jonasbark
6c2fc54612 Merge pull request #166 from jonasbark/copilot/add-keyboard-combination-support
Add modifier key support for keyboard mappings
2025-11-07 19:40:45 +01:00
jonasbark
807c0eaa98 Merge pull request #167 from jonasbark/copilot/support-elite-square-control
Fix Elite Square button detection substring mismatch
2025-11-07 19:29:28 +01:00
copilot-swe-agent[bot]
7d7b1e89e9 Final implementation of modifier key support
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-11-07 18:17:59 +00:00
copilot-swe-agent[bot]
cafb7408d9 Refactor: Consolidate modifier key detection logic
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-11-07 18:17:07 +00:00
copilot-swe-agent[bot]
723f741bca Improve test code quality and fix edge cases
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-11-07 18:15:27 +00:00
copilot-swe-agent[bot]
6a3cc0f8be Refactor: Extract helper methods to reduce code duplication
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-11-07 18:15:07 +00:00
copilot-swe-agent[bot]
66c548fa75 Refactor tests to reduce code duplication
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-11-07 18:13:21 +00:00
copilot-swe-agent[bot]
0b42f7e9c5 Fix Elite Square button detection logic and add tests
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-11-07 18:11:18 +00:00
copilot-swe-agent[bot]
35a995eddc Add support for modifier keys in keyboard mapping
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-11-07 18:10:14 +00:00
copilot-swe-agent[bot]
c3afb23625 Initial plan 2025-11-07 18:05:00 +00:00
copilot-swe-agent[bot]
f15d97585b Initial plan 2025-11-07 18:02:22 +00:00
Jonas Bark
5f03c072ff fix di2 initialization 2025-11-06 18:45:46 +01:00
jonasbark
ce94aea51a Merge pull request #165 from jonasbark/copilot/fix-di-fly-button-triggering
[WIP] Fix Shimano DI2 implementation for DI Fly buttons
2025-11-06 18:44:38 +01:00
copilot-swe-agent[bot]
a27ae070fc Fix DI2 button trigger on startup and add tests
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
2025-11-06 17:43:29 +00:00
copilot-swe-agent[bot]
7bbdc6a4e2 Initial plan 2025-11-06 17:38:51 +00:00
Jonas Bark
3188002ecb fix a few bugs and use some more clearer texts 2025-11-05 20:11:36 +01:00
Jonas Bark
820d0b37db allow to ignore HID device input 2025-11-04 10:05:52 +01:00
Jonas Bark
c18ac16208 support HID key events on Android #110 2025-11-04 09:53:52 +01:00
Jonas Bark
2bbc09bf13 Merge branch 'main' into feature/ble_hid 2025-11-04 08:59:02 +01:00
Jonas Bark
4498729e75 initial support for BLE HID device 2025-11-03 20:26:21 +01:00
Jonas Bark
69dd5c85ef detect media keys on macOS / iOS #1 2025-11-03 08:58:16 +01:00
179 changed files with 5917 additions and 230 deletions

View File

@@ -1,9 +1,13 @@
### 3.3.1 (unreleased)
### 3.4.0 (08-11-2025)
**New Features:**
- Support for Shimano Di2
- Support Keyboard shortcuts with modifier keys (Ctrl, Alt, Shift, ...)
- Support cheap BLE HID remotes
- add Keymap for Rouvy, supporting the new keyboard shortcuts for virtual shifting
**Fixes:**
- fix detection of Elite Square Sterzo devices
- fix detection of Elite Square Sterzo devices
- recognize cheap Bluetooth device clicks also when SwiftControl is in the background
### 3.3.0 (31-10-2025)

View File

@@ -1,8 +1,10 @@
**Instructions for using the MyWhoosh Link method**
**Instructions for using the MyWhoosh Direct Connect method**
1) launch MyWhoosh on the device of your choice
2) launch MyWhoosh Link, check if the "Link" connection works
3) close MyWhoosh Link, open SwiftControl, follow on screen instructions
Step 2 is not necessary after that once step 3 works
3) close MyWhoosh Link
4) open SwiftControl, follow on screen instructions
Once you've confirmed the connection in SwiftControl you won't have to repeat step 2 and 3 again in the future. This is just to make sure the connection works in general.
And here's a video with a few explanations:

View File

@@ -51,8 +51,9 @@ Check the compatibility matrix below!
- Elite Square Smart Frame (beta)
- Gamepads (beta)
- Cheap Bluetooth buttons such as [these](https://www.amazon.com/s?k=bluetooth+remote) (beta)
- BLE HID devices and classic Bluetooth HID devices are supported
- works on Android
- on iOS, macOS and Windows it would require SwiftControl to act as media player - let me know if that works for you
- on iOS and macOS requires SwiftControl to act as media player
Support for other devices can be added; check the issues tab here on GitHub.

View File

@@ -37,12 +37,13 @@ switch the setting to None, then back to Single-Tap and it should work again
## SwiftControl crashes on Windows when searching for the device
You're probably running into [this](https://github.com/jonasbark/swiftcontrol/issues/70) issue. Disconnect your controller device (e.g. Zwift Play) from Windows Bluetooth settings.
## Link requirement for MyWhoosh stuck at "Waiting for MyWhoosh"
## MyWhoosh Direct Connect never connects
The same network restrictions apply for SwiftControl as it applies to MyWhoosh Link app. Please verify with the MyWhoosh Link app if connection is possible at all.
Here are some instructions that can help:
[https://mywhoosh.com/troubleshoot/](https://mywhoosh.com/troubleshoot/)
[https://www.facebook.com/groups/mywhoosh/posts/1323791068858873/](https://www.facebook.com/groups/mywhoosh/posts/1323791068858873/)
[INSTRUCTIONS_IOS.md](INSTRUCTIONS_IOS.md)
In essence:
- your two devices (phone, tablet) need to be on the same WiFi network

View File

@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v25.2.0), do not edit directly.
// Autogenerated from Pigeon (v25.5.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -12,25 +12,57 @@ import io.flutter.plugin.common.StandardMethodCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private object AccessibilityApiPigeonUtils {
private fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
private fun wrapError(exception: Throwable): List<Any?> {
return if (exception is FlutterError) {
listOf(
exception.code,
exception.message,
exception.details
)
} else {
listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
fun wrapError(exception: Throwable): List<Any?> {
return if (exception is FlutterError) {
listOf(
exception.code,
exception.message,
exception.details
)
} else {
listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
}
}
fun deepEquals(a: Any?, b: Any?): Boolean {
if (a is ByteArray && b is ByteArray) {
return a.contentEquals(b)
}
if (a is IntArray && b is IntArray) {
return a.contentEquals(b)
}
if (a is LongArray && b is LongArray) {
return a.contentEquals(b)
}
if (a is DoubleArray && b is DoubleArray) {
return a.contentEquals(b)
}
if (a is Array<*> && b is Array<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
}
if (a is List<*> && b is List<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
}
if (a is Map<*, *> && b is Map<*, *>) {
return a.size == b.size && a.all {
(b as Map<Any?, Any?>).containsKey(it.key) &&
deepEquals(it.value, b[it.key])
}
}
return a == b
}
}
/**
@@ -93,12 +125,7 @@ data class WindowEvent (
if (this === other) {
return true
}
return packageName == other.packageName
&& top == other.top
&& bottom == other.bottom
&& right == other.right
&& left == other.left
}
return AccessibilityApiPigeonUtils.deepEquals(toList(), other.toList()) }
override fun hashCode(): Int = toList().hashCode()
}
@@ -141,6 +168,7 @@ interface Accessibility {
fun openPermissions()
fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean)
fun controlMedia(action: MediaAction)
fun ignoreHidDevices()
companion object {
/** The codec used by Accessibility. */
@@ -158,7 +186,7 @@ interface Accessibility {
val wrapped: List<Any?> = try {
listOf(api.hasPermission())
} catch (exception: Throwable) {
wrapError(exception)
AccessibilityApiPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
@@ -174,7 +202,7 @@ interface Accessibility {
api.openPermissions()
listOf(null)
} catch (exception: Throwable) {
wrapError(exception)
AccessibilityApiPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
@@ -195,7 +223,7 @@ interface Accessibility {
api.performTouch(xArg, yArg, isKeyDownArg, isKeyUpArg)
listOf(null)
} catch (exception: Throwable) {
wrapError(exception)
AccessibilityApiPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
@@ -213,7 +241,23 @@ interface Accessibility {
api.controlMedia(actionArg)
listOf(null)
} catch (exception: Throwable) {
wrapError(exception)
AccessibilityApiPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.accessibility.Accessibility.ignoreHidDevices$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.ignoreHidDevices()
listOf(null)
} catch (exception: Throwable) {
AccessibilityApiPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
@@ -274,3 +318,16 @@ abstract class StreamEventsStreamHandler : AccessibilityApiPigeonEventChannelWra
}
}
abstract class HidKeyPressedStreamHandler : AccessibilityApiPigeonEventChannelWrapper<String> {
companion object {
fun register(messenger: BinaryMessenger, streamHandler: HidKeyPressedStreamHandler, instanceName: String = "") {
var channelName: String = "dev.flutter.pigeon.accessibility.EventChannelMethods.hidKeyPressed"
if (instanceName.isNotEmpty()) {
channelName += ".$instanceName"
}
val internalStreamHandler = AccessibilityApiPigeonStreamHandler<String>(streamHandler)
EventChannel(messenger, channelName, AccessibilityApiPigeonMethodCodec).setStreamHandler(internalStreamHandler)
}
}
}

View File

@@ -1,6 +1,7 @@
package de.jonasbark.accessibility
import Accessibility
import HidKeyPressedStreamHandler
import MediaAction
import PigeonEventSink
import StreamEventsStreamHandler
@@ -10,6 +11,7 @@ import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.provider.Settings
import android.view.KeyEvent
import androidx.core.content.ContextCompat.startActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodChannel
@@ -23,17 +25,21 @@ class AccessibilityPlugin: FlutterPlugin, Accessibility {
/// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel
private lateinit var context: Context
private lateinit var eventHandler: EventListener
private lateinit var windowEventHandler: WindowEventListener
private lateinit var hidEventHandler: HidEventListener
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "accessibility")
eventHandler = EventListener()
windowEventHandler = WindowEventListener()
hidEventHandler = HidEventListener()
context = flutterPluginBinding.applicationContext
Accessibility.setUp(flutterPluginBinding.binaryMessenger, this)
StreamEventsStreamHandler.register(flutterPluginBinding.binaryMessenger, eventHandler)
Observable.fromService = eventHandler
StreamEventsStreamHandler.register(flutterPluginBinding.binaryMessenger, windowEventHandler)
HidKeyPressedStreamHandler.register(flutterPluginBinding.binaryMessenger, hidEventHandler)
Observable.fromServiceWindow = windowEventHandler
Observable.fromServiceKeys = hidEventHandler
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
@@ -59,12 +65,12 @@ class AccessibilityPlugin: FlutterPlugin, Accessibility {
val audioService = context.getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager
when (action) {
MediaAction.PLAY_PAUSE -> {
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
audioService.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
audioService.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
}
MediaAction.NEXT -> {
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_UP, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
audioService.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT))
audioService.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT))
}
MediaAction.VOLUME_DOWN -> {
audioService.adjustVolume(android.media.AudioManager.ADJUST_LOWER, android.media.AudioManager.FLAG_SHOW_UI)
@@ -75,16 +81,20 @@ class AccessibilityPlugin: FlutterPlugin, Accessibility {
}
}
override fun ignoreHidDevices() {
Observable.ignoreHidDevices = true
}
}
class EventListener : StreamEventsStreamHandler(), Receiver {
class WindowEventListener : StreamEventsStreamHandler(), Receiver {
private var eventSink: PigeonEventSink<WindowEvent>? = null
override fun onListen(p0: Any?, sink: PigeonEventSink<WindowEvent>) {
eventSink = sink
}
fun onEventsDone() {
override fun onCancel(p0: Any?) {
eventSink?.endOfStream()
eventSink = null
}
@@ -93,4 +103,27 @@ class EventListener : StreamEventsStreamHandler(), Receiver {
eventSink?.success(WindowEvent(packageName = packageName, right = window.right.toLong(), left = window.left.toLong(), bottom = window.bottom.toLong(), top = window.top.toLong()))
}
override fun onKeyEvent(event: KeyEvent) {
}
}
class HidEventListener : HidKeyPressedStreamHandler(), Receiver {
private var keyEventSink: PigeonEventSink<String>? = null
override fun onListen(p0: Any?, sink: PigeonEventSink<String>) {
keyEventSink = sink
}
override fun onChange(packageName: String, window: Rect) {
}
override fun onKeyEvent(event: KeyEvent) {
val keyString = KeyEvent.keyCodeToString(event.keyCode)
keyEventSink?.success(keyString)
}
}

View File

@@ -3,10 +3,14 @@ package de.jonasbark.accessibility
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.accessibilityservice.GestureDescription.StrokeDescription
import android.accessibilityservice.AccessibilityServiceInfo
import android.content.Context
import android.graphics.Path
import android.graphics.Rect
import android.media.AudioManager
import android.os.Build
import android.util.Log
import android.view.KeyEvent
import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
@@ -37,7 +41,7 @@ class AccessibilityService : AccessibilityService(), Listener {
}
val currentPackageName = event.packageName.toString()
val windowSize = getWindowSize()
Observable.fromService?.onChange(packageName = currentPackageName, window = windowSize)
Observable.fromServiceWindow?.onChange(packageName = currentPackageName, window = windowSize)
}
private fun getWindowSize(): Rect {
@@ -51,6 +55,38 @@ class AccessibilityService : AccessibilityService(), Listener {
Log.d("AccessibilityService", "Service Interrupted")
}
override fun onServiceConnected() {
super.onServiceConnected()
// Request key event filtering so we receive onKeyEvent for hardware/HID media keys
try {
val info = serviceInfo ?: AccessibilityServiceInfo()
info.flags = info.flags or AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS
// keep other capabilities as defined in XML
setServiceInfo(info)
} catch (e: Exception) {
Log.w("AccessibilityService", "Failed to set service info for key events: ${e.message}")
}
}
override fun onKeyEvent(event: KeyEvent): Boolean {
if (!Observable.ignoreHidDevices) {
// Handle media and volume keys from HID devices here
Log.d(
"AccessibilityService",
"onKeyEvent: keyCode=${event.keyCode} action=${event.action} scanCode=${event.scanCode} flags=${event.flags}"
)
// Forward key events to the plugin (Flutter) and swallow them so they don't propagate.
if (event.action == KeyEvent.ACTION_DOWN) {
Observable.fromServiceKeys?.onKeyEvent(event)
}
// Return true to indicate we've handled the event and it should be swallowed.
return true
} else {
return false
}
}
override fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean) {
val gestureBuilder = GestureDescription.Builder()
val path = Path()

View File

@@ -1,10 +1,13 @@
package de.jonasbark.accessibility
import android.graphics.Rect
import android.view.KeyEvent
object Observable {
var toService: Listener? = null
var fromService: Receiver? = null
var fromServiceWindow: Receiver? = null
var fromServiceKeys: Receiver? = null
var ignoreHidDevices: Boolean = false
}
interface Listener {
@@ -13,4 +16,5 @@ interface Listener {
interface Receiver {
fun onChange(packageName: String, window: Rect)
fun onKeyEvent(event: KeyEvent)
}

View File

@@ -9,6 +9,8 @@ abstract class Accessibility {
void performTouch(double x, double y, {bool isKeyDown = true, bool isKeyUp = false});
void controlMedia(MediaAction action);
void ignoreHidDevices();
}
enum MediaAction { playPause, next, volumeUp, volumeDown }
@@ -32,4 +34,5 @@ class WindowEvent {
@EventChannelApi()
abstract class EventChannelMethods {
WindowEvent streamEvents();
String hidKeyPressed();
}

View File

@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v25.2.0), do not edit directly.
// Autogenerated from Pigeon (v25.5.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
@@ -14,6 +14,20 @@ PlatformException _createConnectionError(String channelName) {
message: 'Unable to establish connection on channel: "$channelName".',
);
}
bool _deepEquals(Object? a, Object? b) {
if (a is List && b is List) {
return a.length == b.length &&
a.indexed
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
}
if (a is Map && b is Map) {
return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
(b as Map<Object?, Object?>).containsKey(entry.key) &&
_deepEquals(entry.value, b[entry.key]));
}
return a == b;
}
enum MediaAction {
playPause,
@@ -74,12 +88,7 @@ class WindowEvent {
if (identical(this, other)) {
return true;
}
return
packageName == other.packageName
&& top == other.top
&& bottom == other.bottom
&& right == other.right
&& left == other.left;
return _deepEquals(encode(), other.encode());
}
@override
@@ -232,6 +241,29 @@ class Accessibility {
return;
}
}
Future<void> ignoreHidDevices() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.ignoreHidDevices$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
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;
}
}
}
Stream<WindowEvent> streamEvents( {String instanceName = ''}) {
@@ -245,3 +277,14 @@ Stream<WindowEvent> streamEvents( {String instanceName = ''}) {
});
}
Stream<String> hidKeyPressed( {String instanceName = ''}) {
if (instanceName.isNotEmpty) {
instanceName = '.$instanceName';
}
final EventChannel hidKeyPressedChannel =
EventChannel('dev.flutter.pigeon.accessibility.EventChannelMethods.hidKeyPressed$instanceName', pigeonMethodCodec);
return hidKeyPressedChannel.receiveBroadcastStream().map((dynamic event) {
return event as String;
});
}

View File

@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged|typeViewClicked"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault"
android:accessibilityFlags="flagDefault|flagRequestFilterKeyEvents"
android:canRetrieveWindowContent="true"
android:canRequestFilterKeyEvents="true"
android:canPerformGestures="true"
android:notificationTimeout="100"/>

View File

@@ -11,6 +11,8 @@ PODS:
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- media_key_detector_ios (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
@@ -38,6 +40,7 @@ DEPENDENCIES:
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- gamepads_ios (from `.symlinks/plugins/gamepads_ios/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- media_key_detector_ios (from `.symlinks/plugins/media_key_detector_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`)
@@ -60,6 +63,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/gamepads_ios/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
media_key_detector_ios:
:path: ".symlinks/plugins/media_key_detector_ios/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
@@ -84,6 +89,7 @@ SPEC CHECKSUMS:
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
gamepads_ios: 1d2930c7a4450a9a1b57444ebf305a6a6cbeea0b
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
media_key_detector_ios: 7ff9aefdfea00bb7b71e184132381b7d0e7e1269
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2

View File

@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
@@ -14,13 +16,14 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-16" y="-40"/>
</scene>
</scenes>
</document>

View File

@@ -24,21 +24,22 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocalNetworkUsageDescription</key>
<string>This app connects to your trainer app on your local network.</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>SwiftControl uses Bluetooth to connect to accessories.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>This app connects to your trainer app on your local network.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-peripheral</string>
<string>bluetooth-central</string>
<string>audio</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>

View File

@@ -88,6 +88,38 @@ void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
}
}
// Helper function to send modifier key events
auto sendModifierKey = [](UINT vkCode, bool down) {
WORD sc = (WORD)MapVirtualKey(vkCode, MAPVK_VK_TO_VSC);
INPUT in = {0};
in.type = INPUT_KEYBOARD;
in.ki.wVk = 0;
in.ki.wScan = sc;
in.ki.dwFlags = KEYEVENTF_SCANCODE | (down ? 0 : KEYEVENTF_KEYUP);
SendInput(1, &in, sizeof(INPUT));
};
// Helper function to process modifiers
auto processModifiers = [&sendModifierKey](const std::vector<std::string>& mods, bool down) {
for (const std::string& modifier : mods) {
if (modifier == "shiftModifier") {
sendModifierKey(VK_SHIFT, down);
} else if (modifier == "controlModifier") {
sendModifierKey(VK_CONTROL, down);
} else if (modifier == "altModifier") {
sendModifierKey(VK_MENU, down);
} else if (modifier == "metaModifier") {
sendModifierKey(VK_LWIN, down);
}
}
};
// Press modifier keys first (if keyDown)
if (keyDown) {
processModifiers(modifiers, true);
}
// Send the main key
WORD sc = (WORD)MapVirtualKey(keyCode, MAPVK_VK_TO_VSC);
INPUT in = {0};
@@ -102,6 +134,11 @@ void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
}
SendInput(1, &in, sizeof(INPUT));
// Release modifier keys (if keyUp)
if (!keyDown) {
processModifiers(modifiers, false);
}
/*BYTE byteValue = static_cast<BYTE>(keyCode);
keybd_event(byteValue, 0x45, keyDown ? 0 : KEYEVENTF_KEYUP, 0);*/

View File

@@ -6,10 +6,13 @@ import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:gamepads/gamepads.dart';
import 'package:media_key_detector/media_key_detector.dart';
import 'package:swift_control/bluetooth/devices/bluetooth_device.dart';
import 'package:swift_control/bluetooth/devices/gamepad/gamepad_device.dart';
import 'package:swift_control/bluetooth/devices/hid/hid_device.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/android.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:swift_control/utils/keymap/keymap.dart';
import 'package:swift_control/utils/requirements/android.dart';
import 'package:universal_ble/universal_ble.dart';
@@ -25,8 +28,9 @@ class Connection {
List<BluetoothDevice> get bluetoothDevices => devices.whereType<BluetoothDevice>().toList();
List<GamepadDevice> get gamepadDevices => devices.whereType<GamepadDevice>().toList();
List<BaseDevice> get controllerDevices => [...bluetoothDevices, ...gamepadDevices];
List<BaseDevice> get remoteDevices => devices.whereNot((d) => d is BluetoothDevice || d is GamepadDevice).toList();
List<BaseDevice> get controllerDevices => [...bluetoothDevices, ...gamepadDevices, ...devices.whereType<HidDevice>()];
List<BaseDevice> get remoteDevices =>
devices.whereNot((d) => d is BluetoothDevice || d is GamepadDevice || d is HidDevice).toList();
var _androidNotificationsSetup = false;
@@ -44,10 +48,23 @@ class Connection {
final _lastScanResult = <BleDevice>[];
final ValueNotifier<bool> hasDevices = ValueNotifier(false);
final ValueNotifier<bool> isScanning = ValueNotifier(false);
final ValueNotifier<bool> isMediaKeyDetectionEnabled = ValueNotifier(false);
Timer? _gamePadSearchTimer;
final _dontAllowReconnectDevices = <String>{};
void initialize() {
isMediaKeyDetectionEnabled.addListener(() {
if (!isMediaKeyDetectionEnabled.value) {
mediaKeyDetector.setIsPlaying(isPlaying: false);
mediaKeyDetector.removeListener(_onMediaKeyDetectedListener);
} else {
mediaKeyDetector.addListener(_onMediaKeyDetectedListener);
mediaKeyDetector.setIsPlaying(isPlaying: true);
}
});
UniversalBle.onAvailabilityChange = (available) {
_actionStreams.add(BluetoothAvailabilityNotification(available == AvailabilityState.poweredOn));
if (available == AvailabilityState.poweredOn && !kIsWeb) {
@@ -77,7 +94,7 @@ class Connection {
if (scanResult != null) {
_actionStreams.add(LogNotification('Found new device: ${scanResult.runtimeType}'));
_addDevices([scanResult]);
addDevices([scanResult]);
} else {
final manufacturerData = result.manufacturerDataList;
final data = manufacturerData
@@ -124,7 +141,7 @@ class Connection {
).then((devices) async {
final baseDevices = devices.mapNotNull(BluetoothDevice.fromScanResult).toList();
if (baseDevices.isNotEmpty) {
_addDevices(baseDevices);
addDevices(baseDevices);
}
});
}
@@ -139,7 +156,7 @@ class Connection {
_gamePadSearchTimer = Timer.periodic(Duration(seconds: 3), (_) {
Gamepads.list().then((list) {
final pads = list.map((pad) => GamepadDevice(pad.name, id: pad.id)).toList();
_addDevices(pads);
addDevices(pads);
final removedDevices = gamepadDevices.where((device) => list.none((pad) => pad.id == device.id)).toList();
for (var device in removedDevices) {
@@ -155,7 +172,7 @@ class Connection {
Gamepads.list().then((list) {
final pads = list.map((pad) => GamepadDevice(pad.name, id: pad.id)).toList();
_addDevices(pads);
addDevices(pads);
});
}
@@ -187,8 +204,10 @@ class Connection {
);
}
void _addDevices(List<BaseDevice> dev) {
final newDevices = dev.where((device) => !devices.contains(device)).toList();
void addDevices(List<BaseDevice> dev) {
final newDevices = dev
.where((device) => !devices.contains(device) && !_dontAllowReconnectDevices.contains(device.name))
.toList();
devices.addAll(newDevices);
_connectionQueue.addAll(newDevices);
@@ -316,6 +335,9 @@ class Connection {
if (device is! LinkDevice) {
// keep it in the list to allow reconnect
devices.remove(device);
if (forget) {
_dontAllowReconnectDevices.add(device.name);
}
}
if (!forget && device is BluetoothDevice) {
_lastScanResult.removeWhere((b) => b.deviceId == device.device.deviceId);
@@ -326,4 +348,19 @@ class Connection {
}
signalChange(device);
}
void _onMediaKeyDetectedListener(MediaKey mediaKey) {
final hidDevice = HidDevice('HID Device');
final keyPressed = mediaKey.name;
final button = actionHandler.supportedApp!.keymap.getOrAddButton(keyPressed, () => ControllerButton(keyPressed));
var availableDevice = connection.controllerDevices.firstOrNullWhere((e) => e.name == hidDevice.name);
if (availableDevice == null) {
connection.addDevices([hidDevice]);
availableDevice = hidDevice;
}
availableDevice.handleButtonsClicked([button]);
availableDevice.handleButtonsClicked([]);
}
}

View File

@@ -1,12 +1,9 @@
import 'dart:async';
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/bluetooth/devices/zwift/constants.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/desktop.dart';
import 'package:swift_control/utils/keymap/apps/custom_app.dart';
import 'package:swift_control/utils/keymap/keymap.dart';
import '../../utils/keymap/buttons.dart';
import '../messages/notification.dart';
@@ -32,7 +29,7 @@ abstract class BaseDevice {
@override
String toString() {
return runtimeType.toString();
return name;
}
final StreamController<BaseNotification> actionStreamInternal = StreamController<BaseNotification>.broadcast();
@@ -129,27 +126,4 @@ abstract class BaseDevice {
}
Widget showInformation(BuildContext context);
ControllerButton? getOrAddButton(String name, ControllerButton Function() button) {
if (actionHandler.supportedApp is CustomApp) {
final allButtons = actionHandler.supportedApp!.keymap.keyPairs.expand((kp) => kp.buttons).toSet().toList();
if (allButtons.none((b) => b.name == name)) {
final newButton = button();
availableButtons.add(newButton);
actionHandler.supportedApp!.keymap.addKeyPair(
KeyPair(
touchPosition: Offset.zero,
buttons: [newButton],
physicalKey: null,
logicalKey: null,
isLongPress: false,
),
);
return newButton;
} else {
return allButtons.firstWhere((b) => b.name == name);
}
}
return null;
}
}

View File

@@ -122,7 +122,7 @@ abstract class BluetoothDevice extends BaseDevice {
@override
String toString() {
return runtimeType.toString();
return name + (firmwareVersion != null ? ' v$firmwareVersion' : '');
}
BleDevice get device => scanResult;

View File

@@ -40,11 +40,11 @@ class EliteSquare extends BluetoothDevice {
actionStreamInternal.add(LogNotification('Received $fullValue - vs $currentValue (last: $_lastValue)'));
if (_lastValue != null) {
final currentRelevantPart = fullValue.length >= 19
? fullValue.substring(6, fullValue.length - 13)
final currentRelevantPart = fullValue.length >= 14
? fullValue.substring(6, 14)
: fullValue.substring(6);
final lastRelevantPart = _lastValue!.length >= 19
? _lastValue!.substring(6, _lastValue!.length - 13)
final lastRelevantPart = _lastValue!.length >= 14
? _lastValue!.substring(6, 14)
: _lastValue!.substring(6);
if (currentRelevantPart != lastRelevantPart) {

View File

@@ -23,7 +23,10 @@ class GamepadDevice extends BaseDevice {
Gamepads.eventsByGamepad(id).listen((event) {
actionStreamInternal.add(LogNotification('Gamepad event: $event'));
ControllerButton? button = getOrAddButton(event.key, () => ControllerButton(event.key));
ControllerButton? button = actionHandler.supportedApp?.keymap.getOrAddButton(
event.key,
() => ControllerButton(event.key),
);
final buttonsClicked = event.value == 0.0 && button != null ? [button] : <ControllerButton>[];
if (_lastButtonsClicked.contentEquals(buttonsClicked) == false) {

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/android.dart';
class HidDevice extends BaseDevice {
HidDevice(super.name, {super.availableButtons = const []});
@override
Future<void> connect() {
return Future.value(null);
}
@override
Widget showInformation(BuildContext context) {
return Row(
children: [
Expanded(child: Text(name)),
PopupMenuButton(
itemBuilder: (c) => [
PopupMenuItem(
child: Text('Ignore'),
onTap: () {
connection.disconnect(this, forget: true);
if (actionHandler is AndroidActions) {
(actionHandler as AndroidActions).ignoreHidDevices();
} else if (connection.isMediaKeyDetectionEnabled.value) {
connection.isMediaKeyDetectionEnabled.value = false;
}
},
),
],
),
],
);
}
}

View File

@@ -51,32 +51,34 @@ class WhooshLink {
}
// Accept connection
_server!.listen((Socket socket) {
_socket = socket;
onConnected(socket);
isConnected.value = true;
if (kDebugMode) {
print('Client connected: ${socket.remoteAddress.address}:${socket.remotePort}');
}
_server!.listen(
(Socket socket) {
_socket = socket;
onConnected(socket);
isConnected.value = true;
if (kDebugMode) {
print('Client connected: ${socket.remoteAddress.address}:${socket.remotePort}');
}
// Listen for data from the client
socket.listen(
(List<int> data) {
try {
if (kDebugMode) {
// TODO we could check if virtual shifting is enabled
final message = utf8.decode(data);
print('Received message: $message');
}
} catch (_) {}
},
onDone: () {
print('Client disconnected: $socket');
onDisconnected(socket);
isConnected.value = false;
},
);
});
// Listen for data from the client
socket.listen(
(List<int> data) {
try {
if (kDebugMode) {
// TODO we could check if virtual shifting is enabled
final message = utf8.decode(data);
print('Received message: $message');
}
} catch (_) {}
},
onDone: () {
print('Client disconnected: $socket');
onDisconnected(socket);
isConnected.value = false;
},
);
},
);
}
String sendAction(InGameAction action, int? value) {

View File

@@ -8,7 +8,7 @@ import 'package:url_launcher/url_launcher_string.dart';
class LinkDevice extends BaseDevice {
String identifier;
LinkDevice(this.identifier) : super('MyWhoosh Link', availableButtons: []);
LinkDevice(this.identifier) : super('MyWhoosh Direct Connect', availableButtons: []);
@override
Future<void> connect() async {
@@ -30,8 +30,8 @@ class LinkDevice extends BaseDevice {
return StatefulBuilder(
builder: (context, setState) {
final myWhooshExplanation = actionHandler is RemoteActions
? 'MyWhoosh Link allows you to do some additional features such as Emotes and turn directions.'
: 'MyWhoosh Link is optional, but allows you to do some additional features such as Emotes and turn directions.';
? 'MyWhoosh Direct Connect allows you to do some additional features such as Emotes and turn directions.'
: 'MyWhoosh Direct Connect is optional, but allows you to do some additional features such as Emotes and turn directions.';
return Row(
children: [
Expanded(
@@ -48,7 +48,7 @@ class LinkDevice extends BaseDevice {
}
setState(() {});
},
title: Text('Enable MyWhoosh Link'),
title: Text('Enable MyWhoosh Direct Connect'),
subtitle: Row(
spacing: 12,
children: [

View File

@@ -32,20 +32,37 @@ class ShimanoDi2 extends BluetoothDevice {
}
final _lastButtons = <int, int>{};
bool _isInitialized = false;
@override
Future<void> processCharacteristic(String characteristic, Uint8List bytes) {
if (characteristic.toLowerCase() == ShimanoDi2Constants.D_FLY_CHANNEL_UUID) {
final channels = bytes.sublist(1);
// On first data reception, just initialize the state without triggering buttons
if (!_isInitialized) {
channels.forEachIndexed((int value, int index) {
final readableIndex = index + 1;
_lastButtons[index] = value;
actionHandler.supportedApp?.keymap.getOrAddButton(
'D-Fly Channel $readableIndex',
() => ControllerButton('D-Fly Channel $readableIndex'),
);
});
_isInitialized = true;
return Future.value();
}
final clickedButtons = <ControllerButton>[];
channels.forEachIndexed((int value, int index) {
final didChange = !_lastButtons.containsKey(index) || _lastButtons[index] != value;
final didChange = _lastButtons[index] != value;
_lastButtons[index] = value;
final readableIndex = index + 1;
final button = getOrAddButton(
final button = actionHandler.supportedApp?.keymap.getOrAddButton(
'D-Fly Channel $readableIndex',
() => ControllerButton('D-Fly Channel $readableIndex'),
);

View File

@@ -296,7 +296,7 @@ class ZwiftEmulator {
),
],
);
print('Starting advertising with HID service...');
print('Starting advertising with Zwift service...');
await peripheralManager.startAdvertising(advertisement);
_isAdvertising = true;

View File

@@ -1,6 +1,5 @@
import 'dart:io';
import 'package:accessibility/accessibility.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
@@ -18,7 +17,6 @@ import 'utils/actions/base_actions.dart';
final connection = Connection();
final navigatorKey = GlobalKey<NavigatorState>();
late BaseActions actionHandler;
final accessibilityHandler = Accessibility();
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final settings = Settings();
final whooshLink = WhooshLink();

View File

@@ -359,7 +359,6 @@ class _DevicePageState extends State<DevicePage> with WidgetsBindingObserver {
),
if (settings.getTrainerApp() is MyWhoosh &&
!whooshLink.isConnected.value &&
whooshLink.isCompatible(settings.getLastTarget()!))
LinkDevice('').showInformation(context),
if (settings.getTrainerApp()?.supportsZwiftEmulation == true)

View File

@@ -418,7 +418,10 @@ class KeypairExplanation extends StatelessWidget {
)
else if (keyPair.physicalKey != null && actionHandler.supportedModes.contains(SupportedMode.keyboard)) ...[
_KeyWidget(
label: keyPair.logicalKey?.keyLabel ?? 'Unknown',
label: [
...keyPair.modifiers.map((e) => e.name.replaceAll('Modifier', '')),
keyPair.logicalKey?.keyLabel ?? 'Unknown',
].joinToString(separator: '+'),
),
if (keyPair.isLongPress) Text('long\npress', style: TextStyle(fontSize: 10)),
] else ...[

View File

@@ -1,8 +1,11 @@
import 'package:accessibility/accessibility.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/bluetooth/devices/hid/hid_device.dart';
import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/base_actions.dart';
import 'package:swift_control/utils/keymap/apps/custom_app.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:swift_control/widgets/keymap_explanation.dart';
@@ -12,6 +15,8 @@ import '../single_line_exception.dart';
class AndroidActions extends BaseActions {
WindowEvent? windowInfo;
final accessibilityHandler = Accessibility();
AndroidActions({super.supportedModes = const [SupportedMode.touch, SupportedMode.media]});
@override
@@ -22,6 +27,21 @@ class AndroidActions extends BaseActions {
windowInfo = windowEvent;
}
});
hidKeyPressed().listen((keyPressed) {
if (supportedApp is CustomApp) {
final button = supportedApp.keymap.getOrAddButton(keyPressed, () => ControllerButton(keyPressed));
final hidDevice = HidDevice('HID Device');
var availableDevice = connection.controllerDevices.firstOrNullWhere((e) => e.name == hidDevice.name);
if (availableDevice == null) {
connection.addDevices([hidDevice]);
availableDevice = hidDevice;
}
availableDevice.handleButtonsClicked([button]);
availableDevice.handleButtonsClicked([]);
}
});
}
@override
@@ -66,4 +86,8 @@ class AndroidActions extends BaseActions {
}
return "No action assigned";
}
void ignoreHidDevices() {
accessibilityHandler.ignoreHidDevices();
}
}

View File

@@ -30,14 +30,14 @@ class DesktopActions extends BaseActions {
return zwiftEmulator.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue);
} else if (keyPair.physicalKey != null) {
if (isKeyDown && isKeyUp) {
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey);
await keyPressSimulator.simulateKeyUp(keyPair.physicalKey);
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey, keyPair.modifiers);
await keyPressSimulator.simulateKeyUp(keyPair.physicalKey, keyPair.modifiers);
return 'Key clicked: $keyPair';
} else if (isKeyDown) {
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey);
await keyPressSimulator.simulateKeyDown(keyPair.physicalKey, keyPair.modifiers);
return 'Key pressed: $keyPair';
} else {
await keyPressSimulator.simulateKeyUp(keyPair.physicalKey);
await keyPressSimulator.simulateKeyUp(keyPair.physicalKey, keyPair.modifiers);
return 'Key released: $keyPair';
}
} else {

View File

@@ -52,24 +52,33 @@ class CustomApp extends SupportedApp {
ControllerButton zwiftButton, {
required PhysicalKeyboardKey? physicalKey,
required LogicalKeyboardKey? logicalKey,
List<ModifierKey> modifiers = const [],
bool isLongPress = false,
Offset? touchPosition,
InGameAction? inGameAction,
int? inGameActionValue,
}) {
// set the key for the zwift button
final keyPair = keymap.getKeyPair(zwiftButton);
if (keyPair != null) {
keyPair.physicalKey = physicalKey;
keyPair.logicalKey = logicalKey;
keyPair.modifiers = modifiers;
keyPair.isLongPress = isLongPress;
keyPair.touchPosition = touchPosition ?? Offset.zero;
keyPair.inGameAction = inGameAction;
keyPair.inGameActionValue = inGameActionValue;
} else {
keymap.addKeyPair(
KeyPair(
buttons: [zwiftButton],
physicalKey: physicalKey,
logicalKey: logicalKey,
modifiers: modifiers,
isLongPress: isLongPress,
touchPosition: touchPosition ?? Offset.zero,
inGameAction: inGameAction,
inGameActionValue: inGameActionValue,
),
);
}

View File

@@ -1,4 +1,6 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/services.dart';
import 'package:swift_control/bluetooth/devices/zwift/constants.dart';
import 'package:swift_control/utils/keymap/apps/supported_app.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:swift_control/utils/requirements/multi.dart';
@@ -14,17 +16,25 @@ class Rouvy extends SupportedApp {
supportsZwiftEmulation: true,
keymap: Keymap(
keyPairs: [
// https://support.rouvy.com/hc/de/articles/32452137189393-Virtuelles-Schalten#h_01K5GMVG4KVYZ0Y6W7RBRZC9MA
KeyPair(
buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(),
inGameAction: InGameAction.shiftDown,
physicalKey: null,
logicalKey: null,
physicalKey: PhysicalKeyboardKey.numpadSubtract,
logicalKey: LogicalKeyboardKey.numpadSubtract,
),
KeyPair(
buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(),
inGameAction: InGameAction.shiftUp,
physicalKey: null,
logicalKey: null,
physicalKey: PhysicalKeyboardKey.numpadAdd,
logicalKey: LogicalKeyboardKey.numpadAdd,
),
// like escape
KeyPair(
buttons: [ZwiftButtons.b],
physicalKey: PhysicalKeyboardKey.keyB,
logicalKey: LogicalKeyboardKey.keyB,
inGameAction: InGameAction.back,
),
],
),

View File

@@ -60,12 +60,32 @@ class Keymap {
settings.setKeyMap(actionHandler.supportedApp!);
}
}
ControllerButton getOrAddButton(String name, ControllerButton Function() button) {
final allButtons = keyPairs.expand((kp) => kp.buttons).toSet().toList();
if (allButtons.none((b) => b.name == name)) {
final newButton = button();
addKeyPair(
KeyPair(
touchPosition: Offset.zero,
buttons: [newButton],
physicalKey: null,
logicalKey: null,
isLongPress: false,
),
);
return newButton;
} else {
return allButtons.firstWhere((b) => b.name == name);
}
}
}
class KeyPair {
final List<ControllerButton> buttons;
PhysicalKeyboardKey? physicalKey;
LogicalKeyboardKey? logicalKey;
List<ModifierKey> modifiers;
Offset touchPosition;
bool isLongPress;
InGameAction? inGameAction;
@@ -75,6 +95,7 @@ class KeyPair {
required this.buttons,
required this.physicalKey,
required this.logicalKey,
this.modifiers = const [],
this.touchPosition = Offset.zero,
this.isLongPress = false,
this.inGameAction,
@@ -109,7 +130,7 @@ class KeyPair {
@override
String toString() {
return logicalKey?.keyLabel ??
final baseKey = logicalKey?.keyLabel ??
switch (physicalKey) {
PhysicalKeyboardKey.mediaPlayPause => 'Play/Pause',
PhysicalKeyboardKey.mediaTrackNext => 'Next Track',
@@ -119,6 +140,24 @@ class KeyPair {
PhysicalKeyboardKey.audioVolumeDown => 'Volume Down',
_ => 'Not assigned',
};
if (modifiers.isEmpty || baseKey == 'Not assigned') {
return baseKey;
}
// Format modifiers + key (e.g., "Ctrl+Alt+R")
final modifierStrings = modifiers.map((m) {
return switch (m) {
ModifierKey.shiftModifier => 'Shift',
ModifierKey.controlModifier => 'Ctrl',
ModifierKey.altModifier => 'Alt',
ModifierKey.metaModifier => 'Meta',
ModifierKey.functionModifier => 'Fn',
_ => m.name,
};
}).toList();
return '${modifierStrings.join('+')}+$baseKey';
}
String encode() {
@@ -128,6 +167,7 @@ class KeyPair {
'actions': buttons.map((e) => e.name).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(),
if (touchPosition != Offset.zero) 'touchPosition': {'x': touchPosition.dx, 'y': touchPosition.dy},
'isLongPress': isLongPress,
'inGameAction': inGameAction?.name,
@@ -156,6 +196,15 @@ class KeyPair {
if (buttons.isEmpty) {
return null;
}
// Decode modifiers if present
final List<ModifierKey> modifiers = decoded.containsKey('modifiers')
? (decoded['modifiers'] as List)
.map<ModifierKey?>((e) => ModifierKey.values.firstOrNullWhere((element) => element.name == e))
.whereType<ModifierKey>()
.toList()
: [];
return KeyPair(
buttons: buttons,
logicalKey: decoded.containsKey('logicalKey') && int.parse(decoded['logicalKey']) != 0
@@ -164,6 +213,7 @@ class KeyPair {
physicalKey: decoded.containsKey('physicalKey') && int.parse(decoded['physicalKey']) != 0
? PhysicalKeyboardKey(int.parse(decoded['physicalKey']))
: null,
modifiers: modifiers,
touchPosition: touchPosition,
isLongPress: decoded['isLongPress'] ?? false,
inGameAction: decoded.containsKey('inGameAction')

View File

@@ -233,8 +233,8 @@ class KeymapManager {
);
}
Future<String?> duplicate(BuildContext context, String currentProfile) async {
final newName = await _showDuplicateProfileDialog(context, currentProfile);
Future<String?> duplicate(BuildContext context, String currentProfile, {String? skipName}) async {
final newName = skipName ?? await _showDuplicateProfileDialog(context, currentProfile);
if (newName != null && newName.isNotEmpty) {
if (actionHandler.supportedApp is CustomApp) {
await settings.duplicateCustomAppProfile(currentProfile, newName);
@@ -261,6 +261,8 @@ class KeymapManager {
logicalKey: pair.logicalKey,
isLongPress: pair.isLongPress,
touchPosition: pair.touchPosition,
inGameAction: pair.inGameAction,
inGameActionValue: pair.inGameActionValue,
);
});
});

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/android.dart';
import 'package:swift_control/utils/requirements/platform.dart';
import 'package:swift_control/widgets/accessibility_disclosure_dialog.dart';
import 'package:universal_ble/universal_ble.dart';
@@ -23,7 +24,7 @@ class AccessibilityRequirement extends PlatformRequirement {
@override
Future<void> getStatus() async {
status = await accessibilityHandler.hasPermission();
status = await (actionHandler as AndroidActions).accessibilityHandler.hasPermission();
}
Future<void> _showDisclosureDialog(BuildContext context, VoidCallback onUpdate) async {
@@ -35,7 +36,7 @@ class AccessibilityRequirement extends PlatformRequirement {
onAccept: () {
Navigator.of(context).pop();
// Open accessibility settings after user consents
accessibilityHandler.openPermissions().then((_) {
(actionHandler as AndroidActions).accessibilityHandler.openPermissions().then((_) {
onUpdate();
});
},

View File

@@ -136,13 +136,13 @@ enum Target {
'Due to platform restrictions only controlling ${app?.name ?? 'the Trainer app'} on other devices is supported.',
Target.thisDevice => 'Run ${app?.name ?? 'the Trainer app'} on this device.',
Target.iOS =>
'Run ${app?.name ?? 'the Trainer app'} on your Apple device and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Link method' : ''}.',
'Run ${app?.name ?? 'the Trainer app'} on your Apple device and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.',
Target.android =>
'Run ${app?.name ?? 'the Trainer app'} on your Android device and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Link method' : ''}.',
'Run ${app?.name ?? 'the Trainer app'} on your Android device and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.',
Target.macOS =>
'Run ${app?.name ?? 'the Trainer app'} on your Mac and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Link method' : ''}.',
'Run ${app?.name ?? 'the Trainer app'} on your Mac and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.',
Target.windows =>
'Run ${app?.name ?? 'the Trainer app'} on your Windows PC and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Link method' : ''}.',
'Run ${app?.name ?? 'the Trainer app'} on your Windows PC and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.',
};
}

View File

@@ -323,7 +323,7 @@ class _PairWidgetState extends State<_PairWidget> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Connect via MyWhoosh Link'),
Text('Connect via MyWhoosh Direct Connect'),
Text(
'Most reliable way to control MyWhoosh.',
style: TextStyle(fontSize: 12, color: Colors.black87, fontWeight: FontWeight.normal),

View File

@@ -25,6 +25,7 @@ class _HotKeyListenerState extends State<HotKeyListenerDialog> {
final FocusNode _focusNode = FocusNode();
KeyDownEvent? _pressedKey;
ControllerButton? _pressedButton;
final Set<ModifierKey> _activeModifiers = {};
@override
void initState() {
@@ -52,20 +53,85 @@ class _HotKeyListenerState extends State<HotKeyListenerDialog> {
void _onKey(KeyEvent event) {
setState(() {
// Track modifier keys
if (event is KeyDownEvent) {
_pressedKey = event;
widget.customApp.setKey(
_pressedButton!,
physicalKey: _pressedKey!.physicalKey,
logicalKey: _pressedKey!.logicalKey,
touchPosition: widget.keyPair?.touchPosition,
);
final wasModifier = _updateModifierState(event.logicalKey, add: true);
// Regular key pressed - record it along with active modifiers
if (!wasModifier) {
if (_pressedKey?.logicalKey != event.logicalKey) {}
_pressedKey = event;
widget.customApp.setKey(
_pressedButton!,
physicalKey: _pressedKey!.physicalKey,
logicalKey: _pressedKey!.logicalKey,
modifiers: _activeModifiers.toList(),
touchPosition: widget.keyPair?.touchPosition,
);
}
} else if (event is KeyUpEvent) {
// Clear modifier when released
_updateModifierState(event.logicalKey, add: false);
}
});
}
bool _updateModifierState(LogicalKeyboardKey key, {required bool add}) {
ModifierKey? modifier;
if (key == LogicalKeyboardKey.shift ||
key == LogicalKeyboardKey.shiftLeft ||
key == LogicalKeyboardKey.shiftRight) {
modifier = ModifierKey.shiftModifier;
} else if (key == LogicalKeyboardKey.control ||
key == LogicalKeyboardKey.controlLeft ||
key == LogicalKeyboardKey.controlRight) {
modifier = ModifierKey.controlModifier;
} else if (key == LogicalKeyboardKey.alt ||
key == LogicalKeyboardKey.altLeft ||
key == LogicalKeyboardKey.altRight) {
modifier = ModifierKey.altModifier;
} else if (key == LogicalKeyboardKey.meta ||
key == LogicalKeyboardKey.metaLeft ||
key == LogicalKeyboardKey.metaRight) {
modifier = ModifierKey.metaModifier;
} else if (key == LogicalKeyboardKey.fn) {
modifier = ModifierKey.functionModifier;
}
if (modifier != null) {
if (add) {
_activeModifiers.add(modifier);
} else {
_activeModifiers.remove(modifier);
}
return true;
}
return false;
}
String _formatModifierName(ModifierKey m) {
return switch (m) {
ModifierKey.shiftModifier => 'Shift',
ModifierKey.controlModifier => 'Ctrl',
ModifierKey.altModifier => 'Alt',
ModifierKey.metaModifier => 'Meta',
ModifierKey.functionModifier => 'Fn',
_ => m.name,
};
}
String _formatKey(KeyDownEvent? key) {
return key?.logicalKey.keyLabel ?? 'Waiting...';
if (key == null) {
return _activeModifiers.isEmpty ? 'Waiting...' : '${_activeModifiers.map(_formatModifierName).join('+')}+...';
}
if (_activeModifiers.isEmpty) {
return key.logicalKey.keyLabel;
}
final modifierStrings = _activeModifiers.map(_formatModifierName);
return '${modifierStrings.join('+')}+${key.logicalKey.keyLabel}';
}
@override

View File

@@ -8,7 +8,6 @@ import 'package:swift_control/main.dart';
import 'package:swift_control/pages/device.dart';
import 'package:swift_control/utils/actions/base_actions.dart';
import 'package:swift_control/utils/keymap/apps/custom_app.dart';
import 'package:swift_control/utils/keymap/apps/my_whoosh.dart';
import 'package:swift_control/utils/keymap/keymap.dart';
import 'package:swift_control/utils/keymap/manager.dart';
import 'package:swift_control/widgets/button_widget.dart';
@@ -145,7 +144,7 @@ class _ButtonEditor extends StatelessWidget {
@override
Widget build(BuildContext context) {
final actions = <PopupMenuEntry>[
if (settings.getMyWhooshLinkEnabled() && settings.getTrainerApp() is MyWhoosh)
if (settings.getMyWhooshLinkEnabled() && whooshLink.isCompatible(settings.getLastTarget()!))
PopupMenuItem<PhysicalKeyboardKey>(
child: PopupMenuButton(
itemBuilder: (_) => WhooshLink.supportedActions.map(
@@ -189,7 +188,7 @@ class _ButtonEditor extends StatelessWidget {
spacing: 14,
children: [
Icon(Icons.link),
Expanded(child: Text('MyWhoosh Link Action')),
Expanded(child: Text('MyWhoosh Direct Connect Action')),
Icon(Icons.arrow_right),
],
),
@@ -372,6 +371,7 @@ class _ButtonEditor extends StatelessWidget {
keyPair.isLongPress = false;
keyPair.physicalKey = null;
keyPair.logicalKey = null;
keyPair.modifiers = [];
keyPair.touchPosition = Offset.zero;
keyPair.inGameAction = null;
keyPair.inGameActionValue = null;
@@ -416,7 +416,16 @@ class _ButtonEditor extends StatelessWidget {
IconButton(
onPressed: () async {
final currentProfile = actionHandler.supportedApp!.name;
await KeymapManager().duplicate(context, currentProfile);
final newName = await KeymapManager().duplicate(
context,
currentProfile,
skipName: '$currentProfile (Copy)',
);
if (newName != null) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Created a new custom profile: $newName')));
}
onUpdate();
},
icon: Icon(Icons.edit),

View File

@@ -1,54 +0,0 @@
import 'package:flutter/material.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/pages/markdown.dart';
import 'package:swift_control/widgets/small_progress_indicator.dart';
class LinkWidget extends StatefulWidget {
final VoidCallback onUpdate;
const LinkWidget({super.key, required this.onUpdate});
@override
State<LinkWidget> createState() => _LinkWidgetState();
}
class _LinkWidgetState extends State<LinkWidget> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
ValueListenableBuilder(
valueListenable: whooshLink.isStarted,
builder: (BuildContext context, value, Widget? child) {
return Row(
spacing: 8,
children: [
ElevatedButton(
onPressed: value ? null : () async {},
child: Text(value ? 'Waiting for MyWhoosh...' : 'Start Listening for MyWhoosh'),
),
if (value) SmallProgressIndicator(),
],
);
},
),
Text('Verify with the MyWhoosh Link app if connection is possible.'),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md')),
);
},
child: const Text("Show Troubleshooting Guide"),
),
],
);
}
}

View File

@@ -8,6 +8,7 @@ import 'package:swift_control/main.dart';
import 'package:swift_control/pages/markdown.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:swift_control/widgets/title.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../pages/device.dart';
@@ -91,7 +92,23 @@ List<Widget> buildMenuButtons() {
onTap: () {
final isFromStore = (Platform.isAndroid ? isFromPlayStore == true : Platform.isIOS);
final suffix = isFromStore ? '' : 'ler';
launchUrlString('mailto:jonas.t.bark+swiftcontrol$suffix@gmail.com');
String email = Uri.encodeComponent('jonas.t.bark+swiftcontrol$suffix@gmail.com');
String subject = Uri.encodeComponent("Help requested for SwiftControl v${packageInfoValue?.version}");
String body = Uri.encodeComponent("""
---
App Version: ${packageInfoValue?.version}${shorebirdPatch?.number != null ? '+${shorebirdPatch!.number}' : ''}
Platform: ${Platform.operatingSystem} ${Platform.operatingSystemVersion}
Target: ${settings.getLastTarget()?.title}
Trainer App: ${settings.getTrainerApp()?.name}
Connected Controllers: ${connection.devices.map((e) => e.toString()).join(', ')}
Please don't remove this information, it helps me to assist you better.""");
Uri mail = Uri.parse("mailto:$email?subject=$subject&body=$body");
launchUrl(mail);
},
),
];

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/main.dart';
@@ -57,6 +59,28 @@ class _ScanWidgetState extends State<ScanWidget> {
Text(
'Scanning for devices... Make sure they are powered on and in range and not connected to another device.',
),
if (!kIsWeb && (Platform.isMacOS || Platform.isIOS || Platform.isWindows))
ValueListenableBuilder(
valueListenable: connection.isMediaKeyDetectionEnabled,
builder: (context, value, child) {
return SwitchListTile.adaptive(
value: value,
contentPadding: EdgeInsets.zero,
dense: true,
subtitle: Text(
'Enable this option to allow Swift Control to detect bluetooth remotes. In order to do so SwiftControl needs to act as a media player.',
),
title: Row(
children: [
const Text("Enable Media Key Detection"),
],
),
onChanged: (change) {
connection.isMediaKeyDetectionEnabled.value = change;
},
);
},
),
TextButton(
onPressed: () {
Navigator.push(

View File

@@ -15,6 +15,7 @@ import 'package:version/version.dart';
PackageInfo? packageInfoValue;
bool? isFromPlayStore;
Patch? shorebirdPatch;
class AppTitle extends StatefulWidget {
const AppTitle({super.key});
@@ -25,7 +26,6 @@ class AppTitle extends StatefulWidget {
class _AppTitleState extends State<AppTitle> {
final updater = ShorebirdUpdater();
Patch? _shorebirdPatch;
@override
void initState() {
@@ -34,7 +34,7 @@ class _AppTitleState extends State<AppTitle> {
if (updater.isAvailable) {
updater.readCurrentPatch().then((patch) {
setState(() {
_shorebirdPatch = patch;
shorebirdPatch = patch;
});
});
}
@@ -145,7 +145,7 @@ class _AppTitleState extends State<AppTitle> {
Text('SwiftControl', style: TextStyle(fontWeight: FontWeight.bold)),
if (packageInfoValue != null)
Text(
'v${packageInfoValue!.version}${_shorebirdPatch != null ? '+${_shorebirdPatch!.number}' : ''}${kIsWeb || (Platform.isAndroid && isFromPlayStore == false) ? ' (sideloaded)' : ''}',
'v${packageInfoValue!.version}${shorebirdPatch != null ? '+${shorebirdPatch!.number}' : ''}${kIsWeb || (Platform.isAndroid && isFromPlayStore == false) ? ' (sideloaded)' : ''}',
style: TextStyle(fontFamily: "monospace", fontFamilyFallback: <String>["Courier"], fontSize: 12),
)
else

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 <gamepads_linux/gamepads_linux_plugin.h>
#include <media_key_detector_linux/media_key_detector_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
@@ -23,6 +24,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) 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);
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);

View File

@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
bluetooth_low_energy_linux
file_selector_linux
gamepads_linux
media_key_detector_linux
screen_retriever_linux
url_launcher_linux
window_manager

View File

@@ -11,6 +11,7 @@ import file_selector_macos
import flutter_local_notifications
import gamepads_darwin
import keypress_simulator_macos
import media_key_detector_macos
import package_info_plus
import path_provider_foundation
import screen_retriever_macos
@@ -27,6 +28,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
GamepadsDarwinPlugin.register(with: registry.registrar(forPlugin: "GamepadsDarwinPlugin"))
KeypressSimulatorMacosPlugin.register(with: registry.registrar(forPlugin: "KeypressSimulatorMacosPlugin"))
MediaKeyDetectorPlugin.register(with: registry.registrar(forPlugin: "MediaKeyDetectorPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))

View File

@@ -13,6 +13,8 @@ PODS:
- FlutterMacOS
- keypress_simulator_macos (0.0.1):
- FlutterMacOS
- media_key_detector_macos (0.0.1):
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- path_provider_foundation (0.0.1):
@@ -41,6 +43,7 @@ DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- gamepads_darwin (from `Flutter/ephemeral/.symlinks/plugins/gamepads_darwin/macos`)
- keypress_simulator_macos (from `Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos`)
- media_key_detector_macos (from `Flutter/ephemeral/.symlinks/plugins/media_key_detector_macos/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
@@ -65,6 +68,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/gamepads_darwin/macos
keypress_simulator_macos:
:path: Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos
media_key_detector_macos:
:path: Flutter/ephemeral/.symlinks/plugins/media_key_detector_macos/macos
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
path_provider_foundation:
@@ -90,6 +95,7 @@ SPEC CHECKSUMS:
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
gamepads_darwin: 07af6c60c282902b66574c800e20b2b26e68fda8
keypress_simulator_macos: f8556f9101f9f2f175652e0bceddf0fe82a4c6b2
media_key_detector_macos: a93757a483b4b47283ade432b1af9e427c47329f
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161

48
media_key_detector/.gitignore vendored Normal file
View File

@@ -0,0 +1,48 @@
.DS_Store
.atom/
.idea/
.vscode/
.packages
.pub/
.dart_tool/
pubspec.lock
flutter_export_environment.sh
coverage/
Podfile.lock
Pods/
.symlinks/
**/Flutter/App.framework/
**/Flutter/ephemeral/
**/Flutter/Flutter.podspec
**/Flutter/Flutter.framework/
**/Flutter/Generated.xcconfig
**/Flutter/flutter_assets/
ServiceDefinitions.json
xcuserdata/
**/DerivedData/
local.properties
keystore.properties
.gradle/
gradlew
gradlew.bat
gradle-wrapper.jar
.flutter-plugins-dependencies
*.iml
generated_plugin_registrant.cc
generated_plugin_registrant.h
generated_plugin_registrant.dart
GeneratedPluginRegistrant.java
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
GeneratedPluginRegistrant.swift
build/
.flutter-plugins
.project
.classpath
.settings

View File

@@ -0,0 +1,43 @@
# media_key_detector
[![Very Good Ventures][logo_white]][very_good_ventures_link_dark]
[![Very Good Ventures][logo_black]][very_good_ventures_link_light]
Developed with 💙 by [Very Good Ventures][very_good_ventures_link] 🦄
![coverage][coverage_badge]
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link]
A Very Good Flutter Federated Plugin created by the [Very Good Ventures Team][very_good_ventures_link].
Generated by the [Very Good CLI][very_good_cli_link] 🤖
### Integration tests 🧪
Very Good Flutter Plugin uses [fluttium][fluttium_link] for integration tests. Those tests are located
in the front facing package `media_key_detector` example.
**❗ In order to run the integration tests, you need to have the `fluttium_cli` installed. [See how][fluttium_install].**
To run the integration tests, run the following command from the root of the project:
```sh
cd media_key_detector/example
fluttium test flows/test_platform_name.yaml
```
[coverage_badge]: media_key_detector/coverage_badge.svg
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only
[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only
[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
[very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli
[very_good_ventures_link]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core
[very_good_ventures_link_dark]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core#gh-dark-mode-only
[very_good_ventures_link_light]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core#gh-light-mode-only
[fluttium_link]: https://fluttium.dev/
[fluttium_install]: https://fluttium.dev/docs/getting-started/installing-cli

View File

@@ -0,0 +1,7 @@
# 0.0.2
- TBD
# 0.0.1
- Initial Release

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2024 Evan Kaiser
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,110 @@
[![Pub][pub_badge]][pub_link]
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link]
# media_key_detector
Triggers events when keyboard media keys are pressed. On MacOS, it registers
your app as a [Now Playable App](https://developer.apple.com/documentation/mediaplayer/becoming_a_now_playable_app),
allowing it to respond to media events, regardless of whether the event came
from a keyboard, headset, or media remote.
## Features
- Captures and triggeres events for the following keys:
- Play / Pause
- Previous / Rewind
- Next / Fast-forward
## Rationale
In general media key capture works fine using normal keyboard approaches, such
as RawKeyListener or FocusNode. However, it only works on Windows/Linux. In
MacOS, the key events are not detected, and often will even send the events to
an inactive window, like Music.
## Getting started
1. Add the latest version of this package:
- Run `flutter pub add media_key_detector` -or-
- Edit `pubspec.yaml` and then run `flutter pub get`:
```yaml
dependencies:
media_key_detector: ^latest_version
```
2. Import the package
```
import 'package:media_key_detector/media_key_detector.dart';
```
## Usage
```dart
void _mediaKeyListener(MediaKey mediaKey) {
debugPrint('$mediaKey pressed');
}
@override
void initState() {
super.initState();
mediaKeyDetector.addListener(_mediaKeyListener);
}
@override
void dispose() {
mediaKeyDetector.removeListener(_mediaKeyListener);
super.dispose();
}
/// The following two methods are only really needed if you're relying on
/// the plugin to track the playing state. On MacOS/iOS, this is helpful to
/// display the status in the "Command Center".
/// Get whether the media is playing. Note that it starts out "paused",
/// so if your app plays media on open, you should call
/// [mediaKeyDetector.setIsPlaying(true)]
Future<bool> getIsMediaPlaying() async {
return await mediaKeyDetector.getIsPlaying();
}
/// The app tracks the playing state when the user presses the media key,
/// but there are some cases, i.e. when a play button is pressed on the UI
/// interface, where you may need to set it yourself
Future play() async {
return await mediaKeyDetector.setIsPlaying(isPlaying: true);
}
```
## Code Generation
[![Very Good Ventures][logo_white]][very_good_ventures_link_dark]
[![Very Good Ventures][logo_black]][very_good_ventures_link_light]
Developed with 💙 by [Very Good Ventures][very_good_ventures_link] 🦄
A Very Good Flutter Federated Plugin created by the [Very Good Ventures Team][very_good_ventures_link].
Generated by the [Very Good CLI][very_good_cli_link] 🤖
## Support
You can support me by buying me a coffee <a href="https://www.buymeacoffee.com/honeydoodat"><img src="https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png" alt="Buy me a coffee" width="100" /></a>
And also don't forget to star this package on GitHub <a href="https://github.com/holotrek/media_key_detector"><img src="https://img.shields.io/github/stars/holotrek/media_key_detector?logo=github&style=flat-square"></a>
[pub_badge]: https://img.shields.io/pub/v/media_key_detector
[pub_link]: https://pub.dev/packages/media_key_detector
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only
[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only
[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
[very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli
[very_good_ventures_link]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core
[very_good_ventures_link_dark]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core#gh-dark-mode-only
[very_good_ventures_link_light]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core#gh-light-mode-only

View File

@@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.5.1.0.yaml

View File

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="102" height="20">
<linearGradient id="b" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1" />
<stop offset="1" stop-opacity=".1" />
</linearGradient>
<clipPath id="a">
<rect width="102" height="20" rx="3" fill="#fff" />
</clipPath>
<g clip-path="url(#a)">
<path fill="#555" d="M0 0h59v20H0z" />
<path fill="#44cc11" d="M59 0h43v20H59z" />
<path fill="url(#b)" d="M0 0h102v20H0z" />
</g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110">
<text x="305" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">coverage</text>
<text x="305" y="140" transform="scale(.1)" textLength="490">coverage</text>
<text x="795" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">100%</text>
<text x="795" y="140" transform="scale(.1)" textLength="330">100%</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,49 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Fluttium related files
.fluttium_*_launcher.dart

View File

@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: 7048ed95a5ad3e43d697e0c397464193991fc230
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
- platform: android
create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
- platform: ios
create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
- platform: linux
create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
- platform: macos
create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
- platform: web
create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
- platform: windows
create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -0,0 +1,3 @@
# media_key_detector_example
Demonstrates how to use the media_key_detector plugin.

View File

@@ -0,0 +1,6 @@
include: package:very_good_analysis/analysis_options.5.1.0.yaml
linter:
rules:
public_member_api_docs: false
require_trailing_commas: false
lines_longer_than_80_chars: false

View File

@@ -0,0 +1,144 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:media_key_detector/media_key_detector.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: HomePage());
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String? _platformName;
bool _isPlaying = false;
Map<MediaKey, bool> keyPressed = {
MediaKey.playPause: false,
MediaKey.rewind: false,
MediaKey.fastForward: false,
};
void _mediaKeyListener(MediaKey mediaKey) {
debugPrint('$mediaKey pressed');
mediaKeyDetector
.getIsPlaying()
.then((playing) => setState(() => _isPlaying = playing));
if (keyPressed[mediaKey] == false) {
setState(() => keyPressed[mediaKey] = true);
Timer(const Duration(seconds: 3), () {
setState(() => keyPressed[mediaKey] = false);
});
}
}
Future<void> _togglePlayPause() async {
setState(() => _isPlaying = !_isPlaying);
await mediaKeyDetector.setIsPlaying(isPlaying: _isPlaying);
}
@override
void initState() {
super.initState();
mediaKeyDetector.addListener(_mediaKeyListener);
}
@override
void dispose() {
mediaKeyDetector.removeListener(_mediaKeyListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(title: const Text('MediaKeyDetector Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Press a Media button on your IO device to highlight the corresponding icon.'),
const Text(
'Press the play/pause button to send now playing info to plugin.'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.fast_rewind_rounded,
size: 40,
color: (keyPressed[MediaKey.rewind] ?? false)
? colors.inversePrimary
: colors.onBackground,
),
IconButton.filled(
onPressed: _togglePlayPause,
style:
IconButton.styleFrom(backgroundColor: colors.secondary),
icon: Icon(
_isPlaying
? Icons.pause_circle_rounded
: Icons.play_circle_rounded,
size: 40,
color: (keyPressed[MediaKey.playPause] ?? false)
? colors.inversePrimary
: colors.onPrimary,
),
),
Icon(
Icons.fast_forward_rounded,
size: 40,
color: (keyPressed[MediaKey.fastForward] ?? false)
? colors.inversePrimary
: colors.onBackground,
),
],
),
Text('Is currently playing: $_isPlaying'),
if (_platformName == null)
const SizedBox.shrink()
else
Text(
'Platform Name: $_platformName',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
if (!context.mounted) return;
try {
final result = await getPlatformName();
setState(() => _platformName = result);
} catch (error) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Theme.of(context).primaryColor,
content: Text('$error'),
),
);
}
},
child: const Text('Get Platform Name'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1 @@
flutter/ephemeral

View File

@@ -0,0 +1,106 @@
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
set(BINARY_NAME "example")
set(APPLICATION_ID "com.example.verygoodcore_example")
cmake_policy(SET CMP0063 NEW)
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Configure build options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
# Flutter library and tool build rules.
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Application build
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
apply_standard_settings(${BINARY_NAME})
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@@ -0,0 +1,88 @@
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
PkgConfig::BLKID
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
linux-x64 ${CMAKE_BUILD_TYPE}
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@@ -0,0 +1,24 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
media_key_detector_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@@ -0,0 +1,15 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "my_application.h"
int main(int argc, char** argv) {
// Only X11 is currently supported.
// Wayland support is being developed:
// https://github.com/flutter/flutter/issues/57932.
gdk_set_allowed_backends("x11");
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View File

@@ -0,0 +1,49 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "example");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
return MY_APPLICATION(g_object_new(
my_application_get_type(), "application-id", APPLICATION_ID, nullptr));
}

View File

@@ -0,0 +1,22 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

View File

@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -0,0 +1,40 @@
platform :osx, '10.14'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

View File

@@ -0,0 +1,633 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
8056B9E1FA9168F10A6879E3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF88737E4227AF4483D73A72 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
082C0DA75B3DC0A853AE53D1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
604426B4C55353DB93E63726 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
7CC079B1077C0BC3449BC7C1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
BF88737E4227AF4483D73A72 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8056B9E1FA9168F10A6879E3 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0EEA7CE099CD931C8BF18132 /* Pods */ = {
isa = PBXGroup;
children = (
604426B4C55353DB93E63726 /* Pods-Runner.debug.xcconfig */,
7CC079B1077C0BC3449BC7C1 /* Pods-Runner.release.xcconfig */,
082C0DA75B3DC0A853AE53D1 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
0EEA7CE099CD931C8BF18132 /* Pods */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* example.app */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
BF88737E4227AF4483D73A72 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
BA51270DFB65A51AF6561C86 /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
01C1BE47E39E4FF65E75DFDD /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* example.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
01C1BE47E39E4FF65E75DFDD /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
BA51270DFB65A51AF6561C86 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,9 @@
import Cocoa
import FlutterMacOS
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}

View File

@@ -0,0 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,343 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="EPT-qC-fAb">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
</menuItem>
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</window>
</objects>
</document>

View File

@@ -0,0 +1,14 @@
// Application-level settings for the Runner target.
//
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
// future. If not, the values below would default to using the project name when this becomes a
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = example
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.myplugin.example
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2023 com.example.verygoodcore. All rights reserved.

View File

@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -0,0 +1,13 @@
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
GCC_WARN_UNDECLARED_SELECTOR = YES
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
CLANG_WARN_PRAGMA_PACK = YES
CLANG_WARN_STRICT_PROTOTYPES = YES
CLANG_WARN_COMMA = YES
GCC_WARN_STRICT_SELECTOR_MATCH = YES
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
GCC_WARN_SHADOW = YES
CLANG_WARN_UNREACHABLE_CODE = YES

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,30 @@
name: media_key_detector_example
description: Demonstrates how to use the media_key_detector plugin.
version: 0.1.0+1
publish_to: none
environment:
sdk: "^3.3.0"
dependencies:
flutter:
sdk: flutter
media_key_detector:
# When depending on this package from a real application you should use:
# media_key_detector: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
dev_dependencies:
flutter_driver:
sdk: flutter
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
very_good_analysis: ^5.1.0
flutter:
uses-material-design: true

View File

@@ -0,0 +1,17 @@
flutter/ephemeral/
# Visual Studio user-specific files.
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio build-related files.
x64/
x86/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/

View File

@@ -0,0 +1,95 @@
cmake_minimum_required(VERSION 3.14)
project(example LANGUAGES CXX)
set(BINARY_NAME "example")
cmake_policy(SET CMP0063 NEW)
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Configure build options.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(IS_MULTICONFIG)
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
CACHE STRING "" FORCE)
else()
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
endif()
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
# Use Unicode for all projects.
add_definitions(-DUNICODE -D_UNICODE)
# Compilation settings that should be applied to most targets.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_17)
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
# Flutter library and tool build rules.
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build
add_subdirectory("runner")
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)

View File

@@ -0,0 +1,103 @@
cmake_minimum_required(VERSION 3.14)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_windows.h"
"flutter_messenger.h"
"flutter_plugin_registrar.h"
"flutter_texture_registrar.h"
)
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"core_implementations.cc"
"standard_codec.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_view_controller.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
set_target_properties(flutter_wrapper_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
${PHONY_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
windows-x64 $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)

View File

@@ -0,0 +1,24 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
media_key_detector_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.14)
project(runner LANGUAGES CXX)
add_executable(${BINARY_NAME} WIN32
"flutter_window.cpp"
"main.cpp"
"utils.cpp"
"win32_window.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
"Runner.rc"
"runner.exe.manifest"
)
apply_standard_settings(${BINARY_NAME})
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
add_dependencies(${BINARY_NAME} flutter_assemble)

View File

@@ -0,0 +1,121 @@
// Microsoft Visual C++ generated resource script.
//
#pragma code_page(65001)
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APP_ICON ICON "resources\\app_icon.ico"
/////////////////////////////////////////////////////////////////////////////
//
// Version
//
#ifdef FLUTTER_BUILD_NUMBER
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
#else
#define VERSION_AS_NUMBER 1,0,0
#endif
#ifdef FLUTTER_BUILD_NAME
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
#else
#define VERSION_AS_STRING "1.0.0"
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_AS_NUMBER
PRODUCTVERSION VERSION_AS_NUMBER
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "dev.flutter.plugins" "\0"
VALUE "FileDescription", "example" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "example" "\0"
VALUE "LegalCopyright", "Copyright (C) 2022 dev.flutter.plugins. All rights reserved." "\0"
VALUE "OriginalFilename", "example.exe" "\0"
VALUE "ProductName", "example" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

Some files were not shown because too many files have changed in this diff Show More