diff --git a/README.md b/README.md index d1e7289..3d92697 100644 --- a/README.md +++ b/README.md @@ -847,6 +847,10 @@ Future resetBleState() async { +## Example app + +This repo includes an [example app](example/) you can run to try the API. For a full-blown app, check [Universal-BLE](https://github.com/Navideck/Universal-BLE). + ## Low level API For more granular control, you can use the [Low-Level API](README.low_level.md). This API is "Device ID"-based, offering greater flexibility by enabling direct calls without the need for object instances. @@ -855,7 +859,7 @@ For more granular control, you can use the [Low-Level API](README.low_level.md). Here are some of the apps leveraging the power of `universal_ble`: -- [**Universal BLE**](https://apps.apple.com/app/id6756538573) ([App Store](https://apps.apple.com/app/id6756538573) | [Play Store](https://play.google.com/store/apps/details?id=com.navideck.universalble) | [Web](https://navideck.github.io/universal_ble)) - A comprehensive developer tool for exploring and testing Bluetooth Low Energy (BLE) devices. It enables scanning for nearby BLE devices, connecting to peripherals, discovering and exploring services, characteristics, and descriptors. Supports reading and writing characteristic values, enabling notifications and indications, viewing device information and signal strength, and provides detailed logging of BLE operations. Perfect for developers, engineers, and hobbyist tinkerers working with BLE-enabled devices across iOS, Android, macOS, Windows, Linux & Web. +- [**Universal BLE**](https://github.com/Navideck/Universal-BLE) - A comprehensive developer tool for exploring and testing Bluetooth Low Energy (BLE) devices. It enables scanning for nearby BLE devices, connecting to peripherals, discovering and exploring services, characteristics, and descriptors. Supports reading and writing characteristic values, enabling notifications and indications, viewing device information and signal strength, and provides detailed logging of BLE operations. Perfect for developers, engineers, and hobbyist tinkerers working with BLE-enabled devices across iOS, Android, macOS, Windows, Linux & Web. - [**BT Cam**](https://btcam.app) - A Bluetooth remote app for DSLR and mirrorless cameras. Compatible with Canon, Nikon, Sony, Fujifilm, GoPro, Olympus, Panasonic, Pentax, and Blackmagic. Built using Universal BLE to connect and control cameras across iOS, Android, macOS, Windows, Linux & Web. - [**TukToro**](https://tuktoro.com/en/pages/download-math-learning-app) - Interactive math learning app for kids. Available on iPad and Android tablets, featuring hand-drawn levels, didactic learning games, and ad-free child-safe environment. - [**BikeControl**](https://github.com/jonasbark/swiftcontrol) - Control your favorite trainer app using Zwift Click, Zwift Ride, Zwift Play, Shimano Di2, or other similar devices. Enables virtual gear shifting, steering, workout intensity adjustment, and more across iOS, Android, macOS, Windows, and Linux. diff --git a/example/CHANGELOG.md b/example/CHANGELOG.md deleted file mode 100644 index 9201a00..0000000 --- a/example/CHANGELOG.md +++ /dev/null @@ -1,33 +0,0 @@ -## 1.1.0 -* **Services & Characteristics:** - * Add property filtering for characteristics with visual filter chips - * Add navigation buttons to navigate between characteristics (previous/next) - * Improve service sorting (favorites first, system services last) - * Enhance services list UI with better filtering and navigation - * Improve format for discovered services to be more detailed and human-readable - * Move "Copy Services" button to Services panel header - -* **Company & Manufacturer Data:** - * Display company name based on company identifier from manufacturer data - * Show and filter by company name in device list - * Enhanced search functionality - now supports searching by company name - -* **Scanning & Device Discovery:** - * 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 - * Display RSSI values in device details - * Show last advertisement (name, RSSI, timestamp) on scan screen and list of received ads on detail screen; both flash on new advertisement. Scanning continues when opening a device. - -* **UI & Navigation:** - * 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 - -* **Functionality:** - * Add support for `autoConnect` parameter - * Persist filters across app sessions - * Fix clear log button functionality - -## 1.0.0 -* Initial release \ No newline at end of file diff --git a/example/README.md b/example/README.md index 098071f..d991add 100644 --- a/example/README.md +++ b/example/README.md @@ -1,68 +1,3 @@ -Universal BLE is an open source powerful developer tool for exploring and testing Bluetooth Low Energy (BLE) devices. +# universal_ble_example -Universal BLE is a comprehensive cross-platform developer tool that empowers developers, engineers, and hobbyists to explore, test, and debug Bluetooth Low Energy (BLE) devices with ease. Built with Flutter and supporting multiple platforms, this powerful tool provides everything you need to work with BLE-enabled devices. - -Built on the open source universal_ble Flutter plugin - one of the most comprehensive cross-platform BLE solutions available. Package: https://pub.dev/packages/universal_ble - -Key Features - -Device Discovery -- Scan for nearby BLE devices in real-time -- Filter devices by services, manufacturer data, or name prefixes -- View device information, signal strength, and connection status -- Access already connected system devices - -Connection Management -- Connect and disconnect from BLE peripherals -- Monitor connection state changes in real-time -- Support for multiple simultaneous connections -- Automatic reconnection handling - -Service & Characteristic Exploration -- Discover all services and characteristics on connected devices -- Browse GATT services with detailed UUID information -- View characteristic properties and descriptors -- Intuitive interface for navigating device capabilities - -Data Operations -- Read characteristic values in real-time -- Write data to characteristics (with or without response) -- Subscribe to notifications and indications -- Monitor value changes with live updates - -Pairing & Security -- Initiate pairing with BLE devices -- Check pairing status and monitor pairing state changes -- Support for encrypted characteristics -- Unpair devices when needed - -Advanced Features -- Request MTU (Maximum Transmission Unit) for optimized data transfer -- Command queue system for reliable operation -- Configurable timeouts for all operations -- Comprehensive error handling with detailed error codes -- UUID format agnostic (supports all UUID formats) -- Detailed logging for debugging BLE operations - -Cross-Platform Support -- Native support with consistent API across all platforms -- Platform-specific optimizations where needed - -Perfect For - -- Developers building BLE-enabled applications -- Engineers testing and debugging BLE devices -- Hobbyists exploring IoT and smart device capabilities -- Students learning about Bluetooth Low Energy technology -- QA Teams validating BLE device functionality - -Why Universal BLE? - -Universal BLE is built on the open source universal_ble Flutter plugin, one of the most comprehensive cross-platform BLE solutions available. With support for all major platforms and a unified API, you can develop and test BLE applications with confidence. - -Whether you're developing a new BLE product, debugging an existing device, or simply exploring the world of Bluetooth Low Energy, Universal BLE provides the tools you need to succeed. - -Open Source & Free -This app is built on open source technology and is free to use. The underlying plugin is available under the BSD 3-Clause License, making it perfect for both commercial and personal projects. - -Universal BLE - Your complete solution for Bluetooth Low Energy development and testing. \ No newline at end of file +Demonstrates how to use the universal_ble plugin. diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 539e152..500663a 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -23,7 +23,7 @@ if (flutterVersionName == null) { } android { - namespace = "com.navideck.universalble" + namespace "com.navideck.universal_ble_example" compileSdkVersion 36 ndkVersion flutter.ndkVersion @@ -41,7 +41,7 @@ android { } defaultConfig { - applicationId = "com.navideck.universalble" + applicationId "com.navideck.universal_ble_example" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 30fcaa8..191b9de 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - Universal BLE + universal_ble_example CFBundlePackageType APPL CFBundleShortVersionString diff --git a/example/lib/data/company_identifier_service.dart b/example/lib/data/company_identifier_service.dart deleted file mode 100644 index fac339e..0000000 --- a/example/lib/data/company_identifier_service.dart +++ /dev/null @@ -1,278 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:yaml/yaml.dart'; - -/// Service for loading and querying company identifiers from YAML file -class CompanyIdentifierService { - static CompanyIdentifierService? _instance; - static CompanyIdentifierService get instance { - _instance ??= CompanyIdentifierService._(); - return _instance!; - } - - CompanyIdentifierService._(); - - final Map _companyIdToName = {}; - final Map _companyNameToId = {}; - bool _isLoading = false; - bool _isLoaded = false; - bool _hasWarnedAboutNotLoaded = false; - - /// Parse company ID from a value (String hex or int) - /// Supports formats: "0x1053", "0x004C", "1053" (decimal), "4C" (hex), or integer value - /// Non-prefixed strings are parsed as decimal if they contain only digits, - /// or as hexadecimal if they contain hex characters (a-fA-F) - int? _parseCompanyId(dynamic value) { - if (value is String) { - final trimmed = value.trim(); - if (trimmed.toLowerCase().startsWith('0x')) { - return int.tryParse(trimmed.substring(2), radix: 16); - } else { - // Try parsing as hex first (if it contains letters), then decimal - if (trimmed.contains(RegExp(r'[a-fA-F]'))) { - return int.tryParse(trimmed, radix: 16); - } else { - return int.tryParse(trimmed); - } - } - } else if (value is int) { - return value; - } - return null; - } - - /// Load company identifiers from YAML file - /// - /// **Important:** This method should be called on app startup before using - /// any query methods (e.g., [getCompanyName], [getCompanyIdFromName], etc.). - /// If query methods are called before this method completes, they will return - /// null/empty values and log a warning. - Future load() async { - if (_isLoaded || _isLoading) return; - _isLoading = true; - - try { - final String yamlString = - await rootBundle.loadString('assets/company_identifiers.yaml'); - final YamlMap yaml = loadYaml(yamlString) as YamlMap; - final YamlList? companyIdentifiers = - yaml['company_identifiers'] as YamlList?; - - if (companyIdentifiers == null) { - _companyIdToName.clear(); - _companyNameToId.clear(); - _isLoaded = true; - return; - } - - _companyIdToName.clear(); - _companyNameToId.clear(); - - for (var entry in companyIdentifiers) { - if (entry is! YamlMap) continue; - - final value = entry['value']; - final name = entry['name']; - - if (value == null || name == null) continue; - - final companyId = _parseCompanyId(value); - - if (companyId != null && name is String) { - _companyIdToName[companyId] = name; - // Store case-insensitive lookup for company names - _companyNameToId[name.toLowerCase()] = companyId; - } - } - - // Reset warning flag after successful load - _hasWarnedAboutNotLoaded = false; - _isLoaded = true; - } on PlatformException catch (e) { - // Handle file loading errors (e.g., file not found, permission issues) - debugPrint( - 'CompanyIdentifierService: Failed to load company identifiers file: ${e.message}', - ); - _companyIdToName.clear(); - _companyNameToId.clear(); - _isLoaded = true; - } on YamlException catch (e) { - // Handle YAML parsing errors (e.g., invalid YAML syntax) - debugPrint( - 'CompanyIdentifierService: Failed to parse YAML: ${e.message}', - ); - _companyIdToName.clear(); - _companyNameToId.clear(); - _isLoaded = true; - } catch (e, stackTrace) { - // Handle any other unexpected errors - debugPrint( - 'CompanyIdentifierService: Unexpected error loading company identifiers: $e', - ); - debugPrint('Stack trace: $stackTrace'); - _companyIdToName.clear(); - _companyNameToId.clear(); - _isLoaded = true; - } finally { - _isLoading = false; - } - } - - /// Get company name from company ID - /// - /// Returns the company name for the given [companyId], or `null` if: - /// - The company ID is not found in the loaded data - /// - The data has not been loaded yet (see [load] method) - /// - /// **Important:** The [load] method must be called on app startup before - /// using this method. If called before data is loaded, this method will - /// return `null` and log a warning (only once). - String? getCompanyName(int companyId) { - if (!_isLoaded) { - if (!_hasWarnedAboutNotLoaded) { - _hasWarnedAboutNotLoaded = true; - debugPrint( - 'CompanyIdentifierService: getCompanyName called before load() completed. ' - 'Company names will not be available until load() is called on app startup.', - ); - } - return null; - } - return _companyIdToName[companyId]; - } - - /// Get company name from hex string (e.g., "0x1053" or "0x004C") - String? getCompanyNameFromHex(String hexString) { - final companyId = _parseCompanyId(hexString); - if (companyId == null) return null; - return getCompanyName(companyId); - } - - /// Get company ID from company name (case-insensitive) - /// - /// Returns the company ID for the given [companyName], or `null` if: - /// - The company name is not found in the loaded data - /// - The data has not been loaded yet (see [load] method) - /// - /// **Important:** The [load] method must be called on app startup before - /// using this method. If called before data is loaded, this method will - /// return `null` and log a warning (only once). - int? getCompanyIdFromName(String companyName) { - if (!_isLoaded) { - if (!_hasWarnedAboutNotLoaded) { - _hasWarnedAboutNotLoaded = true; - debugPrint( - 'CompanyIdentifierService: getCompanyIdFromName called before load() completed. ' - 'Company identifiers will not be available until load() is called on app startup.', - ); - } - return null; - } - return _companyNameToId[companyName.toLowerCase()]; - } - - /// Get all company names (for filtering/searching) - /// - /// Returns a list of all company names, or an empty list if: - /// - No company identifiers are loaded - /// - The data has not been loaded yet (see [load] method) - /// - /// **Important:** The [load] method must be called on app startup before - /// using this method. If called before data is loaded, this method will - /// return an empty list and log a warning (only once). - List getAllCompanyNames() { - if (!_isLoaded) { - if (!_hasWarnedAboutNotLoaded) { - _hasWarnedAboutNotLoaded = true; - debugPrint( - 'CompanyIdentifierService: getAllCompanyNames called before load() completed. ' - 'Company names will not be available until load() is called on app startup.', - ); - } - return []; - } - return _companyIdToName.values.toList(); - } - - /// Check if a string matches a company name (case-insensitive partial match) - /// - /// Returns `false` if: - /// - No match is found - /// - The data has not been loaded yet (see [load] method) - /// - /// **Important:** The [load] method must be called on app startup before - /// using this method. If called before data is loaded, this method will - /// return `false` and log a warning (only once). - bool matchesCompanyName(String query) { - if (!_isLoaded) { - if (!_hasWarnedAboutNotLoaded) { - _hasWarnedAboutNotLoaded = true; - debugPrint( - 'CompanyIdentifierService: matchesCompanyName called before load() completed. ' - 'Company name matching will not be available until load() is called on app startup.', - ); - } - return false; - } - final lowerQuery = query.toLowerCase(); - return _companyNameToId.keys.any((name) => name.contains(lowerQuery)); - } - - /// Find company IDs that match a company name query (case-insensitive partial match) - /// - /// Returns a list of matching company IDs, or an empty list if: - /// - No matches are found - /// - The data has not been loaded yet (see [load] method) - /// - /// **Important:** The [load] method must be called on app startup before - /// using this method. If called before data is loaded, this method will - /// return an empty list and log a warning (only once). - List findCompanyIdsByName(String query) { - if (!_isLoaded) { - if (!_hasWarnedAboutNotLoaded) { - _hasWarnedAboutNotLoaded = true; - debugPrint( - 'CompanyIdentifierService: findCompanyIdsByName called before load() completed. ' - 'Company identifier search will not be available until load() is called on app startup.', - ); - } - return []; - } - final lowerQuery = query.toLowerCase(); - return _companyNameToId.entries - .where((entry) => entry.key.contains(lowerQuery)) - .map((entry) => entry.value) - .toList(); - } - - /// Parse a company identifier from a string (either by name or by ID) - /// - /// This method first tries to find the company by name (case-insensitive). - /// If not found, it attempts to parse the string as a company ID: - /// - Supports hex format with "0x" prefix (e.g., "0x004C") - /// - Supports hex format without prefix if it contains hex characters (e.g., "4C") - /// - Falls back to decimal parsing if no hex characters are present (e.g., "76") - /// - /// Returns the company ID if found/parsed successfully, null otherwise. - int? parseCompanyIdentifier(String value) { - final trimmed = value.trim(); - - // First, try to find by company name (case-insensitive) - final companyId = getCompanyIdFromName(trimmed); - if (companyId != null) { - return companyId; - } - - // If not found by name, try parsing as company ID - if (trimmed.toLowerCase().startsWith('0x')) { - return int.tryParse(trimmed.substring(2), radix: 16); - } else { - // Try parsing as hex first (if it contains letters), then decimal - if (trimmed.contains(RegExp(r'[a-fA-F]'))) { - return int.tryParse(trimmed, radix: 16); - } else { - return int.tryParse(trimmed); - } - } - } -} diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart index 0c7d7a5..6337dad 100644 --- a/example/lib/data/mock_universal_ble.dart +++ b/example/lib/data/mock_universal_ble.dart @@ -5,7 +5,6 @@ import 'package:universal_ble/universal_ble.dart'; /// Mock implementation of [UniversalBlePlatform] for testing class MockUniversalBle extends UniversalBlePlatform { - final Map _connectionStateMap = {}; final _mockBleDevice = BleDevice( name: 'MockDevice', deviceId: 'MockDeviceId', @@ -16,20 +15,12 @@ class MockUniversalBle extends UniversalBlePlatform { Uint8List _serviceValue = utf8.encode('Result'); bool _isScanning = false; - final BleService _mockService = BleService('180a', [ - BleCharacteristic.withMetaData( - deviceId: 'MockDeviceId', - serviceId: '180a', - uuid: '220a', - properties: [ - CharacteristicProperty.read, - CharacteristicProperty.write, - CharacteristicProperty.notify, - ], - descriptors: [ - BleDescriptor('220b'), - ], - ), + final BleService _mockService = BleService('180', [ + BleCharacteristic('180A', [ + CharacteristicProperty.read, + CharacteristicProperty.write, + CharacteristicProperty.notify, + ], []), ]); @override @@ -53,15 +44,13 @@ class MockUniversalBle extends UniversalBlePlatform { @override Future connect(String deviceId, - {bool autoConnect = false, Duration? connectionTimeout}) async { + {Duration? connectionTimeout, bool autoConnect = false}) async { updateConnection(deviceId, true); - _connectionStateMap[deviceId] = BleConnectionState.connected; } @override Future disconnect(String deviceId) async { updateConnection(deviceId, false); - _connectionStateMap[deviceId] = BleConnectionState.disconnected; } @override @@ -83,7 +72,7 @@ class MockUniversalBle extends UniversalBlePlatform { @override Future> getSystemDevices(List? withServices) async { - return [_mockBleDevice]; + return []; } @override @@ -114,12 +103,6 @@ class MockUniversalBle extends UniversalBlePlatform { return 512; } - @override - Future readRssi(String deviceId) async { - await Future.delayed(const Duration(milliseconds: 500)); - return -50; // Mock RSSI value in dBm - } - @override Future setNotifiable(String deviceId, String service, String characteristic, BleInputProperty bleInputProperty) async {} @@ -142,18 +125,22 @@ class MockUniversalBle extends UniversalBlePlatform { } @override - Future getConnectionState(String deviceId) async { - return _connectionStateMap[deviceId] ?? BleConnectionState.disconnected; + Future getConnectionState(String deviceId) { + throw UnimplementedError(); } @override - Future disableBluetooth() async { - return true; + Future disableBluetooth() { + throw UnimplementedError(); } @override - Future requestPermissions( - {bool withAndroidFineLocation = false}) async { - return; + Future requestPermissions({bool withAndroidFineLocation = false}) { + throw UnimplementedError(); + } + + @override + Future readRssi(String deviceId) { + throw UnimplementedError(); } } diff --git a/example/lib/data/prefs_async.dart b/example/lib/data/prefs_async.dart deleted file mode 100644 index fa6b7c2..0000000 --- a/example/lib/data/prefs_async.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; - -/// Shared [SharedPreferencesAsync] instance for the example app. -/// Use this instead of the legacy [SharedPreferences.getInstance()] API. -final prefsAsync = SharedPreferencesAsync(); diff --git a/example/lib/data/saved_scan_filter.dart b/example/lib/data/saved_scan_filter.dart deleted file mode 100644 index 38fc6bb..0000000 --- a/example/lib/data/saved_scan_filter.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'dart:convert'; - -/// A saved scan filter preset (name + the three filter field values). -class SavedScanFilter { - const SavedScanFilter({ - required this.name, - required this.services, - required this.namePrefix, - required this.manufacturerData, - }); - - final String name; - final String services; - final String namePrefix; - final String manufacturerData; - - Map toJson() => { - 'name': name, - 'services': services, - 'namePrefix': namePrefix, - 'manufacturerData': manufacturerData, - }; - - factory SavedScanFilter.fromJson(Map json) { - return SavedScanFilter( - name: json['name'] as String? ?? '', - services: json['services'] as String? ?? '', - namePrefix: json['namePrefix'] as String? ?? '', - manufacturerData: json['manufacturerData'] as String? ?? '', - ); - } - - static List fromJsonList(String? jsonString) { - if (jsonString == null || jsonString.isEmpty) return []; - try { - final list = jsonDecode(jsonString) as List?; - if (list == null) return []; - return list - .map((e) => SavedScanFilter.fromJson(e as Map)) - .toList(); - } catch (_) { - return []; - } - } - - static String toJsonList(List filters) { - return jsonEncode(filters.map((e) => e.toJson()).toList()); - } - - SavedScanFilter copyWith({ - String? name, - String? services, - String? namePrefix, - String? manufacturerData, - }) { - return SavedScanFilter( - name: name ?? this.name, - services: services ?? this.services, - namePrefix: namePrefix ?? this.namePrefix, - manufacturerData: manufacturerData ?? this.manufacturerData, - ); - } -} diff --git a/example/lib/data/scan_controller.dart b/example/lib/data/scan_controller.dart deleted file mode 100644 index a3fbf85..0000000 --- a/example/lib/data/scan_controller.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:universal_ble/universal_ble.dart'; -import 'package:universal_ble_example/data/company_identifier_service.dart'; -import 'package:universal_ble_example/data/prefs_async.dart'; - -/// Shared controller for BLE scan state so the scan button can be shown -/// on both the scanner screen and the peripheral detail screen. -class ScanController extends ChangeNotifier { - bool _isScanning = false; - bool get isScanning => _isScanning; - - /// Syncs [isScanning] with the actual native state (e.g. after app restart). - Future syncState() async { - final value = await UniversalBle.isScanning(); - if (_isScanning != value) { - _isScanning = value; - notifyListeners(); - } - } - - Future startScan() async { - if (_isScanning) return; - try { - final filter = await _loadFilterFromPrefs(); - final platformConfig = await _loadPlatformConfigFromPrefs(); - - await UniversalBle.startScan( - scanFilter: filter, - platformConfig: platformConfig, - ); - _isScanning = true; - notifyListeners(); - } catch (e) { - _isScanning = false; - notifyListeners(); - rethrow; - } - } - - Future stopScan() async { - if (!_isScanning) return; - try { - await UniversalBle.stopScan(); - _isScanning = false; - notifyListeners(); - } catch (e) { - rethrow; - } - } - - Future _loadFilterFromPrefs() async { - final services = - (await prefsAsync.getString('scan_filter_services')) ?? ''; - final namePrefix = - (await prefsAsync.getString('scan_filter_name_prefix')) ?? ''; - final manufacturerData = - (await prefsAsync.getString('scan_filter_manufacturer_data')) ?? ''; - - if (services.isEmpty && - namePrefix.isEmpty && - manufacturerData.isEmpty) { - return null; - } - - await CompanyIdentifierService.instance.load(); - - final serviceUUids = []; - if (services.isNotEmpty) { - for (final s in services.split(',').map((e) => e.trim())) { - if (s.isEmpty) continue; - try { - serviceUUids.add(BleUuidParser.string(s)); - } on FormatException catch (_) { - continue; - } - } - } - - final namePrefixes = namePrefix.isEmpty - ? [] - : namePrefix.split(',').map((e) => e.trim()).toList(); - - final manufacturerDataFilters = []; - if (manufacturerData.isNotEmpty) { - final companyService = CompanyIdentifierService.instance; - for (final s in manufacturerData - .split(',') - .map((e) => e.trim()) - .where((s) => s.isNotEmpty)) { - final id = companyService.parseCompanyIdentifier(s); - if (id != null) { - manufacturerDataFilters.add(ManufacturerDataFilter( - companyIdentifier: id, - )); - } - } - } - - if (serviceUUids.isEmpty && - namePrefixes.isEmpty && - manufacturerDataFilters.isEmpty) { - return null; - } - - return ScanFilter( - withServices: serviceUUids, - withNamePrefix: namePrefixes, - withManufacturerData: manufacturerDataFilters, - ); - } - - Future _loadPlatformConfigFromPrefs() async { - WebOptions? web; - if (kIsWeb) { - final webServices = - (await prefsAsync.getString('scan_filter_web_services')) ?? ''; - if (webServices.isNotEmpty) { - final list = webServices - .split(',') - .where((s) => s.trim().isNotEmpty) - .map((s) { - try { - return BleUuidParser.string(s.trim()); - } catch (_) { - return s.trim(); - } - }).toList(); - web = WebOptions(optionalServices: list); - } - } - return PlatformConfig( - android: AndroidOptions(scanMode: AndroidScanMode.lowLatency), - web: web, - ); - } -} diff --git a/example/lib/data/storage_service.dart b/example/lib/data/storage_service.dart deleted file mode 100644 index 4abb72d..0000000 --- a/example/lib/data/storage_service.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:universal_ble_example/data/prefs_async.dart'; - -class StorageService { - StorageService._(); - static StorageService? _instance; - static StorageService get instance => _instance ??= StorageService._(); - - Future init() async { - // SharedPreferencesAsync is used via prefsAsync; no async init needed. - } - - Future setFavoriteServices(List services) async { - await prefsAsync.setStringList('favorite_services', services); - } - - Future> getFavoriteServices() async => - (await prefsAsync.getStringList('favorite_services')) ?? []; -} diff --git a/example/lib/data/utils.dart b/example/lib/data/utils.dart deleted file mode 100644 index 447b6e1..0000000 --- a/example/lib/data/utils.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:universal_ble/universal_ble.dart'; - -bool isSystemService(String uuid) { - final normalized = uuid.toUpperCase().replaceAll('-', ''); - return normalized == '00001800' || - normalized == '00001801' || - normalized == '0000180A' || - normalized.startsWith('000018'); -} - -/// Sorts BLE services with the following priority: -/// 1. Favorite services first -/// 2. System services last -/// 3. Other services in between -List sortBleServices( - List services, { - Set? favoriteServices, -}) { - final sortedServices = List.from(services); - sortedServices.sort((a, b) { - final aIsFavorite = favoriteServices?.contains(a.uuid) ?? false; - final bIsFavorite = favoriteServices?.contains(b.uuid) ?? false; - if (aIsFavorite != bIsFavorite) { - return aIsFavorite ? -1 : 1; - } - final aIsSystem = isSystemService(a.uuid); - final bIsSystem = isSystemService(b.uuid); - if (aIsSystem != bIsSystem) { - return aIsSystem ? 1 : -1; - } - return 0; - }); - return sortedServices; -} - -/// Returns a list of all filtered characteristics with their parent services. -/// Services are sorted (favorites first, system services last). -/// Characteristics are filtered by property filters if provided. -List<({BleService service, BleCharacteristic characteristic})> - getFilteredBleCharacteristics( - List services, { - Set? favoriteServices, - Set? propertyFilters, -}) { - final List<({BleService service, BleCharacteristic characteristic})> result = - []; - - // Sort services: favorites first, then system services, then others - final sortedServices = sortBleServices( - services, - favoriteServices: favoriteServices, - ); - - for (var service in sortedServices) { - for (var char in service.characteristics) { - // Filter by properties if filters are selected - if (propertyFilters != null && propertyFilters.isNotEmpty) { - if (char.properties.any((prop) => propertyFilters.contains(prop))) { - result.add((service: service, characteristic: char)); - } - } else { - result.add((service: service, characteristic: char)); - } - } - } - return result; -} - -/// Finds the next or previous characteristic in a filtered list. -/// -/// [filtered] - The filtered list of (service, characteristic) tuples -/// [currentCharacteristicUuid] - The UUID of the currently selected characteristic -/// [next] - If true, finds the next item; if false, finds the previous item -/// -/// Returns the next/previous item, or the first item if current is not found, -/// or null if the list is empty. -({BleService service, BleCharacteristic characteristic})? - navigateToAdjacentCharacteristic( - List<({BleService service, BleCharacteristic characteristic})> filtered, - String currentCharacteristicUuid, - bool next, -) { - if (filtered.isEmpty) return null; - - final currentIndex = filtered.indexWhere( - (item) => item.characteristic.uuid == currentCharacteristicUuid, - ); - - if (currentIndex == -1) { - // Current selection not in filtered list, return first - return filtered.first; - } - - if (next) { - // Navigate to next (with wrapping) - final nextIndex = (currentIndex + 1) % filtered.length; - return filtered[nextIndex]; - } else { - // Navigate to previous (with wrapping) - final previousIndex = - currentIndex > 0 ? currentIndex - 1 : filtered.length - 1; - return filtered[previousIndex]; - } -} diff --git a/example/lib/home/home.dart b/example/lib/home/home.dart new file mode 100644 index 0000000..0ab9c57 --- /dev/null +++ b/example/lib/home/home.dart @@ -0,0 +1,356 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:universal_ble/universal_ble.dart'; +import 'package:universal_ble_example/data/mock_universal_ble.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'; +import 'package:universal_ble_example/peripheral_details/peripheral_detail_page.dart'; +import 'package:universal_ble_example/widgets/platform_button.dart'; +import 'package:universal_ble_example/widgets/responsive_buttons_grid.dart'; + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final _bleDevices = []; + final _hiddenDevices = []; + bool _isScanning = false; + QueueType _queueType = QueueType.global; + TextEditingController servicesFilterController = TextEditingController(); + TextEditingController namePrefixController = TextEditingController(); + TextEditingController manufacturerDataController = TextEditingController(); + StreamSubscription? _availabilityStreamSubscription; + + bool get isTrackingAvailabilityState => + _availabilityStreamSubscription != null; + AvailabilityState? bleAvailabilityState; + ScanFilter? scanFilter; + + @override + void initState() { + super.initState(); + + /// Set mock instance for testing + if (const bool.fromEnvironment('MOCK')) { + UniversalBle.setInstance(MockUniversalBle()); + } + + /// Setup queue and timeout + UniversalBle.queueType = _queueType; + UniversalBle.timeout = const Duration(seconds: 10); + + UniversalBle.scanStream.listen((result) { + // log(result.toString()); + // If device is already in hidden devices, skip + if (_hiddenDevices.any((e) => e.deviceId == result.deviceId)) { + // debugPrint("Skipping hidden device: ${result.deviceId}"); + return; + } + int index = _bleDevices.indexWhere((e) => e.deviceId == result.deviceId); + if (index == -1) { + _bleDevices.add(result); + } else { + if (result.name == null && _bleDevices[index].name != null) { + result.name = _bleDevices[index].name; + } + _bleDevices[index] = result; + } + setState(() {}); + }); + + // UniversalBle.onQueueUpdate = (String id, int remainingItems) { + // debugPrint("Queue: $id RemainingItems: $remainingItems"); + // }; + + UniversalBle.isScanning().then((value) { + debugPrint("Is Scanning: $value"); + setState(() { + _isScanning = value; + }); + }); + } + + void trackAvailabilityState() { + _availabilityStreamSubscription = UniversalBle.availabilityStream.listen( + (state) { + setState(() { + bleAvailabilityState = state; + }); + }, + ); + setState(() {}); + } + + Future startScan() async { + await UniversalBle.startScan(scanFilter: scanFilter); + } + + Future _getSystemDevices() async { + // For macOS and iOS, it is recommended to set a filter to get system devices + if ((defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.iOS) && + (scanFilter?.withServices ?? []).isEmpty) { + showSnackbar( + "No services filter was set for getting system connected devices. Using default services..."); + } + + List devices = await UniversalBle.getSystemDevices( + withServices: scanFilter?.withServices, + ); + if (devices.isEmpty) { + showSnackbar("No System Connected Devices Found"); + } + setState(() { + _bleDevices.clear(); + _bleDevices.addAll(devices); + }); + } + + void _showScanFilterBottomSheet() { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) { + return ScanFilterWidget( + servicesFilterController: servicesFilterController, + namePrefixController: namePrefixController, + manufacturerDataController: manufacturerDataController, + onScanFilter: (ScanFilter? filter) { + setState(() { + scanFilter = filter; + }); + }, + ); + }, + ); + } + + void showSnackbar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + + @override + void dispose() { + _availabilityStreamSubscription?.cancel(); + servicesFilterController.dispose(); + namePrefixController.dispose(); + manufacturerDataController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Universal BLE'), + elevation: 4, + actions: [ + if (_isScanning) + const Padding( + padding: EdgeInsets.all(8.0), + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + )), + ), + ], + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: ResponsiveButtonsGrid( + children: [ + PlatformButton( + text: 'Start Scan', + onPressed: () async { + setState(() { + _bleDevices.clear(); + _isScanning = true; + }); + try { + await startScan(); + } catch (e) { + setState(() { + _isScanning = false; + }); + showSnackbar(e.toString()); + } + }, + ), + PlatformButton( + text: 'Stop Scan', + onPressed: () async { + await UniversalBle.stopScan(); + setState(() { + _isScanning = false; + }); + }, + ), + if (BleCapabilities.supportsBluetoothEnableApi) + bleAvailabilityState != AvailabilityState.poweredOn + ? PlatformButton( + text: 'Enable Bluetooth', + onPressed: () async { + bool isEnabled = + await UniversalBle.enableBluetooth(); + showSnackbar("BluetoothEnabled: $isEnabled"); + }, + ) + : PlatformButton( + text: 'Disable Bluetooth', + onPressed: () async { + bool isDisabled = + await UniversalBle.disableBluetooth(); + showSnackbar("BluetoothDisabled: $isDisabled"); + }, + ), + if (BleCapabilities.requiresRuntimePermission) ...[ + PlatformButton( + text: 'Has Permissions', + onPressed: () async { + try { + bool hasPermissions = await UniversalBle.hasPermissions( + withAndroidFineLocation: false, + ); + showSnackbar("Has Permissions: $hasPermissions"); + } catch (e) { + showSnackbar(e.toString()); + } + }, + ), + PlatformButton( + text: 'Request Permissions', + onPressed: () async { + try { + await UniversalBle.requestPermissions( + withAndroidFineLocation: false, + ); + showSnackbar("Permissions granted"); + } catch (e) { + showSnackbar(e.toString()); + } + }, + ), + ], + if (!isTrackingAvailabilityState) + PlatformButton( + text: 'Track Availability State', + onPressed: trackAvailabilityState, + ), + if (BleCapabilities.supportsConnectedDevicesApi) + PlatformButton( + text: 'System Devices', + onPressed: _getSystemDevices, + ), + PlatformButton( + text: 'Queue: ${_queueType.name}', + onPressed: () { + setState(() { + _queueType = switch (_queueType) { + QueueType.global => QueueType.perDevice, + QueueType.perDevice => QueueType.none, + QueueType.none => QueueType.global, + }; + UniversalBle.queueType = _queueType; + }); + }, + ), + PlatformButton( + text: 'Scan Filters', + onPressed: _showScanFilterBottomSheet, + ), + if (_hiddenDevices.isNotEmpty) + PlatformButton( + text: 'Unhide ${_hiddenDevices.length} Devices', + onPressed: () { + setState(() { + _hiddenDevices.clear(); + }); + }, + ) + else if (_bleDevices.isNotEmpty) + Tooltip( + message: + 'Hide already discovered devices. When you turn on a new device, it will be easier to spot.', + child: PlatformButton( + text: 'Hide Already Discovered Devices', + onPressed: () { + setState(() { + _hiddenDevices.clear(); + _hiddenDevices.addAll(_bleDevices); + _bleDevices.clear(); + }); + }, + ), + ), + if (_bleDevices.isNotEmpty) + PlatformButton( + text: 'Clear List', + onPressed: () { + setState(() { + _bleDevices.clear(); + }); + }, + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (isTrackingAvailabilityState) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Ble Availability : ${bleAvailabilityState?.name}', + ), + ), + ], + ), + const Divider(color: Colors.blue), + Expanded( + child: _isScanning && _bleDevices.isEmpty + ? const Center(child: CircularProgressIndicator.adaptive()) + : !_isScanning && _bleDevices.isEmpty + ? const ScannedDevicesPlaceholderWidget() + : ListView.separated( + itemCount: _bleDevices.length, + separatorBuilder: (context, index) => const Divider(), + itemBuilder: (context, index) { + BleDevice device = + _bleDevices[_bleDevices.length - index - 1]; + return ScannedItemWidget( + bleDevice: device, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => PeripheralDetailPage(device), + ), + ); + UniversalBle.stopScan(); + setState(() { + _isScanning = false; + }); + }, + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/home/permission_screen.dart b/example/lib/home/permission_screen.dart deleted file mode 100644 index 2468fcf..0000000 --- a/example/lib/home/permission_screen.dart +++ /dev/null @@ -1,242 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:universal_ble/universal_ble.dart'; -import 'package:universal_ble_example/home/scanner_screen.dart'; - -class PermissionScreen extends StatefulWidget { - const PermissionScreen({super.key}); - - @override - State createState() => _PermissionScreenState(); -} - -class _PermissionScreenState extends State - with WidgetsBindingObserver { - final bool _withAndroidFineLocation = false; - bool _isChecking = true; - bool _hasPermissions = false; - bool _isRequesting = false; - String? _errorMessage; - - void navigateToHome() { - if (mounted) { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const ScannerScreen()), - ); - } - } - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - _checkPermissions(); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - super.didChangeAppLifecycleState(state); - // Re-check permissions when app resumes (user might have granted in settings) - if (state == AppLifecycleState.resumed && !_hasPermissions) { - _checkPermissions(); - } - } - - Future _checkPermissions() async { - setState(() { - _isChecking = true; - _errorMessage = null; - }); - - try { - // Check if permissions are required for this platform - if (!BleCapabilities.requiresRuntimePermission) { - // No permissions needed, go directly to home - navigateToHome(); - return; - } - - // Check if we already have permissions - final hasPermissions = await UniversalBle.hasPermissions( - withAndroidFineLocation: _withAndroidFineLocation, - ); - - setState(() { - _isChecking = false; - _hasPermissions = hasPermissions; - }); - - if (hasPermissions && mounted) { - // Permissions already granted, go to home - navigateToHome(); - } - } catch (e) { - setState(() { - _isChecking = false; - _errorMessage = e.toString(); - }); - } - } - - Future _requestPermissions() async { - setState(() { - _isRequesting = true; - _errorMessage = null; - }); - - try { - await UniversalBle.requestPermissions( - withAndroidFineLocation: _withAndroidFineLocation, - ); - - // Permissions granted, go to home - if (mounted) { - navigateToHome(); - } - } catch (e) { - setState(() { - _isRequesting = false; - _errorMessage = e.toString(); - }); - } - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return Scaffold( - backgroundColor: colorScheme.surface, - body: SafeArea( - child: Center( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Icon - Container( - padding: const EdgeInsets.all(32), - decoration: BoxDecoration( - color: colorScheme.primaryContainer.withValues(alpha: 0.3), - shape: BoxShape.circle, - ), - child: Icon( - Icons.security, - size: 80, - color: colorScheme.primary, - ), - ), - const SizedBox(height: 32), - Text( - 'Bluetooth Permissions Required', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - Text( - 'This app needs Bluetooth permissions to scan and connect to nearby devices.', - style: TextStyle( - fontSize: 16, - color: colorScheme.onSurface.withValues(alpha: 0.7), - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 48), - if (_isChecking) - CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - colorScheme.primary, - ), - ) - else if (!_hasPermissions) - Column( - children: [ - ElevatedButton.icon( - onPressed: _isRequesting ? null : _requestPermissions, - icon: _isRequesting - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - colorScheme.onPrimary, - ), - ), - ) - : const Icon(Icons.check_circle), - label: Text( - _isRequesting - ? 'Requesting Permissions...' - : 'Get Started', - style: const TextStyle(fontSize: 16), - ), - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - if (_errorMessage != null) ...[ - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: colorScheme.errorContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Icon( - Icons.error_outline, - color: colorScheme.error, - size: 20, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - _errorMessage!, - style: TextStyle( - color: colorScheme.onErrorContainer, - fontSize: 12, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - TextButton( - onPressed: _requestPermissions, - child: const Text('Try Again'), - ), - ], - ], - ) - else - const SizedBox.shrink(), - ], - ), - ), - ), - ), - ); - } -} diff --git a/example/lib/home/scanner_screen.dart b/example/lib/home/scanner_screen.dart deleted file mode 100644 index 9210b86..0000000 --- a/example/lib/home/scanner_screen.dart +++ /dev/null @@ -1,1035 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_ble/universal_ble.dart'; -import 'package:universal_ble_example/data/prefs_async.dart'; -import 'package:universal_ble_example/data/saved_scan_filter.dart'; -import 'package:universal_ble_example/data/company_identifier_service.dart'; -import 'package:universal_ble_example/data/mock_universal_ble.dart'; -import 'package:universal_ble_example/data/scan_controller.dart'; -import 'package:universal_ble_example/home/widgets/drawer.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'; -import 'package:universal_ble_example/peripheral_details/peripheral_detail_page.dart'; -import 'package:universal_ble_example/widgets/scan_controller_scope.dart'; - -enum ScanOrder { - lastSeen, - name, - rssi, -} - -class ScannerScreen extends StatefulWidget { - const ScannerScreen({super.key}); - - @override - State createState() => _ScannerScreenState(); -} - -class _ScannerScreenState extends State { - final _bleDevices = []; - final _hiddenDevices = []; - ScanController? _scanController; - void _onScanStateChanged() => setState(() {}); - QueueType _queueType = QueueType.global; - TextEditingController servicesFilterController = TextEditingController(); - TextEditingController namePrefixController = TextEditingController(); - TextEditingController manufacturerDataController = TextEditingController(); - final TextEditingController _searchFilterController = TextEditingController(); - final TextEditingController _webServicesController = TextEditingController(); - StreamSubscription? _scanSubscription; - - AvailabilityState? bleAvailabilityState; - ScanFilter? scanFilter; - List _savedFilters = []; - final ValueNotifier> _savedFiltersNotifier = - ValueNotifier([]); - ScanOrder _scanOrder = ScanOrder.rssi; - final Map _isExpanded = {}; - final Map _deviceAdFlashTrigger = {}; - - @override - void initState() { - super.initState(); - if (const bool.fromEnvironment('MOCK')) { - UniversalBle.setInstance(MockUniversalBle()); - } - UniversalBle.queueType = _queueType; - UniversalBle.timeout = const Duration(seconds: 10); - - _scanSubscription = UniversalBle.scanStream.listen(_handleScanResult); - - // Get initial Bluetooth availability state - UniversalBle.getBluetoothAvailabilityState().then((state) { - if (mounted) { - setState(() => bleAvailabilityState = state); - } - }); - - _loadScanFilters().then((_) { - // Auto-start scanning after filters are loaded - _tryAutoStartScan(); - }); - - // Load company identifiers in the background - CompanyIdentifierService.instance.load(); - - // Save search filter when it changes - _searchFilterController.addListener(() { - _saveScanFilters(); - }); - } - - void _handleScanResult(BleDevice result) { - if (_hiddenDevices.any((e) => e.deviceId == result.deviceId)) { - return; - } - int index = _bleDevices.indexWhere((e) => e.deviceId == result.deviceId); - if (index == -1) { - _bleDevices.add(result); - } else { - if (result.name == null && _bleDevices[index].name != null) { - result.name = _bleDevices[index].name; - } - _bleDevices[index] = result; - } - _deviceAdFlashTrigger[result.deviceId] = - (_deviceAdFlashTrigger[result.deviceId] ?? 0) + 1; - setState(() {}); - } - - Future _startScan() async { - setState(() => _bleDevices.clear()); - try { - await _scanController?.startScan(); - } catch (e) { - showSnackbar(e.toString()); - } - } - - Future _loadScanFilters() async { - try { - final services = - (await prefsAsync.getString('scan_filter_services')) ?? ''; - final namePrefix = - (await prefsAsync.getString('scan_filter_name_prefix')) ?? ''; - final manufacturerData = - (await prefsAsync.getString('scan_filter_manufacturer_data')) ?? ''; - final searchFilter = - (await prefsAsync.getString('scan_filter_search')) ?? ''; - final webServices = - (await prefsAsync.getString('scan_filter_web_services')) ?? ''; - final scanOrderStr = await prefsAsync.getString('scan_order'); - final savedFiltersJson = - await prefsAsync.getString('saved_scan_filters'); - _savedFilters = SavedScanFilter.fromJsonList(savedFiltersJson); - _savedFiltersNotifier.value = List.from(_savedFilters); - - servicesFilterController.text = services; - namePrefixController.text = namePrefix; - manufacturerDataController.text = manufacturerData; - _searchFilterController.text = searchFilter; - _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 - if (services.isNotEmpty || - namePrefix.isNotEmpty || - manufacturerData.isNotEmpty) { - _applyFilterFromControllers(); - } - } catch (e) { - // Silently fail if preferences can't be loaded - debugPrint('Failed to load scan filters: $e'); - } - } - - Future _saveSavedFilters() async { - try { - await prefsAsync.setString( - 'saved_scan_filters', - SavedScanFilter.toJsonList(_savedFilters), - ); - } catch (e) { - debugPrint('Failed to save saved filters: $e'); - } - } - - Future _saveScanFilters() async { - try { - await prefsAsync.setString( - 'scan_filter_services', servicesFilterController.text); - await prefsAsync.setString( - 'scan_filter_name_prefix', namePrefixController.text); - await prefsAsync.setString( - 'scan_filter_manufacturer_data', manufacturerDataController.text); - await prefsAsync.setString( - 'scan_filter_search', _searchFilterController.text); - await prefsAsync.setString( - 'scan_filter_web_services', _webServicesController.text); - await prefsAsync.setString('scan_order', _scanOrder.name); - } catch (e) { - debugPrint('Failed to save scan filters: $e'); - } - } - - Future _applyFilterFromControllers() async { - try { - // Ensure company identifier service is loaded - await CompanyIdentifierService.instance.load(); - List serviceUUids = []; - List namePrefixes = []; - List manufacturerDataFilters = []; - - // Parse Services - if (servicesFilterController.text.isNotEmpty) { - List services = servicesFilterController.text - .split(',') - .map((e) => e.trim()) - .where((s) => s.isNotEmpty) - .toList(); - for (String service in services) { - try { - serviceUUids.add(BleUuidParser.string(service.trim())); - } on FormatException catch (_) { - // Skip invalid service UUIDs when loading from preferences - continue; - } - } - } - - // Parse Name Prefix - String namePrefix = namePrefixController.text; - if (namePrefix.isNotEmpty) { - namePrefixes = namePrefix.split(',').map((e) => e.trim()).toList(); - } - - // Parse Manufacturer Data - String manufacturerDataText = manufacturerDataController.text; - if (manufacturerDataText.isNotEmpty) { - final companyService = CompanyIdentifierService.instance; - List manufacturerData = manufacturerDataText - .split(',') - .map((e) => e.trim()) - .where((s) => s.isNotEmpty) - .toList(); - for (String manufacturer in manufacturerData) { - final companyIdentifier = - companyService.parseCompanyIdentifier(manufacturer); - if (companyIdentifier == null) { - // Skip invalid manufacturer data when loading from preferences - continue; - } - manufacturerDataFilters.add( - ManufacturerDataFilter(companyIdentifier: companyIdentifier)); - } - } - - if (serviceUUids.isEmpty && - namePrefixes.isEmpty && - manufacturerDataFilters.isEmpty) { - scanFilter = null; - } else { - scanFilter = ScanFilter( - withServices: serviceUUids, - withNamePrefix: namePrefixes, - withManufacturerData: manufacturerDataFilters, - ); - } - setState(() {}); - } catch (e) { - // Silently fail if filter can't be applied - debugPrint('Failed to apply filter from controllers: $e'); - } - } - - Future _onScanFilterApplied(ScanFilter? filter) async { - setState(() { - scanFilter = filter; - _bleDevices.clear(); - _hiddenDevices.clear(); - _deviceAdFlashTrigger.clear(); - }); - await _saveScanFilters(); - - // Store applied filter as a saved preset (keep last 10), unless it already matches one - final servicesText = servicesFilterController.text; - final namePrefixText = namePrefixController.text; - final manufacturerDataText = manufacturerDataController.text; - final hasValues = servicesText.trim().isNotEmpty || - namePrefixText.trim().isNotEmpty || - manufacturerDataText.trim().isNotEmpty; - if (hasValues) { - final alreadyStored = _savedFilters.any((f) => - f.services == servicesText && - f.namePrefix == namePrefixText && - f.manufacturerData == manufacturerDataText); - if (!alreadyStored) { - final name = 'Filter ${_savedFilters.length + 1}'; - _savedFilters.add(SavedScanFilter( - name: name, - services: servicesText, - namePrefix: namePrefixText, - manufacturerData: manufacturerDataText, - )); - if (_savedFilters.length > 10) { - _savedFilters = _savedFilters.sublist(_savedFilters.length - 10); - } - await _saveSavedFilters(); - if (mounted) { - setState(() {}); - _savedFiltersNotifier.value = List.from(_savedFilters); - } - } - } - - if (_scanController?.isScanning == true) { - await _scanController?.stopScan(); - if (!mounted) return; - } - if (mounted) await _startScan(); - } - - int _getActiveFilterCount(ScanFilter? filter) { - if (filter == null) return 0; - int n = 0; - if (filter.withServices.isNotEmpty) n++; - if (filter.withNamePrefix.isNotEmpty) n++; - if (filter.withManufacturerData.isNotEmpty) n++; - return n; - } - - Future _onSelectSavedFilter(SavedScanFilter preset) async { - servicesFilterController.text = preset.services; - namePrefixController.text = preset.namePrefix; - manufacturerDataController.text = preset.manufacturerData; - await _applyFilterFromControllers(); - await _saveScanFilters(); - if (_scanController?.isScanning == true) { - await _scanController?.stopScan(); - if (!mounted) return; - } - if (mounted) { - await _startScan(); - setState(() {}); - } - } - - Future _onRenameFilter(SavedScanFilter preset, String newName) async { - final i = _savedFilters.indexWhere( - (f) => - f.services == preset.services && - f.namePrefix == preset.namePrefix && - f.manufacturerData == preset.manufacturerData, - ); - if (i >= 0) { - _savedFilters[i] = _savedFilters[i].copyWith(name: newName); - await _saveSavedFilters(); - if (mounted) { - setState(() {}); - _savedFiltersNotifier.value = List.from(_savedFilters); - } - } - } - - Future _onDeleteFilter(SavedScanFilter preset) async { - _savedFilters.removeWhere( - (f) => - f.name == preset.name && - f.services == preset.services && - f.namePrefix == preset.namePrefix && - f.manufacturerData == preset.manufacturerData, - ); - await _saveSavedFilters(); - if (mounted) { - setState(() {}); - _savedFiltersNotifier.value = List.from(_savedFilters); - } - } - - void _showScanFilterBottomSheet() { - showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: (context) { - return ScanFilterWidget( - servicesFilterController: servicesFilterController, - namePrefixController: namePrefixController, - manufacturerDataController: manufacturerDataController, - savedFiltersListenable: _savedFiltersNotifier, - onScanFilter: (ScanFilter? filter) { - _onScanFilterApplied(filter); - }, - onSelectSavedFilter: _onSelectSavedFilter, - onRenameFilter: _onRenameFilter, - onDeleteFilter: _onDeleteFilter, - ); - }, - ); - } - - void showSnackbar(String message) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); - } - - List get _filteredDevices { - if (_searchFilterController.text.isEmpty) { - return _bleDevices; - } - final filter = _searchFilterController.text.toLowerCase(); - final companyService = CompanyIdentifierService.instance; - - // Try to parse the filter as a company ID (supports hex and decimal formats) - final parsedCompanyId = - companyService.parseCompanyIdentifier(_searchFilterController.text); - - return _bleDevices.where((device) { - final name = device.name?.toLowerCase() ?? ''; - final deviceId = device.deviceId.toLowerCase(); - final services = device.services.join(' ').toLowerCase(); - - // Check if filter matches device name, ID, or services - if (name.contains(filter) || - deviceId.contains(filter) || - services.contains(filter)) { - return true; - } - - // Check if filter matches any company name or company ID from manufacturer data - for (final manufacturerData in device.manufacturerDataList) { - // Check company name match - final companyName = - companyService.getCompanyName(manufacturerData.companyId); - if (companyName != null && companyName.toLowerCase().contains(filter)) { - return true; - } - - // Check company ID match (if filter was parsed as a company ID) - if (parsedCompanyId != null && - manufacturerData.companyId == parsedCompanyId) { - return true; - } - - // Also check if filter matches the hex representation of company ID - final companyIdHex = manufacturerData.companyIdRadix16.toLowerCase(); - if (companyIdHex.contains(filter) || - companyIdHex.replaceAll('0x', '').contains(filter)) { - return true; - } - } - - return false; - }).toList(); - } - - List get _sortedDevices { - final list = List.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 => - bleAvailabilityState == AvailabilityState.poweredOn; - - Future _tryAutoStartScan() async { - // Only auto-start if Bluetooth is available and not already scanning - if (_isBluetoothAvailable && _scanController?.isScanning != true) { - final isScanning = await UniversalBle.isScanning(); - if (!isScanning && mounted) { - await _startScan(); - } - } - } - - @override - void setState(VoidCallback fn) { - if (mounted) super.setState(fn); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _scanController ??= ScanControllerScope.of(context) - ..addListener(_onScanStateChanged); - } - - @override - void dispose() { - _scanController?.removeListener(_onScanStateChanged); - servicesFilterController.dispose(); - namePrefixController.dispose(); - manufacturerDataController.dispose(); - _searchFilterController.dispose(); - _webServicesController.dispose(); - _savedFiltersNotifier.dispose(); - - _scanSubscription?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return Scaffold( - backgroundColor: colorScheme.surface, - drawer: AppDrawer( - queueType: _queueType, - onQueueTypeChanged: (queueType) { - setState(() { - _queueType = queueType; - UniversalBle.queueType = _queueType; - }); - }, - availabilityState: bleAvailabilityState, - onAvailabilityStateChanged: (state) { - setState(() => bleAvailabilityState = state); - if (state == AvailabilityState.poweredOn) { - _tryAutoStartScan(); - } - }, - ), - appBar: AppBar( - title: Row( - children: [ - Expanded( - child: Container( - height: 40, - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), - ), - child: TextField( - controller: _searchFilterController, - decoration: InputDecoration( - hintText: 'Search by name, ID, service, company name/ID', - 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(() {}), - ), - ), - ), - ], - ), - elevation: 0, - backgroundColor: colorScheme.surface, - leading: Builder( - builder: (context) => IconButton( - icon: const Icon(Icons.menu), - onPressed: () => Scaffold.of(context).openDrawer(), - ), - ), - actions: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: FilledButton.icon( - onPressed: _isBluetoothAvailable && _scanController != null - ? () async { - if (_scanController!.isScanning) { - await _scanController!.stopScan(); - } else { - await _startScan(); - } - } - : null, - icon: Icon( - _scanController?.isScanning == true - ? Icons.stop_circle - : Icons.play_arrow, - size: 20, - ), - label: Text( - _scanController?.isScanning == true ? 'Stop' : 'Scan', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - style: FilledButton.styleFrom( - backgroundColor: - _scanController?.isScanning == true - ? colorScheme.error - : colorScheme.primary, - foregroundColor: - _scanController?.isScanning == true - ? colorScheme.onError - : colorScheme.onPrimary, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - elevation: _scanController?.isScanning == true ? 0 : 2, - ), - ), - ), - ], - ), - body: Column( - children: [ - // Web Services input (only for web) - if (kIsWeb) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Container( - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: colorScheme.primary.withValues(alpha: 0.3), - width: 1, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), - child: Row( - children: [ - Icon( - Icons.web, - size: 18, - color: colorScheme.primary, - ), - const SizedBox(width: 8), - Text( - 'Ble Services', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 13, - color: colorScheme.onSurface, - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), - child: TextFormField( - controller: _webServicesController, - maxLines: 2, - style: const TextStyle( - fontFamily: 'monospace', fontSize: 12), - decoration: InputDecoration( - hintText: - 'Enter service UUIDs for web (comma-separated)', - helperText: - 'These services will be available to use after connection on Web', - helperMaxLines: 2, - helperStyle: TextStyle( - fontSize: 11, - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - filled: true, - fillColor: colorScheme.surface, - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - ), - ), - ), - ], - ), - ), - ), - - // Device count badge with hide/unhide - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - Tooltip( - message: _hiddenDevices.isNotEmpty - ? 'Show hidden devices' - : _bleDevices.isNotEmpty - ? 'Hide already discovered devices. When you turn on a new device, it will be easier to spot.' - : '', - child: TextButton.icon( - onPressed: - _bleDevices.isNotEmpty || _hiddenDevices.isNotEmpty - ? () { - if (_hiddenDevices.isNotEmpty) { - // Unhide all devices - setState(() { - _hiddenDevices.clear(); - }); - } else if (_bleDevices.isNotEmpty) { - // Hide all devices - setState(() { - _hiddenDevices.clear(); - _hiddenDevices.addAll(_bleDevices); - _bleDevices.clear(); - }); - } - } - : null, - label: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '${_bleDevices.length} ${_hiddenDevices.isNotEmpty ? "/ ${_hiddenDevices.length}" : ""} device${_bleDevices.length == 1 ? '' : 's'}', - style: TextStyle( - color: colorScheme.onPrimaryContainer, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: 4), - Icon( - _hiddenDevices.isNotEmpty - ? Icons.visibility - : Icons.visibility_off, - size: 16, - color: colorScheme.onPrimaryContainer, - ), - ], - ), - style: TextButton.styleFrom( - padding: EdgeInsets.symmetric(horizontal: 8), - backgroundColor: colorScheme.primaryContainer, - ), - ), - ), - const SizedBox(width: 4), - Badge( - isLabelVisible: _getActiveFilterCount(scanFilter) > 0, - label: Text( - '${_getActiveFilterCount(scanFilter)}', - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - ), - ), - child: TextButton.icon( - onPressed: _showScanFilterBottomSheet, - icon: Icon( - scanFilter != null - ? Icons.filter_list - : Icons.filter_list_outlined, - size: 18, - color: scanFilter != null - ? colorScheme.primary - : colorScheme.onSurface, - ), - label: Text( - 'Filter', - style: TextStyle( - color: scanFilter != null - ? colorScheme.primary - : colorScheme.onSurface, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - style: TextButton.styleFrom( - padding: EdgeInsets.symmetric(horizontal: 8), - backgroundColor: scanFilter != null - ? colorScheme.primaryContainer - : null, - ), - ), - ), - const Spacer(), - PopupMenuButton( - tooltip: 'Sort by', - padding: EdgeInsets.zero, - onSelected: (ScanOrder order) { - setState(() { - _scanOrder = order; - }); - _saveScanFilters(); - }, - itemBuilder: (context) => [ - const PopupMenuItem( - value: ScanOrder.rssi, - child: Text('RSSI'), - ), - const PopupMenuItem( - value: ScanOrder.lastSeen, - child: Text('Last seen'), - ), - const PopupMenuItem( - value: ScanOrder.name, - child: Text('Name'), - ), - ], - 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, - ), - ], - ), - ), - ), - ], - ), - ), - const Divider(), - // Devices List - Expanded( - child: !_isBluetoothAvailable - ? Center( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(32), - decoration: BoxDecoration( - color: colorScheme.errorContainer.withValues( - alpha: 0.3, - ), - shape: BoxShape.circle, - ), - child: Icon( - Icons.bluetooth_disabled, - size: 80, - color: colorScheme.error, - ), - ), - const SizedBox(height: 32), - Text( - 'Bluetooth is Turned Off', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - Text( - 'Please turn on Bluetooth to scan for devices', - style: TextStyle( - fontSize: 16, - color: colorScheme.onSurface.withValues( - alpha: 0.7, - ), - ), - textAlign: TextAlign.center, - ), - if (BleCapabilities.supportsBluetoothEnableApi) ...[ - const SizedBox(height: 48), - ElevatedButton.icon( - onPressed: () async { - try { - bool isEnabled = - await UniversalBle.enableBluetooth(); - if (!isEnabled) { - showSnackbar( - "Please enable Bluetooth in system settings", - ); - } - } catch (e) { - showSnackbar(e.toString()); - } - }, - icon: const Icon(Icons.bluetooth, size: 24), - label: const Text( - 'Turn On Bluetooth', - style: TextStyle(fontSize: 18), - ), - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 2, - ), - ), - ] - ], - ), - ), - ) - : _scanController?.isScanning == true && _bleDevices.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator.adaptive( - valueColor: AlwaysStoppedAnimation( - colorScheme.primary, - ), - ), - const SizedBox(height: 16), - Text( - 'Scanning for devices...', - style: TextStyle( - color: colorScheme.onSurface - .withValues(alpha: 0.6), - fontSize: 14, - ), - ), - ], - ), - ) - : _scanController?.isScanning != true && _bleDevices.isEmpty - ? ScannedDevicesPlaceholderWidget(onTap: _startScan) - : _sortedDevices.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.search_off, - size: 64, - color: colorScheme.onSurface - .withValues(alpha: 0.3), - ), - const SizedBox(height: 16), - Text( - 'No devices match your filter', - style: TextStyle( - color: colorScheme.onSurface - .withValues(alpha: 0.6), - fontSize: 16, - ), - ), - ], - ), - ) - : ListView.separated( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - itemCount: _sortedDevices.length, - separatorBuilder: (context, index) => - const SizedBox(height: 8), - itemBuilder: (context, index) { - BleDevice device = _sortedDevices[index]; - return ScannedItemWidget( - bleDevice: device, - adFlashTrigger: - _deviceAdFlashTrigger[device.deviceId] ?? - 0, - isExpanded: - _isExpanded[device.deviceId] ?? false, - onExpand: (isExpanded) { - setState(() { - _isExpanded[device.deviceId] = - isExpanded; - }); - }, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => - PeripheralDetailPage(device), - ), - ); - }, - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/example/lib/home/system_devices_screen.dart b/example/lib/home/system_devices_screen.dart deleted file mode 100644 index e96051b..0000000 --- a/example/lib/home/system_devices_screen.dart +++ /dev/null @@ -1,379 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_ble/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/scanned_item_widget.dart'; -import 'package:universal_ble_example/peripheral_details/peripheral_detail_page.dart'; - -class SystemDevicesScreen extends StatefulWidget { - const SystemDevicesScreen({super.key}); - - @override - State createState() => _SystemDevicesScreenState(); -} - -class _SystemDevicesScreenState extends State { - List _systemDevices = []; - bool _isLoading = false; - AvailabilityState? bleAvailabilityState; - List withServices = []; - final Map _isExpanded = {}; - final TextEditingController _servicesController = TextEditingController(); - - void _parseServices() { - setState(() { - withServices = []; - if (_servicesController.text.isNotEmpty) { - final services = _servicesController.text - .split(',') - .map((s) => s.trim()) - .where((s) => s.isNotEmpty) - .toList(); - for (String service in services) { - try { - withServices.add(BleUuidParser.string(service)); - } catch (e) { - _showSnackbar("Invalid Service UUID: $service"); - return; - } - } - } - }); - } - - Future _getSystemDevices() async { - _parseServices(); - - // For macOS and iOS, it is recommended to set a filter to get system devices - if ((defaultTargetPlatform == TargetPlatform.macOS || - defaultTargetPlatform == TargetPlatform.iOS) && - withServices.isEmpty) { - _showSnackbar( - "No services filter was set for getting system connected devices. Using default services...", - ); - } - - setState(() { - _isLoading = true; - }); - - try { - List devices = await UniversalBle.getSystemDevices( - withServices: withServices, - ); - setState(() { - _systemDevices = devices; - _isLoading = false; - }); - if (devices.isEmpty) { - _showSnackbar("No System Connected Devices Found"); - } - } catch (e) { - setState(() { - _isLoading = false; - }); - _showSnackbar(e.toString()); - } - } - - void _showSnackbar(String message) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(message))); - } - - bool get _isBluetoothAvailable => - bleAvailabilityState == AvailabilityState.poweredOn; - - @override - void setState(VoidCallback fn) { - if (mounted) super.setState(fn); - } - - @override - void dispose() { - _servicesController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return Scaffold( - backgroundColor: colorScheme.surface, - drawer: const AppDrawer(), - appBar: AppBar( - title: Row( - children: [ - BleAvailabilityIcon(onAvailabilityStateChanged: (state) { - setState(() => bleAvailabilityState = state); - }), - const SizedBox(width: 12), - Expanded( - child: const Text( - 'System Devices', - style: TextStyle( - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ), - ], - ), - elevation: 0, - backgroundColor: colorScheme.surface, - actions: [ - IconButton( - icon: Icon( - Icons.refresh, - color: colorScheme.primary, - ), - tooltip: 'Refresh', - onPressed: _isBluetoothAvailable ? _getSystemDevices : null, - ), - ], - ), - body: Column( - children: [ - // Services input text box - Padding( - padding: const EdgeInsets.all(16), - child: Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.apps, - color: colorScheme.primary, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Service UUIDs (Optional)', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: colorScheme.onSurface, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - 'Enter service UUIDs separated by commas. Leave empty to use default services.\nMandatory on Apple platforms.', - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - const SizedBox(height: 12), - TextField( - controller: _servicesController, - maxLines: 8, - minLines: 4, - style: const TextStyle( - fontFamily: 'monospace', - fontSize: 12, - ), - decoration: InputDecoration( - hintText: - 'eg: 0000180f-0000-1000-8000-00805f9b34fb, 0000180a-0000-1000-8000-00805f9b34fb', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: colorScheme.surfaceContainerHighest, - contentPadding: const EdgeInsets.all(12), - ), - ), - ], - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _isBluetoothAvailable && !_isLoading - ? _getSystemDevices - : null, - icon: _isLoading - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - colorScheme.onPrimary, - ), - ), - ) - : const Icon(Icons.search), - label: Text(_isLoading ? 'Loading...' : 'Get System Devices'), - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 2, - ), - ), - ), - ), - if (_systemDevices.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - '${_systemDevices.length} device${_systemDevices.length == 1 ? '' : 's'}', - style: TextStyle( - color: colorScheme.onPrimaryContainer, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - const Divider(), - Expanded( - child: !_isBluetoothAvailable - ? Center( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(32), - decoration: BoxDecoration( - color: colorScheme.errorContainer.withValues( - alpha: 0.3, - ), - shape: BoxShape.circle, - ), - child: Icon( - Icons.bluetooth_disabled, - size: 80, - color: colorScheme.error, - ), - ), - const SizedBox(height: 32), - Text( - 'Bluetooth is Turned Off', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - Text( - 'Please turn on Bluetooth to get system devices', - style: TextStyle( - fontSize: 16, - color: colorScheme.onSurface.withValues( - alpha: 0.7, - ), - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ) - : _systemDevices.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.devices_outlined, - size: 64, - color: - colorScheme.onSurface.withValues(alpha: 0.3), - ), - const SizedBox(height: 16), - Text( - 'No system devices found', - style: TextStyle( - color: colorScheme.onSurface - .withValues(alpha: 0.6), - fontSize: 16, - ), - ), - const SizedBox(height: 8), - Text( - 'Click "Get System Devices" to refresh', - style: TextStyle( - color: colorScheme.onSurface - .withValues(alpha: 0.5), - fontSize: 14, - ), - ), - ], - ), - ) - : ListView.separated( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - itemCount: _systemDevices.length, - separatorBuilder: (context, index) => - const SizedBox(height: 8), - itemBuilder: (context, index) { - BleDevice device = _systemDevices[index]; - return ScannedItemWidget( - bleDevice: device, - isExpanded: _isExpanded[device.deviceId] ?? false, - onExpand: (isExpanded) { - setState(() { - _isExpanded[device.deviceId] = isExpanded; - }); - }, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => PeripheralDetailPage(device), - ), - ); - }, - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/example/lib/home/widgets/ble_availability_icon.dart b/example/lib/home/widgets/ble_availability_icon.dart deleted file mode 100644 index 265240a..0000000 --- a/example/lib/home/widgets/ble_availability_icon.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:universal_ble/universal_ble.dart'; - -class BleAvailabilityIcon extends StatefulWidget { - final Function(AvailabilityState) onAvailabilityStateChanged; - const BleAvailabilityIcon({ - super.key, - required this.onAvailabilityStateChanged, - }); - - @override - State createState() => _BleAvailabilityIconState(); -} - -class _BleAvailabilityIconState extends State { - AvailabilityState? bleAvailabilityState; - StreamSubscription? _availabilitySubscription; - - @override - void initState() { - super.initState(); - UniversalBle.getBluetoothAvailabilityState() - .then(_handleAvailabilityStateChanged); - _availabilitySubscription = - UniversalBle.availabilityStream.listen(_handleAvailabilityStateChanged); - } - - void _handleAvailabilityStateChanged(AvailabilityState state) { - if (mounted) setState(() => bleAvailabilityState = state); - widget.onAvailabilityStateChanged(state); - } - - @override - void dispose() { - _availabilitySubscription?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (bleAvailabilityState == null) { - return const SizedBox.shrink(); - } - return switch (bleAvailabilityState!) { - AvailabilityState.resetting => Icon( - Icons.bluetooth_searching, - color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), - ), - AvailabilityState.poweredOn => Icon( - Icons.bluetooth_connected, - color: Theme.of(context).colorScheme.primary, - ), - AvailabilityState.poweredOff => Icon( - Icons.bluetooth_disabled, - color: Theme.of(context).colorScheme.error, - ), - AvailabilityState.unauthorized => Icon( - Icons.bluetooth_disabled, - color: Theme.of(context).colorScheme.error, - ), - AvailabilityState.unsupported => Icon( - Icons.bluetooth_disabled, - color: Theme.of(context).colorScheme.outline, - ), - AvailabilityState.unknown => Icon( - Icons.bluetooth_searching, - color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), - ), - }; - } -} diff --git a/example/lib/home/widgets/ble_device_info_widget.dart b/example/lib/home/widgets/ble_device_info_widget.dart deleted file mode 100644 index 52c06d9..0000000 --- a/example/lib/home/widgets/ble_device_info_widget.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:universal_ble/universal_ble.dart'; - -class BleDeviceInfoWidget extends StatelessWidget { - final BleDevice bleDevice; - const BleDeviceInfoWidget({super.key, required this.bleDevice}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText('Device ID: ${bleDevice.deviceId}'), - SelectableText('Name: ${bleDevice.name ?? 'N/A'}'), - SelectableText('RSSI: ${bleDevice.rssi}'), - SelectableText('Paired: ${bleDevice.paired}'), - SelectableText( - 'Services: ${bleDevice.services.isNotEmpty ? bleDevice.services.join(', ') : 'N/A'}', - ), - SelectableText( - 'Manufacturer Data: ${bleDevice.manufacturerDataList.isNotEmpty ? bleDevice.manufacturerDataList.map((e) => e.toString()).join(', ') : 'N/A'}', - ), - ], - ), - ); - } -} diff --git a/example/lib/home/widgets/drawer.dart b/example/lib/home/widgets/drawer.dart deleted file mode 100644 index 2d69bd2..0000000 --- a/example/lib/home/widgets/drawer.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:universal_ble/universal_ble.dart'; -import 'package:universal_ble_example/home/widgets/ble_availability_icon.dart'; -import 'package:universal_ble_example/home/scanner_screen.dart'; -import 'package:universal_ble_example/home/system_devices_screen.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class AppDrawer extends StatefulWidget { - final QueueType? queueType; - final Function(QueueType)? onQueueTypeChanged; - final AvailabilityState? availabilityState; - final void Function(AvailabilityState)? onAvailabilityStateChanged; - - const AppDrawer({ - super.key, - this.queueType, - this.onQueueTypeChanged, - this.availabilityState, - this.onAvailabilityStateChanged, - }); - - @override - State createState() => _AppDrawerState(); -} - -class _AppDrawerState extends State { - void _navigateToScreen(BuildContext context, Widget screen) { - Navigator.pop(context); - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (_) => screen), - ); - } - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Drawer( - child: Column( - children: [ - SizedBox( - height: 110, - child: DrawerHeader( - padding: const EdgeInsets.only(top: 0, left: 16), - decoration: BoxDecoration( - color: colorScheme.primary, - ), - child: Row( - children: [ - Image.asset('assets/icon.png', width: 40, height: 40), - const SizedBox(width: 10), - Expanded( - child: Text( - 'Universal BLE', - maxLines: 2, - style: - Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onPrimary, - height: 1, - ), - ), - ), - ], - ), - ), - ), - if (widget.availabilityState != null && - widget.onAvailabilityStateChanged != null) - ListTile( - leading: BleAvailabilityIcon( - onAvailabilityStateChanged: widget.onAvailabilityStateChanged!, - ), - title: Text(_getBluetoothAvailabilityLabel(widget.availabilityState!)), - ), - ListTile( - leading: const Icon(Icons.search), - title: const Text('Scanner'), - onTap: () => _navigateToScreen(context, const ScannerScreen()), - ), - if (BleCapabilities.supportsConnectedDevicesApi) - ListTile( - leading: const Icon(Icons.devices), - title: const Text('System Devices'), - onTap: () => - _navigateToScreen(context, const SystemDevicesScreen()), - ), - 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) { - final theme = Theme.of(context); - final bodyStyle = theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface, - ) ?? TextStyle(color: theme.colorScheme.onSurface); - final linkStyle = bodyStyle.copyWith( - color: theme.colorScheme.primary, - decoration: TextDecoration.underline, - ); - return AboutListTile( - icon: const Icon(Icons.info_outline), - applicationIcon: - Image.asset('assets/icon.png', width: 40, height: 40), - applicationName: 'Universal BLE', - applicationVersion: - "${snapshot.data?.version} (${snapshot.data?.buildNumber})", - applicationLegalese: '\u{a9} 2023 Navideck', - aboutBoxChildren: [ - const SizedBox(height: 24), - RichText( - textAlign: TextAlign.justify, - text: TextSpan( - style: bodyStyle, - children: [ - const TextSpan(text: "Universal BLE is "), - TextSpan( - text: 'open source', - style: linkStyle, - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl(Uri.parse( - "https://github.com/Navideck/Universal-BLE")); - }, - ), - const TextSpan(text: "."), - ], - ), - ), - const SizedBox(height: 24), - RichText( - textAlign: TextAlign.justify, - text: TextSpan( - style: bodyStyle, - children: [ - const TextSpan(text: "Need help with your project? "), - TextSpan( - text: 'Hire us', - style: linkStyle, - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl(Uri.parse('https://navideck.com/contact')); - }, - ), - ], - ), - ), - ], - ); - }, - ), - ], - ), - ); - } - - String _getBluetoothAvailabilityLabel(AvailabilityState state) { - switch (state) { - 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 state unknown'; - } - } - - 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/queue_selector_widget.dart b/example/lib/home/widgets/queue_selector_widget.dart deleted file mode 100644 index e84ca75..0000000 --- a/example/lib/home/widgets/queue_selector_widget.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:universal_ble/universal_ble.dart'; - -class QueueSelectorWidget extends StatelessWidget { - final QueueType queueType; - final Function(QueueType) onQueueTypeChanged; - const QueueSelectorWidget({ - super.key, - required this.queueType, - required this.onQueueTypeChanged, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Container( - padding: const EdgeInsets.all(24), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.queue, - color: colorScheme.primary, - size: 28, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - "Queue Type", - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - tooltip: 'Close', - ), - ], - ), - const SizedBox(height: 2), - Text( - "Controls how BLE commands are executed.", - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - ), - const SizedBox(height: 8), - _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, - ), - const SizedBox(height: 8), - ], - ), - ), - ); - } - - Widget _buildQueueOption( - BuildContext context, - QueueType value, - String title, - String description, - IconData icon, - ) { - final colorScheme = Theme.of(context).colorScheme; - final isSelected = queueType == value; - return InkWell( - onTap: () => onQueueTypeChanged(value), - borderRadius: BorderRadius.circular(12), - child: Container( - padding: const EdgeInsets.all(6), - 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: 16, - color: isSelected - ? colorScheme.onPrimaryContainer - : colorScheme.onSurface, - ), - ), - const SizedBox(height: 4), - Text( - description, - style: TextStyle( - fontSize: 12, - color: isSelected - ? colorScheme.onPrimaryContainer - .withValues(alpha: 0.7) - : colorScheme.onSurface.withValues(alpha: 0.6), - ), - ), - ], - ), - ), - if (isSelected) - Icon( - Icons.check_circle, - color: colorScheme.primary, - ), - ], - ), - ), - ); - } -} diff --git a/example/lib/home/widgets/rssi_signal_indicator.dart b/example/lib/home/widgets/rssi_signal_indicator.dart deleted file mode 100644 index 7e4a9ea..0000000 --- a/example/lib/home/widgets/rssi_signal_indicator.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; - -class RssiSignalIndicator extends StatelessWidget { - final int rssi; - const RssiSignalIndicator({super.key, required this.rssi}); - - @override - Widget build(BuildContext context) { - final bool isPositive = rssi >= 0; - final Color barColor = isPositive ? Colors.green : Colors.red; - // BLE RSSI ranges (in dBm): - // -30 to 0: Excellent signal (4 bars) - // -50 to -30: Good signal (3 bars) - // -70 to -50: Fair signal (2 bars) - // -90 to -70: Weak signal (1 bar) - // Below -90: Very weak signal (0 bars) - // Positive values: Extremely strong (4 bars) - int bars; - if (isPositive) { - bars = 4; // Positive RSSI is extremely strong - } else { - if (rssi >= -30) { - bars = 4; // Excellent - } else if (rssi >= -50) { - bars = 3; // Good - } else if (rssi >= -70) { - bars = 2; // Fair - } else if (rssi >= -90) { - bars = 1; // Weak - } else { - bars = 0; // Very weak - } - } - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 24, - height: 16, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate(4, (index) { - final barHeight = (index + 1) * 3.0 + 2.0; - final isActive = index < bars; - return Container( - width: 3, - height: barHeight, - decoration: BoxDecoration( - color: isActive ? barColor : Colors.grey.withAlpha(30), - borderRadius: BorderRadius.circular(1.5), - ), - ); - }), - ), - ), - const SizedBox(height: 4), - Text('$rssi'), - ], - ); - } -} diff --git a/example/lib/home/widgets/scan_filter_widget.dart b/example/lib/home/widgets/scan_filter_widget.dart index f18d23a..c2da8b7 100644 --- a/example/lib/home/widgets/scan_filter_widget.dart +++ b/example/lib/home/widgets/scan_filter_widget.dart @@ -1,18 +1,12 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:universal_ble/universal_ble.dart'; -import 'package:universal_ble_example/data/company_identifier_service.dart'; -import 'package:universal_ble_example/data/saved_scan_filter.dart'; +import 'package:universal_ble_example/widgets/platform_button.dart'; class ScanFilterWidget extends StatefulWidget { final void Function(ScanFilter? filter) onScanFilter; final TextEditingController servicesFilterController; final TextEditingController namePrefixController; final TextEditingController manufacturerDataController; - final ValueListenable> savedFiltersListenable; - final void Function(SavedScanFilter preset) onSelectSavedFilter; - final void Function(SavedScanFilter preset, String newName) onRenameFilter; - final void Function(SavedScanFilter preset) onDeleteFilter; const ScanFilterWidget({ super.key, @@ -20,10 +14,6 @@ class ScanFilterWidget extends StatefulWidget { required this.servicesFilterController, required this.namePrefixController, required this.manufacturerDataController, - required this.savedFiltersListenable, - required this.onSelectSavedFilter, - required this.onRenameFilter, - required this.onDeleteFilter, }); @override @@ -33,72 +23,18 @@ class ScanFilterWidget extends StatefulWidget { class _ScanFilterWidgetState extends State { String? error; - int? _selectedSavedFilterIndex(List savedFilters) { - final s = widget.servicesFilterController.text; - final n = widget.namePrefixController.text; - final m = widget.manufacturerDataController.text; - for (var i = 0; i < savedFilters.length; i++) { - final f = savedFilters[i]; - if (f.services == s && f.namePrefix == n && f.manufacturerData == m) { - return i; - } - } - return null; - } - - Future _showRenameDialog( - BuildContext context, SavedScanFilter preset) async { - final controller = TextEditingController(text: preset.name); - final newName = await showDialog( - context: context, - builder: (ctx) { - return AlertDialog( - title: const Text('Rename filter'), - content: TextField( - controller: controller, - decoration: const InputDecoration( - labelText: 'Name', - border: OutlineInputBorder(), - ), - autofocus: true, - onSubmitted: (v) => Navigator.pop(ctx, v), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text('Cancel'), - ), - FilledButton( - onPressed: () => Navigator.pop(ctx, controller.text.trim()), - child: const Text('Save'), - ), - ], - ); - }, - ); - if (newName != null && newName.isNotEmpty && mounted) { - widget.onRenameFilter(preset, newName); - } - } - - void applyFilter() async { + void applyFilter() { setState(() { error = null; }); try { - // Ensure company identifier service is loaded - await CompanyIdentifierService.instance.load(); List serviceUUids = []; List namePrefixes = []; List manufacturerDataFilters = []; - // Parse Services - handle both comma and newline separated + // Parse Services if (widget.servicesFilterController.text.isNotEmpty) { - List services = widget.servicesFilterController.text - .split(',') - .map((e) => e.trim()) - .where((s) => s.isNotEmpty) - .toList(); + List services = widget.servicesFilterController.text.split(','); for (String service in services) { try { serviceUUids.add(BleUuidParser.string(service.trim())); @@ -108,28 +44,20 @@ class _ScanFilterWidgetState extends State { } } - // Parse Name Prefix - handle both comma and newline separated + // Parse Name Prefix String namePrefix = widget.namePrefixController.text; if (namePrefix.isNotEmpty) { namePrefixes = namePrefix.split(',').map((e) => e.trim()).toList(); } - // Parse Manufacturer Data - handle both comma and newline separated + // Parse Manufacturer Data String manufacturerDataText = widget.manufacturerDataController.text; if (manufacturerDataText.isNotEmpty) { - final companyService = CompanyIdentifierService.instance; - List manufacturerData = manufacturerDataText - .split(',') - .map((e) => e.trim()) - .where((s) => s.isNotEmpty) - .toList(); + List manufacturerData = manufacturerDataText.split(','); for (String manufacturer in manufacturerData) { - final companyIdentifier = - companyService.parseCompanyIdentifier(manufacturer); - + int? companyIdentifier = int.tryParse(manufacturer); if (companyIdentifier == null) { - throw Exception( - "Invalid Manufacturer Data or Company Name: $manufacturer"); + throw Exception("Invalid Manufacturer Data $manufacturer"); } manufacturerDataFilters.add( ManufacturerDataFilter(companyIdentifier: companyIdentifier)); @@ -148,15 +76,11 @@ class _ScanFilterWidgetState extends State { withManufacturerData: manufacturerDataFilters, ), ); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Filters Applied")), - ); - } - } - if (mounted) { - Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Filters Applied")), + ); } + Navigator.pop(context); } catch (e) { setState(() { error = e.toString(); @@ -174,355 +98,74 @@ class _ScanFilterWidgetState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return Container( - decoration: BoxDecoration( - color: colorScheme.surface, - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), - ), - child: SafeArea( - child: Padding( - padding: EdgeInsets.only( - left: 20, - right: 20, - top: 20, - bottom: MediaQuery.of(context).viewInsets.bottom + 20, - ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Header - Row( - children: [ - Icon( - Icons.filter_list, - color: colorScheme.primary, - size: 28, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - "Scan Filters", - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - tooltip: 'Close', - ), - ], - ), - Text( - "Filter devices by name, services, or manufacturer data. Enter multiple values separated by commas.", - style: theme.textTheme.bodySmall?.copyWith( - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - ), - const SizedBox(height: 8), - if (error != null) - Container( - margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: colorScheme.errorContainer, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: colorScheme.error.withValues(alpha: 0.3), - ), - ), - child: Row( - children: [ - Icon( - Icons.error_outline, - color: colorScheme.error, - size: 20, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - error!, - style: TextStyle( - color: colorScheme.onErrorContainer, - fontSize: 13, - ), - ), - ), - ], - ), - ), - - // Saved filters dropdown (rebuilds when list changes) - ValueListenableBuilder>( - valueListenable: widget.savedFiltersListenable, - builder: (context, savedFilters, child) { - if (savedFilters.isEmpty) { - return const SizedBox.shrink(); - } - final selectedIndex = - _selectedSavedFilterIndex(savedFilters); - final selectedPreset = selectedIndex != null && - selectedIndex < savedFilters.length - ? savedFilters[selectedIndex] - : null; - // With a single filter, always allow edit/delete even if not selected in dropdown - final presetForActions = selectedPreset ?? - (savedFilters.length == 1 ? savedFilters[0] : null); - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'Saved filters', - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - color: colorScheme.primary, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: DropdownButtonFormField( - initialValue: selectedIndex, - decoration: InputDecoration( - isDense: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - ), - hint: const Text('—'), - items: [ - const DropdownMenuItem( - value: null, - child: Text('—'), - ), - ...List.generate( - savedFilters.length, - (i) => DropdownMenuItem( - value: i, - child: Text( - savedFilters[i].name, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - onChanged: (int? index) { - if (index != null && - index < savedFilters.length) { - widget.onSelectSavedFilter( - savedFilters[index]); - } - }, - ), - ), - if (presetForActions != null) ...[ - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.edit_outlined), - onPressed: () => _showRenameDialog( - context, presetForActions), - tooltip: 'Rename', - style: IconButton.styleFrom( - minimumSize: const Size(36, 36), - ), - ), - IconButton( - icon: const Icon(Icons.delete_outline), - onPressed: () => - widget.onDeleteFilter(presetForActions), - tooltip: 'Delete', - style: IconButton.styleFrom( - minimumSize: const Size(36, 36), - ), - ), - ], - ], - ), - const SizedBox(height: 16), - ], - ); - }, - ), - - // Name Prefix Filter - _buildFilterCard( - context: context, - title: "Name Prefixes", - icon: Icons.text_fields, - controller: widget.namePrefixController, - hintText: "e.g. MyDevice Sensor", - helperText: "Device names starting with these prefixes", - ), - const SizedBox(height: 16), - // Services Filter - _buildFilterCard( - context: context, - title: "Service UUIDs", - maxLines: 3, - icon: Icons.apps, - controller: widget.servicesFilterController, - hintText: "e.g. 0000180f-0000-1000-8000-00805f9b34fb,180F", - helperText: "Service UUIDs (16-bit, 32-bit, or 128-bit)", - ), - const SizedBox(height: 16), - // Manufacturer Data Filter - _buildFilterCard( - context: context, - title: "Manufacturer Company IDs", - icon: Icons.business, - controller: widget.manufacturerDataController, - hintText: "e.g. 76,0x004C,Apple, Inc.", - helperText: - "Company identifiers (decimal/hex) or company names", - ), - const SizedBox(height: 24), - // Action Buttons - Row( - children: [ - Expanded( - flex: 2, - child: OutlinedButton.icon( - onPressed: clearFilter, - icon: const Icon(Icons.clear), - label: const Text('Clear All'), - style: OutlinedButton.styleFrom( - foregroundColor: colorScheme.onSurface, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - const SizedBox(width: 5), - Expanded( - flex: 3, - child: ElevatedButton.icon( - onPressed: applyFilter, - icon: const Icon(Icons.check), - label: const Text('Apply Filters'), - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - ], - ), - const SizedBox(height: 8), - ], + return Padding( + padding: MediaQuery.of(context).viewInsets.copyWith(left: 20, right: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 20), + Center( + child: Text( + "Scan Filters", + style: Theme.of(context).textTheme.titleLarge, ), ), - ), - ), - ); - } - - Widget _buildFilterCard({ - required BuildContext context, - required String title, - required IconData icon, - required TextEditingController controller, - required String hintText, - required String helperText, - int maxLines = 2, - }) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return Card( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide( - color: colorScheme.outline.withValues(alpha: 0.2), - ), - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - icon, - size: 20, - color: colorScheme.primary, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - title, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - ), - ], + const Text("Use comma to add multiple values"), + const Divider(), + const SizedBox(height: 10), + TextFormField( + controller: widget.namePrefixController, + maxLines: 2, + decoration: const InputDecoration( + labelText: "Name Prefixes", + border: OutlineInputBorder(), ), - const SizedBox(height: 4), - TextFormField( - controller: controller, - maxLines: maxLines, - style: const TextStyle(fontFamily: 'monospace'), - decoration: InputDecoration( - hintText: hintText, - hintStyle: TextStyle( - color: colorScheme.onSurface.withValues(alpha: 0.15), - ), - helperText: helperText, - helperMaxLines: 2, - helperStyle: TextStyle( - fontSize: 11, - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: colorScheme.outline.withValues(alpha: 0.5), - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: colorScheme.outline.withValues(alpha: 0.3), - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide( - color: colorScheme.primary, - width: 2, - ), - ), - filled: true, - fillColor: - colorScheme.surfaceContainerHighest.withValues(alpha: 0.3), - contentPadding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 8, + ), + const SizedBox(height: 10), + TextFormField( + controller: widget.servicesFilterController, + maxLines: 2, + decoration: const InputDecoration( + labelText: "Services", + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 10), + TextFormField( + controller: widget.manufacturerDataController, + maxLines: 2, + decoration: const InputDecoration( + labelText: "Manufacturer Data Company IDs", + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: PlatformButton( + text: 'Apply', + onPressed: applyFilter, ), ), + const SizedBox(width: 10), + Expanded( + child: PlatformButton( + text: 'Clear', + onPressed: clearFilter, + ), + ), + ], + ), + const SizedBox(height: 10), + if (error != null) + Text( + error!, + style: const TextStyle(color: Colors.red), ), - ], - ), + const SizedBox(height: 10), + ], ), ); } diff --git a/example/lib/home/widgets/scanned_devices_placeholder_widget.dart b/example/lib/home/widgets/scanned_devices_placeholder_widget.dart index e6b1ca5..bcc7437 100644 --- a/example/lib/home/widgets/scanned_devices_placeholder_widget.dart +++ b/example/lib/home/widgets/scanned_devices_placeholder_widget.dart @@ -1,79 +1,26 @@ import 'package:flutter/material.dart'; class ScannedDevicesPlaceholderWidget extends StatelessWidget { - final Function() onTap; - const ScannedDevicesPlaceholderWidget({super.key, required this.onTap}); + const ScannedDevicesPlaceholderWidget({super.key}); @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return Center( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - 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: 32), - Text( - 'No Devices Found', - style: TextStyle( - color: colorScheme.onSurface, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 48), - child: Text( - 'Start scanning to discover nearby Bluetooth devices', - textAlign: TextAlign.center, - style: TextStyle( - 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( - 'Scan', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - style: FilledButton.styleFrom( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 2, - ), - ), - ], + return const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.bluetooth, + color: Colors.grey, + size: 100, + ), ), - ), + Text( + 'Scan For Devices', + style: TextStyle(color: Colors.grey, fontSize: 22), + ) + ], ); } } diff --git a/example/lib/home/widgets/scanned_item_widget.dart b/example/lib/home/widgets/scanned_item_widget.dart index 73b4786..37f5a82 100644 --- a/example/lib/home/widgets/scanned_item_widget.dart +++ b/example/lib/home/widgets/scanned_item_widget.dart @@ -1,692 +1,52 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:universal_ble/universal_ble.dart'; -import 'package:universal_ble_example/data/company_identifier_service.dart'; -import 'package:universal_ble_example/home/widgets/rssi_signal_indicator.dart'; -import 'package:universal_ble_example/widgets/company_info_widget.dart'; -import 'package:universal_ble_example/widgets/flash_on_update_widget.dart'; -class ScannedItemWidget extends StatefulWidget { +class ScannedItemWidget extends StatelessWidget { final BleDevice bleDevice; - final int adFlashTrigger; final VoidCallback? onTap; - final bool isExpanded; - final Function(bool) onExpand; - const ScannedItemWidget({ - super.key, - required this.bleDevice, - this.adFlashTrigger = 0, - this.onTap, - required this.isExpanded, - required this.onExpand, - }); - - @override - State createState() => _ScannedItemWidgetState(); -} - -class _ScannedItemWidgetState extends State { - Timer? _timer; - - static String _formatLastAppeared(DateTime? timestamp) { - if (timestamp == null) return '—'; - final elapsed = DateTime.now().difference(timestamp); - if (elapsed.inSeconds < 2) return 'Actively advertising'; - if (elapsed.inSeconds < 60) { - return '${elapsed.inSeconds} second${elapsed.inSeconds == 1 ? '' : 's'} ago'; - } - if (elapsed.inMinutes < 60) { - return '${elapsed.inMinutes} minute${elapsed.inMinutes == 1 ? '' : 's'} ago'; - } - return '${elapsed.inHours} hour${elapsed.inHours == 1 ? '' : 's'} ago'; - } - - @override - void initState() { - super.initState(); - _timer = Timer.periodic(const Duration(seconds: 1), (_) { - if (mounted) setState(() {}); - }); - } - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } + const ScannedItemWidget({super.key, required this.bleDevice, this.onTap}); @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - String? name = widget.bleDevice.name; - List rawManufacturerData = - widget.bleDevice.manufacturerDataList; - if (name == null || name.isEmpty) name = 'Unknown Device'; - - final lastAppearedStr = - _formatLastAppeared(widget.bleDevice.timestampDateTime); - - return FlashOnUpdateWidget( - trigger: widget.adFlashTrigger, - borderRadius: 16, + String? name = bleDevice.name; + List rawManufacturerData = bleDevice.manufacturerDataList; + ManufacturerData? manufacturerData; + if (rawManufacturerData.isNotEmpty) { + manufacturerData = rawManufacturerData.first; + } + if (name == null || name.isEmpty) name = 'N/A'; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Card( - elevation: 2, - margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: InkWell( - onTap: widget.onTap, - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Signal indicator - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: - colorScheme.primaryContainer.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(12), - ), - child: RssiSignalIndicator( - rssi: widget.bleDevice.rssi ?? 0), - ), - const SizedBox(width: 16), - // Device info - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Text( - name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 8), - // Expand/Collapse button - if (rawManufacturerData.isNotEmpty || - widget.bleDevice.services.isNotEmpty || - widget.bleDevice.serviceData.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 4, - ), - decoration: BoxDecoration( - color: colorScheme.primaryContainer - .withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(8), - ), - child: IconButton( - icon: Icon( - widget.isExpanded - ? Icons.expand_less - : Icons.expand_more, - color: colorScheme.primary, - size: 18, - ), - onPressed: () { - widget.onExpand(!widget.isExpanded); - }, - tooltip: widget.isExpanded - ? 'Collapse' - : 'Expand', - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 20, - minHeight: 20, - ), - ), - ), - ], - ), - const SizedBox(height: 6), - Row( - children: [ - Icon( - Icons.fingerprint, - size: 12, - color: - colorScheme.onSurface.withValues(alpha: 0.6), - ), - const SizedBox(width: 4), - Expanded( - child: Text( - widget.bleDevice.deviceId, - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurface - .withValues(alpha: 0.7), - fontFamily: 'monospace', - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const SizedBox(height: 6), - // Last seen - Row( - children: [ - Icon( - Icons.ad_units_outlined, - size: 12, - color: colorScheme.primary - .withValues(alpha: 0.8), - ), - const SizedBox(width: 6), - Expanded( - child: Text( - lastAppearedStr == 'Actively advertising' - ? lastAppearedStr - : 'Last seen: $lastAppearedStr', - style: TextStyle( - fontSize: 11, - fontFamily: 'monospace', - color: colorScheme.onSurface - .withValues(alpha: 0.7), - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const SizedBox(height: 6), - Wrap( - spacing: 4, - runSpacing: 4, - children: [ - // Pair status - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: widget.bleDevice.paired == true - ? Colors.green.withValues(alpha: 0.2) - : Colors.orange.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - widget.bleDevice.paired == true - ? 'Paired' - : 'Unpaired', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - color: widget.bleDevice.paired == true - ? Colors.green.shade700 - : Colors.orange.shade700, - ), - ), - ), - // Manufacturer data (only in collapsed mode) - if (!widget.isExpanded) ...[ - ...rawManufacturerData.take(2).map((data) { - final companyName = CompanyIdentifierService - .instance - .getCompanyName(data.companyId); - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - data.companyIdRadix16, - style: TextStyle( - fontSize: 10, - color: - colorScheme.onSecondaryContainer, - fontWeight: FontWeight.w500, - fontFamily: 'monospace', - ), - ), - if (companyName != null) ...[ - const SizedBox(height: 2), - Text( - companyName, - style: TextStyle( - fontSize: 9, - color: colorScheme - .onSecondaryContainer - .withValues(alpha: 0.8), - fontWeight: FontWeight.w400, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ], - ), - ); - }), - ], - // Services (only in collapsed mode) - if (!widget.isExpanded) ...[ - ...widget.bleDevice.services.take(3).map((service) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: colorScheme.tertiaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - service.length > 12 - ? '${service.substring(0, 12)}...' - : service, - style: TextStyle( - fontSize: 10, - color: colorScheme.onTertiaryContainer, - fontWeight: FontWeight.w500, - ), - ), - ); - }), - if (widget.bleDevice.services.length > 3) - Text( - '+${widget.bleDevice.services.length - 3} more', - style: TextStyle( - fontSize: 10, - color: colorScheme.onSurface - .withValues(alpha: 0.5), - ), - ), - ], - ], - ), - ], - ), - ), - ], + child: ListTile( + title: Text( + '$name (${bleDevice.rssi})', + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(bleDevice.deviceId), + Visibility( + visible: manufacturerData != null, + child: Text(manufacturerData.toString()), ), - // Expanded details - if (widget.isExpanded) ...[ - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 12), - // Manufacturer Data (always shown) - Text( - 'Manufacturer Data', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 8), - if (rawManufacturerData.isEmpty) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), + if (bleDevice.timestampDateTime != null) + Text("Last Seen: ${bleDevice.timestampDateTime}"), + bleDevice.paired == true + ? const Text( + "Paired", + style: TextStyle(color: Colors.green), + ) + : const Text( + "Not Paired", + style: TextStyle(color: Colors.red), ), - child: Text( - 'No manufacturer data', - style: TextStyle( - fontSize: 11, - color: colorScheme.onSecondaryContainer - .withValues(alpha: 0.7), - fontStyle: FontStyle.italic, - ), - ), - ) - else - ...rawManufacturerData.map((data) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - 'Company ID: ', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: colorScheme - .onSecondaryContainer, - ), - ), - Expanded( - child: SelectableText( - data.companyIdRadix16, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: colorScheme - .onSecondaryContainer, - fontFamily: 'monospace', - ), - ), - ), - ], - ), - CompanyInfoWidget( - companyId: data.companyId, - colorScheme: colorScheme, - labelStyle: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: - colorScheme.onSecondaryContainer, - ), - nameStyle: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w500, - color: - colorScheme.onSecondaryContainer, - ), - ), - if (data.payloadRadix16.isNotEmpty) ...[ - const SizedBox(height: 4), - Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Expanded( - child: SelectableText( - data.payloadRadix16, - style: TextStyle( - fontSize: 10, - color: colorScheme - .onSecondaryContainer - .withValues(alpha: 0.8), - fontFamily: 'monospace', - ), - ), - ), - ], - ), - ], - ], - ), - ), - const SizedBox(width: 8), - IconButton( - icon: Icon( - Icons.copy, - size: 16, - color: colorScheme.onSecondaryContainer, - ), - onPressed: () { - final textToCopy = data - .payloadRadix16.isNotEmpty - ? 'Company ID: ${data.companyIdRadix16}\nPayload: ${data.payloadRadix16}' - : 'Company ID: ${data.companyIdRadix16}'; - Clipboard.setData( - ClipboardData(text: textToCopy), - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - const Text('Copied to clipboard'), - duration: const Duration(seconds: 1), - behavior: SnackBarBehavior.floating, - ), - ); - }, - tooltip: 'Copy', - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 24, - minHeight: 24, - ), - visualDensity: VisualDensity.compact, - ), - ], - ), - ], - ), - ), - ); - }), - const SizedBox(height: 12), - if (widget.bleDevice.services.isNotEmpty) ...[ - Text( - 'Advertised Services', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 8), - Wrap( - spacing: 4, - runSpacing: 4, - children: widget.bleDevice.services.map((service) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: colorScheme.tertiaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SelectableText( - service, - style: TextStyle( - fontSize: 10, - color: colorScheme.onTertiaryContainer, - fontWeight: FontWeight.w500, - fontFamily: 'monospace', - ), - ), - const SizedBox(width: 6), - InkWell( - onTap: () { - Clipboard.setData(ClipboardData(text: service)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('Copied to clipboard'), - duration: const Duration(seconds: 1), - behavior: SnackBarBehavior.floating, - ), - ); - }, - child: Icon( - Icons.copy, - size: 14, - color: colorScheme.onTertiaryContainer - .withValues(alpha: 0.7), - ), - ), - ], - ), - ); - }).toList(), - ), - ], - // Service Data (always shown, last) - Text( - 'Service Data', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 8), - if (widget.bleDevice.serviceData.isEmpty) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: colorScheme.tertiaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - 'No service data', - style: TextStyle( - fontSize: 11, - color: colorScheme.onTertiaryContainer - .withValues(alpha: 0.7), - fontStyle: FontStyle.italic, - ), - ), - ) - else - ...widget.bleDevice.serviceData.entries.map((entry) { - final uuid = entry.key; - final bytes = entry.value; - final hex = bytes.isEmpty - ? '' - : '0x${bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join('').toUpperCase()}'; - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: colorScheme.tertiaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - 'Service UUID: ', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: colorScheme - .onTertiaryContainer, - ), - ), - Expanded( - child: SelectableText( - uuid, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: colorScheme - .onTertiaryContainer, - fontFamily: 'monospace', - ), - ), - ), - ], - ), - if (hex.isNotEmpty) ...[ - const SizedBox(height: 4), - SelectableText( - hex, - style: TextStyle( - fontSize: 10, - color: colorScheme - .onTertiaryContainer - .withValues(alpha: 0.8), - fontFamily: 'monospace', - ), - ), - ], - ], - ), - ), - const SizedBox(width: 8), - IconButton( - icon: Icon( - Icons.copy, - size: 16, - color: colorScheme.onTertiaryContainer, - ), - onPressed: () { - final textToCopy = hex.isNotEmpty - ? 'Service UUID: $uuid\nData: $hex' - : 'Service UUID: $uuid'; - Clipboard.setData( - ClipboardData(text: textToCopy), - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - const Text('Copied to clipboard'), - duration: const Duration(seconds: 1), - behavior: SnackBarBehavior.floating, - ), - ); - }, - tooltip: 'Copy', - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 24, - minHeight: 24, - ), - visualDensity: VisualDensity.compact, - ), - ], - ), - ], - ), - ), - ); - }), - ], ], ), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: onTap, ), ), - ), ); } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 55ce12d..01f1d47 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:universal_ble_example/data/scan_controller.dart'; -import 'package:universal_ble_example/universal_ble_app.dart'; +import 'package:universal_ble/universal_ble.dart'; +import 'package:universal_ble_example/home/home.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - bool hasPermission = await initializeApp(); - final scanController = ScanController(); - await scanController.syncState(); - runApp(UniversalBleApp( - hasPermission: hasPermission, - scanController: scanController, - )); + await UniversalBle.setLogLevel(BleLogLevel.verbose); + runApp( + MaterialApp( + title: 'Universal BLE', + debugShowCheckedModeBanner: false, + darkTheme: ThemeData.dark(), + themeMode: ThemeMode.system, + home: const MyApp(), + ), + ); } diff --git a/example/lib/main_device_preview.dart b/example/lib/main_device_preview.dart deleted file mode 100644 index 63b29ca..0000000 --- a/example/lib/main_device_preview.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'dart:io'; - -import 'package:device_preview_screenshot/device_preview_screenshot.dart'; -import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:universal_ble_example/data/scan_controller.dart'; -import 'package:universal_ble_example/universal_ble_app.dart'; - -void main() async { - bool hasPermission = await initializeApp(); - final scanController = ScanController(); - await scanController.syncState(); - - // On macOS: ~/Library/Containers/com.navideck.universalble/Data/Documents/screenshots - final screenshotsDir = await getScreenshotsDirectory(); - debugPrint('Screenshots will be saved to: ${screenshotsDir.path}'); - - runApp( - DevicePreview( - enabled: true, - builder: (context) => UniversalBleApp( - hasPermission: hasPermission, - scanController: scanController, - locale: DevicePreview.locale(context), - builder: DevicePreview.appBuilder, - ), - devices: [ - createCustomDevice( - name: 'App Store 1242x2688', - physicalWidth: 1242, - physicalHeight: 2688, - ), - createCustomDevice( - name: 'App Store iPad 2752x2064', - physicalWidth: 2752, - physicalHeight: 2064, - deviceType: DeviceType.tablet, - ), - ], - tools: [ - ...DevicePreview.defaultTools, - DevicePreviewScreenshot( - onScreenshot: screenshotAsFiles(screenshotsDir), - ), - ], - ), - ); -} - -/// Gets or creates the screenshots directory in the app's documents directory. -Future getScreenshotsDirectory() async { - final appDocumentsDir = await getApplicationDocumentsDirectory(); - final screenshotsDir = Directory('${appDocumentsDir.path}/screenshots'); - - if (!await screenshotsDir.exists()) { - await screenshotsDir.create(recursive: true); - } - - return screenshotsDir; -} - -/// Creates a custom device with the specified physical pixel dimensions. -DeviceInfo createCustomDevice({ - required String name, - required double physicalWidth, - required double physicalHeight, - double pixelRatio = 3.0, - DeviceType deviceType = DeviceType.phone, -}) { - final logicalWidth = physicalWidth / pixelRatio; - final logicalHeight = physicalHeight / pixelRatio; - - return DeviceInfo( - identifier: DeviceIdentifier( - TargetPlatform.iOS, - deviceType, - name, - ), - name: name, - pixelRatio: pixelRatio, - safeAreas: EdgeInsets.zero, - rotatedSafeAreas: EdgeInsets.zero, - screenPath: Path() - ..addRect(Rect.fromLTWH(0.0, 0.0, logicalWidth, logicalHeight)), - frameSize: Size(logicalWidth, logicalHeight), - screenSize: Size(logicalWidth, logicalHeight), - framePainter: const _EmptyFramePainter(), - ); -} - -class _EmptyFramePainter extends CustomPainter { - const _EmptyFramePainter(); - - @override - void paint(Canvas canvas, Size size) {} - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} diff --git a/example/lib/peripheral_details/peripheral_detail_page.dart b/example/lib/peripheral_details/peripheral_detail_page.dart index dfdf3bc..d940b3f 100644 --- a/example/lib/peripheral_details/peripheral_detail_page.dart +++ b/example/lib/peripheral_details/peripheral_detail_page.dart @@ -3,18 +3,12 @@ import 'dart:async'; import 'package:convert/convert.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:universal_ble/universal_ble.dart'; -import 'package:universal_ble_example/data/storage_service.dart'; -import 'package:universal_ble_example/data/utils.dart'; -import 'package:universal_ble_example/peripheral_details/widgets/result_header_bar.dart'; import 'package:universal_ble_example/peripheral_details/widgets/result_widget.dart'; import 'package:universal_ble_example/peripheral_details/widgets/services_list_widget.dart'; -import 'package:universal_ble_example/peripheral_details/widgets/services_side_widget.dart'; -import 'package:universal_ble_example/data/company_identifier_service.dart'; -import 'package:universal_ble_example/data/scan_controller.dart'; +import 'package:universal_ble_example/widgets/platform_button.dart'; +import 'package:universal_ble_example/widgets/responsive_buttons_grid.dart'; import 'package:universal_ble_example/widgets/responsive_view.dart'; -import 'package:universal_ble_example/widgets/scan_controller_scope.dart'; class PeripheralDetailPage extends StatefulWidget { final BleDevice bleDevice; @@ -28,90 +22,45 @@ class PeripheralDetailPage extends StatefulWidget { class _PeripheralDetailPageState extends State { late final bleDevice = widget.bleDevice; - ScanController? _scanController; - void _onScanStateChanged() => setState(() {}); - bool isConnected = false; GlobalKey valueFormKey = GlobalKey(); List discoveredServices = []; final List _logs = []; final binaryCode = TextEditingController(); - bool _isDiscoveringServices = false; - bool _isDeviceInfoExpanded = false; - bool _isDeviceActionsExpanded = true; - bool _isReceivedAdvertisementsExpanded = true; - final Map _subscribedCharacteristics = {}; - bool _autoConnect = false; StreamSubscription? connectionStreamSubscription; StreamSubscription? pairingStateSubscription; - StreamSubscription? _advertisementSubscription; - List _receivedAdvertisements = []; BleService? selectedService; BleCharacteristic? selectedCharacteristic; - final ScrollController _logsScrollController = ScrollController(); - final Set _favoriteServices = {}; - Set? _currentPropertyFilters; - - Future _loadFavoriteServices() async { - final favorites = await StorageService.instance.getFavoriteServices(); - if (mounted) { - setState(() { - _favoriteServices.addAll(favorites); - }); - } - } - - Future _saveFavoriteServices() async { - await StorageService.instance.setFavoriteServices( - _favoriteServices.toList(), - ); - } @override void initState() { super.initState(); - _receivedAdvertisements = [bleDevice]; - _advertisementSubscription = UniversalBle.scanStream - .where((d) => d.deviceId == bleDevice.deviceId) - .listen((d) { - if (mounted) { + connectionStreamSubscription = + bleDevice.connectionStream.listen(_handleConnectionChange); + pairingStateSubscription = + bleDevice.pairingStateStream.listen(_handlePairingStateChange); + UniversalBle.onValueChange = _handleValueChange; + _asyncInits(); + } + + void _asyncInits() { + bleDevice.connectionState.then((state) { + if (state == BleConnectionState.connected) { setState(() { - _receivedAdvertisements.add(d); + isConnected = true; }); } }); - - connectionStreamSubscription = bleDevice.connectionStream.listen( - _handleConnectionChange, - ); - pairingStateSubscription = bleDevice.pairingStateStream.listen( - _handlePairingStateChange, - ); - UniversalBle.onValueChange = _handleValueChange; - - bleDevice.connectionState.then((state) { - _handleConnectionChange(state == BleConnectionState.connected); - }); - _loadFavoriteServices(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _scanController ??= ScanControllerScope.of(context) - ..addListener(_onScanStateChanged); } @override void dispose() { - _scanController?.removeListener(_onScanStateChanged); - _advertisementSubscription?.cancel(); + super.dispose(); connectionStreamSubscription?.cancel(); pairingStateSubscription?.cancel(); UniversalBle.onValueChange = null; - super.dispose(); } void _addLog(String type, dynamic data) { @@ -132,21 +81,14 @@ class _PeripheralDetailPageState extends State { } } - void _handleValueChange( - String deviceId, - String characteristicId, - Uint8List value, - int? timestamp, - ) { - String data = _formatReadValue(value); + void _handleValueChange(String deviceId, String characteristicId, + Uint8List value, int? timestamp) { + String s = String.fromCharCodes(value); + String data = '$s\nraw : ${value.toString()}'; DateTime? timestampDateTime = timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp) : null; - // Extract hex for debug print (format: (0x...)) - String formattedHex = - '(0x${value.map((b) => b.toRadixString(16).padLeft(2, '0')).join()})'; - debugPrint( - '_handleValueChange ($timestampDateTime) $characteristicId, $formattedHex'); + debugPrint('_handleValueChange ($timestampDateTime) $characteristicId, $s'); _addLog("Value", data); } @@ -158,92 +100,39 @@ class _PeripheralDetailPageState extends State { Future _discoverServices() async { const webWarning = "Note: Only services added in ScanFilter or WebOptions will be discovered"; - - setState(() { - _isDiscoveringServices = true; - }); - - await _executeWithLoading( - () async { - var services = await bleDevice.discoverServices(withDescriptors: false); - debugPrint('${services.length} services discovered'); - debugPrint(_formattedServices(services)); - setState(() { - discoveredServices = services; - }); - - // Build log message step-by-step for clarity - final StringBuffer logMessage = StringBuffer(); - logMessage.write('${services.length} services discovered'); - if (kIsWeb) { - logMessage.write(',\n$webWarning'); - } - logMessage.write('\n\n${_formattedServices(services)}'); - - _addLog("DiscoverServices", logMessage.toString()); - }, - onError: (error) { - final StringBuffer errorMessage = StringBuffer(); - errorMessage.write(error); - if (kIsWeb) { - errorMessage.write('\n$webWarning'); - } - _addLog("DiscoverServicesError", errorMessage.toString()); - }, - ); - - setState(() { - _isDiscoveringServices = false; - }); - } - - String _formatReadValue(Uint8List value) { - String formattedHex = - '(0x${value.map((b) => b.toRadixString(16).padLeft(2, '0')).join()})'; - String stringValue = ''; try { - // Find the first null byte (0x00) to handle null-terminated strings - int nullIndex = value.indexOf(0); - Uint8List stringBytes = - nullIndex >= 0 ? value.sublist(0, nullIndex) : value; + var services = await bleDevice.discoverServices(withDescriptors: false); + debugPrint('${services.length} services discovered'); + debugPrint(services.toString()); + setState(() { + discoveredServices = services; + }); - if (stringBytes.isNotEmpty) { - stringValue = String.fromCharCodes(stringBytes); - // Check if it's a valid printable string (not just control characters) - // Allow tab, newline, carriage return - if (stringValue.isNotEmpty && - !stringValue.codeUnits.every((code) => - (code >= 32 && code <= 126) || - code == 9 || - code == 10 || - code == 13)) { - stringValue = ''; - } + if (kIsWeb) { + _addLog( + "DiscoverServices", + '${services.length} services discovered,\n$webWarning', + ); } } catch (e) { - // Not a valid string, leave empty + _addLog("DiscoverServicesError", '$e\n${kIsWeb ? webWarning : ""}'); } - return stringValue.isNotEmpty - ? '"$stringValue" $formattedHex\nraw: ${value.toString()}' - : '$formattedHex\nraw: ${value.toString()}'; } Future _readValue() async { BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic; if (selectedCharacteristic == null) return; - await _executeWithLoading( - () async { - Uint8List value = await selectedCharacteristic.read(); - String data = _formatReadValue(value); - _addLog('Read', data); - }, - onError: (error) { - _addLog('ReadError', error); - }, - ); + try { + Uint8List value = await selectedCharacteristic.read(); + String s = String.fromCharCodes(value); + String data = '$s\nraw : ${value.toString()}'; + _addLog('Read', data); + } catch (e) { + _addLog('ReadError', e); + } } - Future _writeValue() async { + Future _writeValue({required bool withResponse}) async { BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic; if (selectedCharacteristic == null || !valueFormKey.currentState!.validate() || @@ -259,142 +148,41 @@ class _PeripheralDetailPageState extends State { return; } - bool writeWithResponse = true; - if (!selectedCharacteristic.properties.contains( - CharacteristicProperty.write, - )) { - writeWithResponse = false; + try { + await selectedCharacteristic.write(value, withResponse: withResponse); + _addLog('Write${withResponse ? "" : "WithoutResponse"}', value); + } catch (e) { + debugPrint(e.toString()); + _addLog('WriteError', e); } - - await _executeWithLoading( - () async { - await selectedCharacteristic.write( - value, - withResponse: writeWithResponse, - ); - _addLog('Write${writeWithResponse ? "" : "WithoutResponse"}', value); - }, - onError: (error) { - _addLog('WriteError', error); - }, - ); } Future _subscribeChar() async { BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic; if (selectedCharacteristic == null) return; - await _executeWithLoading( - () async { - var subscription = _getCharacteristicSubscription( - selectedCharacteristic, - ); - if (subscription == null) throw 'No notify or indicate property'; - await subscription.subscribe(); - setState(() { - _subscribedCharacteristics[selectedCharacteristic.uuid] = true; - }); - _addLog('BleCharSubscription', 'Subscribed'); - }, - onError: (error) { - _addLog('NotifyError', error); - }, - ); + try { + var subscription = _getCharacteristicSubscription(selectedCharacteristic); + if (subscription == null) throw 'No notify or indicate property'; + await subscription.subscribe(); + _addLog('BleCharSubscription', 'Subscribed'); + // Updates can also be handled by + // subscription.listen((data) {}); + } catch (e) { + _addLog('NotifyError', e); + } } Future _unsubscribeChar() async { - final char = selectedCharacteristic; - if (char == null) return; - await _executeWithLoading( - () async { - await char.unsubscribe(); - setState(() { - _subscribedCharacteristics.remove(char.uuid); - }); - _addLog('BleCharSubscription', 'UnSubscribed'); - }, - onError: (error) { - _addLog('NotifyError', error); - }, - ); - } - - Future _subscribeToAllCharacteristics() async { - if (!isConnected) return; - await _executeWithLoading( - () async { - int successCount = 0; - int errorCount = 0; - for (var service in discoveredServices) { - for (var characteristic in service.characteristics) { - var subscription = _getCharacteristicSubscription(characteristic); - if (subscription != null) { - try { - await subscription.subscribe(); - setState(() { - _subscribedCharacteristics[characteristic.uuid] = true; - }); - successCount++; - } catch (e) { - errorCount++; - debugPrint('Failed to subscribe to ${characteristic.uuid}: $e'); - } - } - } - } - _addLog( - 'BleCharSubscription', - 'Subscribed to $successCount characteristics${errorCount > 0 ? ', $errorCount failed' : ''}', - ); - }, - onError: (error) { - _addLog('SubscribeToAllCharacteristicsError', error); - }, - ); - } - - Future _readAllCharacteristics() async { - if (!isConnected || discoveredServices.isEmpty) return; - await _executeWithLoading( - () async { - int successCount = 0; - int errorCount = 0; - for (var service in discoveredServices) { - for (var characteristic in service.characteristics) { - if (characteristic.properties - .contains(CharacteristicProperty.read)) { - try { - Uint8List value = await characteristic.read(); - String data = _formatReadValue(value); - _addLog( - 'ReadAll', - '${service.uuid}/${characteristic.uuid}: $data', - ); - successCount++; - } catch (e) { - errorCount++; - _addLog( - 'ReadAllError', - '${service.uuid}/${characteristic.uuid}: $e', - ); - debugPrint('Failed to read ${characteristic.uuid}: $e'); - } - } - } - } - _addLog( - 'ReadAll', - 'Completed: $successCount successful${errorCount > 0 ? ', $errorCount failed' : ''}', - ); - }, - onError: (error) { - _addLog('ReadAllCharacteristicsError', error); - }, - ); + try { + await selectedCharacteristic?.unsubscribe(); + _addLog('BleCharSubscription', 'UnSubscribed'); + } catch (e) { + _addLog('NotifyError', e); + } } CharacteristicSubscription? _getCharacteristicSubscription( - BleCharacteristic characteristic, - ) { + BleCharacteristic characteristic) { var properties = characteristic.properties; if (properties.contains(CharacteristicProperty.notify)) { return characteristic.notifications; @@ -404,1725 +192,306 @@ class _PeripheralDetailPageState extends State { return null; } - Future _executeWithLoading( - Future Function() action, { - Function(dynamic error)? onError, - }) async { - try { - return await action(); - } catch (e) { - onError?.call(e); - rethrow; - } - } - - void _showCharacteristicSelector() { - showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - builder: (context) { - return Padding( - padding: EdgeInsets.only(bottom: 10.0), - child: ServicesSideWidget( - discoveredServices: discoveredServices, - selectedService: selectedService, - selectedCharacteristic: selectedCharacteristic, - initialPropertyFilters: _currentPropertyFilters, - isDiscoveringServices: _isDiscoveringServices, - serviceListBuilder: - (propertyFilters, listKey, isDiscoveringServices) => - _buildServicesList( - onSelect: (service, characteristic) { - setState(() { - selectedService = service; - selectedCharacteristic = characteristic; - }); - Navigator.pop(context); - }, - propertyFilters: propertyFilters, - listKey: listKey, - isDiscoveringServices: isDiscoveringServices, - ), - onCharacteristicSelected: (service, characteristic) { - setState(() { - selectedService = service; - selectedCharacteristic = characteristic; - }); - }, - onPropertyFiltersChanged: (propertyFilters) { - setState(() { - _currentPropertyFilters = propertyFilters; - }); - }, - onCopyServices: discoveredServices.isNotEmpty - ? () async { - await _copyServicesToClipboard(); - if (context.mounted) { - Navigator.pop(context); - } - } - : null, - ), - ); - }, - ); - } - - Future _copyServicesToClipboard() async { - final servicesText = _formattedServices(discoveredServices); - await Clipboard.setData( - ClipboardData(text: servicesText), - ); - if (context.mounted) { - _showSnackBar('All services copied to clipboard'); - } - } - - void _showSnackBar(String message) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - ), - ); - } - } - - String _formattedServices(List services) { - final buffer = StringBuffer(); - for (var service in services) { - buffer.writeln('Service: ${service.uuid}'); - for (var characteristic in service.characteristics) { - final properties = - characteristic.properties.map((p) => p.name).join(', '); - buffer.writeln(' ${characteristic.uuid} ($properties)'); - } - buffer.writeln(); - } - return buffer.toString().trim(); - } - - @override - void setState(VoidCallback fn) { - if (mounted) super.setState(fn); - } - @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return ResponsiveView( - builder: (_, DeviceType deviceType) { - return Scaffold( - appBar: AppBar( - title: Text( - bleDevice.name ?? "Unknown Device", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), + return Scaffold( + appBar: AppBar( + title: Text("${bleDevice.name ?? "Unknown"} - ${bleDevice.deviceId}"), + elevation: 4, + actions: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + isConnected + ? Icons.bluetooth_connected + : Icons.bluetooth_disabled, + color: isConnected ? Colors.greenAccent : Colors.red, + size: 20, ), - centerTitle: false, - elevation: 0, - actions: [ - // Persistent scan button - if (_scanController != null) - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: FilledButton.icon( - onPressed: () async { - if (_scanController!.isScanning) { - await _scanController!.stopScan(); - } else { - try { - await _scanController!.startScan(); - } catch (e) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString())), - ); - } - } - }, - icon: Icon( - _scanController!.isScanning - ? Icons.stop_circle - : Icons.play_arrow, - size: 20, - ), - label: Text( - _scanController!.isScanning ? 'Stop' : 'Scan', - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - style: FilledButton.styleFrom( - backgroundColor: _scanController!.isScanning - ? colorScheme.error - : colorScheme.primary, - foregroundColor: _scanController!.isScanning - ? colorScheme.onError - : colorScheme.onPrimary, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - elevation: _scanController!.isScanning ? 0 : 2, - ), - ), - ), - ], - ), - body: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Services side widget on desktop - if (deviceType == DeviceType.desktop) - Expanded( - flex: 1, - child: ServicesSideWidget( - discoveredServices: discoveredServices, - selectedService: selectedService, - selectedCharacteristic: selectedCharacteristic, - initialPropertyFilters: _currentPropertyFilters, - isDiscoveringServices: _isDiscoveringServices, - serviceListBuilder: - (propertyFilters, listKey, isDiscoveringServices) => - _buildServicesList( - propertyFilters: propertyFilters, - listKey: listKey, - isDiscoveringServices: isDiscoveringServices, - ), - onCharacteristicSelected: (service, characteristic) { - setState(() { - selectedService = service; - selectedCharacteristic = characteristic; - }); - }, - onPropertyFiltersChanged: (propertyFilters) { - setState(() { - _currentPropertyFilters = propertyFilters; - }); - }, - onCopyServices: discoveredServices.isNotEmpty - ? _copyServicesToClipboard - : null, - ), - ), - // Main content + ) + ], + ), + body: ResponsiveView(builder: (_, DeviceType deviceType) { + return Row( + children: [ + if (deviceType == DeviceType.desktop) Expanded( - flex: 2, + flex: 1, + child: Container( + color: Theme.of(context).secondaryHeaderColor, + child: discoveredServices.isEmpty + ? const Center( + child: Text('No Services Discovered'), + ) + : ServicesListWidget( + discoveredServices: discoveredServices, + scrollable: true, + onTap: (service, characteristic) { + setState(() { + selectedService = service; + selectedCharacteristic = characteristic; + }); + }, + ), + ), + ), + Expanded( + flex: 3, + child: Align( + alignment: Alignment.topCenter, child: SingleChildScrollView( child: Column( children: [ - _buildDeviceInfo(), + // Top buttons + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + PlatformButton( + text: 'Connect', + enabled: !isConnected, + onPressed: () async { + try { + await bleDevice.connect(); + _addLog("ConnectionResult", true); + } catch (e) { + _addLog('ConnectError (${e.runtimeType})', e); + } + }, + ), + PlatformButton( + text: 'Disconnect', + enabled: isConnected, + onPressed: () async { + try { + await bleDevice.disconnect(); + _addLog("DisconnectResult", true); + } catch (e) { + _addLog( + 'DisconnectError (${e.runtimeType})', + e, + ); + } + }, + ), + ], + ), + ), + selectedCharacteristic == null + ? Text(discoveredServices.isEmpty + ? "Please discover services" + : "Please select a characteristic") + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + ), + child: Card( + child: ListTile( + title: SelectableText( + "Characteristic: ${selectedCharacteristic?.uuid}", + ), + subtitle: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SelectableText( + "Service: ${selectedService?.uuid}", + ), + Text( + "Properties: ${selectedCharacteristic?.properties.map((e) => e.name)}", + ), + ], + ), + ), + ), + ), - // Received advertisements list - _buildReceivedAdvertisements(), - - // Connect/Disconnect button - _buildConnectDisconnectButton(), - - // Device Actions - _buildDeviceActions(), - - // Characteristic selector - _buildCharacteristicSelector(), - - // Characteristic Actions - _buildCharacteristicActions(), - - // Logs on bottom for mobile - if (deviceType != DeviceType.desktop) ...[ - const Divider(), - _buildResultWidget(scrollable: false), - ], + if (_hasSelectedCharacteristicProperty([ + CharacteristicProperty.write, + CharacteristicProperty.writeWithoutResponse + ])) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Form( + key: valueFormKey, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: binaryCode, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a value'; + } + try { + hex.decode(binaryCode.text); + return null; + } catch (e) { + return 'Please enter a valid hex value ( without spaces or 0x (e.g. F0BB) )'; + } + }, + decoration: const InputDecoration( + hintText: + "Enter Hex values without spaces or 0x (e.g. F0BB)", + border: OutlineInputBorder(), + ), + ), + ), + ), + ), + const Divider(), + Padding( + padding: const EdgeInsets.all(8.0), + child: ResponsiveButtonsGrid( + children: [ + PlatformButton( + onPressed: () async { + _discoverServices(); + }, + enabled: isConnected, + text: 'Discover Services', + ), + PlatformButton( + onPressed: () async { + _addLog( + 'ConnectionState', + await bleDevice.connectionState, + ); + }, + text: 'Connection State', + ), + if (BleCapabilities.supportsRequestMtuApi) + PlatformButton( + enabled: isConnected, + onPressed: () async { + int mtu = await bleDevice.requestMtu(247); + _addLog('MTU', mtu); + }, + text: 'Request Mtu', + ), + PlatformButton( + enabled: isConnected && + discoveredServices.isNotEmpty && + _hasSelectedCharacteristicProperty([ + CharacteristicProperty.read, + ]), + onPressed: _readValue, + text: 'Read', + ), + PlatformButton( + enabled: isConnected && + discoveredServices.isNotEmpty && + _hasSelectedCharacteristicProperty([ + CharacteristicProperty.write, + ]), + onPressed: () => _writeValue(withResponse: true), + text: 'Write', + ), + PlatformButton( + enabled: isConnected && + discoveredServices.isNotEmpty && + _hasSelectedCharacteristicProperty([ + CharacteristicProperty.writeWithoutResponse, + ]), + onPressed: () => _writeValue(withResponse: false), + text: 'WriteWithoutResponse', + ), + PlatformButton( + enabled: isConnected && + discoveredServices.isNotEmpty && + _hasSelectedCharacteristicProperty([ + CharacteristicProperty.notify, + CharacteristicProperty.indicate + ]), + onPressed: _subscribeChar, + text: 'Subscribe', + ), + PlatformButton( + enabled: isConnected && + discoveredServices.isNotEmpty && + _hasSelectedCharacteristicProperty([ + CharacteristicProperty.notify, + CharacteristicProperty.indicate + ]), + onPressed: _unsubscribeChar, + text: 'Unsubscribe', + ), + PlatformButton( + enabled: BleCapabilities.supportsAllPairingKinds, + onPressed: () async { + try { + await bleDevice.pair( + // pairingCommand: BleCommand( + // service: "", + // characteristic: "", + // ), + ); + _addLog("Pairing Result", true); + } catch (e) { + _addLog('PairError (${e.runtimeType})', e); + } + }, + text: 'Pair', + ), + PlatformButton( + onPressed: () async { + bool? isPaired = await bleDevice.isPaired( + // pairingCommand: BleCommand( + // service: "", + // characteristic: "", + // ), + ); + _addLog('isPaired', isPaired); + }, + text: 'isPaired', + ), + PlatformButton( + onPressed: () async { + await bleDevice.unpair(); + }, + text: 'Unpair', + ), + ], + ), + ), + // Services + if (deviceType != DeviceType.desktop) + ServicesListWidget( + discoveredServices: discoveredServices, + onTap: (service, characteristic) { + setState(() { + selectedService = service; + selectedCharacteristic = characteristic; + }); + }, + ), + const Divider(), + ResultWidget( + results: _logs, + onClearTap: (int? index) { + setState(() { + if (index != null) { + _logs.removeAt(index); + } else { + _logs.clear(); + } + }); + }), const SizedBox(height: 20), ], ), ), ), - - // Logs on right side for desktop - if (deviceType == DeviceType.desktop) - Expanded( - flex: 1, - child: _buildResultWidget( - scrollable: true, - ), - ), - ], - ), + ), + ], ); - }, - ); - } - - Widget _buildCharacteristicActions() { - final colorScheme = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - spacing: 12, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Icon( - Icons.tune, - color: colorScheme.primary, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Characteristic Actions', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: colorScheme.onSurface, - ), - ), - ], - ), - Row( - spacing: 12, - children: [ - Expanded( - child: ElevatedButton.icon( - onPressed: isConnected && - _hasSelectedCharacteristicProperty([ - CharacteristicProperty.read, - ]) - ? _readValue - : null, - icon: const Icon(Icons.download), - label: const Text('Read'), - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.secondary, - foregroundColor: colorScheme.onSecondary, - padding: const EdgeInsets.symmetric( - vertical: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - Expanded( - child: ElevatedButton.icon( - onPressed: isConnected && discoveredServices.isNotEmpty - ? _readAllCharacteristics - : null, - icon: const Icon(Icons.download), - label: const Text('Read All'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - vertical: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - ], - ), - Form( - key: valueFormKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 12, - children: [ - Row( - spacing: 12, - children: [ - Expanded( - flex: 3, - child: TextFormField( - controller: binaryCode, - enabled: isConnected && - _hasSelectedCharacteristicProperty([ - CharacteristicProperty.write, - CharacteristicProperty.writeWithoutResponse, - ]), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a value'; - } - try { - hex.decode(binaryCode.text); - return null; - } catch (e) { - return 'Please enter a valid hex value ( without spaces or 0x (e.g. F0BB) )'; - } - }, - decoration: InputDecoration( - hintText: - "Enter Hex values without spaces or 0x (e.g. F0BB)", - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - filled: true, - fillColor: colorScheme.surfaceContainerHighest, - isDense: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - prefixIcon: Icon( - Icons.code, - color: colorScheme.primary, - ), - ), - ), - ), - Expanded( - flex: 1, - child: ElevatedButton.icon( - onPressed: isConnected && - _hasSelectedCharacteristicProperty([ - CharacteristicProperty.write, - CharacteristicProperty - .writeWithoutResponse, - ]) - ? _writeValue - : null, - icon: const Icon(Icons.send), - label: const Text('Write'), - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - padding: const EdgeInsets.symmetric( - vertical: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - ), - ], - ), - ], - ), - ), - Wrap( - spacing: 12, - runSpacing: 12, - children: [ - ElevatedButton.icon( - onPressed: isConnected && - discoveredServices.isNotEmpty && - selectedCharacteristic != null && - _hasSelectedCharacteristicProperty([ - CharacteristicProperty.notify, - CharacteristicProperty.indicate, - ]) - ? _subscribeChar - : null, - icon: const Icon(Icons.notifications_active), - label: const Text('Subscribe'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - OutlinedButton.icon( - onPressed: isConnected && - discoveredServices.isNotEmpty && - selectedCharacteristic != null && - _hasSelectedCharacteristicProperty([ - CharacteristicProperty.notify, - CharacteristicProperty.indicate, - ]) - ? _unsubscribeChar - : null, - icon: const Icon(Icons.notifications_off), - label: const Text('Unsubscribe'), - style: OutlinedButton.styleFrom( - foregroundColor: colorScheme.onSurface, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ElevatedButton.icon( - onPressed: isConnected && discoveredServices.isNotEmpty - ? _subscribeToAllCharacteristics - : null, - icon: const Icon(Icons.notifications_active), - label: const Text('Subscribe All'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ); - } - - Widget _buildConnectDisconnectButton() { - final colorScheme = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // AutoConnect toggle - Card( - elevation: 1, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - color: colorScheme.surfaceContainerHighest, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - child: Row( - children: [ - Icon( - Icons.autorenew, - size: 18, - color: colorScheme.primary, - ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Auto Reconnect', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - ), - Text( - 'Automatically reconnect when device becomes available', - style: TextStyle( - fontSize: 11, - color: colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - ], - ), - ), - Switch( - value: _autoConnect, - onChanged: (value) async { - // If toggling off auto-connect, disconnect to prevent unwanted reconnections - if (!value && _autoConnect) { - if (isConnected) { - // Device is connected, disconnect it to prevent auto-reconnect - await _executeWithLoading( - () async { - await bleDevice.disconnect(); - _addLog("DisconnectResult", - "Disconnected to disable auto-reconnect"); - }, - onError: (error) { - _addLog('DisconnectError', error); - }, - ); - } else { - // Device is already disconnected, but call disconnect() anyway - // to ensure cleanup and prevent any pending auto-reconnection attempts - await _executeWithLoading( - () async { - await bleDevice.disconnect(); - _addLog("DisconnectResult", - "Cleanup performed to prevent auto-reconnect"); - }, - onError: (error) { - _addLog('DisconnectError', error); - }, - ); - } - } - setState(() { - _autoConnect = value; - }); - }, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ], - ), - ), - ), - const SizedBox(height: 12), - // Connect/Disconnect button - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () async { - if (isConnected) { - await _executeWithLoading( - () async { - await bleDevice.disconnect(); - _addLog("DisconnectResult", true); - }, - onError: (error) { - _addLog('DisconnectError', error); - }, - ); - } else { - await _executeWithLoading( - () async { - await bleDevice.connect(autoConnect: _autoConnect); - _addLog( - "ConnectionResult", - "Connected${_autoConnect ? ' (Auto-reconnect enabled)' : ''}", - ); - }, - onError: (error) { - _addLog( - 'ConnectError (${error.runtimeType})', - error, - ); - }, - ); - } - }, - icon: Icon( - isConnected - ? Icons.bluetooth_disabled - : Icons.bluetooth_connected, - size: 20, - ), - label: Text(isConnected ? 'Disconnect' : 'Connect'), - style: ElevatedButton.styleFrom( - backgroundColor: - isConnected ? colorScheme.error : colorScheme.primary, - foregroundColor: - isConnected ? colorScheme.onError : colorScheme.onPrimary, - padding: const EdgeInsets.symmetric( - vertical: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 2, - ), - ), - ), - ], - ), - ); - } - - Widget _buildDeviceInfo() { - final colorScheme = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 2.0, - ), - child: Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Theme( - data: Theme.of(context).copyWith( - dividerColor: Colors.transparent, - ), - child: ExpansionTile( - initiallyExpanded: _isDeviceInfoExpanded, - onExpansionChanged: (expanded) { - setState(() { - _isDeviceInfoExpanded = expanded; - }); - }, - tilePadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - childrenPadding: const EdgeInsets.only( - left: 16, - right: 16, - bottom: 16, - ), - leading: Icon( - Icons.info_outline, - color: colorScheme.primary, - size: 24, - ), - title: Text( - 'Device Information', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - color: colorScheme.onSurface, - ), - ), - subtitle: !_isDeviceInfoExpanded - ? Padding( - padding: const EdgeInsets.only(top: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Name: ${bleDevice.name ?? "Unknown"}', - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - const SizedBox(height: 2), - Text( - 'ID: ${bleDevice.deviceId}', - style: TextStyle( - fontSize: 11, - fontFamily: 'monospace', - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ) - : null, - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildInfoRow( - context, - 'Device ID', - bleDevice.deviceId, - Icons.fingerprint, - ), - const SizedBox(height: 12), - _buildInfoRow( - context, - 'Name', - bleDevice.name ?? "Unknown", - Icons.label, - ), - ], - ), - ), - ), - ); - } - - String _formatAdTime(DateTime dt) { - return '${dt.hour.toString().padLeft(2, '0')}:' - '${dt.minute.toString().padLeft(2, '0')}:' - '${dt.second.toString().padLeft(2, '0')}'; - } - - static String _formatCompanyIdentifiers(BleDevice device) { - if (device.manufacturerDataList.isEmpty) return '—'; - final companyService = CompanyIdentifierService.instance; - return device.manufacturerDataList - .map((d) { - final name = companyService.getCompanyName(d.companyId); - return name != null ? '${d.companyIdRadix16} ($name)' : d.companyIdRadix16; - }) - .join(', '); - } - - static String _formatManufacturerData(BleDevice device) { - if (device.manufacturerDataList.isEmpty) return '—'; - return device.manufacturerDataList - .map((d) => '${d.companyIdRadix16}: ${d.payloadRadix16}') - .join('; '); - } - - static String _formatServiceData(BleDevice device) { - if (device.serviceData.isEmpty) return '—'; - return device.serviceData.entries - .map((e) => - '${e.key}: 0x${e.value.map((b) => b.toRadixString(16).padLeft(2, '0')).join('').toUpperCase()}') - .join('; '); - } - - static String _formatAdvertisedServices(BleDevice device) { - if (device.services.isEmpty) return '—'; - return device.services.join(', '); - } - - Widget _buildAdRow( - String label, - String value, - IconData icon, - TextStyle labelStyle, - TextStyle valueStyle, - ColorScheme colorScheme, - ) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(icon, size: 12, color: colorScheme.onSurface.withValues(alpha: 0.6)), - const SizedBox(width: 4), - SizedBox( - width: 98, - child: Text(label, style: labelStyle), - ), - Expanded( - child: SelectableText( - value, - style: valueStyle, - maxLines: 2, - ), - ), - ], - ); - } - - Future _copyReceivedAdvertisements() async { - if (_receivedAdvertisements.isEmpty) return; - final lines = _receivedAdvertisements.reversed.map((ad) { - final ts = ad.timestampDateTime; - final timeStr = ts != null ? _formatAdTime(ts) : '—'; - final rssiStr = ad.rssi?.toString() ?? '—'; - final companyIdStr = _formatCompanyIdentifiers(ad); - final mfrStr = _formatManufacturerData(ad); - final svcStr = _formatServiceData(ad); - final advSvcStr = _formatAdvertisedServices(ad); - return 'Time: $timeStr RSSI: $rssiStr Company ID: $companyIdStr Manufacturer Data: $mfrStr Service Data: $svcStr Advertised Services: $advSvcStr'; - }).toList(); - await Clipboard.setData(ClipboardData(text: lines.join('\n'))); - if (mounted) { - _showSnackBar('All advertisements copied to clipboard'); - } - } - - Future _copyAdvertisement(BleDevice ad) async { - final ts = ad.timestampDateTime; - final timeStr = ts != null ? _formatAdTime(ts) : '—'; - final rssiStr = ad.rssi?.toString() ?? '—'; - final companyIdStr = _formatCompanyIdentifiers(ad); - final mfrStr = _formatManufacturerData(ad); - final svcStr = _formatServiceData(ad); - final advSvcStr = _formatAdvertisedServices(ad); - final line = - 'Time: $timeStr RSSI: $rssiStr Company ID: $companyIdStr Manufacturer Data: $mfrStr Service Data: $svcStr Advertised Services: $advSvcStr'; - await Clipboard.setData(ClipboardData(text: line)); - if (mounted) { - _showSnackBar('Advertisement copied to clipboard'); - } - } - - Widget _buildReceivedAdvertisements() { - final colorScheme = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 2.0, - ), - child: Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Theme( - data: Theme.of(context).copyWith( - dividerColor: Colors.transparent, - ), - child: ExpansionTile( - initiallyExpanded: _isReceivedAdvertisementsExpanded, - onExpansionChanged: (expanded) { - setState(() { - _isReceivedAdvertisementsExpanded = expanded; - }); - }, - tilePadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - childrenPadding: const EdgeInsets.only( - left: 12, - right: 12, - bottom: 10, - ), - title: ResultHeaderBar( - icon: Icons.ad_units_outlined, - title: 'Received advertisements', - count: _receivedAdvertisements.length, - onCopy: _receivedAdvertisements.isEmpty - ? null - : _copyReceivedAdvertisements, - copyTooltip: 'Copy all advertisements', - onClear: _receivedAdvertisements.isEmpty - ? null - : () { - setState(() { - _receivedAdvertisements.clear(); - }); - }, - clearTooltip: 'Clear all advertisements', - titleFontSize: 18, - ), - trailing: Icon( - _isReceivedAdvertisementsExpanded - ? Icons.expand_less - : Icons.expand_more, - ), - children: [ - _receivedAdvertisements.isEmpty - ? Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Text( - 'No advertisements yet', - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - ), - ) - : ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 240), - child: ListView.separated( - shrinkWrap: true, - itemCount: _receivedAdvertisements.length, - separatorBuilder: (context, _) => Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Divider( - height: 1, - color: Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.2), - ), - ), - itemBuilder: (context, index) { - final ad = _receivedAdvertisements[ - _receivedAdvertisements.length - 1 - index]; - final ts = ad.timestampDateTime; - final timeStr = ts != null - ? _formatAdTime(ts) - : '—'; - final companyIdStr = - _formatCompanyIdentifiers(ad); - final mfrStr = _formatManufacturerData(ad); - final svcStr = _formatServiceData(ad); - final advSvcStr = _formatAdvertisedServices(ad); - final labelStyle = TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - color: colorScheme.onSurface - .withValues(alpha: 0.7), - ); - final valueStyle = TextStyle( - fontSize: 10, - fontFamily: 'monospace', - color: colorScheme.onSurface - .withValues(alpha: 0.85), - ); - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 3, - horizontal: 4, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _buildAdRow( - 'Time', - timeStr, - Icons.schedule, - labelStyle, - valueStyle, - colorScheme, - ), - const SizedBox(height: 2), - _buildAdRow( - 'RSSI', - ad.rssi?.toString() ?? '—', - Icons.signal_cellular_alt, - labelStyle, - valueStyle, - colorScheme, - ), - const SizedBox(height: 2), - _buildAdRow( - 'Company ID', - companyIdStr, - Icons.business, - labelStyle, - valueStyle, - colorScheme, - ), - const SizedBox(height: 2), - _buildAdRow( - 'Adv. Services', - advSvcStr, - Icons.apps, - labelStyle, - valueStyle, - colorScheme, - ), - const SizedBox(height: 2), - _buildAdRow( - 'Mfr Data', - mfrStr, - Icons.memory, - labelStyle, - valueStyle, - colorScheme, - ), - const SizedBox(height: 2), - _buildAdRow( - 'Service Data', - svcStr, - Icons.list, - labelStyle, - valueStyle, - colorScheme, - ), - ], - ), - ), - IconButton( - icon: const Icon(Icons.copy), - iconSize: 18, - padding: const EdgeInsets.all(4), - constraints: const BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - tooltip: 'Copy advertisement', - onPressed: () => _copyAdvertisement(ad), - style: IconButton.styleFrom( - foregroundColor: colorScheme.onSurface - .withValues(alpha: 0.6), - ), - ), - ], - ), - ); - }, - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildDeviceActions() { - final colorScheme = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 2.0, - ), - child: Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Theme( - data: Theme.of(context).copyWith( - dividerColor: Colors.transparent, - ), - child: ExpansionTile( - initiallyExpanded: _isDeviceActionsExpanded, - onExpansionChanged: (expanded) { - setState(() { - _isDeviceActionsExpanded = expanded; - }); - }, - tilePadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - childrenPadding: const EdgeInsets.only( - left: 16, - right: 16, - bottom: 16, - ), - leading: Icon( - Icons.devices, - color: colorScheme.primary, - size: 24, - ), - title: Text( - 'Device Actions', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - color: colorScheme.onSurface, - ), - ), - expandedCrossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: double.infinity, - child: Wrap( - spacing: 6, - runSpacing: 2, - children: [ - ElevatedButton.icon( - onPressed: isConnected - ? () async { - _discoverServices(); - } - : null, - icon: const Icon(Icons.search), - label: const Text('Discover Services'), - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.primary, - foregroundColor: colorScheme.onPrimary, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - OutlinedButton.icon( - onPressed: () async { - _addLog( - 'ConnectionState', - await bleDevice.connectionState, - ); - }, - icon: const Icon(Icons.info_outline), - label: const Text('Connection State'), - style: OutlinedButton.styleFrom( - foregroundColor: colorScheme.onSurface, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - OutlinedButton.icon( - onPressed: isConnected - ? () async { - try { - int rssi = await bleDevice.readRssi(); - _addLog('RSSI', '$rssi dBm'); - } catch (e) { - _addLog('ReadRssiError (${e.runtimeType})', e); - } - } - : null, - icon: const Icon(Icons.signal_cellular_alt), - label: const Text('RSSI'), - style: OutlinedButton.styleFrom( - foregroundColor: colorScheme.onSurface, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - if (BleCapabilities.supportsRequestMtuApi) - ElevatedButton.icon( - onPressed: isConnected - ? () async { - await _executeWithLoading( - () async { - int mtu = await bleDevice.requestMtu(247); - _addLog('MTU', mtu); - }, - onError: (error) { - _addLog('RequestMtuError', error); - }, - ); - } - : null, - icon: const Icon(Icons.speed), - label: const Text('MTU'), - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.secondary, - foregroundColor: colorScheme.onSecondary, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ElevatedButton.icon( - onPressed: BleCapabilities.supportsAllPairingKinds - ? () async { - await _executeWithLoading( - () async { - await bleDevice.pair(); - _addLog("Pairing Result", true); - }, - onError: (error) { - _addLog('PairError', error); - }, - ); - } - : null, - icon: const Icon(Icons.link), - label: const Text('Pair'), - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.tertiary, - foregroundColor: colorScheme.onTertiary, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - OutlinedButton.icon( - onPressed: () async { - await _executeWithLoading( - () async { - bool? isPaired = await bleDevice.isPaired(); - _addLog('isPaired', isPaired); - }, - onError: (error) { - _addLog('isPairedError', error); - }, - ); - }, - icon: const Icon(Icons.check_circle), - label: const Text('Pairing State'), - style: OutlinedButton.styleFrom( - foregroundColor: colorScheme.onSurface, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - if (BleCapabilities.hasSystemPairingApi) - OutlinedButton.icon( - onPressed: () async { - await _executeWithLoading( - () async { - await bleDevice.unpair(); - }, - onError: (error) { - _addLog('UnpairError', error); - }, - ); - }, - icon: const Icon(Icons.link_off), - label: const Text('Unpair'), - style: OutlinedButton.styleFrom( - foregroundColor: colorScheme.error, - side: BorderSide( - color: colorScheme.error, - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ], - ), - ), - ], - ), - ), - ), + }), ); } bool _hasSelectedCharacteristicProperty( - List properties, - ) { - return properties.any( - (property) => - selectedCharacteristic?.properties.contains(property) ?? false, - ); - } - - Widget _buildInfoRow( - BuildContext context, - String label, - String value, - IconData icon, - ) { - final colorScheme = Theme.of(context).colorScheme; - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - icon, - size: 18, - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurface.withValues(alpha: 0.6), - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 2), - SelectableText( - value, - style: TextStyle( - fontSize: 14, - color: colorScheme.onSurface, - fontFamily: label == 'Device ID' ? 'monospace' : null, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ], - ); - } - - Widget _buildCharacteristicSelector() { - final colorScheme = Theme.of(context).colorScheme; - if (selectedCharacteristic == null) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: InkWell( - onTap: () { - if (discoveredServices.isEmpty) { - _discoverServices(); - } else { - _showCharacteristicSelector(); - } - }, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - Icon( - Icons.info_outline, - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - discoveredServices.isEmpty - ? "Please discover services" - : "Please select a characteristic to read/write", - style: TextStyle( - fontStyle: FontStyle.italic, - color: colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - ), - ], - ), - ), - ), - ); - } else { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: _buildSelectedCharacteristicCard(), - ); - } - } - - Widget _buildServicesList({ - Function(BleService, BleCharacteristic?)? onSelect, - Set? propertyFilters, - GlobalKey? listKey, - bool isDiscoveringServices = false, - }) { - return ServicesListWidget( - key: listKey, - discoveredServices: discoveredServices, - selectedService: selectedService, - selectedCharacteristic: selectedCharacteristic, - favoriteServices: _favoriteServices, - subscribedCharacteristics: _subscribedCharacteristics, - scrollable: true, - propertyFilters: propertyFilters, - isDiscoveringServices: isDiscoveringServices, - onTap: (service, characteristic) { - setState(() { - selectedService = service; - selectedCharacteristic = characteristic; - }); - onSelect?.call(service, characteristic); - }, - onFavoriteToggle: (serviceUuid) { - setState(() { - if (_favoriteServices.contains( - serviceUuid, - )) { - _favoriteServices.remove(serviceUuid); - } else { - _favoriteServices.add(serviceUuid); - } - }); - _saveFavoriteServices(); - }, - ); - } - - /// Returns a list of all filtered characteristics with their parent services - List<({BleService service, BleCharacteristic characteristic})> - _getFilteredCharacteristics() { - return getFilteredBleCharacteristics( - discoveredServices, - favoriteServices: _favoriteServices, - propertyFilters: _currentPropertyFilters, - ); - } - - void _navigateToPreviousCharacteristic() { - if (selectedCharacteristic == null) return; - - final filtered = _getFilteredCharacteristics(); - final result = navigateToAdjacentCharacteristic( - filtered, - selectedCharacteristic!.uuid, - false, // previous - ); - - if (result != null) { - setState(() { - selectedService = result.service; - selectedCharacteristic = result.characteristic; - }); - } - } - - void _navigateToNextCharacteristic() { - if (selectedCharacteristic == null) return; - - final filtered = _getFilteredCharacteristics(); - final result = navigateToAdjacentCharacteristic( - filtered, - selectedCharacteristic!.uuid, - true, // next - ); - - if (result != null) { - setState(() { - selectedService = result.service; - selectedCharacteristic = result.characteristic; - }); - } - } - - bool _canNavigateCharacteristics() { - final filtered = _getFilteredCharacteristics(); - return filtered.length > 1; - } - - Widget _buildSelectedCharacteristicCard() { - final colorScheme = Theme.of(context).colorScheme; - if (selectedCharacteristic == null) return const SizedBox.shrink(); - final canNavigate = _canNavigateCharacteristics(); - return Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - color: colorScheme.primaryContainer, - child: InkWell( - onTap: _showCharacteristicSelector, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.apps, - size: 16, - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - const SizedBox(width: 6), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Service', - style: TextStyle( - fontSize: 11, - color: colorScheme.onSurface.withValues(alpha: 0.6), - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 2), - InkWell( - onTap: () { - Clipboard.setData(ClipboardData( - text: selectedService?.uuid ?? "", - )); - _showSnackBar('Copied to clipboard'); - }, - child: Text( - selectedService?.uuid ?? "Unknown", - style: TextStyle( - fontSize: 12, - fontFamily: 'monospace', - color: colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 8), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.settings, - size: 16, - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - const SizedBox(width: 6), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Characteristic', - style: TextStyle( - fontSize: 11, - color: colorScheme.onSurface.withValues(alpha: 0.6), - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 2), - Row( - children: [ - Expanded( - child: InkWell( - onTap: () { - Clipboard.setData(ClipboardData( - text: selectedCharacteristic?.uuid ?? "", - )); - _showSnackBar('Copied to clipboard'); - }, - child: Text( - selectedCharacteristic!.uuid, - style: TextStyle( - fontSize: 12, - fontFamily: 'monospace', - color: colorScheme.onSurface, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ], - ), - ], - ), - ), - if (canNavigate) ...[ - IconButton( - onPressed: _navigateToPreviousCharacteristic, - icon: const Icon(Icons.arrow_back_ios), - iconSize: 16, - tooltip: 'Previous Characteristic', - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 28, - minHeight: 28, - ), - color: colorScheme.primary, - ), - IconButton( - onPressed: _navigateToNextCharacteristic, - icon: const Icon(Icons.arrow_forward_ios), - iconSize: 16, - tooltip: 'Next Characteristic', - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 28, - minHeight: 28, - ), - color: colorScheme.primary, - ), - const SizedBox(width: 4), - ], - Icon( - Icons.arrow_drop_down, - size: 18, - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - ], - ), - const SizedBox(height: 8), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.tune, - size: 16, - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - const SizedBox(width: 6), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Properties', - style: TextStyle( - fontSize: 11, - color: colorScheme.onSurface.withValues(alpha: 0.6), - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 4), - Wrap( - spacing: 4, - runSpacing: 4, - children: - selectedCharacteristic!.properties.map((prop) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - prop.name, - style: TextStyle( - fontSize: 10, - color: colorScheme.onSecondaryContainer, - fontWeight: FontWeight.w600, - ), - ), - ); - }).toList(), - ), - ], - ), - ), - ], - ), - ], - ), - ), - ), - ); - } - - Widget _buildResultWidget({ - required bool scrollable, - }) { - return ResultWidget( - results: _logs, - scrollController: _logsScrollController, - scrollable: scrollable, - onCopyTap: () async { - if (_logs.isEmpty) return; - final logsText = _logs.join('\n'); - await Clipboard.setData(ClipboardData(text: logsText)); - if (context.mounted) { - _showSnackBar('All logs copied to clipboard'); - } - }, - onClearTap: (int? index) { - setState(() { - if (index != null) { - _logs.removeAt(index); - } else { - _logs.clear(); - } - }); - }, - ); + List properties) { + return properties.any((property) => + selectedCharacteristic?.properties.contains(property) ?? false); } } diff --git a/example/lib/peripheral_details/widgets/result_header_bar.dart b/example/lib/peripheral_details/widgets/result_header_bar.dart deleted file mode 100644 index afd22a2..0000000 --- a/example/lib/peripheral_details/widgets/result_header_bar.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Shared header row for result-style cards (e.g. Logs, Received advertisements). -/// Layout: [icon] [title] [copy?] [Spacer] [count?] [clear?] -/// Copy, count badge, and clear button are shown only when [count] > 0. -class ResultHeaderBar extends StatelessWidget { - const ResultHeaderBar({ - super.key, - required this.icon, - required this.title, - required this.count, - this.onCopy, - this.copyTooltip = 'Copy', - this.onClear, - this.clearTooltip = 'Clear all', - this.titleFontSize = 16, - }); - - final IconData icon; - final String title; - final int count; - final VoidCallback? onCopy; - final String copyTooltip; - final VoidCallback? onClear; - final String clearTooltip; - final double titleFontSize; - - bool get _hasItems => count > 0; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Row( - children: [ - Icon( - icon, - color: colorScheme.primary, - size: 20, - ), - const SizedBox(width: 8), - Text( - title, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: titleFontSize, - color: colorScheme.onSurface, - ), - ), - if (_hasItems && onCopy != null) ...[ - const SizedBox(width: 8), - IconButton( - icon: Icon( - Icons.copy, - color: colorScheme.primary, - size: 20, - ), - onPressed: onCopy, - tooltip: copyTooltip, - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - ), - ], - const Spacer(), - if (_hasItems) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - '$count', - style: TextStyle( - color: colorScheme.onPrimaryContainer, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ), - if (_hasItems && onClear != null) ...[ - const SizedBox(width: 8), - IconButton( - icon: Icon( - Icons.close, - color: colorScheme.error, - size: 20, - ), - onPressed: onClear, - tooltip: clearTooltip, - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - ), - ], - ], - ); - } -} diff --git a/example/lib/peripheral_details/widgets/result_widget.dart b/example/lib/peripheral_details/widgets/result_widget.dart index 8cbb135..0a43046 100644 --- a/example/lib/peripheral_details/widgets/result_widget.dart +++ b/example/lib/peripheral_details/widgets/result_widget.dart @@ -1,155 +1,50 @@ import 'package:flutter/material.dart'; -import 'package:universal_ble_example/peripheral_details/widgets/result_header_bar.dart'; - class ResultWidget extends StatelessWidget { final List results; final bool scrollable; - final ScrollController scrollController; final void Function(int? index) onClearTap; - final void Function()? onCopyTap; const ResultWidget({ required this.results, required this.onClearTap, - this.onCopyTap, this.scrollable = false, - required this.scrollController, super.key, }); @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.all(16.0), - child: Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: scrollable ? MainAxisSize.max : MainAxisSize.min, - children: [ - _buildHeader(colorScheme), - if (results.isEmpty) - _buildEmptyState(colorScheme) - else - _buildLogsList(colorScheme), - ], - ), - ), - ); - } - - Widget _buildHeader(ColorScheme colorScheme) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - ), - ), - child: ResultHeaderBar( - icon: Icons.history, - title: 'Logs', - count: results.length, - onCopy: onCopyTap, - copyTooltip: 'Copy all logs', - onClear: () => onClearTap(null), - clearTooltip: 'Clear all logs', - ), - ); - } - - Widget _buildEmptyState(ColorScheme colorScheme) { - final emptyStateContent = Padding( - padding: const EdgeInsets.all(32.0), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.history_outlined, - size: 48, - color: colorScheme.onSurface.withValues(alpha: 0.3), - ), - const SizedBox(height: 16), - Text( - 'No logs yet', - style: TextStyle( - color: colorScheme.onSurface.withValues(alpha: 0.6), - ), - ), - ], - ), - ), - ); - - return scrollable ? Expanded(child: emptyStateContent) : emptyStateContent; - } - - Widget _buildLogsList(ColorScheme colorScheme) { - final listView = ListView.separated( - physics: const AlwaysScrollableScrollPhysics(), - controller: scrollController, - itemCount: results.length, - itemBuilder: (context, index) => _buildLogItem(colorScheme, index), - separatorBuilder: (_, __) => Divider( - height: 1, - color: colorScheme.outline.withValues(alpha: 0.2), - ), - ); - - if (scrollable) { - return Expanded(child: listView); - } else { - return Container( - constraints: const BoxConstraints(maxHeight: 300), - child: listView, - ); - } - } - - Widget _buildLogItem(ColorScheme colorScheme, int index) { - var reversedIndex = results.length - index - 1; - final log = results[reversedIndex]; - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: SelectableText( - log, - style: TextStyle( - fontSize: 12, - fontFamily: 'monospace', - color: colorScheme.onSurface, - ), + return Column( + children: [ + if (results.isNotEmpty) + Padding( + padding: const EdgeInsets.all(8.0), + child: ListTile( + tileColor: Theme.of(context).secondaryHeaderColor, + title: const Text("Logs"), + onTap: () { + onClearTap(null); + }, + trailing: const Icon(Icons.clear), ), ), - const SizedBox(width: 8), - IconButton( - icon: Icon( - Icons.close, - size: 16, - color: colorScheme.onSurface.withValues(alpha: 0.4), - ), - onPressed: () => onClearTap(reversedIndex), - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 24, - minHeight: 24, - ), - ), - ], - ), + ListView.separated( + shrinkWrap: !scrollable, + physics: scrollable ? null : const NeverScrollableScrollPhysics(), + itemCount: results.length, + reverse: true, + itemBuilder: (BuildContext context, int index) { + return InkWell( + onTap: () => onClearTap(index), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 11, vertical: 2), + child: Text(results[index]), + ), + ); + }, + separatorBuilder: (_, __) => const Divider(), + ), + ], ); } } diff --git a/example/lib/peripheral_details/widgets/services_list_widget.dart b/example/lib/peripheral_details/widgets/services_list_widget.dart index 3d1dd33..15ee23b 100644 --- a/example/lib/peripheral_details/widgets/services_list_widget.dart +++ b/example/lib/peripheral_details/widgets/services_list_widget.dart @@ -1,448 +1,78 @@ +import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:universal_ble/universal_ble.dart'; -import 'package:universal_ble_example/data/utils.dart'; -class ServicesListWidget extends StatefulWidget { +class ServicesListWidget extends StatelessWidget { final List discoveredServices; final bool scrollable; - final void Function(BleService service, BleCharacteristic characteristic)? - onTap; - final BleService? selectedService; - final BleCharacteristic? selectedCharacteristic; - final Set? favoriteServices; - final Map? subscribedCharacteristics; - final void Function(String serviceUuid)? onFavoriteToggle; - final Set? propertyFilters; - final bool isDiscoveringServices; + final void Function( + BleService service, + BleCharacteristic characteristic, + )? onTap; const ServicesListWidget({ super.key, required this.discoveredServices, this.onTap, this.scrollable = false, - this.selectedService, - this.selectedCharacteristic, - this.favoriteServices, - this.subscribedCharacteristics, - this.onFavoriteToggle, - this.propertyFilters, - this.isDiscoveringServices = false, }); - @override - State createState() => ServicesListWidgetState(); -} - -class ServicesListWidgetState extends State { - final Map _expandableControllers = {}; - ScrollController? _scrollController; - final Map _characteristicKeys = {}; - - @override - void initState() { - super.initState(); - if (widget.scrollable) { - _scrollController = ScrollController(); - } - _initializeControllers(); - // Scroll to selected characteristic after the first frame - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToSelectedCharacteristic(); - }); - } - - @override - void didUpdateWidget(ServicesListWidget oldWidget) { - super.didUpdateWidget(oldWidget); - // Create or dispose scroll controller if scrollable state changed - if (oldWidget.scrollable != widget.scrollable) { - if (widget.scrollable && _scrollController == null) { - _scrollController = ScrollController(); - } else if (!widget.scrollable && _scrollController != null) { - _scrollController!.dispose(); - _scrollController = null; - } - } - // Update controllers if services or selection changed - if (oldWidget.discoveredServices != widget.discoveredServices || - oldWidget.selectedCharacteristic != widget.selectedCharacteristic) { - _initializeControllers(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToSelectedCharacteristic(); - }); - } - } - - @override - void dispose() { - for (var controller in _expandableControllers.values) { - controller.dispose(); - } - _scrollController?.dispose(); - super.dispose(); - } - - void _initializeControllers() { - // Dispose old controllers - for (var controller in _expandableControllers.values) { - controller.dispose(); - } - _expandableControllers.clear(); - _characteristicKeys.clear(); - - // Create controllers for each service - final sortedServices = _getSortedServices(); - for (var service in sortedServices) { - final controller = ExpansibleController(); - // Expand if this service contains the selected characteristic - if (widget.selectedCharacteristic != null) { - final hasSelectedChar = service.characteristics.any( - (char) => char.uuid == widget.selectedCharacteristic!.uuid, - ); - if (hasSelectedChar) { - controller.expand(); - } - } - _expandableControllers[service.uuid] = controller; - - // Create keys for characteristics - for (var char in service.characteristics) { - _characteristicKeys['${service.uuid}_${char.uuid}'] = GlobalKey(); - } - } - } - - void _scrollToSelectedCharacteristic() { - if (widget.selectedCharacteristic == null) return; - - // Find the key for the selected characteristic - String? selectedKey; - - // Prefer an exact match on both service and characteristic UUIDs - if (widget.selectedService != null) { - final exactKey = - '${widget.selectedService!.uuid}_${widget.selectedCharacteristic!.uuid}'; - if (_characteristicKeys.containsKey(exactKey)) { - selectedKey = exactKey; - } - } - - // Fallback: match by characteristic UUID only if no exact key was found - if (selectedKey == null) { - for (var entry in _characteristicKeys.entries) { - if (entry.key.endsWith('_${widget.selectedCharacteristic!.uuid}')) { - selectedKey = entry.key; - break; - } - } - } - if (selectedKey != null) { - final key = _characteristicKeys[selectedKey]; - if (key?.currentContext != null) { - // Wait a bit for the expansion animation to complete - Future.delayed(const Duration(milliseconds: 100), () { - if (mounted && key?.currentContext != null) { - Scrollable.ensureVisible( - key!.currentContext!, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - }); - } - } - } - - List _getSortedServices() { - return sortBleServices( - widget.discoveredServices, - favoriteServices: widget.favoriteServices, - ); - } - - /// Returns a list of all filtered characteristics with their parent services - List<({BleService service, BleCharacteristic characteristic})> - getFilteredCharacteristics() { - return getFilteredBleCharacteristics( - widget.discoveredServices, - favoriteServices: widget.favoriteServices, - propertyFilters: widget.propertyFilters, - ); - } - @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final favoriteStarColor = Colors.amber; - final subscribedNotificationIconColor = Colors.green; - final selectedColor = colorScheme.primary; - final selectedCharacteristicBackgroundColor = - colorScheme.primaryContainer.withValues(alpha: 0.5); - - final sortedServices = _getSortedServices(); - - // Show loading indicator when discovering services and list is empty - if (widget.isDiscoveringServices && sortedServices.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator( - color: colorScheme.primary, - ), - const SizedBox(height: 16), - Text( - 'Discovering services...', - style: TextStyle( - color: colorScheme.onSurface.withValues(alpha: 0.6), - fontSize: 14, - ), - ), - ], - ), - ); - } - - // Show loading overlay when re-discovering services (list has items) - if (widget.isDiscoveringServices && sortedServices.isNotEmpty) { - return Stack( - children: [ - _buildServicesListView( - sortedServices, - colorScheme, - favoriteStarColor, - subscribedNotificationIconColor, - selectedColor, - selectedCharacteristicBackgroundColor), - Positioned.fill( - child: Container( - color: colorScheme.surface.withValues(alpha: 0.7), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator( - color: colorScheme.primary, - ), - const SizedBox(height: 16), - Text( - 'Discovering services...', - style: TextStyle( - color: colorScheme.onSurface.withValues(alpha: 0.8), - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ), - ), - ], - ); - } - - // Normal list view when not loading - return _buildServicesListView( - sortedServices, - colorScheme, - favoriteStarColor, - subscribedNotificationIconColor, - selectedColor, - selectedCharacteristicBackgroundColor); - } - - Widget _buildServicesListView( - List sortedServices, - ColorScheme colorScheme, - Color favoriteStarColor, - Color subscribedNotificationIconColor, - Color selectedColor, - Color selectedCharacteristicBackgroundColor, - ) { return ListView.builder( - controller: _scrollController, - shrinkWrap: !widget.scrollable, - physics: widget.scrollable ? null : const NeverScrollableScrollPhysics(), - itemCount: sortedServices.length, + shrinkWrap: !scrollable, + physics: scrollable ? null : const NeverScrollableScrollPhysics(), + itemCount: discoveredServices.length, itemBuilder: (BuildContext context, int index) { - final service = sortedServices[index]; - final isFavorite = - widget.favoriteServices?.contains(service.uuid) ?? false; - final isSelected = widget.selectedService?.uuid == service.uuid; - final controller = - _expandableControllers[service.uuid] ?? ExpansibleController(); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + padding: const EdgeInsets.all(8.0), child: Card( - elevation: isSelected ? 2 : 1, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: isSelected - ? BorderSide(color: selectedColor, width: 2) - : BorderSide.none, - ), - child: Theme( - data: Theme.of(context).copyWith( - splashFactory: NoSplash.splashFactory, - highlightColor: Colors.transparent, - hoverColor: Colors.transparent, - dividerColor: Colors.transparent, - ), - child: ExpansionTile( - controller: controller, - tilePadding: - const EdgeInsets.symmetric(horizontal: 12.0, vertical: 0), - shape: const Border(), - collapsedShape: const Border(), - title: Row( + child: ExpandablePanel( + header: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( children: [ - Expanded( - child: Text( - service.uuid, - style: TextStyle( - fontSize: 13, - fontFamily: 'monospace', - color: colorScheme.onSurface, - fontWeight: - isSelected ? FontWeight.bold : FontWeight.w500, - ), - ), - ), - if (widget.onFavoriteToggle != null) ...[ - const SizedBox(width: 8), - IconButton( - icon: Icon( - isFavorite ? Icons.star : Icons.star_border, - color: isFavorite - ? favoriteStarColor - : colorScheme.onSurface.withValues(alpha: 0.4), - ), - onPressed: () => widget.onFavoriteToggle!(service.uuid), - iconSize: 20, - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - hoverColor: Colors.transparent, - ), - ], + const Icon(Icons.arrow_forward_ios), + Expanded(child: Text(discoveredServices[index].uuid)), ], ), - childrenPadding: const EdgeInsets.only(bottom: 8.0), - children: service.characteristics.where((e) { - // Filter by properties if filters are selected - if (widget.propertyFilters != null && - widget.propertyFilters!.isNotEmpty) { - return e.properties - .any((prop) => widget.propertyFilters!.contains(prop)); - } - return true; - }).map((e) { - final isCharSelected = - widget.selectedCharacteristic?.uuid == e.uuid; - final isSubscribed = - widget.subscribedCharacteristics?[e.uuid] ?? false; - final charKey = - _characteristicKeys['${service.uuid}_${e.uuid}']; - return Padding( - key: charKey, - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 4.0, - ), - child: GestureDetector( - onTap: () { - widget.onTap?.call(service, e); - }, - behavior: HitTestBehavior.opaque, - child: Container( - decoration: BoxDecoration( - color: isCharSelected - ? selectedCharacteristicBackgroundColor - : colorScheme.surfaceContainerHighest - .withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(8), - border: isCharSelected - ? Border.all(color: selectedColor, width: 1.5) - : null, - ), - child: Padding( - padding: const EdgeInsets.all(12.0), + ), + collapsed: const SizedBox(), + expanded: Column( + children: discoveredServices[index] + .characteristics + .map((e) => Padding( + padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Icon( - Icons.arrow_right_outlined, - size: 16, - color: isCharSelected - ? selectedColor - : colorScheme.onSurface - .withValues(alpha: 0.6), - ), - const SizedBox(width: 8), - if (isSubscribed) ...[ - Icon( - Icons.notifications_active, - color: subscribedNotificationIconColor, - size: 16, + InkWell( + onTap: () { + onTap?.call(discoveredServices[index], e); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.arrow_right_outlined), + Expanded(child: Text(e.uuid)), + ], ), - const SizedBox(width: 4), + Text( + "Properties: ${e.properties.map((e) => e.name)}", + ), + if (e.descriptors.isNotEmpty) + Text( + "Descriptors: ${e.descriptors.map((e) => e.uuid).join(', ')}", + ) ], - Expanded( - child: Text( - e.uuid, - style: TextStyle( - fontSize: 12, - fontFamily: 'monospace', - fontWeight: isCharSelected - ? FontWeight.bold - : FontWeight.w500, - color: isCharSelected - ? selectedColor - : colorScheme.onSurface, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - Wrap( - spacing: 6, - runSpacing: 6, - children: e.properties.map((prop) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - prop.name, - style: TextStyle( - fontSize: 10, - color: colorScheme.onSecondaryContainer, - fontWeight: FontWeight.w600, - ), - ), - ); - }).toList(), + ), ), ], ), - ), - ), - ), - ); - }).toList(), + )) + .toList(), ), ), ), diff --git a/example/lib/peripheral_details/widgets/services_side_widget.dart b/example/lib/peripheral_details/widgets/services_side_widget.dart deleted file mode 100644 index 5df1063..0000000 --- a/example/lib/peripheral_details/widgets/services_side_widget.dart +++ /dev/null @@ -1,394 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:universal_ble/universal_ble.dart'; -import 'package:universal_ble_example/data/utils.dart'; -import 'services_list_widget.dart'; - -class ServicesSideWidget extends StatefulWidget { - final List discoveredServices; - final Function(Set? selectedProperties, - GlobalKey? listKey, bool isDiscoveringServices) - serviceListBuilder; - final VoidCallback? onCopyServices; - final BleService? selectedService; - final BleCharacteristic? selectedCharacteristic; - final Function(BleService service, BleCharacteristic characteristic)? - onCharacteristicSelected; - final Function(Set? propertyFilters)? - onPropertyFiltersChanged; - final Set? initialPropertyFilters; - final bool isDiscoveringServices; - const ServicesSideWidget({ - super.key, - required this.discoveredServices, - required this.serviceListBuilder, - this.onCopyServices, - this.selectedService, - this.selectedCharacteristic, - this.onCharacteristicSelected, - this.onPropertyFiltersChanged, - this.initialPropertyFilters, - this.isDiscoveringServices = false, - }); - - @override - State createState() => _ServicesSideWidgetState(); -} - -class _ServicesSideWidgetState extends State { - late Set? _selectedProperties; - bool _showFilters = false; - final GlobalKey _servicesListKey = GlobalKey(); - - @override - void initState() { - super.initState(); - _selectedProperties = widget.initialPropertyFilters != null - ? Set.from(widget.initialPropertyFilters!) - : null; - } - - @override - void didUpdateWidget(ServicesSideWidget oldWidget) { - super.didUpdateWidget(oldWidget); - // Update filters if initial filters changed from outside - final newFilters = widget.initialPropertyFilters; - final oldFilters = oldWidget.initialPropertyFilters; - if (newFilters != oldFilters) { - setState(() { - _selectedProperties = newFilters != null - ? Set.from(newFilters) - : null; - }); - } - } - - void _togglePropertyFilter(CharacteristicProperty property) { - setState(() { - _selectedProperties ??= {}; - if (_selectedProperties!.contains(property)) { - _selectedProperties!.remove(property); - if (_selectedProperties!.isEmpty) { - _selectedProperties = null; - } - } else { - _selectedProperties!.add(property); - } - }); - widget.onPropertyFiltersChanged?.call(_selectedProperties); - } - - void _clearFilters() { - setState(() { - _selectedProperties = null; - }); - widget.onPropertyFiltersChanged?.call(null); - } - - void _navigateToAdjacent(bool forward) { - final listState = _servicesListKey.currentState; - if (listState == null || widget.selectedCharacteristic == null) return; - - final filtered = listState.getFilteredCharacteristics(); - final result = navigateToAdjacentCharacteristic( - filtered, - widget.selectedCharacteristic!.uuid, - forward, - ); - - if (result != null) { - widget.onCharacteristicSelected?.call( - result.service, - result.characteristic, - ); - } - } - - void _navigateToPrevious() { - _navigateToAdjacent(false); - } - - void _navigateToNext() { - _navigateToAdjacent(true); - } - - bool _canNavigate() { - final listState = _servicesListKey.currentState; - if (listState == null) return false; - final filtered = listState.getFilteredCharacteristics(); - return filtered.length > 1; - } - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final hasActiveFilters = - _selectedProperties != null && _selectedProperties!.isNotEmpty; - return Container( - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - border: Border( - right: BorderSide( - color: colorScheme.outline.withValues(alpha: 0.2), - width: 1, - ), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: colorScheme.surface, - border: Border( - bottom: BorderSide( - color: colorScheme.outline.withValues(alpha: 0.2), - width: 1, - ), - ), - ), - child: Column( - children: [ - Row( - children: [ - Icon( - Icons.apps, - color: colorScheme.primary, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Services', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: colorScheme.onSurface, - ), - ), - const Spacer(), - // Navigation buttons - if (_canNavigate()) ...[ - IconButton( - onPressed: _navigateToPrevious, - icon: const Icon(Icons.arrow_back_ios), - iconSize: 18, - tooltip: 'Previous Characteristic', - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - color: colorScheme.primary, - ), - IconButton( - onPressed: _navigateToNext, - icon: const Icon(Icons.arrow_forward_ios), - iconSize: 18, - tooltip: 'Next Characteristic', - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - color: colorScheme.primary, - ), - const SizedBox(width: 4), - ], - IconButton( - onPressed: () { - setState(() { - _showFilters = !_showFilters; - }); - }, - icon: Icon( - Icons.filter_list, - color: hasActiveFilters - ? colorScheme.primary - : colorScheme.onSurface, - ), - iconSize: 18, - tooltip: 'Filter by Properties', - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - ), - if (widget.onCopyServices != null) - Padding( - padding: const EdgeInsets.only(right: 8), - child: IconButton( - onPressed: widget.onCopyServices, - icon: const Icon(Icons.copy), - iconSize: 18, - tooltip: 'Copy Services', - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: 32, - minHeight: 32, - ), - color: colorScheme.onSurface, - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - '${widget.discoveredServices.length}', - style: TextStyle( - color: colorScheme.onPrimaryContainer, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - if (_showFilters) ...[ - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: colorScheme.outline.withValues(alpha: 0.2), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - 'Filter by Properties', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - ), - const Spacer(), - if (hasActiveFilters) - TextButton( - onPressed: _clearFilters, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - minimumSize: Size.zero, - tapTargetSize: - MaterialTapTargetSize.shrinkWrap, - ), - child: Text( - 'Clear', - style: TextStyle( - fontSize: 11, - color: colorScheme.primary, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - Wrap( - spacing: 6, - runSpacing: 6, - children: - CharacteristicProperty.values.map((property) { - final isSelected = - _selectedProperties?.contains(property) ?? - false; - return FilterChip( - label: Text( - property.name, - style: TextStyle( - fontSize: 11, - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.normal, - ), - ), - selected: isSelected, - onSelected: (_) => - _togglePropertyFilter(property), - selectedColor: colorScheme.primaryContainer, - checkmarkColor: colorScheme.onPrimaryContainer, - labelStyle: TextStyle( - color: isSelected - ? colorScheme.onPrimaryContainer - : colorScheme.onSurface, - ), - side: BorderSide( - color: isSelected - ? colorScheme.primary - : colorScheme.outline - .withValues(alpha: 0.3), - width: isSelected ? 1.5 : 1, - ), - ); - }).toList(), - ), - ], - ), - ), - ], - ], - ), - ), - Expanded( - child: widget.discoveredServices.isEmpty - ? Center( - child: widget.isDiscoveringServices - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator( - color: colorScheme.primary, - ), - const SizedBox(height: 16), - Text( - 'Discovering services...', - style: TextStyle( - color: colorScheme.onSurface - .withValues(alpha: 0.6), - fontSize: 14, - ), - ), - ], - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.apps_outlined, - size: 48, - color: colorScheme.onSurface - .withValues(alpha: 0.3), - ), - const SizedBox(height: 16), - Text( - 'No Services Discovered', - style: TextStyle( - color: colorScheme.onSurface - .withValues(alpha: 0.6), - ), - ), - ], - ), - ) - : widget.serviceListBuilder( - _selectedProperties, _servicesListKey, widget.isDiscoveringServices), - ), - ], - ), - ); - } -} diff --git a/example/lib/universal_ble_app.dart b/example/lib/universal_ble_app.dart deleted file mode 100644 index b84f86f..0000000 --- a/example/lib/universal_ble_app.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:universal_ble/universal_ble.dart'; -import 'package:universal_ble_example/data/scan_controller.dart'; -import 'package:universal_ble_example/data/storage_service.dart'; -import 'package:universal_ble_example/home/permission_screen.dart'; -import 'package:universal_ble_example/home/scanner_screen.dart'; -import 'package:universal_ble_example/widgets/scan_controller_scope.dart'; - -/// Initializes the app services and checks permissions. -/// Returns whether the app has the required permissions. -Future initializeApp() async { - WidgetsFlutterBinding.ensureInitialized(); - await StorageService.instance.init(); - await UniversalBle.setLogLevel(BleLogLevel.verbose); - return await UniversalBle.hasPermissions( - withAndroidFineLocation: false, - ); -} - -class UniversalBleApp extends StatelessWidget { - final bool hasPermission; - final ScanController scanController; - final Locale? locale; - final Widget Function(BuildContext, Widget?)? builder; - - const UniversalBleApp({ - super.key, - required this.hasPermission, - required this.scanController, - this.locale, - this.builder, - }); - - @override - Widget build(BuildContext context) { - return ScanControllerScope( - controller: scanController, - child: MaterialApp( - title: 'Universal BLE', - debugShowCheckedModeBanner: false, - locale: locale, - builder: builder, - theme: ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.blue, - brightness: Brightness.light, - ), - ), - darkTheme: ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.blue, - brightness: Brightness.dark, - ), - ), - themeMode: ThemeMode.system, - home: hasPermission ? ScannerScreen() : const PermissionScreen(), - ), - ); - } -} diff --git a/example/lib/widgets/company_info_widget.dart b/example/lib/widgets/company_info_widget.dart deleted file mode 100644 index a55d9eb..0000000 --- a/example/lib/widgets/company_info_widget.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:universal_ble_example/data/company_identifier_service.dart'; - -/// A reusable widget that displays company information for a given company ID. -/// -/// This widget fetches the company name from the CompanyIdentifierService -/// and displays it in a consistent format. If no company name is found, -/// the widget returns an empty SizedBox. -class CompanyInfoWidget extends StatelessWidget { - /// The company ID to look up - final int companyId; - - /// Optional text style for the "Company:" label - final TextStyle? labelStyle; - - /// Optional text style for the company name - final TextStyle? nameStyle; - - /// Optional padding around the widget - final EdgeInsets? padding; - - /// Optional color scheme. If not provided, will be obtained from Theme - final ColorScheme? colorScheme; - - const CompanyInfoWidget({ - super.key, - required this.companyId, - this.labelStyle, - this.nameStyle, - this.padding, - this.colorScheme, - }); - - @override - Widget build(BuildContext context) { - final companyName = - CompanyIdentifierService.instance.getCompanyName(companyId); - - if (companyName == null) { - return const SizedBox.shrink(); - } - - final effectiveColorScheme = colorScheme ?? Theme.of(context).colorScheme; - final effectiveLabelStyle = labelStyle ?? - TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: effectiveColorScheme.onSecondaryContainer, - ); - final effectiveNameStyle = nameStyle ?? - TextStyle( - fontSize: 11, - fontWeight: FontWeight.w500, - color: effectiveColorScheme.onSecondaryContainer, - ); - final effectivePadding = padding ?? const EdgeInsets.only(top: 4); - - return Padding( - padding: effectivePadding, - child: Row( - children: [ - Text( - 'Company: ', - style: effectiveLabelStyle, - ), - Expanded( - child: SelectableText( - companyName, - style: effectiveNameStyle, - ), - ), - ], - ), - ); - } -} diff --git a/example/lib/widgets/flash_on_update_widget.dart b/example/lib/widgets/flash_on_update_widget.dart deleted file mode 100644 index a5b173f..0000000 --- a/example/lib/widgets/flash_on_update_widget.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Wraps [child] and briefly shows an outline when [trigger] changes. -/// Draws only a stroked border that fades out smoothly; does not block interaction. -class FlashOnUpdateWidget extends StatefulWidget { - const FlashOnUpdateWidget({ - super.key, - required this.trigger, - required this.child, - this.highlightColor, - this.borderRadius = 16, - }); - - /// When this value changes, the outline animation runs (or restarts). - final Object trigger; - - final Widget child; - - /// If null, [Theme.colorScheme.primaryContainer] is used for the outline. - final Color? highlightColor; - - /// Border radius of the outline to match the child (e.g. Card) corners. - final double borderRadius; - - @override - State createState() => _FlashOnUpdateWidgetState(); -} - -class _FlashOnUpdateWidgetState extends State - with SingleTickerProviderStateMixin { - late final AnimationController _controller; - late final Animation _animation; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - duration: const Duration(milliseconds: 450), - vsync: this, - ); - _animation = CurvedAnimation( - parent: _controller, - curve: Curves.easeOut, - ); - } - - @override - void didUpdateWidget(FlashOnUpdateWidget oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.trigger != oldWidget.trigger) { - _controller.forward(from: 0); - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final color = widget.highlightColor ?? colorScheme.primaryContainer; - - return Stack( - children: [ - widget.child, - AnimatedBuilder( - animation: _animation, - builder: (context, _) { - final opacity = 1 - _animation.value; - if (opacity <= 0) return const SizedBox.shrink(); - return Positioned.fill( - child: IgnorePointer( - child: CustomPaint( - painter: _OutlinePainter( - color: color, - opacity: opacity, - borderRadius: widget.borderRadius, - ), - ), - ), - ); - }, - ), - ], - ); - } -} - -class _OutlinePainter extends CustomPainter { - _OutlinePainter({ - required this.color, - required this.opacity, - required this.borderRadius, - }); - - static const double _strokeWidth = 2; - - final Color color; - final double opacity; - final double borderRadius; - - @override - void paint(Canvas canvas, Size size) { - if (opacity <= 0) return; - final paint = Paint() - ..color = color.withValues(alpha: opacity) - ..style = PaintingStyle.stroke - ..strokeWidth = _strokeWidth; - final half = _strokeWidth / 2; - final rect = Rect.fromLTWH(half, half, size.width - _strokeWidth, size.height - _strokeWidth); - final rrect = RRect.fromRectAndRadius(rect, Radius.circular(borderRadius - half)); - canvas.drawRRect(rrect, paint); - } - - @override - bool shouldRepaint(_OutlinePainter oldDelegate) { - return oldDelegate.opacity != opacity || - oldDelegate.color != color || - oldDelegate.borderRadius != borderRadius; - } -} diff --git a/example/lib/widgets/scan_controller_scope.dart b/example/lib/widgets/scan_controller_scope.dart deleted file mode 100644 index c2751b0..0000000 --- a/example/lib/widgets/scan_controller_scope.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:universal_ble_example/data/scan_controller.dart'; - -/// Provides [ScanController] to the widget tree so the scan button -/// can be shown on both the scanner and detail screens. -class ScanControllerScope extends InheritedWidget { - const ScanControllerScope({ - super.key, - required this.controller, - required super.child, - }); - - final ScanController controller; - - static ScanController of(BuildContext context) { - final scope = context - .dependOnInheritedWidgetOfExactType(); - assert(scope != null, 'No ScanControllerScope found in context'); - return scope!.controller; - } - - @override - bool updateShouldNotify(ScanControllerScope oldWidget) { - return controller != oldWidget.controller; - } -} diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt index 76485b0..b375099 100644 --- a/example/linux/CMakeLists.txt +++ b/example/linux/CMakeLists.txt @@ -4,10 +4,10 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "universal_ble") +set(BINARY_NAME "universal_ble_example") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.navideck.universalble") +set(APPLICATION_ID "com.navideck.universal_ble") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc index f6f23bf..e71a16d 100644 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,6 @@ #include "generated_plugin_registrant.h" -#include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake index f16b4c3..2e1de87 100644 --- a/example/linux/flutter/generated_plugins.cmake +++ b/example/linux/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc index cd68647..a3e4f92 100644 --- a/example/linux/my_application.cc +++ b/example/linux/my_application.cc @@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "Universal BLE"); + gtk_header_bar_set_title(header_bar, "universal_ble_example"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "Universal BLE"); + gtk_window_set_title(window, "universal_ble_example"); } gtk_window_set_default_size(window, 1280, 720); diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 18cc2a5..7ddba83 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,16 +5,8 @@ import FlutterMacOS import Foundation -import package_info_plus -import path_provider_foundation -import shared_preferences_foundation import universal_ble -import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin")) - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index ec214e6..f496afd 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -1,48 +1,22 @@ PODS: - FlutterMacOS (1.0.0) - - package_info_plus (0.0.1): - - FlutterMacOS - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - universal_ble (0.0.1): - Flutter - FlutterMacOS - - url_launcher_macos (0.0.1): - - FlutterMacOS DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - universal_ble (from `Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin`) - - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral - package_info_plus: - :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos - path_provider_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin - shared_preferences_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin universal_ble: :path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin - url_launcher_macos: - :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 - shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb universal_ble: 45519b2aeafe62761e2c6309f8927edb5288b914 - url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 0415455..6fa729e 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -68,7 +68,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* Universal BLE.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Universal BLE.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* universal_ble_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = universal_ble_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -144,7 +144,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* Universal BLE.app */, + 33CC10ED2044A3C60003C045 /* universal_ble_example.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -248,7 +248,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* Universal BLE.app */; + productReference = 33CC10ED2044A3C60003C045 /* universal_ble_example.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -477,10 +477,10 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Universal BLE.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Universal BLE"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/universal_ble_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/universal_ble_example"; }; name = Debug; }; @@ -492,10 +492,10 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Universal BLE.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Universal BLE"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/universal_ble_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/universal_ble_example"; }; name = Release; }; @@ -507,10 +507,10 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Universal BLE.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Universal BLE"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/universal_ble_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/universal_ble_example"; }; name = Profile; }; diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5a50511..b3b52f0 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -66,7 +66,7 @@ @@ -83,7 +83,7 @@ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 96d3fee..a2ec33f 100644 --- a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "info": { - "version": 1, - "author": "xcode" + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" }, - "images": [ - { - "size": "16x16", - "idiom": "mac", - "filename": "app_icon_16.png", - "scale": "1x" - }, - { - "size": "16x16", - "idiom": "mac", - "filename": "app_icon_32.png", - "scale": "2x" - }, - { - "size": "32x32", - "idiom": "mac", - "filename": "app_icon_32.png", - "scale": "1x" - }, - { - "size": "32x32", - "idiom": "mac", - "filename": "app_icon_64.png", - "scale": "2x" - }, - { - "size": "128x128", - "idiom": "mac", - "filename": "app_icon_128.png", - "scale": "1x" - }, - { - "size": "128x128", - "idiom": "mac", - "filename": "app_icon_256.png", - "scale": "2x" - }, - { - "size": "256x256", - "idiom": "mac", - "filename": "app_icon_256.png", - "scale": "1x" - }, - { - "size": "256x256", - "idiom": "mac", - "filename": "app_icon_512.png", - "scale": "2x" - }, - { - "size": "512x512", - "idiom": "mac", - "filename": "app_icon_512.png", - "scale": "1x" - }, - { - "size": "512x512", - "idiom": "mac", - "filename": "app_icon_1024.png", - "scale": "2x" - } - ] -} \ No newline at end of file + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index d2e2066..82b6f9d 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index e3f08fe..13b35eb 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 080adb6..0a3f5fa 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index 4201728..bdb5722 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 3bee483..f083318 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 8925dc9..326c0e7 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index b4bccda..2f1632c 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig index 96515de..d550e14 100644 --- a/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/example/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = Universal BLE +PRODUCT_NAME = universal_ble_example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble +PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2023 com.navideck. All rights reserved. diff --git a/example/package_rename_config.yaml b/example/package_rename_config.yaml deleted file mode 100644 index 252744b..0000000 --- a/example/package_rename_config.yaml +++ /dev/null @@ -1,32 +0,0 @@ -package_rename_config: - android: - app_name: "Universal BLE" - package_name: "com.navideck.universalble" - override_old_package: "com.navideck.universal_ble_example" - lang: "kotlin" - - ios: - app_name: "Universal BLE" - bundle_name: "Universal BLE" - package_name: "com.navideck.universalble" - - linux: - app_name: "Universal BLE" - package_name: "com.navideck.universalble" - exe_name: "universal_ble" - - macos: - app_name: "Universal BLE" - package_name: "com.navideck.universalble" - copyright_notice: "Copyright © 2023 com.navideck. All rights reserved." - - windows: - app_name: "Universal BLE" - organization: "Navideck" - copyright_notice: "Copyright © 2023 com.navideck. All rights reserved." - exe_name: "universal_ble" - - web: - app_name: "Universal BLE" - short_app_name: "Universal BLE" - description: "Universal BLE" diff --git a/example/pubspec.lock b/example/pubspec.lock index 5e4f53b..3bf9a77 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,14 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - archive: - dependency: transitive - description: - name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" - url: "https://pub.dev" - source: hosted - version: "4.0.7" args: dependency: transitive description: @@ -49,22 +41,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" - source: hosted - version: "0.4.2" clock: dependency: transitive description: @@ -89,22 +65,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - csslib: - dependency: transitive - description: - name: csslib - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" - source: hosted - version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -121,30 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" - device_frame: - dependency: transitive - description: - name: device_frame - sha256: "7b2ebb2a09d6cc0f086b51bd1412d7be83e0170056a7290349169be41164c86a" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - device_preview: + expandable: dependency: "direct main" description: - name: device_preview - sha256: "88aa1cc73ee9a8ec771b309dcbc4000cc66b5d8456b825980997640ab1195bf5" + name: expandable + sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2" url: "https://pub.dev" source: hosted - version: "1.3.1" - device_preview_screenshot: - dependency: "direct main" - description: - name: device_preview_screenshot - sha256: c79f185e155c0ed5d97b4e1afff6df181c2045fe28c7414c95f0b46307c549e6 - url: "https://pub.dev" - source: hosted - version: "1.0.0" + version: "5.0.1" fake_async: dependency: transitive description: @@ -179,15 +123,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_launcher_icons: - dependency: "direct dev" - description: - path: "." - ref: improve-windows-ico-generation - resolved-ref: d623fac15f257f06bfc21ed7afdd0b6f2739ea21 - url: "https://github.com/Navideck/flutter_launcher_icons.git" - source: git - version: "0.14.3" flutter_lints: dependency: "direct dev" description: @@ -196,11 +131,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -214,77 +144,16 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" - url: "https://pub.dev" - source: hosted - version: "3.1.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter source: sdk version: "0.0.0" - html: - dependency: transitive - description: - name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.dev" - source: hosted - version: "0.15.6" - http: - dependency: transitive - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - image: - dependency: transitive - description: - name: image - sha256: "51555e36056541237b15b57afc31a0f53d4f9aefd9bd00873a6dc0090e54e332" - url: "https://pub.dev" - source: hosted - version: "4.6.0" integration_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" - intl: - dependency: transitive - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" - source: hosted - version: "4.9.0" leak_tracker: dependency: transitive description: @@ -317,14 +186,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" - logger: - dependency: transitive - description: - name: logger - sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 - url: "https://pub.dev" - source: hosted - version: "2.6.2" logging: dependency: transitive description: @@ -357,38 +218,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - package_info_plus: - dependency: "direct main" - description: - name: package_info_plus - sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d - url: "https://pub.dev" - source: hosted - version: "9.0.0" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - package_rename: - dependency: "direct dev" - description: - name: package_rename - sha256: "8e957670ab3c8ab0aa9976d10dfe8575c279e81a48aeeca9e7de43dbdc2c2be0" - url: "https://pub.dev" - source: hosted - version: "1.10.1" path: dependency: transitive description: @@ -397,54 +226,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" - url: "https://pub.dev" - source: hosted - version: "2.5.1" - 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: @@ -469,14 +250,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - posix: - dependency: transitive - description: - name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" - url: "https://pub.dev" - source: hosted - version: "6.0.3" process: dependency: transitive description: @@ -485,70 +258,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.5" - provider: - dependency: transitive - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713" - url: "https://pub.dev" - source: hosted - version: "2.4.15" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b" - url: "https://pub.dev" - source: hosted - version: "2.5.5" - 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" sky_engine: dependency: transitive description: flutter @@ -624,71 +333,7 @@ packages: path: ".." relative: true source: path - version: "1.2.0" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" - source: hosted - version: "6.3.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" - url: "https://pub.dev" - source: hosted - version: "6.3.28" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad - url: "https://pub.dev" - source: hosted - version: "6.3.6" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.dev" - source: hosted - version: "3.2.2" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.dev" - source: hosted - version: "3.2.5" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.dev" - source: hosted - version: "3.1.5" + version: "1.0.0" vector_math: dependency: transitive description: @@ -721,22 +366,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" - source: hosted - version: "5.15.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" xml: dependency: transitive description: @@ -745,14 +374,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" - yaml: - dependency: "direct main" - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.9.0-0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 87f97ba..1e012a2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -10,16 +10,10 @@ dependencies: flutter: sdk: flutter convert: ^3.1.1 + expandable: ^5.0.1 cupertino_icons: ^1.0.2 - shared_preferences: ^2.5.4 - package_info_plus: ^9.0.0 - url_launcher: ^6.3.2 - path_provider: ^2.1.1 - yaml: ^3.1.3 universal_ble: path: ../ - device_preview: ^1.3.1 - device_preview_screenshot: ^1.0.0 dev_dependencies: integration_test: @@ -27,14 +21,6 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 - package_rename: ^1.10.1 - flutter_launcher_icons: - git: - url: https://github.com/Navideck/flutter_launcher_icons.git - ref: improve-windows-ico-generation flutter: uses-material-design: true - assets: - - assets/icon.png - - assets/company_identifiers.yaml diff --git a/example/web/favicon.png b/example/web/favicon.png index 080adb6..8aaa46a 100644 Binary files a/example/web/favicon.png and b/example/web/favicon.png differ diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png index 9f765bd..b749bfe 100644 Binary files a/example/web/icons/Icon-192.png and b/example/web/icons/Icon-192.png differ diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png index 8925dc9..88cfd48 100644 Binary files a/example/web/icons/Icon-512.png and b/example/web/icons/Icon-512.png differ diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png index 9f765bd..eb9b4d7 100644 Binary files a/example/web/icons/Icon-maskable-192.png and b/example/web/icons/Icon-maskable-192.png differ diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png index 8925dc9..d69c566 100644 Binary files a/example/web/icons/Icon-maskable-512.png and b/example/web/icons/Icon-maskable-512.png differ diff --git a/example/web/index.html b/example/web/index.html index 4ce3164..26f5da2 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -1,4 +1,6 @@ - + + + - + - + - Universal BLE + example - - - - - - - - - + + + diff --git a/example/web/manifest.json b/example/web/manifest.json index d61d7d4..8bc88df 100644 --- a/example/web/manifest.json +++ b/example/web/manifest.json @@ -5,7 +5,7 @@ "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", - "description": "Universal BLE", + "description": "Example app for Web", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt index 174008d..0937d82 100644 --- a/example/windows/CMakeLists.txt +++ b/example/windows/CMakeLists.txt @@ -3,8 +3,7 @@ cmake_minimum_required(VERSION 3.14) project(universal_ble_example LANGUAGES CXX) # The name of the executable created for the application. Change this to change -# the on-disk name of your application. Must match the CMake project/target name -# expected by Flutter (e.g. from pubspec name) or TARGET_FILE_DIR references fail. +# the on-disk name of your application. set(BINARY_NAME "universal_ble_example") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index b3bed2a..5c765e6 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,8 @@ #include "generated_plugin_registrant.h" #include -#include void RegisterPlugins(flutter::PluginRegistry* registry) { UniversalBlePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("UniversalBlePluginCApi")); - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index a6a3a4c..97ecaa5 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST universal_ble - url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc index 966b94f..23c0d5c 100644 --- a/example/windows/runner/Runner.rc +++ b/example/windows/runner/Runner.rc @@ -52,7 +52,7 @@ END // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. -IDI_APP_ICON ICON "resources/app_icon.ico" +IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// @@ -89,13 +89,13 @@ BEGIN BEGIN BLOCK "040904e4" BEGIN - VALUE "CompanyName", "Navideck" "\0" - VALUE "FileDescription", "Universal BLE" "\0" + VALUE "CompanyName", "com.navideck" "\0" + VALUE "FileDescription", "universal_ble_example" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "Universal BLE" "\0" - VALUE "LegalCopyright", "Copyright © 2023 com.navideck. All rights reserved." "\0" - VALUE "OriginalFilename", "universal_ble.exe" "\0" - VALUE "ProductName", "Universal BLE" "\0" + VALUE "InternalName", "universal_ble_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.navideck. All rights reserved." "\0" + VALUE "OriginalFilename", "universal_ble_example.exe" "\0" + VALUE "ProductName", "universal_ble_example" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/example/windows/runner/main.cpp b/example/windows/runner/main.cpp index 5c8b4dd..f2b1e45 100644 --- a/example/windows/runner/main.cpp +++ b/example/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"Universal BLE", origin, size)) { + if (!window.Create(L"universal_ble_example", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); diff --git a/example/windows/runner/resources/app_icon.ico b/example/windows/runner/resources/app_icon.ico index 27c6b4f..c04e20c 100644 Binary files a/example/windows/runner/resources/app_icon.ico and b/example/windows/runner/resources/app_icon.ico differ