feat: everything up to bluetooth scanning

This commit is contained in:
Yandrik 2025-03-26 21:01:42 +01:00
parent be964fab25
commit f4022dd249
21 changed files with 2496 additions and 132 deletions

43
DesignRequirements.md Normal file
View File

@ -0,0 +1,43 @@
# abawo Bluetooth Device App
This app is used for connecting to, configuring, and interacting with
abawo Bluetooth devices, such as the abawo Universal Shifters.
## Features
- See all connected devices
- Add devices (connect)
- Remove devices
- Configure devices
### Supported Devices
- abawo Universal Shifters (only one for now)
- Description: Universal Shifter Device for Smart Trainers. Basically a proxy between
smart trainer and fitness app, that allows the smart trainer to connect to the fitness app, intercept simulation packets, and modify them to allow gear shifting.
- Functions (app): Connect to device, scan other BT devices and send ID to device to establish
connection with smart trainer, send configuration to device (such as resistance calc
variant, and gears and such).
## Tech Stack
- Flutter
- Riverpod
- GoRouter
- flutter_blue_plus
- shared_preferences
## Visual Design
Sleek, modern. Dark and light theme supported. Rounded containers, somewhat glass-like.
Still mostly material design.
### Company Color Theme
todo

View File

@ -24,5 +24,11 @@ linter:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
errors:
invalid_annotation_target: ignore
plugins:
- custom_lint
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@ -1,25 +1,29 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="abawo_bt_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Tell Google Play Store that your app uses Bluetooth LE
Set android:required="true" if bluetooth is necessary -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- legacy for Android 11 or lower -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>
<!-- legacy for Android 9 or lower -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
<application android:label="abawo_bt_app" android:name="${applicationName}" android:icon="@mipmap/ic_launcher">
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@ -27,9 +31,7 @@
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data android:name="flutterEmbedding" android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

View File

@ -0,0 +1,131 @@
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:riverpod_annotation/riverpod_annotation.dart';
part 'bluetooth.g.dart';
final log = Logger('BluetoothController');
@riverpod
Future<BluetoothController> bluetooth(Ref ref) async {
final controller = BluetoothController();
log.info(await controller.init());
return controller;
}
class BluetoothController {
StreamSubscription<BluetoothAdapterState>? _btStateSubscription;
StreamSubscription<List<ScanResult>>? _scanResultsSubscription;
List<ScanResult> _latestScanResults = [];
Future<Result<void>> init() async {
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();
}
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;
}
Future<Result<void>> dispose() async {
await _scanResultsSubscription?.cancel();
await _btStateSubscription?.cancel();
return Ok(null);
}
}

