feat: working connection, conn setting, and gear ratio setting for universal shifters
This commit is contained in:
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
|
||||||
|
|||||||
@ -18,8 +18,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "8.2.1" apply false
|
id "com.android.application" version "8.6.0" apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
|||||||
BIN
assets/images/shifter-wireframe.png
Normal file
BIN
assets/images/shifter-wireframe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 283 KiB |
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
3
flutter_rust_bridge.yaml
Normal file
3
flutter_rust_bridge.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
rust_input: crate::api
|
||||||
|
rust_root: rust/
|
||||||
|
dart_output: lib/src/rust
|
||||||
13
integration_test/simple_test.dart
Normal file
13
integration_test/simple_test.dart
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:abawo_bt_app/main.dart';
|
||||||
|
import 'package:abawo_bt_app/src/rust/frb_generated.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
setUpAll(() async => await RustLib.init());
|
||||||
|
testWidgets('Can call rust function', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const MyApp());
|
||||||
|
expect(find.textContaining('Result: `Hello, Tom!`'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,131 +1,358 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:anyhow/anyhow.dart';
|
import 'package:anyhow/anyhow.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'
|
||||||
|
hide ConnectionStatus, Result, Logger;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'bluetooth.g.dart';
|
part 'bluetooth.g.dart';
|
||||||
|
|
||||||
final log = Logger('BluetoothController');
|
final log = Logger('BluetoothController');
|
||||||
|
|
||||||
@riverpod
|
@Riverpod(keepAlive: true)
|
||||||
|
FlutterReactiveBle reactiveBle(Ref ref) {
|
||||||
|
ref.keepAlive();
|
||||||
|
return FlutterReactiveBle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
Future<BluetoothController> bluetooth(Ref ref) async {
|
Future<BluetoothController> bluetooth(Ref ref) async {
|
||||||
final controller = BluetoothController();
|
ref.keepAlive();
|
||||||
log.info(await controller.init());
|
final controller = BluetoothController(ref.read(reactiveBleProvider));
|
||||||
|
await controller.init();
|
||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Stream<(ConnectionStatus, String?)> connectionStatus(Ref ref) {
|
||||||
|
final asyncController = ref.watch(bluetoothProvider);
|
||||||
|
return asyncController.when(
|
||||||
|
data: (controller) => controller.connectionStateStream,
|
||||||
|
loading: () => Stream.value((ConnectionStatus.disconnected, null)),
|
||||||
|
error: (_, __) => Stream.value((ConnectionStatus.disconnected, null)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the connection status of the Bluetooth device.
|
||||||
|
enum ConnectionStatus { disconnected, connecting, connected, disconnecting }
|
||||||
|
|
||||||
class BluetoothController {
|
class BluetoothController {
|
||||||
StreamSubscription<BluetoothAdapterState>? _btStateSubscription;
|
BluetoothController(this._ble);
|
||||||
StreamSubscription<List<ScanResult>>? _scanResultsSubscription;
|
|
||||||
List<ScanResult> _latestScanResults = [];
|
static const int defaultMtu = 64;
|
||||||
|
|
||||||
|
final FlutterReactiveBle _ble;
|
||||||
|
|
||||||
|
StreamSubscription<BleStatus>? _bleStatusSubscription;
|
||||||
|
StreamSubscription<DiscoveredDevice>? _scanResultsSubscription;
|
||||||
|
Timer? _scanTimeout;
|
||||||
|
final Map<String, DiscoveredDevice> _scanResultsById = {};
|
||||||
|
final _scanResultsSubject =
|
||||||
|
BehaviorSubject<List<DiscoveredDevice>>.seeded(const []);
|
||||||
|
final _isScanningSubject = BehaviorSubject<bool>.seeded(false);
|
||||||
|
|
||||||
|
String? _connectedDeviceId;
|
||||||
|
StreamSubscription<ConnectionStateUpdate>? _connectionStateSubscription;
|
||||||
|
final _connectionStateSubject =
|
||||||
|
BehaviorSubject<(ConnectionStatus, String?)>.seeded(
|
||||||
|
(ConnectionStatus.disconnected, null));
|
||||||
|
|
||||||
|
Stream<(ConnectionStatus, String?)> get connectionStateStream =>
|
||||||
|
_connectionStateSubject.stream;
|
||||||
|
|
||||||
|
(ConnectionStatus, String?) get currentConnectionState =>
|
||||||
|
_connectionStateSubject.value;
|
||||||
|
|
||||||
|
Stream<List<DiscoveredDevice>> get scanResultsStream =>
|
||||||
|
_scanResultsSubject.stream;
|
||||||
|
|
||||||
|
Stream<bool> get isScanningStream => _isScanningSubject.stream;
|
||||||
|
|
||||||
|
List<DiscoveredDevice> get scanResults => _scanResultsSubject.value;
|
||||||
|
|
||||||
Future<Result<void>> init() async {
|
Future<Result<void>> init() async {
|
||||||
if (await FlutterBluePlus.isSupported == false) {
|
_bleStatusSubscription ??= _ble.statusStream.listen((status) {
|
||||||
log.severe("Bluetooth is not supported on this device!");
|
log.info('BLE status: $status');
|
||||||
return bail("Bluetooth is not supported on this device!");
|
|
||||||
}
|
|
||||||
|
|
||||||
_btStateSubscription =
|
|
||||||
FlutterBluePlus.adapterState.listen((BluetoothAdapterState state) {
|
|
||||||
if (state == BluetoothAdapterState.on) {
|
|
||||||
log.info("Bluetooth is on!");
|
|
||||||
// usually start scanning, connecting, etc
|
|
||||||
} else {
|
|
||||||
log.info("Bluetooth is off!");
|
|
||||||
// show an error to the user, etc
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!kIsWeb && Platform.isAndroid) {
|
|
||||||
await FlutterBluePlus.turnOn();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(null);
|
return Ok(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start scanning for Bluetooth devices
|
|
||||||
///
|
|
||||||
/// [withServices] - Optional list of service UUIDs to filter devices by
|
|
||||||
/// [withNames] - Optional list of device names to filter by
|
|
||||||
/// [timeout] - Optional duration after which scanning will automatically stop
|
|
||||||
Future<Result<void>> startScan({
|
Future<Result<void>> startScan({
|
||||||
List<Guid>? withServices,
|
List<Uuid>? withServices,
|
||||||
List<String>? withNames,
|
|
||||||
Duration? timeout,
|
Duration? timeout,
|
||||||
|
ScanMode scanMode = ScanMode.lowLatency,
|
||||||
|
bool requireLocationServicesEnabled = true,
|
||||||
}) async {
|
}) async {
|
||||||
|
if (_isScanningSubject.value) {
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Wait for Bluetooth to be enabled
|
final status = _ble.status;
|
||||||
await FlutterBluePlus.adapterState
|
if (status != BleStatus.ready) {
|
||||||
.where((val) => val == BluetoothAdapterState.on)
|
await _ble.statusStream
|
||||||
.first;
|
.where((value) => value == BleStatus.ready)
|
||||||
|
.first;
|
||||||
|
}
|
||||||
|
|
||||||
// Set up scan results listener
|
_scanTimeout?.cancel();
|
||||||
_scanResultsSubscription = FlutterBluePlus.onScanResults.listen(
|
_scanResultsById.clear();
|
||||||
(results) {
|
_scanResultsSubject.add(const []);
|
||||||
if (results.isNotEmpty) {
|
_isScanningSubject.add(true);
|
||||||
_latestScanResults = results;
|
|
||||||
ScanResult latestResult = results.last;
|
|
||||||
log.info(
|
|
||||||
'${latestResult.device.remoteId}: "${latestResult.advertisementData.advName}" found!');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (e) {
|
|
||||||
log.severe('Scan error: $e');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clean up subscription when scanning completes
|
_scanResultsSubscription = _ble
|
||||||
FlutterBluePlus.cancelWhenScanComplete(_scanResultsSubscription!);
|
.scanForDevices(
|
||||||
|
withServices: withServices ?? const [],
|
||||||
|
scanMode: scanMode,
|
||||||
|
requireLocationServicesEnabled: requireLocationServicesEnabled,
|
||||||
|
)
|
||||||
|
.listen((device) {
|
||||||
|
_scanResultsById[device.id] = device;
|
||||||
|
_scanResultsSubject
|
||||||
|
.add(_scanResultsById.values.toList(growable: false));
|
||||||
|
}, onError: (Object error, StackTrace st) {
|
||||||
|
log.severe('Scan error: $error', error, st);
|
||||||
|
_isScanningSubject.add(false);
|
||||||
|
});
|
||||||
|
|
||||||
// Start scanning with optional parameters
|
if (timeout != null) {
|
||||||
await FlutterBluePlus.startScan(
|
_scanTimeout = Timer(timeout, () {
|
||||||
withServices: withServices ?? [],
|
unawaited(stopScan());
|
||||||
withNames: withNames ?? [],
|
});
|
||||||
timeout: timeout,
|
}
|
||||||
);
|
|
||||||
|
|
||||||
return Ok(null);
|
return Ok(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
_isScanningSubject.add(false);
|
||||||
return bail('Failed to start Bluetooth scan: $e');
|
return bail('Failed to start Bluetooth scan: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop an ongoing Bluetooth scan
|
|
||||||
Future<Result<void>> stopScan() async {
|
Future<Result<void>> stopScan() async {
|
||||||
try {
|
try {
|
||||||
await FlutterBluePlus.stopScan();
|
_scanTimeout?.cancel();
|
||||||
|
_scanTimeout = null;
|
||||||
|
await _scanResultsSubscription?.cancel();
|
||||||
|
_scanResultsSubscription = null;
|
||||||
|
_isScanningSubject.add(false);
|
||||||
return Ok(null);
|
return Ok(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
_isScanningSubject.add(false);
|
||||||
return bail('Failed to stop Bluetooth scan: $e');
|
return bail('Failed to stop Bluetooth scan: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the latest scan results
|
|
||||||
List<ScanResult> get scanResults => _latestScanResults;
|
|
||||||
|
|
||||||
/// Wait for the current scan to complete
|
|
||||||
Future<Result<void>> waitForScanToComplete() async {
|
Future<Result<void>> waitForScanToComplete() async {
|
||||||
try {
|
try {
|
||||||
await FlutterBluePlus.isScanning.where((val) => val == false).first;
|
await isScanningStream.where((val) => val == false).first;
|
||||||
return Ok(null);
|
return Ok(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return bail('Error waiting for scan to complete: $e');
|
return bail('Error waiting for scan to complete: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if currently scanning
|
Future<bool> get isScanning async => isScanningStream.first;
|
||||||
Future<bool> get isScanning async {
|
|
||||||
return await FlutterBluePlus.isScanning.first;
|
Future<Result<void>> connect(DiscoveredDevice device,
|
||||||
|
{Duration? timeout}) async {
|
||||||
|
return connectById(device.id, timeout: timeout ?? Duration(seconds: 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> connectById(
|
||||||
|
String deviceId, {
|
||||||
|
Duration timeout = const Duration(seconds: 10),
|
||||||
|
Map<Uuid, List<Uuid>>? servicesWithCharacteristicsToDiscover,
|
||||||
|
}) async {
|
||||||
|
final currentState = currentConnectionState;
|
||||||
|
final currentDeviceId = currentState.$2;
|
||||||
|
|
||||||
|
if (deviceId == currentDeviceId &&
|
||||||
|
(currentState.$1 == ConnectionStatus.connected ||
|
||||||
|
currentState.$1 == ConnectionStatus.connecting)) {
|
||||||
|
log.info('Already connected or connecting to $deviceId.');
|
||||||
|
if (currentState.$1 == ConnectionStatus.connected) {
|
||||||
|
unawaited(_requestMtuOnConnect(deviceId));
|
||||||
|
}
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDeviceId != null && deviceId != currentDeviceId) {
|
||||||
|
final disconnectResult = await disconnect();
|
||||||
|
if (disconnectResult.isErr()) {
|
||||||
|
return disconnectResult
|
||||||
|
.context('Failed to disconnect from previous device');
|
||||||
|
}
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _connectionStateSubscription?.cancel();
|
||||||
|
_updateConnectionState(ConnectionStatus.connecting, deviceId);
|
||||||
|
|
||||||
|
_connectionStateSubscription = _ble
|
||||||
|
.connectToDevice(
|
||||||
|
id: deviceId,
|
||||||
|
connectionTimeout: timeout,
|
||||||
|
servicesWithCharacteristicsToDiscover:
|
||||||
|
servicesWithCharacteristicsToDiscover,
|
||||||
|
)
|
||||||
|
.listen((update) {
|
||||||
|
switch (update.connectionState) {
|
||||||
|
case DeviceConnectionState.connected:
|
||||||
|
_connectedDeviceId = deviceId;
|
||||||
|
_updateConnectionState(ConnectionStatus.connected, deviceId);
|
||||||
|
unawaited(_requestMtuOnConnect(deviceId));
|
||||||
|
break;
|
||||||
|
case DeviceConnectionState.connecting:
|
||||||
|
_updateConnectionState(ConnectionStatus.connecting, deviceId);
|
||||||
|
break;
|
||||||
|
case DeviceConnectionState.disconnecting:
|
||||||
|
_updateConnectionState(ConnectionStatus.disconnecting, deviceId);
|
||||||
|
break;
|
||||||
|
case DeviceConnectionState.disconnected:
|
||||||
|
_cleanUpConnection();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, onError: (Object error, StackTrace st) {
|
||||||
|
log.severe('Failed to connect to $deviceId: $error', error, st);
|
||||||
|
_cleanUpConnection();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
_cleanUpConnection();
|
||||||
|
return bail('Failed to connect to $deviceId: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> disconnect() async {
|
||||||
|
final deviceIdToDisconnect =
|
||||||
|
_connectedDeviceId ?? _connectionStateSubject.value.$2;
|
||||||
|
if (deviceIdToDisconnect == null) {
|
||||||
|
_cleanUpConnection();
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateConnectionState(
|
||||||
|
ConnectionStatus.disconnecting, deviceIdToDisconnect);
|
||||||
|
try {
|
||||||
|
await _connectionStateSubscription?.cancel();
|
||||||
|
_connectionStateSubscription = null;
|
||||||
|
_cleanUpConnection();
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
_cleanUpConnection();
|
||||||
|
return bail('Failed to disconnect from $deviceIdToDisconnect: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<List<int>>> readCharacteristic(
|
||||||
|
String deviceId,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final characteristic = QualifiedCharacteristic(
|
||||||
|
serviceId: Uuid.parse(serviceUuid),
|
||||||
|
characteristicId: Uuid.parse(characteristicUuid),
|
||||||
|
deviceId: deviceId,
|
||||||
|
);
|
||||||
|
final value = await _ble.readCharacteristic(characteristic);
|
||||||
|
return Ok(value);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error reading characteristic: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> writeCharacteristic(
|
||||||
|
String deviceId,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid,
|
||||||
|
List<int> value, {
|
||||||
|
bool withResponse = true,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final characteristic = QualifiedCharacteristic(
|
||||||
|
serviceId: Uuid.parse(serviceUuid),
|
||||||
|
characteristicId: Uuid.parse(characteristicUuid),
|
||||||
|
deviceId: deviceId,
|
||||||
|
);
|
||||||
|
if (withResponse) {
|
||||||
|
await _ble.writeCharacteristicWithResponse(
|
||||||
|
characteristic,
|
||||||
|
value: value,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await _ble.writeCharacteristicWithoutResponse(
|
||||||
|
characteristic,
|
||||||
|
value: value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error writing characteristic: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> requestMtu(String deviceId,
|
||||||
|
{int mtu = defaultMtu}) async {
|
||||||
|
try {
|
||||||
|
final negotiatedMtu = await _ble.requestMtu(deviceId: deviceId, mtu: mtu);
|
||||||
|
log.info(
|
||||||
|
'MTU negotiated for $deviceId: requested $mtu, got $negotiatedMtu');
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error requesting MTU $mtu for $deviceId: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _requestMtuOnConnect(String deviceId) async {
|
||||||
|
final mtuResult = await requestMtu(deviceId, mtu: defaultMtu);
|
||||||
|
if (mtuResult.isErr()) {
|
||||||
|
log.warning(
|
||||||
|
'MTU request after connect failed for $deviceId: ${mtuResult.unwrapErr()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<int>> subscribeToCharacteristic(
|
||||||
|
String deviceId,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid,
|
||||||
|
) {
|
||||||
|
final characteristic = QualifiedCharacteristic(
|
||||||
|
serviceId: Uuid.parse(serviceUuid),
|
||||||
|
characteristicId: Uuid.parse(characteristicUuid),
|
||||||
|
deviceId: deviceId,
|
||||||
|
);
|
||||||
|
return _ble.subscribeToCharacteristic(characteristic);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateConnectionState(ConnectionStatus status, String? deviceId) {
|
||||||
|
if (_connectionStateSubject.value.$1 == status &&
|
||||||
|
_connectionStateSubject.value.$2 == deviceId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_connectionStateSubject.add((status, deviceId));
|
||||||
|
log.fine(
|
||||||
|
'Connection state updated: $status, device: ${deviceId ?? 'none'}');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cleanUpConnection() {
|
||||||
|
_connectedDeviceId = null;
|
||||||
|
_updateConnectionState(ConnectionStatus.disconnected, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Result<void>> dispose() async {
|
Future<Result<void>> dispose() async {
|
||||||
|
_scanTimeout?.cancel();
|
||||||
await _scanResultsSubscription?.cancel();
|
await _scanResultsSubscription?.cancel();
|
||||||
await _btStateSubscription?.cancel();
|
await _bleStatusSubscription?.cancel();
|
||||||
|
await disconnect();
|
||||||
|
await _scanResultsSubject.close();
|
||||||
|
await _isScanningSubject.close();
|
||||||
|
await _connectionStateSubject.close();
|
||||||
return Ok(null);
|
return Ok(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,24 @@ part of 'bluetooth.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$bluetoothHash() => r'5e9c37c57e723b84dd08fd8763e7c445b3a4dbf3';
|
String _$bluetoothHash() => r'f1fb75c72a7a473fc545baea6bedfdf4a21ab26b';
|
||||||
|
|
||||||
|
String _$reactiveBleHash() => r'9c4d4f37f7a0da1741b42d6a4c3f0f00c2c07f3c';
|
||||||
|
|
||||||
|
/// See also [reactiveBle].
|
||||||
|
@ProviderFor(reactiveBle)
|
||||||
|
final reactiveBleProvider = AutoDisposeProvider<FlutterReactiveBle>.internal(
|
||||||
|
reactiveBle,
|
||||||
|
name: r'reactiveBleProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$reactiveBleHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef ReactiveBleRef = AutoDisposeProviderRef<FlutterReactiveBle>;
|
||||||
|
|
||||||
/// See also [bluetooth].
|
/// See also [bluetooth].
|
||||||
@ProviderFor(bluetooth)
|
@ProviderFor(bluetooth)
|
||||||
@ -23,5 +40,24 @@ final bluetoothProvider =
|
|||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef BluetoothRef = AutoDisposeFutureProviderRef<BluetoothController>;
|
typedef BluetoothRef = AutoDisposeFutureProviderRef<BluetoothController>;
|
||||||
|
String _$connectionStatusHash() => r'4dba587ef7703dabca4b2b9800e0798decfe2977';
|
||||||
|
|
||||||
|
/// See also [connectionStatus].
|
||||||
|
@ProviderFor(connectionStatus)
|
||||||
|
final connectionStatusProvider =
|
||||||
|
AutoDisposeStreamProvider<(ConnectionStatus, String?)>.internal(
|
||||||
|
connectionStatus,
|
||||||
|
name: r'connectionStatusProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$connectionStatusHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef ConnectionStatusRef
|
||||||
|
= AutoDisposeStreamProviderRef<(ConnectionStatus, String?)>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|||||||
627
lib/controller/bluetooth.old.dart
Normal file
627
lib/controller/bluetooth.old.dart
Normal file
@ -0,0 +1,627 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:anyhow/anyhow.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'bluetooth.g.dart';
|
||||||
|
|
||||||
|
final log = Logger('BluetoothController');
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Future<BluetoothController> bluetooth(Ref ref) async {
|
||||||
|
ref.keepAlive();
|
||||||
|
final controller = BluetoothController();
|
||||||
|
log.info(await controller.init());
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Stream<(ConnectionStatus, BluetoothDevice?)> connectionStatus(Ref ref) {
|
||||||
|
// Get the (potentially still loading) BluetoothController
|
||||||
|
final asyncController = ref.watch(bluetoothProvider);
|
||||||
|
|
||||||
|
// If the controller is ready, return its stream. Otherwise, return an empty stream.
|
||||||
|
// The provider will automatically update when the controller becomes ready.
|
||||||
|
return asyncController.when(
|
||||||
|
data: (controller) => controller.connectionStateStream,
|
||||||
|
loading: () => Stream.value((ConnectionStatus.disconnected, null)),
|
||||||
|
error: (_, __) => Stream.value((ConnectionStatus.disconnected, null)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the connection status of the Bluetooth device.
|
||||||
|
enum ConnectionStatus { disconnected, connecting, connected, disconnecting }
|
||||||
|
|
||||||
|
class BluetoothController {
|
||||||
|
StreamSubscription<BluetoothAdapterState>? _btStateSubscription;
|
||||||
|
StreamSubscription<List<ScanResult>>? _scanResultsSubscription;
|
||||||
|
List<ScanResult> _latestScanResults = [];
|
||||||
|
StreamSubscription<void>? _servicesResetSubscription;
|
||||||
|
final Map<String, Map<Guid, BluetoothService>> _servicesByDevice = {};
|
||||||
|
final Map<String, Map<String, BluetoothCharacteristic>>
|
||||||
|
_characteristicsByDevice = {};
|
||||||
|
// Connection State
|
||||||
|
BluetoothDevice? _connectedDevice;
|
||||||
|
StreamSubscription<BluetoothConnectionState>? _connectionStateSubscription;
|
||||||
|
final _connectionStateSubject =
|
||||||
|
BehaviorSubject<(ConnectionStatus, BluetoothDevice?)>.seeded(
|
||||||
|
(ConnectionStatus.disconnected, null));
|
||||||
|
|
||||||
|
/// Stream providing the current connection status and the connected device (if any).
|
||||||
|
Stream<(ConnectionStatus, BluetoothDevice?)> get connectionStateStream =>
|
||||||
|
_connectionStateSubject.stream;
|
||||||
|
|
||||||
|
/// Gets the latest connection status and device.
|
||||||
|
(ConnectionStatus, BluetoothDevice?) get currentConnectionState =>
|
||||||
|
_connectionStateSubject.value;
|
||||||
|
|
||||||
|
Future<Result<void>> init() async {
|
||||||
|
log.severe("CALLED FBPON!");
|
||||||
|
if (await FlutterBluePlus.isSupported == false) {
|
||||||
|
log.severe("Bluetooth is not supported on this device!");
|
||||||
|
return bail("Bluetooth is not supported on this device!");
|
||||||
|
}
|
||||||
|
|
||||||
|
_btStateSubscription =
|
||||||
|
FlutterBluePlus.adapterState.listen((BluetoothAdapterState state) {
|
||||||
|
if (state == BluetoothAdapterState.on) {
|
||||||
|
log.info("Bluetooth is on!");
|
||||||
|
// usually start scanning, connecting, etc
|
||||||
|
} else {
|
||||||
|
log.info("Bluetooth is off!");
|
||||||
|
// show an error to the user, etc
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kIsWeb && Platform.isAndroid) {
|
||||||
|
await FlutterBluePlus.turnOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionStateStream.listen((state) {
|
||||||
|
log.info('Connection state changed: $state');
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start scanning for Bluetooth devices
|
||||||
|
///
|
||||||
|
/// [withServices] - Optional list of service UUIDs to filter devices by
|
||||||
|
/// [withNames] - Optional list of device names to filter by
|
||||||
|
/// [timeout] - Optional duration after which scanning will automatically stop
|
||||||
|
Future<Result<void>> startScan({
|
||||||
|
List<Guid>? withServices,
|
||||||
|
List<String>? withNames,
|
||||||
|
Duration? timeout,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Wait for Bluetooth to be enabled
|
||||||
|
await FlutterBluePlus.adapterState
|
||||||
|
.where((val) => val == BluetoothAdapterState.on)
|
||||||
|
.first;
|
||||||
|
|
||||||
|
// Set up scan results listener
|
||||||
|
_scanResultsSubscription = FlutterBluePlus.onScanResults.listen(
|
||||||
|
(results) {
|
||||||
|
if (results.isNotEmpty) {
|
||||||
|
_latestScanResults = results;
|
||||||
|
ScanResult latestResult = results.last;
|
||||||
|
log.info(
|
||||||
|
'${latestResult.device.remoteId}: "${latestResult.advertisementData.advName}" found!');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (e) {
|
||||||
|
log.severe('Scan error: $e');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up subscription when scanning completes
|
||||||
|
FlutterBluePlus.cancelWhenScanComplete(_scanResultsSubscription!);
|
||||||
|
|
||||||
|
// Start scanning with optional parameters
|
||||||
|
await FlutterBluePlus.startScan(
|
||||||
|
withServices: withServices ?? [],
|
||||||
|
withNames: withNames ?? [],
|
||||||
|
timeout: timeout,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Failed to start Bluetooth scan: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop an ongoing Bluetooth scan
|
||||||
|
Future<Result<void>> stopScan() async {
|
||||||
|
try {
|
||||||
|
await FlutterBluePlus.stopScan();
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Failed to stop Bluetooth scan: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the latest scan results
|
||||||
|
List<ScanResult> get scanResults => _latestScanResults;
|
||||||
|
|
||||||
|
/// Wait for the current scan to complete
|
||||||
|
Future<Result<void>> waitForScanToComplete() async {
|
||||||
|
try {
|
||||||
|
await FlutterBluePlus.isScanning.where((val) => val == false).first;
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error waiting for scan to complete: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if currently scanning
|
||||||
|
Future<bool> get isScanning async {
|
||||||
|
return await FlutterBluePlus.isScanning.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to a specific Bluetooth device.
|
||||||
|
///
|
||||||
|
/// Ensures that only one device is connected at a time. If another device
|
||||||
|
/// is already connected or connecting, it will be disconnected first.
|
||||||
|
Future<Result<void>> connect(BluetoothDevice device,
|
||||||
|
{Duration? timeout}) async {
|
||||||
|
final currentState = currentConnectionState;
|
||||||
|
final currentDevice = currentState.$2;
|
||||||
|
|
||||||
|
// Prevent connecting if already connected/connecting to the *same* device
|
||||||
|
if (device.remoteId == currentDevice?.remoteId &&
|
||||||
|
(currentState.$1 == ConnectionStatus.connected ||
|
||||||
|
currentState.$1 == ConnectionStatus.connecting)) {
|
||||||
|
log.info('Currently connected device: ${currentState.$2}');
|
||||||
|
log.info('Already connected or connecting to ${device.remoteId}.');
|
||||||
|
return Ok(null); // Or potentially an error/different status?
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Attempting to connect to ${device.remoteId}...');
|
||||||
|
|
||||||
|
// If connecting or connected to a *different* device, disconnect it first.
|
||||||
|
if (currentDevice != null && device.remoteId != currentDevice.remoteId) {
|
||||||
|
log.info(
|
||||||
|
'Disconnecting from previous device ${currentDevice.remoteId} first.');
|
||||||
|
final disconnectResult = await disconnect();
|
||||||
|
if (disconnectResult.isErr()) {
|
||||||
|
return disconnectResult
|
||||||
|
.context('Failed to disconnect from previous device');
|
||||||
|
}
|
||||||
|
// Wait a moment for the disconnection to fully process
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Cancel any previous connection state listener before starting a new one
|
||||||
|
await _connectionStateSubscription?.cancel();
|
||||||
|
_connectionStateSubscription =
|
||||||
|
device.connectionState.listen((BluetoothConnectionState state) async {
|
||||||
|
log.info('[${device.remoteId}] Connection state changed: $state');
|
||||||
|
switch (state) {
|
||||||
|
case BluetoothConnectionState.connected:
|
||||||
|
_connectedDevice = device;
|
||||||
|
_updateConnectionState(ConnectionStatus.connected, device);
|
||||||
|
// IMPORTANT: Discover services after connecting
|
||||||
|
try {
|
||||||
|
_attachServicesResetListener(device);
|
||||||
|
final servicesResult =
|
||||||
|
await _discoverAndCacheServices(device, force: true);
|
||||||
|
if (servicesResult.isErr()) {
|
||||||
|
throw servicesResult.unwrapErr();
|
||||||
|
}
|
||||||
|
log.info(
|
||||||
|
'[${device.remoteId}] Services discovered: \n${servicesResult.unwrap().map((e) => e.uuid.toString()).join('\n')}');
|
||||||
|
} catch (e) {
|
||||||
|
log.severe(
|
||||||
|
'[${device.remoteId}] Error discovering services: $e. Disconnecting.');
|
||||||
|
// Disconnect if service discovery fails, as the connection might be unusable
|
||||||
|
await disconnect();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BluetoothConnectionState.disconnected:
|
||||||
|
if (_connectionStateSubject.value.$1 !=
|
||||||
|
ConnectionStatus.connected) {
|
||||||
|
log.warning(
|
||||||
|
'[${device.remoteId}] Disconnected WITHOUT being connected! Reason: ${device.disconnectReason?.code} ${device.disconnectReason?.description}\nDoing nothing');
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
log.warning(
|
||||||
|
'[${device.remoteId}] Disconnected. Reason: ${device.disconnectReason?.code} ${device.disconnectReason?.description}');
|
||||||
|
// Only clean up if this is the device we were connected/connecting to
|
||||||
|
if (_connectionStateSubject.value.$2?.remoteId ==
|
||||||
|
device.remoteId) {
|
||||||
|
// Clean up connection state, handling disconnection.
|
||||||
|
// In general, reconnection is better, but this is how it's handled here.
|
||||||
|
// App behavior would be to go back to the homepage on disconnection
|
||||||
|
_cleanUpConnection();
|
||||||
|
} else {
|
||||||
|
log.info(
|
||||||
|
'[${device.remoteId}] Received disconnect for a device we were not tracking.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BluetoothConnectionState.connecting:
|
||||||
|
case BluetoothConnectionState.disconnecting:
|
||||||
|
// deprecated states
|
||||||
|
log.warning(
|
||||||
|
'Received unexpected connection state: ${device.connectionState}. This should not happen.');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await device.connect(
|
||||||
|
license: License.free,
|
||||||
|
timeout: timeout ?? const Duration(seconds: 15),
|
||||||
|
mtu: 512,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Note: Success is primarily handled by the connectionState listener
|
||||||
|
log.info(
|
||||||
|
'Connection initiated for ${device.remoteId}. Waiting for state change.');
|
||||||
|
_connectionStateSubject.add((ConnectionStatus.connected, device));
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
log.severe('Failed to connect to ${device.remoteId}: $e');
|
||||||
|
_cleanUpConnection(); // Clean up state on connection failure
|
||||||
|
return bail('Failed to connect to ${device.remoteId}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to a device using its remote ID string with a specific timeout.
|
||||||
|
Future<Result<void>> connectById(String remoteId,
|
||||||
|
{Duration timeout = const Duration(seconds: 10)}) async {
|
||||||
|
log.info('Attempting to connect by ID: $remoteId with timeout: $timeout');
|
||||||
|
try {
|
||||||
|
// Get the BluetoothDevice object from the ID
|
||||||
|
final device = BluetoothDevice.fromId(remoteId);
|
||||||
|
|
||||||
|
// Call the existing connect method, passing the device and timeout
|
||||||
|
// Assumes the 'connect' method below is modified to accept the timeout.
|
||||||
|
return await connect(device, timeout: timeout); // Pass timeout here
|
||||||
|
} catch (e, st) {
|
||||||
|
// Catch potential errors from fromId or during connection setup before connect() is called
|
||||||
|
log.severe('Error connecting by ID $remoteId: $e');
|
||||||
|
_cleanUpConnection(); // Ensure state is cleaned up
|
||||||
|
return bail('Failed to initiate connection for ID $remoteId: $e', st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnects from the currently connected device.
|
||||||
|
Future<Result<void>> disconnect() async {
|
||||||
|
final deviceToDisconnect =
|
||||||
|
_connectedDevice ?? _connectionStateSubject.value.$2;
|
||||||
|
if (deviceToDisconnect == null) {
|
||||||
|
log.info('No device is currently connected or connecting.');
|
||||||
|
// Ensure state is definitely disconnected if called unnecessarily
|
||||||
|
_cleanUpConnection();
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Disconnecting from ${deviceToDisconnect.remoteId}...');
|
||||||
|
_updateConnectionState(ConnectionStatus.disconnecting, deviceToDisconnect);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deviceToDisconnect.disconnect();
|
||||||
|
log.info('Disconnect command sent to ${deviceToDisconnect.remoteId}.');
|
||||||
|
// State update to disconnected is handled by the connectionState listener
|
||||||
|
// but we call cleanup here as a safety measure in case the listener fails
|
||||||
|
_cleanUpConnection();
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
log.severe(
|
||||||
|
'Failed to disconnect from ${deviceToDisconnect.remoteId}: $e');
|
||||||
|
// Even on error, try to clean up the state
|
||||||
|
_cleanUpConnection();
|
||||||
|
return bail(
|
||||||
|
'Failed to disconnect from ${deviceToDisconnect.remoteId}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateConnectionState(
|
||||||
|
ConnectionStatus status, BluetoothDevice? device) {
|
||||||
|
// Avoid emitting redundant states
|
||||||
|
if (_connectionStateSubject.value.$1 == status &&
|
||||||
|
_connectionStateSubject.value.$2?.remoteId == device?.remoteId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_connectionStateSubject.add((status, device));
|
||||||
|
log.fine(
|
||||||
|
'Connection state updated: $status, Device: ${device?.remoteId ?? 'none'}');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<List<BluetoothService>>> discoverServices(
|
||||||
|
BluetoothDevice device, {
|
||||||
|
bool force = false,
|
||||||
|
}) async {
|
||||||
|
return _discoverAndCacheServices(device, force: force);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> writeCharacteristic(
|
||||||
|
BluetoothDevice device,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid,
|
||||||
|
List<int> value, {
|
||||||
|
bool withoutResponse = false,
|
||||||
|
bool allowLongWrite = false,
|
||||||
|
int timeout = 15,
|
||||||
|
}) async {
|
||||||
|
final serviceGuid = Guid(serviceUuid);
|
||||||
|
final characteristicGuid = Guid(characteristicUuid);
|
||||||
|
final chrResult =
|
||||||
|
await _getCharacteristic(device, serviceGuid, characteristicGuid);
|
||||||
|
if (chrResult.isErr()) {
|
||||||
|
return chrResult.context('Failed to resolve characteristic for write');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chrResult.unwrap().write(
|
||||||
|
value,
|
||||||
|
withoutResponse: withoutResponse,
|
||||||
|
allowLongWrite: allowLongWrite,
|
||||||
|
timeout: timeout,
|
||||||
|
);
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error writing characteristic $characteristicUuid: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<StreamSubscription<List<int>>>> subscribeToNotifications(
|
||||||
|
BluetoothDevice device,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid, {
|
||||||
|
void Function(List<int>)? onValue,
|
||||||
|
bool useLastValueStream = false,
|
||||||
|
int timeout = 15,
|
||||||
|
}) async {
|
||||||
|
return _subscribeToCharacteristic(
|
||||||
|
device,
|
||||||
|
serviceUuid,
|
||||||
|
characteristicUuid,
|
||||||
|
useLastValueStream: useLastValueStream,
|
||||||
|
timeout: timeout,
|
||||||
|
forceIndications: false,
|
||||||
|
onValue: onValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<StreamSubscription<List<int>>>> subscribeToIndications(
|
||||||
|
BluetoothDevice device,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid, {
|
||||||
|
void Function(List<int>)? onValue,
|
||||||
|
bool useLastValueStream = false,
|
||||||
|
int timeout = 15,
|
||||||
|
}) async {
|
||||||
|
return _subscribeToCharacteristic(
|
||||||
|
device,
|
||||||
|
serviceUuid,
|
||||||
|
characteristicUuid,
|
||||||
|
useLastValueStream: useLastValueStream,
|
||||||
|
timeout: timeout,
|
||||||
|
forceIndications: true,
|
||||||
|
onValue: onValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> unsubscribeFromCharacteristic(
|
||||||
|
BluetoothDevice device,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid, {
|
||||||
|
int timeout = 15,
|
||||||
|
}) async {
|
||||||
|
final serviceGuid = Guid(serviceUuid);
|
||||||
|
final characteristicGuid = Guid(characteristicUuid);
|
||||||
|
final chrResult =
|
||||||
|
await _getCharacteristic(device, serviceGuid, characteristicGuid);
|
||||||
|
if (chrResult.isErr()) {
|
||||||
|
return chrResult
|
||||||
|
.context('Failed to resolve characteristic to unsubscribe');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chrResult.unwrap().setNotifyValue(false, timeout: timeout);
|
||||||
|
return Ok(null);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error disabling notifications for $characteristicUuid: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to clean up connection resources and state.
|
||||||
|
Future<void> _cleanUpConnection() async {
|
||||||
|
log.fine('Cleaning up connection state and subscriptions.');
|
||||||
|
_connectedDevice = null;
|
||||||
|
await _servicesResetSubscription?.cancel();
|
||||||
|
_servicesResetSubscription = null;
|
||||||
|
_servicesByDevice.clear();
|
||||||
|
_characteristicsByDevice.clear();
|
||||||
|
await _connectionStateSubscription?.cancel();
|
||||||
|
_connectionStateSubscription = null;
|
||||||
|
_updateConnectionState(ConnectionStatus.disconnected, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> dispose() async {
|
||||||
|
await _scanResultsSubscription?.cancel();
|
||||||
|
await _btStateSubscription?.cancel();
|
||||||
|
await disconnect(); // Ensure disconnection on dispose
|
||||||
|
await _connectionStateSubject.close();
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<List<int>>> readCharacteristic(
|
||||||
|
BluetoothDevice device, String svcUuid, String characteristic) async {
|
||||||
|
// Implement reading characteristic logic here
|
||||||
|
// This is a placeholder implementation
|
||||||
|
log.info(
|
||||||
|
'Reading characteristic from device: $device, characteristic: $characteristic');
|
||||||
|
final serviceUUID = Guid(svcUuid);
|
||||||
|
final characteristicUUID = Guid(characteristic);
|
||||||
|
|
||||||
|
if (!device.servicesList.map((e) => e.uuid).contains(serviceUUID)) {
|
||||||
|
return bail('Service $svcUuid not found on device ${device.remoteId}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final BluetoothService service =
|
||||||
|
(device.servicesList).firstWhere((s) => s.uuid == serviceUUID);
|
||||||
|
|
||||||
|
if (service.characteristics.isEmpty ||
|
||||||
|
!service.characteristics
|
||||||
|
.map((c) => c.uuid)
|
||||||
|
.contains(characteristicUUID)) {
|
||||||
|
return bail(
|
||||||
|
'Characteristic $characteristic not found on device ${device.remoteId}');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final val = await service.characteristics
|
||||||
|
.firstWhere((c) => c.uuid == characteristicUUID)
|
||||||
|
.read();
|
||||||
|
return Ok(val);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Error reading characteristic: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _deviceKey(BluetoothDevice device) => device.remoteId.str;
|
||||||
|
|
||||||
|
String _characteristicKey(Guid serviceUuid, Guid characteristicUuid) =>
|
||||||
|
'${serviceUuid.toString()}|${characteristicUuid.toString()}';
|
||||||
|
|
||||||
|
void _cacheServices(BluetoothDevice device, List<BluetoothService> services) {
|
||||||
|
final serviceMap = <Guid, BluetoothService>{};
|
||||||
|
final characteristicMap = <String, BluetoothCharacteristic>{};
|
||||||
|
|
||||||
|
for (final service in services) {
|
||||||
|
serviceMap[service.uuid] = service;
|
||||||
|
for (final chr in service.characteristics) {
|
||||||
|
characteristicMap[_characteristicKey(service.uuid, chr.uuid)] = chr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_servicesByDevice[_deviceKey(device)] = serviceMap;
|
||||||
|
_characteristicsByDevice[_deviceKey(device)] = characteristicMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _attachServicesResetListener(BluetoothDevice device) {
|
||||||
|
_servicesResetSubscription?.cancel();
|
||||||
|
_servicesResetSubscription = device.onServicesReset.listen((_) async {
|
||||||
|
log.info('[${device.remoteId}] Services reset. Re-discovering.');
|
||||||
|
final res = await _discoverAndCacheServices(device, force: true);
|
||||||
|
if (res.isErr()) {
|
||||||
|
log.severe(
|
||||||
|
'[${device.remoteId}] Failed to re-discover services: ${res.unwrapErr()}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
device.cancelWhenDisconnected(_servicesResetSubscription!);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<List<BluetoothService>>> _discoverAndCacheServices(
|
||||||
|
BluetoothDevice device, {
|
||||||
|
bool force = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
if (!force) {
|
||||||
|
final cached = _servicesByDevice[_deviceKey(device)];
|
||||||
|
if (cached != null && cached.isNotEmpty) {
|
||||||
|
return Ok(cached.values.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!force && device.servicesList.isNotEmpty) {
|
||||||
|
_cacheServices(device, device.servicesList);
|
||||||
|
return Ok(device.servicesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
final services = await device.discoverServices();
|
||||||
|
_cacheServices(device, services);
|
||||||
|
return Ok(services);
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Failed to discover services for ${device.remoteId}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<BluetoothCharacteristic>> _getCharacteristic(
|
||||||
|
BluetoothDevice device,
|
||||||
|
Guid serviceUuid,
|
||||||
|
Guid characteristicUuid,
|
||||||
|
) async {
|
||||||
|
final deviceKey = _deviceKey(device);
|
||||||
|
final cached = _characteristicsByDevice[deviceKey]
|
||||||
|
?[_characteristicKey(serviceUuid, characteristicUuid)];
|
||||||
|
if (cached != null) {
|
||||||
|
return Ok(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
final discoverResult = await _discoverAndCacheServices(device);
|
||||||
|
if (discoverResult.isErr()) {
|
||||||
|
return bail(discoverResult.unwrapErr().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
final refreshed = _characteristicsByDevice[deviceKey]
|
||||||
|
?[_characteristicKey(serviceUuid, characteristicUuid)];
|
||||||
|
if (refreshed == null) {
|
||||||
|
return bail(
|
||||||
|
'Characteristic $characteristicUuid not found on service $serviceUuid for device ${device.remoteId}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(refreshed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<StreamSubscription<List<int>>>> _subscribeToCharacteristic(
|
||||||
|
BluetoothDevice device,
|
||||||
|
String serviceUuid,
|
||||||
|
String characteristicUuid, {
|
||||||
|
required bool forceIndications,
|
||||||
|
required bool useLastValueStream,
|
||||||
|
required int timeout,
|
||||||
|
void Function(List<int>)? onValue,
|
||||||
|
}) async {
|
||||||
|
final serviceGuid = Guid(serviceUuid);
|
||||||
|
final characteristicGuid = Guid(characteristicUuid);
|
||||||
|
final chrResult =
|
||||||
|
await _getCharacteristic(device, serviceGuid, characteristicGuid);
|
||||||
|
if (chrResult.isErr()) {
|
||||||
|
return bail('Failed to resolve characteristic subscription: '
|
||||||
|
'${chrResult.unwrapErr()}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final characteristic = chrResult.unwrap();
|
||||||
|
final properties = characteristic.properties;
|
||||||
|
if (forceIndications && !properties.indicate) {
|
||||||
|
return bail(
|
||||||
|
'Characteristic $characteristicUuid does not support indications');
|
||||||
|
}
|
||||||
|
if (!forceIndications && !properties.notify && !properties.indicate) {
|
||||||
|
return bail(
|
||||||
|
'Characteristic $characteristicUuid does not support notifications');
|
||||||
|
}
|
||||||
|
if (forceIndications && !kIsWeb && !Platform.isAndroid) {
|
||||||
|
return bail('Indications can only be forced on Android.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final stream = useLastValueStream
|
||||||
|
? characteristic.lastValueStream
|
||||||
|
: characteristic.onValueReceived;
|
||||||
|
final subscription = stream.listen(onValue ?? (_) {});
|
||||||
|
device.cancelWhenDisconnected(subscription);
|
||||||
|
|
||||||
|
await characteristic.setNotifyValue(
|
||||||
|
true,
|
||||||
|
timeout: timeout,
|
||||||
|
forceIndications: forceIndications,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(subscription);
|
||||||
|
} catch (e) {
|
||||||
|
return bail(
|
||||||
|
'Error subscribing to characteristic $characteristicUuid: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
lib/controller/connected_device.dart
Normal file
1
lib/controller/connected_device.dart
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
143
lib/database/database.dart
Normal file
143
lib/database/database.dart
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import 'package:anyhow/anyhow.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift_flutter/drift_flutter.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'database.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class NConnectedDevices extends _$NConnectedDevices {
|
||||||
|
@override
|
||||||
|
Future<List<ConnectedDevice>> build() async {
|
||||||
|
final db = await ref.watch(databaseProvider);
|
||||||
|
return await db.getAllConnectedDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<int>> addConnectedDevice(
|
||||||
|
ConnectedDevicesCompanion device) async {
|
||||||
|
final db = await ref.watch(databaseProvider);
|
||||||
|
final res = await db.addConnectedDevice(device);
|
||||||
|
if (res.isOk()) {
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> deleteConnectedDevice(int id) async {
|
||||||
|
final db = await ref.watch(databaseProvider);
|
||||||
|
final res = await db.deleteConnectedDevice(id);
|
||||||
|
if (res.isOk()) {
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provider for the [AppDatabase] instance
|
||||||
|
final databaseProvider = Provider<AppDatabase>((ref) {
|
||||||
|
final database = AppDatabase();
|
||||||
|
ref.onDispose(() => database.close());
|
||||||
|
return database;
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Provider for all connected devices as a stream
|
||||||
|
final connectedDevicesStreamProvider =
|
||||||
|
StreamProvider<List<ConnectedDevice>>((ref) {
|
||||||
|
final database = ref.watch(databaseProvider);
|
||||||
|
return database.getAllConnectedDevicesStream();
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Provider for all connected devices as a future
|
||||||
|
final connectedDevicesProvider = FutureProvider<List<ConnectedDevice>>((ref) {
|
||||||
|
final database = ref.watch(databaseProvider);
|
||||||
|
return database.getAllConnectedDevices();
|
||||||
|
});
|
||||||
|
|
||||||
|
class ConnectedDevices extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get deviceName => text()();
|
||||||
|
TextColumn get deviceAddress => text()();
|
||||||
|
TextColumn get deviceType => text()();
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
DateTimeColumn get lastConnectedAt => dateTime().nullable()();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DriftDatabase(tables: [ConnectedDevices])
|
||||||
|
class AppDatabase extends _$AppDatabase {
|
||||||
|
// After generating code, this class needs to define a `schemaVersion` getter
|
||||||
|
// and a constructor telling drift where the database should be stored.
|
||||||
|
// These are described in the getting started guide: https://drift.simonbinder.eu/setup/
|
||||||
|
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 1;
|
||||||
|
|
||||||
|
static QueryExecutor _openConnection() {
|
||||||
|
return driftDatabase(
|
||||||
|
name: 'my_database',
|
||||||
|
native: const DriftNativeOptions(
|
||||||
|
// By default, `driftDatabase` from `package:drift_flutter` stores the
|
||||||
|
// database files in `getApplicationDocumentsDirectory()`.
|
||||||
|
databaseDirectory: getApplicationSupportDirectory,
|
||||||
|
),
|
||||||
|
// If you need web support, see https://drift.simonbinder.eu/platforms/web/
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ConnectedDevice>> getAllConnectedDevices() {
|
||||||
|
return select(connectedDevices).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new connected device to the database.
|
||||||
|
///
|
||||||
|
/// [device] is a [ConnectedDevicesCompanion] representing the device to be inserted.
|
||||||
|
///
|
||||||
|
/// Returns a [Result] indicating success or failure of the device insertion.
|
||||||
|
/// On successful insertion, returns [Ok]\(rowid\). On failure, returns an error with a descriptive message.
|
||||||
|
Future<Result<int>> addConnectedDevice(
|
||||||
|
ConnectedDevicesCompanion device) async {
|
||||||
|
try {
|
||||||
|
if (!device.deviceAddress.present) {
|
||||||
|
return bail('Device address is required to save a connected device.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final exists = await (select(connectedDevices)
|
||||||
|
..where(
|
||||||
|
(tbl) => tbl.deviceAddress.equals(device.deviceAddress.value)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (exists != null) {
|
||||||
|
return bail('Device ${device.deviceAddress.value} is already added.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final rowid = await into(connectedDevices).insert(device);
|
||||||
|
return Ok(rowid);
|
||||||
|
} catch (e, st) {
|
||||||
|
return bail('Failed to add device: $e', st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a connected device from the database by its ID.
|
||||||
|
///
|
||||||
|
/// [id] is the ID of the device to be deleted.
|
||||||
|
///
|
||||||
|
/// Returns a [Result] indicating success or failure of the deletion.
|
||||||
|
/// On successful deletion, returns [Ok](number of deleted rows). On failure, returns an error with a descriptive message.
|
||||||
|
Future<Result<void>> deleteConnectedDevice(int id) async {
|
||||||
|
try {
|
||||||
|
final count = await (delete(connectedDevices)
|
||||||
|
..where((tbl) => tbl.id.equals(id)))
|
||||||
|
.go();
|
||||||
|
if (count == 0) {
|
||||||
|
return bail('Device with id $id not found.');
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
} catch (e, st) {
|
||||||
|
return bail('Failed to delete device with id $id: $e', st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<ConnectedDevice>> getAllConnectedDevicesStream() {
|
||||||
|
return select(connectedDevices).watch();
|
||||||
|
}
|
||||||
|
}
|
||||||
586
lib/database/database.g.dart
Normal file
586
lib/database/database.g.dart
Normal file
@ -0,0 +1,586 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'database.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
class $ConnectedDevicesTable extends ConnectedDevices
|
||||||
|
with TableInfo<$ConnectedDevicesTable, ConnectedDevice> {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$ConnectedDevicesTable(this.attachedDatabase, [this._alias]);
|
||||||
|
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||||
|
'id', aliasedName, false,
|
||||||
|
hasAutoIncrement: true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints:
|
||||||
|
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||||
|
static const VerificationMeta _deviceNameMeta =
|
||||||
|
const VerificationMeta('deviceName');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> deviceName = GeneratedColumn<String>(
|
||||||
|
'device_name', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _deviceAddressMeta =
|
||||||
|
const VerificationMeta('deviceAddress');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> deviceAddress = GeneratedColumn<String>(
|
||||||
|
'device_address', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _deviceTypeMeta =
|
||||||
|
const VerificationMeta('deviceType');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> deviceType = GeneratedColumn<String>(
|
||||||
|
'device_type', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _createdAtMeta =
|
||||||
|
const VerificationMeta('createdAt');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||||
|
'created_at', aliasedName, false,
|
||||||
|
type: DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: currentDateAndTime);
|
||||||
|
static const VerificationMeta _lastConnectedAtMeta =
|
||||||
|
const VerificationMeta('lastConnectedAt');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<DateTime> lastConnectedAt =
|
||||||
|
GeneratedColumn<DateTime>('last_connected_at', aliasedName, true,
|
||||||
|
type: DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns =>
|
||||||
|
[id, deviceName, deviceAddress, deviceType, createdAt, lastConnectedAt];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'connected_devices';
|
||||||
|
@override
|
||||||
|
VerificationContext validateIntegrity(Insertable<ConnectedDevice> instance,
|
||||||
|
{bool isInserting = false}) {
|
||||||
|
final context = VerificationContext();
|
||||||
|
final data = instance.toColumns(true);
|
||||||
|
if (data.containsKey('id')) {
|
||||||
|
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('device_name')) {
|
||||||
|
context.handle(
|
||||||
|
_deviceNameMeta,
|
||||||
|
deviceName.isAcceptableOrUnknown(
|
||||||
|
data['device_name']!, _deviceNameMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_deviceNameMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('device_address')) {
|
||||||
|
context.handle(
|
||||||
|
_deviceAddressMeta,
|
||||||
|
deviceAddress.isAcceptableOrUnknown(
|
||||||
|
data['device_address']!, _deviceAddressMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_deviceAddressMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('device_type')) {
|
||||||
|
context.handle(
|
||||||
|
_deviceTypeMeta,
|
||||||
|
deviceType.isAcceptableOrUnknown(
|
||||||
|
data['device_type']!, _deviceTypeMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_deviceTypeMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('created_at')) {
|
||||||
|
context.handle(_createdAtMeta,
|
||||||
|
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('last_connected_at')) {
|
||||||
|
context.handle(
|
||||||
|
_lastConnectedAtMeta,
|
||||||
|
lastConnectedAt.isAcceptableOrUnknown(
|
||||||
|
data['last_connected_at']!, _lastConnectedAtMeta));
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
ConnectedDevice map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return ConnectedDevice(
|
||||||
|
id: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||||
|
deviceName: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}device_name'])!,
|
||||||
|
deviceAddress: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}device_address'])!,
|
||||||
|
deviceType: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}device_type'])!,
|
||||||
|
createdAt: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||||
|
lastConnectedAt: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.dateTime, data['${effectivePrefix}last_connected_at']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$ConnectedDevicesTable createAlias(String alias) {
|
||||||
|
return $ConnectedDevicesTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectedDevice extends DataClass implements Insertable<ConnectedDevice> {
|
||||||
|
final int id;
|
||||||
|
final String deviceName;
|
||||||
|
final String deviceAddress;
|
||||||
|
final String deviceType;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime? lastConnectedAt;
|
||||||
|
const ConnectedDevice(
|
||||||
|
{required this.id,
|
||||||
|
required this.deviceName,
|
||||||
|
required this.deviceAddress,
|
||||||
|
required this.deviceType,
|
||||||
|
required this.createdAt,
|
||||||
|
this.lastConnectedAt});
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
map['id'] = Variable<int>(id);
|
||||||
|
map['device_name'] = Variable<String>(deviceName);
|
||||||
|
map['device_address'] = Variable<String>(deviceAddress);
|
||||||
|
map['device_type'] = Variable<String>(deviceType);
|
||||||
|
map['created_at'] = Variable<DateTime>(createdAt);
|
||||||
|
if (!nullToAbsent || lastConnectedAt != null) {
|
||||||
|
map['last_connected_at'] = Variable<DateTime>(lastConnectedAt);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectedDevicesCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return ConnectedDevicesCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
deviceName: Value(deviceName),
|
||||||
|
deviceAddress: Value(deviceAddress),
|
||||||
|
deviceType: Value(deviceType),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
lastConnectedAt: lastConnectedAt == null && nullToAbsent
|
||||||
|
? const Value.absent()
|
||||||
|
: Value(lastConnectedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ConnectedDevice.fromJson(Map<String, dynamic> json,
|
||||||
|
{ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return ConnectedDevice(
|
||||||
|
id: serializer.fromJson<int>(json['id']),
|
||||||
|
deviceName: serializer.fromJson<String>(json['deviceName']),
|
||||||
|
deviceAddress: serializer.fromJson<String>(json['deviceAddress']),
|
||||||
|
deviceType: serializer.fromJson<String>(json['deviceType']),
|
||||||
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
|
lastConnectedAt: serializer.fromJson<DateTime?>(json['lastConnectedAt']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': serializer.toJson<int>(id),
|
||||||
|
'deviceName': serializer.toJson<String>(deviceName),
|
||||||
|
'deviceAddress': serializer.toJson<String>(deviceAddress),
|
||||||
|
'deviceType': serializer.toJson<String>(deviceType),
|
||||||
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
|
'lastConnectedAt': serializer.toJson<DateTime?>(lastConnectedAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectedDevice copyWith(
|
||||||
|
{int? id,
|
||||||
|
String? deviceName,
|
||||||
|
String? deviceAddress,
|
||||||
|
String? deviceType,
|
||||||
|
DateTime? createdAt,
|
||||||
|
Value<DateTime?> lastConnectedAt = const Value.absent()}) =>
|
||||||
|
ConnectedDevice(
|
||||||
|
id: id ?? this.id,
|
||||||
|
deviceName: deviceName ?? this.deviceName,
|
||||||
|
deviceAddress: deviceAddress ?? this.deviceAddress,
|
||||||
|
deviceType: deviceType ?? this.deviceType,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
lastConnectedAt: lastConnectedAt.present
|
||||||
|
? lastConnectedAt.value
|
||||||
|
: this.lastConnectedAt,
|
||||||
|
);
|
||||||
|
ConnectedDevice copyWithCompanion(ConnectedDevicesCompanion data) {
|
||||||
|
return ConnectedDevice(
|
||||||
|
id: data.id.present ? data.id.value : this.id,
|
||||||
|
deviceName:
|
||||||
|
data.deviceName.present ? data.deviceName.value : this.deviceName,
|
||||||
|
deviceAddress: data.deviceAddress.present
|
||||||
|
? data.deviceAddress.value
|
||||||
|
: this.deviceAddress,
|
||||||
|
deviceType:
|
||||||
|
data.deviceType.present ? data.deviceType.value : this.deviceType,
|
||||||
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
|
lastConnectedAt: data.lastConnectedAt.present
|
||||||
|
? data.lastConnectedAt.value
|
||||||
|
: this.lastConnectedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ConnectedDevice(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('deviceName: $deviceName, ')
|
||||||
|
..write('deviceAddress: $deviceAddress, ')
|
||||||
|
..write('deviceType: $deviceType, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('lastConnectedAt: $lastConnectedAt')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
id, deviceName, deviceAddress, deviceType, createdAt, lastConnectedAt);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is ConnectedDevice &&
|
||||||
|
other.id == this.id &&
|
||||||
|
other.deviceName == this.deviceName &&
|
||||||
|
other.deviceAddress == this.deviceAddress &&
|
||||||
|
other.deviceType == this.deviceType &&
|
||||||
|
other.createdAt == this.createdAt &&
|
||||||
|
other.lastConnectedAt == this.lastConnectedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectedDevicesCompanion extends UpdateCompanion<ConnectedDevice> {
|
||||||
|
final Value<int> id;
|
||||||
|
final Value<String> deviceName;
|
||||||
|
final Value<String> deviceAddress;
|
||||||
|
final Value<String> deviceType;
|
||||||
|
final Value<DateTime> createdAt;
|
||||||
|
final Value<DateTime?> lastConnectedAt;
|
||||||
|
const ConnectedDevicesCompanion({
|
||||||
|
this.id = const Value.absent(),
|
||||||
|
this.deviceName = const Value.absent(),
|
||||||
|
this.deviceAddress = const Value.absent(),
|
||||||
|
this.deviceType = const Value.absent(),
|
||||||
|
this.createdAt = const Value.absent(),
|
||||||
|
this.lastConnectedAt = const Value.absent(),
|
||||||
|
});
|
||||||
|
ConnectedDevicesCompanion.insert({
|
||||||
|
this.id = const Value.absent(),
|
||||||
|
required String deviceName,
|
||||||
|
required String deviceAddress,
|
||||||
|
required String deviceType,
|
||||||
|
this.createdAt = const Value.absent(),
|
||||||
|
this.lastConnectedAt = const Value.absent(),
|
||||||
|
}) : deviceName = Value(deviceName),
|
||||||
|
deviceAddress = Value(deviceAddress),
|
||||||
|
deviceType = Value(deviceType);
|
||||||
|
static Insertable<ConnectedDevice> custom({
|
||||||
|
Expression<int>? id,
|
||||||
|
Expression<String>? deviceName,
|
||||||
|
Expression<String>? deviceAddress,
|
||||||
|
Expression<String>? deviceType,
|
||||||
|
Expression<DateTime>? createdAt,
|
||||||
|
Expression<DateTime>? lastConnectedAt,
|
||||||
|
}) {
|
||||||
|
return RawValuesInsertable({
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
if (deviceName != null) 'device_name': deviceName,
|
||||||
|
if (deviceAddress != null) 'device_address': deviceAddress,
|
||||||
|
if (deviceType != null) 'device_type': deviceType,
|
||||||
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
|
if (lastConnectedAt != null) 'last_connected_at': lastConnectedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectedDevicesCompanion copyWith(
|
||||||
|
{Value<int>? id,
|
||||||
|
Value<String>? deviceName,
|
||||||
|
Value<String>? deviceAddress,
|
||||||
|
Value<String>? deviceType,
|
||||||
|
Value<DateTime>? createdAt,
|
||||||
|
Value<DateTime?>? lastConnectedAt}) {
|
||||||
|
return ConnectedDevicesCompanion(
|
||||||
|
id: id ?? this.id,
|
||||||
|
deviceName: deviceName ?? this.deviceName,
|
||||||
|
deviceAddress: deviceAddress ?? this.deviceAddress,
|
||||||
|
deviceType: deviceType ?? this.deviceType,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
lastConnectedAt: lastConnectedAt ?? this.lastConnectedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
if (id.present) {
|
||||||
|
map['id'] = Variable<int>(id.value);
|
||||||
|
}
|
||||||
|
if (deviceName.present) {
|
||||||
|
map['device_name'] = Variable<String>(deviceName.value);
|
||||||
|
}
|
||||||
|
if (deviceAddress.present) {
|
||||||
|
map['device_address'] = Variable<String>(deviceAddress.value);
|
||||||
|
}
|
||||||
|
if (deviceType.present) {
|
||||||
|
map['device_type'] = Variable<String>(deviceType.value);
|
||||||
|
}
|
||||||
|
if (createdAt.present) {
|
||||||
|
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||||
|
}
|
||||||
|
if (lastConnectedAt.present) {
|
||||||
|
map['last_connected_at'] = Variable<DateTime>(lastConnectedAt.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('ConnectedDevicesCompanion(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('deviceName: $deviceName, ')
|
||||||
|
..write('deviceAddress: $deviceAddress, ')
|
||||||
|
..write('deviceType: $deviceType, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('lastConnectedAt: $lastConnectedAt')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||||
|
_$AppDatabase(QueryExecutor e) : super(e);
|
||||||
|
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
||||||
|
late final $ConnectedDevicesTable connectedDevices =
|
||||||
|
$ConnectedDevicesTable(this);
|
||||||
|
@override
|
||||||
|
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||||
|
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||||
|
@override
|
||||||
|
List<DatabaseSchemaEntity> get allSchemaEntities => [connectedDevices];
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$ConnectedDevicesTableCreateCompanionBuilder
|
||||||
|
= ConnectedDevicesCompanion Function({
|
||||||
|
Value<int> id,
|
||||||
|
required String deviceName,
|
||||||
|
required String deviceAddress,
|
||||||
|
required String deviceType,
|
||||||
|
Value<DateTime> createdAt,
|
||||||
|
Value<DateTime?> lastConnectedAt,
|
||||||
|
});
|
||||||
|
typedef $$ConnectedDevicesTableUpdateCompanionBuilder
|
||||||
|
= ConnectedDevicesCompanion Function({
|
||||||
|
Value<int> id,
|
||||||
|
Value<String> deviceName,
|
||||||
|
Value<String> deviceAddress,
|
||||||
|
Value<String> deviceType,
|
||||||
|
Value<DateTime> createdAt,
|
||||||
|
Value<DateTime?> lastConnectedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
class $$ConnectedDevicesTableFilterComposer
|
||||||
|
extends Composer<_$AppDatabase, $ConnectedDevicesTable> {
|
||||||
|
$$ConnectedDevicesTableFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnFilters<int> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get deviceName => $composableBuilder(
|
||||||
|
column: $table.deviceName, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get deviceAddress => $composableBuilder(
|
||||||
|
column: $table.deviceAddress, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get deviceType => $composableBuilder(
|
||||||
|
column: $table.deviceType, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<DateTime> get lastConnectedAt => $composableBuilder(
|
||||||
|
column: $table.lastConnectedAt,
|
||||||
|
builder: (column) => ColumnFilters(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$ConnectedDevicesTableOrderingComposer
|
||||||
|
extends Composer<_$AppDatabase, $ConnectedDevicesTable> {
|
||||||
|
$$ConnectedDevicesTableOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnOrderings<int> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get deviceName => $composableBuilder(
|
||||||
|
column: $table.deviceName, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get deviceAddress => $composableBuilder(
|
||||||
|
column: $table.deviceAddress,
|
||||||
|
builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get deviceType => $composableBuilder(
|
||||||
|
column: $table.deviceType, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<DateTime> get lastConnectedAt => $composableBuilder(
|
||||||
|
column: $table.lastConnectedAt,
|
||||||
|
builder: (column) => ColumnOrderings(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$ConnectedDevicesTableAnnotationComposer
|
||||||
|
extends Composer<_$AppDatabase, $ConnectedDevicesTable> {
|
||||||
|
$$ConnectedDevicesTableAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
GeneratedColumn<int> get id =>
|
||||||
|
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get deviceName => $composableBuilder(
|
||||||
|
column: $table.deviceName, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get deviceAddress => $composableBuilder(
|
||||||
|
column: $table.deviceAddress, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get deviceType => $composableBuilder(
|
||||||
|
column: $table.deviceType, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<DateTime> get lastConnectedAt => $composableBuilder(
|
||||||
|
column: $table.lastConnectedAt, builder: (column) => column);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$ConnectedDevicesTableTableManager extends RootTableManager<
|
||||||
|
_$AppDatabase,
|
||||||
|
$ConnectedDevicesTable,
|
||||||
|
ConnectedDevice,
|
||||||
|
$$ConnectedDevicesTableFilterComposer,
|
||||||
|
$$ConnectedDevicesTableOrderingComposer,
|
||||||
|
$$ConnectedDevicesTableAnnotationComposer,
|
||||||
|
$$ConnectedDevicesTableCreateCompanionBuilder,
|
||||||
|
$$ConnectedDevicesTableUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
ConnectedDevice,
|
||||||
|
BaseReferences<_$AppDatabase, $ConnectedDevicesTable, ConnectedDevice>
|
||||||
|
),
|
||||||
|
ConnectedDevice,
|
||||||
|
PrefetchHooks Function()> {
|
||||||
|
$$ConnectedDevicesTableTableManager(
|
||||||
|
_$AppDatabase db, $ConnectedDevicesTable table)
|
||||||
|
: super(TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
$$ConnectedDevicesTableFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
$$ConnectedDevicesTableOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
$$ConnectedDevicesTableAnnotationComposer($db: db, $table: table),
|
||||||
|
updateCompanionCallback: ({
|
||||||
|
Value<int> id = const Value.absent(),
|
||||||
|
Value<String> deviceName = const Value.absent(),
|
||||||
|
Value<String> deviceAddress = const Value.absent(),
|
||||||
|
Value<String> deviceType = const Value.absent(),
|
||||||
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
Value<DateTime?> lastConnectedAt = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
ConnectedDevicesCompanion(
|
||||||
|
id: id,
|
||||||
|
deviceName: deviceName,
|
||||||
|
deviceAddress: deviceAddress,
|
||||||
|
deviceType: deviceType,
|
||||||
|
createdAt: createdAt,
|
||||||
|
lastConnectedAt: lastConnectedAt,
|
||||||
|
),
|
||||||
|
createCompanionCallback: ({
|
||||||
|
Value<int> id = const Value.absent(),
|
||||||
|
required String deviceName,
|
||||||
|
required String deviceAddress,
|
||||||
|
required String deviceType,
|
||||||
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
Value<DateTime?> lastConnectedAt = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
ConnectedDevicesCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
deviceName: deviceName,
|
||||||
|
deviceAddress: deviceAddress,
|
||||||
|
deviceType: deviceType,
|
||||||
|
createdAt: createdAt,
|
||||||
|
lastConnectedAt: lastConnectedAt,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$ConnectedDevicesTableProcessedTableManager = ProcessedTableManager<
|
||||||
|
_$AppDatabase,
|
||||||
|
$ConnectedDevicesTable,
|
||||||
|
ConnectedDevice,
|
||||||
|
$$ConnectedDevicesTableFilterComposer,
|
||||||
|
$$ConnectedDevicesTableOrderingComposer,
|
||||||
|
$$ConnectedDevicesTableAnnotationComposer,
|
||||||
|
$$ConnectedDevicesTableCreateCompanionBuilder,
|
||||||
|
$$ConnectedDevicesTableUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
ConnectedDevice,
|
||||||
|
BaseReferences<_$AppDatabase, $ConnectedDevicesTable, ConnectedDevice>
|
||||||
|
),
|
||||||
|
ConnectedDevice,
|
||||||
|
PrefetchHooks Function()>;
|
||||||
|
|
||||||
|
class $AppDatabaseManager {
|
||||||
|
final _$AppDatabase _db;
|
||||||
|
$AppDatabaseManager(this._db);
|
||||||
|
$$ConnectedDevicesTableTableManager get connectedDevices =>
|
||||||
|
$$ConnectedDevicesTableTableManager(_db, _db.connectedDevices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$nConnectedDevicesHash() => r'022e744d950bb37c1016266064639e51ce6031b5';
|
||||||
|
|
||||||
|
/// See also [NConnectedDevices].
|
||||||
|
@ProviderFor(NConnectedDevices)
|
||||||
|
final nConnectedDevicesProvider = AutoDisposeAsyncNotifierProvider<
|
||||||
|
NConnectedDevices, List<ConnectedDevice>>.internal(
|
||||||
|
NConnectedDevices.new,
|
||||||
|
name: r'nConnectedDevicesProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$nConnectedDevicesHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$NConnectedDevices = AutoDisposeAsyncNotifier<List<ConnectedDevice>>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
@ -1,19 +1,23 @@
|
|||||||
import 'package:abawo_bt_app/pages/devices_page.dart';
|
import 'package:abawo_bt_app/pages/devices_page.dart';
|
||||||
|
import 'package:abawo_bt_app/src/rust/frb_generated.dart';
|
||||||
import 'package:abawo_bt_app/util/sharedPrefs.dart';
|
import 'package:abawo_bt_app/util/sharedPrefs.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:nb_utils/nb_utils.dart';
|
||||||
import 'pages/home_page.dart';
|
import 'pages/home_page.dart';
|
||||||
import 'pages/settings_page.dart';
|
import 'pages/settings_page.dart';
|
||||||
|
import 'package:abawo_bt_app/pages/device_details_page.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
Logger.root.level = Level.ALL; // defaults to Level.INFO
|
Logger.root.level = Level.ALL; // defaults to Level.INFO
|
||||||
Logger.root.onRecord.listen((record) {
|
Logger.root.onRecord.listen((record) {
|
||||||
print('${record.level.name}: ${record.time}: ${record.message}');
|
print('${record.level.name}: ${record.time}: ${record.message}');
|
||||||
});
|
});
|
||||||
|
await RustLib.init();
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await initialize();
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
@ -49,6 +53,7 @@ class AbawoBtApp extends StatelessWidget {
|
|||||||
|
|
||||||
// Configure GoRouter
|
// Configure GoRouter
|
||||||
final _router = GoRouter(
|
final _router = GoRouter(
|
||||||
|
navigatorKey: navigatorKey,
|
||||||
initialLocation: '/',
|
initialLocation: '/',
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
@ -65,5 +70,42 @@ final _router = GoRouter(
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/device/:deviceAddress',
|
||||||
|
builder: (context, state) {
|
||||||
|
final deviceAddress = state.pathParameters['deviceAddress']!;
|
||||||
|
return DeviceDetailsPage(deviceAddress: deviceAddress);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:abawo_bt_app/src/rust/api/simple.dart';
|
||||||
|
import 'package:abawo_bt_app/src/rust/frb_generated.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
await RustLib.init();
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('flutter_rust_bridge quickstart')),
|
||||||
|
body: Center(
|
||||||
|
child: Text(
|
||||||
|
'Action: Call Rust `greet("Tom")`\nResult: `${greet(name: "Tom")}`'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
import 'package:abawo_bt_app/util/constants.dart';
|
||||||
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart' show DeviceIdentifier;
|
||||||
|
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'bluetooth_device_model.freezed.dart';
|
part 'bluetooth_device_model.freezed.dart';
|
||||||
@ -12,6 +15,24 @@ enum DeviceType {
|
|||||||
other,
|
other,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DeviceType deviceTypeFromUuids(List<Uuid> uuids) {
|
||||||
|
if (uuids.any((uuid) => isAbawoUniversalShiftersDeviceGuid(uuid))) {
|
||||||
|
return DeviceType.universalShifters;
|
||||||
|
}
|
||||||
|
return DeviceType.other;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceType deviceTypeFromString(String type) {
|
||||||
|
return DeviceType.values.firstWhere(
|
||||||
|
(e) => e.toString().split('.').last == type,
|
||||||
|
orElse: () => DeviceType.other,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String deviceTypeToString(DeviceType type) {
|
||||||
|
return type.toString().split('.').last;
|
||||||
|
}
|
||||||
|
|
||||||
/// Model representing a Bluetooth device
|
/// Model representing a Bluetooth device
|
||||||
@freezed
|
@freezed
|
||||||
abstract class BluetoothDeviceModel with _$BluetoothDeviceModel {
|
abstract class BluetoothDeviceModel with _$BluetoothDeviceModel {
|
||||||
@ -25,23 +46,28 @@ abstract class BluetoothDeviceModel with _$BluetoothDeviceModel {
|
|||||||
/// MAC address of the device
|
/// MAC address of the device
|
||||||
required String address,
|
required String address,
|
||||||
|
|
||||||
/// Signal strength indicator (RSSI)
|
|
||||||
int? rssi,
|
|
||||||
|
|
||||||
/// Type of the device
|
/// Type of the device
|
||||||
@Default(DeviceType.other) DeviceType type,
|
@Default(DeviceType.other) DeviceType type,
|
||||||
|
|
||||||
/// Whether the device is currently connected
|
|
||||||
@Default(false) bool isConnected,
|
|
||||||
|
|
||||||
/// Additional device information
|
/// Additional device information
|
||||||
Map<String, dynamic>? manufacturerData,
|
Map<String, dynamic>? manufacturerData,
|
||||||
|
|
||||||
/// Service UUIDs advertised by the device
|
/// Identifier of the device
|
||||||
List<String>? serviceUuids,
|
@DeviceIdentJsonConverter() required DeviceIdentifier deviceIdent,
|
||||||
}) = _BluetoothDeviceModel;
|
}) = _BluetoothDeviceModel;
|
||||||
|
|
||||||
/// Create a BluetoothDeviceModel from JSON
|
/// Create a BluetoothDeviceModel from JSON
|
||||||
factory BluetoothDeviceModel.fromJson(Map<String, dynamic> json) =>
|
factory BluetoothDeviceModel.fromJson(Map<String, dynamic> json) =>
|
||||||
_$BluetoothDeviceModelFromJson(json);
|
_$BluetoothDeviceModelFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DeviceIdentJsonConverter
|
||||||
|
implements JsonConverter<DeviceIdentifier, String> {
|
||||||
|
const DeviceIdentJsonConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
DeviceIdentifier fromJson(String json) => DeviceIdentifier(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toJson(DeviceIdentifier object) => object.str;
|
||||||
|
}
|
||||||
|
|||||||
@ -24,20 +24,15 @@ mixin _$BluetoothDeviceModel {
|
|||||||
/// MAC address of the device
|
/// MAC address of the device
|
||||||
String get address;
|
String get address;
|
||||||
|
|
||||||
/// Signal strength indicator (RSSI)
|
|
||||||
int? get rssi;
|
|
||||||
|
|
||||||
/// Type of the device
|
/// Type of the device
|
||||||
DeviceType get type;
|
DeviceType get type;
|
||||||
|
|
||||||
/// Whether the device is currently connected
|
|
||||||
bool get isConnected;
|
|
||||||
|
|
||||||
/// Additional device information
|
/// Additional device information
|
||||||
Map<String, dynamic>? get manufacturerData;
|
Map<String, dynamic>? get manufacturerData;
|
||||||
|
|
||||||
/// Service UUIDs advertised by the device
|
/// Identifier of the device
|
||||||
List<String>? get serviceUuids;
|
@DeviceIdentJsonConverter()
|
||||||
|
DeviceIdentifier get deviceIdent;
|
||||||
|
|
||||||
/// Create a copy of BluetoothDeviceModel
|
/// Create a copy of BluetoothDeviceModel
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@ -58,32 +53,21 @@ mixin _$BluetoothDeviceModel {
|
|||||||
(identical(other.id, id) || other.id == id) &&
|
(identical(other.id, id) || other.id == id) &&
|
||||||
(identical(other.name, name) || other.name == name) &&
|
(identical(other.name, name) || other.name == name) &&
|
||||||
(identical(other.address, address) || other.address == address) &&
|
(identical(other.address, address) || other.address == address) &&
|
||||||
(identical(other.rssi, rssi) || other.rssi == rssi) &&
|
|
||||||
(identical(other.type, type) || other.type == type) &&
|
(identical(other.type, type) || other.type == type) &&
|
||||||
(identical(other.isConnected, isConnected) ||
|
|
||||||
other.isConnected == isConnected) &&
|
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.manufacturerData, manufacturerData) &&
|
.equals(other.manufacturerData, manufacturerData) &&
|
||||||
const DeepCollectionEquality()
|
(identical(other.deviceIdent, deviceIdent) ||
|
||||||
.equals(other.serviceUuids, serviceUuids));
|
other.deviceIdent == deviceIdent));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(runtimeType, id, name, address, type,
|
||||||
runtimeType,
|
const DeepCollectionEquality().hash(manufacturerData), deviceIdent);
|
||||||
id,
|
|
||||||
name,
|
|
||||||
address,
|
|
||||||
rssi,
|
|
||||||
type,
|
|
||||||
isConnected,
|
|
||||||
const DeepCollectionEquality().hash(manufacturerData),
|
|
||||||
const DeepCollectionEquality().hash(serviceUuids));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'BluetoothDeviceModel(id: $id, name: $name, address: $address, rssi: $rssi, type: $type, isConnected: $isConnected, manufacturerData: $manufacturerData, serviceUuids: $serviceUuids)';
|
return 'BluetoothDeviceModel(id: $id, name: $name, address: $address, type: $type, manufacturerData: $manufacturerData, deviceIdent: $deviceIdent)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,11 +81,9 @@ abstract mixin class $BluetoothDeviceModelCopyWith<$Res> {
|
|||||||
{String id,
|
{String id,
|
||||||
String? name,
|
String? name,
|
||||||
String address,
|
String address,
|
||||||
int? rssi,
|
|
||||||
DeviceType type,
|
DeviceType type,
|
||||||
bool isConnected,
|
|
||||||
Map<String, dynamic>? manufacturerData,
|
Map<String, dynamic>? manufacturerData,
|
||||||
List<String>? serviceUuids});
|
@DeviceIdentJsonConverter() DeviceIdentifier deviceIdent});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -120,11 +102,9 @@ class _$BluetoothDeviceModelCopyWithImpl<$Res>
|
|||||||
Object? id = null,
|
Object? id = null,
|
||||||
Object? name = freezed,
|
Object? name = freezed,
|
||||||
Object? address = null,
|
Object? address = null,
|
||||||
Object? rssi = freezed,
|
|
||||||
Object? type = null,
|
Object? type = null,
|
||||||
Object? isConnected = null,
|
|
||||||
Object? manufacturerData = freezed,
|
Object? manufacturerData = freezed,
|
||||||
Object? serviceUuids = freezed,
|
Object? deviceIdent = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
id: null == id
|
id: null == id
|
||||||
@ -139,26 +119,18 @@ class _$BluetoothDeviceModelCopyWithImpl<$Res>
|
|||||||
? _self.address
|
? _self.address
|
||||||
: address // ignore: cast_nullable_to_non_nullable
|
: address // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
rssi: freezed == rssi
|
|
||||||
? _self.rssi
|
|
||||||
: rssi // ignore: cast_nullable_to_non_nullable
|
|
||||||
as int?,
|
|
||||||
type: null == type
|
type: null == type
|
||||||
? _self.type
|
? _self.type
|
||||||
: type // ignore: cast_nullable_to_non_nullable
|
: type // ignore: cast_nullable_to_non_nullable
|
||||||
as DeviceType,
|
as DeviceType,
|
||||||
isConnected: null == isConnected
|
|
||||||
? _self.isConnected
|
|
||||||
: isConnected // ignore: cast_nullable_to_non_nullable
|
|
||||||
as bool,
|
|
||||||
manufacturerData: freezed == manufacturerData
|
manufacturerData: freezed == manufacturerData
|
||||||
? _self.manufacturerData
|
? _self.manufacturerData
|
||||||
: manufacturerData // ignore: cast_nullable_to_non_nullable
|
: manufacturerData // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>?,
|
as Map<String, dynamic>?,
|
||||||
serviceUuids: freezed == serviceUuids
|
deviceIdent: null == deviceIdent
|
||||||
? _self.serviceUuids
|
? _self.deviceIdent
|
||||||
: serviceUuids // ignore: cast_nullable_to_non_nullable
|
: deviceIdent // ignore: cast_nullable_to_non_nullable
|
||||||
as List<String>?,
|
as DeviceIdentifier,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,13 +142,10 @@ class _BluetoothDeviceModel implements BluetoothDeviceModel {
|
|||||||
{required this.id,
|
{required this.id,
|
||||||
this.name,
|
this.name,
|
||||||
required this.address,
|
required this.address,
|
||||||
this.rssi,
|
|
||||||
this.type = DeviceType.other,
|
this.type = DeviceType.other,
|
||||||
this.isConnected = false,
|
|
||||||
final Map<String, dynamic>? manufacturerData,
|
final Map<String, dynamic>? manufacturerData,
|
||||||
final List<String>? serviceUuids})
|
@DeviceIdentJsonConverter() required this.deviceIdent})
|
||||||
: _manufacturerData = manufacturerData,
|
: _manufacturerData = manufacturerData;
|
||||||
_serviceUuids = serviceUuids;
|
|
||||||
factory _BluetoothDeviceModel.fromJson(Map<String, dynamic> json) =>
|
factory _BluetoothDeviceModel.fromJson(Map<String, dynamic> json) =>
|
||||||
_$BluetoothDeviceModelFromJson(json);
|
_$BluetoothDeviceModelFromJson(json);
|
||||||
|
|
||||||
@ -192,20 +161,11 @@ class _BluetoothDeviceModel implements BluetoothDeviceModel {
|
|||||||
@override
|
@override
|
||||||
final String address;
|
final String address;
|
||||||
|
|
||||||
/// Signal strength indicator (RSSI)
|
|
||||||
@override
|
|
||||||
final int? rssi;
|
|
||||||
|
|
||||||
/// Type of the device
|
/// Type of the device
|
||||||
@override
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final DeviceType type;
|
final DeviceType type;
|
||||||
|
|
||||||
/// Whether the device is currently connected
|
|
||||||
@override
|
|
||||||
@JsonKey()
|
|
||||||
final bool isConnected;
|
|
||||||
|
|
||||||
/// Additional device information
|
/// Additional device information
|
||||||
final Map<String, dynamic>? _manufacturerData;
|
final Map<String, dynamic>? _manufacturerData;
|
||||||
|
|
||||||
@ -219,18 +179,10 @@ class _BluetoothDeviceModel implements BluetoothDeviceModel {
|
|||||||
return EqualUnmodifiableMapView(value);
|
return EqualUnmodifiableMapView(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Service UUIDs advertised by the device
|
/// Identifier of the device
|
||||||
final List<String>? _serviceUuids;
|
|
||||||
|
|
||||||
/// Service UUIDs advertised by the device
|
|
||||||
@override
|
@override
|
||||||
List<String>? get serviceUuids {
|
@DeviceIdentJsonConverter()
|
||||||
final value = _serviceUuids;
|
final DeviceIdentifier deviceIdent;
|
||||||
if (value == null) return null;
|
|
||||||
if (_serviceUuids is EqualUnmodifiableListView) return _serviceUuids;
|
|
||||||
// ignore: implicit_dynamic_type
|
|
||||||
return EqualUnmodifiableListView(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a copy of BluetoothDeviceModel
|
/// Create a copy of BluetoothDeviceModel
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@ -256,32 +208,21 @@ class _BluetoothDeviceModel implements BluetoothDeviceModel {
|
|||||||
(identical(other.id, id) || other.id == id) &&
|
(identical(other.id, id) || other.id == id) &&
|
||||||
(identical(other.name, name) || other.name == name) &&
|
(identical(other.name, name) || other.name == name) &&
|
||||||
(identical(other.address, address) || other.address == address) &&
|
(identical(other.address, address) || other.address == address) &&
|
||||||
(identical(other.rssi, rssi) || other.rssi == rssi) &&
|
|
||||||
(identical(other.type, type) || other.type == type) &&
|
(identical(other.type, type) || other.type == type) &&
|
||||||
(identical(other.isConnected, isConnected) ||
|
|
||||||
other.isConnected == isConnected) &&
|
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other._manufacturerData, _manufacturerData) &&
|
.equals(other._manufacturerData, _manufacturerData) &&
|
||||||
const DeepCollectionEquality()
|
(identical(other.deviceIdent, deviceIdent) ||
|
||||||
.equals(other._serviceUuids, _serviceUuids));
|
other.deviceIdent == deviceIdent));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(runtimeType, id, name, address, type,
|
||||||
runtimeType,
|
const DeepCollectionEquality().hash(_manufacturerData), deviceIdent);
|
||||||
id,
|
|
||||||
name,
|
|
||||||
address,
|
|
||||||
rssi,
|
|
||||||
type,
|
|
||||||
isConnected,
|
|
||||||
const DeepCollectionEquality().hash(_manufacturerData),
|
|
||||||
const DeepCollectionEquality().hash(_serviceUuids));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'BluetoothDeviceModel(id: $id, name: $name, address: $address, rssi: $rssi, type: $type, isConnected: $isConnected, manufacturerData: $manufacturerData, serviceUuids: $serviceUuids)';
|
return 'BluetoothDeviceModel(id: $id, name: $name, address: $address, type: $type, manufacturerData: $manufacturerData, deviceIdent: $deviceIdent)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,11 +238,9 @@ abstract mixin class _$BluetoothDeviceModelCopyWith<$Res>
|
|||||||
{String id,
|
{String id,
|
||||||
String? name,
|
String? name,
|
||||||
String address,
|
String address,
|
||||||
int? rssi,
|
|
||||||
DeviceType type,
|
DeviceType type,
|
||||||
bool isConnected,
|
|
||||||
Map<String, dynamic>? manufacturerData,
|
Map<String, dynamic>? manufacturerData,
|
||||||
List<String>? serviceUuids});
|
@DeviceIdentJsonConverter() DeviceIdentifier deviceIdent});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -320,11 +259,9 @@ class __$BluetoothDeviceModelCopyWithImpl<$Res>
|
|||||||
Object? id = null,
|
Object? id = null,
|
||||||
Object? name = freezed,
|
Object? name = freezed,
|
||||||
Object? address = null,
|
Object? address = null,
|
||||||
Object? rssi = freezed,
|
|
||||||
Object? type = null,
|
Object? type = null,
|
||||||
Object? isConnected = null,
|
|
||||||
Object? manufacturerData = freezed,
|
Object? manufacturerData = freezed,
|
||||||
Object? serviceUuids = freezed,
|
Object? deviceIdent = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_BluetoothDeviceModel(
|
return _then(_BluetoothDeviceModel(
|
||||||
id: null == id
|
id: null == id
|
||||||
@ -339,26 +276,18 @@ class __$BluetoothDeviceModelCopyWithImpl<$Res>
|
|||||||
? _self.address
|
? _self.address
|
||||||
: address // ignore: cast_nullable_to_non_nullable
|
: address // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
rssi: freezed == rssi
|
|
||||||
? _self.rssi
|
|
||||||
: rssi // ignore: cast_nullable_to_non_nullable
|
|
||||||
as int?,
|
|
||||||
type: null == type
|
type: null == type
|
||||||
? _self.type
|
? _self.type
|
||||||
: type // ignore: cast_nullable_to_non_nullable
|
: type // ignore: cast_nullable_to_non_nullable
|
||||||
as DeviceType,
|
as DeviceType,
|
||||||
isConnected: null == isConnected
|
|
||||||
? _self.isConnected
|
|
||||||
: isConnected // ignore: cast_nullable_to_non_nullable
|
|
||||||
as bool,
|
|
||||||
manufacturerData: freezed == manufacturerData
|
manufacturerData: freezed == manufacturerData
|
||||||
? _self._manufacturerData
|
? _self._manufacturerData
|
||||||
: manufacturerData // ignore: cast_nullable_to_non_nullable
|
: manufacturerData // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>?,
|
as Map<String, dynamic>?,
|
||||||
serviceUuids: freezed == serviceUuids
|
deviceIdent: null == deviceIdent
|
||||||
? _self._serviceUuids
|
? _self.deviceIdent
|
||||||
: serviceUuids // ignore: cast_nullable_to_non_nullable
|
: deviceIdent // ignore: cast_nullable_to_non_nullable
|
||||||
as List<String>?,
|
as DeviceIdentifier,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,14 +12,11 @@ _BluetoothDeviceModel _$BluetoothDeviceModelFromJson(
|
|||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
name: json['name'] as String?,
|
name: json['name'] as String?,
|
||||||
address: json['address'] as String,
|
address: json['address'] as String,
|
||||||
rssi: (json['rssi'] as num?)?.toInt(),
|
|
||||||
type: $enumDecodeNullable(_$DeviceTypeEnumMap, json['type']) ??
|
type: $enumDecodeNullable(_$DeviceTypeEnumMap, json['type']) ??
|
||||||
DeviceType.other,
|
DeviceType.other,
|
||||||
isConnected: json['isConnected'] as bool? ?? false,
|
|
||||||
manufacturerData: json['manufacturerData'] as Map<String, dynamic>?,
|
manufacturerData: json['manufacturerData'] as Map<String, dynamic>?,
|
||||||
serviceUuids: (json['serviceUuids'] as List<dynamic>?)
|
deviceIdent: const DeviceIdentJsonConverter()
|
||||||
?.map((e) => e as String)
|
.fromJson(json['deviceIdent'] as String),
|
||||||
.toList(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$BluetoothDeviceModelToJson(
|
Map<String, dynamic> _$BluetoothDeviceModelToJson(
|
||||||
@ -28,11 +25,10 @@ Map<String, dynamic> _$BluetoothDeviceModelToJson(
|
|||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'address': instance.address,
|
'address': instance.address,
|
||||||
'rssi': instance.rssi,
|
|
||||||
'type': _$DeviceTypeEnumMap[instance.type]!,
|
'type': _$DeviceTypeEnumMap[instance.type]!,
|
||||||
'isConnected': instance.isConnected,
|
|
||||||
'manufacturerData': instance.manufacturerData,
|
'manufacturerData': instance.manufacturerData,
|
||||||
'serviceUuids': instance.serviceUuids,
|
'deviceIdent':
|
||||||
|
const DeviceIdentJsonConverter().toJson(instance.deviceIdent),
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$DeviceTypeEnumMap = {
|
const _$DeviceTypeEnumMap = {
|
||||||
|
|||||||
299
lib/model/shifter_types.dart
Normal file
299
lib/model/shifter_types.dart
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
import 'package:cbor/simple.dart';
|
||||||
|
|
||||||
|
const String universalShifterControlServiceUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba4ffc2';
|
||||||
|
const String universalShifterStatusCharacteristicUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba40000';
|
||||||
|
const String universalShifterConnectToAddrCharacteristicUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba40001';
|
||||||
|
const String universalShifterCommandCharacteristicUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba40005';
|
||||||
|
const String universalShifterGearRatiosCharacteristicUuid =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba40006';
|
||||||
|
const String ftmsServiceUuid = '00001826-0000-1000-8000-00805f9b34fb';
|
||||||
|
|
||||||
|
const int errorSequence = 1;
|
||||||
|
const int errorFtmsMissing = 2;
|
||||||
|
const int errorPairingAuth = 3;
|
||||||
|
const int errorPairingEncrypt = 4;
|
||||||
|
const int errorFtmsRequiredCharMissing = 5;
|
||||||
|
|
||||||
|
class ShifterErrorInfo {
|
||||||
|
const ShifterErrorInfo({
|
||||||
|
required this.code,
|
||||||
|
required this.title,
|
||||||
|
required this.details,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int code;
|
||||||
|
final String title;
|
||||||
|
final String details;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShifterErrorInfo shifterErrorInfo(int code) {
|
||||||
|
switch (code) {
|
||||||
|
case errorSequence:
|
||||||
|
return const ShifterErrorInfo(
|
||||||
|
code: errorSequence,
|
||||||
|
title: 'Invalid command sequence',
|
||||||
|
details:
|
||||||
|
'The button received Connect without a fresh target address. The app must write connect_to_addr first, then the connect command.',
|
||||||
|
);
|
||||||
|
case errorFtmsMissing:
|
||||||
|
return const ShifterErrorInfo(
|
||||||
|
code: errorFtmsMissing,
|
||||||
|
title: 'FTMS service missing',
|
||||||
|
details:
|
||||||
|
'The selected bike does not expose the FTMS service (UUID 0x1826), so pairing cannot continue.',
|
||||||
|
);
|
||||||
|
case errorPairingAuth:
|
||||||
|
return const ShifterErrorInfo(
|
||||||
|
code: errorPairingAuth,
|
||||||
|
title: 'Pairing authentication failed',
|
||||||
|
details:
|
||||||
|
'Bonding authentication with the bike failed. Remove old bonds on both devices and try pairing again nearby.',
|
||||||
|
);
|
||||||
|
case errorPairingEncrypt:
|
||||||
|
return const ShifterErrorInfo(
|
||||||
|
code: errorPairingEncrypt,
|
||||||
|
title: 'Pairing/encryption failed',
|
||||||
|
details:
|
||||||
|
'The secure link to the bike could not be established. Retry close to the bike and ensure it is pairable.',
|
||||||
|
);
|
||||||
|
case errorFtmsRequiredCharMissing:
|
||||||
|
return const ShifterErrorInfo(
|
||||||
|
code: errorFtmsRequiredCharMissing,
|
||||||
|
title: 'Required FTMS characteristic missing',
|
||||||
|
details:
|
||||||
|
'The bike has FTMS but is missing required characteristics (for example Indoor Bike Data), so control cannot start.',
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return ShifterErrorInfo(
|
||||||
|
code: code,
|
||||||
|
title: 'Unknown error',
|
||||||
|
details: 'The button reported an unknown error code ($code).',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UniversalShifterCommand {
|
||||||
|
reset(0x00),
|
||||||
|
startScan(0x01),
|
||||||
|
stopScan(0x02),
|
||||||
|
connectToDevice(0x03),
|
||||||
|
disconnect(0x04),
|
||||||
|
turnOff(0x05);
|
||||||
|
|
||||||
|
const UniversalShifterCommand(this.value);
|
||||||
|
final int value;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ControlConnectionState {
|
||||||
|
disconnected,
|
||||||
|
connected;
|
||||||
|
|
||||||
|
static ControlConnectionState fromRaw(dynamic raw) {
|
||||||
|
if (raw is int) {
|
||||||
|
return raw == 1
|
||||||
|
? ControlConnectionState.connected
|
||||||
|
: ControlConnectionState.disconnected;
|
||||||
|
}
|
||||||
|
if (raw is String) {
|
||||||
|
final normalized = raw.toLowerCase();
|
||||||
|
if (normalized.contains('connected')) {
|
||||||
|
return ControlConnectionState.connected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ControlConnectionState.disconnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TrainerConnectionState {
|
||||||
|
idle,
|
||||||
|
connecting,
|
||||||
|
pairing,
|
||||||
|
discoveringFtms,
|
||||||
|
ftmsReady,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrainerStatus {
|
||||||
|
const TrainerStatus({required this.state, this.errorCode});
|
||||||
|
|
||||||
|
final TrainerConnectionState state;
|
||||||
|
final int? errorCode;
|
||||||
|
|
||||||
|
String get label {
|
||||||
|
switch (state) {
|
||||||
|
case TrainerConnectionState.idle:
|
||||||
|
return 'Idle';
|
||||||
|
case TrainerConnectionState.connecting:
|
||||||
|
return 'Connecting';
|
||||||
|
case TrainerConnectionState.pairing:
|
||||||
|
return 'Pairing';
|
||||||
|
case TrainerConnectionState.discoveringFtms:
|
||||||
|
return 'Discovering FTMS';
|
||||||
|
case TrainerConnectionState.ftmsReady:
|
||||||
|
return 'FTMS Ready';
|
||||||
|
case TrainerConnectionState.error:
|
||||||
|
return 'Error${errorCode != null ? ' ($errorCode)' : ''}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static TrainerStatus fromRaw(dynamic raw) {
|
||||||
|
if (raw is int) {
|
||||||
|
switch (raw) {
|
||||||
|
case 1:
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.connecting);
|
||||||
|
case 2:
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.pairing);
|
||||||
|
case 3:
|
||||||
|
return const TrainerStatus(
|
||||||
|
state: TrainerConnectionState.discoveringFtms);
|
||||||
|
case 4:
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.ftmsReady);
|
||||||
|
default:
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw is List && raw.isNotEmpty) {
|
||||||
|
final variant = raw.first;
|
||||||
|
final value = raw.length > 1 ? raw[1] : null;
|
||||||
|
if (variant is int && variant == 5) {
|
||||||
|
return TrainerStatus(
|
||||||
|
state: TrainerConnectionState.error,
|
||||||
|
errorCode: value is int ? value : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw is Map) {
|
||||||
|
final entry = raw.entries.isNotEmpty ? raw.entries.first : null;
|
||||||
|
if (entry != null) {
|
||||||
|
final key = entry.key;
|
||||||
|
final value = entry.value;
|
||||||
|
if ((key is int && key == 5) ||
|
||||||
|
(key is String && key.toLowerCase().contains('error'))) {
|
||||||
|
return TrainerStatus(
|
||||||
|
state: TrainerConnectionState.error,
|
||||||
|
errorCode: value is int ? value : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw is String) {
|
||||||
|
final normalized = raw.toLowerCase();
|
||||||
|
if (normalized.contains('connecting')) {
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.connecting);
|
||||||
|
}
|
||||||
|
if (normalized.contains('pairing')) {
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.pairing);
|
||||||
|
}
|
||||||
|
if (normalized.contains('discover')) {
|
||||||
|
return const TrainerStatus(
|
||||||
|
state: TrainerConnectionState.discoveringFtms);
|
||||||
|
}
|
||||||
|
if (normalized.contains('ready')) {
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.ftmsReady);
|
||||||
|
}
|
||||||
|
if (normalized.contains('error')) {
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return const TrainerStatus(state: TrainerConnectionState.idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CentralStatus {
|
||||||
|
const CentralStatus({
|
||||||
|
required this.control,
|
||||||
|
required this.trainer,
|
||||||
|
required this.hasSavedBond,
|
||||||
|
required this.connectedTrainerAddr,
|
||||||
|
required this.lastFailure,
|
||||||
|
required this.raw,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ControlConnectionState control;
|
||||||
|
final TrainerStatus trainer;
|
||||||
|
final bool hasSavedBond;
|
||||||
|
final List<int>? connectedTrainerAddr;
|
||||||
|
final int? lastFailure;
|
||||||
|
final dynamic raw;
|
||||||
|
|
||||||
|
String get statusLine =>
|
||||||
|
'Control: ${control.name}, Trainer: ${trainer.label}${lastFailure != null ? ', Last failure: $lastFailure' : ''}';
|
||||||
|
|
||||||
|
static CentralStatus fromCborBytes(List<int> bytes) {
|
||||||
|
final decoded = cbor.decode(bytes);
|
||||||
|
if (decoded is! Map) {
|
||||||
|
return CentralStatus(
|
||||||
|
control: ControlConnectionState.disconnected,
|
||||||
|
trainer: const TrainerStatus(state: TrainerConnectionState.idle),
|
||||||
|
hasSavedBond: false,
|
||||||
|
connectedTrainerAddr: null,
|
||||||
|
lastFailure: null,
|
||||||
|
raw: decoded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final controlRaw = _readMapValue(decoded, [0, 'control']);
|
||||||
|
final trainerRaw = _readMapValue(decoded, [1, 'trainer']);
|
||||||
|
final hasSavedBondRaw = _readMapValue(decoded, [2, 'has_saved_bond']);
|
||||||
|
final connectedTrainerAddrRaw =
|
||||||
|
_readMapValue(decoded, [3, 'connected_trainer_addr']);
|
||||||
|
final lastFailureRaw = _readMapValue(decoded, [4, 'last_failure']);
|
||||||
|
|
||||||
|
return CentralStatus(
|
||||||
|
control: ControlConnectionState.fromRaw(controlRaw),
|
||||||
|
trainer: TrainerStatus.fromRaw(trainerRaw),
|
||||||
|
hasSavedBond: hasSavedBondRaw is bool ? hasSavedBondRaw : false,
|
||||||
|
connectedTrainerAddr: _toByteList(connectedTrainerAddrRaw),
|
||||||
|
lastFailure: lastFailureRaw is int ? lastFailureRaw : null,
|
||||||
|
raw: decoded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic _readMapValue(Map<dynamic, dynamic> map, List<dynamic> keys) {
|
||||||
|
for (final key in keys) {
|
||||||
|
if (map.containsKey(key)) {
|
||||||
|
return map[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int>? _toByteList(dynamic value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value is List) {
|
||||||
|
return value.whereType<int>().toList(growable: false);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> parseMacToLittleEndianBytes(String macAddress) {
|
||||||
|
final compact = macAddress.replaceAll(':', '').replaceAll('-', '');
|
||||||
|
if (compact.length != 12) {
|
||||||
|
throw FormatException('Invalid MAC address format: $macAddress');
|
||||||
|
}
|
||||||
|
final bytes = <int>[];
|
||||||
|
for (int i = 0; i < compact.length; i += 2) {
|
||||||
|
bytes.add(int.parse(compact.substring(i, i + 2), radix: 16));
|
||||||
|
}
|
||||||
|
return bytes.reversed.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatMacAddressFromLittleEndian(List<int> bytes) {
|
||||||
|
if (bytes.length != 6) {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
return bytes.reversed
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||||
|
.join(':');
|
||||||
|
}
|
||||||
740
lib/pages/device_details_page.dart
Normal file
740
lib/pages/device_details_page.dart
Normal file
@ -0,0 +1,740 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||||
|
import 'package:abawo_bt_app/service/shifter_service.dart';
|
||||||
|
import 'package:abawo_bt_app/widgets/bike_scan_dialog.dart';
|
||||||
|
import 'package:abawo_bt_app/widgets/gear_ratio_editor_card.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:nb_utils/nb_utils.dart';
|
||||||
|
|
||||||
|
import '../controller/bluetooth.dart';
|
||||||
|
import '../database/database.dart';
|
||||||
|
|
||||||
|
class DeviceDetailsPage extends ConsumerStatefulWidget {
|
||||||
|
const DeviceDetailsPage({
|
||||||
|
required this.deviceAddress,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String deviceAddress;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DeviceDetailsPage> createState() => _DeviceDetailsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
||||||
|
static const List<double> _keAntRatios = [
|
||||||
|
0.35,
|
||||||
|
0.40,
|
||||||
|
0.47,
|
||||||
|
0.54,
|
||||||
|
0.61,
|
||||||
|
0.69,
|
||||||
|
0.82,
|
||||||
|
0.95,
|
||||||
|
1.13,
|
||||||
|
1.29,
|
||||||
|
1.50,
|
||||||
|
1.71,
|
||||||
|
1.89,
|
||||||
|
2.12,
|
||||||
|
2.40,
|
||||||
|
2.77,
|
||||||
|
3.27,
|
||||||
|
];
|
||||||
|
|
||||||
|
bool _isReconnecting = false;
|
||||||
|
bool _wasConnectedToCurrentDevice = false;
|
||||||
|
bool _isExitingPage = false;
|
||||||
|
bool _hasRequestedDisconnect = false;
|
||||||
|
Timer? _reconnectTimeoutTimer;
|
||||||
|
ProviderSubscription<AsyncValue<(ConnectionStatus, String?)>>?
|
||||||
|
_connectionStatusSubscription;
|
||||||
|
|
||||||
|
ShifterService? _shifterService;
|
||||||
|
StreamSubscription<CentralStatus>? _statusSubscription;
|
||||||
|
CentralStatus? _latestStatus;
|
||||||
|
final List<_StatusHistoryEntry> _statusHistory = [];
|
||||||
|
|
||||||
|
bool _isGearRatiosLoading = false;
|
||||||
|
bool _hasLoadedGearRatios = false;
|
||||||
|
String? _gearRatiosError;
|
||||||
|
List<double> _gearRatios = const [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_connectionStatusSubscription =
|
||||||
|
ref.listenManual<AsyncValue<(ConnectionStatus, String?)>>(
|
||||||
|
connectionStatusProvider,
|
||||||
|
(_, next) {
|
||||||
|
final data = next.valueOrNull;
|
||||||
|
if (data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_onConnectionStatusChanged(data);
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
unawaited(_disconnectOnClose());
|
||||||
|
_reconnectTimeoutTimer?.cancel();
|
||||||
|
_connectionStatusSubscription?.close();
|
||||||
|
_statusSubscription?.cancel();
|
||||||
|
_shifterService?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _disconnectOnClose() async {
|
||||||
|
if (_hasRequestedDisconnect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasRequestedDisconnect = true;
|
||||||
|
_isExitingPage = true;
|
||||||
|
_reconnectTimeoutTimer?.cancel();
|
||||||
|
|
||||||
|
final bluetooth = ref.read(bluetoothProvider).value;
|
||||||
|
await bluetooth?.disconnect();
|
||||||
|
await _stopStatusStreaming();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onConnectionStatusChanged((ConnectionStatus, String?) data) {
|
||||||
|
if (!mounted || _isExitingPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final (status, connectedDeviceId) = data;
|
||||||
|
final isCurrentDevice = connectedDeviceId == widget.deviceAddress;
|
||||||
|
|
||||||
|
if (isCurrentDevice && status == ConnectionStatus.connected) {
|
||||||
|
_wasConnectedToCurrentDevice = true;
|
||||||
|
_startStatusStreamingIfNeeded();
|
||||||
|
if (_isReconnecting) {
|
||||||
|
_reconnectTimeoutTimer?.cancel();
|
||||||
|
setState(() {
|
||||||
|
_isReconnecting = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_wasConnectedToCurrentDevice &&
|
||||||
|
!_isReconnecting &&
|
||||||
|
status == ConnectionStatus.disconnected) {
|
||||||
|
_startReconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCurrentDevice || status == ConnectionStatus.disconnected) {
|
||||||
|
_stopStatusStreaming();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startReconnect() async {
|
||||||
|
if (!mounted || _isExitingPage || _isReconnecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isReconnecting = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final bluetooth = ref.read(bluetoothProvider).value;
|
||||||
|
await bluetooth?.connectById(widget.deviceAddress);
|
||||||
|
|
||||||
|
_reconnectTimeoutTimer?.cancel();
|
||||||
|
_reconnectTimeoutTimer = Timer(const Duration(seconds: 10), () {
|
||||||
|
if (!mounted || !_isReconnecting || _isExitingPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_terminateConnectionAndGoHome(
|
||||||
|
'Connection lost. Could not reconnect in time.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startStatusStreamingIfNeeded() async {
|
||||||
|
if (_shifterService != null) {
|
||||||
|
if (!_hasLoadedGearRatios && !_isGearRatiosLoading) {
|
||||||
|
unawaited(_loadGearRatios());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final asyncBluetooth = ref.read(bluetoothProvider);
|
||||||
|
final BluetoothController bluetooth;
|
||||||
|
if (asyncBluetooth.hasValue) {
|
||||||
|
bluetooth = asyncBluetooth.requireValue;
|
||||||
|
} else {
|
||||||
|
bluetooth = await ref.read(bluetoothProvider.future);
|
||||||
|
}
|
||||||
|
final service = ShifterService(
|
||||||
|
bluetooth: bluetooth,
|
||||||
|
buttonDeviceId: widget.deviceAddress,
|
||||||
|
);
|
||||||
|
|
||||||
|
final initialStatusResult = await service.readStatus();
|
||||||
|
if (mounted && initialStatusResult.isOk()) {
|
||||||
|
_recordStatus(initialStatusResult.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
_statusSubscription = service.statusStream.listen((status) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_recordStatus(status);
|
||||||
|
});
|
||||||
|
|
||||||
|
service.startStatusNotifications();
|
||||||
|
setState(() {
|
||||||
|
_shifterService = service;
|
||||||
|
});
|
||||||
|
unawaited(_loadGearRatios());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _recordStatus(CentralStatus status) {
|
||||||
|
setState(() {
|
||||||
|
_latestStatus = status;
|
||||||
|
_statusHistory.insert(
|
||||||
|
0,
|
||||||
|
_StatusHistoryEntry(
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
status: status,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (_statusHistory.length > 100) {
|
||||||
|
_statusHistory.removeRange(100, _statusHistory.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _stopStatusStreaming() async {
|
||||||
|
await _statusSubscription?.cancel();
|
||||||
|
_statusSubscription = null;
|
||||||
|
await _shifterService?.dispose();
|
||||||
|
_shifterService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadGearRatios() async {
|
||||||
|
final shifter = _shifterService;
|
||||||
|
if (shifter == null || _isGearRatiosLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isGearRatiosLoading = true;
|
||||||
|
_gearRatiosError = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await shifter.readGearRatios();
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isErr()) {
|
||||||
|
setState(() {
|
||||||
|
_gearRatiosError = 'Failed to read gear ratios: ${result.unwrapErr()}';
|
||||||
|
_isGearRatiosLoading = false;
|
||||||
|
_hasLoadedGearRatios = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_gearRatios = result.unwrap();
|
||||||
|
_isGearRatiosLoading = false;
|
||||||
|
_hasLoadedGearRatios = true;
|
||||||
|
_gearRatiosError = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _saveGearRatios(List<double> ratios) async {
|
||||||
|
final shifter = _shifterService;
|
||||||
|
if (shifter == null) {
|
||||||
|
return 'Status channel is not ready yet.';
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await shifter.writeGearRatios(ratios);
|
||||||
|
if (result.isErr()) {
|
||||||
|
return 'Could not save gear ratios: ${result.unwrapErr()}';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_gearRatios = List<double>.from(ratios);
|
||||||
|
_hasLoadedGearRatios = true;
|
||||||
|
_gearRatiosError = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _connectButtonToBike() async {
|
||||||
|
final selectedBike = await BikeScanDialog.show(
|
||||||
|
context,
|
||||||
|
excludedDeviceId: widget.deviceAddress,
|
||||||
|
);
|
||||||
|
if (selectedBike == null || !mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _startStatusStreamingIfNeeded();
|
||||||
|
final shifter = _shifterService;
|
||||||
|
if (shifter == null) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Status channel is not ready yet.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await shifter.connectButtonToBike(selectedBike.id);
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isErr()) {
|
||||||
|
final err = result.unwrapErr();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Connect request failed: $err')),
|
||||||
|
);
|
||||||
|
toast('Connect request failed.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Sent connect request for ${selectedBike.id}.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _terminateConnectionAndGoHome(String toastMessage) async {
|
||||||
|
await _disconnectOnClose();
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast(toastMessage);
|
||||||
|
context.replace('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _cancelReconnect() async {
|
||||||
|
await _terminateConnectionAndGoHome('Reconnect cancelled.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _exitPage() async {
|
||||||
|
await _disconnectOnClose();
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.replace('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showStatusHistory() {
|
||||||
|
showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
showDragHandle: true,
|
||||||
|
builder: (context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.72,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Status Console',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: _statusHistory.isEmpty
|
||||||
|
? const Center(child: Text('No status updates yet.'))
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: _statusHistory.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = _statusHistory[index];
|
||||||
|
final errorCode = _effectiveErrorCode(item.status);
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
title: Text(
|
||||||
|
item.status.statusLine,
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
_formatTimestamp(item.timestamp),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: errorCode == null
|
||||||
|
? null
|
||||||
|
: IconButton(
|
||||||
|
tooltip: 'Explain error',
|
||||||
|
onPressed: () {
|
||||||
|
_showErrorInfoDialog(errorCode);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.info_outline),
|
||||||
|
),
|
||||||
|
onTap: errorCode == null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
_showErrorInfoDialog(errorCode);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatTimestamp(DateTime time) {
|
||||||
|
final h = time.hour.toString().padLeft(2, '0');
|
||||||
|
final m = time.minute.toString().padLeft(2, '0');
|
||||||
|
final s = time.second.toString().padLeft(2, '0');
|
||||||
|
return '$h:$m:$s';
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _effectiveErrorCode(CentralStatus status) {
|
||||||
|
if (status.trainer.state == TrainerConnectionState.error) {
|
||||||
|
return status.trainer.errorCode ?? status.lastFailure;
|
||||||
|
}
|
||||||
|
return status.lastFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showErrorInfoDialog(int errorCode) {
|
||||||
|
final info = shifterErrorInfo(errorCode);
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
icon: const Icon(Icons.info_outline),
|
||||||
|
title: Text('Error ${info.code}: ${info.title}'),
|
||||||
|
content: Text(info.details),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Close'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final connectionData = ref.watch(connectionStatusProvider).valueOrNull;
|
||||||
|
final isCurrentConnected = connectionData != null &&
|
||||||
|
connectionData.$1 == ConnectionStatus.connected &&
|
||||||
|
connectionData.$2 == widget.deviceAddress;
|
||||||
|
|
||||||
|
return PopScope(
|
||||||
|
canPop: false,
|
||||||
|
onPopInvokedWithResult: (bool didPop, bool? result) {
|
||||||
|
_exitPage();
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Device Details'),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: _exitPage,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildDeviceInfo(context, ref, widget.deviceAddress),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildConnectionStatus(context, ref, widget.deviceAddress),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (isCurrentConnected) ...[
|
||||||
|
_StatusBanner(
|
||||||
|
status: _latestStatus,
|
||||||
|
onTap: _showStatusHistory,
|
||||||
|
onErrorInfoTap: _latestStatus == null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
final code = _effectiveErrorCode(_latestStatus!);
|
||||||
|
if (code != null) {
|
||||||
|
_showErrorInfoDialog(code);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
GearRatioEditorCard(
|
||||||
|
ratios: _gearRatios,
|
||||||
|
isLoading: _isGearRatiosLoading,
|
||||||
|
errorText: _gearRatiosError,
|
||||||
|
onRetry: _loadGearRatios,
|
||||||
|
onSave: _saveGearRatios,
|
||||||
|
presets: const [
|
||||||
|
GearRatioPreset(
|
||||||
|
name: 'KeAnt Classic',
|
||||||
|
description:
|
||||||
|
'17-step baseline from KeAnt cross app gearing.',
|
||||||
|
ratios: _keAntRatios,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: _connectButtonToBike,
|
||||||
|
icon: const Icon(Icons.link),
|
||||||
|
label: const Text('Connect Button to Bike'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_isReconnecting)
|
||||||
|
Positioned.fill(
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Colors.black.withValues(alpha: 0.55),
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Reconnecting...',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _cancelReconnect,
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatusHistoryEntry {
|
||||||
|
const _StatusHistoryEntry({
|
||||||
|
required this.timestamp,
|
||||||
|
required this.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateTime timestamp;
|
||||||
|
final CentralStatus status;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatusBanner extends StatelessWidget {
|
||||||
|
const _StatusBanner({
|
||||||
|
required this.status,
|
||||||
|
required this.onTap,
|
||||||
|
this.onErrorInfoTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CentralStatus? status;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final VoidCallback? onErrorInfoTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final color = _resolveColor(colorScheme);
|
||||||
|
final text = status?.statusLine ?? 'Waiting for status updates...';
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: color.withValues(alpha: 0.16),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.memory, color: color),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (onErrorInfoTap != null &&
|
||||||
|
status != null &&
|
||||||
|
(status!.trainer.state == TrainerConnectionState.error ||
|
||||||
|
status!.lastFailure != null))
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Explain error',
|
||||||
|
onPressed: onErrorInfoTap,
|
||||||
|
icon: Icon(Icons.info_outline, color: color),
|
||||||
|
),
|
||||||
|
Icon(Icons.chevron_right, color: color),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _resolveColor(ColorScheme scheme) {
|
||||||
|
final current = status;
|
||||||
|
if (current == null) {
|
||||||
|
return scheme.primary;
|
||||||
|
}
|
||||||
|
if (current.trainer.state == TrainerConnectionState.error) {
|
||||||
|
return scheme.error;
|
||||||
|
}
|
||||||
|
if (current.trainer.state == TrainerConnectionState.ftmsReady) {
|
||||||
|
return Colors.green;
|
||||||
|
}
|
||||||
|
if (current.trainer.state == TrainerConnectionState.connecting ||
|
||||||
|
current.trainer.state == TrainerConnectionState.pairing ||
|
||||||
|
current.trainer.state == TrainerConnectionState.discoveringFtms) {
|
||||||
|
return Colors.orange;
|
||||||
|
}
|
||||||
|
return scheme.primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDeviceInfo(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
String deviceAddress,
|
||||||
|
) {
|
||||||
|
final asyncSavedDevices = ref.watch(nConnectedDevicesProvider);
|
||||||
|
|
||||||
|
return asyncSavedDevices.when(
|
||||||
|
data: (devices) {
|
||||||
|
ConnectedDevice? currentDeviceData;
|
||||||
|
try {
|
||||||
|
currentDeviceData = devices.firstWhere(
|
||||||
|
(d) => d.deviceAddress == deviceAddress,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
currentDeviceData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDeviceData == null) {
|
||||||
|
return Center(
|
||||||
|
child: Text('Device details not found for $deviceAddress.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Name: ${currentDeviceData.deviceName}',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Address: ${currentDeviceData.deviceAddress}',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Type: ${currentDeviceData.deviceType}',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (error, stackTrace) =>
|
||||||
|
Center(child: Text('Error loading device info: $error')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildConnectionStatus(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
String deviceAddress,
|
||||||
|
) {
|
||||||
|
final asyncConnectionStatus = ref.watch(connectionStatusProvider);
|
||||||
|
|
||||||
|
return asyncConnectionStatus.when(
|
||||||
|
data: (data) {
|
||||||
|
final (status, connectedDeviceId) = data;
|
||||||
|
String statusText;
|
||||||
|
final isCurrentDeviceConnected =
|
||||||
|
connectedDeviceId != null && connectedDeviceId == deviceAddress;
|
||||||
|
|
||||||
|
if (isCurrentDeviceConnected) {
|
||||||
|
switch (status) {
|
||||||
|
case ConnectionStatus.connected:
|
||||||
|
statusText = 'Status: Connected';
|
||||||
|
break;
|
||||||
|
case ConnectionStatus.connecting:
|
||||||
|
statusText = 'Status: Connecting...';
|
||||||
|
break;
|
||||||
|
case ConnectionStatus.disconnecting:
|
||||||
|
statusText = 'Status: Disconnecting...';
|
||||||
|
break;
|
||||||
|
case ConnectionStatus.disconnected:
|
||||||
|
statusText = 'Status: Disconnected';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statusText = 'Status: Disconnected';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Text(statusText, style: Theme.of(context).textTheme.titleMedium);
|
||||||
|
},
|
||||||
|
loading: () => const Text(
|
||||||
|
'Status: Unknown',
|
||||||
|
style: TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
),
|
||||||
|
error: (error, stackTrace) => Text(
|
||||||
|
'Status: Error ($error)',
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,11 +1,17 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
||||||
|
import 'package:abawo_bt_app/model/bluetooth_device_model.dart';
|
||||||
import 'package:abawo_bt_app/util/constants.dart';
|
import 'package:abawo_bt_app/util/constants.dart';
|
||||||
|
import 'package:anyhow/anyhow.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
|
||||||
import 'package:abawo_bt_app/widgets/device_listitem.dart';
|
import 'package:abawo_bt_app/widgets/device_listitem.dart';
|
||||||
import 'package:abawo_bt_app/widgets/scanning_animation.dart'; // Import the new animation widget
|
import 'package:abawo_bt_app/widgets/horizontal_scanning_animation.dart'; // Import the new horizontal animation
|
||||||
|
import 'package:abawo_bt_app/database/database.dart';
|
||||||
|
import 'package:drift/drift.dart' show Value;
|
||||||
|
|
||||||
const Duration _scanDuration = Duration(seconds: 10);
|
const Duration _scanDuration = Duration(seconds: 10);
|
||||||
|
|
||||||
@ -18,25 +24,20 @@ class ConnectDevicePage extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
|
class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
|
||||||
with TickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
// Use TickerProviderStateMixin for multiple controllers if needed later, good practice
|
// TickerProviderStateMixin is no longer needed as animations are self-contained or handled by StreamBuilder
|
||||||
bool _initialScanStarted = false;
|
int _retryScanCounter = 0; // Used to force animation reset
|
||||||
|
bool _initialScanTriggered = false; // Track if the first scan was requested
|
||||||
bool _showOnlyAbawoDevices = true; // State for filtering devices
|
bool _showOnlyAbawoDevices = true; // State for filtering devices
|
||||||
late AnimationController
|
|
||||||
_progressController; // Controller for scan duration progress
|
|
||||||
late AnimationController
|
|
||||||
_waveAnimationController; // Controller for wave animation
|
|
||||||
|
|
||||||
// Function to start scan safely after controller is ready
|
// Function to start scan safely after controller is ready
|
||||||
void _startScanIfNeeded(BluetoothController controller) {
|
void _startScanIfNeeded(BluetoothController controller) {
|
||||||
// Use WidgetsBinding to schedule the scan start after the build phase
|
// Use WidgetsBinding to schedule the scan start after the build phase
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!_initialScanStarted && mounted) {
|
// Start scan only if it hasn't been triggered yet and the widget is mounted
|
||||||
|
if (!_initialScanTriggered && mounted) {
|
||||||
controller.startScan(timeout: _scanDuration);
|
controller.startScan(timeout: _scanDuration);
|
||||||
_startScanProgressAnimation(); // Start scan duration progress animation
|
|
||||||
_startWaveAnimation(); // Start the wave animation
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_initialScanStarted = true;
|
_initialScanTriggered = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,64 +47,17 @@ class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Initialize scan progress controller
|
super.initState();
|
||||||
_progressController = AnimationController(
|
// No animation controllers needed here anymore
|
||||||
vsync: this,
|
|
||||||
duration: _scanDuration,
|
|
||||||
)..addListener(() {
|
|
||||||
// Trigger rebuild when animation value changes for the progress indicator
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize wave animation controller
|
|
||||||
_waveAnimationController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 1500), // New duration: 1.5 seconds
|
|
||||||
)..repeat(); // Make the wave animation repeat
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// Stop animations before disposing
|
// Dispose controllers if they existed (they don't anymore)
|
||||||
_progressController.stop();
|
|
||||||
_waveAnimationController.stop();
|
|
||||||
_progressController.dispose();
|
|
||||||
_waveAnimationController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method to start/reset scan progress animation
|
// Removed _startScanProgressAnimation, _startWaveAnimation, _stopAnimations
|
||||||
void _startScanProgressAnimation() {
|
|
||||||
if (mounted) {
|
|
||||||
_progressController.reset();
|
|
||||||
_progressController.forward().whenCompleteOrCancel(() {
|
|
||||||
// Optional: Add logic if needed when scan progress completes or cancels
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper method to start the wave animation
|
|
||||||
void _startWaveAnimation() {
|
|
||||||
if (mounted && !_waveAnimationController.isAnimating) {
|
|
||||||
_waveAnimationController.reset();
|
|
||||||
_waveAnimationController.repeat();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper method to stop animations when scan finishes/cancels
|
|
||||||
void _stopAnimations() {
|
|
||||||
if (mounted) {
|
|
||||||
if (_progressController.isAnimating) {
|
|
||||||
_progressController.stop();
|
|
||||||
}
|
|
||||||
if (_waveAnimationController.isAnimating) {
|
|
||||||
_waveAnimationController.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -134,155 +88,264 @@ class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Column(
|
||||||
child: Column(
|
// Use Column instead of Center(Column(...))
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
children: [
|
const Padding(
|
||||||
const Text(
|
padding: EdgeInsets.all(16.0), // Add padding around the title
|
||||||
|
child: Text(
|
||||||
'Available Devices',
|
'Available Devices',
|
||||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
),
|
||||||
// Use Consumer to react to bluetoothProvider changes
|
// Use Consumer to get the BluetoothController
|
||||||
Expanded(
|
Expanded(
|
||||||
// Allow the Consumer content to expand
|
// Allow the device list to take available space
|
||||||
child: Consumer(builder: (context, ref, child) {
|
child: Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
final btAsyncValue = ref.watch(bluetoothProvider);
|
final btAsyncValue = ref.watch(bluetoothProvider);
|
||||||
|
final connectedDevices =
|
||||||
|
ref.watch(nConnectedDevicesProvider).valueOrNull ??
|
||||||
|
const <ConnectedDevice>[];
|
||||||
|
final connectedDeviceAddresses = connectedDevices
|
||||||
|
.map((device) => device.deviceAddress)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
return btAsyncValue.when(
|
return btAsyncValue.when(
|
||||||
loading: () => const Center(
|
loading: () =>
|
||||||
child:
|
const Center(child: CircularProgressIndicator()),
|
||||||
CircularProgressIndicator()), // Center loading indicator
|
error: (err, stack) => Center(child: Text('Error: $err')),
|
||||||
error: (err, stack) => Center(
|
|
||||||
child: Text(
|
|
||||||
'Error loading Bluetooth: $err')), // Center error
|
|
||||||
data: (controller) {
|
data: (controller) {
|
||||||
// Start the initial scan once the controller is ready
|
// Trigger the initial scan if needed
|
||||||
// Start initial scan and animation
|
|
||||||
_startScanIfNeeded(controller);
|
_startScanIfNeeded(controller);
|
||||||
|
|
||||||
// Use StreamBuilder to watch the scanning state
|
// StreamBuilder for Scan Results (Device List)
|
||||||
return StreamBuilder<bool>(
|
return StreamBuilder<List<DiscoveredDevice>>(
|
||||||
stream: FlutterBluePlus.isScanning,
|
stream: controller.scanResultsStream,
|
||||||
initialData:
|
initialData: const [],
|
||||||
false, // Default to not scanning before check
|
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final isScanning = snapshot.data ?? false;
|
final results = snapshot.data ?? [];
|
||||||
|
// Filter results based on the toggle state
|
||||||
|
final filteredResults = _showOnlyAbawoDevices
|
||||||
|
? results
|
||||||
|
.where((device) =>
|
||||||
|
device.serviceUuids.any(isAbawoDeviceGuid))
|
||||||
|
.toList()
|
||||||
|
: results;
|
||||||
|
|
||||||
if (isScanning && _initialScanStarted) {
|
if (!_initialScanTriggered && filteredResults.isEmpty) {
|
||||||
_startWaveAnimation(); // Ensure wave animation is running
|
// Show a message or placeholder before the first scan starts or if no devices found initially
|
||||||
// Show the new scanning wave animation with the progress indicator value
|
return const Center(
|
||||||
return Center(
|
child: Text(
|
||||||
// Pass the wave animation controller. The progress value will be added
|
'Scanning for devices...')); // Or CircularProgressIndicator()
|
||||||
// to the ScanningWaveAnimation widget itself later.
|
|
||||||
child: ScanningWaveAnimation(
|
|
||||||
animation: _waveAnimationController,
|
|
||||||
progressValue: _progressController
|
|
||||||
.value, // Pass the scan progress
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (!_initialScanStarted) {
|
|
||||||
// Show placeholder or button to start initial scan if needed, or just empty space
|
|
||||||
return const SizedBox(
|
|
||||||
height: 50); // Placeholder before scan starts
|
|
||||||
} else {
|
|
||||||
// Scan finished, stop animations and show results
|
|
||||||
_stopAnimations();
|
|
||||||
final results = controller.scanResults;
|
|
||||||
// Filter results based on the toggle state
|
|
||||||
final filteredResults = _showOnlyAbawoDevices
|
|
||||||
? results
|
|
||||||
.where((device) => device
|
|
||||||
.advertisementData.serviceUuids
|
|
||||||
.contains(Guid(abawoServiceBtUUID)))
|
|
||||||
.toList()
|
|
||||||
: results;
|
|
||||||
|
|
||||||
// Use Column + Expanded for ListView + Button layout
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
// Allow ListView to take available space
|
|
||||||
child: filteredResults
|
|
||||||
.isEmpty // Use filtered list check
|
|
||||||
? const Center(
|
|
||||||
child: Text(
|
|
||||||
'No devices found.')) // Center empty text
|
|
||||||
: ListView.builder(
|
|
||||||
itemCount: filteredResults
|
|
||||||
.length, // Use filtered list length
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final device = filteredResults[
|
|
||||||
index]; // Use filtered list
|
|
||||||
final isAbawoDevice = device
|
|
||||||
.advertisementData.serviceUuids
|
|
||||||
.contains(
|
|
||||||
Guid(abawoServiceBtUUID));
|
|
||||||
final deviceName =
|
|
||||||
device.device.advName.isEmpty
|
|
||||||
? 'Unknown Device'
|
|
||||||
: device.device.advName;
|
|
||||||
// Use the custom DeviceListItem widget
|
|
||||||
return InkWell(
|
|
||||||
// Wrap with InkWell for tap feedback
|
|
||||||
onTap: () {
|
|
||||||
if (!isAbawoDevice) {
|
|
||||||
// Show a snackbar for non-Abawo devices
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(const SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'This app can only connect to abawo devices.')));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// TODO: Implement connect logic
|
|
||||||
// controller.connectToDevice(device.device); // Pass the BluetoothDevice
|
|
||||||
// context.go('/control/${device.device.remoteId.str}');
|
|
||||||
print(
|
|
||||||
'Tapped on ${device.device.remoteId.str}');
|
|
||||||
},
|
|
||||||
child: DeviceListItem(
|
|
||||||
deviceName: deviceName,
|
|
||||||
deviceId:
|
|
||||||
device.device.remoteId.str,
|
|
||||||
isUnknownDevice:
|
|
||||||
device.device.advName.isEmpty,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} // End of itemBuilder
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
// Add padding around the button
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
// Retry scan by calling startScan on the controller
|
|
||||||
// Ensure _initialScanStarted is true so indicator shows
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_initialScanStarted = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
controller.startScan(
|
|
||||||
timeout: _scanDuration);
|
|
||||||
_startScanProgressAnimation(); // Restart scan progress animation
|
|
||||||
_startWaveAnimation(); // Ensure wave animation runs on retry
|
|
||||||
},
|
|
||||||
child: const Text('Retry Scan'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filteredResults.isEmpty && _initialScanTriggered) {
|
||||||
|
// Show 'No devices found' only after the initial scan was triggered
|
||||||
|
return const Center(child: Text('No devices found.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the list
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: filteredResults.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final device = filteredResults[index];
|
||||||
|
final isAlreadyConnected =
|
||||||
|
connectedDeviceAddresses.contains(device.id);
|
||||||
|
final abawoDevice =
|
||||||
|
device.serviceUuids.any(isAbawoDeviceGuid);
|
||||||
|
final connectable = device.serviceUuids
|
||||||
|
.any(isConnectableAbawoDeviceGuid);
|
||||||
|
final deviceName = device.name.isEmpty
|
||||||
|
? 'Unknown Device'
|
||||||
|
: device.name;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
if (isAlreadyConnected) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'This device is already connected in the app.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!abawoDevice) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'This app can only connect to abawo devices.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (!connectable) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'This device is not connectable with the app.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
final res = await controller.connect(device);
|
||||||
|
print('res: $res');
|
||||||
|
switch (res) {
|
||||||
|
case Ok():
|
||||||
|
// trigger pairing/permission prompt if needed
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
controller.readCharacteristic(
|
||||||
|
device.id,
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba4ffc2',
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba40000');
|
||||||
|
}
|
||||||
|
// Save to DB and navigate
|
||||||
|
final notifier = ref.read(
|
||||||
|
nConnectedDevicesProvider.notifier);
|
||||||
|
final name = device.name.isNotEmpty
|
||||||
|
? device.name
|
||||||
|
: 'Unknown Device';
|
||||||
|
final deviceCompanion =
|
||||||
|
ConnectedDevicesCompanion(
|
||||||
|
deviceName: Value(name),
|
||||||
|
deviceAddress: Value(device.id),
|
||||||
|
deviceType: Value(deviceTypeToString(
|
||||||
|
deviceTypeFromUuids(
|
||||||
|
device.serviceUuids))),
|
||||||
|
lastConnectedAt: Value(DateTime.now()),
|
||||||
|
);
|
||||||
|
final addResult = await notifier
|
||||||
|
.addConnectedDevice(deviceCompanion);
|
||||||
|
|
||||||
|
// Check if mounted before using context
|
||||||
|
if (!context.mounted) break;
|
||||||
|
|
||||||
|
if (addResult.isErr()) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Failed to save device: ${addResult.unwrapErr()}')),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
context.go('/device/${device.id}');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Err(:final v):
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Connection unsuccessful:\n${v.toString()}'),
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print('Tapped on ${device.id}');
|
||||||
|
},
|
||||||
|
child: DeviceListItem(
|
||||||
|
deviceName: deviceName,
|
||||||
|
deviceId: device.id,
|
||||||
|
type: deviceTypeFromUuids(device.serviceUuids),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
// Bottom section: Scanning Animation and Retry Button (visible only when scanning)
|
||||||
),
|
Consumer(
|
||||||
|
// Use Consumer to get the controller for the retry button action
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final btController = ref
|
||||||
|
.watch(bluetoothProvider)
|
||||||
|
.asData
|
||||||
|
?.value; // Get controller safely
|
||||||
|
|
||||||
|
return StreamBuilder<bool>(
|
||||||
|
stream: btController?.isScanningStream ?? Stream<bool>.empty(),
|
||||||
|
initialData: false,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final isScanning = snapshot.data ?? false;
|
||||||
|
|
||||||
|
// Show bottom section only if scanning
|
||||||
|
if (!isScanning) {
|
||||||
|
// Show only the retry button when not scanning (optional, could be hidden)
|
||||||
|
// For now, let's keep the button always visible but disabled when not scannable.
|
||||||
|
// A better approach might be to hide the button when not scanning.
|
||||||
|
// Let's show the button but potentially disabled later if controller is null.
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: btController != null
|
||||||
|
? () {
|
||||||
|
// Retry scan ONLY when NOT currently scanning
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_initialScanTriggered =
|
||||||
|
true; // Ensure state reflects scan attempt
|
||||||
|
_retryScanCounter++; // Increment key counter
|
||||||
|
});
|
||||||
|
}
|
||||||
|
btController.startScan(timeout: _scanDuration);
|
||||||
|
}
|
||||||
|
: null, // Disable if controller not ready
|
||||||
|
child: const Text('Retry Scan'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If scanning, show animation and button
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8.0, horizontal: 16.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.scaffoldBackgroundColor
|
||||||
|
.withValues(alpha: 0.9), // Slight overlay effect
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, -4), // Shadow upwards
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min, // Keep column compact
|
||||||
|
children: [
|
||||||
|
// Pass isScanning and the ValueKey
|
||||||
|
HorizontalScanningAnimation(
|
||||||
|
key: ValueKey(
|
||||||
|
_retryScanCounter), // Force state rebuild on counter change
|
||||||
|
isScanning: isScanning,
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
// Button does nothing if pressed *while* scanning.
|
||||||
|
// It just indicates the status.
|
||||||
|
onPressed: null, // Disable button while scanning
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
disabledBackgroundColor: Theme.of(context)
|
||||||
|
.primaryColor
|
||||||
|
.withValues(alpha: 0.5), // Custom disabled color
|
||||||
|
disabledForegroundColor: Colors.white70,
|
||||||
|
),
|
||||||
|
child:
|
||||||
|
const Text('Scanning...'), // Just indicate status
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8), // Add some bottom padding
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
], // End of outer Column children
|
||||||
|
), // End of Scaffold
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
|
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
||||||
|
import 'package:abawo_bt_app/database/database.dart';
|
||||||
|
import 'package:abawo_bt_app/model/bluetooth_device_model.dart';
|
||||||
|
import 'package:abawo_bt_app/widgets/device_listitem.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class HomePage extends StatelessWidget {
|
class HomePage extends StatelessWidget {
|
||||||
@ -53,10 +58,7 @@ class HomePage extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: Text(
|
child: DevicesList(),
|
||||||
'No devices connected yet',
|
|
||||||
style: TextStyle(color: Colors.grey),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -73,3 +75,148 @@ class HomePage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DevicesList extends ConsumerStatefulWidget {
|
||||||
|
const DevicesList({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DevicesList> createState() => _DevicesListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DevicesListState extends ConsumerState<DevicesList> {
|
||||||
|
String? _connectingDeviceId; // ID of device currently being connected
|
||||||
|
|
||||||
|
Future<void> _removeDevice(ConnectedDevice device) async {
|
||||||
|
final shouldRemove = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: const Text('Remove device?'),
|
||||||
|
content:
|
||||||
|
Text('Do you want to remove ${device.deviceName} from the app?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||||
|
child: const Text('Remove'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldRemove != true || !mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref
|
||||||
|
.read(nConnectedDevicesProvider.notifier)
|
||||||
|
.deleteConnectedDevice(device.id);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isErr()) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to remove device: ${result.unwrapErr()}')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_connectingDeviceId == device.deviceAddress) {
|
||||||
|
setState(() {
|
||||||
|
_connectingDeviceId = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('${device.deviceName} removed from the app.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final asyncDevices = ref.watch(nConnectedDevicesProvider);
|
||||||
|
|
||||||
|
return asyncDevices.when(
|
||||||
|
loading: () => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
error: (error, stackTrace) => Center(
|
||||||
|
child: Text(
|
||||||
|
'Error loading devices: ${error.toString()}',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
data: (devices) {
|
||||||
|
if (devices.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text(
|
||||||
|
'No devices connected yet',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: devices.length,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final device = devices[index];
|
||||||
|
return InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
if (_connectingDeviceId != null) return;
|
||||||
|
setState(() {
|
||||||
|
_connectingDeviceId = device.deviceAddress;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final controller = await ref.read(bluetoothProvider.future);
|
||||||
|
final result = await controller.connectById(
|
||||||
|
device.deviceAddress,
|
||||||
|
timeout: const Duration(seconds: 10),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isOk()) {
|
||||||
|
context.go('/device/${device.deviceAddress}');
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Connection failed. Is the device turned on and in range?'),
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error: ${e.toString()}')),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
_connectingDeviceId = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: DeviceListItem(
|
||||||
|
deviceName: device.deviceName,
|
||||||
|
deviceId: device.deviceAddress,
|
||||||
|
type: deviceTypeFromString(device.deviceType),
|
||||||
|
isConnecting: device.deviceAddress == _connectingDeviceId,
|
||||||
|
trailing: IconButton(
|
||||||
|
tooltip: 'Remove device',
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
onPressed: () => _removeDevice(device),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
179
lib/service/shifter_service.dart
Normal file
179
lib/service/shifter_service.dart
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
||||||
|
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||||
|
import 'package:anyhow/anyhow.dart';
|
||||||
|
|
||||||
|
class ShifterService {
|
||||||
|
ShifterService({
|
||||||
|
required BluetoothController bluetooth,
|
||||||
|
required this.buttonDeviceId,
|
||||||
|
}) : _bluetooth = bluetooth;
|
||||||
|
|
||||||
|
final BluetoothController _bluetooth;
|
||||||
|
final String buttonDeviceId;
|
||||||
|
|
||||||
|
final StreamController<CentralStatus> _statusController =
|
||||||
|
StreamController<CentralStatus>.broadcast();
|
||||||
|
StreamSubscription<List<int>>? _statusSubscription;
|
||||||
|
|
||||||
|
Stream<CentralStatus> get statusStream => _statusController.stream;
|
||||||
|
|
||||||
|
static const int _gearRatioSlots = 32;
|
||||||
|
static const double _maxGearRatio = 255 / 64;
|
||||||
|
static const int _gearRatioWriteMtu = 64;
|
||||||
|
|
||||||
|
Future<Result<void>> writeConnectToAddress(String bikeDeviceId) async {
|
||||||
|
try {
|
||||||
|
final payload = parseMacToLittleEndianBytes(bikeDeviceId);
|
||||||
|
return _bluetooth.writeCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterConnectToAddrCharacteristicUuid,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
} on FormatException catch (e) {
|
||||||
|
return bail('Could not parse bike address "$bikeDeviceId": $e');
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Failed writing connect address: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> writeCommand(UniversalShifterCommand command) {
|
||||||
|
return _bluetooth.writeCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterCommandCharacteristicUuid,
|
||||||
|
[command.value],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> connectButtonToBike(String bikeDeviceId) async {
|
||||||
|
final addrRes = await writeConnectToAddress(bikeDeviceId);
|
||||||
|
if (addrRes.isErr()) {
|
||||||
|
return addrRes;
|
||||||
|
}
|
||||||
|
return writeCommand(UniversalShifterCommand.connectToDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<List<double>>> readGearRatios() async {
|
||||||
|
final readRes = await _bluetooth.readCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterGearRatiosCharacteristicUuid,
|
||||||
|
);
|
||||||
|
if (readRes.isErr()) {
|
||||||
|
return bail(readRes.unwrapErr());
|
||||||
|
}
|
||||||
|
|
||||||
|
final raw = readRes.unwrap();
|
||||||
|
if (raw.length > _gearRatioSlots) {
|
||||||
|
return bail(
|
||||||
|
'Invalid gear ratio payload length: expected at most $_gearRatioSlots, got ${raw.length}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalizedRaw = List<int>.filled(_gearRatioSlots, 0, growable: false);
|
||||||
|
for (var i = 0; i < raw.length; i++) {
|
||||||
|
normalizedRaw[i] = raw[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
final ratios = normalizedRaw
|
||||||
|
.where((v) => v > 0)
|
||||||
|
.map((v) => _decodeGearRatio(v))
|
||||||
|
.toList(growable: false);
|
||||||
|
return Ok(ratios);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> writeGearRatios(List<double> ratios) async {
|
||||||
|
final mtuResult =
|
||||||
|
await _bluetooth.requestMtu(buttonDeviceId, mtu: _gearRatioWriteMtu);
|
||||||
|
if (mtuResult.isErr()) {
|
||||||
|
return bail(
|
||||||
|
'Could not request MTU before writing gear ratios: ${mtuResult.unwrapErr()}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final payload = List<int>.filled(_gearRatioSlots, 0, growable: false);
|
||||||
|
final limit =
|
||||||
|
ratios.length < _gearRatioSlots ? ratios.length : _gearRatioSlots;
|
||||||
|
for (var i = 0; i < limit; i++) {
|
||||||
|
payload[i] = _encodeGearRatio(ratios[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _bluetooth.writeCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterGearRatiosCharacteristicUuid,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<CentralStatus>> readStatus() async {
|
||||||
|
final readRes = await _bluetooth.readCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterStatusCharacteristicUuid,
|
||||||
|
);
|
||||||
|
if (readRes.isErr()) {
|
||||||
|
return bail(readRes.unwrapErr());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Ok(CentralStatus.fromCborBytes(readRes.unwrap()));
|
||||||
|
} catch (e) {
|
||||||
|
return bail('Failed to decode status payload: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void startStatusNotifications() {
|
||||||
|
if (_statusSubscription != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_statusSubscription = _bluetooth
|
||||||
|
.subscribeToCharacteristic(
|
||||||
|
buttonDeviceId,
|
||||||
|
universalShifterControlServiceUuid,
|
||||||
|
universalShifterStatusCharacteristicUuid,
|
||||||
|
)
|
||||||
|
.listen(
|
||||||
|
(data) {
|
||||||
|
try {
|
||||||
|
final status = CentralStatus.fromCborBytes(data);
|
||||||
|
_statusController.add(status);
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore malformed payloads but keep stream alive.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (_) {
|
||||||
|
// Keep UI running; reconnection logic is handled elsewhere.
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stopStatusNotifications() async {
|
||||||
|
await _statusSubscription?.cancel();
|
||||||
|
_statusSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await stopStatusNotifications();
|
||||||
|
await _statusController.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
int _encodeGearRatio(double value) {
|
||||||
|
if (value <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
final clamped = value.clamp(0, _maxGearRatio);
|
||||||
|
final scaled = (clamped * 64).round();
|
||||||
|
if (scaled <= 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return scaled.clamp(1, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _decodeGearRatio(int raw) {
|
||||||
|
return raw / 64.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
lib/src/rust/api/simple.dart
Normal file
10
lib/src/rust/api/simple.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||||
|
|
||||||
|
import '../frb_generated.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
|
String greet({required String name}) =>
|
||||||
|
RustLib.instance.api.crateApiSimpleGreet(name: name);
|
||||||
240
lib/src/rust/frb_generated.dart
Normal file
240
lib/src/rust/frb_generated.dart
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||||
|
|
||||||
|
import 'api/simple.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'frb_generated.dart';
|
||||||
|
import 'frb_generated.io.dart'
|
||||||
|
if (dart.library.js_interop) 'frb_generated.web.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
|
/// Main entrypoint of the Rust API
|
||||||
|
class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||||
|
@internal
|
||||||
|
static final instance = RustLib._();
|
||||||
|
|
||||||
|
RustLib._();
|
||||||
|
|
||||||
|
/// Initialize flutter_rust_bridge
|
||||||
|
static Future<void> init({
|
||||||
|
RustLibApi? api,
|
||||||
|
BaseHandler? handler,
|
||||||
|
ExternalLibrary? externalLibrary,
|
||||||
|
bool forceSameCodegenVersion = true,
|
||||||
|
}) async {
|
||||||
|
await instance.initImpl(
|
||||||
|
api: api,
|
||||||
|
handler: handler,
|
||||||
|
externalLibrary: externalLibrary,
|
||||||
|
forceSameCodegenVersion: forceSameCodegenVersion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize flutter_rust_bridge in mock mode.
|
||||||
|
/// No libraries for FFI are loaded.
|
||||||
|
static void initMock({
|
||||||
|
required RustLibApi api,
|
||||||
|
}) {
|
||||||
|
instance.initMockImpl(
|
||||||
|
api: api,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispose flutter_rust_bridge
|
||||||
|
///
|
||||||
|
/// The call to this function is optional, since flutter_rust_bridge (and everything else)
|
||||||
|
/// is automatically disposed when the app stops.
|
||||||
|
static void dispose() => instance.disposeImpl();
|
||||||
|
|
||||||
|
@override
|
||||||
|
ApiImplConstructor<RustLibApiImpl, RustLibWire> get apiImplConstructor =>
|
||||||
|
RustLibApiImpl.new;
|
||||||
|
|
||||||
|
@override
|
||||||
|
WireConstructor<RustLibWire> get wireConstructor =>
|
||||||
|
RustLibWire.fromExternalLibrary;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> executeRustInitializers() async {
|
||||||
|
await api.crateApiSimpleInitApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig =>
|
||||||
|
kDefaultExternalLibraryLoaderConfig;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get codegenVersion => '2.11.1';
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get rustContentHash => -1918914929;
|
||||||
|
|
||||||
|
static const kDefaultExternalLibraryLoaderConfig =
|
||||||
|
ExternalLibraryLoaderConfig(
|
||||||
|
stem: 'rust_lib_abawo_bt_app',
|
||||||
|
ioDirectory: 'rust/target/release/',
|
||||||
|
webPrefix: 'pkg/',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class RustLibApi extends BaseApi {
|
||||||
|
String crateApiSimpleGreet({required String name});
|
||||||
|
|
||||||
|
Future<void> crateApiSimpleInitApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
|
RustLibApiImpl({
|
||||||
|
required super.handler,
|
||||||
|
required super.wire,
|
||||||
|
required super.generalizedFrbRustBinding,
|
||||||
|
required super.portManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String crateApiSimpleGreet({required String name}) {
|
||||||
|
return handler.executeSync(SyncTask(
|
||||||
|
callFfi: () {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_String(name, serializer);
|
||||||
|
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!;
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_String,
|
||||||
|
decodeErrorData: null,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiSimpleGreetConstMeta,
|
||||||
|
argValues: [name],
|
||||||
|
apiImpl: this,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiSimpleGreetConstMeta => const TaskConstMeta(
|
||||||
|
debugName: "greet",
|
||||||
|
argNames: ["name"],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> crateApiSimpleInitApp() {
|
||||||
|
return handler.executeNormal(NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
pdeCallFfi(generalizedFrbRustBinding, serializer,
|
||||||
|
funcId: 2, port: port_);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: null,
|
||||||
|
),
|
||||||
|
constMeta: kCrateApiSimpleInitAppConstMeta,
|
||||||
|
argValues: [],
|
||||||
|
apiImpl: this,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta get kCrateApiSimpleInitAppConstMeta => const TaskConstMeta(
|
||||||
|
debugName: "init_app",
|
||||||
|
argNames: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String dco_decode_String(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw as Uint8List;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int dco_decode_u_8(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return raw as int;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void dco_decode_unit(dynamic raw) {
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String sse_decode_String(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
var inner = sse_decode_list_prim_u_8_strict(deserializer);
|
||||||
|
return utf8.decoder.convert(inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
var len_ = sse_decode_i_32(deserializer);
|
||||||
|
return deserializer.buffer.getUint8List(len_);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_u_8(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
return deserializer.buffer.getUint8();
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_decode_unit(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_i_32(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
return deserializer.buffer.getInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool sse_decode_bool(SseDeserializer deserializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
return deserializer.buffer.getUint8() != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_String(String self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_prim_u_8_strict(
|
||||||
|
Uint8List self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
sse_encode_i_32(self.length, serializer);
|
||||||
|
serializer.buffer.putUint8List(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_u_8(int self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
serializer.buffer.putUint8(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_unit(void self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_i_32(int self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
serializer.buffer.putInt32(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_bool(bool self, SseSerializer serializer) {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
serializer.buffer.putUint8(self ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
lib/src/rust/frb_generated.io.dart
Normal file
84
lib/src/rust/frb_generated.io.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||||
|
|
||||||
|
import 'api/simple.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:ffi' as ffi;
|
||||||
|
import 'frb_generated.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
||||||
|
|
||||||
|
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
|
RustLibApiImplPlatform({
|
||||||
|
required super.handler,
|
||||||
|
required super.wire,
|
||||||
|
required super.generalizedFrbRustBinding,
|
||||||
|
required super.portManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int dco_decode_u_8(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void dco_decode_unit(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_u_8(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_decode_unit(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_i_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool sse_decode_bool(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_prim_u_8_strict(
|
||||||
|
Uint8List self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_u_8(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_unit(void self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_i_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section: wire_class
|
||||||
|
|
||||||
|
class RustLibWire implements BaseWire {
|
||||||
|
factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) =>
|
||||||
|
RustLibWire(lib.ffiDynamicLibrary);
|
||||||
|
|
||||||
|
/// Holds the symbol lookup function.
|
||||||
|
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
|
||||||
|
_lookup;
|
||||||
|
|
||||||
|
/// The symbols are looked up in [dynamicLibrary].
|
||||||
|
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
|
||||||
|
: _lookup = dynamicLibrary.lookup;
|
||||||
|
}
|
||||||
84
lib/src/rust/frb_generated.web.dart
Normal file
84
lib/src/rust/frb_generated.web.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
|
||||||
|
|
||||||
|
// Static analysis wrongly picks the IO variant, thus ignore this
|
||||||
|
// ignore_for_file: argument_type_not_assignable
|
||||||
|
|
||||||
|
import 'api/simple.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'frb_generated.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
||||||
|
|
||||||
|
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
|
RustLibApiImplPlatform({
|
||||||
|
required super.handler,
|
||||||
|
required super.wire,
|
||||||
|
required super.generalizedFrbRustBinding,
|
||||||
|
required super.portManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String dco_decode_String(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int dco_decode_u_8(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void dco_decode_unit(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
String sse_decode_String(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_u_8(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_decode_unit(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
int sse_decode_i_32(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
bool sse_decode_bool(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_String(String self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_list_prim_u_8_strict(
|
||||||
|
Uint8List self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_u_8(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_unit(void self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_i_32(int self, SseSerializer serializer);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_bool(bool self, SseSerializer serializer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section: wire_class
|
||||||
|
|
||||||
|
class RustLibWire implements BaseWire {
|
||||||
|
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JS('wasm_bindgen')
|
||||||
|
external RustLibWasmModule get wasmModule;
|
||||||
|
|
||||||
|
@JS()
|
||||||
|
@anonymous
|
||||||
|
extension type RustLibWasmModule._(JSObject _) implements JSObject {}
|
||||||
@ -1 +1,21 @@
|
|||||||
const abawoServiceBtUUID = '0993826f-0ee4-4b37-9614-d13ecba4ffc2';
|
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
|
||||||
|
|
||||||
|
const abawoServiceBtUUIDPrefix = '0993826f-0ee4-4b37-9614';
|
||||||
|
const abawoUniversalShiftersServiceBtUUID =
|
||||||
|
'0993826f-0ee4-4b37-9614-d13ecba4ffc2';
|
||||||
|
|
||||||
|
bool isAbawoDeviceGuid(Uuid guid) {
|
||||||
|
return guid
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll('-', '')
|
||||||
|
.startsWith(abawoServiceBtUUIDPrefix.toLowerCase().replaceAll('-', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAbawoUniversalShiftersDeviceGuid(Uuid guid) {
|
||||||
|
return guid == Uuid.parse(abawoUniversalShiftersServiceBtUUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isConnectableAbawoDeviceGuid(Uuid guid) {
|
||||||
|
return isAbawoUniversalShiftersDeviceGuid(guid);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
part 'sharedPrefs.g.dart';
|
// part 'sharedPrefs.g.dart';
|
||||||
|
|
||||||
final sharedPreferencesProvider =
|
final sharedPreferencesProvider =
|
||||||
Provider<SharedPreferences>((ref) => throw UnimplementedError());
|
Provider<SharedPreferences>((ref) => throw UnimplementedError());
|
||||||
|
|||||||
@ -1,197 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'sharedPrefs.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// RiverpodGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
String _$sharedPrefValueHash() => r'6c78fac8d11d0df162d4d53f465c1c8535fcd150';
|
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
|
||||||
class _SystemHash {
|
|
||||||
_SystemHash._();
|
|
||||||
|
|
||||||
static int combine(int hash, int value) {
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
hash = 0x1fffffff & (hash + value);
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
|
||||||
return hash ^ (hash >> 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int finish(int hash) {
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
|
||||||
// ignore: parameter_assignments
|
|
||||||
hash = hash ^ (hash >> 11);
|
|
||||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class _$SharedPrefValue extends BuildlessAutoDisposeNotifier<T> {
|
|
||||||
late final String key;
|
|
||||||
late final T defaultValue;
|
|
||||||
|
|
||||||
T build(
|
|
||||||
String key,
|
|
||||||
T defaultValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See also [SharedPrefValue].
|
|
||||||
@ProviderFor(SharedPrefValue)
|
|
||||||
const sharedPrefValueProvider = SharedPrefValueFamily();
|
|
||||||
|
|
||||||
/// See also [SharedPrefValue].
|
|
||||||
class SharedPrefValueFamily extends Family<T> {
|
|
||||||
/// See also [SharedPrefValue].
|
|
||||||
const SharedPrefValueFamily();
|
|
||||||
|
|
||||||
/// See also [SharedPrefValue].
|
|
||||||
SharedPrefValueProvider call(
|
|
||||||
String key,
|
|
||||||
T defaultValue,
|
|
||||||
) {
|
|
||||||
return SharedPrefValueProvider(
|
|
||||||
key,
|
|
||||||
defaultValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
SharedPrefValueProvider getProviderOverride(
|
|
||||||
covariant SharedPrefValueProvider provider,
|
|
||||||
) {
|
|
||||||
return call(
|
|
||||||
provider.key,
|
|
||||||
provider.defaultValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
|
||||||
|
|
||||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
|
||||||
_allTransitiveDependencies;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String? get name => r'sharedPrefValueProvider';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See also [SharedPrefValue].
|
|
||||||
class SharedPrefValueProvider
|
|
||||||
extends AutoDisposeNotifierProviderImpl<SharedPrefValue, T> {
|
|
||||||
/// See also [SharedPrefValue].
|
|
||||||
SharedPrefValueProvider(
|
|
||||||
String key,
|
|
||||||
T defaultValue,
|
|
||||||
) : this._internal(
|
|
||||||
() => SharedPrefValue()
|
|
||||||
..key = key
|
|
||||||
..defaultValue = defaultValue,
|
|
||||||
from: sharedPrefValueProvider,
|
|
||||||
name: r'sharedPrefValueProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$sharedPrefValueHash,
|
|
||||||
dependencies: SharedPrefValueFamily._dependencies,
|
|
||||||
allTransitiveDependencies:
|
|
||||||
SharedPrefValueFamily._allTransitiveDependencies,
|
|
||||||
key: key,
|
|
||||||
defaultValue: defaultValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
SharedPrefValueProvider._internal(
|
|
||||||
super._createNotifier, {
|
|
||||||
required super.name,
|
|
||||||
required super.dependencies,
|
|
||||||
required super.allTransitiveDependencies,
|
|
||||||
required super.debugGetCreateSourceHash,
|
|
||||||
required super.from,
|
|
||||||
required this.key,
|
|
||||||
required this.defaultValue,
|
|
||||||
}) : super.internal();
|
|
||||||
|
|
||||||
final String key;
|
|
||||||
final T defaultValue;
|
|
||||||
|
|
||||||
@override
|
|
||||||
T runNotifierBuild(
|
|
||||||
covariant SharedPrefValue notifier,
|
|
||||||
) {
|
|
||||||
return notifier.build(
|
|
||||||
key,
|
|
||||||
defaultValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Override overrideWith(SharedPrefValue Function() create) {
|
|
||||||
return ProviderOverride(
|
|
||||||
origin: this,
|
|
||||||
override: SharedPrefValueProvider._internal(
|
|
||||||
() => create()
|
|
||||||
..key = key
|
|
||||||
..defaultValue = defaultValue,
|
|
||||||
from: from,
|
|
||||||
name: null,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
debugGetCreateSourceHash: null,
|
|
||||||
key: key,
|
|
||||||
defaultValue: defaultValue,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
AutoDisposeNotifierProviderElement<SharedPrefValue, T> createElement() {
|
|
||||||
return _SharedPrefValueProviderElement(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return other is SharedPrefValueProvider &&
|
|
||||||
other.key == key &&
|
|
||||||
other.defaultValue == defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
|
||||||
hash = _SystemHash.combine(hash, key.hashCode);
|
|
||||||
hash = _SystemHash.combine(hash, defaultValue.hashCode);
|
|
||||||
|
|
||||||
return _SystemHash.finish(hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
|
||||||
// ignore: unused_element
|
|
||||||
mixin SharedPrefValueRef on AutoDisposeNotifierProviderRef<T> {
|
|
||||||
/// The parameter `key` of this provider.
|
|
||||||
String get key;
|
|
||||||
|
|
||||||
/// The parameter `defaultValue` of this provider.
|
|
||||||
T get defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SharedPrefValueProviderElement
|
|
||||||
extends AutoDisposeNotifierProviderElement<SharedPrefValue, T>
|
|
||||||
with SharedPrefValueRef {
|
|
||||||
_SharedPrefValueProviderElement(super.provider);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get key => (origin as SharedPrefValueProvider).key;
|
|
||||||
@override
|
|
||||||
T get defaultValue => (origin as SharedPrefValueProvider).defaultValue;
|
|
||||||
}
|
|
||||||
// ignore_for_file: type=lint
|
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
|
||||||
214
lib/widgets/bike_scan_dialog.dart
Normal file
214
lib/widgets/bike_scan_dialog.dart
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import 'package:abawo_bt_app/controller/bluetooth.dart';
|
||||||
|
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class BikeScanDialog extends ConsumerStatefulWidget {
|
||||||
|
const BikeScanDialog({
|
||||||
|
required this.excludedDeviceId,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String excludedDeviceId;
|
||||||
|
|
||||||
|
static Future<DiscoveredDevice?> show(
|
||||||
|
BuildContext context, {
|
||||||
|
required String excludedDeviceId,
|
||||||
|
}) {
|
||||||
|
return showDialog<DiscoveredDevice>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (_) => BikeScanDialog(excludedDeviceId: excludedDeviceId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<BikeScanDialog> createState() => _BikeScanDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BikeScanDialogState extends ConsumerState<BikeScanDialog> {
|
||||||
|
bool _showAll = false;
|
||||||
|
BluetoothController? _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_startScan();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startScan() async {
|
||||||
|
final controller = await ref.read(bluetoothProvider.future);
|
||||||
|
_controller = controller;
|
||||||
|
await controller.stopScan();
|
||||||
|
await controller.startScan();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller?.stopScan();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final btAsync = ref.watch(bluetoothProvider);
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 520,
|
||||||
|
height: 520,
|
||||||
|
child: btAsync.when(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (err, _) => Center(child: Text('Bluetooth error: $err')),
|
||||||
|
data: (controller) {
|
||||||
|
_controller ??= controller;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildHeader(context),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: StreamBuilder<List<DiscoveredDevice>>(
|
||||||
|
stream: controller.scanResultsStream,
|
||||||
|
initialData: controller.scanResults,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final devices =
|
||||||
|
_filteredDevices(snapshot.data ?? const []);
|
||||||
|
if (devices.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text('No matching devices nearby.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
itemCount: devices.length,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final device = devices[index];
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primaryContainer,
|
||||||
|
child: const Icon(Icons.pedal_bike),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
device.name.isEmpty
|
||||||
|
? 'Unknown Device'
|
||||||
|
: device.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
device.id,
|
||||||
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
trailing: _RssiBadge(rssi: device.rssi),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop(device);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 12, 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Select Bike',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text('Show All'),
|
||||||
|
Switch(
|
||||||
|
value: _showAll,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_showAll = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Rescan',
|
||||||
|
onPressed: _startScan,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Close',
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DiscoveredDevice> _filteredDevices(List<DiscoveredDevice> devices) {
|
||||||
|
final ftmsUuid = Uuid.parse(ftmsServiceUuid);
|
||||||
|
return devices.where((device) {
|
||||||
|
if (device.id == widget.excludedDeviceId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (_showAll) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return device.serviceUuids.contains(ftmsUuid);
|
||||||
|
}).toList(growable: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RssiBadge extends StatelessWidget {
|
||||||
|
const _RssiBadge({required this.rssi});
|
||||||
|
|
||||||
|
final int rssi;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = rssi > -65
|
||||||
|
? Colors.green
|
||||||
|
: rssi > -80
|
||||||
|
? Colors.orange
|
||||||
|
: Colors.red;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.15),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$rssi dBm',
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,18 +1,23 @@
|
|||||||
|
import 'package:abawo_bt_app/model/bluetooth_device_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'dart:ui'; // Required for ImageFilter
|
import 'dart:ui'; // Required for ImageFilter
|
||||||
|
|
||||||
class DeviceListItem extends StatelessWidget {
|
class DeviceListItem extends StatelessWidget {
|
||||||
final String deviceName;
|
final String deviceName;
|
||||||
final String deviceId; // Added for potential future use or subtitle
|
final String deviceId; // Added for potential future use or subtitle
|
||||||
final bool isUnknownDevice;
|
final DeviceType type;
|
||||||
// final String? imageUrl; // Optional image URL - commented out for now
|
// final String? imageUrl; // Optional image URL - commented out for now
|
||||||
|
final bool isConnecting; // Add this line
|
||||||
|
final Widget? trailing;
|
||||||
|
|
||||||
const DeviceListItem({
|
const DeviceListItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.deviceName,
|
required this.deviceName,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
this.isUnknownDevice = false,
|
required this.type,
|
||||||
// this.imageUrl,
|
// this.imageUrl,
|
||||||
|
this.isConnecting = false, // Add this line
|
||||||
|
this.trailing,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -22,11 +27,11 @@ class DeviceListItem extends StatelessWidget {
|
|||||||
|
|
||||||
// Glassy effect colors - adjust transparency and base color as needed
|
// Glassy effect colors - adjust transparency and base color as needed
|
||||||
final glassColor = isDarkMode
|
final glassColor = isDarkMode
|
||||||
? Colors.white.withOpacity(0.1)
|
? Colors.white.withValues(alpha: 0.1)
|
||||||
: Colors.black.withOpacity(0.05);
|
: Colors.black.withValues(alpha: 0.05);
|
||||||
final shadowColor = isDarkMode
|
final shadowColor = isDarkMode
|
||||||
? Colors.black.withOpacity(0.4)
|
? Colors.black.withValues(alpha: 0.4)
|
||||||
: Colors.grey.withOpacity(0.5);
|
: Colors.grey.withValues(alpha: 0.5);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
@ -57,21 +62,33 @@ class DeviceListItem extends StatelessWidget {
|
|||||||
glassColor, // Semi-transparent color for glass effect
|
glassColor, // Semi-transparent color for glass effect
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Colors.white.withOpacity(0.2), // Subtle border
|
color:
|
||||||
|
Colors.white.withValues(alpha: 0.2), // Subtle border
|
||||||
width: 0.5,
|
width: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: type == DeviceType.universalShifters
|
||||||
// Placeholder '?' - replace with Image widget when imageUrl is available
|
// For Universal Shifters: Image fills the container, constrained by rounded borders
|
||||||
child: Text(
|
? ClipRRect(
|
||||||
'?',
|
borderRadius: BorderRadius.circular(12),
|
||||||
style: TextStyle(
|
child: Image.asset(
|
||||||
fontSize: 30,
|
'assets/images/shifter-wireframe.png',
|
||||||
fontWeight: FontWeight.bold,
|
fit: BoxFit.cover, // Cover the entire container
|
||||||
color: Colors.white70, // Adjust color as needed
|
width: 60,
|
||||||
),
|
height: 60,
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
|
// For other devices: Question mark with padding
|
||||||
|
: const Center(
|
||||||
|
child: Text(
|
||||||
|
'?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 30,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -84,14 +101,16 @@ class DeviceListItem extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
isUnknownDevice ? 'Unknown Device' : deviceName,
|
deviceName.isEmpty ? 'Unknown Device' : deviceName,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight:
|
fontWeight: deviceName.isEmpty
|
||||||
isUnknownDevice ? FontWeight.normal : FontWeight.w500,
|
? FontWeight.normal
|
||||||
fontStyle:
|
: FontWeight.w500,
|
||||||
isUnknownDevice ? FontStyle.italic : FontStyle.normal,
|
fontStyle: deviceName.isEmpty
|
||||||
color: isUnknownDevice
|
? FontStyle.italic
|
||||||
|
: FontStyle.normal,
|
||||||
|
color: deviceName.isEmpty
|
||||||
? theme.hintColor
|
? theme.hintColor
|
||||||
: theme.textTheme.bodyLarge?.color,
|
: theme.textTheme.bodyLarge?.color,
|
||||||
),
|
),
|
||||||
@ -108,6 +127,19 @@ class DeviceListItem extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Add spinner if connecting (Add this block)
|
||||||
|
if (isConnecting)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 12.0), // Add some spacing
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20, // Define spinner size
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.0), // Use a small spinner
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (trailing != null)
|
||||||
|
trailing!,
|
||||||
// Optional: Add an icon or button on the far right if needed later
|
// Optional: Add an icon or button on the far right if needed later
|
||||||
// Icon(Icons.chevron_right, color: theme.hintColor),
|
// Icon(Icons.chevron_right, color: theme.hintColor),
|
||||||
],
|
],
|
||||||
|
|||||||
892
lib/widgets/gear_ratio_editor_card.dart
Normal file
892
lib/widgets/gear_ratio_editor_card.dart
Normal file
@ -0,0 +1,892 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class GearRatioPreset {
|
||||||
|
const GearRatioPreset({
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
required this.ratios,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
final List<double> ratios;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GearRatioEditorCard extends StatefulWidget {
|
||||||
|
const GearRatioEditorCard({
|
||||||
|
required this.ratios,
|
||||||
|
required this.isLoading,
|
||||||
|
required this.onSave,
|
||||||
|
required this.presets,
|
||||||
|
this.errorText,
|
||||||
|
this.onRetry,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<double> ratios;
|
||||||
|
final bool isLoading;
|
||||||
|
final Future<String?> Function(List<double> ratios) onSave;
|
||||||
|
final List<GearRatioPreset> presets;
|
||||||
|
final String? errorText;
|
||||||
|
final VoidCallback? onRetry;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GearRatioEditorCard> createState() => _GearRatioEditorCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GearRatioEditorCardState extends State<GearRatioEditorCard> {
|
||||||
|
static const double _sliderMin = 0.10;
|
||||||
|
static const double _sliderMax = 3.90;
|
||||||
|
static const double _sliderPivotT = 0.50;
|
||||||
|
static const double _sliderPivotV = 1.00;
|
||||||
|
static const Duration _animDuration = Duration(milliseconds: 280);
|
||||||
|
static const Curve _animCurve = Cubic(0.2, 0.8, 0.2, 1.0);
|
||||||
|
|
||||||
|
bool _isExpanded = false;
|
||||||
|
bool _isEditing = false;
|
||||||
|
bool _sortAscending = true;
|
||||||
|
bool _isSaving = false;
|
||||||
|
double _stretchFactor = 1.0;
|
||||||
|
List<double>? _stretchBase;
|
||||||
|
int _gearLayoutVersion = 0;
|
||||||
|
|
||||||
|
List<double> _committed = const [];
|
||||||
|
List<double> _draft = const [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_committed = List<double>.from(widget.ratios);
|
||||||
|
_draft = List<double>.from(widget.ratios);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant GearRatioEditorCard oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (!_isEditing && !_listEquals(oldWidget.ratios, widget.ratios)) {
|
||||||
|
_committed = List<double>.from(widget.ratios);
|
||||||
|
_draft = List<double>.from(widget.ratios);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
color:
|
||||||
|
theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.55),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: AnimatedSize(
|
||||||
|
duration: _animDuration,
|
||||||
|
curve: _animCurve,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 12, 10, 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Gear Ratios',
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 17, fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_isEditing) ...[
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isSaving ? null : _onCancel,
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: _isSaving ? null : _onSave,
|
||||||
|
child: _isSaving
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text('Save'),
|
||||||
|
),
|
||||||
|
] else
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Edit ratios',
|
||||||
|
onPressed: (widget.isLoading || widget.errorText != null)
|
||||||
|
? null
|
||||||
|
: _enterEditMode,
|
||||||
|
icon: const Icon(Icons.edit_outlined),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.isLoading)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 20, 16, 24),
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
)
|
||||||
|
else if (widget.errorText != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 6, 16, 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.errorText!,
|
||||||
|
style: TextStyle(color: theme.colorScheme.error),
|
||||||
|
),
|
||||||
|
if (widget.onRetry != null)
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: widget.onRetry,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('Retry'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (_committed.isEmpty && !_isEditing)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 8, 16, 18),
|
||||||
|
child: Text('No gear ratios found on device.'),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
if ((_isEditing ? _draft : _committed).isNotEmpty)
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: _isEditing
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
setState(() {
|
||||||
|
_isExpanded = !_isExpanded;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 2, 14, 8),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: _animDuration,
|
||||||
|
curve: _animCurve,
|
||||||
|
height: _isExpanded ? 210 : 130,
|
||||||
|
child: _GearRatioGraph(
|
||||||
|
ratios: _isEditing ? _draft : _committed,
|
||||||
|
compact: !_isExpanded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!_isExpanded && !_isEditing && _committed.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 0, 14, 10),
|
||||||
|
child: _compactRatioStrip(context, _committed),
|
||||||
|
),
|
||||||
|
AnimatedSize(
|
||||||
|
duration: _animDuration,
|
||||||
|
curve: _animCurve,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: _isExpanded && !_isEditing
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 0, 14, 12),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < _committed.length; i++)
|
||||||
|
_ratioChip(context, i + 1, _committed[i]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
if (_isEditing) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Text('Sort ascending'),
|
||||||
|
Switch(
|
||||||
|
value: _sortAscending,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_sortAscending = value;
|
||||||
|
if (_sortAscending) {
|
||||||
|
_sortDraft(animate: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _openPresetPicker,
|
||||||
|
icon: const Icon(Icons.tune),
|
||||||
|
label: const Text('Load preset'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_draft.isEmpty)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(14, 0, 14, 10),
|
||||||
|
child:
|
||||||
|
Text('No ratios yet. Load a preset to start editing.'),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Stretch all: ${_stretchFactor.toStringAsFixed(2)}x',
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
Slider(
|
||||||
|
min: 0.0,
|
||||||
|
max: 1.0,
|
||||||
|
value: _stretchToSlider(_stretchFactor),
|
||||||
|
onChangeStart: (_) {
|
||||||
|
_stretchBase = List<double>.from(_draft);
|
||||||
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
final factor = _sliderToStretch(value);
|
||||||
|
final base = _stretchBase ?? _draft;
|
||||||
|
setState(() {
|
||||||
|
_stretchFactor = factor;
|
||||||
|
_draft = base
|
||||||
|
.map((ratio) => _quantizeRatio(ratio * factor))
|
||||||
|
.toList(growable: false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChangeEnd: (_) {
|
||||||
|
setState(() {
|
||||||
|
if (_sortAscending) {
|
||||||
|
_sortDraft(animate: true);
|
||||||
|
}
|
||||||
|
_stretchBase = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(10, 0, 10, 12),
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: _animDuration,
|
||||||
|
switchInCurve: _animCurve,
|
||||||
|
switchOutCurve: Curves.easeInCubic,
|
||||||
|
transitionBuilder: _snappyTransition,
|
||||||
|
child: Wrap(
|
||||||
|
key: ValueKey('editors-$_gearLayoutVersion'),
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < _draft.length; i++)
|
||||||
|
KeyedSubtree(
|
||||||
|
key: ValueKey('editor-${i + 1}'),
|
||||||
|
child: _buildGearEditor(context, i),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _snappyTransition(Widget child, Animation<double> animation) {
|
||||||
|
final curved = CurvedAnimation(parent: animation, curve: _animCurve);
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: curved,
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: curved,
|
||||||
|
axisAlignment: -1,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGearEditor(BuildContext context, int index) {
|
||||||
|
final ratio = _draft[index];
|
||||||
|
final sliderValue = _valueToSlider(ratio);
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 230, maxWidth: 280),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(10, 8, 10, 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.7),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.outlineVariant
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Gear ${index + 1}',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
onTap: () => _editRatioText(index),
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
child: Text(
|
||||||
|
ratio.toStringAsFixed(2),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Slider(
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
value: sliderValue,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_draft[index] = _quantizeRatio(_sliderToValue(value));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChangeEnd: (_) {
|
||||||
|
setState(() {
|
||||||
|
if (_sortAscending) {
|
||||||
|
_sortDraft(animate: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _editRatioText(int index) async {
|
||||||
|
final controller = TextEditingController(
|
||||||
|
text: _draft[index].toStringAsFixed(2),
|
||||||
|
);
|
||||||
|
|
||||||
|
final value = await showDialog<double>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Set gear ${index + 1} ratio'),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(hintText: 'e.g. 1.25'),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
final parsed = double.tryParse(controller.text.trim());
|
||||||
|
Navigator.of(context).pop(parsed);
|
||||||
|
},
|
||||||
|
child: const Text('Set'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted || value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_draft[index] = _quantizeRatio(value);
|
||||||
|
if (_sortAscending) {
|
||||||
|
_sortDraft(animate: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openPresetPicker() async {
|
||||||
|
final selected = await showModalBottomSheet<GearRatioPreset>(
|
||||||
|
context: context,
|
||||||
|
showDragHandle: true,
|
||||||
|
builder: (context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 6, 12, 12),
|
||||||
|
child: ListView.separated(
|
||||||
|
itemCount: widget.presets.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final preset = widget.presets[index];
|
||||||
|
return Material(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHighest
|
||||||
|
.withValues(alpha: 0.45),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
onTap: () => Navigator.of(context).pop(preset),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
preset.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
preset.description,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
SizedBox(
|
||||||
|
height: 90,
|
||||||
|
child: _GearRatioGraph(
|
||||||
|
ratios: preset.ratios, compact: true),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_compactRatioStrip(
|
||||||
|
context,
|
||||||
|
preset.ratios,
|
||||||
|
showGearLabel: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted || selected == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_draft = selected.ratios.map(_quantizeRatio).toList(growable: false);
|
||||||
|
if (_sortAscending) {
|
||||||
|
_sortDraft(animate: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sortDraft({bool animate = false}) {
|
||||||
|
final sorted = _sorted(_draft);
|
||||||
|
if (animate && !_listEquals(_draft, sorted)) {
|
||||||
|
_gearLayoutVersion++;
|
||||||
|
}
|
||||||
|
_draft = sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _enterEditMode() {
|
||||||
|
setState(() {
|
||||||
|
_isEditing = true;
|
||||||
|
_isExpanded = true;
|
||||||
|
_stretchFactor = 1.0;
|
||||||
|
_stretchBase = null;
|
||||||
|
_draft = List<double>.from(_committed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCancel() {
|
||||||
|
setState(() {
|
||||||
|
_isEditing = false;
|
||||||
|
_draft = List<double>.from(_committed);
|
||||||
|
_stretchFactor = 1.0;
|
||||||
|
_stretchBase = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSave() async {
|
||||||
|
setState(() {
|
||||||
|
_isSaving = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final message = await widget.onSave(List<double>.from(_draft));
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isSaving = false;
|
||||||
|
if (message == null) {
|
||||||
|
_committed = List<double>.from(_draft);
|
||||||
|
_isEditing = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(message)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _ratioChip(BuildContext context, int gear, double ratio) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
color: theme.colorScheme.surface.withValues(alpha: 0.7),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text('G$gear ${ratio.toStringAsFixed(2)}'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _compactRatioStrip(
|
||||||
|
BuildContext context,
|
||||||
|
List<double> ratios, {
|
||||||
|
bool showGearLabel = true,
|
||||||
|
}) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return SizedBox(
|
||||||
|
height: 26,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < ratios.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(right: i == ratios.length - 1 ? 0 : 6),
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
color: theme.colorScheme.surface.withValues(alpha: 0.7),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.outlineVariant
|
||||||
|
.withValues(alpha: 0.55),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
showGearLabel
|
||||||
|
? 'G${i + 1} ${ratios[i].toStringAsFixed(2)}'
|
||||||
|
: ratios[i].toStringAsFixed(2),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _quantizeRatio(double raw) {
|
||||||
|
final clamped = raw.clamp(_sliderMin, _sliderMax);
|
||||||
|
return ((clamped * 64).round() / 64.0).clamp(_sliderMin, _sliderMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<double> _sorted(List<double> values) {
|
||||||
|
final out = List<double>.from(values)..sort();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _listEquals(List<double> a, List<double> b) {
|
||||||
|
if (a.length != b.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < a.length; i++) {
|
||||||
|
if (a[i] != b[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _sliderToValue(double t) {
|
||||||
|
final normalized = t.clamp(0.0, 1.0);
|
||||||
|
if (normalized <= _sliderPivotT) {
|
||||||
|
final u = normalized / _sliderPivotT;
|
||||||
|
return _sliderMin * math.pow(_sliderPivotV / _sliderMin, u);
|
||||||
|
}
|
||||||
|
final u = (normalized - _sliderPivotT) / (1 - _sliderPivotT);
|
||||||
|
return _sliderPivotV * math.pow(_sliderMax / _sliderPivotV, u);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _valueToSlider(double value) {
|
||||||
|
final clamped = value.clamp(_sliderMin, _sliderMax);
|
||||||
|
if (clamped <= _sliderPivotV) {
|
||||||
|
final u =
|
||||||
|
math.log(clamped / _sliderMin) / math.log(_sliderPivotV / _sliderMin);
|
||||||
|
return (u * _sliderPivotT).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
final u = math.log(clamped / _sliderPivotV) /
|
||||||
|
math.log(_sliderMax / _sliderPivotV);
|
||||||
|
return (_sliderPivotT + u * (1 - _sliderPivotT)).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _sliderToStretch(double t) {
|
||||||
|
return (0.6 + (1.6 - 0.6) * t).clamp(0.6, 1.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _stretchToSlider(double factor) {
|
||||||
|
return ((factor - 0.6) / (1.6 - 0.6)).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GearRatioGraph extends StatelessWidget {
|
||||||
|
const _GearRatioGraph({required this.ratios, required this.compact});
|
||||||
|
|
||||||
|
final List<double> ratios;
|
||||||
|
final bool compact;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.65),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(10, 10, 10, 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
if (!compact)
|
||||||
|
Text(
|
||||||
|
'Input RPM -> Output RPM',
|
||||||
|
style: textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: _GearRatioGraphPainter(
|
||||||
|
ratios: ratios,
|
||||||
|
axisColor: Theme.of(context).colorScheme.outline,
|
||||||
|
lineColor: Theme.of(context).colorScheme.primary,
|
||||||
|
textColor: Theme.of(context).colorScheme.onSurface,
|
||||||
|
compact: compact,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GearRatioGraphPainter extends CustomPainter {
|
||||||
|
const _GearRatioGraphPainter({
|
||||||
|
required this.ratios,
|
||||||
|
required this.axisColor,
|
||||||
|
required this.lineColor,
|
||||||
|
required this.textColor,
|
||||||
|
required this.compact,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<double> ratios;
|
||||||
|
final Color axisColor;
|
||||||
|
final Color lineColor;
|
||||||
|
final Color textColor;
|
||||||
|
final bool compact;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
if (ratios.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = 28.0;
|
||||||
|
const right = 10.0;
|
||||||
|
const top = 8.0;
|
||||||
|
const bottom = 24.0;
|
||||||
|
final chart = Rect.fromLTWH(
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
size.width - left - right,
|
||||||
|
size.height - top - bottom,
|
||||||
|
);
|
||||||
|
|
||||||
|
final axisPaint = Paint()
|
||||||
|
..color = axisColor.withValues(alpha: 0.55)
|
||||||
|
..strokeWidth = 1.2;
|
||||||
|
canvas.drawRect(chart, axisPaint..style = PaintingStyle.stroke);
|
||||||
|
|
||||||
|
final maxRatio = ratios.reduce(math.max);
|
||||||
|
final xMax = 120.0;
|
||||||
|
final computedYMax = (xMax * math.max(maxRatio, 1.0)).toDouble();
|
||||||
|
final yMax = math.min(400.0, computedYMax);
|
||||||
|
|
||||||
|
for (var i = 1; i <= 3; i++) {
|
||||||
|
final y = chart.bottom - (chart.height * (i / 4));
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(chart.left, y),
|
||||||
|
Offset(chart.right, y),
|
||||||
|
axisPaint..color = axisColor.withValues(alpha: 0.2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 1; i <= 3; i++) {
|
||||||
|
final x = chart.left + (chart.width * (i / 4));
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(x, chart.top),
|
||||||
|
Offset(x, chart.bottom),
|
||||||
|
axisPaint..color = axisColor.withValues(alpha: 0.15),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < ratios.length; i++) {
|
||||||
|
final ratio = ratios[i];
|
||||||
|
final p = i / math.max(1, ratios.length - 1);
|
||||||
|
final color = Color.lerp(
|
||||||
|
lineColor.withValues(alpha: 0.40),
|
||||||
|
lineColor,
|
||||||
|
p,
|
||||||
|
)!;
|
||||||
|
|
||||||
|
final endYValue = xMax * ratio;
|
||||||
|
final isClipped = endYValue > yMax;
|
||||||
|
final endX = isClipped
|
||||||
|
? chart.left + chart.width * (yMax / endYValue)
|
||||||
|
: chart.right;
|
||||||
|
final endY = isClipped
|
||||||
|
? chart.top
|
||||||
|
: chart.bottom - (endYValue / yMax) * chart.height;
|
||||||
|
|
||||||
|
final linePaint = Paint()
|
||||||
|
..color = color
|
||||||
|
..strokeWidth = 2;
|
||||||
|
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(chart.left, chart.bottom),
|
||||||
|
Offset(endX, endY),
|
||||||
|
linePaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!compact && i % 2 == 0) {
|
||||||
|
final tp = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: 'G${i + 1}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10, color: textColor.withValues(alpha: 0.75)),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
tp.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(
|
||||||
|
(endX - tp.width - 2).clamp(chart.left, chart.right - tp.width),
|
||||||
|
(endY - 10).clamp(chart.top, chart.bottom - tp.height),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final xLabel = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: 'In RPM',
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 10, color: textColor.withValues(alpha: 0.75)),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
xLabel.paint(canvas, Offset(chart.right - xLabel.width, chart.bottom + 17));
|
||||||
|
|
||||||
|
final xMinValue = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: '0',
|
||||||
|
style: TextStyle(fontSize: 9, color: textColor.withValues(alpha: 0.65)),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
xMinValue.paint(canvas, Offset(chart.left, chart.bottom + 5));
|
||||||
|
|
||||||
|
final xMaxValue = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: xMax.toStringAsFixed(0),
|
||||||
|
style: TextStyle(fontSize: 9, color: textColor.withValues(alpha: 0.65)),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
xMaxValue.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(chart.right - xMaxValue.width, chart.bottom + 5),
|
||||||
|
);
|
||||||
|
|
||||||
|
final yLabel = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: 'Out RPM',
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 10, color: textColor.withValues(alpha: 0.75)),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
yLabel.paint(canvas, Offset(chart.left - 28, chart.top - 14));
|
||||||
|
|
||||||
|
final yMinValue = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: '0',
|
||||||
|
style: TextStyle(fontSize: 9, color: textColor.withValues(alpha: 0.65)),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
yMinValue.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(chart.left - yMinValue.width - 4, chart.bottom - yMinValue.height),
|
||||||
|
);
|
||||||
|
|
||||||
|
final yMaxValue = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: yMax.toStringAsFixed(0),
|
||||||
|
style: TextStyle(fontSize: 9, color: textColor.withValues(alpha: 0.65)),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
yMaxValue.paint(
|
||||||
|
canvas,
|
||||||
|
Offset(chart.left - yMaxValue.width - 4, chart.top),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _GearRatioGraphPainter oldDelegate) {
|
||||||
|
if (oldDelegate.compact != compact ||
|
||||||
|
oldDelegate.ratios.length != ratios.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < ratios.length; i++) {
|
||||||
|
if (ratios[i] != oldDelegate.ratios[i]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
207
lib/widgets/horizontal_scanning_animation.dart
Normal file
207
lib/widgets/horizontal_scanning_animation.dart
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class HorizontalScanningAnimation extends StatefulWidget {
|
||||||
|
final bool isScanning; // Add this to control the animation
|
||||||
|
final Color waveColor;
|
||||||
|
final double height;
|
||||||
|
const HorizontalScanningAnimation({
|
||||||
|
super.key,
|
||||||
|
required this.isScanning, // Make it required
|
||||||
|
this.waveColor = Colors.lightBlueAccent,
|
||||||
|
this.height = 50.0,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
_HorizontalScanningAnimationState createState() =>
|
||||||
|
_HorizontalScanningAnimationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HorizontalScanningAnimationState
|
||||||
|
extends State<HorizontalScanningAnimation>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
// Start repeating only if initially scanning
|
||||||
|
if (widget.isScanning) {
|
||||||
|
_controller.repeat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant HorizontalScanningAnimation oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.isScanning != oldWidget.isScanning) {
|
||||||
|
if (widget.isScanning) {
|
||||||
|
// Start or resume repeating
|
||||||
|
if (!_controller.isAnimating) {
|
||||||
|
// If stopped previously, reset before repeating for a clean start
|
||||||
|
// Though, repeat() should handle restarting if stopped. Testing needed.
|
||||||
|
// _controller.reset(); // Optional: uncomment if repeat doesn't restart smoothly
|
||||||
|
_controller.repeat();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Stop repeating, but let the current animation cycle finish visually
|
||||||
|
if (_controller.isAnimating) {
|
||||||
|
_controller.stop(
|
||||||
|
canceled:
|
||||||
|
false); // Use canceled: false to let it finish the current tick
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Only build the painter if the controller is active or was recently stopped
|
||||||
|
// This prevents drawing when completely idle. Check if value is changing or non-zero.
|
||||||
|
// Or simply rely on the AnimatedBuilder which won't rebuild if controller is idle at 0.0
|
||||||
|
return SizedBox(
|
||||||
|
height: widget.height,
|
||||||
|
width: double.infinity,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
return CustomPaint(
|
||||||
|
painter: _HorizontalWavePainter(
|
||||||
|
progress: _controller.value,
|
||||||
|
waveColor: widget.waveColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HorizontalWavePainter extends CustomPainter {
|
||||||
|
final double progress; // Animation value from 0.0 to 1.0
|
||||||
|
final Color waveColor;
|
||||||
|
final int waveCount = 2; // Number of waves visible at once
|
||||||
|
final double waveAmplitude = 10.0; // Max height deviation of the wave
|
||||||
|
|
||||||
|
_HorizontalWavePainter({required this.progress, required this.waveColor});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = waveColor.withValues(alpha: 0.6) // Semi-transparent waves
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
final centerY = size.height / 2;
|
||||||
|
final width = size.width;
|
||||||
|
|
||||||
|
// Draw multiple waves propagating outwards
|
||||||
|
for (int i = 0; i < waveCount; i++) {
|
||||||
|
// Calculate the phase offset for each wave based on progress and index
|
||||||
|
// This creates the effect of waves moving outwards
|
||||||
|
double waveProgress = (progress + i / waveCount) % 1.0;
|
||||||
|
|
||||||
|
// Use an easing curve for smoother expansion
|
||||||
|
double easedProgress = Curves.easeInOutSine.transform(waveProgress);
|
||||||
|
|
||||||
|
// Calculate the current horizontal position (expanding from center)
|
||||||
|
// The wave starts narrow and expands outwards
|
||||||
|
double currentWidth =
|
||||||
|
width * easedProgress * 0.8; // Max 80% width expansion
|
||||||
|
double startX = (width / 2) - (currentWidth / 2);
|
||||||
|
double endX = (width / 2) + (currentWidth / 2);
|
||||||
|
|
||||||
|
// Calculate opacity based on progress (fade in and out)
|
||||||
|
double opacity;
|
||||||
|
if (waveProgress < 0.1) {
|
||||||
|
opacity = waveProgress / 0.1; // Fade in
|
||||||
|
} else if (waveProgress > 0.8) {
|
||||||
|
opacity = (1.0 - waveProgress) / 0.2; // Fade out
|
||||||
|
} else {
|
||||||
|
opacity = 1.0;
|
||||||
|
}
|
||||||
|
opacity = max(0.0, opacity); // Clamp opacity
|
||||||
|
|
||||||
|
if (opacity <= 0.0 || currentWidth < 5)
|
||||||
|
continue; // Skip drawing if invisible or too small
|
||||||
|
|
||||||
|
// Create the wave path
|
||||||
|
final path = Path();
|
||||||
|
path.moveTo(startX, centerY);
|
||||||
|
|
||||||
|
// Calculate points for the sine wave shape within the current width
|
||||||
|
const int segments = 50; // Number of segments for the curve
|
||||||
|
for (int j = 0; j <= segments; j++) {
|
||||||
|
double segmentProgress = j / segments;
|
||||||
|
double x = startX + currentWidth * segmentProgress;
|
||||||
|
// Apply sine wave based on segment progress and overall animation progress
|
||||||
|
// Multiply by (1 - easedProgress) to reduce amplitude as it expands
|
||||||
|
double yOffset = waveAmplitude *
|
||||||
|
sin(segmentProgress * 2 * pi + progress * 4 * pi) *
|
||||||
|
(1 - easedProgress * 0.8) * // Reduce amplitude as it expands
|
||||||
|
opacity; // Apply opacity effect to amplitude too
|
||||||
|
path.lineTo(x, centerY + yOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a filled shape (like a lens flare or horizontal bar)
|
||||||
|
// Adjust thickness based on easedProgress (thicker in the middle, thinner at ends)
|
||||||
|
double thickness =
|
||||||
|
waveAmplitude * (1 - easedProgress * 0.9) * opacity * 0.5;
|
||||||
|
paint.color = waveColor.withValues(
|
||||||
|
alpha: opacity * 0.5); // Update paint color with opacity
|
||||||
|
|
||||||
|
// Simplified: Draw a rectangle that pulses
|
||||||
|
// More complex shapes could be drawn here using path.arcTo or path.quadraticBezierTo
|
||||||
|
// For simplicity, let's use a slightly blurred rectangle effect
|
||||||
|
|
||||||
|
final rectPath = Path()
|
||||||
|
..addRRect(RRect.fromRectAndRadius(
|
||||||
|
Rect.fromCenter(
|
||||||
|
center: Offset(width / 2, centerY),
|
||||||
|
width: currentWidth,
|
||||||
|
height: thickness * 2),
|
||||||
|
Radius.circular(thickness)));
|
||||||
|
|
||||||
|
// Apply a blur effect
|
||||||
|
final blurPaint = Paint()
|
||||||
|
..color = waveColor.withValues(alpha: opacity * 0.4)
|
||||||
|
..maskFilter = MaskFilter.blur(
|
||||||
|
BlurStyle.normal, thickness * 1.5); // Blur based on thickness
|
||||||
|
|
||||||
|
// Draw the blurred shape
|
||||||
|
canvas.drawPath(rectPath, blurPaint);
|
||||||
|
|
||||||
|
// Draw a slightly smaller, less opaque shape on top for highlight
|
||||||
|
final highlightPaint = Paint()
|
||||||
|
..color = waveColor.withValues(alpha: opacity * 0.7)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
final highlightRectPath = Path()
|
||||||
|
..addRRect(RRect.fromRectAndRadius(
|
||||||
|
Rect.fromCenter(
|
||||||
|
center: Offset(width / 2, centerY),
|
||||||
|
width: currentWidth * 0.95,
|
||||||
|
height: thickness * 1.5),
|
||||||
|
Radius.circular(thickness * 0.8)));
|
||||||
|
canvas.drawPath(highlightRectPath, highlightPaint);
|
||||||
|
|
||||||
|
// Old Path drawing - keep if rectangle isn't desired
|
||||||
|
// paint.color = waveColor.withValues(alpha: opacity * 0.5); // Apply opacity
|
||||||
|
// canvas.drawPath(path, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant _HorizontalWavePainter oldDelegate) {
|
||||||
|
// Repaint whenever the animation progress or color changes
|
||||||
|
return oldDelegate.progress != progress ||
|
||||||
|
oldDelegate.waveColor != waveColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,14 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <nb_utils/nb_utils_plugin.h>
|
||||||
|
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) nb_utils_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "NbUtilsPlugin");
|
||||||
|
nb_utils_plugin_register_with_registrar(nb_utils_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
|
||||||
|
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,12 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
nb_utils
|
||||||
|
sqlite3_flutter_libs
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
rust_lib_abawo_bt_app
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
@ -5,10 +5,20 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import connectivity_plus
|
||||||
import flutter_blue_plus_darwin
|
import flutter_blue_plus_darwin
|
||||||
|
import nb_utils
|
||||||
|
import path_provider_foundation
|
||||||
|
import reactive_ble_mobile
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import sqlite3_flutter_libs
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
|
||||||
|
NbUtilsPlugin.register(with: registry.registrar(forPlugin: "NbUtilsPlugin"))
|
||||||
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
ReactiveBlePlugin.register(with: registry.registrar(forPlugin: "ReactiveBlePlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
352
pubspec.lock
352
pubspec.lock
@ -73,6 +73,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.4.2"
|
||||||
|
build_cli_annotations:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: build_cli_annotations
|
||||||
|
sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -129,14 +137,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.9.5"
|
version: "8.9.5"
|
||||||
|
cbor:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cbor
|
||||||
|
sha256: "2c5c37650f0a2d25149f03e748ab7b2857787bde338f95fe947738b80d713da2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.1"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.4.1"
|
||||||
|
charcode:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: charcode
|
||||||
|
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -165,10 +189,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: clock
|
name: clock
|
||||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.2"
|
||||||
code_builder:
|
code_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -181,10 +205,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.0"
|
version: "1.19.1"
|
||||||
|
connectivity_plus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: connectivity_plus
|
||||||
|
sha256: "33bae12a398f841c6cda09d1064212957265869104c478e5ad51e2fb26c3973c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
|
connectivity_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: connectivity_plus_platform_interface
|
||||||
|
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
convert:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -257,14 +297,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
version: "0.7.11"
|
||||||
|
drift:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: drift
|
||||||
|
sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.26.0"
|
||||||
|
drift_dev:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: drift_dev
|
||||||
|
sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.26.0"
|
||||||
|
drift_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: drift_flutter
|
||||||
|
sha256: "0cadbf3b8733409a6cf61d18ba2e94e149df81df7de26f48ae0695b48fd71922"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.4"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.3"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -298,50 +362,63 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_blue_plus
|
name: flutter_blue_plus
|
||||||
sha256: "2d926dbef0fd6c58d4be8fca9eaaf1ba747c0ccb8373ddd5386665317e26eb61"
|
sha256: "399b3dbc15562ef59749f04e43a99ccbb91540022380d5f269aff3c2787534e4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.35.3"
|
version: "2.1.0"
|
||||||
flutter_blue_plus_android:
|
flutter_blue_plus_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_blue_plus_android
|
name: flutter_blue_plus_android
|
||||||
sha256: c1d83f84b514e46345a8a58599c428f20b11e78379521e0d3b0611c7b7cbf2c1
|
sha256: "5010b0960cce533a8fa71401573f044362c3e2e111dc6eb4898c92e85f85f50c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "8.1.0"
|
||||||
flutter_blue_plus_darwin:
|
flutter_blue_plus_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_blue_plus_darwin
|
name: flutter_blue_plus_darwin
|
||||||
sha256: "8d0a0f11f83b13dda173396b7e4028b4e8656bc8dbbc82c26a7e49aafc62644b"
|
sha256: d160a8128e3a016fa58dd65ab6dac05cbc73e0fa799a1f24211d041641ed63ba
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "8.1.0"
|
||||||
flutter_blue_plus_linux:
|
flutter_blue_plus_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_blue_plus_linux
|
name: flutter_blue_plus_linux
|
||||||
sha256: "1d367ed378b2bd6c3b9685fda7044e1d2f169884802b7dec7badb31a99a72660"
|
sha256: f5b02244d89465ba82c8c512686c66362fbb01f52fa03d645ed353ebf3883242
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "8.1.0"
|
||||||
flutter_blue_plus_platform_interface:
|
flutter_blue_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_blue_plus_platform_interface
|
name: flutter_blue_plus_platform_interface
|
||||||
sha256: "114f8e85a03a28a48d707a4df6cc9218e1f2005cf260c5e815e5585a00da5778"
|
sha256: "6e0fc04b77491dbfdbcd46c1a021b12f2f5fc5d6e01777f93a38a8431989b7f0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "8.1.0"
|
||||||
flutter_blue_plus_web:
|
flutter_blue_plus_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_blue_plus_web
|
name: flutter_blue_plus_web
|
||||||
sha256: db70cdc41bc743763dc0d47e8c7c10f3923cbbe71b33d9dc21deea482affeb4d
|
sha256: "376aad9595ee389c7cd56e0c373e78abcaa790c821ece9cb81f0969ec94c5bca"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "8.1.0"
|
||||||
|
flutter_blue_plus_winrt:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_blue_plus_winrt
|
||||||
|
sha256: "34be2d8e23d5881b46accebb0e71025f7d52869d72ea98b5082c20764e06aa80"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.16"
|
||||||
|
flutter_driver:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -350,6 +427,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_reactive_ble:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_reactive_ble
|
||||||
|
sha256: "3ca8430bc7a6cabe5529aab4afaa2e3cb285941f7f7ab7472604074e347c1302"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.4.0"
|
||||||
flutter_riverpod:
|
flutter_riverpod:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -358,6 +443,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.1"
|
version: "2.6.1"
|
||||||
|
flutter_rust_bridge:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_rust_bridge
|
||||||
|
sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -368,6 +461,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
fluttertoast:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fluttertoast
|
||||||
|
sha256: "144ddd74d49c865eba47abe31cbc746c7b311c82d6c32e571fd73c4264b740e2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.0.0"
|
||||||
freezed:
|
freezed:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -392,6 +493,19 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
fuchsia_remote_debug_protocol:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
functional_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: functional_data
|
||||||
|
sha256: "76d17dc707c40e552014f5a49c0afcc3f1e3f05e800cd6b7872940bfe41a5039"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -416,6 +530,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.2"
|
||||||
|
hex:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hex
|
||||||
|
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
hotreloader:
|
hotreloader:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -428,10 +550,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.6.0"
|
||||||
http_multi_server:
|
http_multi_server:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -448,6 +570,11 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
integration_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -484,26 +611,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.7"
|
version: "11.0.2"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.8"
|
version: "3.0.10"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -524,26 +651,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.16+1"
|
version: "0.12.18"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
version: "1.17.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -552,6 +679,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
nb_utils:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: nb_utils
|
||||||
|
sha256: "2cfecbe67bc09ab31069cfe68e4eb232bc71f0670c98a91d0e73e7741a4798ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.2.0"
|
||||||
|
nm:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nm
|
||||||
|
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.0"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -564,10 +707,34 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.1"
|
||||||
|
path_provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.16"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -624,6 +791,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.1"
|
version: "1.5.1"
|
||||||
|
process:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: process
|
||||||
|
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.5"
|
||||||
|
protobuf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: protobuf
|
||||||
|
sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -640,6 +823,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.0"
|
||||||
|
reactive_ble_mobile:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: reactive_ble_mobile
|
||||||
|
sha256: "78af92cfb184770a277a8bdaa76b396a28dbada8931f3d8d0ff552414681f5db"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.4.0"
|
||||||
|
reactive_ble_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: reactive_ble_platform_interface
|
||||||
|
sha256: d0e6f86c7ee3865b74d8a7b6deb1fb8320b24176dfd9a3e475f84b7117eb42c7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.4.0"
|
||||||
|
recase:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: recase
|
||||||
|
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.0"
|
||||||
riverpod:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -688,8 +895,15 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
|
rust_lib_abawo_bt_app:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: rust_builder
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.0.1"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: rxdart
|
name: rxdart
|
||||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||||
@ -700,10 +914,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a"
|
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.2"
|
version: "2.5.4"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -805,14 +1019,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
version: "7.0.0"
|
||||||
|
sqlite3:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlite3
|
||||||
|
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.5"
|
||||||
|
sqlite3_flutter_libs:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlite3_flutter_libs
|
||||||
|
sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.32"
|
||||||
|
sqlparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqlparser
|
||||||
|
sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.41.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.0"
|
version: "1.12.1"
|
||||||
state_notifier:
|
state_notifier:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -825,10 +1063,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.4"
|
||||||
stream_transform:
|
stream_transform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -845,6 +1083,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
sync_http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sync_http
|
||||||
|
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -857,10 +1103,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.3"
|
version: "0.7.9"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -889,10 +1135,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.2.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -933,6 +1179,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
webdriver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webdriver
|
||||||
|
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -958,5 +1212,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.1 <4.0.0"
|
dart: ">=3.9.0 <4.0.0"
|
||||||
flutter: ">=3.27.0"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
17
pubspec.yaml
17
pubspec.yaml
@ -39,11 +39,21 @@ dependencies:
|
|||||||
go_router: ^14.8.1
|
go_router: ^14.8.1
|
||||||
freezed_annotation: ^3.0.0
|
freezed_annotation: ^3.0.0
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
flutter_blue_plus: ^1.35.3
|
flutter_blue_plus: ^2.1.0
|
||||||
rust: ^3.1.0
|
rust: ^3.1.0
|
||||||
anyhow: ^3.0.1
|
anyhow: ^3.0.1
|
||||||
logging: ^1.3.0
|
logging: ^1.3.0
|
||||||
shared_preferences: ^2.5.2
|
shared_preferences: ^2.5.2
|
||||||
|
drift: ^2.26.0
|
||||||
|
drift_flutter: ^0.2.4
|
||||||
|
path_provider: ^2.1.5
|
||||||
|
rxdart: ^0.28.0
|
||||||
|
rust_lib_abawo_bt_app:
|
||||||
|
path: rust_builder
|
||||||
|
flutter_rust_bridge: 2.11.1
|
||||||
|
flutter_reactive_ble: ^5.4.0
|
||||||
|
nb_utils: ^7.2.0
|
||||||
|
cbor: ^6.3.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -61,6 +71,9 @@ dev_dependencies:
|
|||||||
riverpod_lint: ^2.6.5
|
riverpod_lint: ^2.6.5
|
||||||
freezed: ^3.0.4
|
freezed: ^3.0.4
|
||||||
json_serializable: ^6.9.4
|
json_serializable: ^6.9.4
|
||||||
|
drift_dev: ^2.26.0
|
||||||
|
integration_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
@ -77,6 +90,8 @@ flutter:
|
|||||||
# assets:
|
# assets:
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
assets:
|
||||||
|
- assets/images/shifter-wireframe.png
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|||||||
1
rust/.gitignore
vendored
Normal file
1
rust/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
729
rust/Cargo.lock
generated
Normal file
729
rust/Cargo.lock
generated
Normal file
@ -0,0 +1,729 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "addr2line"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
|
||||||
|
dependencies = [
|
||||||
|
"gimli",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allo-isolate"
|
||||||
|
version = "0.1.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "449e356a4864c017286dbbec0e12767ea07efba29e3b7d984194c2a7ff3c4550"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"atomic",
|
||||||
|
"backtrace",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_log-sys"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_logger"
|
||||||
|
version = "0.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
|
||||||
|
dependencies = [
|
||||||
|
"android_log-sys",
|
||||||
|
"env_filter",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.75"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backtrace"
|
||||||
|
version = "0.3.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
|
||||||
|
dependencies = [
|
||||||
|
"addr2line",
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"miniz_oxide",
|
||||||
|
"object",
|
||||||
|
"rustc-demangle",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "build-target"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.0.83"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_error_panic_hook"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dart-sys"
|
||||||
|
version = "4.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57967e4b200d767d091b961d6ab42cc7d0cc14fe9e052e75d0d3cf9eb732d895"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dashmap"
|
||||||
|
version = "5.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"hashbrown",
|
||||||
|
"lock_api",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "delegate-attr"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_filter"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flutter_rust_bridge"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dde126295b2acc5f0a712e265e91b6fdc0ed38767496483e592ae7134db83725"
|
||||||
|
dependencies = [
|
||||||
|
"allo-isolate",
|
||||||
|
"android_logger",
|
||||||
|
"anyhow",
|
||||||
|
"build-target",
|
||||||
|
"bytemuck",
|
||||||
|
"byteorder",
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"dart-sys",
|
||||||
|
"delegate-attr",
|
||||||
|
"flutter_rust_bridge_macros",
|
||||||
|
"futures",
|
||||||
|
"js-sys",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"oslog",
|
||||||
|
"portable-atomic",
|
||||||
|
"threadpool",
|
||||||
|
"tokio",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flutter_rust_bridge_macros"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d5f0420326b13675321b194928bb7830043b68cf8b810e1c651285c747abb080"
|
||||||
|
dependencies = [
|
||||||
|
"hex",
|
||||||
|
"md-5",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-macro"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-macro",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"memchr",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gimli"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
||||||
|
dependencies = [
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.150"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md-5"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
|
||||||
|
dependencies = [
|
||||||
|
"adler",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_cpus"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object"
|
||||||
|
version = "0.32.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oslog"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"dashmap",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.70"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust_lib_abawo_bt_app"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"flutter_rust_bridge",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.39"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "threadpool"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
|
||||||
|
dependencies = [
|
||||||
|
"num_cpus",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"num_cpus",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-futures"
|
||||||
|
version = "0.4.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.66"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
13
rust/Cargo.toml
Normal file
13
rust/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "rust_lib_abawo_bt_app"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "staticlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
flutter_rust_bridge = "=2.11.1"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
|
||||||
1
rust/src/api/mod.rs
Normal file
1
rust/src/api/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod simple;
|
||||||
12
rust/src/api/simple.rs
Normal file
12
rust/src/api/simple.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#[flutter_rust_bridge::frb(sync)] // Synchronous mode for simplicity of the demo
|
||||||
|
pub fn greet(name: String) -> String {
|
||||||
|
format!("Hello, {name}!")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[flutter_rust_bridge::frb(init)]
|
||||||
|
pub fn init_app() {
|
||||||
|
// Default utilities - feel free to customize
|
||||||
|
flutter_rust_bridge::setup_default_user_utils();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
276
rust/src/frb_generated.rs
Normal file
276
rust/src/frb_generated.rs
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
#![allow(
|
||||||
|
non_camel_case_types,
|
||||||
|
unused,
|
||||||
|
non_snake_case,
|
||||||
|
clippy::needless_return,
|
||||||
|
clippy::redundant_closure_call,
|
||||||
|
clippy::redundant_closure,
|
||||||
|
clippy::useless_conversion,
|
||||||
|
clippy::unit_arg,
|
||||||
|
clippy::unused_unit,
|
||||||
|
clippy::double_parens,
|
||||||
|
clippy::let_and_return,
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
clippy::match_single_binding,
|
||||||
|
clippy::clone_on_copy,
|
||||||
|
clippy::let_unit_value,
|
||||||
|
clippy::deref_addrof,
|
||||||
|
clippy::explicit_auto_deref,
|
||||||
|
clippy::borrow_deref_ref,
|
||||||
|
clippy::needless_borrow
|
||||||
|
)]
|
||||||
|
|
||||||
|
// Section: imports
|
||||||
|
|
||||||
|
use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||||
|
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||||
|
|
||||||
|
// Section: boilerplate
|
||||||
|
|
||||||
|
flutter_rust_bridge::frb_generated_boilerplate!(
|
||||||
|
default_stream_sink_codec = SseCodec,
|
||||||
|
default_rust_opaque = RustOpaqueMoi,
|
||||||
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
|
);
|
||||||
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1";
|
||||||
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1918914929;
|
||||||
|
|
||||||
|
// Section: executor
|
||||||
|
|
||||||
|
flutter_rust_bridge::frb_generated_default_handler!();
|
||||||
|
|
||||||
|
// Section: wire_funcs
|
||||||
|
|
||||||
|
fn wire__crate__api__simple__greet_impl(
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "greet",
|
||||||
|
port: None,
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_name = <String>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
transform_result_sse::<_, ()>((move || {
|
||||||
|
let output_ok = Result::<_, ()>::Ok(crate::api::simple::greet(api_name))?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn wire__crate__api__simple__init_app_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "init_app",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| {
|
||||||
|
transform_result_sse::<_, ()>((move || {
|
||||||
|
let output_ok = Result::<_, ()>::Ok({
|
||||||
|
crate::api::simple::init_app();
|
||||||
|
})?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section: dart2rust
|
||||||
|
|
||||||
|
impl SseDecode for String {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut inner = <Vec<u8>>::sse_decode(deserializer);
|
||||||
|
return String::from_utf8(inner).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for Vec<u8> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut len_ = <i32>::sse_decode(deserializer);
|
||||||
|
let mut ans_ = vec![];
|
||||||
|
for idx_ in 0..len_ {
|
||||||
|
ans_.push(<u8>::sse_decode(deserializer));
|
||||||
|
}
|
||||||
|
return ans_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for u8 {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
deserializer.cursor.read_u8().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for () {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for i32 {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
deserializer.cursor.read_i32::<NativeEndian>().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseDecode for bool {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
deserializer.cursor.read_u8().unwrap() != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pde_ffi_dispatcher_primary_impl(
|
||||||
|
func_id: i32,
|
||||||
|
port: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len: i32,
|
||||||
|
data_len: i32,
|
||||||
|
) {
|
||||||
|
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||||
|
match func_id {
|
||||||
|
2 => wire__crate__api__simple__init_app_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pde_ffi_dispatcher_sync_impl(
|
||||||
|
func_id: i32,
|
||||||
|
ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len: i32,
|
||||||
|
data_len: i32,
|
||||||
|
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||||
|
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||||
|
match func_id {
|
||||||
|
1 => wire__crate__api__simple__greet_impl(ptr, rust_vec_len, data_len),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section: rust2dart
|
||||||
|
|
||||||
|
impl SseEncode for String {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<Vec<u8>>::sse_encode(self.into_bytes(), serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for Vec<u8> {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<i32>::sse_encode(self.len() as _, serializer);
|
||||||
|
for item in self {
|
||||||
|
<u8>::sse_encode(item, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for u8 {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
serializer.cursor.write_u8(self).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for () {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for i32 {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
serializer.cursor.write_i32::<NativeEndian>(self).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SseEncode for bool {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
serializer.cursor.write_u8(self as _).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
mod io {
|
||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
// Section: imports
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use flutter_rust_bridge::for_generated::byteorder::{
|
||||||
|
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||||
|
};
|
||||||
|
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||||
|
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||||
|
|
||||||
|
// Section: boilerplate
|
||||||
|
|
||||||
|
flutter_rust_bridge::frb_generated_boilerplate_io!();
|
||||||
|
}
|
||||||
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
pub use io::*;
|
||||||
|
|
||||||
|
/// cbindgen:ignore
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
mod web {
|
||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.11.1.
|
||||||
|
|
||||||
|
// Section: imports
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use flutter_rust_bridge::for_generated::byteorder::{
|
||||||
|
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||||
|
};
|
||||||
|
use flutter_rust_bridge::for_generated::wasm_bindgen;
|
||||||
|
use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*;
|
||||||
|
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||||
|
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||||
|
|
||||||
|
// Section: boilerplate
|
||||||
|
|
||||||
|
flutter_rust_bridge::frb_generated_boilerplate_web!();
|
||||||
|
}
|
||||||
|
#[cfg(target_family = "wasm")]
|
||||||
|
pub use web::*;
|
||||||
2
rust/src/lib.rs
Normal file
2
rust/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod api;
|
||||||
|
mod frb_generated;
|
||||||
29
rust_builder/.gitignore
vendored
Normal file
29
rust_builder/.gitignore
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
/pubspec.lock
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
build/
|
||||||
1
rust_builder/README.md
Normal file
1
rust_builder/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
Please ignore this folder, which is just glue to build Rust with Flutter.
|
||||||
9
rust_builder/android/.gitignore
vendored
Normal file
9
rust_builder/android/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/libraries
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.cxx
|
||||||
56
rust_builder/android/build.gradle
Normal file
56
rust_builder/android/build.gradle
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// The Android Gradle Plugin builds the native code with the Android NDK.
|
||||||
|
|
||||||
|
group 'com.flutter_rust_bridge.rust_lib_abawo_bt_app'
|
||||||
|
version '1.0'
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// The Android Gradle Plugin knows how to build native code with the NDK.
|
||||||
|
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
if (project.android.hasProperty("namespace")) {
|
||||||
|
namespace 'com.flutter_rust_bridge.rust_lib_abawo_bt_app'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bumping the plugin compileSdkVersion requires all clients of this plugin
|
||||||
|
// to bump the version in their app.
|
||||||
|
compileSdkVersion 33
|
||||||
|
|
||||||
|
// Use the NDK version
|
||||||
|
// declared in /android/app/build.gradle file of the Flutter project.
|
||||||
|
// Replace it with a version number if this plugin requires a specfic NDK version.
|
||||||
|
// (e.g. ndkVersion "23.1.7779620")
|
||||||
|
ndkVersion android.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 19
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "../cargokit/gradle/plugin.gradle"
|
||||||
|
cargokit {
|
||||||
|
manifestDir = "../../rust"
|
||||||
|
libname = "rust_lib_abawo_bt_app"
|
||||||
|
}
|
||||||
1
rust_builder/android/settings.gradle
Normal file
1
rust_builder/android/settings.gradle
Normal file
@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'rust_lib_abawo_bt_app'
|
||||||
3
rust_builder/android/src/main/AndroidManifest.xml
Normal file
3
rust_builder/android/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.flutter_rust_bridge.rust_lib_abawo_bt_app">
|
||||||
|
</manifest>
|
||||||
4
rust_builder/cargokit/.gitignore
vendored
Normal file
4
rust_builder/cargokit/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
target
|
||||||
|
.dart_tool
|
||||||
|
*.iml
|
||||||
|
!pubspec.lock
|
||||||
42
rust_builder/cargokit/LICENSE
Normal file
42
rust_builder/cargokit/LICENSE
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
Copyright 2022 Matej Knopp
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
MIT LICENSE
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||||
|
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
APACHE LICENSE, VERSION 2.0
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
11
rust_builder/cargokit/README
Normal file
11
rust_builder/cargokit/README
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
Experimental repository to provide glue for seamlessly integrating cargo build
|
||||||
|
with flutter plugins and packages.
|
||||||
|
|
||||||
|
See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/
|
||||||
|
for a tutorial on how to use Cargokit.
|
||||||
|
|
||||||
|
Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin.
|
||||||
|
|
||||||
58
rust_builder/cargokit/build_pod.sh
Executable file
58
rust_builder/cargokit/build_pod.sh
Executable file
@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BASEDIR=$(dirname "$0")
|
||||||
|
|
||||||
|
# Workaround for https://github.com/dart-lang/pub/issues/4010
|
||||||
|
BASEDIR=$(cd "$BASEDIR" ; pwd -P)
|
||||||
|
|
||||||
|
# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project
|
||||||
|
NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"`
|
||||||
|
|
||||||
|
export PATH=${NEW_PATH%?} # remove trailing :
|
||||||
|
|
||||||
|
env
|
||||||
|
|
||||||
|
# Platform name (macosx, iphoneos, iphonesimulator)
|
||||||
|
export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME
|
||||||
|
|
||||||
|
# Arctive architectures (arm64, armv7, x86_64), space separated.
|
||||||
|
export CARGOKIT_DARWIN_ARCHS=$ARCHS
|
||||||
|
|
||||||
|
# Current build configuration (Debug, Release)
|
||||||
|
export CARGOKIT_CONFIGURATION=$CONFIGURATION
|
||||||
|
|
||||||
|
# Path to directory containing Cargo.toml.
|
||||||
|
export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1
|
||||||
|
|
||||||
|
# Temporary directory for build artifacts.
|
||||||
|
export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR
|
||||||
|
|
||||||
|
# Output directory for final artifacts.
|
||||||
|
export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME
|
||||||
|
|
||||||
|
# Directory to store built tool artifacts.
|
||||||
|
export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool
|
||||||
|
|
||||||
|
# Directory inside root project. Not necessarily the top level directory of root project.
|
||||||
|
export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT
|
||||||
|
|
||||||
|
FLUTTER_EXPORT_BUILD_ENVIRONMENT=(
|
||||||
|
"$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS
|
||||||
|
"$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS
|
||||||
|
)
|
||||||
|
|
||||||
|
for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}"
|
||||||
|
do
|
||||||
|
if [[ -f "$path" ]]; then
|
||||||
|
source "$path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
sh "$BASEDIR/run_build_tool.sh" build-pod "$@"
|
||||||
|
|
||||||
|
# Make a symlink from built framework to phony file, which will be used as input to
|
||||||
|
# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate
|
||||||
|
# attribute on custom build phase)
|
||||||
|
ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony"
|
||||||
|
ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out"
|
||||||
5
rust_builder/cargokit/build_tool/README.md
Normal file
5
rust_builder/cargokit/build_tool/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
A sample command-line application with an entrypoint in `bin/`, library code
|
||||||
|
in `lib/`, and example unit test in `test/`.
|
||||||
34
rust_builder/cargokit/build_tool/analysis_options.yaml
Normal file
34
rust_builder/cargokit/build_tool/analysis_options.yaml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
# This file configures the static analysis results for your project (errors,
|
||||||
|
# warnings, and lints).
|
||||||
|
#
|
||||||
|
# This enables the 'recommended' set of lints from `package:lints`.
|
||||||
|
# This set helps identify many issues that may lead to problems when running
|
||||||
|
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||||
|
# style and format.
|
||||||
|
#
|
||||||
|
# If you want a smaller set of lints you can change this to specify
|
||||||
|
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||||
|
# (the recommended set includes the core lints).
|
||||||
|
# The core lints are also what is used by pub.dev for scoring packages.
|
||||||
|
|
||||||
|
include: package:lints/recommended.yaml
|
||||||
|
|
||||||
|
# Uncomment the following section to specify additional rules.
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
||||||
|
- prefer_relative_imports
|
||||||
|
- directives_ordering
|
||||||
|
|
||||||
|
# analyzer:
|
||||||
|
# exclude:
|
||||||
|
# - path/to/excluded/files/**
|
||||||
|
|
||||||
|
# For more information about the core and recommended set of lints, see
|
||||||
|
# https://dart.dev/go/core-lints
|
||||||
|
|
||||||
|
# For additional information about configuring this file, see
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
8
rust_builder/cargokit/build_tool/bin/build_tool.dart
Normal file
8
rust_builder/cargokit/build_tool/bin/build_tool.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'package:build_tool/build_tool.dart' as build_tool;
|
||||||
|
|
||||||
|
void main(List<String> arguments) {
|
||||||
|
build_tool.runMain(arguments);
|
||||||
|
}
|
||||||
8
rust_builder/cargokit/build_tool/lib/build_tool.dart
Normal file
8
rust_builder/cargokit/build_tool/lib/build_tool.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'src/build_tool.dart' as build_tool;
|
||||||
|
|
||||||
|
Future<void> runMain(List<String> args) async {
|
||||||
|
return build_tool.runMain(args);
|
||||||
|
}
|
||||||
@ -0,0 +1,195 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:version/version.dart';
|
||||||
|
|
||||||
|
import 'target.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class AndroidEnvironment {
|
||||||
|
AndroidEnvironment({
|
||||||
|
required this.sdkPath,
|
||||||
|
required this.ndkVersion,
|
||||||
|
required this.minSdkVersion,
|
||||||
|
required this.targetTempDir,
|
||||||
|
required this.target,
|
||||||
|
});
|
||||||
|
|
||||||
|
static void clangLinkerWrapper(List<String> args) {
|
||||||
|
final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG'];
|
||||||
|
if (clang == null) {
|
||||||
|
throw Exception(
|
||||||
|
"cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var");
|
||||||
|
}
|
||||||
|
final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET'];
|
||||||
|
if (target == null) {
|
||||||
|
throw Exception(
|
||||||
|
"cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var");
|
||||||
|
}
|
||||||
|
|
||||||
|
runCommand(clang, [
|
||||||
|
target,
|
||||||
|
...args,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full path to Android SDK.
|
||||||
|
final String sdkPath;
|
||||||
|
|
||||||
|
/// Full version of Android NDK.
|
||||||
|
final String ndkVersion;
|
||||||
|
|
||||||
|
/// Minimum supported SDK version.
|
||||||
|
final int minSdkVersion;
|
||||||
|
|
||||||
|
/// Target directory for build artifacts.
|
||||||
|
final String targetTempDir;
|
||||||
|
|
||||||
|
/// Target being built.
|
||||||
|
final Target target;
|
||||||
|
|
||||||
|
bool ndkIsInstalled() {
|
||||||
|
final ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
||||||
|
final ndkPackageXml = File(path.join(ndkPath, 'package.xml'));
|
||||||
|
return ndkPackageXml.existsSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void installNdk({
|
||||||
|
required String javaHome,
|
||||||
|
}) {
|
||||||
|
final sdkManagerExtension = Platform.isWindows ? '.bat' : '';
|
||||||
|
final sdkManager = path.join(
|
||||||
|
sdkPath,
|
||||||
|
'cmdline-tools',
|
||||||
|
'latest',
|
||||||
|
'bin',
|
||||||
|
'sdkmanager$sdkManagerExtension',
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info('Installing NDK $ndkVersion');
|
||||||
|
runCommand(sdkManager, [
|
||||||
|
'--install',
|
||||||
|
'ndk;$ndkVersion',
|
||||||
|
], environment: {
|
||||||
|
'JAVA_HOME': javaHome,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>> buildEnvironment() async {
|
||||||
|
final hostArch = Platform.isMacOS
|
||||||
|
? "darwin-x86_64"
|
||||||
|
: (Platform.isLinux ? "linux-x86_64" : "windows-x86_64");
|
||||||
|
|
||||||
|
final ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
||||||
|
final toolchainPath = path.join(
|
||||||
|
ndkPath,
|
||||||
|
'toolchains',
|
||||||
|
'llvm',
|
||||||
|
'prebuilt',
|
||||||
|
hostArch,
|
||||||
|
'bin',
|
||||||
|
);
|
||||||
|
|
||||||
|
final minSdkVersion =
|
||||||
|
math.max(target.androidMinSdkVersion!, this.minSdkVersion);
|
||||||
|
|
||||||
|
final exe = Platform.isWindows ? '.exe' : '';
|
||||||
|
|
||||||
|
final arKey = 'AR_${target.rust}';
|
||||||
|
final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe']
|
||||||
|
.map((e) => path.join(toolchainPath, e))
|
||||||
|
.firstWhereOrNull((element) => File(element).existsSync());
|
||||||
|
if (arValue == null) {
|
||||||
|
throw Exception('Failed to find ar for $target in $toolchainPath');
|
||||||
|
}
|
||||||
|
|
||||||
|
final targetArg = '--target=${target.rust}$minSdkVersion';
|
||||||
|
|
||||||
|
final ccKey = 'CC_${target.rust}';
|
||||||
|
final ccValue = path.join(toolchainPath, 'clang$exe');
|
||||||
|
final cfFlagsKey = 'CFLAGS_${target.rust}';
|
||||||
|
final cFlagsValue = targetArg;
|
||||||
|
|
||||||
|
final cxxKey = 'CXX_${target.rust}';
|
||||||
|
final cxxValue = path.join(toolchainPath, 'clang++$exe');
|
||||||
|
final cxxFlagsKey = 'CXXFLAGS_${target.rust}';
|
||||||
|
final cxxFlagsValue = targetArg;
|
||||||
|
|
||||||
|
final linkerKey =
|
||||||
|
'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase();
|
||||||
|
|
||||||
|
final ranlibKey = 'RANLIB_${target.rust}';
|
||||||
|
final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe');
|
||||||
|
|
||||||
|
final ndkVersionParsed = Version.parse(ndkVersion);
|
||||||
|
final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS';
|
||||||
|
final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed);
|
||||||
|
|
||||||
|
final runRustTool =
|
||||||
|
Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh';
|
||||||
|
|
||||||
|
final packagePath = (await Isolate.resolvePackageUri(
|
||||||
|
Uri.parse('package:build_tool/buildtool.dart')))!
|
||||||
|
.toFilePath();
|
||||||
|
final selfPath = path.canonicalize(path.join(
|
||||||
|
packagePath,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
runRustTool,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Make sure that run_build_tool is working properly even initially launched directly
|
||||||
|
// through dart run.
|
||||||
|
final toolTempDir =
|
||||||
|
Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir;
|
||||||
|
|
||||||
|
return {
|
||||||
|
arKey: arValue,
|
||||||
|
ccKey: ccValue,
|
||||||
|
cfFlagsKey: cFlagsValue,
|
||||||
|
cxxKey: cxxValue,
|
||||||
|
cxxFlagsKey: cxxFlagsValue,
|
||||||
|
ranlibKey: ranlibValue,
|
||||||
|
rustFlagsKey: rustFlagsValue,
|
||||||
|
linkerKey: selfPath,
|
||||||
|
// Recognized by main() so we know when we're acting as a wrapper
|
||||||
|
'_CARGOKIT_NDK_LINK_TARGET': targetArg,
|
||||||
|
'_CARGOKIT_NDK_LINK_CLANG': ccValue,
|
||||||
|
'CARGOKIT_TOOL_TEMP_DIR': toolTempDir,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround for libgcc missing in NDK23, inspired by cargo-ndk
|
||||||
|
String _libGccWorkaround(String buildDir, Version ndkVersion) {
|
||||||
|
final workaroundDir = path.join(
|
||||||
|
buildDir,
|
||||||
|
'cargokit',
|
||||||
|
'libgcc_workaround',
|
||||||
|
'${ndkVersion.major}',
|
||||||
|
);
|
||||||
|
Directory(workaroundDir).createSync(recursive: true);
|
||||||
|
if (ndkVersion.major >= 23) {
|
||||||
|
File(path.join(workaroundDir, 'libgcc.a'))
|
||||||
|
.writeAsStringSync('INPUT(-lunwind)');
|
||||||
|
} else {
|
||||||
|
// Other way around, untested, forward libgcc.a from libunwind once Rust
|
||||||
|
// gets updated for NDK23+.
|
||||||
|
File(path.join(workaroundDir, 'libunwind.a'))
|
||||||
|
.writeAsStringSync('INPUT(-lgcc)');
|
||||||
|
}
|
||||||
|
|
||||||
|
var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? '';
|
||||||
|
if (rustFlags.isNotEmpty) {
|
||||||
|
rustFlags = '$rustFlags\x1f';
|
||||||
|
}
|
||||||
|
rustFlags = '$rustFlags-L\x1f$workaroundDir';
|
||||||
|
return rustFlags;
|
||||||
|
}
|
||||||
|
}
|
||||||
266
rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart
Normal file
266
rust_builder/cargokit/build_tool/lib/src/artifacts_provider.dart
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'crate_hash.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'precompile_binaries.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
class Artifact {
|
||||||
|
/// File system location of the artifact.
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
/// Actual file name that the artifact should have in destination folder.
|
||||||
|
final String finalFileName;
|
||||||
|
|
||||||
|
AritifactType get type {
|
||||||
|
if (finalFileName.endsWith('.dll') ||
|
||||||
|
finalFileName.endsWith('.dll.lib') ||
|
||||||
|
finalFileName.endsWith('.pdb') ||
|
||||||
|
finalFileName.endsWith('.so') ||
|
||||||
|
finalFileName.endsWith('.dylib')) {
|
||||||
|
return AritifactType.dylib;
|
||||||
|
} else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) {
|
||||||
|
return AritifactType.staticlib;
|
||||||
|
} else {
|
||||||
|
throw Exception('Unknown artifact type for $finalFileName');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Artifact({
|
||||||
|
required this.path,
|
||||||
|
required this.finalFileName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final _log = Logger('artifacts_provider');
|
||||||
|
|
||||||
|
class ArtifactProvider {
|
||||||
|
ArtifactProvider({
|
||||||
|
required this.environment,
|
||||||
|
required this.userOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BuildEnvironment environment;
|
||||||
|
final CargokitUserOptions userOptions;
|
||||||
|
|
||||||
|
Future<Map<Target, List<Artifact>>> getArtifacts(List<Target> targets) async {
|
||||||
|
final result = await _getPrecompiledArtifacts(targets);
|
||||||
|
|
||||||
|
final pendingTargets = List.of(targets);
|
||||||
|
pendingTargets.removeWhere((element) => result.containsKey(element));
|
||||||
|
|
||||||
|
if (pendingTargets.isEmpty) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
final rustup = Rustup();
|
||||||
|
for (final target in targets) {
|
||||||
|
final builder = RustBuilder(target: target, environment: environment);
|
||||||
|
builder.prepare(rustup);
|
||||||
|
_log.info('Building ${environment.crateInfo.packageName} for $target');
|
||||||
|
final targetDir = await builder.build();
|
||||||
|
// For local build accept both static and dynamic libraries.
|
||||||
|
final artifactNames = <String>{
|
||||||
|
...getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: environment.crateInfo.packageName,
|
||||||
|
aritifactType: AritifactType.dylib,
|
||||||
|
remote: false,
|
||||||
|
),
|
||||||
|
...getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: environment.crateInfo.packageName,
|
||||||
|
aritifactType: AritifactType.staticlib,
|
||||||
|
remote: false,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
final artifacts = artifactNames
|
||||||
|
.map((artifactName) => Artifact(
|
||||||
|
path: path.join(targetDir, artifactName),
|
||||||
|
finalFileName: artifactName,
|
||||||
|
))
|
||||||
|
.where((element) => File(element.path).existsSync())
|
||||||
|
.toList();
|
||||||
|
result[target] = artifacts;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<Target, List<Artifact>>> _getPrecompiledArtifacts(
|
||||||
|
List<Target> targets) async {
|
||||||
|
if (userOptions.usePrecompiledBinaries == false) {
|
||||||
|
_log.info('Precompiled binaries are disabled');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (environment.crateOptions.precompiledBinaries == null) {
|
||||||
|
_log.fine('Precompiled binaries not enabled for this crate');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
final start = Stopwatch()..start();
|
||||||
|
final crateHash = CrateHash.compute(environment.manifestDir,
|
||||||
|
tempStorage: environment.targetTempDir);
|
||||||
|
_log.fine(
|
||||||
|
'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms');
|
||||||
|
|
||||||
|
final downloadedArtifactsDir =
|
||||||
|
path.join(environment.targetTempDir, 'precompiled', crateHash);
|
||||||
|
Directory(downloadedArtifactsDir).createSync(recursive: true);
|
||||||
|
|
||||||
|
final res = <Target, List<Artifact>>{};
|
||||||
|
|
||||||
|
for (final target in targets) {
|
||||||
|
final requiredArtifacts = getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: environment.crateInfo.packageName,
|
||||||
|
remote: true,
|
||||||
|
);
|
||||||
|
final artifactsForTarget = <Artifact>[];
|
||||||
|
|
||||||
|
for (final artifact in requiredArtifacts) {
|
||||||
|
final fileName = PrecompileBinaries.fileName(target, artifact);
|
||||||
|
final downloadedPath = path.join(downloadedArtifactsDir, fileName);
|
||||||
|
if (!File(downloadedPath).existsSync()) {
|
||||||
|
final signatureFileName =
|
||||||
|
PrecompileBinaries.signatureFileName(target, artifact);
|
||||||
|
await _tryDownloadArtifacts(
|
||||||
|
crateHash: crateHash,
|
||||||
|
fileName: fileName,
|
||||||
|
signatureFileName: signatureFileName,
|
||||||
|
finalPath: downloadedPath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (File(downloadedPath).existsSync()) {
|
||||||
|
artifactsForTarget.add(Artifact(
|
||||||
|
path: downloadedPath,
|
||||||
|
finalFileName: artifact,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only provide complete set of artifacts.
|
||||||
|
if (artifactsForTarget.length == requiredArtifacts.length) {
|
||||||
|
_log.fine('Found precompiled artifacts for $target');
|
||||||
|
res[target] = artifactsForTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Response> _get(Uri url, {Map<String, String>? headers}) async {
|
||||||
|
int attempt = 0;
|
||||||
|
const maxAttempts = 10;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return await get(url, headers: headers);
|
||||||
|
} on SocketException catch (e) {
|
||||||
|
// Try to detect reset by peer error and retry.
|
||||||
|
if (attempt++ < maxAttempts &&
|
||||||
|
(e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) {
|
||||||
|
_log.severe(
|
||||||
|
'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...');
|
||||||
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _tryDownloadArtifacts({
|
||||||
|
required String crateHash,
|
||||||
|
required String fileName,
|
||||||
|
required String signatureFileName,
|
||||||
|
required String finalPath,
|
||||||
|
}) async {
|
||||||
|
final precompiledBinaries = environment.crateOptions.precompiledBinaries!;
|
||||||
|
final prefix = precompiledBinaries.uriPrefix;
|
||||||
|
final url = Uri.parse('$prefix$crateHash/$fileName');
|
||||||
|
final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName');
|
||||||
|
_log.fine('Downloading signature from $signatureUrl');
|
||||||
|
final signature = await _get(signatureUrl);
|
||||||
|
if (signature.statusCode == 404) {
|
||||||
|
_log.warning(
|
||||||
|
'Precompiled binaries not available for crate hash $crateHash ($fileName)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (signature.statusCode != 200) {
|
||||||
|
_log.severe(
|
||||||
|
'Failed to download signature $signatureUrl: status ${signature.statusCode}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_log.fine('Downloading binary from $url');
|
||||||
|
final res = await _get(url);
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
_log.severe('Failed to download binary $url: status ${res.statusCode}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (verify(
|
||||||
|
precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) {
|
||||||
|
File(finalPath).writeAsBytesSync(res.bodyBytes);
|
||||||
|
} else {
|
||||||
|
_log.shout('Signature verification failed! Ignoring binary.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AritifactType {
|
||||||
|
staticlib,
|
||||||
|
dylib,
|
||||||
|
}
|
||||||
|
|
||||||
|
AritifactType artifactTypeForTarget(Target target) {
|
||||||
|
if (target.darwinPlatform != null) {
|
||||||
|
return AritifactType.staticlib;
|
||||||
|
} else {
|
||||||
|
return AritifactType.dylib;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> getArtifactNames({
|
||||||
|
required Target target,
|
||||||
|
required String libraryName,
|
||||||
|
required bool remote,
|
||||||
|
AritifactType? aritifactType,
|
||||||
|
}) {
|
||||||
|
aritifactType ??= artifactTypeForTarget(target);
|
||||||
|
if (target.darwinArch != null) {
|
||||||
|
if (aritifactType == AritifactType.staticlib) {
|
||||||
|
return ['lib$libraryName.a'];
|
||||||
|
} else {
|
||||||
|
return ['lib$libraryName.dylib'];
|
||||||
|
}
|
||||||
|
} else if (target.rust.contains('-windows-')) {
|
||||||
|
if (aritifactType == AritifactType.staticlib) {
|
||||||
|
return ['$libraryName.lib'];
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
'$libraryName.dll',
|
||||||
|
'$libraryName.dll.lib',
|
||||||
|
if (!remote) '$libraryName.pdb'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else if (target.rust.contains('-linux-')) {
|
||||||
|
if (aritifactType == AritifactType.staticlib) {
|
||||||
|
return ['lib$libraryName.a'];
|
||||||
|
} else {
|
||||||
|
return ['lib$libraryName.so'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception("Unsupported target: ${target.rust}");
|
||||||
|
}
|
||||||
|
}
|
||||||
40
rust_builder/cargokit/build_tool/lib/src/build_cmake.dart
Normal file
40
rust_builder/cargokit/build_tool/lib/src/build_cmake.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
class BuildCMake {
|
||||||
|
final CargokitUserOptions userOptions;
|
||||||
|
|
||||||
|
BuildCMake({required this.userOptions});
|
||||||
|
|
||||||
|
Future<void> build() async {
|
||||||
|
final targetPlatform = Environment.targetPlatform;
|
||||||
|
final target = Target.forFlutterName(Environment.targetPlatform);
|
||||||
|
if (target == null) {
|
||||||
|
throw Exception("Unknown target platform: $targetPlatform");
|
||||||
|
}
|
||||||
|
|
||||||
|
final environment = BuildEnvironment.fromEnvironment(isAndroid: false);
|
||||||
|
final provider =
|
||||||
|
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||||
|
final artifacts = await provider.getArtifacts([target]);
|
||||||
|
|
||||||
|
final libs = artifacts[target]!;
|
||||||
|
|
||||||
|
for (final lib in libs) {
|
||||||
|
if (lib.type == AritifactType.dylib) {
|
||||||
|
File(lib.path)
|
||||||
|
.copySync(path.join(Environment.outputDir, lib.finalFileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
rust_builder/cargokit/build_tool/lib/src/build_gradle.dart
Normal file
49
rust_builder/cargokit/build_tool/lib/src/build_gradle.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
final log = Logger('build_gradle');
|
||||||
|
|
||||||
|
class BuildGradle {
|
||||||
|
BuildGradle({required this.userOptions});
|
||||||
|
|
||||||
|
final CargokitUserOptions userOptions;
|
||||||
|
|
||||||
|
Future<void> build() async {
|
||||||
|
final targets = Environment.targetPlatforms.map((arch) {
|
||||||
|
final target = Target.forFlutterName(arch);
|
||||||
|
if (target == null) {
|
||||||
|
throw Exception(
|
||||||
|
"Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}");
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final environment = BuildEnvironment.fromEnvironment(isAndroid: true);
|
||||||
|
final provider =
|
||||||
|
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||||
|
final artifacts = await provider.getArtifacts(targets);
|
||||||
|
|
||||||
|
for (final target in targets) {
|
||||||
|
final libs = artifacts[target]!;
|
||||||
|
final outputDir = path.join(Environment.outputDir, target.android!);
|
||||||
|
Directory(outputDir).createSync(recursive: true);
|
||||||
|
|
||||||
|
for (final lib in libs) {
|
||||||
|
if (lib.type == AritifactType.dylib) {
|
||||||
|
File(lib.path).copySync(path.join(outputDir, lib.finalFileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
rust_builder/cargokit/build_tool/lib/src/build_pod.dart
Normal file
89
rust_builder/cargokit/build_tool/lib/src/build_pod.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class BuildPod {
|
||||||
|
BuildPod({required this.userOptions});
|
||||||
|
|
||||||
|
final CargokitUserOptions userOptions;
|
||||||
|
|
||||||
|
Future<void> build() async {
|
||||||
|
final targets = Environment.darwinArchs.map((arch) {
|
||||||
|
final target = Target.forDarwin(
|
||||||
|
platformName: Environment.darwinPlatformName, darwinAarch: arch);
|
||||||
|
if (target == null) {
|
||||||
|
throw Exception(
|
||||||
|
"Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}");
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final environment = BuildEnvironment.fromEnvironment(isAndroid: false);
|
||||||
|
final provider =
|
||||||
|
ArtifactProvider(environment: environment, userOptions: userOptions);
|
||||||
|
final artifacts = await provider.getArtifacts(targets);
|
||||||
|
|
||||||
|
void performLipo(String targetFile, Iterable<String> sourceFiles) {
|
||||||
|
runCommand("lipo", [
|
||||||
|
'-create',
|
||||||
|
...sourceFiles,
|
||||||
|
'-output',
|
||||||
|
targetFile,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final outputDir = Environment.outputDir;
|
||||||
|
|
||||||
|
Directory(outputDir).createSync(recursive: true);
|
||||||
|
|
||||||
|
final staticLibs = artifacts.values
|
||||||
|
.expand((element) => element)
|
||||||
|
.where((element) => element.type == AritifactType.staticlib)
|
||||||
|
.toList();
|
||||||
|
final dynamicLibs = artifacts.values
|
||||||
|
.expand((element) => element)
|
||||||
|
.where((element) => element.type == AritifactType.dylib)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final libName = environment.crateInfo.packageName;
|
||||||
|
|
||||||
|
// If there is static lib, use it and link it with pod
|
||||||
|
if (staticLibs.isNotEmpty) {
|
||||||
|
final finalTargetFile = path.join(outputDir, "lib$libName.a");
|
||||||
|
performLipo(finalTargetFile, staticLibs.map((e) => e.path));
|
||||||
|
} else {
|
||||||
|
// Otherwise try to replace bundle dylib with our dylib
|
||||||
|
final bundlePaths = [
|
||||||
|
'$libName.framework/Versions/A/$libName',
|
||||||
|
'$libName.framework/$libName',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final bundlePath in bundlePaths) {
|
||||||
|
final targetFile = path.join(outputDir, bundlePath);
|
||||||
|
if (File(targetFile).existsSync()) {
|
||||||
|
performLipo(targetFile, dynamicLibs.map((e) => e.path));
|
||||||
|
|
||||||
|
// Replace absolute id with @rpath one so that it works properly
|
||||||
|
// when moved to Frameworks.
|
||||||
|
runCommand("install_name_tool", [
|
||||||
|
'-id',
|
||||||
|
'@rpath/$bundlePath',
|
||||||
|
targetFile,
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw Exception('Unable to find bundle for dynamic library');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
271
rust_builder/cargokit/build_tool/lib/src/build_tool.dart
Normal file
271
rust_builder/cargokit/build_tool/lib/src/build_tool.dart
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:args/command_runner.dart';
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:github/github.dart';
|
||||||
|
import 'package:hex/hex.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import 'android_environment.dart';
|
||||||
|
import 'build_cmake.dart';
|
||||||
|
import 'build_gradle.dart';
|
||||||
|
import 'build_pod.dart';
|
||||||
|
import 'logging.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'precompile_binaries.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
import 'verify_binaries.dart';
|
||||||
|
|
||||||
|
final log = Logger('build_tool');
|
||||||
|
|
||||||
|
abstract class BuildCommand extends Command {
|
||||||
|
Future<void> runBuildCommand(CargokitUserOptions options);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> run() async {
|
||||||
|
final options = CargokitUserOptions.load();
|
||||||
|
|
||||||
|
if (options.verboseLogging ||
|
||||||
|
Platform.environment['CARGOKIT_VERBOSE'] == '1') {
|
||||||
|
enableVerboseLogging();
|
||||||
|
}
|
||||||
|
|
||||||
|
await runBuildCommand(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildPodCommand extends BuildCommand {
|
||||||
|
@override
|
||||||
|
final name = 'build-pod';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Build cocoa pod library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||||
|
final build = BuildPod(userOptions: options);
|
||||||
|
await build.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildGradleCommand extends BuildCommand {
|
||||||
|
@override
|
||||||
|
final name = 'build-gradle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Build android library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||||
|
final build = BuildGradle(userOptions: options);
|
||||||
|
await build.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildCMakeCommand extends BuildCommand {
|
||||||
|
@override
|
||||||
|
final name = 'build-cmake';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Build CMake library';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> runBuildCommand(CargokitUserOptions options) async {
|
||||||
|
final build = BuildCMake(userOptions: options);
|
||||||
|
await build.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GenKeyCommand extends Command {
|
||||||
|
@override
|
||||||
|
final name = 'gen-key';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Generate key pair for signing precompiled binaries';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void run() {
|
||||||
|
final kp = generateKey();
|
||||||
|
final private = HEX.encode(kp.privateKey.bytes);
|
||||||
|
final public = HEX.encode(kp.publicKey.bytes);
|
||||||
|
print("Private Key: $private");
|
||||||
|
print("Public Key: $public");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrecompileBinariesCommand extends Command {
|
||||||
|
PrecompileBinariesCommand() {
|
||||||
|
argParser
|
||||||
|
..addOption(
|
||||||
|
'repository',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Github repository slug in format owner/name',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'manifest-dir',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Directory containing Cargo.toml',
|
||||||
|
)
|
||||||
|
..addMultiOption('target',
|
||||||
|
help: 'Rust target triple of artifact to build.\n'
|
||||||
|
'Can be specified multiple times or omitted in which case\n'
|
||||||
|
'all targets for current platform will be built.')
|
||||||
|
..addOption(
|
||||||
|
'android-sdk-location',
|
||||||
|
help: 'Location of Android SDK (if available)',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'android-ndk-version',
|
||||||
|
help: 'Android NDK version (if available)',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'android-min-sdk-version',
|
||||||
|
help: 'Android minimum rquired version (if available)',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'temp-dir',
|
||||||
|
help: 'Directory to store temporary build artifacts',
|
||||||
|
)
|
||||||
|
..addFlag(
|
||||||
|
"verbose",
|
||||||
|
abbr: "v",
|
||||||
|
defaultsTo: false,
|
||||||
|
help: "Enable verbose logging",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final name = 'precompile-binaries';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Prebuild and upload binaries\n'
|
||||||
|
'Private key must be passed through PRIVATE_KEY environment variable. '
|
||||||
|
'Use gen_key through generate priave key.\n'
|
||||||
|
'Github token must be passed as GITHUB_TOKEN environment variable.\n';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> run() async {
|
||||||
|
final verbose = argResults!['verbose'] as bool;
|
||||||
|
if (verbose) {
|
||||||
|
enableVerboseLogging();
|
||||||
|
}
|
||||||
|
|
||||||
|
final privateKeyString = Platform.environment['PRIVATE_KEY'];
|
||||||
|
if (privateKeyString == null) {
|
||||||
|
throw ArgumentError('Missing PRIVATE_KEY environment variable');
|
||||||
|
}
|
||||||
|
final githubToken = Platform.environment['GITHUB_TOKEN'];
|
||||||
|
if (githubToken == null) {
|
||||||
|
throw ArgumentError('Missing GITHUB_TOKEN environment variable');
|
||||||
|
}
|
||||||
|
final privateKey = HEX.decode(privateKeyString);
|
||||||
|
if (privateKey.length != 64) {
|
||||||
|
throw ArgumentError('Private key must be 64 bytes long');
|
||||||
|
}
|
||||||
|
final manifestDir = argResults!['manifest-dir'] as String;
|
||||||
|
if (!Directory(manifestDir).existsSync()) {
|
||||||
|
throw ArgumentError('Manifest directory does not exist: $manifestDir');
|
||||||
|
}
|
||||||
|
String? androidMinSdkVersionString =
|
||||||
|
argResults!['android-min-sdk-version'] as String?;
|
||||||
|
int? androidMinSdkVersion;
|
||||||
|
if (androidMinSdkVersionString != null) {
|
||||||
|
androidMinSdkVersion = int.tryParse(androidMinSdkVersionString);
|
||||||
|
if (androidMinSdkVersion == null) {
|
||||||
|
throw ArgumentError(
|
||||||
|
'Invalid android-min-sdk-version: $androidMinSdkVersionString');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final targetStrigns = argResults!['target'] as List<String>;
|
||||||
|
final targets = targetStrigns.map((target) {
|
||||||
|
final res = Target.forRustTriple(target);
|
||||||
|
if (res == null) {
|
||||||
|
throw ArgumentError('Invalid target: $target');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}).toList(growable: false);
|
||||||
|
final precompileBinaries = PrecompileBinaries(
|
||||||
|
privateKey: PrivateKey(privateKey),
|
||||||
|
githubToken: githubToken,
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
repositorySlug: RepositorySlug.full(argResults!['repository'] as String),
|
||||||
|
targets: targets,
|
||||||
|
androidSdkLocation: argResults!['android-sdk-location'] as String?,
|
||||||
|
androidNdkVersion: argResults!['android-ndk-version'] as String?,
|
||||||
|
androidMinSdkVersion: androidMinSdkVersion,
|
||||||
|
tempDir: argResults!['temp-dir'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
await precompileBinaries.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VerifyBinariesCommand extends Command {
|
||||||
|
VerifyBinariesCommand() {
|
||||||
|
argParser.addOption(
|
||||||
|
'manifest-dir',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Directory containing Cargo.toml',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final name = "verify-binaries";
|
||||||
|
|
||||||
|
@override
|
||||||
|
final description = 'Verifies published binaries\n'
|
||||||
|
'Checks whether there is a binary published for each targets\n'
|
||||||
|
'and checks the signature.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> run() async {
|
||||||
|
final manifestDir = argResults!['manifest-dir'] as String;
|
||||||
|
final verifyBinaries = VerifyBinaries(
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
);
|
||||||
|
await verifyBinaries.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> runMain(List<String> args) async {
|
||||||
|
try {
|
||||||
|
// Init logging before options are loaded
|
||||||
|
initLogging();
|
||||||
|
|
||||||
|
if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) {
|
||||||
|
return AndroidEnvironment.clangLinkerWrapper(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
final runner = CommandRunner('build_tool', 'Cargokit built_tool')
|
||||||
|
..addCommand(BuildPodCommand())
|
||||||
|
..addCommand(BuildGradleCommand())
|
||||||
|
..addCommand(BuildCMakeCommand())
|
||||||
|
..addCommand(GenKeyCommand())
|
||||||
|
..addCommand(PrecompileBinariesCommand())
|
||||||
|
..addCommand(VerifyBinariesCommand());
|
||||||
|
|
||||||
|
await runner.run(args);
|
||||||
|
} on ArgumentError catch (e) {
|
||||||
|
stderr.writeln(e.toString());
|
||||||
|
exit(1);
|
||||||
|
} catch (e, s) {
|
||||||
|
log.severe(kDoubleSeparator);
|
||||||
|
log.severe('Cargokit BuildTool failed with error:');
|
||||||
|
log.severe(kSeparator);
|
||||||
|
log.severe(e);
|
||||||
|
// This tells user to install Rust, there's no need to pollute the log with
|
||||||
|
// stack trace.
|
||||||
|
if (e is! RustupNotFoundException) {
|
||||||
|
log.severe(kSeparator);
|
||||||
|
log.severe(s);
|
||||||
|
log.severe(kSeparator);
|
||||||
|
log.severe('BuildTool arguments: $args');
|
||||||
|
}
|
||||||
|
log.severe(kDoubleSeparator);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
198
rust_builder/cargokit/build_tool/lib/src/builder.dart
Normal file
198
rust_builder/cargokit/build_tool/lib/src/builder.dart
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'android_environment.dart';
|
||||||
|
import 'cargo.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
final _log = Logger('builder');
|
||||||
|
|
||||||
|
enum BuildConfiguration {
|
||||||
|
debug,
|
||||||
|
release,
|
||||||
|
profile,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on BuildConfiguration {
|
||||||
|
bool get isDebug => this == BuildConfiguration.debug;
|
||||||
|
String get rustName => switch (this) {
|
||||||
|
BuildConfiguration.debug => 'debug',
|
||||||
|
BuildConfiguration.release => 'release',
|
||||||
|
BuildConfiguration.profile => 'release',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildException implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
BuildException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'BuildException: $message';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuildEnvironment {
|
||||||
|
final BuildConfiguration configuration;
|
||||||
|
final CargokitCrateOptions crateOptions;
|
||||||
|
final String targetTempDir;
|
||||||
|
final String manifestDir;
|
||||||
|
final CrateInfo crateInfo;
|
||||||
|
|
||||||
|
final bool isAndroid;
|
||||||
|
final String? androidSdkPath;
|
||||||
|
final String? androidNdkVersion;
|
||||||
|
final int? androidMinSdkVersion;
|
||||||
|
final String? javaHome;
|
||||||
|
|
||||||
|
BuildEnvironment({
|
||||||
|
required this.configuration,
|
||||||
|
required this.crateOptions,
|
||||||
|
required this.targetTempDir,
|
||||||
|
required this.manifestDir,
|
||||||
|
required this.crateInfo,
|
||||||
|
required this.isAndroid,
|
||||||
|
this.androidSdkPath,
|
||||||
|
this.androidNdkVersion,
|
||||||
|
this.androidMinSdkVersion,
|
||||||
|
this.javaHome,
|
||||||
|
});
|
||||||
|
|
||||||
|
static BuildConfiguration parseBuildConfiguration(String value) {
|
||||||
|
// XCode configuration adds the flavor to configuration name.
|
||||||
|
final firstSegment = value.split('-').first;
|
||||||
|
final buildConfiguration = BuildConfiguration.values.firstWhereOrNull(
|
||||||
|
(e) => e.name == firstSegment,
|
||||||
|
);
|
||||||
|
if (buildConfiguration == null) {
|
||||||
|
_log.warning('Unknown build configuraiton $value, will assume release');
|
||||||
|
return BuildConfiguration.release;
|
||||||
|
}
|
||||||
|
return buildConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BuildEnvironment fromEnvironment({
|
||||||
|
required bool isAndroid,
|
||||||
|
}) {
|
||||||
|
final buildConfiguration =
|
||||||
|
parseBuildConfiguration(Environment.configuration);
|
||||||
|
final manifestDir = Environment.manifestDir;
|
||||||
|
final crateOptions = CargokitCrateOptions.load(
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
);
|
||||||
|
final crateInfo = CrateInfo.load(manifestDir);
|
||||||
|
return BuildEnvironment(
|
||||||
|
configuration: buildConfiguration,
|
||||||
|
crateOptions: crateOptions,
|
||||||
|
targetTempDir: Environment.targetTempDir,
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
crateInfo: crateInfo,
|
||||||
|
isAndroid: isAndroid,
|
||||||
|
androidSdkPath: isAndroid ? Environment.sdkPath : null,
|
||||||
|
androidNdkVersion: isAndroid ? Environment.ndkVersion : null,
|
||||||
|
androidMinSdkVersion:
|
||||||
|
isAndroid ? int.parse(Environment.minSdkVersion) : null,
|
||||||
|
javaHome: isAndroid ? Environment.javaHome : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RustBuilder {
|
||||||
|
final Target target;
|
||||||
|
final BuildEnvironment environment;
|
||||||
|
|
||||||
|
RustBuilder({
|
||||||
|
required this.target,
|
||||||
|
required this.environment,
|
||||||
|
});
|
||||||
|
|
||||||
|
void prepare(
|
||||||
|
Rustup rustup,
|
||||||
|
) {
|
||||||
|
final toolchain = _toolchain;
|
||||||
|
if (rustup.installedTargets(toolchain) == null) {
|
||||||
|
rustup.installToolchain(toolchain);
|
||||||
|
}
|
||||||
|
if (toolchain == 'nightly') {
|
||||||
|
rustup.installRustSrcForNightly();
|
||||||
|
}
|
||||||
|
if (!rustup.installedTargets(toolchain)!.contains(target.rust)) {
|
||||||
|
rustup.installTarget(target.rust, toolchain: toolchain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CargoBuildOptions? get _buildOptions =>
|
||||||
|
environment.crateOptions.cargo[environment.configuration];
|
||||||
|
|
||||||
|
String get _toolchain => _buildOptions?.toolchain.name ?? 'stable';
|
||||||
|
|
||||||
|
/// Returns the path of directory containing build artifacts.
|
||||||
|
Future<String> build() async {
|
||||||
|
final extraArgs = _buildOptions?.flags ?? [];
|
||||||
|
final manifestPath = path.join(environment.manifestDir, 'Cargo.toml');
|
||||||
|
runCommand(
|
||||||
|
'rustup',
|
||||||
|
[
|
||||||
|
'run',
|
||||||
|
_toolchain,
|
||||||
|
'cargo',
|
||||||
|
'build',
|
||||||
|
...extraArgs,
|
||||||
|
'--manifest-path',
|
||||||
|
manifestPath,
|
||||||
|
'-p',
|
||||||
|
environment.crateInfo.packageName,
|
||||||
|
if (!environment.configuration.isDebug) '--release',
|
||||||
|
'--target',
|
||||||
|
target.rust,
|
||||||
|
'--target-dir',
|
||||||
|
environment.targetTempDir,
|
||||||
|
],
|
||||||
|
environment: await _buildEnvironment(),
|
||||||
|
);
|
||||||
|
return path.join(
|
||||||
|
environment.targetTempDir,
|
||||||
|
target.rust,
|
||||||
|
environment.configuration.rustName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String>> _buildEnvironment() async {
|
||||||
|
if (target.android == null) {
|
||||||
|
return {};
|
||||||
|
} else {
|
||||||
|
final sdkPath = environment.androidSdkPath;
|
||||||
|
final ndkVersion = environment.androidNdkVersion;
|
||||||
|
final minSdkVersion = environment.androidMinSdkVersion;
|
||||||
|
if (sdkPath == null) {
|
||||||
|
throw BuildException('androidSdkPath is not set');
|
||||||
|
}
|
||||||
|
if (ndkVersion == null) {
|
||||||
|
throw BuildException('androidNdkVersion is not set');
|
||||||
|
}
|
||||||
|
if (minSdkVersion == null) {
|
||||||
|
throw BuildException('androidMinSdkVersion is not set');
|
||||||
|
}
|
||||||
|
final env = AndroidEnvironment(
|
||||||
|
sdkPath: sdkPath,
|
||||||
|
ndkVersion: ndkVersion,
|
||||||
|
minSdkVersion: minSdkVersion,
|
||||||
|
targetTempDir: environment.targetTempDir,
|
||||||
|
target: target,
|
||||||
|
);
|
||||||
|
if (!env.ndkIsInstalled() && environment.javaHome != null) {
|
||||||
|
env.installNdk(javaHome: environment.javaHome!);
|
||||||
|
}
|
||||||
|
return env.buildEnvironment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
rust_builder/cargokit/build_tool/lib/src/cargo.dart
Normal file
48
rust_builder/cargokit/build_tool/lib/src/cargo.dart
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:toml/toml.dart';
|
||||||
|
|
||||||
|
class ManifestException {
|
||||||
|
ManifestException(this.message, {required this.fileName});
|
||||||
|
|
||||||
|
final String? fileName;
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
if (fileName != null) {
|
||||||
|
return 'Failed to parse package manifest at $fileName: $message';
|
||||||
|
} else {
|
||||||
|
return 'Failed to parse package manifest: $message';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CrateInfo {
|
||||||
|
CrateInfo({required this.packageName});
|
||||||
|
|
||||||
|
final String packageName;
|
||||||
|
|
||||||
|
static CrateInfo parseManifest(String manifest, {final String? fileName}) {
|
||||||
|
final toml = TomlDocument.parse(manifest);
|
||||||
|
final package = toml.toMap()['package'];
|
||||||
|
if (package == null) {
|
||||||
|
throw ManifestException('Missing package section', fileName: fileName);
|
||||||
|
}
|
||||||
|
final name = package['name'];
|
||||||
|
if (name == null) {
|
||||||
|
throw ManifestException('Missing package name', fileName: fileName);
|
||||||
|
}
|
||||||
|
return CrateInfo(packageName: name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CrateInfo load(String manifestDir) {
|
||||||
|
final manifestFile = File(path.join(manifestDir, 'Cargo.toml'));
|
||||||
|
final manifest = manifestFile.readAsStringSync();
|
||||||
|
return parseManifest(manifest, fileName: manifestFile.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
rust_builder/cargokit/build_tool/lib/src/crate_hash.dart
Normal file
124
rust_builder/cargokit/build_tool/lib/src/crate_hash.dart
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:convert/convert.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
class CrateHash {
|
||||||
|
/// Computes a hash uniquely identifying crate content. This takes into account
|
||||||
|
/// content all all .rs files inside the src directory, as well as Cargo.toml,
|
||||||
|
/// Cargo.lock, build.rs and cargokit.yaml.
|
||||||
|
///
|
||||||
|
/// If [tempStorage] is provided, computed hash is stored in a file in that directory
|
||||||
|
/// and reused on subsequent calls if the crate content hasn't changed.
|
||||||
|
static String compute(String manifestDir, {String? tempStorage}) {
|
||||||
|
return CrateHash._(
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
tempStorage: tempStorage,
|
||||||
|
)._compute();
|
||||||
|
}
|
||||||
|
|
||||||
|
CrateHash._({
|
||||||
|
required this.manifestDir,
|
||||||
|
required this.tempStorage,
|
||||||
|
});
|
||||||
|
|
||||||
|
String _compute() {
|
||||||
|
final files = getFiles();
|
||||||
|
final tempStorage = this.tempStorage;
|
||||||
|
if (tempStorage != null) {
|
||||||
|
final quickHash = _computeQuickHash(files);
|
||||||
|
final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash'));
|
||||||
|
quickHashFolder.createSync(recursive: true);
|
||||||
|
final quickHashFile = File(path.join(quickHashFolder.path, quickHash));
|
||||||
|
if (quickHashFile.existsSync()) {
|
||||||
|
return quickHashFile.readAsStringSync();
|
||||||
|
}
|
||||||
|
final hash = _computeHash(files);
|
||||||
|
quickHashFile.writeAsStringSync(hash);
|
||||||
|
return hash;
|
||||||
|
} else {
|
||||||
|
return _computeHash(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes a quick hash based on files stat (without reading contents). This
|
||||||
|
/// is used to cache the real hash, which is slower to compute since it involves
|
||||||
|
/// reading every single file.
|
||||||
|
String _computeQuickHash(List<File> files) {
|
||||||
|
final output = AccumulatorSink<Digest>();
|
||||||
|
final input = sha256.startChunkedConversion(output);
|
||||||
|
|
||||||
|
final data = ByteData(8);
|
||||||
|
for (final file in files) {
|
||||||
|
input.add(utf8.encode(file.path));
|
||||||
|
final stat = file.statSync();
|
||||||
|
data.setUint64(0, stat.size);
|
||||||
|
input.add(data.buffer.asUint8List());
|
||||||
|
data.setUint64(0, stat.modified.millisecondsSinceEpoch);
|
||||||
|
input.add(data.buffer.asUint8List());
|
||||||
|
}
|
||||||
|
|
||||||
|
input.close();
|
||||||
|
return base64Url.encode(output.events.single.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _computeHash(List<File> files) {
|
||||||
|
final output = AccumulatorSink<Digest>();
|
||||||
|
final input = sha256.startChunkedConversion(output);
|
||||||
|
|
||||||
|
void addTextFile(File file) {
|
||||||
|
// text Files are hashed by lines in case we're dealing with github checkout
|
||||||
|
// that auto-converts line endings.
|
||||||
|
final splitter = LineSplitter();
|
||||||
|
if (file.existsSync()) {
|
||||||
|
final data = file.readAsStringSync();
|
||||||
|
final lines = splitter.convert(data);
|
||||||
|
for (final line in lines) {
|
||||||
|
input.add(utf8.encode(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final file in files) {
|
||||||
|
addTextFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.close();
|
||||||
|
final res = output.events.single;
|
||||||
|
|
||||||
|
// Truncate to 128bits.
|
||||||
|
final hash = res.bytes.sublist(0, 16);
|
||||||
|
return hex.encode(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<File> getFiles() {
|
||||||
|
final src = Directory(path.join(manifestDir, 'src'));
|
||||||
|
final files = src
|
||||||
|
.listSync(recursive: true, followLinks: false)
|
||||||
|
.whereType<File>()
|
||||||
|
.toList();
|
||||||
|
files.sortBy((element) => element.path);
|
||||||
|
void addFile(String relative) {
|
||||||
|
final file = File(path.join(manifestDir, relative));
|
||||||
|
if (file.existsSync()) {
|
||||||
|
files.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addFile('Cargo.toml');
|
||||||
|
addFile('Cargo.lock');
|
||||||
|
addFile('build.rs');
|
||||||
|
addFile('cargokit.yaml');
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String manifestDir;
|
||||||
|
final String? tempStorage;
|
||||||
|
}
|
||||||
68
rust_builder/cargokit/build_tool/lib/src/environment.dart
Normal file
68
rust_builder/cargokit/build_tool/lib/src/environment.dart
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
extension on String {
|
||||||
|
String resolveSymlink() => File(this).resolveSymbolicLinksSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Environment {
|
||||||
|
/// Current build configuration (debug or release).
|
||||||
|
static String get configuration =>
|
||||||
|
_getEnv("CARGOKIT_CONFIGURATION").toLowerCase();
|
||||||
|
|
||||||
|
static bool get isDebug => configuration == 'debug';
|
||||||
|
static bool get isRelease => configuration == 'release';
|
||||||
|
|
||||||
|
/// Temporary directory where Rust build artifacts are placed.
|
||||||
|
static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR");
|
||||||
|
|
||||||
|
/// Final output directory where the build artifacts are placed.
|
||||||
|
static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR');
|
||||||
|
|
||||||
|
/// Path to the crate manifest (containing Cargo.toml).
|
||||||
|
static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR');
|
||||||
|
|
||||||
|
/// Directory inside root project. Not necessarily root folder. Symlinks are
|
||||||
|
/// not resolved on purpose.
|
||||||
|
static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR');
|
||||||
|
|
||||||
|
// Pod
|
||||||
|
|
||||||
|
/// Platform name (macosx, iphoneos, iphonesimulator).
|
||||||
|
static String get darwinPlatformName =>
|
||||||
|
_getEnv("CARGOKIT_DARWIN_PLATFORM_NAME");
|
||||||
|
|
||||||
|
/// List of architectures to build for (arm64, armv7, x86_64).
|
||||||
|
static List<String> get darwinArchs =>
|
||||||
|
_getEnv("CARGOKIT_DARWIN_ARCHS").split(' ');
|
||||||
|
|
||||||
|
// Gradle
|
||||||
|
static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION");
|
||||||
|
static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION");
|
||||||
|
static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR");
|
||||||
|
static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME");
|
||||||
|
static List<String> get targetPlatforms =>
|
||||||
|
_getEnv("CARGOKIT_TARGET_PLATFORMS").split(',');
|
||||||
|
|
||||||
|
// CMAKE
|
||||||
|
static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM");
|
||||||
|
|
||||||
|
static String _getEnv(String key) {
|
||||||
|
final res = Platform.environment[key];
|
||||||
|
if (res == null) {
|
||||||
|
throw Exception("Missing environment variable $key");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _getEnvPath(String key) {
|
||||||
|
final res = _getEnv(key);
|
||||||
|
if (Directory(res).existsSync()) {
|
||||||
|
return res.resolveSymlink();
|
||||||
|
} else {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
rust_builder/cargokit/build_tool/lib/src/logging.dart
Normal file
52
rust_builder/cargokit/build_tool/lib/src/logging.dart
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
const String kSeparator = "--";
|
||||||
|
const String kDoubleSeparator = "==";
|
||||||
|
|
||||||
|
bool _lastMessageWasSeparator = false;
|
||||||
|
|
||||||
|
void _log(LogRecord rec) {
|
||||||
|
final prefix = '${rec.level.name}: ';
|
||||||
|
final out = rec.level == Level.SEVERE ? stderr : stdout;
|
||||||
|
if (rec.message == kSeparator) {
|
||||||
|
if (!_lastMessageWasSeparator) {
|
||||||
|
out.write(prefix);
|
||||||
|
out.writeln('-' * 80);
|
||||||
|
_lastMessageWasSeparator = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (rec.message == kDoubleSeparator) {
|
||||||
|
out.write(prefix);
|
||||||
|
out.writeln('=' * 80);
|
||||||
|
_lastMessageWasSeparator = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.write(prefix);
|
||||||
|
out.writeln(rec.message);
|
||||||
|
_lastMessageWasSeparator = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void initLogging() {
|
||||||
|
Logger.root.level = Level.INFO;
|
||||||
|
Logger.root.onRecord.listen((LogRecord rec) {
|
||||||
|
final lines = rec.message.split('\n');
|
||||||
|
for (final line in lines) {
|
||||||
|
if (line.isNotEmpty || lines.length == 1 || line != lines.last) {
|
||||||
|
_log(LogRecord(
|
||||||
|
rec.level,
|
||||||
|
line,
|
||||||
|
rec.loggerName,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void enableVerboseLogging() {
|
||||||
|
Logger.root.level = Level.ALL;
|
||||||
|
}
|
||||||
309
rust_builder/cargokit/build_tool/lib/src/options.dart
Normal file
309
rust_builder/cargokit/build_tool/lib/src/options.dart
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:hex/hex.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:source_span/source_span.dart';
|
||||||
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'environment.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
|
||||||
|
final _log = Logger('options');
|
||||||
|
|
||||||
|
/// A class for exceptions that have source span information attached.
|
||||||
|
class SourceSpanException implements Exception {
|
||||||
|
// This is a getter so that subclasses can override it.
|
||||||
|
/// A message describing the exception.
|
||||||
|
String get message => _message;
|
||||||
|
final String _message;
|
||||||
|
|
||||||
|
// This is a getter so that subclasses can override it.
|
||||||
|
/// The span associated with this exception.
|
||||||
|
///
|
||||||
|
/// This may be `null` if the source location can't be determined.
|
||||||
|
SourceSpan? get span => _span;
|
||||||
|
final SourceSpan? _span;
|
||||||
|
|
||||||
|
SourceSpanException(this._message, this._span);
|
||||||
|
|
||||||
|
/// Returns a string representation of `this`.
|
||||||
|
///
|
||||||
|
/// [color] may either be a [String], a [bool], or `null`. If it's a string,
|
||||||
|
/// it indicates an ANSI terminal color escape that should be used to
|
||||||
|
/// highlight the span's text. If it's `true`, it indicates that the text
|
||||||
|
/// should be highlighted using the default color. If it's `false` or `null`,
|
||||||
|
/// it indicates that the text shouldn't be highlighted.
|
||||||
|
@override
|
||||||
|
String toString({Object? color}) {
|
||||||
|
if (span == null) return message;
|
||||||
|
return 'Error on ${span!.message(message, color: color)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Toolchain {
|
||||||
|
stable,
|
||||||
|
beta,
|
||||||
|
nightly,
|
||||||
|
}
|
||||||
|
|
||||||
|
class CargoBuildOptions {
|
||||||
|
final Toolchain toolchain;
|
||||||
|
final List<String> flags;
|
||||||
|
|
||||||
|
CargoBuildOptions({
|
||||||
|
required this.toolchain,
|
||||||
|
required this.flags,
|
||||||
|
});
|
||||||
|
|
||||||
|
static Toolchain _toolchainFromNode(YamlNode node) {
|
||||||
|
if (node case YamlScalar(value: String name)) {
|
||||||
|
final toolchain =
|
||||||
|
Toolchain.values.firstWhereOrNull((element) => element.name == name);
|
||||||
|
if (toolchain != null) {
|
||||||
|
return toolchain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.',
|
||||||
|
node.span);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CargoBuildOptions parse(YamlNode node) {
|
||||||
|
if (node is! YamlMap) {
|
||||||
|
throw SourceSpanException('Cargo options must be a map', node.span);
|
||||||
|
}
|
||||||
|
Toolchain toolchain = Toolchain.stable;
|
||||||
|
List<String> flags = [];
|
||||||
|
for (final MapEntry(:key, :value) in node.nodes.entries) {
|
||||||
|
if (key case YamlScalar(value: 'toolchain')) {
|
||||||
|
toolchain = _toolchainFromNode(value);
|
||||||
|
} else if (key case YamlScalar(value: 'extra_flags')) {
|
||||||
|
if (value case YamlList(nodes: List<YamlNode> list)) {
|
||||||
|
if (list.every((element) {
|
||||||
|
if (element case YamlScalar(value: String _)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})) {
|
||||||
|
flags = list.map((e) => e.value as String).toList();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Extra flags must be a list of strings', value.span);
|
||||||
|
} else {
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown cargo option type. Must be "toolchain" or "extra_flags".',
|
||||||
|
key.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CargoBuildOptions(toolchain: toolchain, flags: flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on YamlMap {
|
||||||
|
/// Map that extracts keys so that we can do map case check on them.
|
||||||
|
Map<dynamic, YamlNode> get valueMap =>
|
||||||
|
nodes.map((key, value) => MapEntry(key.value, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrecompiledBinaries {
|
||||||
|
final String uriPrefix;
|
||||||
|
final PublicKey publicKey;
|
||||||
|
|
||||||
|
PrecompiledBinaries({
|
||||||
|
required this.uriPrefix,
|
||||||
|
required this.publicKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
static PublicKey _publicKeyFromHex(String key, SourceSpan? span) {
|
||||||
|
final bytes = HEX.decode(key);
|
||||||
|
if (bytes.length != 32) {
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Invalid public key. Must be 32 bytes long.', span);
|
||||||
|
}
|
||||||
|
return PublicKey(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PrecompiledBinaries parse(YamlNode node) {
|
||||||
|
if (node case YamlMap(valueMap: Map<dynamic, YamlNode> map)) {
|
||||||
|
if (map
|
||||||
|
case {
|
||||||
|
'url_prefix': YamlNode urlPrefixNode,
|
||||||
|
'public_key': YamlNode publicKeyNode,
|
||||||
|
}) {
|
||||||
|
final urlPrefix = switch (urlPrefixNode) {
|
||||||
|
YamlScalar(value: String urlPrefix) => urlPrefix,
|
||||||
|
_ => throw SourceSpanException(
|
||||||
|
'Invalid URL prefix value.', urlPrefixNode.span),
|
||||||
|
};
|
||||||
|
final publicKey = switch (publicKeyNode) {
|
||||||
|
YamlScalar(value: String publicKey) =>
|
||||||
|
_publicKeyFromHex(publicKey, publicKeyNode.span),
|
||||||
|
_ => throw SourceSpanException(
|
||||||
|
'Invalid public key value.', publicKeyNode.span),
|
||||||
|
};
|
||||||
|
return PrecompiledBinaries(
|
||||||
|
uriPrefix: urlPrefix,
|
||||||
|
publicKey: publicKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Invalid precompiled binaries value. '
|
||||||
|
'Expected Map with "url_prefix" and "public_key".',
|
||||||
|
node.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cargokit options specified for Rust crate.
|
||||||
|
class CargokitCrateOptions {
|
||||||
|
CargokitCrateOptions({
|
||||||
|
this.cargo = const {},
|
||||||
|
this.precompiledBinaries,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Map<BuildConfiguration, CargoBuildOptions> cargo;
|
||||||
|
final PrecompiledBinaries? precompiledBinaries;
|
||||||
|
|
||||||
|
static CargokitCrateOptions parse(YamlNode node) {
|
||||||
|
if (node is! YamlMap) {
|
||||||
|
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||||||
|
}
|
||||||
|
final options = <BuildConfiguration, CargoBuildOptions>{};
|
||||||
|
PrecompiledBinaries? precompiledBinaries;
|
||||||
|
|
||||||
|
for (final entry in node.nodes.entries) {
|
||||||
|
if (entry
|
||||||
|
case MapEntry(
|
||||||
|
key: YamlScalar(value: 'cargo'),
|
||||||
|
value: YamlNode node,
|
||||||
|
)) {
|
||||||
|
if (node is! YamlMap) {
|
||||||
|
throw SourceSpanException('Cargo options must be a map', node.span);
|
||||||
|
}
|
||||||
|
for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) {
|
||||||
|
if (key case YamlScalar(value: String name)) {
|
||||||
|
final configuration = BuildConfiguration.values
|
||||||
|
.firstWhereOrNull((element) => element.name == name);
|
||||||
|
if (configuration != null) {
|
||||||
|
options[configuration] = CargoBuildOptions.parse(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.',
|
||||||
|
key.span);
|
||||||
|
}
|
||||||
|
} else if (entry.key case YamlScalar(value: 'precompiled_binaries')) {
|
||||||
|
precompiledBinaries = PrecompiledBinaries.parse(entry.value);
|
||||||
|
} else {
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".',
|
||||||
|
entry.key.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CargokitCrateOptions(
|
||||||
|
cargo: options,
|
||||||
|
precompiledBinaries: precompiledBinaries,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CargokitCrateOptions load({
|
||||||
|
required String manifestDir,
|
||||||
|
}) {
|
||||||
|
final uri = Uri.file(path.join(manifestDir, "cargokit.yaml"));
|
||||||
|
final file = File.fromUri(uri);
|
||||||
|
if (file.existsSync()) {
|
||||||
|
final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri);
|
||||||
|
return parse(contents);
|
||||||
|
} else {
|
||||||
|
return CargokitCrateOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CargokitUserOptions {
|
||||||
|
// When Rustup is installed always build locally unless user opts into
|
||||||
|
// using precompiled binaries.
|
||||||
|
static bool defaultUsePrecompiledBinaries() {
|
||||||
|
return Rustup.executablePath() == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CargokitUserOptions({
|
||||||
|
required this.usePrecompiledBinaries,
|
||||||
|
required this.verboseLogging,
|
||||||
|
});
|
||||||
|
|
||||||
|
CargokitUserOptions._()
|
||||||
|
: usePrecompiledBinaries = defaultUsePrecompiledBinaries(),
|
||||||
|
verboseLogging = false;
|
||||||
|
|
||||||
|
static CargokitUserOptions parse(YamlNode node) {
|
||||||
|
if (node is! YamlMap) {
|
||||||
|
throw SourceSpanException('Cargokit options must be a map', node.span);
|
||||||
|
}
|
||||||
|
bool usePrecompiledBinaries = defaultUsePrecompiledBinaries();
|
||||||
|
bool verboseLogging = false;
|
||||||
|
|
||||||
|
for (final entry in node.nodes.entries) {
|
||||||
|
if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) {
|
||||||
|
if (entry.value case YamlScalar(value: bool value)) {
|
||||||
|
usePrecompiledBinaries = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Invalid value for "use_precompiled_binaries". Must be a boolean.',
|
||||||
|
entry.value.span);
|
||||||
|
} else if (entry.key case YamlScalar(value: 'verbose_logging')) {
|
||||||
|
if (entry.value case YamlScalar(value: bool value)) {
|
||||||
|
verboseLogging = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Invalid value for "verbose_logging". Must be a boolean.',
|
||||||
|
entry.value.span);
|
||||||
|
} else {
|
||||||
|
throw SourceSpanException(
|
||||||
|
'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".',
|
||||||
|
entry.key.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CargokitUserOptions(
|
||||||
|
usePrecompiledBinaries: usePrecompiledBinaries,
|
||||||
|
verboseLogging: verboseLogging,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CargokitUserOptions load() {
|
||||||
|
String fileName = "cargokit_options.yaml";
|
||||||
|
var userProjectDir = Directory(Environment.rootProjectDir);
|
||||||
|
|
||||||
|
while (userProjectDir.parent.path != userProjectDir.path) {
|
||||||
|
final configFile = File(path.join(userProjectDir.path, fileName));
|
||||||
|
if (configFile.existsSync()) {
|
||||||
|
final contents = loadYamlNode(
|
||||||
|
configFile.readAsStringSync(),
|
||||||
|
sourceUrl: configFile.uri,
|
||||||
|
);
|
||||||
|
final res = parse(contents);
|
||||||
|
if (res.verboseLogging) {
|
||||||
|
_log.info('Found user options file at ${configFile.path}');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
userProjectDir = userProjectDir.parent;
|
||||||
|
}
|
||||||
|
return CargokitUserOptions._();
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool usePrecompiledBinaries;
|
||||||
|
final bool verboseLogging;
|
||||||
|
}
|
||||||
@ -0,0 +1,202 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:github/github.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'builder.dart';
|
||||||
|
import 'cargo.dart';
|
||||||
|
import 'crate_hash.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
final _log = Logger('precompile_binaries');
|
||||||
|
|
||||||
|
class PrecompileBinaries {
|
||||||
|
PrecompileBinaries({
|
||||||
|
required this.privateKey,
|
||||||
|
required this.githubToken,
|
||||||
|
required this.repositorySlug,
|
||||||
|
required this.manifestDir,
|
||||||
|
required this.targets,
|
||||||
|
this.androidSdkLocation,
|
||||||
|
this.androidNdkVersion,
|
||||||
|
this.androidMinSdkVersion,
|
||||||
|
this.tempDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PrivateKey privateKey;
|
||||||
|
final String githubToken;
|
||||||
|
final RepositorySlug repositorySlug;
|
||||||
|
final String manifestDir;
|
||||||
|
final List<Target> targets;
|
||||||
|
final String? androidSdkLocation;
|
||||||
|
final String? androidNdkVersion;
|
||||||
|
final int? androidMinSdkVersion;
|
||||||
|
final String? tempDir;
|
||||||
|
|
||||||
|
static String fileName(Target target, String name) {
|
||||||
|
return '${target.rust}_$name';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String signatureFileName(Target target, String name) {
|
||||||
|
return '${target.rust}_$name.sig';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> run() async {
|
||||||
|
final crateInfo = CrateInfo.load(manifestDir);
|
||||||
|
|
||||||
|
final targets = List.of(this.targets);
|
||||||
|
if (targets.isEmpty) {
|
||||||
|
targets.addAll([
|
||||||
|
...Target.buildableTargets(),
|
||||||
|
if (androidSdkLocation != null) ...Target.androidTargets(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.info('Precompiling binaries for $targets');
|
||||||
|
|
||||||
|
final hash = CrateHash.compute(manifestDir);
|
||||||
|
_log.info('Computed crate hash: $hash');
|
||||||
|
|
||||||
|
final String tagName = 'precompiled_$hash';
|
||||||
|
|
||||||
|
final github = GitHub(auth: Authentication.withToken(githubToken));
|
||||||
|
final repo = github.repositories;
|
||||||
|
final release = await _getOrCreateRelease(
|
||||||
|
repo: repo,
|
||||||
|
tagName: tagName,
|
||||||
|
packageName: crateInfo.packageName,
|
||||||
|
hash: hash,
|
||||||
|
);
|
||||||
|
|
||||||
|
final tempDir = this.tempDir != null
|
||||||
|
? Directory(this.tempDir!)
|
||||||
|
: Directory.systemTemp.createTempSync('precompiled_');
|
||||||
|
|
||||||
|
tempDir.createSync(recursive: true);
|
||||||
|
|
||||||
|
final crateOptions = CargokitCrateOptions.load(
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
);
|
||||||
|
|
||||||
|
final buildEnvironment = BuildEnvironment(
|
||||||
|
configuration: BuildConfiguration.release,
|
||||||
|
crateOptions: crateOptions,
|
||||||
|
targetTempDir: tempDir.path,
|
||||||
|
manifestDir: manifestDir,
|
||||||
|
crateInfo: crateInfo,
|
||||||
|
isAndroid: androidSdkLocation != null,
|
||||||
|
androidSdkPath: androidSdkLocation,
|
||||||
|
androidNdkVersion: androidNdkVersion,
|
||||||
|
androidMinSdkVersion: androidMinSdkVersion,
|
||||||
|
);
|
||||||
|
|
||||||
|
final rustup = Rustup();
|
||||||
|
|
||||||
|
for (final target in targets) {
|
||||||
|
final artifactNames = getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: crateInfo.packageName,
|
||||||
|
remote: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (artifactNames.every((name) {
|
||||||
|
final fileName = PrecompileBinaries.fileName(target, name);
|
||||||
|
return (release.assets ?? []).any((e) => e.name == fileName);
|
||||||
|
})) {
|
||||||
|
_log.info("All artifacts for $target already exist - skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.info('Building for $target');
|
||||||
|
|
||||||
|
final builder =
|
||||||
|
RustBuilder(target: target, environment: buildEnvironment);
|
||||||
|
builder.prepare(rustup);
|
||||||
|
final res = await builder.build();
|
||||||
|
|
||||||
|
final assets = <CreateReleaseAsset>[];
|
||||||
|
for (final name in artifactNames) {
|
||||||
|
final file = File(path.join(res, name));
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
throw Exception('Missing artifact: ${file.path}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = file.readAsBytesSync();
|
||||||
|
final create = CreateReleaseAsset(
|
||||||
|
name: PrecompileBinaries.fileName(target, name),
|
||||||
|
contentType: "application/octet-stream",
|
||||||
|
assetData: data,
|
||||||
|
);
|
||||||
|
final signature = sign(privateKey, data);
|
||||||
|
final signatureCreate = CreateReleaseAsset(
|
||||||
|
name: signatureFileName(target, name),
|
||||||
|
contentType: "application/octet-stream",
|
||||||
|
assetData: signature,
|
||||||
|
);
|
||||||
|
bool verified = verify(public(privateKey), data, signature);
|
||||||
|
if (!verified) {
|
||||||
|
throw Exception('Signature verification failed');
|
||||||
|
}
|
||||||
|
assets.add(create);
|
||||||
|
assets.add(signatureCreate);
|
||||||
|
}
|
||||||
|
_log.info('Uploading assets: ${assets.map((e) => e.name)}');
|
||||||
|
for (final asset in assets) {
|
||||||
|
// This seems to be failing on CI so do it one by one
|
||||||
|
int retryCount = 0;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
await repo.uploadReleaseAssets(release, [asset]);
|
||||||
|
break;
|
||||||
|
} on Exception catch (e) {
|
||||||
|
if (retryCount == 10) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
++retryCount;
|
||||||
|
_log.shout(
|
||||||
|
'Upload failed (attempt $retryCount, will retry): ${e.toString()}');
|
||||||
|
await Future.delayed(Duration(seconds: 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.info('Cleaning up');
|
||||||
|
tempDir.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Release> _getOrCreateRelease({
|
||||||
|
required RepositoriesService repo,
|
||||||
|
required String tagName,
|
||||||
|
required String packageName,
|
||||||
|
required String hash,
|
||||||
|
}) async {
|
||||||
|
Release release;
|
||||||
|
try {
|
||||||
|
_log.info('Fetching release $tagName');
|
||||||
|
release = await repo.getReleaseByTagName(repositorySlug, tagName);
|
||||||
|
} on ReleaseNotFound {
|
||||||
|
_log.info('Release not found - creating release $tagName');
|
||||||
|
release = await repo.createRelease(
|
||||||
|
repositorySlug,
|
||||||
|
CreateRelease.from(
|
||||||
|
tagName: tagName,
|
||||||
|
name: 'Precompiled binaries ${hash.substring(0, 8)}',
|
||||||
|
targetCommitish: null,
|
||||||
|
isDraft: false,
|
||||||
|
isPrerelease: false,
|
||||||
|
body: 'Precompiled binaries for crate $packageName, '
|
||||||
|
'crate hash $hash.',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return release;
|
||||||
|
}
|
||||||
|
}
|
||||||
136
rust_builder/cargokit/build_tool/lib/src/rustup.dart
Normal file
136
rust_builder/cargokit/build_tool/lib/src/rustup.dart
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class _Toolchain {
|
||||||
|
_Toolchain(
|
||||||
|
this.name,
|
||||||
|
this.targets,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final List<String> targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Rustup {
|
||||||
|
List<String>? installedTargets(String toolchain) {
|
||||||
|
final targets = _installedTargets(toolchain);
|
||||||
|
return targets != null ? List.unmodifiable(targets) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void installToolchain(String toolchain) {
|
||||||
|
log.info("Installing Rust toolchain: $toolchain");
|
||||||
|
runCommand("rustup", ['toolchain', 'install', toolchain]);
|
||||||
|
_installedToolchains
|
||||||
|
.add(_Toolchain(toolchain, _getInstalledTargets(toolchain)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void installTarget(
|
||||||
|
String target, {
|
||||||
|
required String toolchain,
|
||||||
|
}) {
|
||||||
|
log.info("Installing Rust target: $target");
|
||||||
|
runCommand("rustup", [
|
||||||
|
'target',
|
||||||
|
'add',
|
||||||
|
'--toolchain',
|
||||||
|
toolchain,
|
||||||
|
target,
|
||||||
|
]);
|
||||||
|
_installedTargets(toolchain)?.add(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<_Toolchain> _installedToolchains;
|
||||||
|
|
||||||
|
Rustup() : _installedToolchains = _getInstalledToolchains();
|
||||||
|
|
||||||
|
List<String>? _installedTargets(String toolchain) => _installedToolchains
|
||||||
|
.firstWhereOrNull(
|
||||||
|
(e) => e.name == toolchain || e.name.startsWith('$toolchain-'))
|
||||||
|
?.targets;
|
||||||
|
|
||||||
|
static List<_Toolchain> _getInstalledToolchains() {
|
||||||
|
String extractToolchainName(String line) {
|
||||||
|
// ignore (default) after toolchain name
|
||||||
|
final parts = line.split(' ');
|
||||||
|
return parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
final res = runCommand("rustup", ['toolchain', 'list']);
|
||||||
|
|
||||||
|
// To list all non-custom toolchains, we need to filter out lines that
|
||||||
|
// don't start with "stable", "beta", or "nightly".
|
||||||
|
Pattern nonCustom = RegExp(r"^(stable|beta|nightly)");
|
||||||
|
final lines = res.stdout
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.where((e) => e.isNotEmpty && e.startsWith(nonCustom))
|
||||||
|
.map(extractToolchainName)
|
||||||
|
.toList(growable: true);
|
||||||
|
|
||||||
|
return lines
|
||||||
|
.map(
|
||||||
|
(name) => _Toolchain(
|
||||||
|
name,
|
||||||
|
_getInstalledTargets(name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> _getInstalledTargets(String toolchain) {
|
||||||
|
final res = runCommand("rustup", [
|
||||||
|
'target',
|
||||||
|
'list',
|
||||||
|
'--toolchain',
|
||||||
|
toolchain,
|
||||||
|
'--installed',
|
||||||
|
]);
|
||||||
|
final lines = res.stdout
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.where((e) => e.isNotEmpty)
|
||||||
|
.toList(growable: true);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _didInstallRustSrcForNightly = false;
|
||||||
|
|
||||||
|
void installRustSrcForNightly() {
|
||||||
|
if (_didInstallRustSrcForNightly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Useful for -Z build-std
|
||||||
|
runCommand(
|
||||||
|
"rustup",
|
||||||
|
['component', 'add', 'rust-src', '--toolchain', 'nightly'],
|
||||||
|
);
|
||||||
|
_didInstallRustSrcForNightly = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? executablePath() {
|
||||||
|
final envPath = Platform.environment['PATH'];
|
||||||
|
final envPathSeparator = Platform.isWindows ? ';' : ':';
|
||||||
|
final home = Platform.isWindows
|
||||||
|
? Platform.environment['USERPROFILE']
|
||||||
|
: Platform.environment['HOME'];
|
||||||
|
final paths = [
|
||||||
|
if (home != null) path.join(home, '.cargo', 'bin'),
|
||||||
|
if (envPath != null) ...envPath.split(envPathSeparator),
|
||||||
|
];
|
||||||
|
for (final p in paths) {
|
||||||
|
final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup';
|
||||||
|
final rustupPath = path.join(p, rustup);
|
||||||
|
if (File(rustupPath).existsSync()) {
|
||||||
|
return rustupPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
140
rust_builder/cargokit/build_tool/lib/src/target.dart
Normal file
140
rust_builder/cargokit/build_tool/lib/src/target.dart
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
class Target {
|
||||||
|
Target({
|
||||||
|
required this.rust,
|
||||||
|
this.flutter,
|
||||||
|
this.android,
|
||||||
|
this.androidMinSdkVersion,
|
||||||
|
this.darwinPlatform,
|
||||||
|
this.darwinArch,
|
||||||
|
});
|
||||||
|
|
||||||
|
static final all = [
|
||||||
|
Target(
|
||||||
|
rust: 'armv7-linux-androideabi',
|
||||||
|
flutter: 'android-arm',
|
||||||
|
android: 'armeabi-v7a',
|
||||||
|
androidMinSdkVersion: 16,
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-linux-android',
|
||||||
|
flutter: 'android-arm64',
|
||||||
|
android: 'arm64-v8a',
|
||||||
|
androidMinSdkVersion: 21,
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'i686-linux-android',
|
||||||
|
flutter: 'android-x86',
|
||||||
|
android: 'x86',
|
||||||
|
androidMinSdkVersion: 16,
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-linux-android',
|
||||||
|
flutter: 'android-x64',
|
||||||
|
android: 'x86_64',
|
||||||
|
androidMinSdkVersion: 21,
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-pc-windows-msvc',
|
||||||
|
flutter: 'windows-x64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-unknown-linux-gnu',
|
||||||
|
flutter: 'linux-x64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-unknown-linux-gnu',
|
||||||
|
flutter: 'linux-arm64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-apple-darwin',
|
||||||
|
darwinPlatform: 'macosx',
|
||||||
|
darwinArch: 'x86_64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-apple-darwin',
|
||||||
|
darwinPlatform: 'macosx',
|
||||||
|
darwinArch: 'arm64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-apple-ios',
|
||||||
|
darwinPlatform: 'iphoneos',
|
||||||
|
darwinArch: 'arm64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'aarch64-apple-ios-sim',
|
||||||
|
darwinPlatform: 'iphonesimulator',
|
||||||
|
darwinArch: 'arm64',
|
||||||
|
),
|
||||||
|
Target(
|
||||||
|
rust: 'x86_64-apple-ios',
|
||||||
|
darwinPlatform: 'iphonesimulator',
|
||||||
|
darwinArch: 'x86_64',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
static Target? forFlutterName(String flutterName) {
|
||||||
|
return all.firstWhereOrNull((element) => element.flutter == flutterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Target? forDarwin({
|
||||||
|
required String platformName,
|
||||||
|
required String darwinAarch,
|
||||||
|
}) {
|
||||||
|
return all.firstWhereOrNull((element) => //
|
||||||
|
element.darwinPlatform == platformName &&
|
||||||
|
element.darwinArch == darwinAarch);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Target? forRustTriple(String triple) {
|
||||||
|
return all.firstWhereOrNull((element) => element.rust == triple);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Target> androidTargets() {
|
||||||
|
return all
|
||||||
|
.where((element) => element.android != null)
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns buildable targets on current host platform ignoring Android targets.
|
||||||
|
static List<Target> buildableTargets() {
|
||||||
|
if (Platform.isLinux) {
|
||||||
|
// Right now we don't support cross-compiling on Linux. So we just return
|
||||||
|
// the host target.
|
||||||
|
final arch = runCommand('arch', []).stdout as String;
|
||||||
|
if (arch.trim() == 'aarch64') {
|
||||||
|
return [Target.forRustTriple('aarch64-unknown-linux-gnu')!];
|
||||||
|
} else {
|
||||||
|
return [Target.forRustTriple('x86_64-unknown-linux-gnu')!];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all.where((target) {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
return target.rust.contains('-windows-');
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
return target.darwinPlatform != null;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}).toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return rust;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? flutter;
|
||||||
|
final String rust;
|
||||||
|
final String? android;
|
||||||
|
final int? androidMinSdkVersion;
|
||||||
|
final String? darwinPlatform;
|
||||||
|
final String? darwinArch;
|
||||||
|
}
|
||||||
172
rust_builder/cargokit/build_tool/lib/src/util.dart
Normal file
172
rust_builder/cargokit/build_tool/lib/src/util.dart
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'logging.dart';
|
||||||
|
import 'rustup.dart';
|
||||||
|
|
||||||
|
final log = Logger("process");
|
||||||
|
|
||||||
|
class CommandFailedException implements Exception {
|
||||||
|
final String executable;
|
||||||
|
final List<String> arguments;
|
||||||
|
final ProcessResult result;
|
||||||
|
|
||||||
|
CommandFailedException({
|
||||||
|
required this.executable,
|
||||||
|
required this.arguments,
|
||||||
|
required this.result,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
final stdout = result.stdout.toString().trim();
|
||||||
|
final stderr = result.stderr.toString().trim();
|
||||||
|
return [
|
||||||
|
"External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}",
|
||||||
|
"Returned Exit Code: ${result.exitCode}",
|
||||||
|
kSeparator,
|
||||||
|
"STDOUT:",
|
||||||
|
if (stdout.isNotEmpty) stdout,
|
||||||
|
kSeparator,
|
||||||
|
"STDERR:",
|
||||||
|
if (stderr.isNotEmpty) stderr,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestRunCommandArgs {
|
||||||
|
final String executable;
|
||||||
|
final List<String> arguments;
|
||||||
|
final String? workingDirectory;
|
||||||
|
final Map<String, String>? environment;
|
||||||
|
final bool includeParentEnvironment;
|
||||||
|
final bool runInShell;
|
||||||
|
final Encoding? stdoutEncoding;
|
||||||
|
final Encoding? stderrEncoding;
|
||||||
|
|
||||||
|
TestRunCommandArgs({
|
||||||
|
required this.executable,
|
||||||
|
required this.arguments,
|
||||||
|
this.workingDirectory,
|
||||||
|
this.environment,
|
||||||
|
this.includeParentEnvironment = true,
|
||||||
|
this.runInShell = false,
|
||||||
|
this.stdoutEncoding,
|
||||||
|
this.stderrEncoding,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestRunCommandResult {
|
||||||
|
TestRunCommandResult({
|
||||||
|
this.pid = 1,
|
||||||
|
this.exitCode = 0,
|
||||||
|
this.stdout = '',
|
||||||
|
this.stderr = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
final int pid;
|
||||||
|
final int exitCode;
|
||||||
|
final String stdout;
|
||||||
|
final String stderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride;
|
||||||
|
|
||||||
|
ProcessResult runCommand(
|
||||||
|
String executable,
|
||||||
|
List<String> arguments, {
|
||||||
|
String? workingDirectory,
|
||||||
|
Map<String, String>? environment,
|
||||||
|
bool includeParentEnvironment = true,
|
||||||
|
bool runInShell = false,
|
||||||
|
Encoding? stdoutEncoding = systemEncoding,
|
||||||
|
Encoding? stderrEncoding = systemEncoding,
|
||||||
|
}) {
|
||||||
|
if (testRunCommandOverride != null) {
|
||||||
|
final result = testRunCommandOverride!(TestRunCommandArgs(
|
||||||
|
executable: executable,
|
||||||
|
arguments: arguments,
|
||||||
|
workingDirectory: workingDirectory,
|
||||||
|
environment: environment,
|
||||||
|
includeParentEnvironment: includeParentEnvironment,
|
||||||
|
runInShell: runInShell,
|
||||||
|
stdoutEncoding: stdoutEncoding,
|
||||||
|
stderrEncoding: stderrEncoding,
|
||||||
|
));
|
||||||
|
return ProcessResult(
|
||||||
|
result.pid,
|
||||||
|
result.exitCode,
|
||||||
|
result.stdout,
|
||||||
|
result.stderr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.finer('Running command $executable ${arguments.join(' ')}');
|
||||||
|
final res = Process.runSync(
|
||||||
|
_resolveExecutable(executable),
|
||||||
|
arguments,
|
||||||
|
workingDirectory: workingDirectory,
|
||||||
|
environment: environment,
|
||||||
|
includeParentEnvironment: includeParentEnvironment,
|
||||||
|
runInShell: runInShell,
|
||||||
|
stderrEncoding: stderrEncoding,
|
||||||
|
stdoutEncoding: stdoutEncoding,
|
||||||
|
);
|
||||||
|
if (res.exitCode != 0) {
|
||||||
|
throw CommandFailedException(
|
||||||
|
executable: executable,
|
||||||
|
arguments: arguments,
|
||||||
|
result: res,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RustupNotFoundException implements Exception {
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return [
|
||||||
|
' ',
|
||||||
|
'rustup not found in PATH.',
|
||||||
|
' ',
|
||||||
|
'Maybe you need to install Rust? It only takes a minute:',
|
||||||
|
' ',
|
||||||
|
if (Platform.isWindows) 'https://www.rust-lang.org/tools/install',
|
||||||
|
if (hasHomebrewRustInPath()) ...[
|
||||||
|
'\$ brew unlink rust # Unlink homebrew Rust from PATH',
|
||||||
|
],
|
||||||
|
if (!Platform.isWindows)
|
||||||
|
"\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh",
|
||||||
|
' ',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hasHomebrewRustInPath() {
|
||||||
|
if (!Platform.isMacOS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final envPath = Platform.environment['PATH'] ?? '';
|
||||||
|
final paths = envPath.split(':');
|
||||||
|
return paths.any((p) {
|
||||||
|
return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _resolveExecutable(String executable) {
|
||||||
|
if (executable == 'rustup') {
|
||||||
|
final resolved = Rustup.executablePath();
|
||||||
|
if (resolved != null) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
throw RustupNotFoundException();
|
||||||
|
} else {
|
||||||
|
return executable;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:ed25519_edwards/ed25519_edwards.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
|
||||||
|
import 'artifacts_provider.dart';
|
||||||
|
import 'cargo.dart';
|
||||||
|
import 'crate_hash.dart';
|
||||||
|
import 'options.dart';
|
||||||
|
import 'precompile_binaries.dart';
|
||||||
|
import 'target.dart';
|
||||||
|
|
||||||
|
class VerifyBinaries {
|
||||||
|
VerifyBinaries({
|
||||||
|
required this.manifestDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String manifestDir;
|
||||||
|
|
||||||
|
Future<void> run() async {
|
||||||
|
final crateInfo = CrateInfo.load(manifestDir);
|
||||||
|
|
||||||
|
final config = CargokitCrateOptions.load(manifestDir: manifestDir);
|
||||||
|
final precompiledBinaries = config.precompiledBinaries;
|
||||||
|
if (precompiledBinaries == null) {
|
||||||
|
stdout.writeln('Crate does not support precompiled binaries.');
|
||||||
|
} else {
|
||||||
|
final crateHash = CrateHash.compute(manifestDir);
|
||||||
|
stdout.writeln('Crate hash: $crateHash');
|
||||||
|
|
||||||
|
for (final target in Target.all) {
|
||||||
|
final message = 'Checking ${target.rust}...';
|
||||||
|
stdout.write(message.padRight(40));
|
||||||
|
stdout.flush();
|
||||||
|
|
||||||
|
final artifacts = getArtifactNames(
|
||||||
|
target: target,
|
||||||
|
libraryName: crateInfo.packageName,
|
||||||
|
remote: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final prefix = precompiledBinaries.uriPrefix;
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
|
||||||
|
for (final artifact in artifacts) {
|
||||||
|
final fileName = PrecompileBinaries.fileName(target, artifact);
|
||||||
|
final signatureFileName =
|
||||||
|
PrecompileBinaries.signatureFileName(target, artifact);
|
||||||
|
|
||||||
|
final url = Uri.parse('$prefix$crateHash/$fileName');
|
||||||
|
final signatureUrl =
|
||||||
|
Uri.parse('$prefix$crateHash/$signatureFileName');
|
||||||
|
|
||||||
|
final signature = await get(signatureUrl);
|
||||||
|
if (signature.statusCode != 200) {
|
||||||
|
stdout.writeln('MISSING');
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
final asset = await get(url);
|
||||||
|
if (asset.statusCode != 200) {
|
||||||
|
stdout.writeln('MISSING');
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verify(precompiledBinaries.publicKey, asset.bodyBytes,
|
||||||
|
signature.bodyBytes)) {
|
||||||
|
stdout.writeln('INVALID SIGNATURE');
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
stdout.writeln('OK');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
453
rust_builder/cargokit/build_tool/pubspec.lock
Normal file
453
rust_builder/cargokit/build_tool/pubspec.lock
Normal file
@ -0,0 +1,453 @@
|
|||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
_fe_analyzer_shared:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _fe_analyzer_shared
|
||||||
|
sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "64.0.0"
|
||||||
|
adaptive_number:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: adaptive_number
|
||||||
|
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
analyzer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: analyzer
|
||||||
|
sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.2.0"
|
||||||
|
args:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
collection:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.18.0"
|
||||||
|
convert:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: convert
|
||||||
|
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
|
coverage:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: coverage
|
||||||
|
sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.3"
|
||||||
|
crypto:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
|
ed25519_edwards:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: ed25519_edwards
|
||||||
|
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.1"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.4"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
frontend_server_client:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: frontend_server_client
|
||||||
|
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.0"
|
||||||
|
github:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: github
|
||||||
|
sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.17.0"
|
||||||
|
glob:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: glob
|
||||||
|
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
hex:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: hex
|
||||||
|
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
|
http:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
http_multi_server:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_multi_server
|
||||||
|
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.2"
|
||||||
|
io:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: io
|
||||||
|
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
js:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: js
|
||||||
|
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.7"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.8.1"
|
||||||
|
lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
logging:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: logging
|
||||||
|
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.16"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
node_preamble:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: node_preamble
|
||||||
|
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
|
path:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.8.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.4.0"
|
||||||
|
pool:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pool
|
||||||
|
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.1"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
shelf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf
|
||||||
|
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
shelf_packages_handler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_packages_handler
|
||||||
|
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
shelf_static:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_static
|
||||||
|
sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
shelf_web_socket:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shelf_web_socket
|
||||||
|
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
source_map_stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_map_stack_trace
|
||||||
|
sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
|
source_maps:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_maps
|
||||||
|
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.12"
|
||||||
|
source_span:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.0"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.11.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: test
|
||||||
|
sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.24.6"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.1"
|
||||||
|
test_core:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_core
|
||||||
|
sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.6"
|
||||||
|
toml:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: toml
|
||||||
|
sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.14.0"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.2"
|
||||||
|
version:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: version
|
||||||
|
sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.9.0"
|
||||||
|
watcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: watcher
|
||||||
|
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
web_socket_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web_socket_channel
|
||||||
|
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
webkit_inspection_protocol:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webkit_inspection_protocol
|
||||||
|
sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
|
yaml:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.0.0 <4.0.0"
|
||||||
33
rust_builder/cargokit/build_tool/pubspec.yaml
Normal file
33
rust_builder/cargokit/build_tool/pubspec.yaml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
name: build_tool
|
||||||
|
description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build.
|
||||||
|
publish_to: none
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
|
||||||
|
# Add regular dependencies here.
|
||||||
|
dependencies:
|
||||||
|
# these are pinned on purpose because the bundle_tool_runner doesn't have
|
||||||
|
# pubspec.lock. See run_build_tool.sh
|
||||||
|
logging: 1.2.0
|
||||||
|
path: 1.8.0
|
||||||
|
version: 3.0.0
|
||||||
|
collection: 1.18.0
|
||||||
|
ed25519_edwards: 0.3.1
|
||||||
|
hex: 0.2.0
|
||||||
|
yaml: 3.1.2
|
||||||
|
source_span: 1.10.0
|
||||||
|
github: 9.17.0
|
||||||
|
args: 2.4.2
|
||||||
|
crypto: 3.0.3
|
||||||
|
convert: 3.1.1
|
||||||
|
http: 1.1.0
|
||||||
|
toml: 0.14.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
lints: ^2.1.0
|
||||||
|
test: ^1.24.0
|
||||||
99
rust_builder/cargokit/cmake/cargokit.cmake
Normal file
99
rust_builder/cargokit/cmake/cargokit.cmake
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||||
|
|
||||||
|
# Workaround for https://github.com/dart-lang/pub/issues/4010
|
||||||
|
get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH)
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
# REALPATH does not properly resolve symlinks on windows :-/
|
||||||
|
execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
# - target: CMAKE target to which rust library is linked
|
||||||
|
# - manifest_dir: relative path from current folder to directory containing cargo manifest
|
||||||
|
# - lib_name: cargo package name
|
||||||
|
# - any_symbol_name: name of any exported symbol from the library.
|
||||||
|
# used on windows to force linking with library.
|
||||||
|
function(apply_cargokit target manifest_dir lib_name any_symbol_name)
|
||||||
|
|
||||||
|
set(CARGOKIT_LIB_NAME "${lib_name}")
|
||||||
|
set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}")
|
||||||
|
if (CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>")
|
||||||
|
set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/${CARGOKIT_LIB_FULL_NAME}")
|
||||||
|
else()
|
||||||
|
set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}")
|
||||||
|
set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}")
|
||||||
|
endif()
|
||||||
|
set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build")
|
||||||
|
|
||||||
|
if (FLUTTER_TARGET_PLATFORM)
|
||||||
|
set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}")
|
||||||
|
else()
|
||||||
|
set(CARGOKIT_TARGET_PLATFORM "windows-x64")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(CARGOKIT_ENV
|
||||||
|
"CARGOKIT_CMAKE=${CMAKE_COMMAND}"
|
||||||
|
"CARGOKIT_CONFIGURATION=$<CONFIG>"
|
||||||
|
"CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}"
|
||||||
|
"CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}"
|
||||||
|
"CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}"
|
||||||
|
"CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}"
|
||||||
|
"CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool"
|
||||||
|
"CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
set(SCRIPT_EXTENSION ".cmd")
|
||||||
|
set(IMPORT_LIB_EXTENSION ".lib")
|
||||||
|
else()
|
||||||
|
set(SCRIPT_EXTENSION ".sh")
|
||||||
|
set(IMPORT_LIB_EXTENSION "")
|
||||||
|
execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Using generators in custom command is only supported in CMake 3.20+
|
||||||
|
if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0")
|
||||||
|
foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}"
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/_phony_"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV}
|
||||||
|
"${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
endforeach()
|
||||||
|
else()
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT
|
||||||
|
${OUTPUT_LIB}
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/_phony_"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV}
|
||||||
|
"${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE)
|
||||||
|
|
||||||
|
if (TARGET ${target})
|
||||||
|
# If we have actual cmake target provided create target and make existing
|
||||||
|
# target depend on it
|
||||||
|
add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB})
|
||||||
|
add_dependencies("${target}" "${target}_cargokit")
|
||||||
|
target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}")
|
||||||
|
if(WIN32)
|
||||||
|
target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}")
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
# Otherwise (FFI) just use ALL to force building always
|
||||||
|
add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Allow adding the output library to plugin bundled libraries
|
||||||
|
set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE)
|
||||||
|
|
||||||
|
endfunction()
|
||||||
34
rust_builder/cargokit/cmake/resolve_symlinks.ps1
Normal file
34
rust_builder/cargokit/cmake/resolve_symlinks.ps1
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
function Resolve-Symlinks {
|
||||||
|
[CmdletBinding()]
|
||||||
|
[OutputType([string])]
|
||||||
|
param(
|
||||||
|
[Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
|
||||||
|
[string] $Path
|
||||||
|
)
|
||||||
|
|
||||||
|
[string] $separator = '/'
|
||||||
|
[string[]] $parts = $Path.Split($separator)
|
||||||
|
|
||||||
|
[string] $realPath = ''
|
||||||
|
foreach ($part in $parts) {
|
||||||
|
if ($realPath -and !$realPath.EndsWith($separator)) {
|
||||||
|
$realPath += $separator
|
||||||
|
}
|
||||||
|
|
||||||
|
$realPath += $part.Replace('\', '/')
|
||||||
|
|
||||||
|
# The slash is important when using Get-Item on Drive letters in pwsh.
|
||||||
|
if (-not($realPath.Contains($separator)) -and $realPath.EndsWith(':')) {
|
||||||
|
$realPath += '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = Get-Item $realPath
|
||||||
|
if ($item.LinkTarget) {
|
||||||
|
$realPath = $item.LinkTarget.Replace('\', '/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$realPath
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = Resolve-Symlinks -Path $args[0]
|
||||||
|
Write-Host $path
|
||||||
179
rust_builder/cargokit/gradle/plugin.gradle
Normal file
179
rust_builder/cargokit/gradle/plugin.gradle
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
/// This is copied from Cargokit (which is the official way to use it currently)
|
||||||
|
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
|
||||||
|
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
|
|
||||||
|
CargoKitPlugin.file = buildscript.sourceFile
|
||||||
|
|
||||||
|
apply plugin: CargoKitPlugin
|
||||||
|
|
||||||
|
class CargoKitExtension {
|
||||||
|
String manifestDir; // Relative path to folder containing Cargo.toml
|
||||||
|
String libname; // Library name within Cargo.toml. Must be a cdylib
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class CargoKitBuildTask extends DefaultTask {
|
||||||
|
|
||||||
|
@Input
|
||||||
|
String buildMode
|
||||||
|
|
||||||
|
@Input
|
||||||
|
String buildDir
|
||||||
|
|
||||||
|
@Input
|
||||||
|
String outputDir
|
||||||
|
|
||||||
|
@Input
|
||||||
|
String ndkVersion
|
||||||
|
|
||||||
|
@Input
|
||||||
|
String sdkDirectory
|
||||||
|
|
||||||
|
@Input
|
||||||
|
int compileSdkVersion;
|
||||||
|
|
||||||
|
@Input
|
||||||
|
int minSdkVersion;
|
||||||
|
|
||||||
|
@Input
|
||||||
|
String pluginFile
|
||||||
|
|
||||||
|
@Input
|
||||||
|
List<String> targetPlatforms
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
def build() {
|
||||||
|
if (project.cargokit.manifestDir == null) {
|
||||||
|
throw new GradleException("Property 'manifestDir' must be set on cargokit extension");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.cargokit.libname == null) {
|
||||||
|
throw new GradleException("Property 'libname' must be set on cargokit extension");
|
||||||
|
}
|
||||||
|
|
||||||
|
def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh"
|
||||||
|
def path = Paths.get(new File(pluginFile).parent, "..", executableName);
|
||||||
|
|
||||||
|
def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir)
|
||||||
|
|
||||||
|
def rootProjectDir = project.rootProject.projectDir
|
||||||
|
|
||||||
|
if (!Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||||
|
project.exec {
|
||||||
|
commandLine 'chmod', '+x', path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
project.exec {
|
||||||
|
executable path
|
||||||
|
args "build-gradle"
|
||||||
|
environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir
|
||||||
|
environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool"
|
||||||
|
environment "CARGOKIT_MANIFEST_DIR", manifestDir
|
||||||
|
environment "CARGOKIT_CONFIGURATION", buildMode
|
||||||
|
environment "CARGOKIT_TARGET_TEMP_DIR", buildDir
|
||||||
|
environment "CARGOKIT_OUTPUT_DIR", outputDir
|
||||||
|
environment "CARGOKIT_NDK_VERSION", ndkVersion
|
||||||
|
environment "CARGOKIT_SDK_DIR", sdkDirectory
|
||||||
|
environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion
|
||||||
|
environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion
|
||||||
|
environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",")
|
||||||
|
environment "CARGOKIT_JAVA_HOME", System.properties['java.home']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CargoKitPlugin implements Plugin<Project> {
|
||||||
|
|
||||||
|
static String file;
|
||||||
|
|
||||||
|
private Plugin findFlutterPlugin(Project rootProject) {
|
||||||
|
_findFlutterPlugin(rootProject.childProjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
private Plugin _findFlutterPlugin(Map projects) {
|
||||||
|
for (project in projects) {
|
||||||
|
for (plugin in project.value.getPlugins()) {
|
||||||
|
if (plugin.class.name == "com.flutter.gradle.FlutterPlugin") {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def plugin = _findFlutterPlugin(project.value.childProjects);
|
||||||
|
if (plugin != null) {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void apply(Project project) {
|
||||||
|
def plugin = findFlutterPlugin(project.rootProject);
|
||||||
|
|
||||||
|
project.extensions.create("cargokit", CargoKitExtension)
|
||||||
|
|
||||||
|
if (plugin == null) {
|
||||||
|
print("Flutter plugin not found, CargoKit plugin will not be applied.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
def cargoBuildDir = "${project.buildDir}/build"
|
||||||
|
|
||||||
|
// Determine if the project is an application or library
|
||||||
|
def isApplication = plugin.project.plugins.hasPlugin('com.android.application')
|
||||||
|
def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants
|
||||||
|
|
||||||
|
variants.all { variant ->
|
||||||
|
|
||||||
|
final buildType = variant.buildType.name
|
||||||
|
|
||||||
|
def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}";
|
||||||
|
def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs;
|
||||||
|
jniLibs.srcDir(new File(cargoOutputDir))
|
||||||
|
|
||||||
|
def platforms = com.flutter.gradle.FlutterPluginUtils.getTargetPlatforms(project).collect()
|
||||||
|
|
||||||
|
// Same thing addFlutterDependencies does in flutter.gradle
|
||||||
|
if (buildType == "debug") {
|
||||||
|
platforms.add("android-x86")
|
||||||
|
platforms.add("android-x64")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The task name depends on plugin properties, which are not available
|
||||||
|
// at this point
|
||||||
|
project.getGradle().afterProject {
|
||||||
|
def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}";
|
||||||
|
|
||||||
|
if (project.tasks.findByName(taskName)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.project.android.ndkVersion == null) {
|
||||||
|
throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
def task = project.tasks.create(taskName, CargoKitBuildTask.class) {
|
||||||
|
buildMode = variant.buildType.name
|
||||||
|
buildDir = cargoBuildDir
|
||||||
|
outputDir = cargoOutputDir
|
||||||
|
ndkVersion = plugin.project.android.ndkVersion
|
||||||
|
sdkDirectory = plugin.project.android.sdkDirectory
|
||||||
|
minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int
|
||||||
|
compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int
|
||||||
|
targetPlatforms = platforms
|
||||||
|
pluginFile = CargoKitPlugin.file
|
||||||
|
}
|
||||||
|
def onTask = { newTask ->
|
||||||
|
if (newTask.name == "merge${buildType.capitalize()}NativeLibs") {
|
||||||
|
newTask.dependsOn task
|
||||||
|
// Fix gradle 7.4.2 not picking up JNI library changes
|
||||||
|
newTask.outputs.upToDateWhen { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
project.tasks.each onTask
|
||||||
|
project.tasks.whenTaskAdded onTask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
rust_builder/cargokit/run_build_tool.cmd
Executable file
91
rust_builder/cargokit/run_build_tool.cmd
Executable file
@ -0,0 +1,91 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
setlocal ENABLEDELAYEDEXPANSION
|
||||||
|
|
||||||
|
SET BASEDIR=%~dp0
|
||||||
|
|
||||||
|
if not exist "%CARGOKIT_TOOL_TEMP_DIR%" (
|
||||||
|
mkdir "%CARGOKIT_TOOL_TEMP_DIR%"
|
||||||
|
)
|
||||||
|
cd /D "%CARGOKIT_TOOL_TEMP_DIR%"
|
||||||
|
|
||||||
|
SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool
|
||||||
|
SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart
|
||||||
|
|
||||||
|
set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/%
|
||||||
|
|
||||||
|
(
|
||||||
|
echo name: build_tool_runner
|
||||||
|
echo version: 1.0.0
|
||||||
|
echo publish_to: none
|
||||||
|
echo.
|
||||||
|
echo environment:
|
||||||
|
echo sdk: '^>=3.0.0 ^<4.0.0'
|
||||||
|
echo.
|
||||||
|
echo dependencies:
|
||||||
|
echo build_tool:
|
||||||
|
echo path: %BUILD_TOOL_PKG_DIR_POSIX%
|
||||||
|
) >pubspec.yaml
|
||||||
|
|
||||||
|
if not exist bin (
|
||||||
|
mkdir bin
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
echo import 'package:build_tool/build_tool.dart' as build_tool;
|
||||||
|
echo void main^(List^<String^> args^) ^{
|
||||||
|
echo build_tool.runMain^(args^);
|
||||||
|
echo ^}
|
||||||
|
) >bin\build_tool_runner.dart
|
||||||
|
|
||||||
|
SET PRECOMPILED=bin\build_tool_runner.dill
|
||||||
|
|
||||||
|
REM To detect changes in package we compare output of DIR /s (recursive)
|
||||||
|
set PREV_PACKAGE_INFO=.dart_tool\package_info.prev
|
||||||
|
set CUR_PACKAGE_INFO=.dart_tool\package_info.cur
|
||||||
|
|
||||||
|
DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig"
|
||||||
|
|
||||||
|
REM Last line in dir output is free space on harddrive. That is bound to
|
||||||
|
REM change between invocation so we need to remove it
|
||||||
|
(
|
||||||
|
Set "Line="
|
||||||
|
For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do (
|
||||||
|
SetLocal EnableDelayedExpansion
|
||||||
|
If Defined Line Echo !Line!
|
||||||
|
EndLocal
|
||||||
|
Set "Line=%%A")
|
||||||
|
) >"%CUR_PACKAGE_INFO%"
|
||||||
|
DEL "%CUR_PACKAGE_INFO%_orig"
|
||||||
|
|
||||||
|
REM Compare current directory listing with previous
|
||||||
|
FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1
|
||||||
|
|
||||||
|
If %ERRORLEVEL% neq 0 (
|
||||||
|
REM Changed - copy current to previous and remove precompiled kernel
|
||||||
|
if exist "%PREV_PACKAGE_INFO%" (
|
||||||
|
DEL "%PREV_PACKAGE_INFO%"
|
||||||
|
)
|
||||||
|
MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%"
|
||||||
|
if exist "%PRECOMPILED%" (
|
||||||
|
DEL "%PRECOMPILED%"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO%
|
||||||
|
REM which means we need to do pub get and precompile
|
||||||
|
if not exist "%PRECOMPILED%" (
|
||||||
|
echo Running pub get in "%cd%"
|
||||||
|
"%DART%" pub get --no-precompile
|
||||||
|
"%DART%" compile kernel bin/build_tool_runner.dart
|
||||||
|
)
|
||||||
|
|
||||||
|
"%DART%" "%PRECOMPILED%" %*
|
||||||
|
|
||||||
|
REM 253 means invalid snapshot version.
|
||||||
|
If %ERRORLEVEL% equ 253 (
|
||||||
|
"%DART%" pub get --no-precompile
|
||||||
|
"%DART%" compile kernel bin/build_tool_runner.dart
|
||||||
|
"%DART%" "%PRECOMPILED%" %*
|
||||||
|
)
|
||||||
99
rust_builder/cargokit/run_build_tool.sh
Executable file
99
rust_builder/cargokit/run_build_tool.sh
Executable file
@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BASEDIR=$(dirname "$0")
|
||||||
|
|
||||||
|
mkdir -p "$CARGOKIT_TOOL_TEMP_DIR"
|
||||||
|
|
||||||
|
cd "$CARGOKIT_TOOL_TEMP_DIR"
|
||||||
|
|
||||||
|
# Write a very simple bin package in temp folder that depends on build_tool package
|
||||||
|
# from Cargokit. This is done to ensure that we don't pollute Cargokit folder
|
||||||
|
# with .dart_tool contents.
|
||||||
|
|
||||||
|
BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool"
|
||||||
|
|
||||||
|
if [[ -z $FLUTTER_ROOT ]]; then # not defined
|
||||||
|
DART=dart
|
||||||
|
else
|
||||||
|
DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat << EOF > "pubspec.yaml"
|
||||||
|
name: build_tool_runner
|
||||||
|
version: 1.0.0
|
||||||
|
publish_to: none
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
build_tool:
|
||||||
|
path: "$BUILD_TOOL_PKG_DIR"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mkdir -p "bin"
|
||||||
|
|
||||||
|
cat << EOF > "bin/build_tool_runner.dart"
|
||||||
|
import 'package:build_tool/build_tool.dart' as build_tool;
|
||||||
|
void main(List<String> args) {
|
||||||
|
build_tool.runMain(args);
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create alias for `shasum` if it does not exist and `sha1sum` exists
|
||||||
|
if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then
|
||||||
|
shopt -s expand_aliases
|
||||||
|
alias shasum="sha1sum"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Dart run will not cache any package that has a path dependency, which
|
||||||
|
# is the case for our build_tool_runner. So instead we precompile the package
|
||||||
|
# ourselves.
|
||||||
|
# To invalidate the cached kernel we use the hash of ls -LR of the build_tool
|
||||||
|
# package directory. This should be good enough, as the build_tool package
|
||||||
|
# itself is not meant to have any path dependencies.
|
||||||
|
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum)
|
||||||
|
else
|
||||||
|
PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum)
|
||||||
|
fi
|
||||||
|
|
||||||
|
PACKAGE_HASH_FILE=".package_hash"
|
||||||
|
|
||||||
|
if [ -f "$PACKAGE_HASH_FILE" ]; then
|
||||||
|
EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE")
|
||||||
|
if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then
|
||||||
|
rm "$PACKAGE_HASH_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run pub get if needed.
|
||||||
|
if [ ! -f "$PACKAGE_HASH_FILE" ]; then
|
||||||
|
"$DART" pub get --no-precompile
|
||||||
|
"$DART" compile kernel bin/build_tool_runner.dart
|
||||||
|
echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rebuild the tool if it was deleted by Android Studio
|
||||||
|
if [ ! -f "bin/build_tool_runner.dill" ]; then
|
||||||
|
"$DART" compile kernel bin/build_tool_runner.dart
|
||||||
|
fi
|
||||||
|
|
||||||
|
set +e
|
||||||
|
|
||||||
|
"$DART" bin/build_tool_runner.dill "$@"
|
||||||
|
|
||||||
|
exit_code=$?
|
||||||
|
|
||||||
|
# 253 means invalid snapshot version.
|
||||||
|
if [ $exit_code == 253 ]; then
|
||||||
|
"$DART" pub get --no-precompile
|
||||||
|
"$DART" compile kernel bin/build_tool_runner.dart
|
||||||
|
"$DART" bin/build_tool_runner.dill "$@"
|
||||||
|
exit_code=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $exit_code
|
||||||
1
rust_builder/ios/Classes/dummy_file.c
Normal file
1
rust_builder/ios/Classes/dummy_file.c
Normal file
@ -0,0 +1 @@
|
|||||||
|
// This is an empty file to force CocoaPods to create a framework.
|
||||||
45
rust_builder/ios/rust_lib_abawo_bt_app.podspec
Normal file
45
rust_builder/ios/rust_lib_abawo_bt_app.podspec
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#
|
||||||
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||||
|
# Run `pod lib lint rust_lib_abawo_bt_app.podspec` to validate before publishing.
|
||||||
|
#
|
||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'rust_lib_abawo_bt_app'
|
||||||
|
s.version = '0.0.1'
|
||||||
|
s.summary = 'A new Flutter FFI plugin project.'
|
||||||
|
s.description = <<-DESC
|
||||||
|
A new Flutter FFI plugin project.
|
||||||
|
DESC
|
||||||
|
s.homepage = 'http://example.com'
|
||||||
|
s.license = { :file => '../LICENSE' }
|
||||||
|
s.author = { 'Your Company' => 'email@example.com' }
|
||||||
|
|
||||||
|
# This will ensure the source files in Classes/ are included in the native
|
||||||
|
# builds of apps using this FFI plugin. Podspec does not support relative
|
||||||
|
# paths, so Classes contains a forwarder C file that relatively imports
|
||||||
|
# `../src/*` so that the C sources can be shared among all target platforms.
|
||||||
|
s.source = { :path => '.' }
|
||||||
|
s.source_files = 'Classes/**/*'
|
||||||
|
s.dependency 'Flutter'
|
||||||
|
s.platform = :ios, '11.0'
|
||||||
|
|
||||||
|
# Flutter.framework does not contain a i386 slice.
|
||||||
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
|
s.swift_version = '5.0'
|
||||||
|
|
||||||
|
s.script_phase = {
|
||||||
|
:name => 'Build Rust library',
|
||||||
|
# First argument is relative path to the `rust` folder, second is name of rust library
|
||||||
|
:script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../rust rust_lib_abawo_bt_app',
|
||||||
|
:execution_position => :before_compile,
|
||||||
|
:input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'],
|
||||||
|
# Let XCode know that the static library referenced in -force_load below is
|
||||||
|
# created by this build step.
|
||||||
|
:output_files => ["${BUILT_PRODUCTS_DIR}/librust_lib_abawo_bt_app.a"],
|
||||||
|
}
|
||||||
|
s.pod_target_xcconfig = {
|
||||||
|
'DEFINES_MODULE' => 'YES',
|
||||||
|
# Flutter.framework does not contain a i386 slice.
|
||||||
|
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
|
||||||
|
'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_lib_abawo_bt_app.a',
|
||||||
|
}
|
||||||
|
end
|
||||||
19
rust_builder/linux/CMakeLists.txt
Normal file
19
rust_builder/linux/CMakeLists.txt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# The Flutter tooling requires that developers have CMake 3.10 or later
|
||||||
|
# installed. You should not increase this version, as doing so will cause
|
||||||
|
# the plugin to fail to compile for some customers of the plugin.
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
|
# Project-level configuration.
|
||||||
|
set(PROJECT_NAME "rust_lib_abawo_bt_app")
|
||||||
|
project(${PROJECT_NAME} LANGUAGES CXX)
|
||||||
|
|
||||||
|
include("../cargokit/cmake/cargokit.cmake")
|
||||||
|
apply_cargokit(${PROJECT_NAME} ../../rust rust_lib_abawo_bt_app "")
|
||||||
|
|
||||||
|
# List of absolute paths to libraries that should be bundled with the plugin.
|
||||||
|
# This list could contain prebuilt libraries, or libraries created by an
|
||||||
|
# external build triggered from this build file.
|
||||||
|
set(rust_lib_abawo_bt_app_bundled_libraries
|
||||||
|
"${${PROJECT_NAME}_cargokit_lib}"
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
1
rust_builder/macos/Classes/dummy_file.c
Normal file
1
rust_builder/macos/Classes/dummy_file.c
Normal file
@ -0,0 +1 @@
|
|||||||
|
// This is an empty file to force CocoaPods to create a framework.
|
||||||
44
rust_builder/macos/rust_lib_abawo_bt_app.podspec
Normal file
44
rust_builder/macos/rust_lib_abawo_bt_app.podspec
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#
|
||||||
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||||
|
# Run `pod lib lint rust_lib_abawo_bt_app.podspec` to validate before publishing.
|
||||||
|
#
|
||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'rust_lib_abawo_bt_app'
|
||||||
|
s.version = '0.0.1'
|
||||||
|
s.summary = 'A new Flutter FFI plugin project.'
|
||||||
|
s.description = <<-DESC
|
||||||
|
A new Flutter FFI plugin project.
|
||||||
|
DESC
|
||||||
|
s.homepage = 'http://example.com'
|
||||||
|
s.license = { :file => '../LICENSE' }
|
||||||
|
s.author = { 'Your Company' => 'email@example.com' }
|
||||||
|
|
||||||
|
# This will ensure the source files in Classes/ are included in the native
|
||||||
|
# builds of apps using this FFI plugin. Podspec does not support relative
|
||||||
|
# paths, so Classes contains a forwarder C file that relatively imports
|
||||||
|
# `../src/*` so that the C sources can be shared among all target platforms.
|
||||||
|
s.source = { :path => '.' }
|
||||||
|
s.source_files = 'Classes/**/*'
|
||||||
|
s.dependency 'FlutterMacOS'
|
||||||
|
|
||||||
|
s.platform = :osx, '10.11'
|
||||||
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||||
|
s.swift_version = '5.0'
|
||||||
|
|
||||||
|
s.script_phase = {
|
||||||
|
:name => 'Build Rust library',
|
||||||
|
# First argument is relative path to the `rust` folder, second is name of rust library
|
||||||
|
:script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../../rust rust_lib_abawo_bt_app',
|
||||||
|
:execution_position => :before_compile,
|
||||||
|
:input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'],
|
||||||
|
# Let XCode know that the static library referenced in -force_load below is
|
||||||
|
# created by this build step.
|
||||||
|
:output_files => ["${BUILT_PRODUCTS_DIR}/librust_lib_abawo_bt_app.a"],
|
||||||
|
}
|
||||||
|
s.pod_target_xcconfig = {
|
||||||
|
'DEFINES_MODULE' => 'YES',
|
||||||
|
# Flutter.framework does not contain a i386 slice.
|
||||||
|
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
|
||||||
|
'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/librust_lib_abawo_bt_app.a',
|
||||||
|
}
|
||||||
|
end
|
||||||
34
rust_builder/pubspec.yaml
Normal file
34
rust_builder/pubspec.yaml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: rust_lib_abawo_bt_app
|
||||||
|
description: "Utility to build Rust code"
|
||||||
|
version: 0.0.1
|
||||||
|
publish_to: none
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
flutter: '>=3.3.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
plugin_platform_interface: ^2.0.2
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
ffi: ^2.0.2
|
||||||
|
ffigen: ^11.0.0
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
plugin:
|
||||||
|
platforms:
|
||||||
|
android:
|
||||||
|
ffiPlugin: true
|
||||||
|
ios:
|
||||||
|
ffiPlugin: true
|
||||||
|
linux:
|
||||||
|
ffiPlugin: true
|
||||||
|
macos:
|
||||||
|
ffiPlugin: true
|
||||||
|
windows:
|
||||||
|
ffiPlugin: true
|
||||||
17
rust_builder/windows/.gitignore
vendored
Normal file
17
rust_builder/windows/.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
flutter/
|
||||||
|
|
||||||
|
# Visual Studio user-specific files.
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Visual Studio build-related files.
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
20
rust_builder/windows/CMakeLists.txt
Normal file
20
rust_builder/windows/CMakeLists.txt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# The Flutter tooling requires that developers have a version of Visual Studio
|
||||||
|
# installed that includes CMake 3.14 or later. You should not increase this
|
||||||
|
# version, as doing so will cause the plugin to fail to compile for some
|
||||||
|
# customers of the plugin.
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
|
||||||
|
# Project-level configuration.
|
||||||
|
set(PROJECT_NAME "rust_lib_abawo_bt_app")
|
||||||
|
project(${PROJECT_NAME} LANGUAGES CXX)
|
||||||
|
|
||||||
|
include("../cargokit/cmake/cargokit.cmake")
|
||||||
|
apply_cargokit(${PROJECT_NAME} ../../../../../../rust rust_lib_abawo_bt_app "")
|
||||||
|
|
||||||
|
# List of absolute paths to libraries that should be bundled with the plugin.
|
||||||
|
# This list could contain prebuilt libraries, or libraries created by an
|
||||||
|
# external build triggered from this build file.
|
||||||
|
set(rust_lib_abawo_bt_app_bundled_libraries
|
||||||
|
"${${PROJECT_NAME}_cargokit_lib}"
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
3
test_driver/integration_test.dart
Normal file
3
test_driver/integration_test.dart
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import 'package:integration_test/integration_test_driver.dart';
|
||||||
|
|
||||||
|
Future<void> main() => integrationDriver();
|
||||||
@ -6,6 +6,18 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
|
#include <flutter_blue_plus_winrt/flutter_blue_plus_plugin.h>
|
||||||
|
#include <nb_utils/nb_utils_plugin_c_api.h>
|
||||||
|
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
|
FlutterBluePlusPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FlutterBluePlusPlugin"));
|
||||||
|
NbUtilsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("NbUtilsPluginCApi"));
|
||||||
|
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,14 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
connectivity_plus
|
||||||
|
flutter_blue_plus_winrt
|
||||||
|
nb_utils
|
||||||
|
sqlite3_flutter_libs
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
rust_lib_abawo_bt_app
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
Reference in New Issue
Block a user