feat: everything up to bluetooth scanning
This commit is contained in:
parent
be964fab25
commit
f4022dd249
43
DesignRequirements.md
Normal file
43
DesignRequirements.md
Normal 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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
131
lib/controller/bluetooth.dart
Normal file
131
lib/controller/bluetooth.dart
Normal 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);
|
||||
}
|
||||
}
|
27
lib/controller/bluetooth.g.dart
Normal file
27
lib/controller/bluetooth.g.dart
Normal 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
|
164
lib/main.dart
164
lib/main.dart
@ -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(),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
47
lib/model/bluetooth_device_model.dart
Normal file
47
lib/model/bluetooth_device_model.dart
Normal 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);
|
||||
}
|
366
lib/model/bluetooth_device_model.freezed.dart
Normal file
366
lib/model/bluetooth_device_model.freezed.dart
Normal 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
|
41
lib/model/bluetooth_device_model.g.dart
Normal file
41
lib/model/bluetooth_device_model.g.dart
Normal 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
288
lib/pages/devices_page.dart
Normal 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
75
lib/pages/home_page.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
50
lib/pages/settings_page.dart
Normal file
50
lib/pages/settings_page.dart
Normal 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
1
lib/util/constants.dart
Normal file
@ -0,0 +1 @@
|
||||
const abawoServiceBtUUID = '0993826f-0ee4-4b37-9614-d13ecba4ffc2';
|
73
lib/util/sharedPrefs.dart
Normal file
73
lib/util/sharedPrefs.dart
Normal 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
197
lib/util/sharedPrefs.g.dart
Normal 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
|
117
lib/widgets/device_listitem.dart
Normal file
117
lib/widgets/device_listitem.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
187
lib/widgets/scanning_animation.dart
Normal file
187
lib/widgets/scanning_animation.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -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"))
|
||||
}
|
||||
|
751
pubspec.lock
751
pubspec.lock
@ -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"
|
||||
|
16
pubspec.yaml
16
pubspec.yaml
@ -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
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user