View File

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bluetooth.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$bluetoothHash() => r'5e9c37c57e723b84dd08fd8763e7c445b3a4dbf3';
/// See also [bluetooth].
@ProviderFor(bluetooth)
final bluetoothProvider =
AutoDisposeFutureProvider<BluetoothController>.internal(
bluetooth,
name: r'bluetoothProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$bluetoothHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef BluetoothRef = AutoDisposeFutureProviderRef<BluetoothController>;
// 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

View File

@ -1,125 +1,69 @@
import 'package:abawo_bt_app/pages/devices_page.dart';
import 'package:abawo_bt_app/util/sharedPrefs.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'pages/home_page.dart';
import 'pages/settings_page.dart';
void main() {
runApp(const MyApp());
Future<void> main() async {
Logger.root.level = Level.ALL; // defaults to Level.INFO
Logger.root.onRecord.listen((record) {
print('${record.level.name}: ${record.time}: ${record.message}');
});
WidgetsFlutterBinding.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
runApp(ProviderScope(overrides: [
// Override the unimplemented provider with the actual instance
sharedPreferencesProvider.overrideWithValue(prefs),
], child: const AbawoBtApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
class AbawoBtApp extends StatelessWidget {
const AbawoBtApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
return MaterialApp.router(
title: 'Abawo BT App',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
primarySwatch: Colors.blue,
useMaterial3: true,
brightness: Brightness.light,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
darkTheme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
brightness: Brightness.dark,
),
themeMode: ThemeMode.system,
routerConfig: _router,
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
// Configure GoRouter
final _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: 'settings',
builder: (context, state) => const SettingsPage(),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
GoRoute(
path: 'connect_device',
builder: (context, state) => const ConnectDevicePage(),
)
],
),
],
);

View File

@ -0,0 +1,47 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'bluetooth_device_model.freezed.dart';
part 'bluetooth_device_model.g.dart';
/// Enum representing the type of Bluetooth device
enum DeviceType {
/// Universal Shifters device
universalShifters,
/// Other unspecified device types
other,
}
/// Model representing a Bluetooth device
@freezed
abstract class BluetoothDeviceModel with _$BluetoothDeviceModel {
const factory BluetoothDeviceModel({
/// Unique identifier for the device
required String id,
/// Name of the device as advertised
String? name,
/// MAC address of the device
required String address,
/// Signal strength indicator (RSSI)
int? rssi,
/// Type of the device
@Default(DeviceType.other) DeviceType type,
/// Whether the device is currently connected
@Default(false) bool isConnected,
/// Additional device information
Map<String, dynamic>? manufacturerData,
/// Service UUIDs advertised by the device
List<String>? serviceUuids,
}) = _BluetoothDeviceModel;
/// Create a BluetoothDeviceModel from JSON
factory BluetoothDeviceModel.fromJson(Map<String, dynamic> json) =>
_$BluetoothDeviceModelFromJson(json);
}

View File

@ -0,0 +1,366 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'bluetooth_device_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$BluetoothDeviceModel {
/// Unique identifier for the device
String get id;
/// Name of the device as advertised
String? get name;
/// MAC address of the device
String get address;
/// Signal strength indicator (RSSI)
int? get rssi;
/// Type of the device
DeviceType get type;
/// Whether the device is currently connected
bool get isConnected;
/// Additional device information
Map<String, dynamic>? get manufacturerData;
/// Service UUIDs advertised by the device
List<String>? get serviceUuids;
/// Create a copy of BluetoothDeviceModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$BluetoothDeviceModelCopyWith<BluetoothDeviceModel> get copyWith =>
_$BluetoothDeviceModelCopyWithImpl<BluetoothDeviceModel>(
this as BluetoothDeviceModel, _$identity);
/// Serializes this BluetoothDeviceModel to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is BluetoothDeviceModel &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.address, address) || other.address == address) &&
(identical(other.rssi, rssi) || other.rssi == rssi) &&
(identical(other.type, type) || other.type == type) &&
(identical(other.isConnected, isConnected) ||
other.isConnected == isConnected) &&
const DeepCollectionEquality()
.equals(other.manufacturerData, manufacturerData) &&
const DeepCollectionEquality()
.equals(other.serviceUuids, serviceUuids));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
name,
address,
rssi,
type,
isConnected,
const DeepCollectionEquality().hash(manufacturerData),
const DeepCollectionEquality().hash(serviceUuids));
@override
String toString() {
return 'BluetoothDeviceModel(id: $id, name: $name, address: $address, rssi: $rssi, type: $type, isConnected: $isConnected, manufacturerData: $manufacturerData, serviceUuids: $serviceUuids)';
}
}
/// @nodoc
abstract mixin class $BluetoothDeviceModelCopyWith<$Res> {
factory $BluetoothDeviceModelCopyWith(BluetoothDeviceModel value,
$Res Function(BluetoothDeviceModel) _then) =
_$BluetoothDeviceModelCopyWithImpl;
@useResult
$Res call(
{String id,
String? name,
String address,
int? rssi,
DeviceType type,
bool isConnected,
Map<String, dynamic>? manufacturerData,
List<String>? serviceUuids});
}
/// @nodoc
class _$BluetoothDeviceModelCopyWithImpl<$Res>
implements $BluetoothDeviceModelCopyWith<$Res> {
_$BluetoothDeviceModelCopyWithImpl(this._self, this._then);
final BluetoothDeviceModel _self;
final $Res Function(BluetoothDeviceModel) _then;
/// Create a copy of BluetoothDeviceModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = freezed,
Object? address = null,
Object? rssi = freezed,
Object? type = null,
Object? isConnected = null,
Object? manufacturerData = freezed,
Object? serviceUuids = freezed,
}) {
return _then(_self.copyWith(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: freezed == name
? _self.name
: name // ignore: cast_nullable_to_non_nullable
as String?,
address: null == address
? _self.address
: address // ignore: cast_nullable_to_non_nullable
as String,
rssi: freezed == rssi
? _self.rssi
: rssi // ignore: cast_nullable_to_non_nullable
as int?,
type: null == type
? _self.type
: type // ignore: cast_nullable_to_non_nullable
as DeviceType,
isConnected: null == isConnected
? _self.isConnected
: isConnected // ignore: cast_nullable_to_non_nullable
as bool,
manufacturerData: freezed == manufacturerData
? _self.manufacturerData
: manufacturerData // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
serviceUuids: freezed == serviceUuids
? _self.serviceUuids
: serviceUuids // ignore: cast_nullable_to_non_nullable
as List<String>?,
));
}
}
/// @nodoc
@JsonSerializable()
class _BluetoothDeviceModel implements BluetoothDeviceModel {
const _BluetoothDeviceModel(
{required this.id,
this.name,
required this.address,
this.rssi,
this.type = DeviceType.other,
this.isConnected = false,
final Map<String, dynamic>? manufacturerData,
final List<String>? serviceUuids})
: _manufacturerData = manufacturerData,
_serviceUuids = serviceUuids;
factory _BluetoothDeviceModel.fromJson(Map<String, dynamic> json) =>
_$BluetoothDeviceModelFromJson(json);
/// Unique identifier for the device
@override
final String id;
/// Name of the device as advertised
@override
final String? name;
/// MAC address of the device
@override
final String address;
/// Signal strength indicator (RSSI)
@override
final int? rssi;
/// Type of the device
@override
@JsonKey()
final DeviceType type;
/// Whether the device is currently connected
@override
@JsonKey()
final bool isConnected;
/// Additional device information
final Map<String, dynamic>? _manufacturerData;
/// Additional device information
@override
Map<String, dynamic>? get manufacturerData {
final value = _manufacturerData;
if (value == null) return null;
if (_manufacturerData is EqualUnmodifiableMapView) return _manufacturerData;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
/// Service UUIDs advertised by the device
final List<String>? _serviceUuids;
/// Service UUIDs advertised by the device
@override
List<String>? get serviceUuids {
final value = _serviceUuids;
if (value == null) return null;
if (_serviceUuids is EqualUnmodifiableListView) return _serviceUuids;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
/// Create a copy of BluetoothDeviceModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$BluetoothDeviceModelCopyWith<_BluetoothDeviceModel> get copyWith =>
__$BluetoothDeviceModelCopyWithImpl<_BluetoothDeviceModel>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$BluetoothDeviceModelToJson(
this,
);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _BluetoothDeviceModel &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.address, address) || other.address == address) &&
(identical(other.rssi, rssi) || other.rssi == rssi) &&
(identical(other.type, type) || other.type == type) &&
(identical(other.isConnected, isConnected) ||
other.isConnected == isConnected) &&
const DeepCollectionEquality()
.equals(other._manufacturerData, _manufacturerData) &&
const DeepCollectionEquality()
.equals(other._serviceUuids, _serviceUuids));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
name,
address,
rssi,
type,
isConnected,
const DeepCollectionEquality().hash(_manufacturerData),
const DeepCollectionEquality().hash(_serviceUuids));
@override
String toString() {
return 'BluetoothDeviceModel(id: $id, name: $name, address: $address, rssi: $rssi, type: $type, isConnected: $isConnected, manufacturerData: $manufacturerData, serviceUuids: $serviceUuids)';
}
}
/// @nodoc
abstract mixin class _$BluetoothDeviceModelCopyWith<$Res>
implements $BluetoothDeviceModelCopyWith<$Res> {
factory _$BluetoothDeviceModelCopyWith(_BluetoothDeviceModel value,
$Res Function(_BluetoothDeviceModel) _then) =
__$BluetoothDeviceModelCopyWithImpl;
@override
@useResult
$Res call(
{String id,
String? name,
String address,
int? rssi,
DeviceType type,
bool isConnected,
Map<String, dynamic>? manufacturerData,
List<String>? serviceUuids});
}
/// @nodoc
class __$BluetoothDeviceModelCopyWithImpl<$Res>
implements _$BluetoothDeviceModelCopyWith<$Res> {
__$BluetoothDeviceModelCopyWithImpl(this._self, this._then);
final _BluetoothDeviceModel _self;
final $Res Function(_BluetoothDeviceModel) _then;
/// Create a copy of BluetoothDeviceModel
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$Res call({
Object? id = null,
Object? name = freezed,
Object? address = null,
Object? rssi = freezed,
Object? type = null,
Object? isConnected = null,
Object? manufacturerData = freezed,
Object? serviceUuids = freezed,
}) {
return _then(_BluetoothDeviceModel(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: freezed == name
? _self.name
: name // ignore: cast_nullable_to_non_nullable
as String?,
address: null == address
? _self.address
: address // ignore: cast_nullable_to_non_nullable
as String,
rssi: freezed == rssi
? _self.rssi
: rssi // ignore: cast_nullable_to_non_nullable
as int?,
type: null == type
? _self.type
: type // ignore: cast_nullable_to_non_nullable
as DeviceType,
isConnected: null == isConnected
? _self.isConnected
: isConnected // ignore: cast_nullable_to_non_nullable
as bool,
manufacturerData: freezed == manufacturerData
? _self._manufacturerData
: manufacturerData // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
serviceUuids: freezed == serviceUuids
? _self._serviceUuids
: serviceUuids // ignore: cast_nullable_to_non_nullable
as List<String>?,
));
}
}
// dart format on

View File

@ -0,0 +1,41 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bluetooth_device_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_BluetoothDeviceModel _$BluetoothDeviceModelFromJson(
Map<String, dynamic> json) =>
_BluetoothDeviceModel(
id: json['id'] as String,
name: json['name'] as String?,
address: json['address'] as String,
rssi: (json['rssi'] as num?)?.toInt(),
type: $enumDecodeNullable(_$DeviceTypeEnumMap, json['type']) ??
DeviceType.other,
isConnected: json['isConnected'] as bool? ?? false,
manufacturerData: json['manufacturerData'] as Map<String, dynamic>?,
serviceUuids: (json['serviceUuids'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
);
Map<String, dynamic> _$BluetoothDeviceModelToJson(
_BluetoothDeviceModel instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'address': instance.address,
'rssi': instance.rssi,
'type': _$DeviceTypeEnumMap[instance.type]!,
'isConnected': instance.isConnected,
'manufacturerData': instance.manufacturerData,
'serviceUuids': instance.serviceUuids,
};
const _$DeviceTypeEnumMap = {
DeviceType.universalShifters: 'universalShifters',
DeviceType.other: 'other',
};

288
lib/pages/devices_page.dart Normal file
View File

@ -0,0 +1,288 @@
import 'package:abawo_bt_app/controller/bluetooth.dart';
import 'package:abawo_bt_app/util/constants.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:abawo_bt_app/widgets/device_listitem.dart';
import 'package:abawo_bt_app/widgets/scanning_animation.dart'; // Import the new animation widget
const Duration _scanDuration = Duration(seconds: 10);
class ConnectDevicePage extends ConsumerStatefulWidget {
const ConnectDevicePage({super.key});
@override
ConsumerState<ConnectDevicePage> createState() => _ConnectDevicePageState();
}
class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
with TickerProviderStateMixin {
// Use TickerProviderStateMixin for multiple controllers if needed later, good practice
bool _initialScanStarted = false;
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
void _startScanIfNeeded(BluetoothController controller) {
// Use WidgetsBinding to schedule the scan start after the build phase
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_initialScanStarted && mounted) {
controller.startScan(timeout: _scanDuration);
_startScanProgressAnimation(); // Start scan duration progress animation
_startWaveAnimation(); // Start the wave animation
if (mounted) {
setState(() {
_initialScanStarted = true;
});
}
}
});
}
@override
void initState() {
super.initState();
// Initialize scan progress controller
_progressController = AnimationController(
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
void dispose() {
// Stop animations before disposing
_progressController.stop();
_waveAnimationController.stop();
_progressController.dispose();
_waveAnimationController.dispose();
super.dispose();
}
// Helper method to start/reset scan progress animation
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
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Connect Device'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/'),
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: Row(
children: [
const Text('abawo only'), // Label for the switch
Switch(
value: _showOnlyAbawoDevices,
onChanged: (value) {
if (mounted) {
setState(() {
_showOnlyAbawoDevices = value;
});
}
},
),
],
),
)
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Available Devices',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
// Use Consumer to react to bluetoothProvider changes
Expanded(
// Allow the Consumer content to expand
child: Consumer(builder: (context, ref, child) {
final btAsyncValue = ref.watch(bluetoothProvider);
return btAsyncValue.when(
loading: () => const Center(
child:
CircularProgressIndicator()), // Center loading indicator
error: (err, stack) => Center(
child: Text(
'Error loading Bluetooth: $err')), // Center error
data: (controller) {
// Start the initial scan once the controller is ready
// Start initial scan and animation
_startScanIfNeeded(controller);
// Use StreamBuilder to watch the scanning state
return StreamBuilder<bool>(
stream: FlutterBluePlus.isScanning,
initialData:
false, // Default to not scanning before check
builder: (context, snapshot) {
final isScanning = snapshot.data ?? false;
if (isScanning && _initialScanStarted) {
_startWaveAnimation(); // Ensure wave animation is running
// Show the new scanning wave animation with the progress indicator value
return Center(
// Pass the wave animation controller. The progress value will be added
// 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'),
),
),
],
);
}
},
);
},
);
}),
),
],
),
),
);
}
}

75
lib/pages/home_page.dart Normal file
View File

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Abawo BT App'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () => context.go('/settings'),
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Welcome to Abawo BT App',
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 20),
// Devices Section
Container(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Devices',
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.w500),
),
IconButton(
icon: const Icon(Icons.add_circle),
onPressed: () => context.go('/connect_device'),
tooltip: 'Connect a device',
),
],
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text(
'No devices connected yet',
style: TextStyle(color: Colors.grey),
),
),
),
],
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () => context.go('/settings'),
child: const Text('Go to Settings'),
),
],
),
),
);
}
}

View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/'),
),
),
body: ListView(
children: [
ListTile(
leading: const Icon(Icons.brightness_6),
title: const Text('Theme'),
subtitle: const Text('Change app theme'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// Theme settings functionality
},
),
ListTile(
leading: const Icon(Icons.bluetooth),
title: const Text('Bluetooth Settings'),
subtitle: const Text('Configure Bluetooth connections'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// Bluetooth settings functionality
},
),
ListTile(
leading: const Icon(Icons.info),
title: const Text('About'),
subtitle: const Text('App information'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// About screen functionality
},
),
],
),
);
}
}

1
lib/util/constants.dart Normal file
View File

@ -0,0 +1 @@
const abawoServiceBtUUID = '0993826f-0ee4-4b37-9614-d13ecba4ffc2';

