Compare commits

...

25 Commits

Author SHA1 Message Date
Jonas Bark
b875489ad3 more logging 2025-04-02 18:29:41 +02:00
Jonas Bark
ece3f3822f increase connection timeout, logging 2025-04-02 13:34:40 +02:00
Jonas Bark
0780cdc80b increase connection timeout, logging 2025-04-02 13:30:25 +02:00
Jonas Bark
1c0e027abb Android: make sure the touch reassignment page is fullscreen 2025-04-02 12:20:56 +02:00
Jonas Bark
0dcb666bbd update changelog 2025-04-01 21:21:58 +02:00
Jonas Bark
00ebca7a01 Merge branch 'windows' 2025-04-01 21:20:25 +02:00
Jonas Bark
f9c8820303 version++ 2025-04-01 21:20:20 +02:00
Jonas Bark
3d414edda9 Zwift Ride: cleanup, fix button 2025-04-01 21:17:46 +02:00
Jonas Bark
0e3f6f1d5e Windows: implement keybd_event 2025-04-01 19:54:41 +02:00
Jonas Bark
c4b0ef38c0 Zwift Ride: only connect to the left controller 2025-04-01 19:38:52 +02:00
Jonas Bark
2e800bb2de add hint to install runtime libraries for windows 2025-04-01 11:48:38 +02:00
Jonas Bark
505b970497 show the connected device type if the bluetooth name is null 2025-04-01 09:43:08 +02:00
Jonas Bark
986bfd481c show the connected device type if the bluetooth name is null 2025-04-01 09:42:40 +02:00
Jonas Bark
ab8d480a01 change SendInput to use keybd_event instead 2025-04-01 09:39:41 +02:00
Jonas Bark
7e19b76403 integrate keypress_simulator to make changes 2025-04-01 09:25:05 +02:00
Jonas Bark
cbc2f103ac run connection sequentially - may fix issues on Windows 2025-04-01 09:21:31 +02:00
Jonas Bark
6a7922cf17 version++ 2025-03-31 21:00:37 +02:00
Jonas Bark
8e23de2718 version update check => internet permission is required 2025-03-31 20:58:27 +02:00
Jonas Bark
11aec5fba1 Zwift Ride: add buttonPowerDown to shift gears
Zwift Play: Fix buttonShift assignment
Android: fix Media next button
2025-03-31 20:20:47 +02:00
Jonas Bark
c98f213e2e add some comments 2025-03-30 21:34:29 +02:00
Jonas Bark
8c11cfcad6 remove location requirement again 2025-03-30 21:21:31 +02:00
Jonas Bark
5fe88ffc6a version++ 2025-03-30 21:18:38 +02:00
Jonas Bark
7a8c7a4ee1 fix #6 for real this time 2025-03-30 21:18:18 +02:00
Jonas Bark
3343325195 adjust reinstating saved keymaps 2025-03-30 20:06:44 +02:00
Jonas Bark
edda16dc06 adjust reinstating saved keymaps 2025-03-30 19:30:34 +02:00
50 changed files with 1133 additions and 100 deletions

View File

@@ -1,4 +1,24 @@
### 1.1.0 (2025-03-30)
### 1.1.9 (2025-04-03)
- Add more troubleshooting during connection
### 1.1.8 (2025-04-02)
- Android: make sure the touch reassignment page is fullscreen
### 1.1.7 (2025-04-01)
- Zwift Ride: fix connection issues by connecting only to the left controller
- Windows: connect sequentially to fix (finally?) fix connection issues
- Windows: change the way keyboard is simulated, should fix glitches
### 1.1.6 (2025-03-31)
- Zwift Ride: add buttonPowerDown to shift gears
- Zwift Play: Fix buttonShift assignment
- Android: fix action to go to next song
- App now checks if you run the latest available version
### 1.1.5 (2025-03-30)
- fix bluetooth connection #6, also add missing entitlement on macOS
### 1.1.3 (2025-03-30)
- Windows: fix custom keyboard profile recreation after restart, also warn when choosing MyWhoosh profile (may fix #7)
- Zwift Ride: button map adjustments to prevent double shifting
- potential fix for #6

View File

@@ -30,9 +30,12 @@ Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
## Supported Platforms
- Android
- macOS
- Windows
- Windows (make sure you have installed the "[Microsoft Visual C++ Runtime libraries](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170)")
- [Web](https://jonasbark.github.io/swiftcontrol/) (you won't be able to do much)
## Troubleshooting
Your Zwift device is found but connection does not work properly? You may need to update the firmware in Zwift Companion app.
## How does it work?
The app connects to your Zwift device automatically.

View File

@@ -11,14 +11,11 @@ import android.os.Bundle
import android.provider.Settings
import androidx.core.content.ContextCompat.startActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
/** AccessibilityPlugin */
class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
class AccessibilityPlugin: FlutterPlugin, Accessibility {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
@@ -38,14 +35,6 @@ class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
Observable.fromService = eventHandler
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
@@ -72,7 +61,10 @@ class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility {
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))
}
MediaAction.NEXT -> audioService.dispatchMediaKeyEvent(android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_NEXT))
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))
}
MediaAction.VOLUME_DOWN -> audioService.adjustVolume(android.media.AudioManager.ADJUST_LOWER, android.media.AudioManager.FLAG_SHOW_UI)
MediaAction.VOLUME_UP -> audioService.adjustVolume(android.media.AudioManager.ADJUST_RAISE, android.media.AudioManager.FLAG_SHOW_UI)
}

View File

@@ -19,6 +19,9 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- to check if you have the latest version -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="SwiftControl"
android:name="${applicationName}"

View File

@@ -0,0 +1,12 @@
# Defines the Chromium style for automatic reformatting.
# http://clang.llvm.org/docs/ClangFormatStyleOptions.html
BasedOnStyle: Chromium
# This defaults to 'Auto'. Explicitly set it for a while, so that
# 'vector<vector<int> >' in existing files gets formatted to
# 'vector<vector<int>>'. ('Auto' means that clang-format will only use
# 'int>>' if the file already contains at least one such instance.)
Standard: Cpp11
SortIncludes: true
---
Language: ObjC
ColumnLimit: 100

View File

@@ -0,0 +1 @@
liberapay: lijy91

View File

@@ -0,0 +1,50 @@
name: build
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
jobs:
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.8"
channel: "stable"
- uses: bluefireteam/melos-action@v2
- working-directory: ./packages/keypress_simulator/example
run: |
melos bs
flutter build macos --release
build-web:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.8"
channel: "stable"
- uses: bluefireteam/melos-action@v2
- working-directory: ./packages/keypress_simulator/example
run: |
melos bs
flutter build web --release
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.8"
channel: "stable"
- uses: bluefireteam/melos-action@v2
- working-directory: ./packages/keypress_simulator/example
run: |
melos bs
flutter build windows --release

