Add scan order functionality to ScannerScreen
- Introduced an enum `ScanOrder` to define sorting options for scanned devices. - Implemented logic to save and retrieve the selected scan order from shared preferences. - Added a popup menu for users to select the sorting method (Last Seen, Name, RSSI). - Updated the device list display to reflect the selected sorting order, enhancing user experience.
This commit is contained in:
@@ -15,6 +15,12 @@ import 'package:universal_ble_example/home/widgets/scanned_item_widget.dart';
|
|||||||
import 'package:universal_ble_example/peripheral_details/peripheral_detail_page.dart';
|
import 'package:universal_ble_example/peripheral_details/peripheral_detail_page.dart';
|
||||||
import 'package:universal_ble_example/widgets/scan_controller_scope.dart';
|
import 'package:universal_ble_example/widgets/scan_controller_scope.dart';
|
||||||
|
|
||||||
|
enum ScanOrder {
|
||||||
|
lastSeen,
|
||||||
|
name,
|
||||||
|
rssi,
|
||||||
|
}
|
||||||
|
|
||||||
class ScannerScreen extends StatefulWidget {
|
class ScannerScreen extends StatefulWidget {
|
||||||
const ScannerScreen({super.key});
|
const ScannerScreen({super.key});
|
||||||
|
|
||||||
@@ -37,6 +43,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
|
|
||||||
AvailabilityState? bleAvailabilityState;
|
AvailabilityState? bleAvailabilityState;
|
||||||
ScanFilter? scanFilter;
|
ScanFilter? scanFilter;
|
||||||
|
ScanOrder _scanOrder = ScanOrder.lastSeen;
|
||||||
final Map<String, bool> _isExpanded = {};
|
final Map<String, bool> _isExpanded = {};
|
||||||
final Map<String, int> _deviceAdFlashTrigger = {};
|
final Map<String, int> _deviceAdFlashTrigger = {};
|
||||||
|
|
||||||
@@ -108,6 +115,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
prefs.getString('scan_filter_manufacturer_data') ?? '';
|
prefs.getString('scan_filter_manufacturer_data') ?? '';
|
||||||
final searchFilter = prefs.getString('scan_filter_search') ?? '';
|
final searchFilter = prefs.getString('scan_filter_search') ?? '';
|
||||||
final webServices = prefs.getString('scan_filter_web_services') ?? '';
|
final webServices = prefs.getString('scan_filter_web_services') ?? '';
|
||||||
|
final scanOrderStr = prefs.getString('scan_order');
|
||||||
|
|
||||||
servicesFilterController.text = services;
|
servicesFilterController.text = services;
|
||||||
namePrefixController.text = namePrefix;
|
namePrefixController.text = namePrefix;
|
||||||
@@ -115,6 +123,21 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
_searchFilterController.text = searchFilter;
|
_searchFilterController.text = searchFilter;
|
||||||
_webServicesController.text = webServices;
|
_webServicesController.text = webServices;
|
||||||
|
|
||||||
|
if (scanOrderStr != null) {
|
||||||
|
switch (scanOrderStr) {
|
||||||
|
case 'lastSeen':
|
||||||
|
_scanOrder = ScanOrder.lastSeen;
|
||||||
|
break;
|
||||||
|
case 'name':
|
||||||
|
_scanOrder = ScanOrder.name;
|
||||||
|
break;
|
||||||
|
case 'rssi':
|
||||||
|
_scanOrder = ScanOrder.rssi;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
// Reconstruct the scan filter if any values exist
|
// Reconstruct the scan filter if any values exist
|
||||||
if (services.isNotEmpty ||
|
if (services.isNotEmpty ||
|
||||||
namePrefix.isNotEmpty ||
|
namePrefix.isNotEmpty ||
|
||||||
@@ -139,6 +162,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
await prefs.setString('scan_filter_search', _searchFilterController.text);
|
await prefs.setString('scan_filter_search', _searchFilterController.text);
|
||||||
await prefs.setString(
|
await prefs.setString(
|
||||||
'scan_filter_web_services', _webServicesController.text);
|
'scan_filter_web_services', _webServicesController.text);
|
||||||
|
await prefs.setString('scan_order', _scanOrder.name);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to save scan filters: $e');
|
debugPrint('Failed to save scan filters: $e');
|
||||||
}
|
}
|
||||||
@@ -302,6 +326,37 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<BleDevice> get _sortedDevices {
|
||||||
|
final list = List<BleDevice>.from(_filteredDevices);
|
||||||
|
switch (_scanOrder) {
|
||||||
|
case ScanOrder.lastSeen:
|
||||||
|
list.sort((a, b) {
|
||||||
|
final ta = a.timestamp ?? 0;
|
||||||
|
final tb = b.timestamp ?? 0;
|
||||||
|
return tb.compareTo(ta); // newest first
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ScanOrder.name:
|
||||||
|
list.sort((a, b) {
|
||||||
|
final na = (a.name ?? '').toLowerCase();
|
||||||
|
final nb = (b.name ?? '').toLowerCase();
|
||||||
|
if (na.isEmpty && nb.isEmpty) return 0;
|
||||||
|
if (na.isEmpty) return 1;
|
||||||
|
if (nb.isEmpty) return -1;
|
||||||
|
return na.compareTo(nb);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ScanOrder.rssi:
|
||||||
|
list.sort((a, b) {
|
||||||
|
final ra = a.rssi ?? -999;
|
||||||
|
final rb = b.rssi ?? -999;
|
||||||
|
return rb.compareTo(ra); // strongest first
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
bool get _isBluetoothAvailable =>
|
bool get _isBluetoothAvailable =>
|
||||||
bleAvailabilityState == AvailabilityState.poweredOn;
|
bleAvailabilityState == AvailabilityState.poweredOn;
|
||||||
|
|
||||||
@@ -623,7 +678,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const SizedBox(width: 4),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: _showScanFilterBottomSheet,
|
onPressed: _showScanFilterBottomSheet,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -652,30 +707,54 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const Spacer(),
|
||||||
TextButton.icon(
|
PopupMenuButton<ScanOrder>(
|
||||||
onPressed: () {
|
tooltip: 'Sort by',
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onSelected: (ScanOrder order) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_bleDevices.clear();
|
_scanOrder = order;
|
||||||
_hiddenDevices.clear();
|
|
||||||
_deviceAdFlashTrigger.clear();
|
|
||||||
});
|
});
|
||||||
|
_saveScanFilters();
|
||||||
},
|
},
|
||||||
icon: Icon(
|
itemBuilder: (context) => [
|
||||||
Icons.clear_all,
|
const PopupMenuItem(
|
||||||
size: 18,
|
value: ScanOrder.lastSeen,
|
||||||
color: colorScheme.error,
|
child: Text('Last seen'),
|
||||||
),
|
),
|
||||||
label: Text(
|
const PopupMenuItem(
|
||||||
'Clear',
|
value: ScanOrder.name,
|
||||||
style: TextStyle(
|
child: Text('Name'),
|
||||||
color: colorScheme.error,
|
),
|
||||||
fontSize: 12,
|
const PopupMenuItem(
|
||||||
fontWeight: FontWeight.w600,
|
value: ScanOrder.rssi,
|
||||||
|
child: Text('RSSI'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_scanOrder == ScanOrder.lastSeen
|
||||||
|
? 'Last seen'
|
||||||
|
: _scanOrder == ScanOrder.name
|
||||||
|
? 'Name'
|
||||||
|
: 'RSSI',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_drop_down,
|
||||||
|
size: 18,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -789,7 +868,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
)
|
)
|
||||||
: _scanController?.isScanning != true && _bleDevices.isEmpty
|
: _scanController?.isScanning != true && _bleDevices.isEmpty
|
||||||
? ScannedDevicesPlaceholderWidget(onTap: _startScan)
|
? ScannedDevicesPlaceholderWidget(onTap: _startScan)
|
||||||
: _filteredDevices.isEmpty
|
: _sortedDevices.isEmpty
|
||||||
? Center(
|
? Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -817,12 +896,11 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
|||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
itemCount: _filteredDevices.length,
|
itemCount: _sortedDevices.length,
|
||||||
separatorBuilder: (context, index) =>
|
separatorBuilder: (context, index) =>
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
BleDevice device = _filteredDevices[
|
BleDevice device = _sortedDevices[index];
|
||||||
_filteredDevices.length - index - 1];
|
|
||||||
return ScannedItemWidget(
|
return ScannedItemWidget(
|
||||||
bleDevice: device,
|
bleDevice: device,
|
||||||
adFlashTrigger:
|
adFlashTrigger:
|
||||||
|
|||||||
Reference in New Issue
Block a user