73
lib/util/sharedPrefs.dart Normal file
View File

@ -0,0 +1,73 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'sharedPrefs.g.dart';
final sharedPreferencesProvider =
Provider<SharedPreferences>((ref) => throw UnimplementedError());
class SharedPrefNotifier<T> extends StateNotifier<T> {
final SharedPreferences prefs;
final String key;
final T defaultValue;
SharedPrefNotifier({
required this.prefs,
required this.key,
required this.defaultValue,
}) : super(prefs.containsKey(key)
? _getValue<T>(prefs, key, defaultValue)
: defaultValue);
// Helper to get the value with proper typing
static T _getValue<T>(SharedPreferences prefs, String key, T defaultValue) {
switch (T) {
case String:
return prefs.getString(key) as T ?? defaultValue;
case bool:
return prefs.getBool(key) as T ?? defaultValue;
case int:
return prefs.getInt(key) as T ?? defaultValue;
case double:
return prefs.getDouble(key) as T ?? defaultValue;
case const (List<String>):
return prefs.getStringList(key) as T ?? defaultValue;
default:
throw UnsupportedError('Type $T not supported by SharedPreferences');
}
}
Future<void> update(T value) async {
switch (T) {
case String:
await prefs.setString(key, value as String);
break;
case bool:
await prefs.setBool(key, value as bool);
break;
case int:
await prefs.setInt(key, value as int);
break;
case double:
await prefs.setDouble(key, value as double);
break;
case const (List<String>):
await prefs.setStringList(key, value as List<String>);
break;
default:
throw UnsupportedError('Type $T not supported by SharedPreferences');
}
state = value;
}
}
final isDarkModeProvider =
StateNotifierProvider<SharedPrefNotifier<bool>, bool>((ref) {
final prefs = ref.watch(sharedPreferencesProvider);
return SharedPrefNotifier<bool>(
prefs: prefs,
key: 'is_dark_mode',
defaultValue: false,
);
});

197
lib/util/sharedPrefs.g.dart Normal file
View File

@ -0,0 +1,197 @@
// 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

View File

@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'dart:ui'; // Required for ImageFilter
class DeviceListItem extends StatelessWidget {
final String deviceName;
final String deviceId; // Added for potential future use or subtitle
final bool isUnknownDevice;
// final String? imageUrl; // Optional image URL - commented out for now
const DeviceListItem({
super.key,
required this.deviceName,
required this.deviceId,
this.isUnknownDevice = false,
// this.imageUrl,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDarkMode = theme.brightness == Brightness.dark;
// Glassy effect colors - adjust transparency and base color as needed
final glassColor = isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.black.withOpacity(0.05);
final shadowColor = isDarkMode
? Colors.black.withOpacity(0.4)
: Colors.grey.withOpacity(0.5);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
children: [
// Left side: Rounded Square Container
Container(
width: 60, // Square size
height: 60,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), // Rounded corners
boxShadow: [
BoxShadow(
color: shadowColor,
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ClipRRect(
// Apply blur effect within rounded corners
borderRadius: BorderRadius.circular(12),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
decoration: BoxDecoration(
color:
glassColor, // Semi-transparent color for glass effect
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(0.2), // Subtle border
width: 0.5,
),
),
child: const Center(
// Placeholder '?' - replace with Image widget when imageUrl is available
child: Text(
'?',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.white70, // Adjust color as needed
),
),
),
),
),
),
),
const SizedBox(width: 16), // Spacing between image and text
// Right side: Device Name and ID
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isUnknownDevice ? 'Unknown Device' : deviceName,
style: TextStyle(
fontSize: 16,
fontWeight:
isUnknownDevice ? FontWeight.normal : FontWeight.w500,
fontStyle:
isUnknownDevice ? FontStyle.italic : FontStyle.normal,
color: isUnknownDevice
? theme.hintColor
: theme.textTheme.bodyLarge?.color,
),
overflow: TextOverflow
.ellipsis, // Prevent long names from overflowing
),
const SizedBox(height: 4),
Text(
deviceId, // Display device ID as subtitle
style: theme.textTheme.bodySmall
?.copyWith(color: theme.hintColor),
overflow: TextOverflow.ellipsis,
),
],
),
),
// Optional: Add an icon or button on the far right if needed later
// Icon(Icons.chevron_right, color: theme.hintColor),
],
),
);
}
}

View File

@ -0,0 +1,187 @@
import 'dart:math';
import 'package:flutter/material.dart';
class ScanningWaveAnimation extends StatefulWidget {
final Animation<double>
animation; // For the wave effect (0.0 to 1.0 over 1.5s)
final double
progressValue; // For the CircularProgressIndicator (0.0 to 1.0 over scan duration)
final Color waveColor; // Color for the scanning wave
const ScanningWaveAnimation({
super.key,
required this.animation,
required this.progressValue,
this.waveColor = Colors.lightBlueAccent, // Default color using constant
});
@override
_ScanningWaveAnimationState createState() => _ScanningWaveAnimationState();
}
class _ScanningWaveAnimationState extends State<ScanningWaveAnimation> {
@override
Widget build(BuildContext context) {
// Stack to layer the painter, progress indicator, and text
return Stack(
alignment: Alignment.center,
children: [
// The wave animation painter rebuilds based on the wave animation
AnimatedBuilder(
animation: widget.animation,
builder: (context, child) {
return CustomPaint(
// Use full available size for the painter
size: Size.infinite,
painter: _WavePainter(
progress: widget.animation.value, // Pass wave progress
waveColor: widget.waveColor), // Pass wave color
);
},
),
// The progress indicator and text in the center
Column(
mainAxisSize: MainAxisSize.min, // Keep column compact
children: [
CircularProgressIndicator(
value: widget.progressValue, // Use the scan duration progress
backgroundColor:
Colors.white.withValues(alpha: 0.1), // Subtle background
),
const SizedBox(height: 16), // Consistent spacing
const Text(
'Scanning for devices...',
style: TextStyle(
fontSize: 16, color: Colors.white70), // Lighter text
),
], // Close children[] of Column
), // Close Column
], // Close children[] of Stack
); // Close Stack
}
}
// --- New Wave Painter Implementation ---
class _WavePainter extends CustomPainter {
final double
progress; // Animation value from 0.0 to 1.0, drives the single wave
final Color waveColor; // The color of the wave
final double startRadius = 50.0; // Start wave ~175px from center
final double waveThickness = 10.0; // Thickness of the wave
final double waveExpansion = 50.0; // Amount of thickness increase at the end
_WavePainter({required this.progress, required this.waveColor});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
// Max radius should reach significantly beyond the screen edge
final maxRadius = min(size.width, size.height) * 0.8; // Extend further
// Ensure endRadius is always larger than startRadius
final endRadius = max(startRadius + 1.0, maxRadius);
// Apply deceleration curve to the progress
final easedProgress = Curves.easeOut.transform(progress);
final realWaveThickness = waveThickness + waveExpansion * easedProgress;
// --- Opacity Calculation (Fade-in / Fade-out) ---
const double fadeInEnd = 0.20; // Faster fade-in (0% to 20% of animation)
const double fadeOutStart = 0.45; // Start fade-out earlier (45% to 100%)
final double fadeInOpacity =
(progress < fadeInEnd) ? progress / fadeInEnd : 1.0;
final double fadeOutProgress = (progress < fadeOutStart)
? 0.0 // Not fading out yet
: (progress - fadeOutStart) /
(1.0 - fadeOutStart); // Map fade-out phase to 0.0-1.0
final double fadeOutOpacity =
max(0.0, 1.0 - fadeOutProgress); // Linear fade-out
// Combine fade-in and fade-out using minimum
final opacity = max(0.0, min(fadeInOpacity, fadeOutOpacity));
// Skip drawing if fully faded out
if (opacity <= 0.001) {
// Use a small threshold to avoid floating point issues
return;
}
// --- Base Radius Calculation ---
// Calculate the base radius for this frame based on decelerated progress
final baseRadius = startRadius + (endRadius - startRadius) * easedProgress;
// Skip drawing if radius hasn't reached startRadius yet
// (Needed because easedProgress might be 0 at the very start)
if (baseRadius < startRadius) {
return;
}
// --- Path Generation with Deformation ---
final path = Path();
final int steps = 200; // Increase steps for smoother deformation
final double angleStep = (2 * pi) / steps;
// Noise parameters (tune these for desired 'waviness' and 'shimmer')
final double noiseMaxAmplitude =
realWaveThickness * 0.4; // Max radius deviation
final double noiseAmplitude = noiseMaxAmplitude *
min(easedProgress * 4.0,
1.0 - easedProgress * 0.8); // Amplitude decreases as it expands
final double noiseFreq1 = 6.0; // Controls number of 'bumps'
final double noiseFreq2 = 12.0; // Another layer of bumps
final double phaseShift = progress * pi * 6; // Controls 'shimmer' speed
for (int i = 0; i <= steps; i++) {
final double angle = i * angleStep;
// Calculate noise perturbation using layered sine waves
// Different frequencies and phase shifts create complex patterns
double perturbation = noiseAmplitude *
(0.6 * sin(noiseFreq1 * angle + phaseShift) +
0.4 *
sin(noiseFreq2 * angle -
phaseShift * 0.6) // Opposite phase shift adds complexity
);
final double currentRadius = max(
0.0, baseRadius + perturbation); // Ensure radius doesn't go negative
// Convert polar (angle, radius) to cartesian (x, y)
final double x = center.dx + currentRadius * cos(angle);
final double y = center.dy + currentRadius * sin(angle);
if (i == 0) {
path.moveTo(x, y); // Start the path
} else {
path.lineTo(x, y); // Draw line to next point
}
}
path.close(); // Connect the last point back to the first
// --- Painting the Path ---
// Use the provided wave color with calculated opacity
final paintColor = waveColor.withValues(alpha: opacity * 0.75);
final paint = Paint()
..color = paintColor
..style = PaintingStyle.stroke
..strokeWidth = realWaveThickness
..strokeCap =
StrokeCap.round // Soften line endings (though path is closed)
..strokeJoin = StrokeJoin.round // Soften corners in the deformation
// Apply blur for feathered/soft edges, sigma related to thickness
..maskFilter = MaskFilter.blur(BlurStyle.normal, realWaveThickness * 0.5);
// Draw the deformed, blurred path
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant _WavePainter oldDelegate) {
// Repaint whenever the animation progress changes
return oldDelegate.progress != progress ||
oldDelegate.waveColor != waveColor;
}
}