View File

@@ -0,0 +1,31 @@
name: lint
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.8"
channel: "stable"
- uses: bluefireteam/melos-action@v2
- run: melos run analyze
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.8"
channel: "stable"
cache: true
- uses: bluefireteam/melos-action@v2
- run: melos run format-check

View File

@@ -0,0 +1,20 @@
name: test
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.16.8"
channel: "stable"
cache: true
- uses: bluefireteam/melos-action@v2
- run: melos run test --no-select

6
keypress_simulator/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.dart_tool/
.idea/
*.iml
pubspec_overrides.yaml
pubspec.lock

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-2024 LiJianying <lijy91@foxmail.com>
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,93 @@
> **🚀 快速发布您的应用**: 试试 [Fastforge](https://fastforge.dev) - 构建、打包和分发您的 Flutter 应用最简单的方式。
# keypress_simulator
[![pub version][pub-image]][pub-url] [![][discord-image]][discord-url]
[pub-image]: https://img.shields.io/pub/v/keypress_simulator.svg
[pub-url]: https://pub.dev/packages/keypress_simulator
[discord-image]: https://img.shields.io/discord/884679008049037342.svg
[discord-url]: https://discord.gg/zPa6EZ2jqb
这个插件允许 Flutter 桌面应用模拟按键操作。
---
[English](./README.md) | 简体中文
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [平台支持](#%E5%B9%B3%E5%8F%B0%E6%94%AF%E6%8C%81)
- [快速开始](#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
- [安装](#%E5%AE%89%E8%A3%85)
- [用法](#%E7%94%A8%E6%B3%95)
- [谁在用使用它?](#%E8%B0%81%E5%9C%A8%E7%94%A8%E4%BD%BF%E7%94%A8%E5%AE%83)
- [许可证](#%E8%AE%B8%E5%8F%AF%E8%AF%81)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 平台支持
| Linux | macOS | Windows |
| :---: | :---: | :-----: |
| | ✔️ | ✔️ |
## 快速开始
### 安装
将此添加到你的软件包的 pubspec.yaml 文件:
```yaml
dependencies:
keypress_simulator: ^0.2.0
```
### 用法
```dart
import 'package:keypress_simulator/keypress_simulator.dart';
// 1. Simulate pressing ⌘ + C
// 1.1 Simulate key down
await keyPressSimulator.simulateKeyDown(
PhysicalKeyboardKey.keyC,
[ModifierKey.metaModifier],
);
// 1.2 Simulate key up
await keyPressSimulator.simulateKeyUp(
PhysicalKeyboardKey.keyC,
[ModifierKey.metaModifier],
);
// 2. Simulate long pressing ⌘ + space
// 2.1. Simulate key down
await keyPressSimulator.simulateKeyDown(
PhysicalKeyboardKey.space,
[ModifierKey.metaModifier],
);
await Future.delayed(const Duration(seconds: 5));
// 2.2. Simulate key up
await keyPressSimulator.simulateKeyUp(
PhysicalKeyboardKey.space,
[ModifierKey.metaModifier],
);
```
> 请看这个插件的示例应用,以了解完整的例子。
## 谁在用使用它?
- [Biyi (比译)](https://biyidev.com/) - 一个便捷的翻译和词典应用程序。
## 许可证
[MIT](./LICENSE)

View File

@@ -0,0 +1,93 @@
> **🚀 Ship Your App Faster**: Try [Fastforge](https://fastforge.dev) - The simplest way to build, package and distribute your Flutter apps.
# keypress_simulator
[![pub version][pub-image]][pub-url] [![][discord-image]][discord-url]
[pub-image]: https://img.shields.io/pub/v/keypress_simulator.svg
[pub-url]: https://pub.dev/packages/keypress_simulator
[discord-image]: https://img.shields.io/discord/884679008049037342.svg
[discord-url]: https://discord.gg/zPa6EZ2jqb
This plugin allows Flutter desktop apps to simulate key presses.
---
English | [简体中文](./README-ZH.md)
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Platform Support](#platform-support)
- [Quick Start](#quick-start)
- [Installation](#installation)
- [Usage](#usage)
- [Who's using it?](#whos-using-it)
- [License](#license)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Platform Support
| Linux | macOS | Windows |
| :---: | :---: | :-----: |
| | ✔️ | ✔️ |
## Quick Start
### Installation
Add this to your package's pubspec.yaml file:
```yaml
dependencies:
keypress_simulator: ^0.2.0
```
### Usage
```dart
import 'package:keypress_simulator/keypress_simulator.dart';
// 1. Simulate pressing ⌘ + C
// 1.1 Simulate key down
await keyPressSimulator.simulateKeyDown(
PhysicalKeyboardKey.keyC,
[ModifierKey.metaModifier],
);
// 1.2 Simulate key up
await keyPressSimulator.simulateKeyUp(
PhysicalKeyboardKey.keyC,
[ModifierKey.metaModifier],
);
// 2. Simulate long pressing ⌘ + space
// 2.1. Simulate key down
await keyPressSimulator.simulateKeyDown(
PhysicalKeyboardKey.space,
[ModifierKey.metaModifier],
);
await Future.delayed(const Duration(seconds: 5));
// 2.2. Simulate key up
await keyPressSimulator.simulateKeyUp(
PhysicalKeyboardKey.space,
[ModifierKey.metaModifier],
);
```
> Please see the example app of this plugin for a full example.
## Who's using it?
- [Biyi (比译)](https://biyidev.com/) - A convenient translation and dictionary app.
## License
[MIT](./LICENSE)

View File

@@ -0,0 +1,35 @@
name: keypress_simulator_workspace
repository: https://github.com/leanflutter/keypress_simulator
packages:
- examples/**
- packages/**
command:
bootstrap:
# Uses the pubspec_overrides.yaml instead of having Melos modifying the lock file.
usePubspecOverrides: true
scripts:
analyze:
exec: flutter analyze --fatal-infos
description: Run `flutter analyze` for all packages.
test:
exec: flutter test
description: Run `flutter test` for a specific package.
packageFilters:
dirExists:
- test
format:
exec: dart format . --fix
description: Run `dart format` for all packages.
format-check:
exec: dart format . --fix --set-exit-if-changed
description: Run `dart format` checks for all packages.
fix:
exec: dart fix . --apply
description: Run `dart fix` for all packages.

View File

@@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# 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
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

View File

@@ -0,0 +1,30 @@
# 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 and should not be manually edited.
version:
revision: "67457e669f79e9f8d13d7a68fe09775fefbb79f4"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
- platform: windows
create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4
# 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 @@
## 0.2.0
* First release.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-2024 LiJianying <lijy91@foxmail.com>
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,12 @@
# keypress_simulator_windows
[![pub version][pub-image]][pub-url]
[pub-image]: https://img.shields.io/pub/v/keypress_simulator_windows.svg
[pub-url]: https://pub.dev/packages/keypress_simulator_windows
The Windows implementation of [keypress_simulator](https://pub.dev/packages/keypress_simulator).
## License
[MIT](./LICENSE)

View File

@@ -0,0 +1 @@
include: package:mostly_reasonable_lints/flutter.yaml

View File

@@ -0,0 +1,25 @@
name: keypress_simulator_windows
description: Windows implementation of the keypress_simulator plugin.
version: 0.2.0
repository: https://github.com/leanflutter/keypress_simulator/tree/main/packages/keypress_simulator_windows
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
keypress_simulator_platform_interface: ^0.2.0
dev_dependencies:
flutter_test:
sdk: flutter
mostly_reasonable_lints: ^0.1.1
flutter:
plugin:
implements: keypress_simulator
platforms:
windows:
pluginClass: KeypressSimulatorWindowsPluginCApi

View File

@@ -0,0 +1,17 @@
flutter/
# 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,100 @@
# The Flutter tooling requires that developers have a version of Visual Studio
# installed that includes CMake 3.14 or later. You should not increase this
# version, as doing so will cause the plugin to fail to compile for some
# customers of the plugin.
cmake_minimum_required(VERSION 3.14)
# Project-level configuration.
set(PROJECT_NAME "keypress_simulator_windows")
project(${PROJECT_NAME} LANGUAGES CXX)
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(VERSION 3.14...3.25)
# This value is used when generating builds using this plugin, so it must
# not be changed
set(PLUGIN_NAME "keypress_simulator_windows_plugin")
# Any new source files that you add to the plugin should be added here.
list(APPEND PLUGIN_SOURCES
"keypress_simulator_windows_plugin.cpp"
"keypress_simulator_windows_plugin.h"
)
# Define the plugin library target. Its name must not be changed (see comment
# on PLUGIN_NAME above).
add_library(${PLUGIN_NAME} SHARED
"include/keypress_simulator_windows/keypress_simulator_windows_plugin_c_api.h"
"keypress_simulator_windows_plugin_c_api.cpp"
${PLUGIN_SOURCES}
)
# Apply a standard set of build settings that are configured in the
# application-level CMakeLists.txt. This can be removed for plugins that want
# full control over build settings.
apply_standard_settings(${PLUGIN_NAME})
# Symbols are hidden by default to reduce the chance of accidental conflicts
# between plugins. This should not be removed; any symbols that should be
# exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro.
set_target_properties(${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
# Source include directories and library dependencies. Add any plugin-specific
# dependencies here.
target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)
# List of absolute paths to libraries that should be bundled with the plugin.
# This list could contain prebuilt libraries, or libraries created by an
# external build triggered from this build file.
set(keypress_simulator_windows_bundled_libraries
""
PARENT_SCOPE
)
# === Tests ===
# These unit tests can be run from a terminal after building the example, or
# from Visual Studio after opening the generated solution file.
# Only enable test builds when building the example (which sets this variable)
# so that plugin clients aren't building the tests.
if (${include_${PROJECT_NAME}_tests})
set(TEST_RUNNER "${PROJECT_NAME}_test")
enable_testing()
# Add the Google Test dependency.
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/release-1.11.0.zip
)
# Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
# Disable install commands for gtest so it doesn't end up in the bundle.
set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE)
FetchContent_MakeAvailable(googletest)
# The plugin's C API is not very useful for unit testing, so build the sources
# directly into the test binary rather than using the DLL.
add_executable(${TEST_RUNNER}
test/keypress_simulator_windows_plugin_test.cpp
${PLUGIN_SOURCES}
)
apply_standard_settings(${TEST_RUNNER})
target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin)
target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock)
# flutter_wrapper_plugin has link dependencies on the Flutter DLL.
add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${FLUTTER_LIBRARY}" $<TARGET_FILE_DIR:${TEST_RUNNER}>
)
# Enable automatic test discovery.
include(GoogleTest)
gtest_discover_tests(${TEST_RUNNER})
endif()

View File

@@ -0,0 +1,23 @@
#ifndef FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_C_API_H_
#define FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_C_API_H_
#include <flutter_plugin_registrar.h>
#ifdef FLUTTER_PLUGIN_IMPL
#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport)
#else
#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport)
#endif
#if defined(__cplusplus)
extern "C" {
#endif
FLUTTER_PLUGIN_EXPORT void KeypressSimulatorWindowsPluginCApiRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar);
#if defined(__cplusplus)
} // extern "C"
#endif
#endif // FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_C_API_H_

View File

@@ -0,0 +1,98 @@
#include "keypress_simulator_windows_plugin.h"
// This must be included before many other Windows headers.
#include <windows.h>
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <memory>
#include <sstream>
using flutter::EncodableList;
using flutter::EncodableMap;
using flutter::EncodableValue;
namespace keypress_simulator_windows {
// static
void KeypressSimulatorWindowsPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarWindows* registrar) {
auto channel =
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
registrar->messenger(), "dev.leanflutter.plugins/keypress_simulator",
&flutter::StandardMethodCodec::GetInstance());
auto plugin = std::make_unique<KeypressSimulatorWindowsPlugin>();
channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto& call, auto result) {
plugin_pointer->HandleMethodCall(call, std::move(result));
});
registrar->AddPlugin(std::move(plugin));
}
KeypressSimulatorWindowsPlugin::KeypressSimulatorWindowsPlugin() {}
KeypressSimulatorWindowsPlugin::~KeypressSimulatorWindowsPlugin() {}
void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
const EncodableMap& args = std::get<EncodableMap>(*method_call.arguments());
UINT keyCode = std::get<int>(args.at(EncodableValue("keyCode")));
std::vector<std::string> modifiers;
bool keyDown = std::get<bool>(args.at(EncodableValue("keyDown")));
EncodableList key_modifier_list =
std::get<EncodableList>(args.at(EncodableValue("modifiers")));
for (flutter::EncodableValue key_modifier_value : key_modifier_list) {
std::string key_modifier = std::get<std::string>(key_modifier_value);
modifiers.push_back(key_modifier);
}
INPUT input[6];
for (int32_t i = 0; i < modifiers.size(); i++) {
if (modifiers[i].compare("shiftModifier") == 0) {
input[i].ki.wVk = VK_SHIFT;
} else if (modifiers[i].compare("controlModifier") == 0) {
input[i].ki.wVk = VK_CONTROL;
} else if (modifiers[i].compare("altModifier") == 0) {
input[i].ki.wVk = VK_MENU;
} else if (modifiers[i].compare("metaModifier") == 0) {
input[i].ki.wVk = VK_LWIN;
}
input[i].ki.dwFlags = keyDown ? 0 : KEYEVENTF_KEYUP;
input[i].type = INPUT_KEYBOARD;
}
/*int keyIndex = static_cast<int>(modifiers.size());
input[keyIndex].ki.wVk = static_cast<WORD>(keyCode);
input[keyIndex].ki.dwFlags = keyDown ? 0 : KEYEVENTF_KEYUP;
input[keyIndex].type = INPUT_KEYBOARD;*/
// Send key sequence to system
//SendInput(static_cast<UINT>(std::size(input)), input, sizeof(INPUT));
BYTE byteValue = static_cast<BYTE>(keyCode);
keybd_event(byteValue, 0x45, keyDown ? 0 : KEYEVENTF_KEYUP, 0);
result->Success(flutter::EncodableValue(true));
}
void KeypressSimulatorWindowsPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
if (method_call.method_name().compare("simulateKeyPress") == 0) {
SimulateKeyPress(method_call, std::move(result));
} else {
result->NotImplemented();
}
}
} // namespace keypress_simulator_windows

View File

@@ -0,0 +1,37 @@
#ifndef FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_H_
#define FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_H_
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <memory>
namespace keypress_simulator_windows {
class KeypressSimulatorWindowsPlugin : public flutter::Plugin {
public:
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar);
KeypressSimulatorWindowsPlugin();
virtual ~KeypressSimulatorWindowsPlugin();
// Disallow copy and assign.
KeypressSimulatorWindowsPlugin(const KeypressSimulatorWindowsPlugin&) =
delete;
KeypressSimulatorWindowsPlugin& operator=(
const KeypressSimulatorWindowsPlugin&) = delete;
void KeypressSimulatorWindowsPlugin::SimulateKeyPress(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
// Called when a method is called on this plugin's channel from Dart.
void HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
};
} // namespace keypress_simulator_windows
#endif // FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_H_

View File

@@ -0,0 +1,12 @@
#include "include/keypress_simulator_windows/keypress_simulator_windows_plugin_c_api.h"
#include <flutter/plugin_registrar_windows.h>
#include "keypress_simulator_windows_plugin.h"
void KeypressSimulatorWindowsPluginCApiRegisterWithRegistrar(
FlutterDesktopPluginRegistrarRef registrar) {
keypress_simulator_windows::KeypressSimulatorWindowsPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarManager::GetInstance()
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
}

View File

@@ -0,0 +1,43 @@
#include <flutter/method_call.h>
#include <flutter/method_result_functions.h>
#include <flutter/standard_method_codec.h>
#include <gtest/gtest.h>
#include <windows.h>
#include <memory>
#include <string>
#include <variant>
#include "keypress_simulator_windows_plugin.h"
namespace keypress_simulator_windows {
namespace test {
namespace {
using flutter::EncodableMap;
using flutter::EncodableValue;
using flutter::MethodCall;
using flutter::MethodResultFunctions;
} // namespace
TEST(KeypressSimulatorWindowsPlugin, GetPlatformVersion) {
KeypressSimulatorWindowsPlugin plugin;
// Save the reply value from the success callback.
std::string result_string;
plugin.HandleMethodCall(
MethodCall("getPlatformVersion", std::make_unique<EncodableValue>()),
std::make_unique<MethodResultFunctions<>>(
[&result_string](const EncodableValue* result) {
result_string = std::get<std::string>(*result);
},
nullptr, nullptr));
// Since the exact string varies by host, just ensure that it's a string
// with the expected format.
EXPECT_TRUE(result_string.rfind("Windows ", 0) == 0);
}
} // namespace test
} // namespace keypress_simulator_windows

View File

@@ -0,0 +1,9 @@
name: keypress_simulator_workspace
homepage: https://github.com/leanflutter/keypress_simulator
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
dev_dependencies:
melos: ^3.1.0

View File

@@ -15,6 +15,9 @@ class Connection {
final devices = <BaseDevice>[];
var androidNotificationsSetup = false;
final _connectionQueue = <BaseDevice>[];
var _handlingConnectionQueue = false;
final Map<BaseDevice, StreamSubscription<BaseNotification>> _streamSubscriptions = {};
final StreamController<BaseNotification> _actionStreams = StreamController<BaseNotification>.broadcast();
Stream<BaseNotification> get actionStream => _actionStreams.stream;
@@ -31,8 +34,10 @@ class Connection {
UniversalBle.onScanResult = (result) {
if (_lastScanResult.none((e) => e.deviceId == result.deviceId)) {
_lastScanResult.add(result);
_actionStreams.add(LogNotification('Found new devices: ${result.name}'));
final scanResult = BaseDevice.fromScanResult(result);
_actionStreams.add(
LogNotification('Found new device: ${result.name ?? scanResult?.runtimeType ?? result.deviceId}'),
);
if (scanResult != null) {
_addDevices([scanResult]);
}
@@ -57,7 +62,7 @@ class Connection {
if (!kIsWeb && !Platform.isWindows) {
UniversalBle.getSystemDevices(
withServices: [BleUuid.ZWIFT_CUSTOM_SERVICE_UUID, BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID],
).then((devices) {
).then((devices) async {
final baseDevices = devices.mapNotNull(BaseDevice.fromScanResult).toList();
if (baseDevices.isNotEmpty) {
_addDevices(baseDevices);
@@ -81,9 +86,8 @@ class Connection {
final newDevices = dev.where((device) => !devices.contains(device)).toList();
devices.addAll(newDevices);
for (final device in newDevices) {
_connect(device).then((_) {});
}
_connectionQueue.addAll(newDevices);
_handleConnectionQueue();
hasDevices.value = devices.isNotEmpty;
if (devices.isNotEmpty && !androidNotificationsSetup && !kIsWeb && Platform.isAndroid) {
@@ -95,6 +99,30 @@ class Connection {
}
}
void _handleConnectionQueue() {
// windows apparently has issues when connecting to multiple devices at once, so don't
if (_connectionQueue.isNotEmpty && !_handlingConnectionQueue) {
_handlingConnectionQueue = true;
final device = _connectionQueue.removeAt(0);
_actionStreams.add(LogNotification('Connecting to: ${device.device.name ?? device.runtimeType}'));
_connect(device)
.then((_) {
_handlingConnectionQueue = false;
_actionStreams.add(LogNotification('Connection finished: ${device.device.name ?? device.runtimeType}'));
if (_connectionQueue.isNotEmpty) {
_handleConnectionQueue();
}
})
.catchError((e) {
_handlingConnectionQueue = false;
_actionStreams.add(LogNotification('Connection failed: ${device.device.name ?? device.runtimeType} - $e'));
if (_connectionQueue.isNotEmpty) {
_handleConnectionQueue();
}
});
}
}
Future<void> _connect(BaseDevice bleDevice) async {
try {
final actionSubscription = bleDevice.actionStream.listen((data) {
@@ -117,6 +145,7 @@ class Connection {
print(e);
print("backtrace: $backtrace");
}
rethrow;
}
}

View File

@@ -28,16 +28,19 @@ abstract class BaseDevice {
String get customServiceId => BleUuid.ZWIFT_CUSTOM_SERVICE_UUID;
static BaseDevice? fromScanResult(BleDevice scanResult) {
// Use the name first, probably safest method on all platforms
// Use the name first as the "System Devices" and Web (android sometimes Windows) don't have manufacturer data
final device = switch (scanResult.name) {
'Zwift Ride' => ZwiftRide(scanResult),
//'Zwift Ride' => ZwiftRide(scanResult), special case for Zwift Ride: we must only connect to the left controller
// https://www.makinolo.com/blog/2024/07/26/zwift-ride-protocol/
'Zwift Play' => ZwiftPlay(scanResult),
'Zwift Click' => ZwiftClick(scanResult),
_ => null,
};
// otherwise use the manufacturer data, which doesn't exist on Web and "System Devices"
if (device == null) {
if (device != null) {
return device;
} else {
// otherwise use the manufacturer data to identify the device
final manufacturerData = scanResult.manufacturerDataList;
final data = manufacturerData.firstOrNullWhere((e) => e.companyId == Constants.ZWIFT_MANUFACTURER_ID)?.payload;
@@ -50,7 +53,7 @@ abstract class BaseDevice {
DeviceType.click => ZwiftClick(scanResult),
DeviceType.playRight => ZwiftPlay(scanResult),
DeviceType.playLeft => ZwiftPlay(scanResult),
DeviceType.rideRight => ZwiftRide(scanResult),
//DeviceType.rideRight => ZwiftRide(scanResult), // see comment above
DeviceType.rideLeft => ZwiftRide(scanResult),
_ => null,
};
@@ -75,7 +78,7 @@ abstract class BaseDevice {
Stream<BaseNotification> get actionStream => actionStreamInternal.stream;
Future<void> connect() async {
await UniversalBle.connect(device.deviceId, connectionTimeout: const Duration(seconds: 3));
await UniversalBle.connect(device.deviceId);
if (!kIsWeb && Platform.isAndroid) {
//await UniversalBle.requestMtu(device.deviceId, 256);
@@ -89,7 +92,9 @@ abstract class BaseDevice {
final customService = services.firstOrNullWhere((service) => service.uuid == customServiceId);
if (customService == null) {
throw Exception('Custom service $customServiceId not found for device $this ${device.name ?? device.rawName}');
throw Exception(
'Custom service $customServiceId not found for device $this ${device.name ?? device.rawName}.\nYou may need to update the firmware in Zwift Companion app.\nWe found: ${services.joinToString(transform: (s) => s.uuid)}',
);
}
final asyncCharacteristic = customService.characteristics.firstOrNullWhere(

View File

@@ -24,12 +24,15 @@ class ZwiftRide extends BaseDevice {
_lastControllerNotification = clickNotification;
actionStreamInternal.add(clickNotification);
if (clickNotification.buttonShiftDownLeft || clickNotification.buttonShiftUpLeft || clickNotification.buttonZ) {
if (clickNotification.buttonShiftDownLeft ||
clickNotification.buttonShiftUpLeft ||
clickNotification.buttonOnOffLeft ||
clickNotification.buttonPowerUpLeft) {
actionHandler.decreaseGear();
} else if (clickNotification.buttonShiftUpRight ||
clickNotification.buttonShiftDownRight ||
clickNotification.buttonOnOffLeft) {
// TODO remove buttonZ once the assignment is fixed for real
clickNotification.buttonOnOffRight ||
clickNotification.buttonPowerUpRight) {
actionHandler.increaseGear();
}
/*if (clickNotification.buttonA) {

View File

@@ -4,15 +4,13 @@ import '../protocol/zwift.pb.dart';
import 'notification.dart';
class ClickNotification extends BaseNotification {
static const int BTN_PRESSED = 0;
bool buttonUp = false;
bool buttonDown = false;
ClickNotification(Uint8List message) {
final status = ClickKeyPadStatus.fromBuffer(message);
buttonUp = status.buttonPlus.value == BTN_PRESSED;
buttonDown = status.buttonMinus.value == BTN_PRESSED;
buttonUp = status.buttonPlus == PlayButtonStatus.ON;
buttonDown = status.buttonMinus == PlayButtonStatus.ON;
}
@override

View File

@@ -4,21 +4,19 @@ import 'package:swift_control/bluetooth/messages/notification.dart';
import 'package:swift_control/bluetooth/protocol/zwift.pb.dart';
class PlayNotification extends BaseNotification {
static const int BTN_PRESSED = 0;
late bool rightPad, buttonY, buttonZ, buttonA, buttonB, buttonOn, buttonShift;
late int analogLR, analogUD;
PlayNotification(Uint8List message) {
final status = PlayKeyPadStatus.fromBuffer(message);
rightPad = status.rightPad.value == BTN_PRESSED;
buttonY = status.buttonYUp.value == BTN_PRESSED;
buttonZ = status.buttonZLeft.value == BTN_PRESSED;
buttonA = status.buttonARight.value == BTN_PRESSED;
buttonB = status.buttonBDown.value == BTN_PRESSED;
buttonOn = status.buttonOn.value == BTN_PRESSED;
buttonShift = status.buttonShift.value == BTN_PRESSED;
rightPad = status.rightPad == PlayButtonStatus.ON;
buttonY = status.buttonYUp == PlayButtonStatus.ON;
buttonZ = status.buttonZLeft == PlayButtonStatus.ON;
buttonA = status.buttonARight == PlayButtonStatus.ON;
buttonB = status.buttonBDown == PlayButtonStatus.ON;
buttonOn = status.buttonOn == PlayButtonStatus.ON;
buttonShift = status.buttonShift == PlayButtonStatus.ON;
analogLR = status.analogLR;
analogUD = status.analogUD;
}

View File

@@ -36,7 +36,7 @@ class RideNotification extends BaseNotification {
late bool buttonA, buttonB, buttonY, buttonZ;
late bool buttonShiftUpLeft, buttonShiftDownLeft;
late bool buttonShiftUpRight, buttonShiftDownRight;
late bool buttonPowerUpLeft, buttonPowerDownLeft;
late bool buttonPowerUpLeft, buttonPowerUpRight;
late bool buttonOnOffLeft, buttonOnOffRight;
int analogLR = 0, analogUD = 0;
@@ -57,7 +57,7 @@ class RideNotification extends BaseNotification {
buttonShiftUpRight = status.buttonMap & _RideButtonMask.SHFT_UP_R_BTN.mask == BTN_PRESSED;
buttonShiftDownRight = status.buttonMap & _RideButtonMask.SHFT_DN_R_BTN.mask == BTN_PRESSED;
buttonPowerUpLeft = status.buttonMap & _RideButtonMask.POWERUP_L_BTN.mask == BTN_PRESSED;
buttonPowerDownLeft = status.buttonMap & _RideButtonMask.POWERUP_R_BTN.mask == BTN_PRESSED;
buttonPowerUpRight = status.buttonMap & _RideButtonMask.POWERUP_R_BTN.mask == BTN_PRESSED;
buttonOnOffLeft = status.buttonMap & _RideButtonMask.ONOFF_L_BTN.mask == BTN_PRESSED;
buttonOnOffRight = status.buttonMap & _RideButtonMask.ONOFF_R_BTN.mask == BTN_PRESSED;
@@ -86,7 +86,7 @@ class RideNotification extends BaseNotification {
if (buttonShiftUpRight) 'buttonShiftUpRight',
if (buttonShiftDownRight) 'buttonShiftDownRight',
if (buttonPowerUpLeft) 'buttonPowerUpLeft',
if (buttonPowerDownLeft) 'buttonPowerDownLeft',
if (buttonPowerUpRight) 'buttonPowerUpRight',
if (buttonOnOffLeft) 'buttonOnOffLeft',
if (buttonOnOffRight) 'buttonOnOffRight',
];
@@ -111,7 +111,7 @@ class RideNotification extends BaseNotification {
buttonShiftUpRight == other.buttonShiftUpRight &&
buttonShiftDownRight == other.buttonShiftDownRight &&
buttonPowerUpLeft == other.buttonPowerUpLeft &&
buttonPowerDownLeft == other.buttonPowerDownLeft &&
buttonPowerUpRight == other.buttonPowerUpRight &&
buttonOnOffLeft == other.buttonOnOffLeft &&
buttonOnOffRight == other.buttonOnOffRight &&
analogLR == other.analogLR &&
@@ -132,7 +132,7 @@ class RideNotification extends BaseNotification {
buttonShiftUpRight.hashCode ^
buttonShiftDownRight.hashCode ^
buttonPowerUpLeft.hashCode ^
buttonPowerDownLeft.hashCode ^
buttonPowerUpRight.hashCode ^
buttonOnOffLeft.hashCode ^
buttonOnOffRight.hashCode ^
analogLR.hashCode ^

View File

@@ -25,8 +25,8 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage {
PlayButtonStatus? buttonZLeft,
PlayButtonStatus? buttonARight,
PlayButtonStatus? buttonBDown,
PlayButtonStatus? buttonOn,
PlayButtonStatus? buttonShift,
PlayButtonStatus? buttonOn,
$core.int? analogLR,
$core.int? analogUD,
}) {
@@ -46,12 +46,12 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage {
if (buttonBDown != null) {
$result.buttonBDown = buttonBDown;
}
if (buttonOn != null) {
$result.buttonOn = buttonOn;
}
if (buttonShift != null) {
$result.buttonShift = buttonShift;
}
if (buttonOn != null) {
$result.buttonOn = buttonOn;
}
if (analogLR != null) {
$result.analogLR = analogLR;
}
@@ -70,8 +70,8 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage {
..e<PlayButtonStatus>(3, _omitFieldNames ? '' : 'ButtonZLeft', $pb.PbFieldType.OE, protoName: 'Button_Z_Left', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
..e<PlayButtonStatus>(4, _omitFieldNames ? '' : 'ButtonARight', $pb.PbFieldType.OE, protoName: 'Button_A_Right', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
..e<PlayButtonStatus>(5, _omitFieldNames ? '' : 'ButtonBDown', $pb.PbFieldType.OE, protoName: 'Button_B_Down', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
..e<PlayButtonStatus>(6, _omitFieldNames ? '' : 'ButtonOn', $pb.PbFieldType.OE, protoName: 'Button_On', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
..e<PlayButtonStatus>(7, _omitFieldNames ? '' : 'ButtonShift', $pb.PbFieldType.OE, protoName: 'Button_Shift', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
..e<PlayButtonStatus>(6, _omitFieldNames ? '' : 'ButtonShift', $pb.PbFieldType.OE, protoName: 'Button_Shift', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
..e<PlayButtonStatus>(7, _omitFieldNames ? '' : 'ButtonOn', $pb.PbFieldType.OE, protoName: 'Button_On', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values)
..a<$core.int>(8, _omitFieldNames ? '' : 'AnalogLR', $pb.PbFieldType.OS3, protoName: 'Analog_LR')
..a<$core.int>(9, _omitFieldNames ? '' : 'AnalogUD', $pb.PbFieldType.OS3, protoName: 'Analog_UD')
..hasRequiredFields = false
@@ -144,22 +144,22 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage {
void clearButtonBDown() => clearField(5);
@$pb.TagNumber(6)
PlayButtonStatus get buttonOn => $_getN(5);
PlayButtonStatus get buttonShift => $_getN(5);
@$pb.TagNumber(6)
set buttonOn(PlayButtonStatus v) { setField(6, v); }
set buttonShift(PlayButtonStatus v) { setField(6, v); }
@$pb.TagNumber(6)
$core.bool hasButtonOn() => $_has(5);
$core.bool hasButtonShift() => $_has(5);
@$pb.TagNumber(6)
void clearButtonOn() => clearField(6);
void clearButtonShift() => clearField(6);
@$pb.TagNumber(7)
PlayButtonStatus get buttonShift => $_getN(6);
PlayButtonStatus get buttonOn => $_getN(6);
@$pb.TagNumber(7)
set buttonShift(PlayButtonStatus v) { setField(7, v); }
set buttonOn(PlayButtonStatus v) { setField(7, v); }
@$pb.TagNumber(7)
$core.bool hasButtonShift() => $_has(6);
$core.bool hasButtonOn() => $_has(6);
@$pb.TagNumber(7)
void clearButtonShift() => clearField(7);
void clearButtonOn() => clearField(7);
@$pb.TagNumber(8)
$core.int get analogLR => $_getIZ(7);

View File

@@ -82,8 +82,8 @@ const PlayKeyPadStatus$json = {
{'1': 'Button_Z_Left', '3': 3, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonZLeft'},
{'1': 'Button_A_Right', '3': 4, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonARight'},
{'1': 'Button_B_Down', '3': 5, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonBDown'},
{'1': 'Button_On', '3': 6, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonOn'},
{'1': 'Button_Shift', '3': 7, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonShift'},
{'1': 'Button_Shift', '3': 6, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonShift'},
{'1': 'Button_On', '3': 7, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonOn'},
{'1': 'Analog_LR', '3': 8, '4': 1, '5': 17, '10': 'AnalogLR'},
{'1': 'Analog_UD', '3': 9, '4': 1, '5': 17, '10': 'AnalogUD'},
],
@@ -97,9 +97,9 @@ final $typed_data.Uint8List playKeyPadStatusDescriptor = $convert.base64Decode(
'4uZGUuam9uYXNiYXJrLlBsYXlCdXR0b25TdGF0dXNSC0J1dHRvblpMZWZ0EkQKDkJ1dHRvbl9B'
'X1JpZ2h0GAQgASgOMh4uZGUuam9uYXNiYXJrLlBsYXlCdXR0b25TdGF0dXNSDEJ1dHRvbkFSaW'
'dodBJCCg1CdXR0b25fQl9Eb3duGAUgASgOMh4uZGUuam9uYXNiYXJrLlBsYXlCdXR0b25TdGF0'
'dXNSC0J1dHRvbkJEb3duEjsKCUJ1dHRvbl9PbhgGIAEoDjIeLmRlLmpvbmFzYmFyay5QbGF5Qn'
'V0dG9uU3RhdHVzUghCdXR0b25PbhJBCgxCdXR0b25fU2hpZnQYByABKA4yHi5kZS5qb25hc2Jh'
'cmsuUGxheUJ1dHRvblN0YXR1c1ILQnV0dG9uU2hpZnQSGwoJQW5hbG9nX0xSGAggASgRUghBbm'
'dXNSC0J1dHRvbkJEb3duEkEKDEJ1dHRvbl9TaGlmdBgGIAEoDjIeLmRlLmpvbmFzYmFyay5QbG'
'F5QnV0dG9uU3RhdHVzUgtCdXR0b25TaGlmdBI7CglCdXR0b25fT24YByABKA4yHi5kZS5qb25h'
'c2JhcmsuUGxheUJ1dHRvblN0YXR1c1IIQnV0dG9uT24SGwoJQW5hbG9nX0xSGAggASgRUghBbm'
'Fsb2dMUhIbCglBbmFsb2dfVUQYCSABKBFSCEFuYWxvZ1VE');
@$core.Deprecated('Use playCommandParametersDescriptor instead')

View File

@@ -16,8 +16,8 @@ message PlayKeyPadStatus {
optional PlayButtonStatus Button_Z_Left = 3;
optional PlayButtonStatus Button_A_Right = 4;
optional PlayButtonStatus Button_B_Down = 5;
optional PlayButtonStatus Button_On = 6;
optional PlayButtonStatus Button_Shift = 7;
optional PlayButtonStatus Button_Shift = 6;
optional PlayButtonStatus Button_On = 7;
optional sint32 Analog_LR = 8;
optional sint32 Analog_UD = 9;
}

View File

@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/pages/touch_area.dart';
import 'package:swift_control/widgets/logviewer.dart';
import 'package:swift_control/widgets/title.dart';
import '../bluetooth/devices/base_device.dart';
import '../widgets/menu.dart';
@@ -48,7 +49,7 @@ class _DevicePageState extends State<DevicePage> {
},
child: Scaffold(
appBar: AppBar(
title: Text('SwiftControl'),
title: AppTitle(),
actions: buildMenuButtons(),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
@@ -60,7 +61,7 @@ class _DevicePageState extends State<DevicePage> {
children: [
Text(
'Devices:\n${connection.devices.joinToString(separator: '\n', transform: (it) {
return "${it.device.name}: ${it.isConnected ? 'Connected' : 'Not connected'}";
return "${it.device.name ?? it.runtimeType}: ${it.isConnected ? 'Connected' : 'Not connected'}";
})}',
),
Divider(color: Theme.of(context).colorScheme.primary, height: 30),

View File

@@ -7,6 +7,7 @@ import 'package:swift_control/main.dart';
import 'package:swift_control/utils/requirements/multi.dart';
import 'package:swift_control/utils/requirements/platform.dart';
import 'package:swift_control/widgets/menu.dart';
import 'package:swift_control/widgets/title.dart';
import 'device.dart';
@@ -65,7 +66,7 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('SwiftControl'),
title: AppTitle(),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: buildMenuButtons(),
),

View File

@@ -30,7 +30,9 @@ class _ScanWidgetState extends State<ScanWidget> {
WidgetsBinding.instance.addPostFrameCallback((_) {
// must be called from a button
if (!kIsWeb) {
connection.performScanning();
Future.delayed(Duration(seconds: 1)).then((_) {
connection.performScanning();
});
}
});
}

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:swift_control/main.dart';
@@ -35,10 +36,18 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
Navigator.of(context).pop();
}
@override
void dispose() {
super.dispose();
// Exit full screen
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
}
@override
void initState() {
super.initState();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky, overlays: []);
WidgetsBinding.instance.addPostFrameCallback((_) {
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
@@ -90,35 +99,38 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
Positioned.fill(child: Opacity(opacity: 0.5, child: Image.file(_backgroundImage!, fit: BoxFit.cover)))
else
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Text('''1. Create an in-game screenshot of your app (e.g. within MyWhoosh)
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Text('''1. Create an in-game screenshot of your app (e.g. within MyWhoosh)
2. Load the screenshot with the button below
3. Make sure the app is in the correct orientation (portrait or landscape)
4. Drag the touch areas to the correct position where the gear up / down buttons are located
5. Save and close this screen'''),
ElevatedButton(
onPressed: () {
_pickScreenshot();
},
child: Text('Load in-game screenshot for placement'),
),
],
ElevatedButton(
onPressed: () {
_pickScreenshot();
},
child: Text('Load in-game screenshot for placement'),
),
],
),
),
),
// Touch Areas
_buildDraggableArea(
position: _gearUpPos,
onPositionChanged: (newPos) => _gearUpPos = newPos,
color: Colors.green,
color: Colors.red,
label: "Gear ↑",
),
_buildDraggableArea(
position: _gearDownPos,
onPositionChanged: (newPos) => _gearDownPos = newPos,
color: Colors.red,
color: Colors.green,
label: "Gear ↓",
),
Positioned(

View File

@@ -36,14 +36,18 @@ class Keymap {
];
}
static Keymap decode(List<String> data) {
static Keymap? decode(List<String> data) {
// decode from preferences
if (data.length < 4) {
return custom;
return null;
}
final name = data[0];
final keymap = values.firstWhere((element) => element.name == name, orElse: () => custom);
final keymap = values.firstOrNullWhere((element) => element.name == name);
if (keymap == null) {
return null;
}
if (keymap.name != custom.name) {
return keymap;
@@ -58,8 +62,10 @@ class Keymap {
physicalKey: PhysicalKeyboardKey(int.parse(data[4])),
logicalKey: LogicalKeyboardKey(int.parse(data[3])),
);
return keymap;
} else {
return null;
}
return keymap;
}
}

94
lib/widgets/title.dart Normal file
View File

@@ -0,0 +1,94 @@
import 'dart:convert';
import 'dart:io';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:swift_control/widgets/small_progress_indicator.dart';
import 'package:url_launcher/url_launcher_string.dart';
String? _latestVersionUrlValue;
PackageInfo? _packageInfoValue;
class AppTitle extends StatefulWidget {
const AppTitle({super.key});
@override
State<AppTitle> createState() => _AppTitleState();
}
class _AppTitleState extends State<AppTitle> {
Future<String?> getLatestVersionUrlIfNewer() async {
final response = await http.get(Uri.parse('https://api.github.com/repos/jonasbark/swiftcontrol/releases/latest'));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final latestVersion = data['tag_name'].split('+').first;
final currentVersion = 'v${_packageInfoValue!.version}';
if (latestVersion != null && latestVersion != currentVersion) {
final assets = data['assets'] as List;
if (Platform.isAndroid) {
final apkUrl = assets.firstOrNullWhere((asset) => asset['name'].endsWith('.apk'))['browser_download_url'];
return apkUrl;
} else if (Platform.isMacOS) {
final dmgUrl =
assets.firstOrNullWhere((asset) => asset['name'].endsWith('.macos.zip'))['browser_download_url'];
return dmgUrl;
} else if (Platform.isWindows) {
final appImageUrl =
assets.firstOrNullWhere((asset) => asset['name'].endsWith('.windows.zip'))['browser_download_url'];
return appImageUrl;
}
}
}
return null;
}
@override
void initState() {
super.initState();
if (_packageInfoValue == null) {
PackageInfo.fromPlatform().then((value) {
setState(() {
_packageInfoValue = value;
});
_loadLatestVersionUrl();
});
} else {
_loadLatestVersionUrl();
}
}
void _loadLatestVersionUrl() async {
if (_latestVersionUrlValue == null && !kIsWeb) {
final url = await getLatestVersionUrlIfNewer();
if (url != null && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('New version available: ${url.split("/").takeLast(2).first.split('%').first}'),
duration: Duration(seconds: 1337),
action: SnackBarAction(
label: 'Download',
onPressed: () {
launchUrlString(url);
},
),
),
);
}
}
}
@override
Widget build(BuildContext context) {
return Row(
spacing: 8,
children: [
Text('SwiftControl'),
if (_packageInfoValue != null) Text('v${_packageInfoValue!.version}') else SmallProgressIndicator(),
],
);
}
}

View File

@@ -8,6 +8,7 @@ import Foundation
import file_selector_macos
import flutter_local_notifications
import keypress_simulator_macos
import package_info_plus
import shared_preferences_foundation
import universal_ble
import url_launcher_macos
@@ -16,6 +17,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
KeypressSimulatorMacosPlugin.register(with: registry.registrar(forPlugin: "KeypressSimulatorMacosPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View File

@@ -6,6 +6,8 @@ PODS:
- FlutterMacOS (1.0.0)
- keypress_simulator_macos (0.0.1):
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -20,6 +22,7 @@ DEPENDENCIES:
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- keypress_simulator_macos (from `Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- universal_ble (from `Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
@@ -33,6 +36,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral
keypress_simulator_macos:
:path: Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
universal_ble:
@@ -45,6 +50,7 @@ SPEC CHECKSUMS:
flutter_local_notifications: 4ccab5b7a22835214a6672e3f9c5e8ae207dab36
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
keypress_simulator_macos: f8556f9101f9f2f175652e0bceddf0fe82a4c6b2
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404

View File

@@ -12,5 +12,7 @@
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -1,5 +1,10 @@
<?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/>
<dict>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -288,7 +288,7 @@ packages:
source: sdk
version: "0.0.0"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
@@ -408,12 +408,11 @@ packages:
source: hosted
version: "0.2.0"
keypress_simulator_windows:
dependency: transitive
dependency: "direct overridden"
description:
name: keypress_simulator_windows
sha256: b4ff055131a2e5ea920eb3b6a185e1889fe00749b027df3b83aa726ed590a9b5
url: "https://pub.dev"
source: hosted
path: "keypress_simulator/packages/keypress_simulator_windows"
relative: true
source: path
version: "0.2.0"
leak_tracker:
dependency: transitive
@@ -487,6 +486,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
url: "https://pub.dev"
source: hosted
version: "8.3.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
path:
dependency: transitive
description:
@@ -852,6 +867,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
url: "https://pub.dev"
source: hosted
version: "5.12.0"
xdg_directories:
dependency: transitive
description:

View File

@@ -1,7 +1,7 @@
name: swift_control
description: "SwiftControl - Control your virtual riding"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.1.2+0
version: 1.1.9+0
environment:
sdk: ^3.7.0
@@ -18,11 +18,17 @@ dependencies:
dartx: any
image_picker: ^1.1.2
pointycastle: any
keypress_simulator: ^0.2.0
keypress_simulator: any
shared_preferences: ^2.5.3
flex_color_scheme: ^8.2.0
package_info_plus: ^8.3.0
accessibility:
path: accessibility
http: ^1.3.0
dependency_overrides:
keypress_simulator_windows:
path: keypress_simulator/packages/keypress_simulator_windows
dev_dependencies:
flutter_test: