Improve example app UI (#193)
* Improve example app ui * Store favorite services in local storage * Use Text instead of SelectableText * Improve colors * Fix flutter analyzer issues * Revert example app minimum sdk requirement * Implement hasPermission api and improve ui flow * More ui improvements * Improve ScannedItem ui * Improve control ui * Change color theme to blue * Minor improvements wip * improve responsive ui and flow * More improvements * More improvements * Update Mac podfile * Fix home page scroll view * Update app and package name * Revert changelog * Resolve Ai comments --------- Co-authored-by: Foti Dim <foti@navideck.com>
@@ -270,7 +270,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
||||
UniversalBleLogger.logError("Device might be connected but not known to this app")
|
||||
BleConnectionState.Disconnected.value
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
return BleConnectionState.Disconnected.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ if (flutterVersionName == null) {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "com.navideck.universal_ble_example"
|
||||
namespace = "com.navideck.universalble"
|
||||
compileSdkVersion 36
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
@@ -41,7 +41,7 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.navideck.universal_ble_example"
|
||||
applicationId = "com.navideck.universalble"
|
||||
// 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_example"
|
||||
android:label="Universal BLE"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.navideck.universal_ble_example
|
||||
package com.navideck.universalble
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 718 KiB |
@@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
@@ -0,0 +1,27 @@
|
||||
# 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.0'
|
||||
platform :ios, '13.1'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
@@ -2,6 +2,11 @@ PODS:
|
||||
- Flutter (1.0.0)
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- package_info (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- universal_ble (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -9,6 +14,8 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- package_info (from `.symlinks/plugins/package_info/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- universal_ble (from `.symlinks/plugins/universal_ble/darwin`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@@ -16,14 +23,20 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
package_info:
|
||||
:path: ".symlinks/plugins/package_info/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
universal_ble:
|
||||
:path: ".symlinks/plugins/universal_ble/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
|
||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||
universal_ble: 65e1257dffc557cc7991a93d253beeddc7c1dc92
|
||||
|
||||
PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694
|
||||
PODFILE CHECKSUM: 65200e6210154a397af888bf7e7c86dbadc6d523
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -475,7 +475,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble;
|
||||
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.universalBleExample.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble.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.universalBleExample.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble.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.universalBleExample.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble.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.universalBleExample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble;
|
||||
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.universalBleExample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -1,122 +1 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
{"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"}}
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 663 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 820 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 24 KiB |
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>universal_ble_example</string>
|
||||
<string>Universal BLE</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@@ -5,6 +5,7 @@ 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',
|
||||
@@ -15,12 +16,20 @@ class MockUniversalBle extends UniversalBlePlatform {
|
||||
Uint8List _serviceValue = utf8.encode('Result');
|
||||
bool _isScanning = false;
|
||||
|
||||
final BleService _mockService = BleService('180', [
|
||||
BleCharacteristic('180A', [
|
||||
CharacteristicProperty.read,
|
||||
CharacteristicProperty.write,
|
||||
CharacteristicProperty.notify,
|
||||
], []),
|
||||
final BleService _mockService = BleService('180a', [
|
||||
BleCharacteristic.withMetaData(
|
||||
deviceId: 'MockDeviceId',
|
||||
serviceId: '180a',
|
||||
uuid: '220a',
|
||||
properties: [
|
||||
CharacteristicProperty.read,
|
||||
CharacteristicProperty.write,
|
||||
CharacteristicProperty.notify,
|
||||
],
|
||||
descriptors: [
|
||||
BleDescriptor('220b'),
|
||||
],
|
||||
),
|
||||
]);
|
||||
|
||||
@override
|
||||
@@ -45,11 +54,13 @@ class MockUniversalBle extends UniversalBlePlatform {
|
||||
@override
|
||||
Future<void> connect(String deviceId, {Duration? connectionTimeout}) async {
|
||||
updateConnection(deviceId, true);
|
||||
_connectionStateMap[deviceId] = BleConnectionState.connected;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect(String deviceId) async {
|
||||
updateConnection(deviceId, false);
|
||||
_connectionStateMap[deviceId] = BleConnectionState.disconnected;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -71,7 +82,7 @@ class MockUniversalBle extends UniversalBlePlatform {
|
||||
|
||||
@override
|
||||
Future<List<BleDevice>> getSystemDevices(List<String>? withServices) async {
|
||||
return [];
|
||||
return [_mockBleDevice];
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -124,17 +135,18 @@ class MockUniversalBle extends UniversalBlePlatform {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BleConnectionState> getConnectionState(String deviceId) {
|
||||
throw UnimplementedError();
|
||||
Future<BleConnectionState> getConnectionState(String deviceId) async {
|
||||
return _connectionStateMap[deviceId] ?? BleConnectionState.disconnected;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> disableBluetooth() {
|
||||
throw UnimplementedError();
|
||||
Future<bool> disableBluetooth() async {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestPermissions({bool withAndroidFineLocation = false}) {
|
||||
throw UnimplementedError();
|
||||
Future<void> requestPermissions(
|
||||
{bool withAndroidFineLocation = false}) async {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class StorageService {
|
||||
StorageService._();
|
||||
static StorageService? _instance;
|
||||
static StorageService get instance => _instance ??= StorageService._();
|
||||
|
||||
late SharedPreferencesWithCache _preferences;
|
||||
|
||||
Future<void> init() async {
|
||||
_preferences = await SharedPreferencesWithCache.create(
|
||||
cacheOptions: const SharedPreferencesWithCacheOptions(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setFavoriteServices(List<String> services) async {
|
||||
await _preferences.setStringList('favorite_services', services);
|
||||
}
|
||||
|
||||
List<String> getFavoriteServices() =>
|
||||
_preferences.getStringList('favorite_services') ?? [];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
bool isSystemService(String uuid) {
|
||||
final normalized = uuid.toUpperCase().replaceAll('-', '');
|
||||
return normalized == '00001800' ||
|
||||
normalized == '00001801' ||
|
||||
normalized == '0000180A' ||
|
||||
normalized.startsWith('000018');
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
import 'package:universal_ble_example/data/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;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
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...'
|
||||
: 'Grant Permissions',
|
||||
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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
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/ble_availability_icon.dart';
|
||||
import 'package:universal_ble_example/home/widgets/drawer.dart';
|
||||
import 'package:universal_ble_example/home/widgets/queue_selector_widget.dart';
|
||||
import 'package:universal_ble_example/home/widgets/scan_filter_widget.dart';
|
||||
import 'package:universal_ble_example/home/widgets/scanned_devices_placeholder_widget.dart';
|
||||
import 'package:universal_ble_example/home/widgets/scanned_item_widget.dart';
|
||||
import 'package:universal_ble_example/peripheral_details/peripheral_detail_page.dart';
|
||||
|
||||
class ScannerScreen extends StatefulWidget {
|
||||
const ScannerScreen({super.key});
|
||||
|
||||
@override
|
||||
State createState() => _ScannerScreenState();
|
||||
}
|
||||
|
||||
class _ScannerScreenState extends State<ScannerScreen> {
|
||||
final _bleDevices = <BleDevice>[];
|
||||
final _hiddenDevices = <BleDevice>[];
|
||||
bool _isScanning = false;
|
||||
QueueType _queueType = QueueType.global;
|
||||
TextEditingController servicesFilterController = TextEditingController();
|
||||
TextEditingController namePrefixController = TextEditingController();
|
||||
TextEditingController manufacturerDataController = TextEditingController();
|
||||
final TextEditingController _searchFilterController = TextEditingController();
|
||||
final TextEditingController _webServicesController = TextEditingController();
|
||||
StreamSubscription<BleDevice>? _scanSubscription;
|
||||
|
||||
AvailabilityState? bleAvailabilityState;
|
||||
ScanFilter? scanFilter;
|
||||
final Map<String, bool> _isExpanded = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (const bool.fromEnvironment('MOCK')) {
|
||||
UniversalBle.setInstance(MockUniversalBle());
|
||||
}
|
||||
UniversalBle.queueType = _queueType;
|
||||
UniversalBle.timeout = const Duration(seconds: 10);
|
||||
|
||||
_scanSubscription = UniversalBle.scanStream.listen(_handleScanResult);
|
||||
|
||||
UniversalBle.isScanning().then(
|
||||
(isScanning) => setState(() => _isScanning = isScanning),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleScanResult(BleDevice result) {
|
||||
if (_hiddenDevices.any((e) => e.deviceId == result.deviceId)) {
|
||||
return;
|
||||
}
|
||||
int index = _bleDevices.indexWhere((e) => e.deviceId == result.deviceId);
|
||||
if (index == -1) {
|
||||
_bleDevices.add(result);
|
||||
} else {
|
||||
if (result.name == null && _bleDevices[index].name != null) {
|
||||
result.name = _bleDevices[index].name;
|
||||
}
|
||||
_bleDevices[index] = result;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _startScan() async {
|
||||
setState(() {
|
||||
_bleDevices.clear();
|
||||
_isScanning = true;
|
||||
});
|
||||
try {
|
||||
PlatformConfig? platformConfig;
|
||||
if (kIsWeb && _webServicesController.text.isNotEmpty) {
|
||||
List<String> webServices = _webServicesController.text
|
||||
.split(',')
|
||||
.where((s) => s.trim().isNotEmpty)
|
||||
.map((s) {
|
||||
try {
|
||||
return BleUuidParser.string(s.trim());
|
||||
} catch (_) {
|
||||
return s.trim();
|
||||
}
|
||||
}).toList();
|
||||
platformConfig = PlatformConfig(
|
||||
web: WebOptions(optionalServices: webServices),
|
||||
);
|
||||
}
|
||||
await UniversalBle.startScan(
|
||||
scanFilter: scanFilter,
|
||||
platformConfig: platformConfig,
|
||||
);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isScanning = false;
|
||||
});
|
||||
showSnackbar(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
List<BleDevice> get _filteredDevices {
|
||||
if (_searchFilterController.text.isEmpty) {
|
||||
return _bleDevices;
|
||||
}
|
||||
final filter = _searchFilterController.text.toLowerCase();
|
||||
return _bleDevices.where((device) {
|
||||
final name = device.name?.toLowerCase() ?? '';
|
||||
final deviceId = device.deviceId.toLowerCase();
|
||||
final services = device.services.join(' ').toLowerCase();
|
||||
return name.contains(filter) ||
|
||||
deviceId.contains(filter) ||
|
||||
services.contains(filter);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
bool get _isBluetoothAvailable =>
|
||||
bleAvailabilityState == AvailabilityState.poweredOn;
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (mounted) super.setState(fn);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
servicesFilterController.dispose();
|
||||
namePrefixController.dispose();
|
||||
manufacturerDataController.dispose();
|
||||
_searchFilterController.dispose();
|
||||
_webServicesController.dispose();
|
||||
|
||||
_scanSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showQueueBottomSheet() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => QueueSelectorWidget(
|
||||
queueType: _queueType,
|
||||
onQueueTypeChanged: (queueType) {
|
||||
setState(() {
|
||||
_queueType = queueType;
|
||||
UniversalBle.queueType = _queueType;
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
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(
|
||||
'Scanner',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: colorScheme.surface,
|
||||
leading: Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: const Icon(Icons.menu),
|
||||
onPressed: () => Scaffold.of(context).openDrawer(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_isScanning ? Icons.stop_circle : Icons.play_arrow,
|
||||
color: _isScanning ? colorScheme.error : colorScheme.primary,
|
||||
),
|
||||
tooltip: _isScanning ? 'Stop Scan' : 'Start Scan',
|
||||
onPressed: _isBluetoothAvailable
|
||||
? () async {
|
||||
if (_isScanning) {
|
||||
await UniversalBle.stopScan();
|
||||
setState(() {
|
||||
_isScanning = false;
|
||||
});
|
||||
} else {
|
||||
await _startScan();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.queue,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
tooltip: 'Queue Type',
|
||||
onPressed: _showQueueBottomSheet,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Search filter
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchFilterController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search by name, ID, or services...',
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
suffixIcon: _searchFilterController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
onPressed: () {
|
||||
_searchFilterController.clear();
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Web Services input (only for web)
|
||||
if (kIsWeb)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.web,
|
||||
size: 18,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Ble Services',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: TextFormField(
|
||||
controller: _webServicesController,
|
||||
maxLines: 2,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace', fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
'Enter service UUIDs for web (comma-separated)',
|
||||
helperText:
|
||||
'These services will be available to use after connection on Web',
|
||||
helperMaxLines: 2,
|
||||
helperStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: colorScheme.surface,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Device count badge with hide/unhide
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Tooltip(
|
||||
message: _hiddenDevices.isNotEmpty
|
||||
? 'Show hidden devices'
|
||||
: _bleDevices.isNotEmpty
|
||||
? 'Hide already discovered devices. When you turn on a new device, it will be easier to spot.'
|
||||
: '',
|
||||
child: TextButton.icon(
|
||||
onPressed:
|
||||
_bleDevices.isNotEmpty || _hiddenDevices.isNotEmpty
|
||||
? () {
|
||||
if (_hiddenDevices.isNotEmpty) {
|
||||
// Unhide all devices
|
||||
setState(() {
|
||||
_hiddenDevices.clear();
|
||||
});
|
||||
} else if (_bleDevices.isNotEmpty) {
|
||||
// Hide all devices
|
||||
setState(() {
|
||||
_hiddenDevices.clear();
|
||||
_hiddenDevices.addAll(_bleDevices);
|
||||
_bleDevices.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${_bleDevices.length} ${_hiddenDevices.isNotEmpty ? "/ ${_hiddenDevices.length}" : ""} device${_bleDevices.length == 1 ? '' : 's'}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
_hiddenDevices.isNotEmpty
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
size: 16,
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
backgroundColor: colorScheme.primaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: _showScanFilterBottomSheet,
|
||||
icon: Icon(
|
||||
scanFilter != null
|
||||
? Icons.filter_list
|
||||
: Icons.filter_list_outlined,
|
||||
size: 18,
|
||||
color: scanFilter != null
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurface,
|
||||
),
|
||||
label: Text(
|
||||
'Filter',
|
||||
style: TextStyle(
|
||||
color: scanFilter != null
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurface,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
backgroundColor: scanFilter != null
|
||||
? colorScheme.primaryContainer
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_bleDevices.clear();
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.clear_all,
|
||||
size: 18,
|
||||
color: colorScheme.error,
|
||||
),
|
||||
label: Text(
|
||||
'Clear',
|
||||
style: TextStyle(
|
||||
color: colorScheme.error,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
// Devices List
|
||||
Expanded(
|
||||
child: !_isBluetoothAvailable
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.bluetooth_disabled,
|
||||
size: 80,
|
||||
color: colorScheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Bluetooth is Turned Off',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Please turn on Bluetooth to scan for devices',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: colorScheme.onSurface.withValues(
|
||||
alpha: 0.7,
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (BleCapabilities.supportsBluetoothEnableApi) ...[
|
||||
const SizedBox(height: 48),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
try {
|
||||
bool isEnabled =
|
||||
await UniversalBle.enableBluetooth();
|
||||
if (!isEnabled) {
|
||||
showSnackbar(
|
||||
"Please enable Bluetooth in system settings",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
showSnackbar(e.toString());
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.bluetooth, size: 24),
|
||||
label: const Text(
|
||||
'Turn On Bluetooth',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: _isScanning && _bleDevices.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator.adaptive(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Scanning for devices...',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: !_isScanning && _bleDevices.isEmpty
|
||||
? ScannedDevicesPlaceholderWidget(onTap: _startScan)
|
||||
: _filteredDevices.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
size: 64,
|
||||
color: colorScheme.onSurface
|
||||
.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No devices match your filter',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
itemCount: _filteredDevices.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
BleDevice device = _filteredDevices[
|
||||
_filteredDevices.length - index - 1];
|
||||
return ScannedItemWidget(
|
||||
bleDevice: device,
|
||||
isExpanded:
|
||||
_isExpanded[device.deviceId] ?? false,
|
||||
onExpand: (isExpanded) {
|
||||
setState(() {
|
||||
_isExpanded[device.deviceId] =
|
||||
isExpanded;
|
||||
});
|
||||
},
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) =>
|
||||
PeripheralDetailPage(device),
|
||||
),
|
||||
);
|
||||
// Stop scan but keep results visible
|
||||
UniversalBle.stopScan();
|
||||
setState(() {
|
||||
_isScanning = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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'}',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
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/scanner_screen.dart';
|
||||
import 'package:universal_ble_example/home/system_devices_screen.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class AppDrawer extends StatefulWidget {
|
||||
const AppDrawer({super.key});
|
||||
|
||||
@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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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()),
|
||||
),
|
||||
const Divider(),
|
||||
FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (_, snapshot) => 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(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Learn More',
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrl(Uri.parse(
|
||||
"https://github.com/Navideck/universal_ble"));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
import 'package:universal_ble_example/widgets/platform_button.dart';
|
||||
|
||||
class ScanFilterWidget extends StatefulWidget {
|
||||
final void Function(ScanFilter? filter) onScanFilter;
|
||||
@@ -32,9 +31,13 @@ class _ScanFilterWidgetState extends State<ScanFilterWidget> {
|
||||
List<String> namePrefixes = [];
|
||||
List<ManufacturerDataFilter> manufacturerDataFilters = [];
|
||||
|
||||
// Parse Services
|
||||
// Parse Services - handle both comma and newline separated
|
||||
if (widget.servicesFilterController.text.isNotEmpty) {
|
||||
List<String> services = widget.servicesFilterController.text.split(',');
|
||||
List<String> services = widget.servicesFilterController.text
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
for (String service in services) {
|
||||
try {
|
||||
serviceUUids.add(BleUuidParser.string(service.trim()));
|
||||
@@ -44,18 +47,34 @@ class _ScanFilterWidgetState extends State<ScanFilterWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Name Prefix
|
||||
// Parse Name Prefix - handle both comma and newline separated
|
||||
String namePrefix = widget.namePrefixController.text;
|
||||
if (namePrefix.isNotEmpty) {
|
||||
namePrefixes = namePrefix.split(',').map((e) => e.trim()).toList();
|
||||
}
|
||||
|
||||
// Parse Manufacturer Data
|
||||
// Parse Manufacturer Data - handle both comma and newline separated
|
||||
String manufacturerDataText = widget.manufacturerDataController.text;
|
||||
if (manufacturerDataText.isNotEmpty) {
|
||||
List<String> manufacturerData = manufacturerDataText.split(',');
|
||||
List<String> manufacturerData = manufacturerDataText
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
for (String manufacturer in manufacturerData) {
|
||||
int? companyIdentifier = int.tryParse(manufacturer);
|
||||
String trimmed = manufacturer.trim();
|
||||
// Remove 0x prefix if present, otherwise parse as decimal or hex
|
||||
int? companyIdentifier;
|
||||
if (trimmed.toLowerCase().startsWith('0x')) {
|
||||
companyIdentifier = 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]'))) {
|
||||
companyIdentifier = int.tryParse(trimmed, radix: 16);
|
||||
} else {
|
||||
companyIdentifier = int.tryParse(trimmed);
|
||||
}
|
||||
}
|
||||
if (companyIdentifier == null) {
|
||||
throw Exception("Invalid Manufacturer Data $manufacturer");
|
||||
}
|
||||
@@ -98,74 +117,253 @@ class _ScanFilterWidgetState extends State<ScanFilterWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: MediaQuery.of(context).viewInsets.copyWith(left: 20, right: 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: Text(
|
||||
"Scan Filters",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 20,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 20,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.filter_list,
|
||||
color: colorScheme.primary,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Scan Filters",
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: 'Close',
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"Filter devices by name, services, or manufacturer data. Enter multiple values separated by commas.",
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (error != null)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: colorScheme.error.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: colorScheme.error,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
error!,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onErrorContainer,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 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",
|
||||
helperText: "Company identifiers in decimal or hex format",
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Action Buttons
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: clearFilter,
|
||||
icon: const Icon(Icons.clear_all),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Text("Use comma to add multiple values"),
|
||||
const Divider(),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: widget.namePrefixController,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Name Prefixes",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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: 10),
|
||||
TextFormField(
|
||||
controller: widget.servicesFilterController,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Services",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: widget.manufacturerDataController,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Manufacturer Data Company IDs",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
child: PlatformButton(
|
||||
text: 'Apply',
|
||||
onPressed: applyFilter,
|
||||
const SizedBox(height: 4),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
helperText: helperText,
|
||||
helperMaxLines: 2,
|
||||
helperStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outline.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outline.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor:
|
||||
colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
const SizedBox(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,26 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ScannedDevicesPlaceholderWidget extends StatelessWidget {
|
||||
const ScannedDevicesPlaceholderWidget({super.key});
|
||||
final Function() onTap;
|
||||
const ScannedDevicesPlaceholderWidget({super.key, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.bluetooth,
|
||||
color: Colors.grey,
|
||||
size: 100,
|
||||
),
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.bluetooth_searching,
|
||||
size: 80,
|
||||
color: colorScheme.primary.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'No Devices Found',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 48),
|
||||
child: Text(
|
||||
'Tap Scan button to discover nearby Bluetooth devices',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'Scan For Devices',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 22),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,418 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
import 'package:universal_ble_example/home/widgets/rssi_signal_indicator.dart';
|
||||
|
||||
class ScannedItemWidget extends StatelessWidget {
|
||||
final BleDevice bleDevice;
|
||||
final VoidCallback? onTap;
|
||||
const ScannedItemWidget({super.key, required this.bleDevice, this.onTap});
|
||||
final bool isExpanded;
|
||||
final Function(bool) onExpand;
|
||||
const ScannedItemWidget({
|
||||
super.key,
|
||||
required this.bleDevice,
|
||||
this.onTap,
|
||||
required this.isExpanded,
|
||||
required this.onExpand,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
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(
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
'$name (${bleDevice.rssi})',
|
||||
),
|
||||
subtitle: Column(
|
||||
if (name == null || name.isEmpty) name = 'Unknown Device';
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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),
|
||||
)
|
||||
: const Text(
|
||||
"Not Paired",
|
||||
style: TextStyle(color: Colors.red),
|
||||
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: 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 ||
|
||||
bleDevice.services.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(
|
||||
isExpanded
|
||||
? Icons.expand_less
|
||||
: Icons.expand_more,
|
||||
color: colorScheme.primary,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () {
|
||||
onExpand(!isExpanded);
|
||||
},
|
||||
tooltip: 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(
|
||||
bleDevice.deviceId,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurface
|
||||
.withValues(alpha: 0.7),
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
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: bleDevice.paired == true
|
||||
? Colors.green.withValues(alpha: 0.2)
|
||||
: Colors.orange.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
bleDevice.paired == true
|
||||
? 'Paired'
|
||||
: 'Unpaired',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: bleDevice.paired == true
|
||||
? Colors.green.shade700
|
||||
: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Manufacturer data (only in collapsed mode)
|
||||
if (!isExpanded) ...[
|
||||
...rawManufacturerData.take(2).map((data) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
data.companyIdRadix16,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
// Services (only in collapsed mode)
|
||||
if (!isExpanded) ...[
|
||||
...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 (bleDevice.services.length > 3)
|
||||
Text(
|
||||
'+${bleDevice.services.length - 3} more',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: colorScheme.onSurface
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Expanded details
|
||||
if (isExpanded) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 12),
|
||||
if (rawManufacturerData.isNotEmpty) ...[
|
||||
Text(
|
||||
'Manufacturer Data',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...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',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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 (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: 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(),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
trailing: const Icon(Icons.arrow_forward_ios),
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,17 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
import 'package:universal_ble_example/home/home.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';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await UniversalBle.setLogLevel(BleLogLevel.verbose);
|
||||
runApp(
|
||||
MaterialApp(
|
||||
await StorageService.instance.init();
|
||||
// await UniversalBle.setLogLevel(BleLogLevel.verbose);
|
||||
bool hasPermission = await UniversalBle.hasPermissions(
|
||||
withAndroidFineLocation: false,
|
||||
);
|
||||
|
||||
runApp(MyApp(
|
||||
hasPermission: hasPermission,
|
||||
));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
final bool hasPermission;
|
||||
const MyApp({super.key, required this.hasPermission});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Universal BLE',
|
||||
debugShowCheckedModeBanner: false,
|
||||
darkTheme: ThemeData.dark(),
|
||||
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: const MyApp(),
|
||||
),
|
||||
);
|
||||
home: hasPermission ? ScannerScreen() : const PermissionScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,48 +3,196 @@ import 'package:flutter/material.dart';
|
||||
class ResultWidget extends StatelessWidget {
|
||||
final List<String> results;
|
||||
final bool scrollable;
|
||||
final ScrollController scrollController;
|
||||
final void Function(int? index) onClearTap;
|
||||
const ResultWidget({
|
||||
required this.results,
|
||||
required this.onClearTap,
|
||||
this.scrollable = false,
|
||||
required this.scrollController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
if (results.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ListTile(
|
||||
tileColor: Theme.of(context).secondaryHeaderColor,
|
||||
title: const Text("Logs"),
|
||||
onTap: () {
|
||||
onClearTap(null);
|
||||
},
|
||||
trailing: const Icon(Icons.clear),
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: scrollable ? MainAxisSize.max : MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeader(colorScheme),
|
||||
if (results.isEmpty)
|
||||
_buildEmptyState(colorScheme)
|
||||
else
|
||||
_buildLogsList(colorScheme),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.history,
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Logs',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
ListView.separated(
|
||||
shrinkWrap: !scrollable,
|
||||
physics: scrollable ? null : const NeverScrollableScrollPhysics(),
|
||||
itemCount: results.length,
|
||||
reverse: true,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return InkWell(
|
||||
onTap: () => onClearTap(index),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 11, vertical: 2),
|
||||
child: Text(results[index]),
|
||||
const Spacer(),
|
||||
if (results.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Divider(),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'${results.length}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (results.isNotEmpty) ...[
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear_all,
|
||||
color: colorScheme.error,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => onClearTap(null),
|
||||
tooltip: 'Clear all logs',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(ColorScheme colorScheme) {
|
||||
final emptyStateContent = Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.history_outlined,
|
||||
size: 48,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No logs yet',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return scrollable ? Expanded(child: emptyStateContent) : emptyStateContent;
|
||||
}
|
||||
|
||||
Widget _buildLogsList(ColorScheme colorScheme) {
|
||||
final listView = ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: scrollController,
|
||||
itemCount: results.length,
|
||||
itemBuilder: (context, index) => _buildLogItem(colorScheme, index),
|
||||
separatorBuilder: (_, __) => Divider(
|
||||
height: 1,
|
||||
color: colorScheme.outline.withValues(alpha: 0.2),
|
||||
),
|
||||
);
|
||||
|
||||
if (scrollable) {
|
||||
return Expanded(child: listView);
|
||||
} else {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: listView,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLogItem(ColorScheme colorScheme, int index) {
|
||||
var reversedIndex = results.length - index - 1;
|
||||
final log = results[reversedIndex];
|
||||
return InkWell(
|
||||
onTap: () => onClearTap(reversedIndex),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 6,
|
||||
color: colorScheme.primary.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
log,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,222 @@
|
||||
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 StatelessWidget {
|
||||
final List<BleService> discoveredServices;
|
||||
final bool scrollable;
|
||||
final void Function(
|
||||
BleService service,
|
||||
BleCharacteristic characteristic,
|
||||
)? onTap;
|
||||
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;
|
||||
|
||||
const ServicesListWidget({
|
||||
super.key,
|
||||
required this.discoveredServices,
|
||||
this.onTap,
|
||||
this.scrollable = false,
|
||||
this.selectedService,
|
||||
this.selectedCharacteristic,
|
||||
this.favoriteServices,
|
||||
this.subscribedCharacteristics,
|
||||
this.onFavoriteToggle,
|
||||
});
|
||||
|
||||
@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);
|
||||
|
||||
// Sort services: favorites first, then system services, then others
|
||||
final sortedServices = List<BleService>.from(discoveredServices);
|
||||
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 ListView.builder(
|
||||
shrinkWrap: !scrollable,
|
||||
physics: scrollable ? null : const NeverScrollableScrollPhysics(),
|
||||
itemCount: discoveredServices.length,
|
||||
itemCount: sortedServices.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final service = sortedServices[index];
|
||||
final isFavorite = favoriteServices?.contains(service.uuid) ?? false;
|
||||
final isSelected = selectedService?.uuid == service.uuid;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
child: Card(
|
||||
elevation: isSelected ? 2 : 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: isSelected
|
||||
? BorderSide(color: selectedColor, width: 2)
|
||||
: BorderSide.none,
|
||||
),
|
||||
child: ExpandablePanel(
|
||||
header: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.arrow_forward_ios),
|
||||
Expanded(child: Text(discoveredServices[index].uuid)),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 14,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
service.uuid,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: 'monospace',
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.bold : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (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: () => onFavoriteToggle!(service.uuid),
|
||||
iconSize: 20,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
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: () {
|
||||
onTap?.call(discoveredServices[index], e);
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
expanded: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Column(
|
||||
children: service.characteristics.map((e) {
|
||||
final isCharSelected =
|
||||
selectedCharacteristic?.uuid == e.uuid;
|
||||
final isSubscribed =
|
||||
subscribedCharacteristics?[e.uuid] ?? false;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
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: InkWell(
|
||||
onTap: () {
|
||||
onTap?.call(service, e);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.arrow_right_outlined),
|
||||
Expanded(child: Text(e.uuid)),
|
||||
],
|
||||
Icon(
|
||||
Icons.arrow_right_outlined,
|
||||
size: 16,
|
||||
color: isCharSelected
|
||||
? selectedColor
|
||||
: colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
Text(
|
||||
"Properties: ${e.properties.map((e) => e.name)}",
|
||||
const SizedBox(width: 8),
|
||||
if (isSubscribed) ...[
|
||||
Icon(
|
||||
Icons.notifications_active,
|
||||
color: subscribedNotificationIconColor,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
e.uuid,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: isCharSelected
|
||||
? FontWeight.bold
|
||||
: FontWeight.w500,
|
||||
color: isCharSelected
|
||||
? selectedColor
|
||||
: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
class ServicesSideWidget extends StatelessWidget {
|
||||
final List<BleService> discoveredServices;
|
||||
final Function() serviceListBuilder;
|
||||
const ServicesSideWidget({
|
||||
super.key,
|
||||
required this.discoveredServices,
|
||||
required this.serviceListBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
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: 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(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'${discoveredServices.length}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: discoveredServices.isEmpty
|
||||
? Center(
|
||||
child: 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: serviceListBuilder(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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_example")
|
||||
set(BINARY_NAME "universal_ble")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.navideck.universal_ble")
|
||||
set(APPLICATION_ID "com.navideck.universalble")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
#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,6 +3,7 @@
|
||||
#
|
||||
|
||||
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_example");
|
||||
gtk_header_bar_set_title(header_bar, "Universal BLE");
|
||||
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_example");
|
||||
gtk_window_set_title(window, "Universal BLE");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
||||
@@ -5,8 +5,14 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import package_info_plus
|
||||
import shared_preferences_foundation
|
||||
import universal_ble
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
||||
@@ -1,22 +1,41 @@
|
||||
PODS:
|
||||
- FlutterMacOS (1.0.0)
|
||||
- package_info_plus (0.0.1):
|
||||
- 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`)
|
||||
- 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
|
||||
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: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
|
||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||
universal_ble: 65e1257dffc557cc7991a93d253beeddc7c1dc92
|
||||
url_launcher_macos: 175a54c831f4375a6cf895875f716ee5af3888ce
|
||||
|
||||
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_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = universal_ble_example.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10ED2044A3C60003C045 /* Universal BLE.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Universal BLE.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_example.app */,
|
||||
33CC10ED2044A3C60003C045 /* Universal BLE.app */,
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
@@ -248,7 +248,7 @@
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* universal_ble_example.app */;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* Universal BLE.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.universalBleExample.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/universal_ble_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/universal_ble_example";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Universal BLE.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Universal BLE";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -492,10 +492,10 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/universal_ble_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/universal_ble_example";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Universal BLE.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Universal BLE";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -507,10 +507,10 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/universal_ble_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/universal_ble_example";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Universal BLE.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Universal BLE";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "universal_ble_example.app"
|
||||
BuildableName = "Universal BLE.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -31,7 +31,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "universal_ble_example.app"
|
||||
BuildableName = "Universal BLE.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -66,7 +66,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "universal_ble_example.app"
|
||||
BuildableName = "Universal BLE.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -83,7 +83,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "universal_ble_example.app"
|
||||
BuildableName = "Universal BLE.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_16.png",
|
||||
"scale" : "1x"
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_32.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_32.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_64.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_256.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_256.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_512.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_512.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_1024.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
"images": [
|
||||
{
|
||||
"size": "16x16",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_16.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size": "16x16",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_32.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "32x32",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_32.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size": "32x32",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_64.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "128x128",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_128.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size": "128x128",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_256.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "256x256",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_256.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size": "256x256",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_512.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size": "512x512",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_512.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size": "512x512",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_1024.png",
|
||||
"scale": "2x"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 663 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 578 B |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 5.4 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_example
|
||||
PRODUCT_NAME = Universal BLE
|
||||
|
||||
// The application's bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2023 com.navideck. All rights reserved.
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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,6 +1,14 @@
|
||||
# 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:
|
||||
@@ -41,6 +49,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
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:
|
||||
@@ -65,6 +89,22 @@ 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:
|
||||
@@ -123,6 +163,15 @@ 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:
|
||||
@@ -144,16 +193,61 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.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"
|
||||
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:
|
||||
@@ -186,6 +280,14 @@ 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:
|
||||
@@ -218,6 +320,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.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:
|
||||
@@ -226,6 +352,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.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:
|
||||
@@ -250,6 +400,14 @@ 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:
|
||||
@@ -258,6 +416,62 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.5"
|
||||
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
|
||||
@@ -334,6 +548,70 @@ packages:
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.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"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -366,6 +644,22 @@ 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:
|
||||
@@ -374,6 +668,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.8.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
|
||||
@@ -12,6 +12,9 @@ dependencies:
|
||||
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
|
||||
universal_ble:
|
||||
path: ../
|
||||
|
||||
@@ -21,6 +24,13 @@ 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
|
||||
|
||||
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 578 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 170 KiB |
@@ -1,6 +1,4 @@
|
||||
<!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.
|
||||
@@ -18,21 +16,27 @@
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="Example app for Web.">
|
||||
<meta name="description" content="Universal BLE">
|
||||
|
||||
<!-- 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="Example app for Web.">
|
||||
<meta name="apple-mobile-web-app-title" content="Universal BLE">
|
||||
<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>example</title>
|
||||
<title>Universal BLE</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": "Example app for Web",
|
||||
"description": "Universal BLE",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
|
||||
@@ -4,7 +4,7 @@ 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.
|
||||
set(BINARY_NAME "universal_ble_example")
|
||||
set(BINARY_NAME "universal_ble")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
|
||||
@@ -89,13 +89,13 @@ BEGIN
|
||||
BEGIN
|
||||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "com.navideck" "\0"
|
||||
VALUE "FileDescription", "universal_ble_example" "\0"
|
||||
VALUE "CompanyName", "Navideck" "\0"
|
||||
VALUE "FileDescription", "Universal BLE" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "universal_ble_example" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2023 com.navideck. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "universal_ble_example.exe" "\0"
|
||||
VALUE "ProductName", "universal_ble_example" "\0"
|
||||
VALUE "InternalName", "Universal BLE" "\0"
|
||||
VALUE "LegalCopyright", "Copyright © 2023 com.navideck. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "universal_ble.exe" "\0"
|
||||
VALUE "ProductName", "Universal BLE" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
END
|
||||
END
|
||||
|
||||
@@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.Create(L"universal_ble_example", origin, size)) {
|
||||
if (!window.Create(L"Universal BLE", origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 83 KiB |
@@ -8,6 +8,9 @@ class ManufacturerData {
|
||||
|
||||
String get companyIdRadix16 => "0x0${companyId.toRadixString(16)}";
|
||||
|
||||
String get payloadRadix16 =>
|
||||
"0x${payload.map((e) => e.toRadixString(16).toUpperCase().padLeft(2, '0')).join('')}";
|
||||
|
||||
factory ManufacturerData.fromData(Uint8List data) {
|
||||
if (data.length < 2) {
|
||||
throw const FormatException("Invalid Manufacturer Data");
|
||||
|
||||