mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c0e027abb | ||
|
|
0dcb666bbd | ||
|
|
00ebca7a01 | ||
|
|
f9c8820303 | ||
|
|
3d414edda9 | ||
|
|
0e3f6f1d5e | ||
|
|
c4b0ef38c0 | ||
|
|
2e800bb2de | ||
|
|
505b970497 | ||
|
|
986bfd481c | ||
|
|
ab8d480a01 | ||
|
|
7e19b76403 | ||
|
|
cbc2f103ac | ||
|
|
6a7922cf17 | ||
|
|
8e23de2718 | ||
|
|
11aec5fba1 | ||
|
|
c98f213e2e | ||
|
|
8c11cfcad6 | ||
|
|
5fe88ffc6a | ||
|
|
7a8c7a4ee1 | ||
|
|
3343325195 | ||
|
|
edda16dc06 | ||
|
|
7a3d120123 | ||
|
|
92419c9182 | ||
|
|
68bb5bf371 | ||
|
|
b0d8bfcadd | ||
|
|
a58ad1daf6 |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,4 +1,26 @@
|
||||
### 1.1.0 (2025-03-30)
|
||||
### 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
|
||||
|
||||
### 1.1.1 (2025-03-30)
|
||||
- potential fix for Bluetooth device detection
|
||||
|
||||
### 1.1.0 (2025-03-30)
|
||||
|
||||
@@ -30,7 +30,7 @@ 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)
|
||||
|
||||
## How does it work?
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
|
||||
12
keypress_simulator/.clang-format
Normal file
12
keypress_simulator/.clang-format
Normal 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
|
||||
1
keypress_simulator/.github/FUNDING.yml
vendored
Normal file
1
keypress_simulator/.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
liberapay: lijy91
|
||||
50
keypress_simulator/.github/workflows/build.yml
vendored
Normal file
50
keypress_simulator/.github/workflows/build.yml
vendored
Normal 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
|
||||
31
keypress_simulator/.github/workflows/lint.yml
vendored
Normal file
31
keypress_simulator/.github/workflows/lint.yml
vendored
Normal 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
|
||||
20
keypress_simulator/.github/workflows/test.yml
vendored
Normal file
20
keypress_simulator/.github/workflows/test.yml
vendored
Normal 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
6
keypress_simulator/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.dart_tool/
|
||||
.idea/
|
||||
|
||||
*.iml
|
||||
pubspec_overrides.yaml
|
||||
pubspec.lock
|
||||
21
keypress_simulator/LICENSE
Normal file
21
keypress_simulator/LICENSE
Normal 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.
|
||||
93
keypress_simulator/README-ZH.md
Normal file
93
keypress_simulator/README-ZH.md
Normal 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)
|
||||
93
keypress_simulator/README.md
Normal file
93
keypress_simulator/README.md
Normal 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)
|
||||
35
keypress_simulator/melos.yaml
Normal file
35
keypress_simulator/melos.yaml
Normal 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.
|
||||
29
keypress_simulator/packages/keypress_simulator_windows/.gitignore
vendored
Normal file
29
keypress_simulator/packages/keypress_simulator_windows/.gitignore
vendored
Normal 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/
|
||||
@@ -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'
|
||||
@@ -0,0 +1,3 @@
|
||||
## 0.2.0
|
||||
|
||||
* First release.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
include: package:mostly_reasonable_lints/flutter.yaml
|
||||
@@ -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
|
||||
17
keypress_simulator/packages/keypress_simulator_windows/windows/.gitignore
vendored
Normal file
17
keypress_simulator/packages/keypress_simulator_windows/windows/.gitignore
vendored
Normal 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/
|
||||
@@ -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()
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
9
keypress_simulator/pubspec.yaml
Normal file
9
keypress_simulator/pubspec.yaml
Normal 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
|
||||
@@ -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,7 +34,7 @@ class Connection {
|
||||
UniversalBle.onScanResult = (result) {
|
||||
if (_lastScanResult.none((e) => e.deviceId == result.deviceId)) {
|
||||
_lastScanResult.add(result);
|
||||
_actionStreams.add(LogNotification('Found new devices: ${result.name}'));
|
||||
_actionStreams.add(LogNotification('Found new device: ${result.name}'));
|
||||
final scanResult = BaseDevice.fromScanResult(result);
|
||||
if (scanResult != null) {
|
||||
_addDevices([scanResult]);
|
||||
@@ -53,11 +56,12 @@ class Connection {
|
||||
Future<void> performScanning() async {
|
||||
isScanning.value = true;
|
||||
|
||||
if (!kIsWeb) {
|
||||
// does not work on web, may not work on Windows
|
||||
if (!kIsWeb && !Platform.isWindows) {
|
||||
UniversalBle.getSystemDevices(
|
||||
withServices: [BleUuid.ZWIFT_CUSTOM_SERVICE_UUID, BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID],
|
||||
).then((devices) {
|
||||
final baseDevices = devices.map((device) => BaseDevice.fromScanResult(device)).whereNotNull().toList();
|
||||
).then((devices) async {
|
||||
final baseDevices = devices.mapNotNull(BaseDevice.fromScanResult).toList();
|
||||
if (baseDevices.isNotEmpty) {
|
||||
_addDevices(baseDevices);
|
||||
}
|
||||
@@ -80,9 +84,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) {
|
||||
@@ -94,6 +97,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) {
|
||||
|
||||
@@ -28,24 +28,32 @@ abstract class BaseDevice {
|
||||
String get customServiceId => BleUuid.ZWIFT_CUSTOM_SERVICE_UUID;
|
||||
|
||||
static BaseDevice? fromScanResult(BleDevice scanResult) {
|
||||
final manufacturerData = scanResult.manufacturerDataList;
|
||||
final data = manufacturerData.firstOrNullWhere((e) => e.companyId == Constants.ZWIFT_MANUFACTURER_ID)?.payload;
|
||||
// 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), 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,
|
||||
};
|
||||
|
||||
// Web does not support manufacturer data, also the "system devices" don't return any, so use name fallback
|
||||
if (data == null || data.isEmpty) {
|
||||
return switch (scanResult.name) {
|
||||
'Zwift Ride' => ZwiftRide(scanResult),
|
||||
'Zwift Play' => ZwiftPlay(scanResult),
|
||||
'Zwift Click' => ZwiftClick(scanResult),
|
||||
_ => 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;
|
||||
|
||||
if (data == null || data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final type = DeviceType.fromManufacturerData(data.first);
|
||||
return switch (type) {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ enum _RideButtonMask {
|
||||
SHFT_DN_R_BTN(0x02000),
|
||||
|
||||
POWERUP_L_BTN(0x00400),
|
||||
ONOFF_L_BTN(0x01000),
|
||||
POWERUP_R_BTN(0x04000),
|
||||
ONOFF_R_BTN(0x20000);
|
||||
ONOFF_L_BTN(0x00800),
|
||||
ONOFF_R_BTN(0x08000);
|
||||
|
||||
final int mask;
|
||||
|
||||
@@ -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 ^
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -19,8 +19,8 @@ class DesktopActions extends BaseActions {
|
||||
if (keymap == null) {
|
||||
throw Exception('Keymap is not set');
|
||||
}
|
||||
await keyPressSimulator.simulateKeyDown(_keymap!.decrease);
|
||||
await keyPressSimulator.simulateKeyUp(_keymap!.decrease);
|
||||
await keyPressSimulator.simulateKeyDown(_keymap!.decrease?.physicalKey);
|
||||
await keyPressSimulator.simulateKeyUp(_keymap!.decrease?.physicalKey);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -28,7 +28,7 @@ class DesktopActions extends BaseActions {
|
||||
if (keymap == null) {
|
||||
throw Exception('Keymap is not set');
|
||||
}
|
||||
await keyPressSimulator.simulateKeyDown(_keymap!.increase);
|
||||
await keyPressSimulator.simulateKeyUp(_keymap!.increase);
|
||||
await keyPressSimulator.simulateKeyDown(_keymap!.increase?.physicalKey);
|
||||
await keyPressSimulator.simulateKeyUp(_keymap!.increase?.physicalKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class Keymap {
|
||||
static Keymap myWhoosh = Keymap('MyWhoosh', increase: PhysicalKeyboardKey.keyK, decrease: PhysicalKeyboardKey.keyI);
|
||||
static Keymap myWhoosh = Keymap(
|
||||
'MyWhoosh',
|
||||
increase: KeyPair(physicalKey: PhysicalKeyboardKey.keyK, logicalKey: LogicalKeyboardKey.keyK),
|
||||
decrease: KeyPair(physicalKey: PhysicalKeyboardKey.keyI, logicalKey: LogicalKeyboardKey.keyI),
|
||||
);
|
||||
static Keymap custom = Keymap('Custom', increase: null, decrease: null);
|
||||
|
||||
static List<Keymap> values = [myWhoosh, custom];
|
||||
|
||||
PhysicalKeyboardKey? increase;
|
||||
PhysicalKeyboardKey? decrease;
|
||||
KeyPair? increase;
|
||||
KeyPair? decrease;
|
||||
final String name;
|
||||
|
||||
Keymap(this.name, {required this.increase, required this.decrease});
|
||||
@@ -17,25 +22,56 @@ class Keymap {
|
||||
if (increase == null && decrease == null) {
|
||||
return name;
|
||||
}
|
||||
return "$name: ${increase?.debugName} + ${decrease?.debugName}";
|
||||
return "$name: ${increase?.logicalKey.keyLabel} + ${decrease?.logicalKey.keyLabel}";
|
||||
}
|
||||
|
||||
List<String> encode() {
|
||||
// encode to save in preferences
|
||||
return [name, increase?.usbHidUsage.toString() ?? '', decrease?.usbHidUsage.toString() ?? ''];
|
||||
return [
|
||||
name,
|
||||
increase?.logicalKey.keyId.toString() ?? '',
|
||||
increase?.physicalKey.usbHidUsage.toString() ?? '',
|
||||
decrease?.logicalKey.keyId.toString() ?? '',
|
||||
decrease?.physicalKey.usbHidUsage.toString() ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
static Keymap decode(List<String> data) {
|
||||
static Keymap? decode(List<String> data) {
|
||||
// decode from preferences
|
||||
|
||||
if (data.length < 4) {
|
||||
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;
|
||||
}
|
||||
|
||||
keymap.increase = data[1].isNotEmpty ? PhysicalKeyboardKey(int.parse(data[1])) : null;
|
||||
keymap.decrease = data[2].isNotEmpty ? PhysicalKeyboardKey(int.parse(data[2])) : null;
|
||||
return keymap;
|
||||
if (data.sublist(1).all((e) => e.isNotEmpty)) {
|
||||
keymap.increase = KeyPair(
|
||||
physicalKey: PhysicalKeyboardKey(int.parse(data[2])),
|
||||
logicalKey: LogicalKeyboardKey(int.parse(data[1])),
|
||||
);
|
||||
keymap.decrease = KeyPair(
|
||||
physicalKey: PhysicalKeyboardKey(int.parse(data[4])),
|
||||
logicalKey: LogicalKeyboardKey(int.parse(data[3])),
|
||||
);
|
||||
return keymap;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class KeyPair {
|
||||
final PhysicalKeyboardKey physicalKey;
|
||||
final LogicalKeyboardKey logicalKey;
|
||||
|
||||
KeyPair({required this.physicalKey, required this.logicalKey});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:keypress_simulator/keypress_simulator.dart';
|
||||
import 'package:swift_control/pages/scan.dart';
|
||||
@@ -35,15 +38,25 @@ class KeymapRequirement extends PlatformRequirement {
|
||||
|
||||
@override
|
||||
Widget? build(BuildContext context, VoidCallback onUpdate) {
|
||||
final controller = TextEditingController(text: actionHandler.keymap?.name);
|
||||
return DropdownMenu<Keymap>(
|
||||
controller: controller,
|
||||
dropdownMenuEntries:
|
||||
Keymap.values.map((key) => DropdownMenuEntry<Keymap>(value: key, label: key.toString())).toList(),
|
||||
onSelected: (keymap) async {
|
||||
if (keymap!.name == Keymap.custom.name) {
|
||||
keymap = await showCustomKeymapDialog(context, keymap: keymap);
|
||||
} else if (keymap.name == Keymap.myWhoosh.name && (!kIsWeb && Platform.isWindows)) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Use a Custom Keymap if you experience any issues on Windows')));
|
||||
}
|
||||
controller.text = keymap?.name ?? '';
|
||||
if (keymap == null) {
|
||||
return;
|
||||
}
|
||||
actionHandler.init(keymap);
|
||||
settings.setKeymap(keymap!);
|
||||
settings.setKeymap(keymap);
|
||||
onUpdate();
|
||||
},
|
||||
initialSelection: actionHandler.keymap,
|
||||
|
||||
@@ -21,9 +21,9 @@ class GearHotkeyDialog extends StatefulWidget {
|
||||
|
||||
class _GearHotkeyDialogState extends State<GearHotkeyDialog> {
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
final Set<PhysicalKeyboardKey> _pressedKeys = {};
|
||||
Set<PhysicalKeyboardKey>? _gearUpHotkey;
|
||||
Set<PhysicalKeyboardKey>? _gearDownHotkey;
|
||||
KeyDownEvent? _pressedKey;
|
||||
KeyDownEvent? _gearUpHotkey;
|
||||
KeyDownEvent? _gearDownHotkey;
|
||||
|
||||
String _mode = 'up'; // 'up' or 'down'
|
||||
|
||||
@@ -36,27 +36,32 @@ class _GearHotkeyDialogState extends State<GearHotkeyDialog> {
|
||||
void _onKey(KeyEvent event) {
|
||||
setState(() {
|
||||
if (event is KeyDownEvent) {
|
||||
_pressedKeys.add(event.physicalKey);
|
||||
_pressedKey = event;
|
||||
} else if (event is KeyUpEvent) {
|
||||
if (_pressedKeys.isNotEmpty) {
|
||||
if (_pressedKey != null) {
|
||||
if (_mode == 'up') {
|
||||
_gearUpHotkey = {..._pressedKeys};
|
||||
_gearUpHotkey = _pressedKey;
|
||||
_mode = 'down';
|
||||
} else {
|
||||
_gearDownHotkey = {..._pressedKeys};
|
||||
widget.keymap.increase = _gearUpHotkey!.first;
|
||||
widget.keymap.decrease = _gearDownHotkey!.first;
|
||||
_gearDownHotkey = _pressedKey;
|
||||
widget.keymap.increase = KeyPair(
|
||||
physicalKey: _gearUpHotkey!.physicalKey,
|
||||
logicalKey: _gearUpHotkey!.logicalKey,
|
||||
);
|
||||
widget.keymap.decrease = KeyPair(
|
||||
physicalKey: _gearDownHotkey!.physicalKey,
|
||||
logicalKey: _gearDownHotkey!.logicalKey,
|
||||
);
|
||||
Navigator.of(context).pop(widget.keymap);
|
||||
}
|
||||
_pressedKeys.clear();
|
||||
_pressedKey = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String _formatKeys(Set<PhysicalKeyboardKey>? keys) {
|
||||
if (keys == null || keys.isEmpty) return 'Not set';
|
||||
return keys.map((k) => k.debugName ?? k).join(' + ');
|
||||
String _formatKey(KeyDownEvent? key) {
|
||||
return key?.logicalKey.keyLabel ?? 'Not set';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -76,18 +81,13 @@ class _GearHotkeyDialogState extends State<GearHotkeyDialog> {
|
||||
ListTile(
|
||||
leading: Icon(Icons.arrow_upward),
|
||||
title: Text("Gear Up Hotkey"),
|
||||
subtitle: Text(_formatKeys(_gearUpHotkey)),
|
||||
subtitle: Text(_formatKey(_gearUpHotkey)),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.arrow_downward),
|
||||
title: Text("Gear Down Hotkey"),
|
||||
subtitle: Text(_formatKeys(_gearDownHotkey)),
|
||||
subtitle: Text(_formatKey(_gearDownHotkey)),
|
||||
),
|
||||
if (_pressedKeys.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text("Recording: ${_formatKeys(_pressedKeys)}", style: TextStyle(color: Colors.blue)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
94
lib/widgets/title.dart
Normal file
94
lib/widgets/title.dart
Normal 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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
35
pubspec.lock
35
pubspec.lock
@@ -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:
|
||||
|
||||
10
pubspec.yaml
10
pubspec.yaml
@@ -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.1+0
|
||||
version: 1.1.8+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:
|
||||
|
||||
Reference in New Issue
Block a user