mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Merge branch 'web'
This commit is contained in:
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -151,6 +151,13 @@ jobs:
|
||||
- name: Web Deploy
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
- name: Extract latest changelog
|
||||
id: changelog
|
||||
run: |
|
||||
chmod +x scripts/get_latest_changelog.sh
|
||||
mkdir -p whatsnew
|
||||
./scripts/get_latest_changelog.sh > whatsnew/whatsnew-en-US
|
||||
|
||||
- name: Upload to Play Store
|
||||
# only upload when env.VERSION does not end with 1337, which is our indicator for beta releases
|
||||
if: "!endsWith(env.VERSION, '1337')"
|
||||
@@ -160,6 +167,7 @@ jobs:
|
||||
packageName: de.jonasbark.swiftcontrol
|
||||
releaseFiles: build/app/outputs/bundle/release/app-release.aab
|
||||
track: production
|
||||
whatsNewDirectory: whatsnew
|
||||
|
||||
windows:
|
||||
needs: build
|
||||
|
||||
49
.github/workflows/web.yml
vendored
Normal file
49
.github/workflows/web.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: "Build"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- web
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- 'lib/**'
|
||||
- 'accessibility/**'
|
||||
- 'keypress_simulator/**'
|
||||
- 'pubspec.yaml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Release
|
||||
runs-on: macos-latest
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
pages: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
#1 Checkout Repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
#3 Setup Flutter
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
|
||||
#4 Install Dependencies
|
||||
- name: Install Dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Web
|
||||
run: flutter build web --release --base-href "/swiftcontrol/"
|
||||
|
||||
- name: Upload static files as artifact
|
||||
id: deployment
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: build/web
|
||||
|
||||
- name: Web Deploy
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -1,7 +1,10 @@
|
||||
### 2.6.0 (2025-09-28)
|
||||
- Fix crashes on some Android devices
|
||||
- refactor touch placements: show touches on screen, fix misplaced coordinates - should fix #64
|
||||
- show firmware version of connected device
|
||||
- Fix crashes on some Android devices
|
||||
- warn the user how to make Zwift Click V2 work properly
|
||||
- many UI improvements
|
||||
- add setting to enable or disable vibration on button press for Zwift Ride and Zwift Play controllers
|
||||
|
||||
### 2.5.0 (2025-09-25)
|
||||
- Improve usability
|
||||
|
||||
@@ -4,6 +4,9 @@ class BleUuid {
|
||||
static final DEVICE_INFORMATION_SERVICE_UUID = "0000180a-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION =
|
||||
"00002a26-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
|
||||
static final DEVICE_BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final DEVICE_INFORMATION_CHARACTERISTIC_BATTERY_LEVEL = "00002a19-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final ZWIFT_CUSTOM_SERVICE_UUID = "00000001-19CA-4651-86E5-FA29DCDD09D1".toLowerCase();
|
||||
static final ZWIFT_RIDE_CUSTOM_SERVICE_UUID = "0000fc82-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final ZWIFT_ASYNC_CHARACTERISTIC_UUID = "00000002-19CA-4651-86E5-FA29DCDD09D1".toLowerCase();
|
||||
|
||||
@@ -3,6 +3,9 @@ import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/actions/android.dart';
|
||||
import 'package:swift_control/utils/requirements/android.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
@@ -12,7 +15,7 @@ import 'messages/notification.dart';
|
||||
|
||||
class Connection {
|
||||
final devices = <BaseDevice>[];
|
||||
var androidNotificationsSetup = false;
|
||||
var _androidNotificationsSetup = false;
|
||||
|
||||
final _connectionQueue = <BaseDevice>[];
|
||||
var _handlingConnectionQueue = false;
|
||||
@@ -95,8 +98,8 @@ class Connection {
|
||||
_handleConnectionQueue();
|
||||
|
||||
hasDevices.value = devices.isNotEmpty;
|
||||
if (devices.isNotEmpty && !androidNotificationsSetup && !kIsWeb && Platform.isAndroid) {
|
||||
androidNotificationsSetup = true;
|
||||
if (devices.isNotEmpty && !_androidNotificationsSetup && !kIsWeb && Platform.isAndroid) {
|
||||
_androidNotificationsSetup = true;
|
||||
NotificationRequirement.setup().catchError((e) {
|
||||
_actionStreams.add(LogNotification(e.toString()));
|
||||
});
|
||||
@@ -165,6 +168,10 @@ class Connection {
|
||||
|
||||
void reset() {
|
||||
_actionStreams.add(LogNotification('Disconnecting all devices'));
|
||||
if (actionHandler is AndroidActions) {
|
||||
AndroidFlutterLocalNotificationsPlugin().stopForegroundService();
|
||||
_androidNotificationsSetup = false;
|
||||
}
|
||||
UniversalBle.stopScan();
|
||||
isScanning.value = false;
|
||||
for (var device in devices) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -43,13 +42,21 @@ abstract class BaseDevice {
|
||||
|
||||
static BaseDevice? fromScanResult(BleDevice scanResult) {
|
||||
// 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), special case for Zwift Click v2: we must only connect to the left controller
|
||||
_ => null,
|
||||
};
|
||||
final device =
|
||||
kIsWeb
|
||||
? switch (scanResult.name) {
|
||||
'Zwift Ride' => ZwiftRide(scanResult),
|
||||
'Zwift Play' => ZwiftPlay(scanResult),
|
||||
'Zwift Click' => ZwiftClickV2(scanResult),
|
||||
_ => null,
|
||||
}
|
||||
: 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), special case for Zwift Click v2: we must only connect to the left controller
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (device != null) {
|
||||
return device;
|
||||
@@ -101,7 +108,7 @@ abstract class BaseDevice {
|
||||
|
||||
await UniversalBle.connect(device.deviceId);
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
if (!kIsWeb) {
|
||||
await UniversalBle.requestMtu(device.deviceId, 517);
|
||||
}
|
||||
|
||||
@@ -151,10 +158,10 @@ abstract class BaseDevice {
|
||||
await UniversalBle.subscribeNotifications(device.deviceId, customService.uuid, asyncCharacteristic.uuid);
|
||||
await UniversalBle.subscribeIndications(device.deviceId, customService.uuid, syncTxCharacteristic.uuid);
|
||||
|
||||
await _setupHandshake();
|
||||
await setupHandshake();
|
||||
}
|
||||
|
||||
Future<void> _setupHandshake() async {
|
||||
Future<void> setupHandshake() async {
|
||||
if (supportsEncryption) {
|
||||
await UniversalBle.write(
|
||||
device.deviceId,
|
||||
@@ -181,10 +188,9 @@ abstract class BaseDevice {
|
||||
Future<void> processCharacteristic(String characteristic, Uint8List bytes) async {
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
'${DateTime.now().toString().split(" ").last} Received $characteristic: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')} => ${String.fromCharCodes(bytes)} ',
|
||||
"${DateTime.now().toString().split(" ").last} Received data on $characteristic: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}",
|
||||
);
|
||||
}
|
||||
|
||||
if (bytes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
@@ -192,27 +198,8 @@ abstract class BaseDevice {
|
||||
try {
|
||||
if (bytes.startsWith(startCommand)) {
|
||||
_processDevicePublicKeyResponse(bytes);
|
||||
} else if (bytes.startsWith(Constants.RIDE_ON)) {
|
||||
//print("Empty RideOn response - unencrypted mode");
|
||||
if (this is ZwiftClickV2) {
|
||||
// TODO figure out if this is the key to make V2 work
|
||||
await UniversalBle.write(
|
||||
device.deviceId,
|
||||
customServiceId,
|
||||
syncRxCharacteristic!.uuid,
|
||||
Uint8List.fromList([0x00, 0x08, 0x00]),
|
||||
withoutResponse: true,
|
||||
);
|
||||
await UniversalBle.write(
|
||||
device.deviceId,
|
||||
customServiceId,
|
||||
syncRxCharacteristic!.uuid,
|
||||
Uint8List.fromList([0x41, 0x05, 0x05]),
|
||||
withoutResponse: true,
|
||||
);
|
||||
}
|
||||
} else if (!supportsEncryption || (bytes.length > Int32List.bytesPerElement + EncryptionUtils.MAC_LENGTH)) {
|
||||
_processData(bytes);
|
||||
processData(bytes);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
print("Error processing data: $e");
|
||||
@@ -233,7 +220,7 @@ abstract class BaseDevice {
|
||||
zapEncryption.initialise(devicePublicKeyBytes);
|
||||
}
|
||||
|
||||
Future<void> _processData(Uint8List bytes) async {
|
||||
Future<void> processData(Uint8List bytes) async {
|
||||
int type;
|
||||
Uint8List message;
|
||||
|
||||
@@ -258,59 +245,7 @@ abstract class BaseDevice {
|
||||
message = bytes.sublist(1);
|
||||
}
|
||||
|
||||
if (bytes.startsWith(Constants.RESPONSE_STOPPED_CLICK_V2)) {
|
||||
print('no more events');
|
||||
//connect();
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case Constants.UNKNOWN_CLICKV2_TYPE:
|
||||
if (!_isInited) {
|
||||
_isInited = true;
|
||||
final commands = [
|
||||
[0x00, 0x08, 0x80, 0x08],
|
||||
[0x00, 0x08, 0x83, 0x06],
|
||||
[
|
||||
0xFF,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0A,
|
||||
0x15,
|
||||
0x40,
|
||||
0xE9,
|
||||
0xD9,
|
||||
0xC9,
|
||||
0x6B,
|
||||
0x74,
|
||||
0x63,
|
||||
0xC2,
|
||||
0x7F,
|
||||
0x1B,
|
||||
0x4E,
|
||||
0x4D,
|
||||
0x9F,
|
||||
0x1C,
|
||||
0xB1,
|
||||
0x20,
|
||||
0x5D,
|
||||
0x88,
|
||||
0x2E,
|
||||
0xD7,
|
||||
0xCE,
|
||||
],
|
||||
];
|
||||
for (final command in commands) {
|
||||
await UniversalBle.write(
|
||||
device.deviceId,
|
||||
customServiceId,
|
||||
syncRxCharacteristic!.uuid,
|
||||
Uint8List.fromList(command),
|
||||
withoutResponse: true,
|
||||
);
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Constants.EMPTY_MESSAGE_TYPE:
|
||||
//print("Empty Message"); // expected when nothing happening
|
||||
break;
|
||||
@@ -322,47 +257,10 @@ abstract class BaseDevice {
|
||||
break;
|
||||
case Constants.CLICK_NOTIFICATION_MESSAGE_TYPE:
|
||||
case Constants.PLAY_NOTIFICATION_MESSAGE_TYPE:
|
||||
case Constants.RIDE_NOTIFICATION_MESSAGE_TYPE: // untested
|
||||
case Constants.RIDE_NOTIFICATION_MESSAGE_TYPE:
|
||||
processClickNotification(message)
|
||||
.then((buttonsClicked) async {
|
||||
if (buttonsClicked == null) {
|
||||
// ignore, no changes
|
||||
} else if (buttonsClicked.isEmpty) {
|
||||
actionStreamInternal.add(LogNotification('Buttons released'));
|
||||
_longPressTimer?.cancel();
|
||||
|
||||
// Handle release events for long press keys
|
||||
final buttonsReleased = _previouslyPressedButtons.toList();
|
||||
if (buttonsReleased.isNotEmpty) {
|
||||
await _performRelease(buttonsReleased);
|
||||
}
|
||||
_previouslyPressedButtons.clear();
|
||||
} else {
|
||||
// Handle release events for buttons that are no longer pressed
|
||||
final buttonsReleased = _previouslyPressedButtons.difference(buttonsClicked.toSet()).toList();
|
||||
if (buttonsReleased.isNotEmpty) {
|
||||
await _performRelease(buttonsReleased);
|
||||
}
|
||||
|
||||
final isLongPress =
|
||||
buttonsClicked.singleOrNull != null &&
|
||||
actionHandler.supportedApp?.keymap.getKeyPair(buttonsClicked.single)?.isLongPress == true;
|
||||
|
||||
if (!isLongPress &&
|
||||
!(buttonsClicked.singleOrNull == ZwiftButton.onOffLeft ||
|
||||
buttonsClicked.singleOrNull == ZwiftButton.onOffRight)) {
|
||||
// we don't want to trigger the long press timer for the on/off buttons, also not when it's a long press key
|
||||
_longPressTimer?.cancel();
|
||||
_longPressTimer = Timer.periodic(const Duration(milliseconds: 250), (timer) async {
|
||||
_performActions(buttonsClicked, true);
|
||||
});
|
||||
} else if (isLongPress) {
|
||||
// Update currently pressed buttons
|
||||
_previouslyPressedButtons = buttonsClicked.toSet();
|
||||
}
|
||||
|
||||
return _performActions(buttonsClicked, false);
|
||||
}
|
||||
return handleButtonsClicked(buttonsClicked);
|
||||
})
|
||||
.catchError((e) {
|
||||
actionStreamInternal.add(LogNotification(e.toString()));
|
||||
@@ -373,9 +271,51 @@ abstract class BaseDevice {
|
||||
|
||||
Future<List<ZwiftButton>?> processClickNotification(Uint8List message);
|
||||
|
||||
Future<void> handleButtonsClicked(List<ZwiftButton>? buttonsClicked) async {
|
||||
if (buttonsClicked == null) {
|
||||
// ignore, no changes
|
||||
} else if (buttonsClicked.isEmpty) {
|
||||
actionStreamInternal.add(LogNotification('Buttons released'));
|
||||
_longPressTimer?.cancel();
|
||||
|
||||
// Handle release events for long press keys
|
||||
final buttonsReleased = _previouslyPressedButtons.toList();
|
||||
if (buttonsReleased.isNotEmpty) {
|
||||
await _performRelease(buttonsReleased);
|
||||
}
|
||||
_previouslyPressedButtons.clear();
|
||||
} else {
|
||||
// Handle release events for buttons that are no longer pressed
|
||||
final buttonsReleased = _previouslyPressedButtons.difference(buttonsClicked.toSet()).toList();
|
||||
if (buttonsReleased.isNotEmpty) {
|
||||
await _performRelease(buttonsReleased);
|
||||
}
|
||||
|
||||
final isLongPress =
|
||||
buttonsClicked.singleOrNull != null &&
|
||||
actionHandler.supportedApp?.keymap.getKeyPair(buttonsClicked.single)?.isLongPress == true;
|
||||
|
||||
if (!isLongPress &&
|
||||
!(buttonsClicked.singleOrNull == ZwiftButton.onOffLeft ||
|
||||
buttonsClicked.singleOrNull == ZwiftButton.onOffRight)) {
|
||||
// we don't want to trigger the long press timer for the on/off buttons, also not when it's a long press key
|
||||
_longPressTimer?.cancel();
|
||||
_longPressTimer = Timer.periodic(const Duration(milliseconds: 250), (timer) async {
|
||||
_performActions(buttonsClicked, true);
|
||||
});
|
||||
} else if (isLongPress) {
|
||||
// Update currently pressed buttons
|
||||
_previouslyPressedButtons = buttonsClicked.toSet();
|
||||
}
|
||||
|
||||
return _performActions(buttonsClicked, false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _performActions(List<ZwiftButton> buttonsClicked, bool repeated) async {
|
||||
if (!repeated &&
|
||||
buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp))) {
|
||||
buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp)) &&
|
||||
settings.getVibrationEnabled()) {
|
||||
await _vibrate();
|
||||
}
|
||||
for (final action in buttonsClicked) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:swift_control/bluetooth/devices/zwift_ride.dart';
|
||||
|
||||
import '../ble.dart';
|
||||
import '../protocol/zp.pb.dart';
|
||||
|
||||
class ZwiftClickV2 extends ZwiftRide {
|
||||
ZwiftClickV2(super.scanResult);
|
||||
@@ -10,4 +13,56 @@ class ZwiftClickV2 extends ZwiftRide {
|
||||
|
||||
@override
|
||||
List<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_CLICK_V2;
|
||||
|
||||
@override
|
||||
Future<void> setupHandshake() async {
|
||||
super.setupHandshake();
|
||||
await sendCommandBuffer(Uint8List.fromList([0xFF, 0x04, 0x00]));
|
||||
}
|
||||
|
||||
Future<void> test() async {
|
||||
await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_DEV_INFO.value)); // 0008 00
|
||||
await sendCommand(Opcode.LOG_LEVEL_SET, LogLevelSet(logLevel: LogLevel.LOGLEVEL_TRACE)); // 4108 05
|
||||
|
||||
await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_CLIENT_SERVER_CONFIGURATION.value)); // 0008 10
|
||||
await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_CLIENT_SERVER_CONFIGURATION.value)); // 0008 10
|
||||
await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_CLIENT_SERVER_CONFIGURATION.value)); // 0008 10
|
||||
|
||||
await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_CONTROLLER_INPUT_CONFIG.value)); // 0008 80 08
|
||||
|
||||
await sendCommand(Opcode.GET, Get(dataObjectId: DO.BATTERY_STATE.value)); // 0008 83 06
|
||||
|
||||
// Value: FF04 000A 1540 E9D9 C96B 7463 C27F 1B4E 4D9F 1CB1 205D 882E D7CE
|
||||
// Value: FF04 000A 15B2 6324 0A31 D6C6 B81F C129 D6A4 E99D FFFC B9FC 418D
|
||||
await sendCommandBuffer(
|
||||
Uint8List.fromList([
|
||||
0xFF,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0A,
|
||||
0x15,
|
||||
0x40,
|
||||
0xE9,
|
||||
0xD9,
|
||||
0xC9,
|
||||
0x6B,
|
||||
0x74,
|
||||
0x63,
|
||||
0xC2,
|
||||
0x7F,
|
||||
0x1B,
|
||||
0x4E,
|
||||
0x4D,
|
||||
0x9F,
|
||||
0x1C,
|
||||
0xB1,
|
||||
0x20,
|
||||
0x5D,
|
||||
0x88,
|
||||
0x2E,
|
||||
0xD7,
|
||||
0xCE,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
import 'package:swift_control/bluetooth/devices/base_device.dart';
|
||||
import 'package:swift_control/bluetooth/messages/ride_notification.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../ble.dart';
|
||||
import '../messages/notification.dart';
|
||||
import '../protocol/zp.pb.dart';
|
||||
|
||||
class ZwiftRide extends BaseDevice {
|
||||
ZwiftRide(super.scanResult)
|
||||
@@ -39,6 +44,114 @@ class ZwiftRide extends BaseDevice {
|
||||
|
||||
RideNotification? _lastControllerNotification;
|
||||
|
||||
@override
|
||||
Future<void> processData(Uint8List bytes) async {
|
||||
Opcode? opcode;
|
||||
Uint8List message;
|
||||
|
||||
if (supportsEncryption) {
|
||||
final counter = bytes.sublist(0, 4); // Int.SIZE_BYTES is 4
|
||||
final payload = bytes.sublist(4);
|
||||
|
||||
if (zapEncryption.encryptionKeyBytes == null) {
|
||||
actionStreamInternal.add(
|
||||
LogNotification(
|
||||
'Encryption not initialized, yet. You may need to update the firmware of your device with the Zwift Companion app.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final data = zapEncryption.decrypt(counter, payload);
|
||||
opcode = Opcode.valueOf(data[0]);
|
||||
message = data.sublist(1);
|
||||
} else {
|
||||
opcode = Opcode.valueOf(bytes[0]);
|
||||
message = bytes.sublist(1);
|
||||
}
|
||||
|
||||
if (bytes.startsWith(Constants.RESPONSE_STOPPED_CLICK_V2)) {
|
||||
actionStreamInternal.add(
|
||||
LogNotification('Your Zwift Click V2 no longer sends events. Connect it in the Zwift app once per day.'),
|
||||
);
|
||||
}
|
||||
|
||||
switch (opcode) {
|
||||
case Opcode.RIDE_ON:
|
||||
//print("Empty RideOn response - unencrypted mode");
|
||||
|
||||
break;
|
||||
case Opcode.STATUS_RESPONSE:
|
||||
final status = StatusResponse.fromBuffer(message);
|
||||
if (kDebugMode) {
|
||||
print('StatusResponse: ${status.command} status: ${Status.valueOf(status.status)}');
|
||||
}
|
||||
break;
|
||||
case Opcode.GET_RESPONSE:
|
||||
final response = GetResponse.fromBuffer(message);
|
||||
final dataObjectType = DO.valueOf(response.dataObjectId);
|
||||
if (kDebugMode) {
|
||||
print('GetResponse: ${dataObjectType?.value.toRadixString(16).padLeft(4, '0')} $dataObjectType');
|
||||
}
|
||||
|
||||
switch (dataObjectType) {
|
||||
case DO.PAGE_DEV_INFO:
|
||||
final pageDevInfo = DevInfoPage.fromBuffer(response.dataObjectData);
|
||||
if (kDebugMode) {
|
||||
print('PageDevInfo: $pageDevInfo');
|
||||
}
|
||||
break;
|
||||
case DO.PAGE_DATE_TIME:
|
||||
final pageDateTime = DateTimePage.fromBuffer(response.dataObjectData);
|
||||
if (kDebugMode) {
|
||||
print('PageDateTime: $pageDateTime');
|
||||
}
|
||||
break;
|
||||
case DO.PAGE_CONTROLLER_INPUT_CONFIG:
|
||||
final pageDateTime = ControllerInputConfigPage.fromBuffer(response.dataObjectData);
|
||||
if (kDebugMode) {
|
||||
print('PageDateTime: $pageDateTime');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case Opcode.VENDOR_MESSAGE:
|
||||
break;
|
||||
case Opcode.LOG_DATA:
|
||||
final logMessage = LogDataNotification.fromBuffer(message);
|
||||
if (kDebugMode) {
|
||||
actionStreamInternal.add(LogNotification(logMessage.toString()));
|
||||
}
|
||||
break;
|
||||
case Opcode.BATTERY_NOTIF:
|
||||
final notification = BatteryNotification.fromBuffer(message);
|
||||
if (batteryLevel != notification.newPercLevel) {
|
||||
batteryLevel = notification.newPercLevel;
|
||||
connection.signalChange(this);
|
||||
}
|
||||
break;
|
||||
case Opcode.CONTROLLER_NOTIFICATION:
|
||||
processClickNotification(message)
|
||||
.then((buttonsClicked) async {
|
||||
return handleButtonsClicked(buttonsClicked);
|
||||
})
|
||||
.catchError((e) {
|
||||
actionStreamInternal.add(LogNotification(e.toString()));
|
||||
});
|
||||
break;
|
||||
case null:
|
||||
if (bytes[0] == 0x1A) {
|
||||
final batteryStatus = BatteryStatus.fromBuffer(message);
|
||||
if (kDebugMode) {
|
||||
print('BatteryStatus: $batteryStatus');
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
|
||||
final RideNotification clickNotification = RideNotification(message);
|
||||
@@ -53,4 +166,32 @@ class ZwiftRide extends BaseDevice {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendCommand(Opcode opCode, $pb.GeneratedMessage message) async {
|
||||
final buffer = Uint8List.fromList([opCode.value, ...message.writeToBuffer()]);
|
||||
if (kDebugMode) {
|
||||
print("Sending $opCode: ${buffer.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}");
|
||||
}
|
||||
await UniversalBle.write(
|
||||
device.deviceId,
|
||||
customServiceId,
|
||||
syncRxCharacteristic!.uuid,
|
||||
buffer,
|
||||
withoutResponse: true,
|
||||
);
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
Future<void> sendCommandBuffer(Uint8List buffer) async {
|
||||
if (kDebugMode) {
|
||||
print("Sending ${buffer.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}");
|
||||
}
|
||||
await UniversalBle.write(
|
||||
device.deviceId,
|
||||
customServiceId,
|
||||
syncRxCharacteristic!.uuid,
|
||||
buffer,
|
||||
withoutResponse: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
6146
lib/bluetooth/protocol/zp.pb.dart
Normal file
6146
lib/bluetooth/protocol/zp.pb.dart
Normal file
File diff suppressed because it is too large
Load Diff
583
lib/bluetooth/protocol/zp.pbenum.dart
Normal file
583
lib/bluetooth/protocol/zp.pbenum.dart
Normal file
@@ -0,0 +1,583 @@
|
||||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: zp.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
/// ///////////////////////////////////////////////////////////////
|
||||
/// Enumerations
|
||||
/// ///////////////////////////////////////////////////////////////
|
||||
class Opcode extends $pb.ProtobufEnum {
|
||||
static const Opcode GET = Opcode._(0, _omitEnumNames ? '' : 'GET');
|
||||
static const Opcode DEV_INFO_STATUS = Opcode._(1, _omitEnumNames ? '' : 'DEV_INFO_STATUS');
|
||||
static const Opcode BLE_SECURITY_REQUEST = Opcode._(2, _omitEnumNames ? '' : 'BLE_SECURITY_REQUEST');
|
||||
static const Opcode TRAINER_NOTIF = Opcode._(3, _omitEnumNames ? '' : 'TRAINER_NOTIF');
|
||||
static const Opcode TRAINER_CONFIG_SET = Opcode._(4, _omitEnumNames ? '' : 'TRAINER_CONFIG_SET');
|
||||
static const Opcode TRAINER_CONFIG_STATUS = Opcode._(5, _omitEnumNames ? '' : 'TRAINER_CONFIG_STATUS');
|
||||
static const Opcode DEV_INFO_SET = Opcode._(12, _omitEnumNames ? '' : 'DEV_INFO_SET');
|
||||
static const Opcode POWER_OFF = Opcode._(15, _omitEnumNames ? '' : 'POWER_OFF');
|
||||
static const Opcode RESET = Opcode._(24, _omitEnumNames ? '' : 'RESET');
|
||||
static const Opcode BATTERY_NOTIF = Opcode._(25, _omitEnumNames ? '' : 'BATTERY_NOTIF');
|
||||
static const Opcode CONTROLLER_NOTIFICATION = Opcode._(35, _omitEnumNames ? '' : 'CONTROLLER_NOTIFICATION');
|
||||
static const Opcode LOG_DATA = Opcode._(42, _omitEnumNames ? '' : 'LOG_DATA');
|
||||
static const Opcode SPINDOWN_REQUEST = Opcode._(58, _omitEnumNames ? '' : 'SPINDOWN_REQUEST');
|
||||
static const Opcode SPINDOWN_NOTIFICATION = Opcode._(59, _omitEnumNames ? '' : 'SPINDOWN_NOTIFICATION');
|
||||
static const Opcode GET_RESPONSE = Opcode._(60, _omitEnumNames ? '' : 'GET_RESPONSE');
|
||||
static const Opcode STATUS_RESPONSE = Opcode._(62, _omitEnumNames ? '' : 'STATUS_RESPONSE');
|
||||
static const Opcode SET = Opcode._(63, _omitEnumNames ? '' : 'SET');
|
||||
static const Opcode SET_RESPONSE = Opcode._(64, _omitEnumNames ? '' : 'SET_RESPONSE');
|
||||
static const Opcode LOG_LEVEL_SET = Opcode._(65, _omitEnumNames ? '' : 'LOG_LEVEL_SET');
|
||||
static const Opcode DATA_CHANGE_NOTIFICATION = Opcode._(66, _omitEnumNames ? '' : 'DATA_CHANGE_NOTIFICATION');
|
||||
static const Opcode GAME_STATE_NOTIFICATION = Opcode._(67, _omitEnumNames ? '' : 'GAME_STATE_NOTIFICATION');
|
||||
static const Opcode SENSOR_RELAY_CONFIG = Opcode._(68, _omitEnumNames ? '' : 'SENSOR_RELAY_CONFIG');
|
||||
static const Opcode SENSOR_RELAY_GET = Opcode._(69, _omitEnumNames ? '' : 'SENSOR_RELAY_GET');
|
||||
static const Opcode SENSOR_RELAY_RESPONSE = Opcode._(70, _omitEnumNames ? '' : 'SENSOR_RELAY_RESPONSE');
|
||||
static const Opcode SENSOR_RELAY_NOTIFICATION = Opcode._(71, _omitEnumNames ? '' : 'SENSOR_RELAY_NOTIFICATION');
|
||||
static const Opcode HRM_DATA_NOTIFICATION = Opcode._(72, _omitEnumNames ? '' : 'HRM_DATA_NOTIFICATION');
|
||||
static const Opcode WIFI_CONFIG_REQUEST = Opcode._(73, _omitEnumNames ? '' : 'WIFI_CONFIG_REQUEST');
|
||||
static const Opcode WIFI_NOTIFICATION = Opcode._(74, _omitEnumNames ? '' : 'WIFI_NOTIFICATION');
|
||||
static const Opcode POWER_METER_NOTIFICATION = Opcode._(75, _omitEnumNames ? '' : 'POWER_METER_NOTIFICATION');
|
||||
static const Opcode CADENCE_SENSOR_NOTIFICATION = Opcode._(76, _omitEnumNames ? '' : 'CADENCE_SENSOR_NOTIFICATION');
|
||||
static const Opcode DEVICE_UPDATE_REQUEST = Opcode._(77, _omitEnumNames ? '' : 'DEVICE_UPDATE_REQUEST');
|
||||
static const Opcode RELAY_ZP_MESSAGE = Opcode._(78, _omitEnumNames ? '' : 'RELAY_ZP_MESSAGE');
|
||||
static const Opcode RIDE_ON = Opcode._(82, _omitEnumNames ? '' : 'RIDE_ON');
|
||||
static const Opcode RESERVED = Opcode._(253, _omitEnumNames ? '' : 'RESERVED');
|
||||
static const Opcode LOST_CONTROL = Opcode._(254, _omitEnumNames ? '' : 'LOST_CONTROL');
|
||||
static const Opcode VENDOR_MESSAGE = Opcode._(255, _omitEnumNames ? '' : 'VENDOR_MESSAGE');
|
||||
|
||||
static const $core.List<Opcode> values = <Opcode> [
|
||||
GET,
|
||||
DEV_INFO_STATUS,
|
||||
BLE_SECURITY_REQUEST,
|
||||
TRAINER_NOTIF,
|
||||
TRAINER_CONFIG_SET,
|
||||
TRAINER_CONFIG_STATUS,
|
||||
DEV_INFO_SET,
|
||||
POWER_OFF,
|
||||
RESET,
|
||||
BATTERY_NOTIF,
|
||||
CONTROLLER_NOTIFICATION,
|
||||
LOG_DATA,
|
||||
SPINDOWN_REQUEST,
|
||||
SPINDOWN_NOTIFICATION,
|
||||
GET_RESPONSE,
|
||||
STATUS_RESPONSE,
|
||||
SET,
|
||||
SET_RESPONSE,
|
||||
LOG_LEVEL_SET,
|
||||
DATA_CHANGE_NOTIFICATION,
|
||||
GAME_STATE_NOTIFICATION,
|
||||
SENSOR_RELAY_CONFIG,
|
||||
SENSOR_RELAY_GET,
|
||||
SENSOR_RELAY_RESPONSE,
|
||||
SENSOR_RELAY_NOTIFICATION,
|
||||
HRM_DATA_NOTIFICATION,
|
||||
WIFI_CONFIG_REQUEST,
|
||||
WIFI_NOTIFICATION,
|
||||
POWER_METER_NOTIFICATION,
|
||||
CADENCE_SENSOR_NOTIFICATION,
|
||||
DEVICE_UPDATE_REQUEST,
|
||||
RELAY_ZP_MESSAGE,
|
||||
RIDE_ON,
|
||||
RESERVED,
|
||||
LOST_CONTROL,
|
||||
VENDOR_MESSAGE,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, Opcode> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static Opcode? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const Opcode._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
/// Data Objects
|
||||
class DO extends $pb.ProtobufEnum {
|
||||
static const DO PAGE_DEV_INFO = DO._(0, _omitEnumNames ? '' : 'PAGE_DEV_INFO');
|
||||
static const DO PROTOCOL_VERSION = DO._(1, _omitEnumNames ? '' : 'PROTOCOL_VERSION');
|
||||
static const DO SYSTEM_FW_VERSION = DO._(2, _omitEnumNames ? '' : 'SYSTEM_FW_VERSION');
|
||||
static const DO DEVICE_NAME = DO._(3, _omitEnumNames ? '' : 'DEVICE_NAME');
|
||||
static const DO SERIAL_NUMBER = DO._(5, _omitEnumNames ? '' : 'SERIAL_NUMBER');
|
||||
static const DO SYSTEM_HW_REVISION = DO._(6, _omitEnumNames ? '' : 'SYSTEM_HW_REVISION');
|
||||
static const DO DEVICE_CAPABILITIES = DO._(7, _omitEnumNames ? '' : 'DEVICE_CAPABILITIES');
|
||||
static const DO MANUFACTURER_ID = DO._(8, _omitEnumNames ? '' : 'MANUFACTURER_ID');
|
||||
static const DO PRODUCT_ID = DO._(9, _omitEnumNames ? '' : 'PRODUCT_ID');
|
||||
static const DO DEVICE_UID = DO._(10, _omitEnumNames ? '' : 'DEVICE_UID');
|
||||
static const DO PAGE_CLIENT_SERVER_CONFIGURATION = DO._(16, _omitEnumNames ? '' : 'PAGE_CLIENT_SERVER_CONFIGURATION');
|
||||
static const DO CLIENT_SERVER_NOTIFICATIONS = DO._(17, _omitEnumNames ? '' : 'CLIENT_SERVER_NOTIFICATIONS');
|
||||
static const DO PAGE_DEVICE_UPDATE_INFO = DO._(32, _omitEnumNames ? '' : 'PAGE_DEVICE_UPDATE_INFO');
|
||||
static const DO DEVICE_UPDATE_STATUS = DO._(33, _omitEnumNames ? '' : 'DEVICE_UPDATE_STATUS');
|
||||
static const DO DEVICE_UPDATE_NEW_VERSION = DO._(34, _omitEnumNames ? '' : 'DEVICE_UPDATE_NEW_VERSION');
|
||||
static const DO PAGE_DATE_TIME = DO._(48, _omitEnumNames ? '' : 'PAGE_DATE_TIME');
|
||||
static const DO UTC_DATE_TIME = DO._(49, _omitEnumNames ? '' : 'UTC_DATE_TIME');
|
||||
static const DO PAGE_BLE_SECURITY = DO._(64, _omitEnumNames ? '' : 'PAGE_BLE_SECURITY');
|
||||
static const DO BLE_SECURE_CONNECTION_STATUS = DO._(65, _omitEnumNames ? '' : 'BLE_SECURE_CONNECTION_STATUS');
|
||||
static const DO BLE_SECURE_CONNECTION_WINDOW_STATUS = DO._(66, _omitEnumNames ? '' : 'BLE_SECURE_CONNECTION_WINDOW_STATUS');
|
||||
static const DO PAGE_TRAINER_CONFIG = DO._(512, _omitEnumNames ? '' : 'PAGE_TRAINER_CONFIG');
|
||||
static const DO TRAINER_MODE = DO._(513, _omitEnumNames ? '' : 'TRAINER_MODE');
|
||||
static const DO CFG_RESISTANCE = DO._(514, _omitEnumNames ? '' : 'CFG_RESISTANCE');
|
||||
static const DO ERG_POWER = DO._(515, _omitEnumNames ? '' : 'ERG_POWER');
|
||||
static const DO AVERAGING_WINDOW = DO._(516, _omitEnumNames ? '' : 'AVERAGING_WINDOW');
|
||||
static const DO SIM_WIND = DO._(517, _omitEnumNames ? '' : 'SIM_WIND');
|
||||
static const DO SIM_GRADE = DO._(518, _omitEnumNames ? '' : 'SIM_GRADE');
|
||||
static const DO SIM_REAL_GEAR_RATIO = DO._(519, _omitEnumNames ? '' : 'SIM_REAL_GEAR_RATIO');
|
||||
static const DO SIM_VIRT_GEAR_RATIO = DO._(520, _omitEnumNames ? '' : 'SIM_VIRT_GEAR_RATIO');
|
||||
static const DO SIM_CW = DO._(521, _omitEnumNames ? '' : 'SIM_CW');
|
||||
static const DO SIM_WHEEL_DIAMETER = DO._(522, _omitEnumNames ? '' : 'SIM_WHEEL_DIAMETER');
|
||||
static const DO SIM_BIKE_MASS = DO._(523, _omitEnumNames ? '' : 'SIM_BIKE_MASS');
|
||||
static const DO SIM_RIDER_MASS = DO._(524, _omitEnumNames ? '' : 'SIM_RIDER_MASS');
|
||||
static const DO SIM_CRR = DO._(525, _omitEnumNames ? '' : 'SIM_CRR');
|
||||
static const DO SIM_RESERVED_FRONTAL_AREA = DO._(526, _omitEnumNames ? '' : 'SIM_RESERVED_FRONTAL_AREA');
|
||||
static const DO SIM_EBRAKE = DO._(527, _omitEnumNames ? '' : 'SIM_EBRAKE');
|
||||
static const DO PAGE_TRAINER_GEAR_INDEX_CONFIG = DO._(528, _omitEnumNames ? '' : 'PAGE_TRAINER_GEAR_INDEX_CONFIG');
|
||||
static const DO FRONT_GEAR_INDEX = DO._(529, _omitEnumNames ? '' : 'FRONT_GEAR_INDEX');
|
||||
static const DO FRONT_GEAR_INDEX_MAX = DO._(530, _omitEnumNames ? '' : 'FRONT_GEAR_INDEX_MAX');
|
||||
static const DO FRONT_GEAR_INDEX_MIN = DO._(531, _omitEnumNames ? '' : 'FRONT_GEAR_INDEX_MIN');
|
||||
static const DO REAR_GEAR_INDEX = DO._(532, _omitEnumNames ? '' : 'REAR_GEAR_INDEX');
|
||||
static const DO REAR_GEAR_INDEX_MAX = DO._(533, _omitEnumNames ? '' : 'REAR_GEAR_INDEX_MAX');
|
||||
static const DO REAR_GEAR_INDEX_MIN = DO._(534, _omitEnumNames ? '' : 'REAR_GEAR_INDEX_MIN');
|
||||
static const DO PAGE_TRAINER_CONFIG2 = DO._(544, _omitEnumNames ? '' : 'PAGE_TRAINER_CONFIG2');
|
||||
static const DO HIGH_SPEED_DATA = DO._(545, _omitEnumNames ? '' : 'HIGH_SPEED_DATA');
|
||||
static const DO ERG_POWER_SMOOTHING = DO._(546, _omitEnumNames ? '' : 'ERG_POWER_SMOOTHING');
|
||||
static const DO VIRTUAL_SHIFTING_MODE = DO._(547, _omitEnumNames ? '' : 'VIRTUAL_SHIFTING_MODE');
|
||||
static const DO PAGE_DEVICE_TILT_CONFIG = DO._(560, _omitEnumNames ? '' : 'PAGE_DEVICE_TILT_CONFIG');
|
||||
static const DO DEVICE_TILT_ENABLED = DO._(561, _omitEnumNames ? '' : 'DEVICE_TILT_ENABLED');
|
||||
static const DO DEVICE_TILT_GRADIENT_MIN = DO._(562, _omitEnumNames ? '' : 'DEVICE_TILT_GRADIENT_MIN');
|
||||
static const DO DEVICE_TILT_GRADIENT_MAX = DO._(563, _omitEnumNames ? '' : 'DEVICE_TILT_GRADIENT_MAX');
|
||||
static const DO DEVICE_TILT_GRADIENT = DO._(564, _omitEnumNames ? '' : 'DEVICE_TILT_GRADIENT');
|
||||
static const DO BATTERY_STATE = DO._(771, _omitEnumNames ? '' : 'BATTERY_STATE');
|
||||
static const DO PAGE_CONTROLLER_INPUT_CONFIG = DO._(1024, _omitEnumNames ? '' : 'PAGE_CONTROLLER_INPUT_CONFIG');
|
||||
static const DO INPUT_SUPPORTED_DIGITAL_INPUTS = DO._(1025, _omitEnumNames ? '' : 'INPUT_SUPPORTED_DIGITAL_INPUTS');
|
||||
static const DO INPUT_SUPPORTED_ANALOG_INPUTS = DO._(1026, _omitEnumNames ? '' : 'INPUT_SUPPORTED_ANALOG_INPUTS');
|
||||
static const DO INPUT_ANALOG_INPUT_RANGE = DO._(1027, _omitEnumNames ? '' : 'INPUT_ANALOG_INPUT_RANGE');
|
||||
static const DO INPUT_ANALOG_INPUT_DEADZONE = DO._(1028, _omitEnumNames ? '' : 'INPUT_ANALOG_INPUT_DEADZONE');
|
||||
static const DO PAGE_WIFI_CONFIGURATION = DO._(1056, _omitEnumNames ? '' : 'PAGE_WIFI_CONFIGURATION');
|
||||
static const DO WIFI_ENABLED = DO._(1057, _omitEnumNames ? '' : 'WIFI_ENABLED');
|
||||
static const DO WIFI_STATUS = DO._(1058, _omitEnumNames ? '' : 'WIFI_STATUS');
|
||||
static const DO WIFI_SSID = DO._(1059, _omitEnumNames ? '' : 'WIFI_SSID');
|
||||
static const DO WIFI_BAND = DO._(1060, _omitEnumNames ? '' : 'WIFI_BAND');
|
||||
static const DO WIFI_RSSI = DO._(1061, _omitEnumNames ? '' : 'WIFI_RSSI');
|
||||
static const DO WIFI_REGION_CODE = DO._(1062, _omitEnumNames ? '' : 'WIFI_REGION_CODE');
|
||||
static const DO SENSOR_RELAY_DATA_PAGE = DO._(1280, _omitEnumNames ? '' : 'SENSOR_RELAY_DATA_PAGE');
|
||||
static const DO SENSOR_RELAY_SUPPORTED_SENSORS = DO._(1281, _omitEnumNames ? '' : 'SENSOR_RELAY_SUPPORTED_SENSORS');
|
||||
static const DO SENSOR_RELAY_PAIRED_SENSORS = DO._(1282, _omitEnumNames ? '' : 'SENSOR_RELAY_PAIRED_SENSORS');
|
||||
|
||||
static const $core.List<DO> values = <DO> [
|
||||
PAGE_DEV_INFO,
|
||||
PROTOCOL_VERSION,
|
||||
SYSTEM_FW_VERSION,
|
||||
DEVICE_NAME,
|
||||
SERIAL_NUMBER,
|
||||
SYSTEM_HW_REVISION,
|
||||
DEVICE_CAPABILITIES,
|
||||
MANUFACTURER_ID,
|
||||
PRODUCT_ID,
|
||||
DEVICE_UID,
|
||||
PAGE_CLIENT_SERVER_CONFIGURATION,
|
||||
CLIENT_SERVER_NOTIFICATIONS,
|
||||
PAGE_DEVICE_UPDATE_INFO,
|
||||
DEVICE_UPDATE_STATUS,
|
||||
DEVICE_UPDATE_NEW_VERSION,
|
||||
PAGE_DATE_TIME,
|
||||
UTC_DATE_TIME,
|
||||
PAGE_BLE_SECURITY,
|
||||
BLE_SECURE_CONNECTION_STATUS,
|
||||
BLE_SECURE_CONNECTION_WINDOW_STATUS,
|
||||
PAGE_TRAINER_CONFIG,
|
||||
TRAINER_MODE,
|
||||
CFG_RESISTANCE,
|
||||
ERG_POWER,
|
||||
AVERAGING_WINDOW,
|
||||
SIM_WIND,
|
||||
SIM_GRADE,
|
||||
SIM_REAL_GEAR_RATIO,
|
||||
SIM_VIRT_GEAR_RATIO,
|
||||
SIM_CW,
|
||||
SIM_WHEEL_DIAMETER,
|
||||
SIM_BIKE_MASS,
|
||||
SIM_RIDER_MASS,
|
||||
SIM_CRR,
|
||||
SIM_RESERVED_FRONTAL_AREA,
|
||||
SIM_EBRAKE,
|
||||
PAGE_TRAINER_GEAR_INDEX_CONFIG,
|
||||
FRONT_GEAR_INDEX,
|
||||
FRONT_GEAR_INDEX_MAX,
|
||||
FRONT_GEAR_INDEX_MIN,
|
||||
REAR_GEAR_INDEX,
|
||||
REAR_GEAR_INDEX_MAX,
|
||||
REAR_GEAR_INDEX_MIN,
|
||||
PAGE_TRAINER_CONFIG2,
|
||||
HIGH_SPEED_DATA,
|
||||
ERG_POWER_SMOOTHING,
|
||||
VIRTUAL_SHIFTING_MODE,
|
||||
PAGE_DEVICE_TILT_CONFIG,
|
||||
DEVICE_TILT_ENABLED,
|
||||
DEVICE_TILT_GRADIENT_MIN,
|
||||
DEVICE_TILT_GRADIENT_MAX,
|
||||
DEVICE_TILT_GRADIENT,
|
||||
BATTERY_STATE,
|
||||
PAGE_CONTROLLER_INPUT_CONFIG,
|
||||
INPUT_SUPPORTED_DIGITAL_INPUTS,
|
||||
INPUT_SUPPORTED_ANALOG_INPUTS,
|
||||
INPUT_ANALOG_INPUT_RANGE,
|
||||
INPUT_ANALOG_INPUT_DEADZONE,
|
||||
PAGE_WIFI_CONFIGURATION,
|
||||
WIFI_ENABLED,
|
||||
WIFI_STATUS,
|
||||
WIFI_SSID,
|
||||
WIFI_BAND,
|
||||
WIFI_RSSI,
|
||||
WIFI_REGION_CODE,
|
||||
SENSOR_RELAY_DATA_PAGE,
|
||||
SENSOR_RELAY_SUPPORTED_SENSORS,
|
||||
SENSOR_RELAY_PAIRED_SENSORS,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, DO> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static DO? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const DO._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class Status extends $pb.ProtobufEnum {
|
||||
static const Status SUCCESS = Status._(0, _omitEnumNames ? '' : 'SUCCESS');
|
||||
static const Status FAILURE = Status._(1, _omitEnumNames ? '' : 'FAILURE');
|
||||
static const Status BUSY = Status._(2, _omitEnumNames ? '' : 'BUSY');
|
||||
static const Status INVALID_PARAM = Status._(3, _omitEnumNames ? '' : 'INVALID_PARAM');
|
||||
static const Status NOT_PERMITTED = Status._(4, _omitEnumNames ? '' : 'NOT_PERMITTED');
|
||||
static const Status NOT_SUPPORTED = Status._(5, _omitEnumNames ? '' : 'NOT_SUPPORTED');
|
||||
static const Status INVALID_MODE = Status._(6, _omitEnumNames ? '' : 'INVALID_MODE');
|
||||
|
||||
static const $core.List<Status> values = <Status> [
|
||||
SUCCESS,
|
||||
FAILURE,
|
||||
BUSY,
|
||||
INVALID_PARAM,
|
||||
NOT_PERMITTED,
|
||||
NOT_SUPPORTED,
|
||||
INVALID_MODE,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, Status> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static Status? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const Status._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class DeviceType extends $pb.ProtobufEnum {
|
||||
static const DeviceType UNDEFINED = DeviceType._(0, _omitEnumNames ? '' : 'UNDEFINED');
|
||||
static const DeviceType CYCLING_TURBO_TRAINER = DeviceType._(1, _omitEnumNames ? '' : 'CYCLING_TURBO_TRAINER');
|
||||
static const DeviceType USER_INPUT_DEVICE = DeviceType._(2, _omitEnumNames ? '' : 'USER_INPUT_DEVICE');
|
||||
static const DeviceType TREADMILL = DeviceType._(3, _omitEnumNames ? '' : 'TREADMILL');
|
||||
static const DeviceType SENSOR_RELAY = DeviceType._(4, _omitEnumNames ? '' : 'SENSOR_RELAY');
|
||||
static const DeviceType HEART_RATE_MONITOR = DeviceType._(5, _omitEnumNames ? '' : 'HEART_RATE_MONITOR');
|
||||
static const DeviceType POWER_METER = DeviceType._(6, _omitEnumNames ? '' : 'POWER_METER');
|
||||
static const DeviceType CADENCE_SENSOR = DeviceType._(7, _omitEnumNames ? '' : 'CADENCE_SENSOR');
|
||||
static const DeviceType WIFI = DeviceType._(8, _omitEnumNames ? '' : 'WIFI');
|
||||
|
||||
static const $core.List<DeviceType> values = <DeviceType> [
|
||||
UNDEFINED,
|
||||
CYCLING_TURBO_TRAINER,
|
||||
USER_INPUT_DEVICE,
|
||||
TREADMILL,
|
||||
SENSOR_RELAY,
|
||||
HEART_RATE_MONITOR,
|
||||
POWER_METER,
|
||||
CADENCE_SENSOR,
|
||||
WIFI,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, DeviceType> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static DeviceType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const DeviceType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class TrainerMode extends $pb.ProtobufEnum {
|
||||
static const TrainerMode MODE_UNKNOWN = TrainerMode._(0, _omitEnumNames ? '' : 'MODE_UNKNOWN');
|
||||
static const TrainerMode MODE_ERG = TrainerMode._(1, _omitEnumNames ? '' : 'MODE_ERG');
|
||||
static const TrainerMode MODE_RESISTANCE = TrainerMode._(2, _omitEnumNames ? '' : 'MODE_RESISTANCE');
|
||||
static const TrainerMode MODE_SIM = TrainerMode._(3, _omitEnumNames ? '' : 'MODE_SIM');
|
||||
|
||||
static const $core.List<TrainerMode> values = <TrainerMode> [
|
||||
MODE_UNKNOWN,
|
||||
MODE_ERG,
|
||||
MODE_RESISTANCE,
|
||||
MODE_SIM,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, TrainerMode> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static TrainerMode? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const TrainerMode._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class ChargingState extends $pb.ProtobufEnum {
|
||||
static const ChargingState CHARGING_IDLE = ChargingState._(0, _omitEnumNames ? '' : 'CHARGING_IDLE');
|
||||
static const ChargingState CHARGING_PROGRESS = ChargingState._(1, _omitEnumNames ? '' : 'CHARGING_PROGRESS');
|
||||
static const ChargingState CHARGING_DONE = ChargingState._(2, _omitEnumNames ? '' : 'CHARGING_DONE');
|
||||
|
||||
static const $core.List<ChargingState> values = <ChargingState> [
|
||||
CHARGING_IDLE,
|
||||
CHARGING_PROGRESS,
|
||||
CHARGING_DONE,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, ChargingState> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static ChargingState? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const ChargingState._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class SpindownStatus extends $pb.ProtobufEnum {
|
||||
static const SpindownStatus SPINDOWN_IDLE = SpindownStatus._(0, _omitEnumNames ? '' : 'SPINDOWN_IDLE');
|
||||
static const SpindownStatus SPINDOWN_REQUESTED = SpindownStatus._(1, _omitEnumNames ? '' : 'SPINDOWN_REQUESTED');
|
||||
static const SpindownStatus SPINDOWN_SUCCESS = SpindownStatus._(2, _omitEnumNames ? '' : 'SPINDOWN_SUCCESS');
|
||||
static const SpindownStatus SPINDOWN_ERROR = SpindownStatus._(3, _omitEnumNames ? '' : 'SPINDOWN_ERROR');
|
||||
static const SpindownStatus SPINDOWN_STOP_PEDALLING = SpindownStatus._(4, _omitEnumNames ? '' : 'SPINDOWN_STOP_PEDALLING');
|
||||
static const SpindownStatus SPINDOWN_ERROR_TIMEOUT = SpindownStatus._(5, _omitEnumNames ? '' : 'SPINDOWN_ERROR_TIMEOUT');
|
||||
static const SpindownStatus SPINDOWN_ERROR_TOSHORT = SpindownStatus._(6, _omitEnumNames ? '' : 'SPINDOWN_ERROR_TOSHORT');
|
||||
static const SpindownStatus SPINDOWN_ERROR_TOSLOW = SpindownStatus._(7, _omitEnumNames ? '' : 'SPINDOWN_ERROR_TOSLOW');
|
||||
static const SpindownStatus SPINDOWN_ERROR_TOFAST = SpindownStatus._(8, _omitEnumNames ? '' : 'SPINDOWN_ERROR_TOFAST');
|
||||
static const SpindownStatus SPINDOWN_ERROR_SAMPLEERROR = SpindownStatus._(9, _omitEnumNames ? '' : 'SPINDOWN_ERROR_SAMPLEERROR');
|
||||
static const SpindownStatus SPINDOWN_ERROR_ABORT = SpindownStatus._(10, _omitEnumNames ? '' : 'SPINDOWN_ERROR_ABORT');
|
||||
|
||||
static const $core.List<SpindownStatus> values = <SpindownStatus> [
|
||||
SPINDOWN_IDLE,
|
||||
SPINDOWN_REQUESTED,
|
||||
SPINDOWN_SUCCESS,
|
||||
SPINDOWN_ERROR,
|
||||
SPINDOWN_STOP_PEDALLING,
|
||||
SPINDOWN_ERROR_TIMEOUT,
|
||||
SPINDOWN_ERROR_TOSHORT,
|
||||
SPINDOWN_ERROR_TOSLOW,
|
||||
SPINDOWN_ERROR_TOFAST,
|
||||
SPINDOWN_ERROR_SAMPLEERROR,
|
||||
SPINDOWN_ERROR_ABORT,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, SpindownStatus> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static SpindownStatus? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const SpindownStatus._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class LogLevel extends $pb.ProtobufEnum {
|
||||
static const LogLevel LOGLEVEL_OFF = LogLevel._(0, _omitEnumNames ? '' : 'LOGLEVEL_OFF');
|
||||
static const LogLevel LOGLEVEL_ERROR = LogLevel._(1, _omitEnumNames ? '' : 'LOGLEVEL_ERROR');
|
||||
static const LogLevel LOGLEVEL_WARNING = LogLevel._(2, _omitEnumNames ? '' : 'LOGLEVEL_WARNING');
|
||||
static const LogLevel LOGLEVEL_INFO = LogLevel._(3, _omitEnumNames ? '' : 'LOGLEVEL_INFO');
|
||||
static const LogLevel LOGLEVEL_DEBUG = LogLevel._(4, _omitEnumNames ? '' : 'LOGLEVEL_DEBUG');
|
||||
static const LogLevel LOGLEVEL_TRACE = LogLevel._(5, _omitEnumNames ? '' : 'LOGLEVEL_TRACE');
|
||||
|
||||
static const $core.List<LogLevel> values = <LogLevel> [
|
||||
LOGLEVEL_OFF,
|
||||
LOGLEVEL_ERROR,
|
||||
LOGLEVEL_WARNING,
|
||||
LOGLEVEL_INFO,
|
||||
LOGLEVEL_DEBUG,
|
||||
LOGLEVEL_TRACE,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, LogLevel> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static LogLevel? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const LogLevel._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class RoadSurfaceType extends $pb.ProtobufEnum {
|
||||
static const RoadSurfaceType ROAD_SURFACE_SMOOTH_TARMAC = RoadSurfaceType._(0, _omitEnumNames ? '' : 'ROAD_SURFACE_SMOOTH_TARMAC');
|
||||
static const RoadSurfaceType ROAD_SURFACE_BRICK_ROAD = RoadSurfaceType._(1, _omitEnumNames ? '' : 'ROAD_SURFACE_BRICK_ROAD');
|
||||
static const RoadSurfaceType ROAD_SURFACE_HARD_COBBLES = RoadSurfaceType._(2, _omitEnumNames ? '' : 'ROAD_SURFACE_HARD_COBBLES');
|
||||
static const RoadSurfaceType ROAD_SURFACE_SOFT_COBBLES = RoadSurfaceType._(3, _omitEnumNames ? '' : 'ROAD_SURFACE_SOFT_COBBLES');
|
||||
static const RoadSurfaceType ROAD_SURFACE_NARROW_WOODEN_PLANKS = RoadSurfaceType._(4, _omitEnumNames ? '' : 'ROAD_SURFACE_NARROW_WOODEN_PLANKS');
|
||||
static const RoadSurfaceType ROAD_SURFACE_WIDE_WOODEN_PLANKS = RoadSurfaceType._(5, _omitEnumNames ? '' : 'ROAD_SURFACE_WIDE_WOODEN_PLANKS');
|
||||
static const RoadSurfaceType ROAD_SURFACE_DIRT = RoadSurfaceType._(6, _omitEnumNames ? '' : 'ROAD_SURFACE_DIRT');
|
||||
static const RoadSurfaceType ROAD_SURFACE_GRAVEL = RoadSurfaceType._(7, _omitEnumNames ? '' : 'ROAD_SURFACE_GRAVEL');
|
||||
static const RoadSurfaceType ROAD_SURFACE_CATTLE_GRID = RoadSurfaceType._(8, _omitEnumNames ? '' : 'ROAD_SURFACE_CATTLE_GRID');
|
||||
static const RoadSurfaceType ROAD_SURFACE_CONCRETE_FLAG_STONES = RoadSurfaceType._(9, _omitEnumNames ? '' : 'ROAD_SURFACE_CONCRETE_FLAG_STONES');
|
||||
static const RoadSurfaceType ROAD_SURFACE_ICE = RoadSurfaceType._(10, _omitEnumNames ? '' : 'ROAD_SURFACE_ICE');
|
||||
|
||||
static const $core.List<RoadSurfaceType> values = <RoadSurfaceType> [
|
||||
ROAD_SURFACE_SMOOTH_TARMAC,
|
||||
ROAD_SURFACE_BRICK_ROAD,
|
||||
ROAD_SURFACE_HARD_COBBLES,
|
||||
ROAD_SURFACE_SOFT_COBBLES,
|
||||
ROAD_SURFACE_NARROW_WOODEN_PLANKS,
|
||||
ROAD_SURFACE_WIDE_WOODEN_PLANKS,
|
||||
ROAD_SURFACE_DIRT,
|
||||
ROAD_SURFACE_GRAVEL,
|
||||
ROAD_SURFACE_CATTLE_GRID,
|
||||
ROAD_SURFACE_CONCRETE_FLAG_STONES,
|
||||
ROAD_SURFACE_ICE,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, RoadSurfaceType> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static RoadSurfaceType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const RoadSurfaceType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class WifiStatusCode extends $pb.ProtobufEnum {
|
||||
static const WifiStatusCode WIFI_STATUS_DISABLED = WifiStatusCode._(0, _omitEnumNames ? '' : 'WIFI_STATUS_DISABLED');
|
||||
static const WifiStatusCode WIFI_STATUS_NOT_PROVISIONED = WifiStatusCode._(1, _omitEnumNames ? '' : 'WIFI_STATUS_NOT_PROVISIONED');
|
||||
static const WifiStatusCode WIFI_STATUS_SCANNING = WifiStatusCode._(2, _omitEnumNames ? '' : 'WIFI_STATUS_SCANNING');
|
||||
static const WifiStatusCode WIFI_STATUS_DISCONNECTED = WifiStatusCode._(3, _omitEnumNames ? '' : 'WIFI_STATUS_DISCONNECTED');
|
||||
static const WifiStatusCode WIFI_STATUS_CONNECTING = WifiStatusCode._(4, _omitEnumNames ? '' : 'WIFI_STATUS_CONNECTING');
|
||||
static const WifiStatusCode WIFI_STATUS_CONNECTED = WifiStatusCode._(5, _omitEnumNames ? '' : 'WIFI_STATUS_CONNECTED');
|
||||
static const WifiStatusCode WIFI_STATUS_ERROR = WifiStatusCode._(6, _omitEnumNames ? '' : 'WIFI_STATUS_ERROR');
|
||||
|
||||
static const $core.List<WifiStatusCode> values = <WifiStatusCode> [
|
||||
WIFI_STATUS_DISABLED,
|
||||
WIFI_STATUS_NOT_PROVISIONED,
|
||||
WIFI_STATUS_SCANNING,
|
||||
WIFI_STATUS_DISCONNECTED,
|
||||
WIFI_STATUS_CONNECTING,
|
||||
WIFI_STATUS_CONNECTED,
|
||||
WIFI_STATUS_ERROR,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, WifiStatusCode> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static WifiStatusCode? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const WifiStatusCode._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class WifiErrorCode extends $pb.ProtobufEnum {
|
||||
static const WifiErrorCode WIFI_ERROR_UNKNOWN = WifiErrorCode._(0, _omitEnumNames ? '' : 'WIFI_ERROR_UNKNOWN');
|
||||
static const WifiErrorCode WIFI_ERROR_NO_MEMORY = WifiErrorCode._(1, _omitEnumNames ? '' : 'WIFI_ERROR_NO_MEMORY');
|
||||
static const WifiErrorCode WIFI_ERROR_INVALID_PARAMETERS = WifiErrorCode._(2, _omitEnumNames ? '' : 'WIFI_ERROR_INVALID_PARAMETERS');
|
||||
static const WifiErrorCode WIFI_ERROR_INVALID_STATE = WifiErrorCode._(3, _omitEnumNames ? '' : 'WIFI_ERROR_INVALID_STATE');
|
||||
static const WifiErrorCode WIFI_ERROR_NOT_FOUND = WifiErrorCode._(4, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_FOUND');
|
||||
static const WifiErrorCode WIFI_ERROR_NOT_SUPPORTED = WifiErrorCode._(5, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_SUPPORTED');
|
||||
static const WifiErrorCode WIFI_ERROR_NOT_ALLOWED = WifiErrorCode._(6, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_ALLOWED');
|
||||
static const WifiErrorCode WIFI_ERROR_NOT_INITIALISED = WifiErrorCode._(7, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_INITIALISED');
|
||||
static const WifiErrorCode WIFI_ERROR_NOT_STARTED = WifiErrorCode._(8, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_STARTED');
|
||||
static const WifiErrorCode WIFI_ERROR_TIMEOUT = WifiErrorCode._(9, _omitEnumNames ? '' : 'WIFI_ERROR_TIMEOUT');
|
||||
static const WifiErrorCode WIFI_ERROR_MODE = WifiErrorCode._(10, _omitEnumNames ? '' : 'WIFI_ERROR_MODE');
|
||||
static const WifiErrorCode WIFI_ERROR_SSID_INVALID = WifiErrorCode._(11, _omitEnumNames ? '' : 'WIFI_ERROR_SSID_INVALID');
|
||||
|
||||
static const $core.List<WifiErrorCode> values = <WifiErrorCode> [
|
||||
WIFI_ERROR_UNKNOWN,
|
||||
WIFI_ERROR_NO_MEMORY,
|
||||
WIFI_ERROR_INVALID_PARAMETERS,
|
||||
WIFI_ERROR_INVALID_STATE,
|
||||
WIFI_ERROR_NOT_FOUND,
|
||||
WIFI_ERROR_NOT_SUPPORTED,
|
||||
WIFI_ERROR_NOT_ALLOWED,
|
||||
WIFI_ERROR_NOT_INITIALISED,
|
||||
WIFI_ERROR_NOT_STARTED,
|
||||
WIFI_ERROR_TIMEOUT,
|
||||
WIFI_ERROR_MODE,
|
||||
WIFI_ERROR_SSID_INVALID,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, WifiErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static WifiErrorCode? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const WifiErrorCode._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class InterfaceType extends $pb.ProtobufEnum {
|
||||
static const InterfaceType INTERFACE_BLE = InterfaceType._(1, _omitEnumNames ? '' : 'INTERFACE_BLE');
|
||||
static const InterfaceType INTERFACE_ANT = InterfaceType._(2, _omitEnumNames ? '' : 'INTERFACE_ANT');
|
||||
static const InterfaceType INTERFACE_USB = InterfaceType._(3, _omitEnumNames ? '' : 'INTERFACE_USB');
|
||||
static const InterfaceType INTERFACE_ETH = InterfaceType._(4, _omitEnumNames ? '' : 'INTERFACE_ETH');
|
||||
static const InterfaceType INTERFACE_WIFI = InterfaceType._(5, _omitEnumNames ? '' : 'INTERFACE_WIFI');
|
||||
|
||||
static const $core.List<InterfaceType> values = <InterfaceType> [
|
||||
INTERFACE_BLE,
|
||||
INTERFACE_ANT,
|
||||
INTERFACE_USB,
|
||||
INTERFACE_ETH,
|
||||
INTERFACE_WIFI,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, InterfaceType> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static InterfaceType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const InterfaceType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class SensorConnectionStatus extends $pb.ProtobufEnum {
|
||||
static const SensorConnectionStatus SENSOR_STATUS_DISCOVERED = SensorConnectionStatus._(1, _omitEnumNames ? '' : 'SENSOR_STATUS_DISCOVERED');
|
||||
static const SensorConnectionStatus SENSOR_STATUS_DISCONNECTED = SensorConnectionStatus._(2, _omitEnumNames ? '' : 'SENSOR_STATUS_DISCONNECTED');
|
||||
static const SensorConnectionStatus SENSOR_STATUS_PAIRING = SensorConnectionStatus._(3, _omitEnumNames ? '' : 'SENSOR_STATUS_PAIRING');
|
||||
static const SensorConnectionStatus SENSOR_STATUS_CONNECTED = SensorConnectionStatus._(4, _omitEnumNames ? '' : 'SENSOR_STATUS_CONNECTED');
|
||||
|
||||
static const $core.List<SensorConnectionStatus> values = <SensorConnectionStatus> [
|
||||
SENSOR_STATUS_DISCOVERED,
|
||||
SENSOR_STATUS_DISCONNECTED,
|
||||
SENSOR_STATUS_PAIRING,
|
||||
SENSOR_STATUS_CONNECTED,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, SensorConnectionStatus> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static SensorConnectionStatus? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const SensorConnectionStatus._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class BleSecureConnectionStatus extends $pb.ProtobufEnum {
|
||||
static const BleSecureConnectionStatus BLE_CONNECTION_SECURITY_STATUS_NONE = BleSecureConnectionStatus._(0, _omitEnumNames ? '' : 'BLE_CONNECTION_SECURITY_STATUS_NONE');
|
||||
static const BleSecureConnectionStatus BLE_CONNECTION_SECURITY_STATUS_INPROGRESS = BleSecureConnectionStatus._(1, _omitEnumNames ? '' : 'BLE_CONNECTION_SECURITY_STATUS_INPROGRESS');
|
||||
static const BleSecureConnectionStatus BLE_CONNECTION_SECURITY_STATUS_ACTIVE = BleSecureConnectionStatus._(2, _omitEnumNames ? '' : 'BLE_CONNECTION_SECURITY_STATUS_ACTIVE');
|
||||
static const BleSecureConnectionStatus BLE_CONNECTION_SECURITY_STATUS_REJECTED = BleSecureConnectionStatus._(3, _omitEnumNames ? '' : 'BLE_CONNECTION_SECURITY_STATUS_REJECTED');
|
||||
|
||||
static const $core.List<BleSecureConnectionStatus> values = <BleSecureConnectionStatus> [
|
||||
BLE_CONNECTION_SECURITY_STATUS_NONE,
|
||||
BLE_CONNECTION_SECURITY_STATUS_INPROGRESS,
|
||||
BLE_CONNECTION_SECURITY_STATUS_ACTIVE,
|
||||
BLE_CONNECTION_SECURITY_STATUS_REJECTED,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, BleSecureConnectionStatus> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static BleSecureConnectionStatus? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const BleSecureConnectionStatus._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class BleSecureConnectionWindowStatus extends $pb.ProtobufEnum {
|
||||
static const BleSecureConnectionWindowStatus BLE_SECURE_CONNECTION_WINDOW_STATUS_CLOSED = BleSecureConnectionWindowStatus._(0, _omitEnumNames ? '' : 'BLE_SECURE_CONNECTION_WINDOW_STATUS_CLOSED');
|
||||
static const BleSecureConnectionWindowStatus BLE_SECURE_CONNECTION_WINDOW_STATUS_OPEN = BleSecureConnectionWindowStatus._(1, _omitEnumNames ? '' : 'BLE_SECURE_CONNECTION_WINDOW_STATUS_OPEN');
|
||||
|
||||
static const $core.List<BleSecureConnectionWindowStatus> values = <BleSecureConnectionWindowStatus> [
|
||||
BLE_SECURE_CONNECTION_WINDOW_STATUS_CLOSED,
|
||||
BLE_SECURE_CONNECTION_WINDOW_STATUS_OPEN,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, BleSecureConnectionWindowStatus> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static BleSecureConnectionWindowStatus? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const BleSecureConnectionWindowStatus._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class WifiRegionCode_RegionCodeType extends $pb.ProtobufEnum {
|
||||
static const WifiRegionCode_RegionCodeType ALPHA_2 = WifiRegionCode_RegionCodeType._(0, _omitEnumNames ? '' : 'ALPHA_2');
|
||||
static const WifiRegionCode_RegionCodeType ALPHA_3 = WifiRegionCode_RegionCodeType._(1, _omitEnumNames ? '' : 'ALPHA_3');
|
||||
static const WifiRegionCode_RegionCodeType NUMERIC = WifiRegionCode_RegionCodeType._(2, _omitEnumNames ? '' : 'NUMERIC');
|
||||
static const WifiRegionCode_RegionCodeType UNKNOWN = WifiRegionCode_RegionCodeType._(3, _omitEnumNames ? '' : 'UNKNOWN');
|
||||
|
||||
static const $core.List<WifiRegionCode_RegionCodeType> values = <WifiRegionCode_RegionCodeType> [
|
||||
ALPHA_2,
|
||||
ALPHA_3,
|
||||
NUMERIC,
|
||||
UNKNOWN,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, WifiRegionCode_RegionCodeType> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static WifiRegionCode_RegionCodeType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const WifiRegionCode_RegionCodeType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
|
||||
const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names');
|
||||
1692
lib/bluetooth/protocol/zp.pbjson.dart
Normal file
1692
lib/bluetooth/protocol/zp.pbjson.dart
Normal file
File diff suppressed because it is too large
Load Diff
14
lib/bluetooth/protocol/zp.pbserver.dart
Normal file
14
lib/bluetooth/protocol/zp.pbserver.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: zp.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'zp.pb.dart';
|
||||
|
||||
98
lib/pages/changelog.dart
Normal file
98
lib/pages/changelog.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/utils/changelog.dart';
|
||||
|
||||
class ChangelogPage extends StatefulWidget {
|
||||
const ChangelogPage({super.key});
|
||||
|
||||
@override
|
||||
State<ChangelogPage> createState() => _ChangelogPageState();
|
||||
}
|
||||
|
||||
class _ChangelogPageState extends State<ChangelogPage> {
|
||||
List<ChangelogEntry>? _entries;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadChangelog();
|
||||
}
|
||||
|
||||
Future<void> _loadChangelog() async {
|
||||
try {
|
||||
final entries = await ChangelogParser.parse();
|
||||
setState(() {
|
||||
_entries = entries;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = 'Failed to load changelog: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Changelog'),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
body: _error != null
|
||||
? Center(child: Text(_error!))
|
||||
: _entries == null
|
||||
? Center(child: CircularProgressIndicator())
|
||||
: ListView.builder(
|
||||
padding: EdgeInsets.all(16),
|
||||
itemCount: _entries!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = _entries![index];
|
||||
return Card(
|
||||
margin: EdgeInsets.only(bottom: 16),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Version ${entry.version}',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
entry.date,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
...entry.changes.map(
|
||||
(change) => Padding(
|
||||
padding: EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('• ', style: TextStyle(fontSize: 16)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
change,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift_clickv2.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/pages/touch_area.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
@@ -68,6 +69,21 @@ class _DevicePageState extends State<DevicePage> {
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text('Connected Devices:', style: Theme.of(context).textTheme.titleMedium),
|
||||
|
||||
if (connection.devices.any((device) => (device is ZwiftClickV2) && device.isConnected))
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
'''To make your Zwift Click V2 work properly you need to connect it to with in the Zwift app once each day:
|
||||
1. Open Zwift app
|
||||
2. After logging in (subscription not required) find it in the device connection screen and connect it
|
||||
3. Close the Zwift app again and connect again in SwiftControl''',
|
||||
),
|
||||
),
|
||||
Text(
|
||||
connection.devices.joinToString(
|
||||
separator: '\n',
|
||||
@@ -104,7 +120,7 @@ ${it.firmwareVersion != null ? ' - Firmware Version: ${it.firmwareVersion}' : ''
|
||||
}
|
||||
controller.text = app.name ?? '';
|
||||
actionHandler.supportedApp = app;
|
||||
settings.setApp(app);
|
||||
await settings.setApp(app);
|
||||
setState(() {});
|
||||
if (app is! CustomApp && !kIsWeb && (Platform.isMacOS || Platform.isWindows)) {
|
||||
_snackBarMessengerKey.currentState!.showSnackBar(
|
||||
@@ -147,14 +163,14 @@ ${it.firmwareVersion != null ? ' - Firmware Version: ${it.firmwareVersion}' : ''
|
||||
});
|
||||
|
||||
actionHandler.supportedApp = customApp;
|
||||
settings.setApp(customApp);
|
||||
await settings.setApp(customApp);
|
||||
}
|
||||
final result = await Navigator.of(
|
||||
context,
|
||||
).push<bool>(MaterialPageRoute(builder: (_) => TouchAreaSetupPage()));
|
||||
|
||||
if (result == true && actionHandler.supportedApp is CustomApp) {
|
||||
settings.setApp(actionHandler.supportedApp!);
|
||||
await settings.setApp(actionHandler.supportedApp!);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
@@ -171,6 +187,28 @@ ${it.firmwareVersion != null ? ' - Firmware Version: ${it.firmwareVersion}' : ''
|
||||
controller.text = actionHandler.supportedApp?.name ?? '';
|
||||
},
|
||||
),
|
||||
if (connection.devices.any(
|
||||
(device) =>
|
||||
(device.device.name == 'Zwift Ride' || device.device.name == 'Zwift Play') &&
|
||||
device.isConnected,
|
||||
))
|
||||
SwitchListTile(
|
||||
title: Text('Vibration on Shift'),
|
||||
subtitle: Text('Enable vibration feedback when shifting gears'),
|
||||
value: settings.getVibrationEnabled(),
|
||||
onChanged: (value) async {
|
||||
await settings.setVibrationEnabled(value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (kDebugMode &&
|
||||
connection.devices.any((device) => (device is ZwiftClickV2) && device.isConnected))
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
(connection.devices.first as ZwiftClickV2).test();
|
||||
},
|
||||
child: Text('Test'),
|
||||
),
|
||||
],
|
||||
),
|
||||
LogViewer(),
|
||||
|
||||
@@ -3,8 +3,10 @@ import 'dart:io';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/requirements/platform.dart';
|
||||
import 'package:swift_control/widgets/changelog_dialog.dart';
|
||||
import 'package:swift_control/widgets/menu.dart';
|
||||
import 'package:swift_control/widgets/title.dart';
|
||||
|
||||
@@ -30,6 +32,7 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
|
||||
// call after first frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
settings.init().then((_) {
|
||||
_checkAndShowChangelog();
|
||||
if (!kIsWeb && Platform.isMacOS) {
|
||||
// add more delay due to CBManagerStateUnknown
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
@@ -48,6 +51,23 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkAndShowChangelog() async {
|
||||
try {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final currentVersion = packageInfo.version;
|
||||
final lastSeenVersion = settings.getLastSeenVersion();
|
||||
|
||||
if (mounted) {
|
||||
await ChangelogDialog.showIfNeeded(context, currentVersion, lastSeenVersion);
|
||||
}
|
||||
|
||||
// Update last seen version
|
||||
await settings.setLastSeenVersion(currentVersion);
|
||||
} catch (e) {
|
||||
print('Failed to check changelog: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
|
||||
@@ -55,6 +55,13 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
_actionSubscription.cancel();
|
||||
// Exit full screen
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
|
||||
// Reset orientation preferences to allow all orientations
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
windowManager.setFullScreen(false);
|
||||
}
|
||||
@@ -64,6 +71,9 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Force landscape orientation during keymap editing
|
||||
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky, overlays: []);
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
windowManager.setFullScreen(true);
|
||||
@@ -286,12 +296,12 @@ class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text('''1. Create an in-game screenshot of your app (e.g. within MyWhoosh)
|
||||
Text('''1. Create an in-game screenshot of your app (e.g. within MyWhoosh) in landscape orientation
|
||||
2. Load the screenshot with the button below
|
||||
3. Make sure the app is in the correct orientation (portrait or landscape)
|
||||
3. The app is automatically set to landscape orientation for accurate mapping
|
||||
4. Press a button on your Zwift device to create a touch area
|
||||
5. Drag the touch areas to the desired position on the screenshot
|
||||
5. Save and close this screen'''),
|
||||
6. Save and close this screen'''),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_pickScreenshot();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:keypress_simulator/keypress_simulator.dart';
|
||||
import 'package:swift_control/utils/actions/base_actions.dart';
|
||||
import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/widgets/keymap_explanation.dart';
|
||||
|
||||
class DesktopActions extends BaseActions {
|
||||
// Track keys that are currently held down in long press mode
|
||||
@@ -14,7 +15,7 @@ class DesktopActions extends BaseActions {
|
||||
|
||||
final keyPair = supportedApp!.keymap.getKeyPair(action);
|
||||
if (keyPair == null) {
|
||||
return ('Keymap entry not found for action: $action');
|
||||
return ('Keymap entry not found for action: ${action.toString().splitByUpperCase()}');
|
||||
}
|
||||
|
||||
// Handle long press mode
|
||||
|
||||
79
lib/utils/changelog.dart
Normal file
79
lib/utils/changelog.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class ChangelogEntry {
|
||||
final String version;
|
||||
final String date;
|
||||
final List<String> changes;
|
||||
|
||||
ChangelogEntry({
|
||||
required this.version,
|
||||
required this.date,
|
||||
required this.changes,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '### $version ($date)\n${changes.map((c) => '- $c').join('\n')}';
|
||||
}
|
||||
}
|
||||
|
||||
class ChangelogParser {
|
||||
static Future<List<ChangelogEntry>> parse() async {
|
||||
final content = await rootBundle.loadString('CHANGELOG.md');
|
||||
return parseContent(content);
|
||||
}
|
||||
|
||||
static List<ChangelogEntry> parseContent(String content) {
|
||||
final entries = <ChangelogEntry>[];
|
||||
final lines = content.split('\n');
|
||||
|
||||
ChangelogEntry? currentEntry;
|
||||
|
||||
for (var line in lines) {
|
||||
// Check if this is a version header (e.g., "### 2.6.0 (2025-09-28)")
|
||||
if (line.startsWith('### ')) {
|
||||
// Save previous entry if exists
|
||||
if (currentEntry != null) {
|
||||
entries.add(currentEntry);
|
||||
}
|
||||
|
||||
// Parse new entry
|
||||
final header = line.substring(4).trim();
|
||||
final match = RegExp(r'^(\S+)\s+\(([^)]+)\)').firstMatch(header);
|
||||
if (match != null) {
|
||||
currentEntry = ChangelogEntry(
|
||||
version: match.group(1)!,
|
||||
date: match.group(2)!,
|
||||
changes: [],
|
||||
);
|
||||
}
|
||||
} else if (line.startsWith('- ') && currentEntry != null) {
|
||||
// Add change to current entry
|
||||
currentEntry.changes.add(line.substring(2).trim());
|
||||
} else if (line.startsWith(' - ') && currentEntry != null) {
|
||||
// Sub-bullet point
|
||||
currentEntry.changes.add(line.substring(4).trim());
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last entry
|
||||
if (currentEntry != null) {
|
||||
entries.add(currentEntry);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
static Future<ChangelogEntry?> getLatestEntry() async {
|
||||
final entries = await parse();
|
||||
return entries.isNotEmpty ? entries.first : null;
|
||||
}
|
||||
|
||||
static Future<String?> getLatestEntryForPlayStore() async {
|
||||
final entry = await getLatestEntry();
|
||||
if (entry == null) return null;
|
||||
|
||||
// Format for Play Store: just the changes, no version header
|
||||
return entry.changes.join('\n');
|
||||
}
|
||||
}
|
||||
@@ -37,10 +37,26 @@ class Settings {
|
||||
actionHandler.init(null);
|
||||
}
|
||||
|
||||
void setApp(SupportedApp app) {
|
||||
Future<void> setApp(SupportedApp app) async {
|
||||
if (app is CustomApp) {
|
||||
_prefs.setStringList("customapp", app.encodeKeymap());
|
||||
await _prefs.setStringList("customapp", app.encodeKeymap());
|
||||
}
|
||||
_prefs.setString('app', app.name);
|
||||
await _prefs.setString('app', app.name);
|
||||
}
|
||||
|
||||
String? getLastSeenVersion() {
|
||||
return _prefs.getString('last_seen_version');
|
||||
}
|
||||
|
||||
Future<void> setLastSeenVersion(String version) async {
|
||||
await _prefs.setString('last_seen_version', version);
|
||||
}
|
||||
|
||||
bool getVibrationEnabled() {
|
||||
return _prefs.getBool('vibration_enabled') ?? true;
|
||||
}
|
||||
|
||||
Future<void> setVibrationEnabled(bool enabled) async {
|
||||
await _prefs.setBool('vibration_enabled', enabled);
|
||||
}
|
||||
}
|
||||
|
||||
59
lib/widgets/changelog_dialog.dart
Normal file
59
lib/widgets/changelog_dialog.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/utils/changelog.dart';
|
||||
|
||||
class ChangelogDialog extends StatelessWidget {
|
||||
final ChangelogEntry entry;
|
||||
|
||||
const ChangelogDialog({super.key, required this.entry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('What\'s New'),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Version ${entry.version}',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.normal),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children:
|
||||
entry.changes
|
||||
.map(
|
||||
(change) => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('• ', style: TextStyle(fontSize: 16)),
|
||||
Expanded(child: Text(change, style: Theme.of(context).textTheme.bodyMedium)),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text('Got it!'))],
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> showIfNeeded(BuildContext context, String currentVersion, String? lastSeenVersion) async {
|
||||
// Show dialog if this is a new version
|
||||
if (lastSeenVersion != currentVersion) {
|
||||
try {
|
||||
final entry = await ChangelogParser.getLatestEntry();
|
||||
if (entry != null && context.mounted) {
|
||||
showDialog(context: context, builder: (context) => ChangelogDialog(entry: entry));
|
||||
}
|
||||
} catch (e) {
|
||||
print('Failed to load changelog for dialog: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import 'package:swift_control/utils/keymap/buttons.dart';
|
||||
import 'package:swift_control/widgets/title.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import '../pages/changelog.dart';
|
||||
import '../pages/device.dart';
|
||||
|
||||
List<Widget> buildMenuButtons() {
|
||||
@@ -91,6 +92,12 @@ class MenuButton extends StatelessWidget {
|
||||
),
|
||||
PopupMenuItem(child: PopupMenuDivider()),
|
||||
],
|
||||
PopupMenuItem(
|
||||
child: Text('Changelog'),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (c) => ChangelogPage()));
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text('Feedback'),
|
||||
onTap: () {
|
||||
|
||||
@@ -308,13 +308,15 @@ class _KeyboardToast extends StatelessWidget {
|
||||
final t = (age.inMilliseconds / duration.inMilliseconds.clamp(1, 1 << 30)).clamp(0.0, 1.0);
|
||||
final fade = 1.0 - t;
|
||||
|
||||
return Opacity(
|
||||
opacity: fade,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(color: badgeColor, borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(text, style: textStyle),
|
||||
return Material(
|
||||
child: Opacity(
|
||||
opacity: fade,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(color: badgeColor, borderRadius: BorderRadius.circular(12)),
|
||||
child: Text(text, style: textStyle),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class _AppTitleState extends State<AppTitle> {
|
||||
}
|
||||
|
||||
void _checkForUpdate() async {
|
||||
if (Platform.isAndroid) {
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
try {
|
||||
final appUpdateInfo = await InAppUpdate.checkForUpdate();
|
||||
if (context.mounted && appUpdateInfo.updateAvailability == UpdateAvailability.updateAvailable) {
|
||||
@@ -117,7 +117,7 @@ class _AppTitleState extends State<AppTitle> {
|
||||
Text('SwiftControl'),
|
||||
if (_packageInfoValue != null)
|
||||
Text(
|
||||
'v${_packageInfoValue!.version}+${_packageInfoValue!.buildNumber}',
|
||||
'v${_packageInfoValue!.version}',
|
||||
style: TextStyle(fontFamily: "monospace", fontFamilyFallback: <String>["Courier"], fontSize: 12),
|
||||
)
|
||||
else
|
||||
|
||||
@@ -20,7 +20,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- window_manager (0.2.0):
|
||||
- window_manager (0.5.0):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
@@ -71,7 +71,7 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
window_manager: e25faf20d88283a0d46e7b1a759d07261ca27575
|
||||
|
||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||
|
||||
|
||||
162
pubspec.lock
162
pubspec.lock
@@ -12,10 +12,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12"
|
||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.5"
|
||||
version: "4.0.7"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -60,10 +60,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.0.4"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -132,18 +132,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "306b78788d1bb569edb7c55d622953c2414ca12445b41c9117963e03afc5c513"
|
||||
sha256: "49413c8ca514dea7633e8def233b25efdf83ec8522955cc2c0e3ad802927e7c6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.3.3"
|
||||
version: "12.1.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
|
||||
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
version: "7.0.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -180,10 +180,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
|
||||
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+2"
|
||||
version: "0.9.4+4"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -233,26 +233,26 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
|
||||
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.3"
|
||||
version: "0.14.4"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
|
||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
version: "6.0.0"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: a9966c850de5e445331b854fa42df96a8020066d67f125a5964cbc6556643f68
|
||||
sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "19.4.1"
|
||||
version: "19.4.2"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -273,18 +273,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_windows
|
||||
sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98
|
||||
sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.0.3"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3"
|
||||
sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.27"
|
||||
version: "2.0.30"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -307,10 +307,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.5.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -331,66 +331,66 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
|
||||
sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.2.0"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9"
|
||||
sha256: "8dfe08ea7fcf7467dbaf6889e72eebd5e0d6711caae201fdac780eb45232cd02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+22"
|
||||
version: "0.8.13+3"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
|
||||
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.1.0"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
|
||||
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+2"
|
||||
version: "0.8.13"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
|
||||
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+2"
|
||||
version: "0.2.2"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
|
||||
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+2"
|
||||
version: "0.2.2"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
|
||||
sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.1"
|
||||
version: "2.11.0"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_windows
|
||||
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
||||
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
version: "0.2.2"
|
||||
in_app_update:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -447,10 +447,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.1"
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -471,10 +471,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
|
||||
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
version: "6.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -519,18 +519,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
|
||||
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.0"
|
||||
version: "9.0.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -567,26 +567,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
|
||||
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.4.0"
|
||||
version: "12.0.1"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
|
||||
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.1.0"
|
||||
version: "13.0.1"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98
|
||||
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.6"
|
||||
version: "9.4.7"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -615,10 +615,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
version: "7.0.1"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -647,18 +647,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
version: "6.0.3"
|
||||
protobuf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
|
||||
sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "4.2.0"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -711,10 +711,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad"
|
||||
sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.8"
|
||||
version: "2.4.13"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -820,10 +820,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d
|
||||
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.0"
|
||||
version: "0.10.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -852,26 +852,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
version: "6.3.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4"
|
||||
sha256: "199bc33e746088546a39cc5f36bac5a278c5e53b40cb3196f99e7345fdcfae6b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.15"
|
||||
version: "6.3.22"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
||||
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
version: "6.3.4"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -884,10 +884,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
version: "3.2.3"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -900,10 +900,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
|
||||
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.1"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -924,10 +924,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
version: "15.0.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -940,10 +940,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
|
||||
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.12.0"
|
||||
version: "5.14.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -956,10 +956,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: window_manager
|
||||
sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059"
|
||||
sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
version: "0.5.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -972,10 +972,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -985,5 +985,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
|
||||
14
pubspec.yaml
14
pubspec.yaml
@@ -14,18 +14,18 @@ dependencies:
|
||||
flutter_local_notifications: ^19.4.1
|
||||
universal_ble: ^0.21.1
|
||||
intl: any
|
||||
protobuf: ^3.1.0
|
||||
permission_handler: ^11.4.0
|
||||
protobuf: ^4.2.0
|
||||
permission_handler: ^12.0.1
|
||||
dartx: any
|
||||
image_picker: ^1.1.2
|
||||
pointycastle: any
|
||||
window_manager: ^0.4.3
|
||||
device_info_plus: ^11.3.3
|
||||
window_manager: ^0.5.1
|
||||
device_info_plus: ^12.1.0
|
||||
keypress_simulator:
|
||||
path: keypress_simulator/packages/keypress_simulator
|
||||
shared_preferences: ^2.5.3
|
||||
flex_color_scheme: ^8.3.0
|
||||
package_info_plus: ^8.3.0
|
||||
package_info_plus: ^9.0.0
|
||||
in_app_update: ^4.2.5
|
||||
accessibility:
|
||||
path: accessibility
|
||||
@@ -36,7 +36,9 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
|
||||
flutter_launcher_icons: "^0.14.3"
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- CHANGELOG.md
|
||||
|
||||
16
scripts/get_latest_changelog.sh
Executable file
16
scripts/get_latest_changelog.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Script to extract the latest changelog entry for Play Store uploads
|
||||
# Usage: ./scripts/get_latest_changelog.sh
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
CHANGELOG_FILE="$PROJECT_ROOT/CHANGELOG.md"
|
||||
|
||||
if [ ! -f "$CHANGELOG_FILE" ]; then
|
||||
echo "Error: CHANGELOG.md not found at $CHANGELOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the first changelog entry (between first ### and second ###)
|
||||
awk '/^### / {if (count++) exit} count' "$CHANGELOG_FILE" | tail -n +2 | sed 's/^- /• /'
|
||||
69
test/changelog_test.dart
Normal file
69
test/changelog_test.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:swift_control/utils/changelog.dart';
|
||||
|
||||
void main() {
|
||||
group('ChangelogParser', () {
|
||||
test('parses changelog entries correctly', () {
|
||||
const testContent = '''
|
||||
### 2.6.0 (2025-09-28)
|
||||
- Fix crashes on some Android devices
|
||||
- refactor touch placements: show touches on screen
|
||||
- show firmware version of connected device
|
||||
|
||||
### 2.5.0 (2025-09-25)
|
||||
- Improve usability
|
||||
- SwiftControl is now available via the Play Store
|
||||
- SwiftControl will continue to be available to download for free on GitHub
|
||||
''';
|
||||
|
||||
final entries = ChangelogParser.parseContent(testContent);
|
||||
|
||||
expect(entries.length, 2);
|
||||
|
||||
expect(entries[0].version, '2.6.0');
|
||||
expect(entries[0].date, '2025-09-28');
|
||||
expect(entries[0].changes.length, 3);
|
||||
expect(entries[0].changes[0], 'Fix crashes on some Android devices');
|
||||
|
||||
expect(entries[1].version, '2.5.0');
|
||||
expect(entries[1].date, '2025-09-25');
|
||||
expect(entries[1].changes.length, 3);
|
||||
expect(entries[1].changes[0], 'Improve usability');
|
||||
expect(entries[1].changes[1], 'SwiftControl is now available via the Play Store');
|
||||
expect(entries[1].changes[2], 'SwiftControl will continue to be available to download for free on GitHub');
|
||||
});
|
||||
|
||||
test('handles empty content', () {
|
||||
const testContent = '';
|
||||
final entries = ChangelogParser.parseContent(testContent);
|
||||
expect(entries.length, 0);
|
||||
});
|
||||
|
||||
test('handles single entry', () {
|
||||
const testContent = '''
|
||||
### 1.0.0 (2025-01-01)
|
||||
- Initial release
|
||||
''';
|
||||
|
||||
final entries = ChangelogParser.parseContent(testContent);
|
||||
|
||||
expect(entries.length, 1);
|
||||
expect(entries[0].version, '1.0.0');
|
||||
expect(entries[0].changes.length, 1);
|
||||
expect(entries[0].changes[0], 'Initial release');
|
||||
});
|
||||
|
||||
test('ChangelogEntry toString formats correctly', () {
|
||||
final entry = ChangelogEntry(
|
||||
version: '1.0.0',
|
||||
date: '2025-01-01',
|
||||
changes: ['Change 1', 'Change 2'],
|
||||
);
|
||||
|
||||
final result = entry.toString();
|
||||
expect(result, contains('### 1.0.0 (2025-01-01)'));
|
||||
expect(result, contains('- Change 1'));
|
||||
expect(result, contains('- Change 2'));
|
||||
});
|
||||
});
|
||||
}
|
||||
58
test/orientation_test.dart
Normal file
58
test/orientation_test.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:swift_control/pages/touch_area.dart';
|
||||
|
||||
void main() {
|
||||
group('TouchAreaSetupPage Orientation Tests', () {
|
||||
testWidgets('TouchAreaSetupPage should force landscape orientation on init', (WidgetTester tester) async {
|
||||
// Track system chrome method calls
|
||||
final List<MethodCall> systemChromeCalls = [];
|
||||
|
||||
// Mock SystemChrome.setPreferredOrientations
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||
systemChromeCalls.add(methodCall);
|
||||
return null;
|
||||
});
|
||||
|
||||
// Build the TouchAreaSetupPage
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: TouchAreaSetupPage(),
|
||||
),
|
||||
);
|
||||
|
||||
// Verify that setPreferredOrientations was called with landscape orientations
|
||||
final orientationCalls = systemChromeCalls
|
||||
.where((call) => call.method == 'SystemChrome.setPreferredOrientations')
|
||||
.toList();
|
||||
|
||||
expect(orientationCalls, isNotEmpty);
|
||||
|
||||
// Check if landscape orientations were set
|
||||
final lastOrientationCall = orientationCalls.last;
|
||||
final orientations = lastOrientationCall.arguments as List<String>;
|
||||
|
||||
expect(orientations, contains('DeviceOrientation.landscapeLeft'));
|
||||
expect(orientations, contains('DeviceOrientation.landscapeRight'));
|
||||
expect(orientations, hasLength(2)); // Only landscape orientations
|
||||
});
|
||||
|
||||
test('DeviceOrientation enum values are accessible', () {
|
||||
// Test that we can access the DeviceOrientation enum values
|
||||
final orientations = [
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
];
|
||||
|
||||
expect(orientations, hasLength(4));
|
||||
expect(orientations, contains(DeviceOrientation.landscapeLeft));
|
||||
expect(orientations, contains(DeviceOrientation.landscapeRight));
|
||||
expect(orientations, contains(DeviceOrientation.portraitUp));
|
||||
expect(orientations, contains(DeviceOrientation.portraitDown));
|
||||
});
|
||||
});
|
||||
}
|
||||
56
test/vibration_setting_test.dart
Normal file
56
test/vibration_setting_test.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:swift_control/utils/settings/settings.dart';
|
||||
|
||||
void main() {
|
||||
group('Vibration Setting Tests', () {
|
||||
late Settings settings;
|
||||
|
||||
setUp(() async {
|
||||
// Initialize SharedPreferences with in-memory storage for testing
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
settings = Settings();
|
||||
await settings.init();
|
||||
});
|
||||
|
||||
test('Vibration setting should default to true', () {
|
||||
expect(settings.getVibrationEnabled(), true);
|
||||
});
|
||||
|
||||
test('Vibration setting should persist when set to false', () async {
|
||||
await settings.setVibrationEnabled(false);
|
||||
expect(settings.getVibrationEnabled(), false);
|
||||
});
|
||||
|
||||
test('Vibration setting should persist when set to true', () async {
|
||||
await settings.setVibrationEnabled(true);
|
||||
expect(settings.getVibrationEnabled(), true);
|
||||
});
|
||||
|
||||
test('Vibration setting should toggle correctly', () async {
|
||||
// Start with default (true)
|
||||
expect(settings.getVibrationEnabled(), true);
|
||||
|
||||
// Toggle to false
|
||||
await settings.setVibrationEnabled(false);
|
||||
expect(settings.getVibrationEnabled(), false);
|
||||
|
||||
// Toggle back to true
|
||||
await settings.setVibrationEnabled(true);
|
||||
expect(settings.getVibrationEnabled(), true);
|
||||
});
|
||||
|
||||
test('Vibration setting should persist across Settings instances', () async {
|
||||
// Set vibration to false
|
||||
await settings.setVibrationEnabled(false);
|
||||
expect(settings.getVibrationEnabled(), false);
|
||||
|
||||
// Create a new Settings instance
|
||||
final newSettings = Settings();
|
||||
await newSettings.init();
|
||||
|
||||
// Should still be false
|
||||
expect(newSettings.getVibrationEnabled(), false);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user