Files
abawo-bt-app/lib/pages/devices_page.dart

352 lines
16 KiB
Dart

import 'dart:io';
import 'package:abawo_bt_app/controller/bluetooth.dart';
import 'package:abawo_bt_app/model/bluetooth_device_model.dart';
import 'package:abawo_bt_app/util/constants.dart';
import 'package:anyhow/anyhow.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
import 'package:abawo_bt_app/widgets/device_listitem.dart';
import 'package:abawo_bt_app/widgets/horizontal_scanning_animation.dart'; // Import the new horizontal animation
import 'package:abawo_bt_app/database/database.dart';
import 'package:drift/drift.dart' show Value;
const Duration _scanDuration = Duration(seconds: 10);
class ConnectDevicePage extends ConsumerStatefulWidget {
const ConnectDevicePage({super.key});
@override
ConsumerState<ConnectDevicePage> createState() => _ConnectDevicePageState();
}
class _ConnectDevicePageState extends ConsumerState<ConnectDevicePage>
with TickerProviderStateMixin {
// TickerProviderStateMixin is no longer needed as animations are self-contained or handled by StreamBuilder
int _retryScanCounter = 0; // Used to force animation reset
bool _initialScanTriggered = false; // Track if the first scan was requested
bool _showOnlyAbawoDevices = true; // State for filtering devices
// 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((_) {
// Start scan only if it hasn't been triggered yet and the widget is mounted
if (!_initialScanTriggered && mounted) {
controller.startScan(timeout: _scanDuration);
if (mounted) {
setState(() {
_initialScanTriggered = true;
});
}
}
});
}
@override
void initState() {
super.initState();
super.initState();
// No animation controllers needed here anymore
}
@override
void dispose() {
// Dispose controllers if they existed (they don't anymore)
super.dispose();
}
// Removed _startScanProgressAnimation, _startWaveAnimation, _stopAnimations
@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: Column(
// Use Column instead of Center(Column(...))
children: [
const Padding(
padding: EdgeInsets.all(16.0), // Add padding around the title
child: Text(
'Available Devices',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
// Use Consumer to get the BluetoothController
Expanded(
// Allow the device list to take available space
child: Consumer(
builder: (context, ref, child) {
final btAsyncValue = ref.watch(bluetoothProvider);
final connectedDevices =
ref.watch(nConnectedDevicesProvider).valueOrNull ??
const <ConnectedDevice>[];
final connectedDeviceAddresses = connectedDevices
.map((device) => device.deviceAddress)
.toSet();
return btAsyncValue.when(
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Error: $err')),
data: (controller) {
// Trigger the initial scan if needed
_startScanIfNeeded(controller);
// StreamBuilder for Scan Results (Device List)
return StreamBuilder<List<DiscoveredDevice>>(
stream: controller.scanResultsStream,
initialData: const [],
builder: (context, snapshot) {
final results = snapshot.data ?? [];
// Filter results based on the toggle state
final filteredResults = _showOnlyAbawoDevices
? results
.where((device) =>
device.serviceUuids.any(isAbawoDeviceGuid))
.toList()
: results;
if (!_initialScanTriggered && filteredResults.isEmpty) {
// Show a message or placeholder before the first scan starts or if no devices found initially
return const Center(
child: Text(
'Scanning for devices...')); // Or CircularProgressIndicator()
}
if (filteredResults.isEmpty && _initialScanTriggered) {
// Show 'No devices found' only after the initial scan was triggered
return const Center(child: Text('No devices found.'));
}
// Display the list
return ListView.builder(
itemCount: filteredResults.length,
itemBuilder: (context, index) {
final device = filteredResults[index];
final isAlreadyConnected =
connectedDeviceAddresses.contains(device.id);
final abawoDevice =
device.serviceUuids.any(isAbawoDeviceGuid);
final connectable = device.serviceUuids
.any(isConnectableAbawoDeviceGuid);
final deviceName = device.name.isEmpty
? 'Unknown Device'
: device.name;
return InkWell(
onTap: () async {
if (isAlreadyConnected) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'This device is already connected in the app.'),
),
);
return;
}
if (!abawoDevice) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'This app can only connect to abawo devices.')),
);
return;
} else if (!connectable) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'This device is not connectable with the app.')),
);
return;
} else {
final res = await controller.connect(device);
print('res: $res');
switch (res) {
case Ok():
// trigger pairing/permission prompt if needed
if (!Platform.isAndroid) {
controller.readCharacteristic(
device.id,
'0993826f-0ee4-4b37-9614-d13ecba4ffc2',
'0993826f-0ee4-4b37-9614-d13ecba40000');
}
// Save to DB and navigate
final notifier = ref.read(
nConnectedDevicesProvider.notifier);
final name = device.name.isNotEmpty
? device.name
: 'Unknown Device';
final deviceCompanion =
ConnectedDevicesCompanion(
deviceName: Value(name),
deviceAddress: Value(device.id),
deviceType: Value(deviceTypeToString(
deviceTypeFromUuids(
device.serviceUuids))),
lastConnectedAt: Value(DateTime.now()),
);
final addResult = await notifier
.addConnectedDevice(deviceCompanion);
// Check if mounted before using context
if (!context.mounted) break;
if (addResult.isErr()) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
'Failed to save device: ${addResult.unwrapErr()}')),
);
} else {
context.go('/device/${device.id}');
}
break;
case Err(:final v):
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(
content: Text(
'Connection unsuccessful:\n${v.toString()}'),
));
break;
}
}
print('Tapped on ${device.id}');
},
child: DeviceListItem(
deviceName: deviceName,
deviceId: device.id,
type: deviceTypeFromUuids(device.serviceUuids),
),
);
},
);
},
);
},
);
},
),
),
// Bottom section: Scanning Animation and Retry Button (visible only when scanning)
Consumer(
// Use Consumer to get the controller for the retry button action
builder: (context, ref, child) {
final btController = ref
.watch(bluetoothProvider)
.asData
?.value; // Get controller safely
return StreamBuilder<bool>(
stream: btController?.isScanningStream ?? Stream<bool>.empty(),
initialData: false,
builder: (context, snapshot) {
final isScanning = snapshot.data ?? false;
// Show bottom section only if scanning
if (!isScanning) {
// Show only the retry button when not scanning (optional, could be hidden)
// For now, let's keep the button always visible but disabled when not scannable.
// A better approach might be to hide the button when not scanning.
// Let's show the button but potentially disabled later if controller is null.
return Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
onPressed: btController != null
? () {
// Retry scan ONLY when NOT currently scanning
if (mounted) {
setState(() {
_initialScanTriggered =
true; // Ensure state reflects scan attempt
_retryScanCounter++; // Increment key counter
});
}
btController.startScan(timeout: _scanDuration);
}
: null, // Disable if controller not ready
child: const Text('Retry Scan'),
),
);
}
// If scanning, show animation and button
return Container(
padding: const EdgeInsets.symmetric(
vertical: 8.0, horizontal: 16.0),
decoration: BoxDecoration(
color: Theme.of(context)
.scaffoldBackgroundColor
.withValues(alpha: 0.9), // Slight overlay effect
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, -4), // Shadow upwards
)
],
),
child: Column(
mainAxisSize: MainAxisSize.min, // Keep column compact
children: [
// Pass isScanning and the ValueKey
HorizontalScanningAnimation(
key: ValueKey(
_retryScanCounter), // Force state rebuild on counter change
isScanning: isScanning,
height: 40,
),
const SizedBox(height: 8),
ElevatedButton(
// Button does nothing if pressed *while* scanning.
// It just indicates the status.
onPressed: null, // Disable button while scanning
style: ElevatedButton.styleFrom(
disabledBackgroundColor: Theme.of(context)
.primaryColor
.withValues(alpha: 0.5), // Custom disabled color
disabledForegroundColor: Colors.white70,
),
child:
const Text('Scanning...'), // Just indicate status
),
const SizedBox(height: 8), // Add some bottom padding
],
),
);
},
);
}),
], // End of outer Column children
), // End of Scaffold
);
}
}