Merge branch 'web'

This commit is contained in:
Jonas Bark
2025-09-30 15:18:54 +02:00
30 changed files with 9412 additions and 240 deletions

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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,
]),
);
}
}

View File

@@ -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,
);
}
}

File diff suppressed because it is too large Load Diff

View 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');

File diff suppressed because it is too large Load Diff

View 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
View 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,
),
),
],
),
),
),
],
),
),
);
},
),
);
}
}

View File

@@ -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(),

View File

@@ -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);

View File

@@ -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();

View File

@@ -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
View 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');
}
}

View File

@@ -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);
}
}

View 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');
}
}
}
}

View File

@@ -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: () {

View File

@@ -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),
),
),
);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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
View 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
View 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'));
});
});
}

View 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));
});
});
}

View 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);
});
});
}