diff --git a/example/CHANGELOG.md b/example/CHANGELOG.md index f5487fd..ecd978e 100644 --- a/example/CHANGELOG.md +++ b/example/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.2.0 +* Improved scan button visibility - converted to prominent filled button with text label +* Enhanced "no devices found" state with explicit "Start Scan" call-to-action button +* Moved search field to app bar header for better accessibility +* Moved queue type settings to drawer menu as expandable section +* Added tooltip to Bluetooth availability icon (tap to view on mobile) +* Improved overall UI layout and navigation flow + ## 1.1.0 * Add support for `autoConnect` parameter * Display RSSI values in device details diff --git a/example/lib/home/scanner_screen.dart b/example/lib/home/scanner_screen.dart index 578fabc..05220ad 100644 --- a/example/lib/home/scanner_screen.dart +++ b/example/lib/home/scanner_screen.dart @@ -7,7 +7,6 @@ import 'package:universal_ble/universal_ble.dart'; import 'package:universal_ble_example/data/mock_universal_ble.dart'; import 'package:universal_ble_example/home/widgets/ble_availability_icon.dart'; import 'package:universal_ble_example/home/widgets/drawer.dart'; -import 'package:universal_ble_example/home/widgets/queue_selector_widget.dart'; import 'package:universal_ble_example/home/widgets/scan_filter_widget.dart'; import 'package:universal_ble_example/home/widgets/scanned_devices_placeholder_widget.dart'; import 'package:universal_ble_example/home/widgets/scanned_item_widget.dart'; @@ -270,6 +269,25 @@ class _ScannerScreenState extends State { bool get _isBluetoothAvailable => bleAvailabilityState == AvailabilityState.poweredOn; + String _getBluetoothAvailabilityTooltip() { + switch (bleAvailabilityState) { + case AvailabilityState.poweredOn: + return 'Bluetooth is on'; + case AvailabilityState.poweredOff: + return 'Bluetooth is off'; + case AvailabilityState.resetting: + return 'Bluetooth is resetting'; + case AvailabilityState.unauthorized: + return 'Bluetooth permission denied'; + case AvailabilityState.unsupported: + return 'Bluetooth not supported'; + case AvailabilityState.unknown: + return 'Bluetooth status unknown'; + case null: + return 'Checking Bluetooth status...'; + } + } + @override void setState(VoidCallback fn) { if (mounted) super.setState(fn); @@ -287,22 +305,6 @@ class _ScannerScreenState extends State { super.dispose(); } - void _showQueueBottomSheet() { - showModalBottomSheet( - context: context, - builder: (context) => QueueSelectorWidget( - queueType: _queueType, - onQueueTypeChanged: (queueType) { - setState(() { - _queueType = queueType; - UniversalBle.queueType = _queueType; - }); - Navigator.pop(context); - }, - ), - ); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -310,23 +312,73 @@ class _ScannerScreenState extends State { return Scaffold( backgroundColor: colorScheme.surface, - drawer: const AppDrawer(), + drawer: AppDrawer( + queueType: _queueType, + onQueueTypeChanged: (queueType) { + setState(() { + _queueType = queueType; + UniversalBle.queueType = _queueType; + }); + }, + ), appBar: AppBar( title: Row( children: [ - BleAvailabilityIcon(onAvailabilityStateChanged: (state) { - setState(() => bleAvailabilityState = state); - }), - const SizedBox(width: 12), Expanded( - child: const Text( - 'Scanner', - style: TextStyle( - fontWeight: FontWeight.bold, - letterSpacing: 0.5, + child: Container( + height: 40, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: TextField( + controller: _searchFilterController, + decoration: InputDecoration( + hintText: 'Search by name, ID, or services...', + prefixIcon: Icon( + Icons.search, + color: colorScheme.primary, + size: 20, + ), + suffixIcon: _searchFilterController.text.isNotEmpty + ? IconButton( + icon: Icon( + Icons.clear, + size: 20, + color: + colorScheme.onSurface.withValues(alpha: 0.6), + ), + onPressed: () { + _searchFilterController.clear(); + _saveScanFilters(); + setState(() {}); + }, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: colorScheme.surfaceContainerHighest, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + isDense: true, + ), + onChanged: (_) => setState(() {}), ), ), ), + const SizedBox(width: 12), + Tooltip( + message: _getBluetoothAvailabilityTooltip(), + triggerMode: TooltipTriggerMode.tap, + child: BleAvailabilityIcon(onAvailabilityStateChanged: (state) { + setState(() => bleAvailabilityState = state); + }), + ), ], ), elevation: 0, @@ -338,88 +390,52 @@ class _ScannerScreenState extends State { ), ), actions: [ - IconButton( - icon: Icon( - _isScanning ? Icons.stop_circle : Icons.play_arrow, - color: _isScanning ? colorScheme.error : colorScheme.primary, - ), - tooltip: _isScanning ? 'Stop Scan' : 'Start Scan', - onPressed: _isBluetoothAvailable - ? () async { - if (_isScanning) { - await UniversalBle.stopScan(); - setState(() { - _isScanning = false; - }); - } else { - await _startScan(); + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: FilledButton.icon( + onPressed: _isBluetoothAvailable + ? () async { + if (_isScanning) { + await UniversalBle.stopScan(); + setState(() { + _isScanning = false; + }); + } else { + await _startScan(); + } } - } - : null, - ), - IconButton( - icon: Icon( - Icons.queue, - color: colorScheme.onSurface, + : null, + icon: Icon( + _isScanning ? Icons.stop_circle : Icons.play_arrow, + size: 20, + ), + label: Text( + _isScanning ? 'Stop Scan' : 'Start Scan', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + style: FilledButton.styleFrom( + backgroundColor: + _isScanning ? colorScheme.error : colorScheme.primary, + foregroundColor: + _isScanning ? colorScheme.onError : colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: _isScanning ? 0 : 2, + ), ), - tooltip: 'Queue Type', - onPressed: _showQueueBottomSheet, ), ], ), body: Column( children: [ - // Search filter - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Container( - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: TextField( - controller: _searchFilterController, - decoration: InputDecoration( - hintText: 'Search by name, ID, or services...', - prefixIcon: Icon( - Icons.search, - color: colorScheme.primary, - ), - suffixIcon: _searchFilterController.text.isNotEmpty - ? IconButton( - icon: Icon( - Icons.clear, - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - onPressed: () { - _searchFilterController.clear(); - _saveScanFilters(); - setState(() {}); - }, - ) - : null, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - filled: true, - fillColor: colorScheme.surfaceContainerHighest, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - ), - onChanged: (_) => setState(() {}), - ), - ), - ), // Web Services input (only for web) if (kIsWeb) Padding( diff --git a/example/lib/home/widgets/drawer.dart b/example/lib/home/widgets/drawer.dart index 63c5236..0ebabec 100644 --- a/example/lib/home/widgets/drawer.dart +++ b/example/lib/home/widgets/drawer.dart @@ -7,7 +7,14 @@ import 'package:universal_ble_example/home/system_devices_screen.dart'; import 'package:url_launcher/url_launcher.dart'; class AppDrawer extends StatefulWidget { - const AppDrawer({super.key}); + final QueueType? queueType; + final Function(QueueType)? onQueueTypeChanged; + + const AppDrawer({ + super.key, + this.queueType, + this.onQueueTypeChanged, + }); @override State createState() => _AppDrawerState(); @@ -67,7 +74,50 @@ class _AppDrawerState extends State { onTap: () => _navigateToScreen(context, const SystemDevicesScreen()), ), - const Divider(), + if (widget.queueType != null && widget.onQueueTypeChanged != null) + ExpansionTile( + leading: const Icon(Icons.queue), + title: const Text('Queue Type'), + subtitle: Text( + _getQueueTypeLabel(widget.queueType!), + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + children: [ + _buildQueueOption( + context, + QueueType.global, + 'Global', + 'All commands from all devices execute sequentially in a single queue', + Icons.queue, + ), + const SizedBox(height: 8), + _buildQueueOption( + context, + QueueType.perDevice, + 'Per Device', + 'Commands for each device execute in separate queues', + Icons.devices, + ), + const SizedBox(height: 8), + _buildQueueOption( + context, + QueueType.none, + 'None', + 'All commands execute in parallel without queuing', + Icons.all_inclusive, + ), + ], + ), + ), + ], + ), FutureBuilder( future: PackageInfo.fromPlatform(), builder: (_, snapshot) => AboutListTile( @@ -128,4 +178,93 @@ class _AppDrawerState extends State { ), ); } + + String _getQueueTypeLabel(QueueType queueType) { + switch (queueType) { + case QueueType.global: + return 'Global'; + case QueueType.perDevice: + return 'Per Device'; + case QueueType.none: + return 'None'; + } + } + + Widget _buildQueueOption( + BuildContext context, + QueueType value, + String title, + String description, + IconData icon, + ) { + final colorScheme = Theme.of(context).colorScheme; + final isSelected = widget.queueType == value; + return InkWell( + onTap: () { + widget.onQueueTypeChanged?.call(value); + Navigator.pop(context); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isSelected + ? colorScheme.primaryContainer + : colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? colorScheme.primary + : colorScheme.outline.withValues(alpha: 0.2), + width: isSelected ? 2 : 1, + ), + ), + child: Row( + children: [ + Icon( + icon, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurface, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: isSelected + ? colorScheme.onPrimaryContainer + : colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: TextStyle( + fontSize: 11, + color: isSelected + ? colorScheme.onPrimaryContainer + .withValues(alpha: 0.7) + : colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ], + ), + ), + if (isSelected) + Icon( + Icons.check_circle, + color: colorScheme.primary, + size: 20, + ), + ], + ), + ), + ); + } } diff --git a/example/lib/home/widgets/scanned_devices_placeholder_widget.dart b/example/lib/home/widgets/scanned_devices_placeholder_widget.dart index db615ff..ed220a7 100644 --- a/example/lib/home/widgets/scanned_devices_placeholder_widget.dart +++ b/example/lib/home/widgets/scanned_devices_placeholder_widget.dart @@ -14,22 +14,19 @@ class ScannedDevicesPlaceholderWidget extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - InkWell( - onTap: onTap, - child: Container( - padding: const EdgeInsets.all(32), - decoration: BoxDecoration( - color: colorScheme.primaryContainer.withValues(alpha: 0.3), - shape: BoxShape.circle, - ), - child: Icon( - Icons.bluetooth_searching, - size: 80, - color: colorScheme.primary.withValues(alpha: 0.6), - ), + Container( + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: colorScheme.primaryContainer.withValues(alpha: 0.3), + shape: BoxShape.circle, + ), + child: Icon( + Icons.bluetooth_searching, + size: 80, + color: colorScheme.primary.withValues(alpha: 0.6), ), ), - const SizedBox(height: 24), + const SizedBox(height: 32), Text( 'No Devices Found', style: TextStyle( @@ -38,18 +35,42 @@ class ScannedDevicesPlaceholderWidget extends StatelessWidget { fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 8), + const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 48), child: Text( - 'Tap Scan button to discover nearby Bluetooth devices', + 'Start scanning to discover nearby Bluetooth devices', textAlign: TextAlign.center, style: TextStyle( - color: colorScheme.onSurface.withValues(alpha: 0.6), - fontSize: 14, + color: colorScheme.onSurface.withValues(alpha: 0.7), + fontSize: 16, ), ), ), + const SizedBox(height: 32), + FilledButton.icon( + onPressed: onTap, + icon: const Icon(Icons.play_arrow, size: 24), + label: const Text( + 'Start Scan', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + style: FilledButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + ), ], ), ), diff --git a/example/pubspec.lock b/example/pubspec.lock index 470487a..549707b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -632,7 +632,7 @@ packages: path: ".." relative: true source: path - version: "1.1.0" + version: "1.2.0" url_launcher: dependency: "direct main" description: