Switch to simple example app
@@ -847,6 +847,10 @@ Future<void> resetBleState() async {
|
||||
|
||||
</details>
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Demonstrates how to use the universal_ble plugin.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
|
||||
|
||||
<application
|
||||
android:label="Universal BLE"
|
||||
android:label="universal_ble_example"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.navideck.universalble
|
||||
package com.navideck.universal_ble_example
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 544 B |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 442 B |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 718 KiB |
@@ -1,4 +0,0 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
- shared_preferences: true
|
||||
@@ -1,27 +0,0 @@
|
||||
# flutter pub run flutter_launcher_icons
|
||||
flutter_launcher_icons:
|
||||
image_path: "assets/icon.png"
|
||||
|
||||
android: "ic_launcher"
|
||||
image_path_android: "assets/icon.png"
|
||||
min_sdk_android: 21 # android min sdk min:16, default 21
|
||||
# adaptive_icon_background: "#2C2B2B"
|
||||
# adaptive_icon_foreground: "assets/ic_launcher_monochrome.png"
|
||||
|
||||
ios: true
|
||||
remove_alpha_ios: true
|
||||
|
||||
web:
|
||||
generate: true
|
||||
image_path: "assets/icon.png"
|
||||
# background_color: "#2C2B2B"
|
||||
# theme_color: "#2C2B2B"
|
||||
|
||||
windows:
|
||||
generate: true
|
||||
image_path: "assets/icon.png"
|
||||
icon_size: 48 # min:48, max:256, default: 48
|
||||
|
||||
macos:
|
||||
generate: true
|
||||
image_path: "assets/icon.png"
|
||||
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '13.1'
|
||||
# platform :ios, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
@@ -2,54 +2,28 @@ PODS:
|
||||
- Flutter (1.0.0)
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- 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_ios (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- universal_ble (from `.symlinks/plugins/universal_ble/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
universal_ble:
|
||||
:path: ".symlinks/plugins/universal_ble/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
universal_ble: 45519b2aeafe62761e2c6309f8927edb5288b914
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
universal_ble: 65e1257dffc557cc7991a93d253beeddc7c1dc92
|
||||
|
||||
PODFILE CHECKSUM: 65200e6210154a397af888bf7e7c86dbadc6d523
|
||||
PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -475,7 +475,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -492,7 +492,7 @@
|
||||
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_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -510,7 +510,7 @@
|
||||
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)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -526,7 +526,7 @@
|
||||
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)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -654,7 +654,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -677,7 +677,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -1 +1,122 @@
|
||||
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 663 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 820 B After Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 450 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 282 B |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 462 B |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 704 B |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 586 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 762 B |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Universal BLE</string>
|
||||
<string>universal_ble_example</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@@ -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<int, String> _companyIdToName = {};
|
||||
final Map<String, int> _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<void> 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<String> 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<int> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
/// Mock implementation of [UniversalBlePlatform] for testing
|
||||
class MockUniversalBle extends UniversalBlePlatform {
|
||||
final Map<String, BleConnectionState> _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: [
|
||||
final BleService _mockService = BleService('180', [
|
||||
BleCharacteristic('180A', [
|
||||
CharacteristicProperty.read,
|
||||
CharacteristicProperty.write,
|
||||
CharacteristicProperty.notify,
|
||||
],
|
||||
descriptors: [
|
||||
BleDescriptor('220b'),
|
||||
],
|
||||
),
|
||||
], []),
|
||||
]);
|
||||
|
||||
@override
|
||||
@@ -53,15 +44,13 @@ class MockUniversalBle extends UniversalBlePlatform {
|
||||
|
||||
@override
|
||||
Future<void> connect(String deviceId,
|
||||
{bool autoConnect = false, Duration? connectionTimeout}) async {
|
||||
{Duration? connectionTimeout, bool autoConnect = false}) async {
|
||||
updateConnection(deviceId, true);
|
||||
_connectionStateMap[deviceId] = BleConnectionState.connected;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect(String deviceId) async {
|
||||
updateConnection(deviceId, false);
|
||||
_connectionStateMap[deviceId] = BleConnectionState.disconnected;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -83,7 +72,7 @@ class MockUniversalBle extends UniversalBlePlatform {
|
||||
|
||||
@override
|
||||
Future<List<BleDevice>> getSystemDevices(List<String>? withServices) async {
|
||||
return [_mockBleDevice];
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -114,12 +103,6 @@ class MockUniversalBle extends UniversalBlePlatform {
|
||||
return 512;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> readRssi(String deviceId) async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return -50; // Mock RSSI value in dBm
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setNotifiable(String deviceId, String service,
|
||||
String characteristic, BleInputProperty bleInputProperty) async {}
|
||||
@@ -142,18 +125,22 @@ class MockUniversalBle extends UniversalBlePlatform {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BleConnectionState> getConnectionState(String deviceId) async {
|
||||
return _connectionStateMap[deviceId] ?? BleConnectionState.disconnected;
|
||||
Future<BleConnectionState> getConnectionState(String deviceId) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> disableBluetooth() async {
|
||||
return true;
|
||||
Future<bool> disableBluetooth() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestPermissions(
|
||||
{bool withAndroidFineLocation = false}) async {
|
||||
return;
|
||||
Future<void> requestPermissions({bool withAndroidFineLocation = false}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> readRssi(String deviceId) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -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<String, dynamic> toJson() => {
|
||||
'name': name,
|
||||
'services': services,
|
||||
'namePrefix': namePrefix,
|
||||
'manufacturerData': manufacturerData,
|
||||
};
|
||||
|
||||
factory SavedScanFilter.fromJson(Map<String, dynamic> 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<SavedScanFilter> fromJsonList(String? jsonString) {
|
||||
if (jsonString == null || jsonString.isEmpty) return [];
|
||||
try {
|
||||
final list = jsonDecode(jsonString) as List<dynamic>?;
|
||||
if (list == null) return [];
|
||||
return list
|
||||
.map((e) => SavedScanFilter.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static String toJsonList(List<SavedScanFilter> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<void> syncState() async {
|
||||
final value = await UniversalBle.isScanning();
|
||||
if (_isScanning != value) {
|
||||
_isScanning = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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<void> stopScan() async {
|
||||
if (!_isScanning) return;
|
||||
try {
|
||||
await UniversalBle.stopScan();
|
||||
_isScanning = false;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<ScanFilter?> _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 = <String>[];
|
||||
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
|
||||
? <String>[]
|
||||
: namePrefix.split(',').map((e) => e.trim()).toList();
|
||||
|
||||
final manufacturerDataFilters = <ManufacturerDataFilter>[];
|
||||
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<PlatformConfig> _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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<void> init() async {
|
||||
// SharedPreferencesAsync is used via prefsAsync; no async init needed.
|
||||
}
|
||||
|
||||
Future<void> setFavoriteServices(List<String> services) async {
|
||||
await prefsAsync.setStringList('favorite_services', services);
|
||||
}
|
||||
|
||||
Future<List<String>> getFavoriteServices() async =>
|
||||
(await prefsAsync.getStringList('favorite_services')) ?? [];
|
||||
}
|
||||
@@ -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<BleService> sortBleServices(
|
||||
List<BleService> services, {
|
||||
Set<String>? favoriteServices,
|
||||
}) {
|
||||
final sortedServices = List<BleService>.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<BleService> services, {
|
||||
Set<String>? favoriteServices,
|
||||
Set<CharacteristicProperty>? 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];
|
||||
}
|
||||
}
|
||||
@@ -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<MyApp> {
|
||||
final _bleDevices = <BleDevice>[];
|
||||
final _hiddenDevices = <BleDevice>[];
|
||||
bool _isScanning = false;
|
||||
QueueType _queueType = QueueType.global;
|
||||
TextEditingController servicesFilterController = TextEditingController();
|
||||
TextEditingController namePrefixController = TextEditingController();
|
||||
TextEditingController manufacturerDataController = TextEditingController();
|
||||
StreamSubscription<AvailabilityState>? _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<void> startScan() async {
|
||||
await UniversalBle.startScan(scanFilter: scanFilter);
|
||||
}
|
||||
|
||||
Future<void> _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<BleDevice> 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;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<PermissionScreen> createState() => _PermissionScreenState();
|
||||
}
|
||||
|
||||
class _PermissionScreenState extends State<PermissionScreen>
|
||||
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<void> _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<void> _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<Color>(
|
||||
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<Color>(
|
||||
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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<SystemDevicesScreen> createState() => _SystemDevicesScreenState();
|
||||
}
|
||||
|
||||
class _SystemDevicesScreenState extends State<SystemDevicesScreen> {
|
||||
List<BleDevice> _systemDevices = [];
|
||||
bool _isLoading = false;
|
||||
AvailabilityState? bleAvailabilityState;
|
||||
List<String> withServices = [];
|
||||
final Map<String, bool> _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<void> _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<BleDevice> 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<Color>(
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<BleAvailabilityIcon> createState() => _BleAvailabilityIconState();
|
||||
}
|
||||
|
||||
class _BleAvailabilityIconState extends State<BleAvailabilityIcon> {
|
||||
AvailabilityState? bleAvailabilityState;
|
||||
StreamSubscription<AvailabilityState>? _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),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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'}',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<AppDrawer> createState() => _AppDrawerState();
|
||||
}
|
||||
|
||||
class _AppDrawerState extends State<AppDrawer> {
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<List<SavedScanFilter>> 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<ScanFilterWidget> {
|
||||
String? error;
|
||||
|
||||
int? _selectedSavedFilterIndex(List<SavedScanFilter> 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<void> _showRenameDialog(
|
||||
BuildContext context, SavedScanFilter preset) async {
|
||||
final controller = TextEditingController(text: preset.name);
|
||||
final newName = await showDialog<String>(
|
||||
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<String> serviceUUids = [];
|
||||
List<String> namePrefixes = [];
|
||||
List<ManufacturerDataFilter> manufacturerDataFilters = [];
|
||||
|
||||
// Parse Services - handle both comma and newline separated
|
||||
// Parse Services
|
||||
if (widget.servicesFilterController.text.isNotEmpty) {
|
||||
List<String> services = widget.servicesFilterController.text
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
List<String> 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<ScanFilterWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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<String> manufacturerData = manufacturerDataText
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
List<String> 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<ScanFilterWidget> {
|
||||
withManufacturerData: manufacturerDataFilters,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Filters Applied")),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e.toString();
|
||||
@@ -174,355 +98,74 @@ class _ScanFilterWidgetState extends State<ScanFilterWidget> {
|
||||
|
||||
@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(
|
||||
return Padding(
|
||||
padding: MediaQuery.of(context).viewInsets.copyWith(left: 20, right: 20),
|
||||
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(
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: Text(
|
||||
"Scan Filters",
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
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<List<SavedScanFilter>>(
|
||||
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<int>(
|
||||
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<int>(
|
||||
value: null,
|
||||
child: Text('—'),
|
||||
),
|
||||
...List.generate(
|
||||
savedFilters.length,
|
||||
(i) => DropdownMenuItem<int>(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 SizedBox(height: 4),
|
||||
const Text("Use comma to add multiple values"),
|
||||
const Divider(),
|
||||
const SizedBox(height: 10),
|
||||
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),
|
||||
controller: widget.namePrefixController,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Name Prefixes",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outline.withValues(alpha: 0.3),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: widget.servicesFilterController,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Services",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2,
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: widget.manufacturerDataController,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Manufacturer Data Company IDs",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor:
|
||||
colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8,
|
||||
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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
return const 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,
|
||||
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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,690 +1,50 @@
|
||||
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<ScannedItemWidget> createState() => _ScannedItemWidgetState();
|
||||
}
|
||||
|
||||
class _ScannedItemWidgetState extends State<ScannedItemWidget> {
|
||||
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<ManufacturerData> 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<ManufacturerData> 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: ListTile(
|
||||
title: Text(
|
||||
'$name (${bleDevice.rssi})',
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
subtitle: 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 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),
|
||||
),
|
||||
child: Text(
|
||||
'No manufacturer data',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colorScheme.onSecondaryContainer
|
||||
.withValues(alpha: 0.7),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
Text(bleDevice.deviceId),
|
||||
Visibility(
|
||||
visible: manufacturerData != null,
|
||||
child: Text(manufacturerData.toString()),
|
||||
),
|
||||
if (bleDevice.timestampDateTime != null)
|
||||
Text("Last Seen: ${bleDevice.timestampDateTime}"),
|
||||
bleDevice.paired == true
|
||||
? const Text(
|
||||
"Paired",
|
||||
style: TextStyle(color: Colors.green),
|
||||
)
|
||||
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',
|
||||
),
|
||||
),
|
||||
: const Text(
|
||||
"Not Paired",
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Directory> 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;
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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,
|
||||
return Column(
|
||||
children: [
|
||||
_buildHeader(colorScheme),
|
||||
if (results.isEmpty)
|
||||
_buildEmptyState(colorScheme)
|
||||
else
|
||||
_buildLogsList(colorScheme),
|
||||
],
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
ListView.separated(
|
||||
shrinkWrap: !scrollable,
|
||||
physics: scrollable ? null : const NeverScrollableScrollPhysics(),
|
||||
itemCount: results.length,
|
||||
itemBuilder: (context, index) => _buildLogItem(colorScheme, index),
|
||||
separatorBuilder: (_, __) => Divider(
|
||||
height: 1,
|
||||
color: colorScheme.outline.withValues(alpha: 0.2),
|
||||
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]),
|
||||
),
|
||||
);
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
},
|
||||
separatorBuilder: (_, __) => const Divider(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BleService> discoveredServices;
|
||||
final bool scrollable;
|
||||
final void Function(BleService service, BleCharacteristic characteristic)?
|
||||
onTap;
|
||||
final BleService? selectedService;
|
||||
final BleCharacteristic? selectedCharacteristic;
|
||||
final Set<String>? favoriteServices;
|
||||
final Map<String, bool>? subscribedCharacteristics;
|
||||
final void Function(String serviceUuid)? onFavoriteToggle;
|
||||
final Set<CharacteristicProperty>? 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<ServicesListWidget> createState() => ServicesListWidgetState();
|
||||
}
|
||||
|
||||
class ServicesListWidgetState extends State<ServicesListWidget> {
|
||||
final Map<String, ExpansibleController> _expandableControllers = {};
|
||||
ScrollController? _scrollController;
|
||||
final Map<String, GlobalKey> _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<BleService> _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<BleService> 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(
|
||||
collapsed: const SizedBox(),
|
||||
expanded: Column(
|
||||
children: discoveredServices[index]
|
||||
.characteristics
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
widget.onTap?.call(service, e);
|
||||
onTap?.call(discoveredServices[index], 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),
|
||||
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,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.arrow_right_outlined),
|
||||
Expanded(child: Text(e.uuid)),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
e.uuid,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: isCharSelected
|
||||
? FontWeight.bold
|
||||
: FontWeight.w500,
|
||||
color: isCharSelected
|
||||
? selectedColor
|
||||
: colorScheme.onSurface,
|
||||
),
|
||||
Text(
|
||||
"Properties: ${e.properties.map((e) => e.name)}",
|
||||
),
|
||||
if (e.descriptors.isNotEmpty)
|
||||
Text(
|
||||
"Descriptors: ${e.descriptors.map((e) => e.uuid).join(', ')}",
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<BleService> discoveredServices;
|
||||
final Function(Set<CharacteristicProperty>? selectedProperties,
|
||||
GlobalKey<ServicesListWidgetState>? listKey, bool isDiscoveringServices)
|
||||
serviceListBuilder;
|
||||
final VoidCallback? onCopyServices;
|
||||
final BleService? selectedService;
|
||||
final BleCharacteristic? selectedCharacteristic;
|
||||
final Function(BleService service, BleCharacteristic characteristic)?
|
||||
onCharacteristicSelected;
|
||||
final Function(Set<CharacteristicProperty>? propertyFilters)?
|
||||
onPropertyFiltersChanged;
|
||||
final Set<CharacteristicProperty>? 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<ServicesSideWidget> createState() => _ServicesSideWidgetState();
|
||||
}
|
||||
|
||||
class _ServicesSideWidgetState extends State<ServicesSideWidget> {
|
||||
late Set<CharacteristicProperty>? _selectedProperties;
|
||||
bool _showFilters = false;
|
||||
final GlobalKey<ServicesListWidgetState> _servicesListKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedProperties = widget.initialPropertyFilters != null
|
||||
? Set<CharacteristicProperty>.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<CharacteristicProperty>.from(newFilters)
|
||||
: null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _togglePropertyFilter(CharacteristicProperty property) {
|
||||
setState(() {
|
||||
_selectedProperties ??= <CharacteristicProperty>{};
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<bool> 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<FlashOnUpdateWidget> createState() => _FlashOnUpdateWidgetState();
|
||||
}
|
||||
|
||||
class _FlashOnUpdateWidgetState extends State<FlashOnUpdateWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _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;
|
||||
}
|
||||
}
|
||||
@@ -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<ScanControllerScope>();
|
||||
assert(scope != null, 'No ScanControllerScope found in context');
|
||||
return scope!.controller;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ScanControllerScope oldWidget) {
|
||||
return controller != oldWidget.controller;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "Universal BLE.app"
|
||||
BuildableName = "universal_ble_example.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -31,7 +31,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "Universal BLE.app"
|
||||
BuildableName = "universal_ble_example.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -66,7 +66,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "Universal BLE.app"
|
||||
BuildableName = "universal_ble_example.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -83,7 +83,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "Universal BLE.app"
|
||||
BuildableName = "universal_ble_example.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
{
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
},
|
||||
"images": [
|
||||
"images" : [
|
||||
{
|
||||
"size": "16x16",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_16.png",
|
||||
"scale": "1x"
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_16.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size": "16x16",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_32.png",
|
||||
"scale": "2x"
|
||||
"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_32.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size": "32x32",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_64.png",
|
||||
"scale": "2x"
|
||||
"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_128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size": "128x128",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_256.png",
|
||||
"scale": "2x"
|
||||
"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_256.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size": "256x256",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_512.png",
|
||||
"scale": "2x"
|
||||
"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_512.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size": "512x512",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_1024.png",
|
||||
"scale": "2x"
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_1024.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 663 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 520 B |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 578 B After Width: | Height: | Size: 917 B |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 20 KiB |
@@ -1,4 +1,6 @@
|
||||
<!DOCTYPE html><html><head>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
@@ -16,27 +18,21 @@
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="Universal BLE">
|
||||
<meta name="description" content="Example app for Web.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Universal BLE">
|
||||
<meta name="apple-mobile-web-app-title" content="Example app for Web.">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>Universal BLE</title>
|
||||
<title>example</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async=""></script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</body></html>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,11 +7,8 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <universal_ble/universal_ble_plugin_c_api.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
UniversalBlePluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UniversalBlePluginCApi"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||