View File

@ -5,6 +5,10 @@
import FlutterMacOS
import Foundation
import flutter_blue_plus_darwin
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}

View File

@ -1,6 +1,46 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
url: "https://pub.dev"
source: hosted
version: "80.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
url: "https://pub.dev"
source: hosted
version: "7.3.0"
analyzer_plugin:
dependency: transitive
description:
name: analyzer_plugin
sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
url: "https://pub.dev"
source: hosted
version: "0.13.0"
anyhow:
dependency: "direct main"
description:
name: anyhow
sha256: d4c9ecc118e5fcd74d154661df98a088dcc445e5aa953caa1ffb69f8cd3e1f4f
url: "https://pub.dev"
source: hosted
version: "3.0.1"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
@ -9,6 +49,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
bluez:
dependency: transitive
description:
name: bluez
sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545"
url: "https://pub.dev"
source: hosted
version: "0.8.3"
boolean_selector:
dependency: transitive
description:
@ -17,6 +65,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
build:
dependency: transitive
description:
name: build
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
url: "https://pub.dev"
source: hosted
version: "2.4.2"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
url: "https://pub.dev"
source: hosted
version: "4.0.4"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
url: "https://pub.dev"
source: hosted
version: "2.4.4"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
url: "https://pub.dev"
source: hosted
version: "2.4.15"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
url: "https://pub.dev"
source: hosted
version: "8.9.5"
characters:
dependency: transitive
description:
@ -25,6 +137,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev"
source: hosted
version: "2.0.3"
ci:
dependency: transitive
description:
name: ci
sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@ -33,6 +169,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
url: "https://pub.dev"
source: hosted
version: "4.10.1"
collection:
dependency: transitive
description:
@ -41,6 +185,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.0"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
cupertino_icons:
dependency: "direct main"
description:
@ -49,6 +209,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
custom_lint:
dependency: "direct dev"
description:
name: custom_lint
sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1"
url: "https://pub.dev"
source: hosted
version: "0.7.5"
custom_lint_builder:
dependency: transitive
description:
name: custom_lint_builder
sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228"
url: "https://pub.dev"
source: hosted
version: "0.7.5"
custom_lint_core:
dependency: transitive
description:
name: custom_lint_core
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
url: "https://pub.dev"
source: hosted
version: "0.7.5"
custom_lint_visitor:
dependency: transitive
description:
name: custom_lint_visitor
sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8"
url: "https://pub.dev"
source: hosted
version: "1.0.0+7.3.0"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
fake_async:
dependency: transitive
description:
@ -57,11 +265,83 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_blue_plus:
dependency: "direct main"
description:
name: flutter_blue_plus
sha256: "2d926dbef0fd6c58d4be8fca9eaaf1ba747c0ccb8373ddd5386665317e26eb61"
url: "https://pub.dev"
source: hosted
version: "1.35.3"
flutter_blue_plus_android:
dependency: transitive
description:
name: flutter_blue_plus_android
sha256: c1d83f84b514e46345a8a58599c428f20b11e78379521e0d3b0611c7b7cbf2c1
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flutter_blue_plus_darwin:
dependency: transitive
description:
name: flutter_blue_plus_darwin
sha256: "8d0a0f11f83b13dda173396b7e4028b4e8656bc8dbbc82c26a7e49aafc62644b"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flutter_blue_plus_linux:
dependency: transitive
description:
name: flutter_blue_plus_linux
sha256: "1d367ed378b2bd6c3b9685fda7044e1d2f169884802b7dec7badb31a99a72660"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flutter_blue_plus_platform_interface:
dependency: transitive
description:
name: flutter_blue_plus_platform_interface
sha256: "114f8e85a03a28a48d707a4df6cc9218e1f2005cf260c5e815e5585a00da5778"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flutter_blue_plus_web:
dependency: transitive
description:
name: flutter_blue_plus_web
sha256: db70cdc41bc743763dc0d47e8c7c10f3923cbbe71b33d9dc21deea482affeb4d
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flutter_lints:
dependency: "direct dev"
description:
@ -70,11 +350,136 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
url: "https://pub.dev"
source: hosted
version: "2.6.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c"
url: "https://pub.dev"
source: hosted
version: "3.0.4"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
url: "https://pub.dev"
source: hosted
version: "3.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3
url: "https://pub.dev"
source: hosted
version: "14.8.1"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hotreloader:
dependency: transitive
description:
name: hotreloader
sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b
url: "https://pub.dev"
source: hosted
version: "4.3.0"
http:
dependency: transitive
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev"
source: hosted
version: "1.3.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.7.1"
json_annotation:
dependency: "direct main"
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
sha256: "81f04dee10969f89f604e1249382d46b97a1ccad53872875369622b5bfc9e58a"
url: "https://pub.dev"
source: hosted
version: "6.9.4"
leak_tracker:
dependency: transitive
description:
@ -107,6 +512,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.1"
logging:
dependency: "direct main"
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
@ -131,6 +544,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.15.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
@ -139,11 +568,227 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
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: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
url: "https://pub.dev"
source: hosted
version: "2.6.1"
riverpod_analyzer_utils:
dependency: transitive
description:
name: riverpod_analyzer_utils
sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611"
url: "https://pub.dev"
source: hosted
version: "0.5.10"
riverpod_annotation:
dependency: "direct main"
description:
name: riverpod_annotation
sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
url: "https://pub.dev"
source: hosted
version: "2.6.1"
riverpod_generator:
dependency: "direct dev"
description:
name: riverpod_generator
sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36"
url: "https://pub.dev"
source: hosted
version: "2.6.5"
riverpod_lint:
dependency: "direct dev"
description:
name: riverpod_lint
sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35"
url: "https://pub.dev"
source: hosted
version: "2.6.5"
rust:
dependency: "direct main"
description:
name: rust
sha256: "188c3c2fd7bcce972277d78ae38ab156874d174cc661a9b4b7fa85e7e7303400"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a"
url: "https://pub.dev"
source: hosted
version: "2.5.2"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad"
url: "https://pub.dev"
source: hosted
version: "2.4.8"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
url: "https://pub.dev"
source: hosted
version: "1.3.5"
source_span:
dependency: transitive
description:
@ -152,6 +797,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
@ -160,6 +813,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.12.0"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
stream_channel:
dependency: transitive
description:
@ -168,6 +829,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@ -192,6 +861,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.3"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_math:
dependency: transitive
description:
@ -208,6 +901,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "14.3.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.6.1 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.27.0"

View File

@ -34,6 +34,16 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
flutter_riverpod: ^2.6.1
riverpod_annotation: ^2.6.1
go_router: ^14.8.1
freezed_annotation: ^3.0.0
json_annotation: ^4.9.0
flutter_blue_plus: ^1.35.3
rust: ^3.1.0
anyhow: ^3.0.1
logging: ^1.3.0
shared_preferences: ^2.5.2
dev_dependencies:
flutter_test:
@ -45,6 +55,12 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
riverpod_generator: ^2.6.5
build_runner: ^2.4.15
custom_lint: ^0.7.5
riverpod_lint: ^2.6.5
freezed: ^3.0.4
json_serializable: ^6.9.4
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View File

@ -13,7 +13,7 @@ import 'package:abawo_bt_app/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
await tester.pumpWidget(const AbawoBtApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);