Show and filter by company name (#208)
* Update company identifier handling and enhance scanning features - Updated `pubspec.lock` to version 1.2.0 and added `yaml` dependency. - Introduced `CompanyIdentifierService` for loading and querying company identifiers from a new YAML file. - Enhanced scanning functionality to support filtering by company names and IDs. - Updated UI components to display company names alongside manufacturer data in scanned items. - Improved search functionality to include company names in the filter criteria. * Address AI comments * Address AI comments * Allow searching by company id * Remove redundant loading state update in CompanyIdentifierService * Enhance company ID parsing in CompanyIdentifierService - Updated the `_parseCompanyId` method to support additional formats for company IDs, including non-prefixed strings interpreted as decimal or hexadecimal based on their content. - Improved parsing logic to first attempt hexadecimal conversion if the string contains hex characters, enhancing flexibility in input handling.
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
* Moved search field to app bar header for better accessibility
|
||||
* Moved queue type settings to drawer menu as expandable section
|
||||
* Added tooltip to Bluetooth availability icon (tap to view on mobile)
|
||||
* Display company name based on company identifier from manufacturer data
|
||||
* Enhanced search functionality - now supports searching by company name
|
||||
* Improved overall UI layout and navigation flow
|
||||
|
||||
## 1.1.0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,278 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
/// Service for loading and querying company identifiers from YAML file
|
||||
class CompanyIdentifierService {
|
||||
static CompanyIdentifierService? _instance;
|
||||
static CompanyIdentifierService get instance {
|
||||
_instance ??= CompanyIdentifierService._();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
CompanyIdentifierService._();
|
||||
|
||||
final Map<int, String> _companyIdToName = {};
|
||||
final Map<String, int> _companyNameToId = {};
|
||||
bool _isLoading = false;
|
||||
bool _isLoaded = false;
|
||||
bool _hasWarnedAboutNotLoaded = false;
|
||||
|
||||
/// Parse company ID from a value (String hex or int)
|
||||
/// Supports formats: "0x1053", "0x004C", "1053" (decimal), "4C" (hex), or integer value
|
||||
/// Non-prefixed strings are parsed as decimal if they contain only digits,
|
||||
/// or as hexadecimal if they contain hex characters (a-fA-F)
|
||||
int? _parseCompanyId(dynamic value) {
|
||||
if (value is String) {
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.toLowerCase().startsWith('0x')) {
|
||||
return int.tryParse(trimmed.substring(2), radix: 16);
|
||||
} else {
|
||||
// Try parsing as hex first (if it contains letters), then decimal
|
||||
if (trimmed.contains(RegExp(r'[a-fA-F]'))) {
|
||||
return int.tryParse(trimmed, radix: 16);
|
||||
} else {
|
||||
return int.tryParse(trimmed);
|
||||
}
|
||||
}
|
||||
} else if (value is int) {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Load company identifiers from YAML file
|
||||
///
|
||||
/// **Important:** This method should be called on app startup before using
|
||||
/// any query methods (e.g., [getCompanyName], [getCompanyIdFromName], etc.).
|
||||
/// If query methods are called before this method completes, they will return
|
||||
/// null/empty values and log a warning.
|
||||
Future<void> load() async {
|
||||
if (_isLoaded || _isLoading) return;
|
||||
_isLoading = true;
|
||||
|
||||
try {
|
||||
final String yamlString =
|
||||
await rootBundle.loadString('assets/company_identifiers.yaml');
|
||||
final YamlMap yaml = loadYaml(yamlString) as YamlMap;
|
||||
final YamlList? companyIdentifiers =
|
||||
yaml['company_identifiers'] as YamlList?;
|
||||
|
||||
if (companyIdentifiers == null) {
|
||||
_companyIdToName.clear();
|
||||
_companyNameToId.clear();
|
||||
_isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_companyIdToName.clear();
|
||||
_companyNameToId.clear();
|
||||
|
||||
for (var entry in companyIdentifiers) {
|
||||
if (entry is! YamlMap) continue;
|
||||
|
||||
final value = entry['value'];
|
||||
final name = entry['name'];
|
||||
|
||||
if (value == null || name == null) continue;
|
||||
|
||||
final companyId = _parseCompanyId(value);
|
||||
|
||||
if (companyId != null && name is String) {
|
||||
_companyIdToName[companyId] = name;
|
||||
// Store case-insensitive lookup for company names
|
||||
_companyNameToId[name.toLowerCase()] = companyId;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset warning flag after successful load
|
||||
_hasWarnedAboutNotLoaded = false;
|
||||
_isLoaded = true;
|
||||
} on PlatformException catch (e) {
|
||||
// Handle file loading errors (e.g., file not found, permission issues)
|
||||
debugPrint(
|
||||
'CompanyIdentifierService: Failed to load company identifiers file: ${e.message}',
|
||||
);
|
||||
_companyIdToName.clear();
|
||||
_companyNameToId.clear();
|
||||
_isLoaded = true;
|
||||
} on YamlException catch (e) {
|
||||
// Handle YAML parsing errors (e.g., invalid YAML syntax)
|
||||
debugPrint(
|
||||
'CompanyIdentifierService: Failed to parse YAML: ${e.message}',
|
||||
);
|
||||
_companyIdToName.clear();
|
||||
_companyNameToId.clear();
|
||||
_isLoaded = true;
|
||||
} catch (e, stackTrace) {
|
||||
// Handle any other unexpected errors
|
||||
debugPrint(
|
||||
'CompanyIdentifierService: Unexpected error loading company identifiers: $e',
|
||||
);
|
||||
debugPrint('Stack trace: $stackTrace');
|
||||
_companyIdToName.clear();
|
||||
_companyNameToId.clear();
|
||||
_isLoaded = true;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get company name from company ID
|
||||
///
|
||||
/// Returns the company name for the given [companyId], or `null` if:
|
||||
/// - The company ID is not found in the loaded data
|
||||
/// - The data has not been loaded yet (see [load] method)
|
||||
///
|
||||
/// **Important:** The [load] method must be called on app startup before
|
||||
/// using this method. If called before data is loaded, this method will
|
||||
/// return `null` and log a warning (only once).
|
||||
String? getCompanyName(int companyId) {
|
||||
if (!_isLoaded) {
|
||||
if (!_hasWarnedAboutNotLoaded) {
|
||||
_hasWarnedAboutNotLoaded = true;
|
||||
debugPrint(
|
||||
'CompanyIdentifierService: getCompanyName called before load() completed. '
|
||||
'Company names will not be available until load() is called on app startup.',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return _companyIdToName[companyId];
|
||||
}
|
||||
|
||||
/// Get company name from hex string (e.g., "0x1053" or "0x004C")
|
||||
String? getCompanyNameFromHex(String hexString) {
|
||||
final companyId = _parseCompanyId(hexString);
|
||||
if (companyId == null) return null;
|
||||
return getCompanyName(companyId);
|
||||
}
|
||||
|
||||
/// Get company ID from company name (case-insensitive)
|
||||
///
|
||||
/// Returns the company ID for the given [companyName], or `null` if:
|
||||
/// - The company name is not found in the loaded data
|
||||
/// - The data has not been loaded yet (see [load] method)
|
||||
///
|
||||
/// **Important:** The [load] method must be called on app startup before
|
||||
/// using this method. If called before data is loaded, this method will
|
||||
/// return `null` and log a warning (only once).
|
||||
int? getCompanyIdFromName(String companyName) {
|
||||
if (!_isLoaded) {
|
||||
if (!_hasWarnedAboutNotLoaded) {
|
||||
_hasWarnedAboutNotLoaded = true;
|
||||
debugPrint(
|
||||
'CompanyIdentifierService: getCompanyIdFromName called before load() completed. '
|
||||
'Company identifiers will not be available until load() is called on app startup.',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return _companyNameToId[companyName.toLowerCase()];
|
||||
}
|
||||
|
||||
/// Get all company names (for filtering/searching)
|
||||
///
|
||||
/// Returns a list of all company names, or an empty list if:
|
||||
/// - No company identifiers are loaded
|
||||
/// - The data has not been loaded yet (see [load] method)
|
||||
///
|
||||
/// **Important:** The [load] method must be called on app startup before
|
||||
/// using this method. If called before data is loaded, this method will
|
||||
/// return an empty list and log a warning (only once).
|
||||
List<String> getAllCompanyNames() {
|
||||
if (!_isLoaded) {
|
||||
if (!_hasWarnedAboutNotLoaded) {
|
||||
_hasWarnedAboutNotLoaded = true;
|
||||
debugPrint(
|
||||
'CompanyIdentifierService: getAllCompanyNames called before load() completed. '
|
||||
'Company names will not be available until load() is called on app startup.',
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
return _companyIdToName.values.toList();
|
||||
}
|
||||
|
||||
/// Check if a string matches a company name (case-insensitive partial match)
|
||||
///
|
||||
/// Returns `false` if:
|
||||
/// - No match is found
|
||||
/// - The data has not been loaded yet (see [load] method)
|
||||
///
|
||||
/// **Important:** The [load] method must be called on app startup before
|
||||
/// using this method. If called before data is loaded, this method will
|
||||
/// return `false` and log a warning (only once).
|
||||
bool matchesCompanyName(String query) {
|
||||
if (!_isLoaded) {
|
||||
if (!_hasWarnedAboutNotLoaded) {
|
||||
_hasWarnedAboutNotLoaded = true;
|
||||
debugPrint(
|
||||
'CompanyIdentifierService: matchesCompanyName called before load() completed. '
|
||||
'Company name matching will not be available until load() is called on app startup.',
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
final lowerQuery = query.toLowerCase();
|
||||
return _companyNameToId.keys.any((name) => name.contains(lowerQuery));
|
||||
}
|
||||
|
||||
/// Find company IDs that match a company name query (case-insensitive partial match)
|
||||
///
|
||||
/// Returns a list of matching company IDs, or an empty list if:
|
||||
/// - No matches are found
|
||||
/// - The data has not been loaded yet (see [load] method)
|
||||
///
|
||||
/// **Important:** The [load] method must be called on app startup before
|
||||
/// using this method. If called before data is loaded, this method will
|
||||
/// return an empty list and log a warning (only once).
|
||||
List<int> findCompanyIdsByName(String query) {
|
||||
if (!_isLoaded) {
|
||||
if (!_hasWarnedAboutNotLoaded) {
|
||||
_hasWarnedAboutNotLoaded = true;
|
||||
debugPrint(
|
||||
'CompanyIdentifierService: findCompanyIdsByName called before load() completed. '
|
||||
'Company identifier search will not be available until load() is called on app startup.',
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
final lowerQuery = query.toLowerCase();
|
||||
return _companyNameToId.entries
|
||||
.where((entry) => entry.key.contains(lowerQuery))
|
||||
.map((entry) => entry.value)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Parse a company identifier from a string (either by name or by ID)
|
||||
///
|
||||
/// This method first tries to find the company by name (case-insensitive).
|
||||
/// If not found, it attempts to parse the string as a company ID:
|
||||
/// - Supports hex format with "0x" prefix (e.g., "0x004C")
|
||||
/// - Supports hex format without prefix if it contains hex characters (e.g., "4C")
|
||||
/// - Falls back to decimal parsing if no hex characters are present (e.g., "76")
|
||||
///
|
||||
/// Returns the company ID if found/parsed successfully, null otherwise.
|
||||
int? parseCompanyIdentifier(String value) {
|
||||
final trimmed = value.trim();
|
||||
|
||||
// First, try to find by company name (case-insensitive)
|
||||
final companyId = getCompanyIdFromName(trimmed);
|
||||
if (companyId != null) {
|
||||
return companyId;
|
||||
}
|
||||
|
||||
// If not found by name, try parsing as company ID
|
||||
if (trimmed.toLowerCase().startsWith('0x')) {
|
||||
return int.tryParse(trimmed.substring(2), radix: 16);
|
||||
} else {
|
||||
// Try parsing as hex first (if it contains letters), then decimal
|
||||
if (trimmed.contains(RegExp(r'[a-fA-F]'))) {
|
||||
return int.tryParse(trimmed, radix: 16);
|
||||
} else {
|
||||
return int.tryParse(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
import 'package:universal_ble_example/data/company_identifier_service.dart';
|
||||
import 'package:universal_ble_example/data/mock_universal_ble.dart';
|
||||
import 'package:universal_ble_example/home/widgets/ble_availability_icon.dart';
|
||||
import 'package:universal_ble_example/home/widgets/drawer.dart';
|
||||
@@ -52,6 +53,9 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
|
||||
_loadScanFilters();
|
||||
|
||||
// Load company identifiers in the background
|
||||
CompanyIdentifierService.instance.load();
|
||||
|
||||
// Save search filter when it changes
|
||||
_searchFilterController.addListener(() {
|
||||
_saveScanFilters();
|
||||
@@ -149,8 +153,10 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _applyFilterFromControllers() {
|
||||
void _applyFilterFromControllers() async {
|
||||
try {
|
||||
// Ensure company identifier service is loaded
|
||||
await CompanyIdentifierService.instance.load();
|
||||
List<String> serviceUUids = [];
|
||||
List<String> namePrefixes = [];
|
||||
List<ManufacturerDataFilter> manufacturerDataFilters = [];
|
||||
@@ -181,23 +187,15 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
// Parse Manufacturer Data
|
||||
String manufacturerDataText = manufacturerDataController.text;
|
||||
if (manufacturerDataText.isNotEmpty) {
|
||||
final companyService = CompanyIdentifierService.instance;
|
||||
List<String> manufacturerData = manufacturerDataText
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
for (String manufacturer in manufacturerData) {
|
||||
String trimmed = manufacturer.trim();
|
||||
int? companyIdentifier;
|
||||
if (trimmed.toLowerCase().startsWith('0x')) {
|
||||
companyIdentifier = int.tryParse(trimmed.substring(2), radix: 16);
|
||||
} else {
|
||||
if (trimmed.contains(RegExp(r'[a-fA-F]'))) {
|
||||
companyIdentifier = int.tryParse(trimmed, radix: 16);
|
||||
} else {
|
||||
companyIdentifier = int.tryParse(trimmed);
|
||||
}
|
||||
}
|
||||
final companyIdentifier =
|
||||
companyService.parseCompanyIdentifier(manufacturer);
|
||||
if (companyIdentifier == null) {
|
||||
// Skip invalid manufacturer data when loading from preferences
|
||||
continue;
|
||||
@@ -256,13 +254,48 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
return _bleDevices;
|
||||
}
|
||||
final filter = _searchFilterController.text.toLowerCase();
|
||||
final companyService = CompanyIdentifierService.instance;
|
||||
|
||||
// Try to parse the filter as a company ID (supports hex and decimal formats)
|
||||
final parsedCompanyId =
|
||||
companyService.parseCompanyIdentifier(_searchFilterController.text);
|
||||
|
||||
return _bleDevices.where((device) {
|
||||
final name = device.name?.toLowerCase() ?? '';
|
||||
final deviceId = device.deviceId.toLowerCase();
|
||||
final services = device.services.join(' ').toLowerCase();
|
||||
return name.contains(filter) ||
|
||||
|
||||
// Check if filter matches device name, ID, or services
|
||||
if (name.contains(filter) ||
|
||||
deviceId.contains(filter) ||
|
||||
services.contains(filter);
|
||||
services.contains(filter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if filter matches any company name or company ID from manufacturer data
|
||||
for (final manufacturerData in device.manufacturerDataList) {
|
||||
// Check company name match
|
||||
final companyName =
|
||||
companyService.getCompanyName(manufacturerData.companyId);
|
||||
if (companyName != null && companyName.toLowerCase().contains(filter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check company ID match (if filter was parsed as a company ID)
|
||||
if (parsedCompanyId != null &&
|
||||
manufacturerData.companyId == parsedCompanyId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also check if filter matches the hex representation of company ID
|
||||
final companyIdHex = manufacturerData.companyIdRadix16.toLowerCase();
|
||||
if (companyIdHex.contains(filter) ||
|
||||
companyIdHex.replaceAll('0x', '').contains(filter)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -334,17 +367,15 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
child: TextField(
|
||||
controller: _searchFilterController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search by name, ID, or services...',
|
||||
hintText: 'Search by name, ID, service, company name/ID',
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
suffixIcon: _searchFilterController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
size: 20,
|
||||
color:
|
||||
colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
@@ -356,16 +387,15 @@ class _ScannerScreenState extends State<ScannerScreen> {
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
import 'package:universal_ble_example/data/company_identifier_service.dart';
|
||||
|
||||
class ScanFilterWidget extends StatefulWidget {
|
||||
final void Function(ScanFilter? filter) onScanFilter;
|
||||
@@ -22,11 +23,13 @@ class ScanFilterWidget extends StatefulWidget {
|
||||
class _ScanFilterWidgetState extends State<ScanFilterWidget> {
|
||||
String? error;
|
||||
|
||||
void applyFilter() {
|
||||
void applyFilter() async {
|
||||
setState(() {
|
||||
error = null;
|
||||
});
|
||||
try {
|
||||
// Ensure company identifier service is loaded
|
||||
await CompanyIdentifierService.instance.load();
|
||||
List<String> serviceUUids = [];
|
||||
List<String> namePrefixes = [];
|
||||
List<ManufacturerDataFilter> manufacturerDataFilters = [];
|
||||
@@ -56,27 +59,18 @@ class _ScanFilterWidgetState extends State<ScanFilterWidget> {
|
||||
// Parse Manufacturer Data - handle both comma and newline separated
|
||||
String manufacturerDataText = widget.manufacturerDataController.text;
|
||||
if (manufacturerDataText.isNotEmpty) {
|
||||
final companyService = CompanyIdentifierService.instance;
|
||||
List<String> manufacturerData = manufacturerDataText
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
for (String manufacturer in manufacturerData) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
final companyIdentifier = companyService.parseCompanyIdentifier(manufacturer);
|
||||
|
||||
if (companyIdentifier == null) {
|
||||
throw Exception("Invalid Manufacturer Data $manufacturer");
|
||||
throw Exception(
|
||||
"Invalid Manufacturer Data or Company Name: $manufacturer");
|
||||
}
|
||||
manufacturerDataFilters.add(
|
||||
ManufacturerDataFilter(companyIdentifier: companyIdentifier));
|
||||
@@ -95,11 +89,15 @@ class _ScanFilterWidgetState extends State<ScanFilterWidget> {
|
||||
withManufacturerData: manufacturerDataFilters,
|
||||
),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Filters Applied")),
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Filters Applied")),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e.toString();
|
||||
@@ -228,8 +226,9 @@ class _ScanFilterWidgetState extends State<ScanFilterWidget> {
|
||||
title: "Manufacturer Company IDs",
|
||||
icon: Icons.business,
|
||||
controller: widget.manufacturerDataController,
|
||||
hintText: "e.g. 76,0x004C",
|
||||
helperText: "Company identifiers in decimal or hex format",
|
||||
hintText: "e.g. 76,0x004C,Apple, Inc.",
|
||||
helperText:
|
||||
"Company identifiers (decimal/hex) or company names",
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Action Buttons
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
import 'package:universal_ble_example/data/company_identifier_service.dart';
|
||||
import 'package:universal_ble_example/home/widgets/rssi_signal_indicator.dart';
|
||||
import 'package:universal_ble_example/widgets/company_info_widget.dart';
|
||||
|
||||
class ScannedItemWidget extends StatelessWidget {
|
||||
final BleDevice bleDevice;
|
||||
@@ -164,6 +166,9 @@ class ScannedItemWidget extends StatelessWidget {
|
||||
// Manufacturer data (only in collapsed mode)
|
||||
if (!isExpanded) ...[
|
||||
...rawManufacturerData.take(2).map((data) {
|
||||
final companyName = CompanyIdentifierService
|
||||
.instance
|
||||
.getCompanyName(data.companyId);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
@@ -173,13 +178,37 @@ class ScannedItemWidget extends StatelessWidget {
|
||||
color: colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
data.companyIdRadix16,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
data.companyIdRadix16,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color:
|
||||
colorScheme.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
if (companyName != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
companyName,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: colorScheme
|
||||
.onSecondaryContainer
|
||||
.withValues(alpha: 0.8),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -285,6 +314,22 @@ class ScannedItemWidget extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
CompanyInfoWidget(
|
||||
companyId: data.companyId,
|
||||
colorScheme: colorScheme,
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color:
|
||||
colorScheme.onSecondaryContainer,
|
||||
),
|
||||
nameStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color:
|
||||
colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
if (data.payloadRadix16.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:universal_ble_example/data/storage_service.dart';
|
||||
import 'package:universal_ble_example/peripheral_details/widgets/result_widget.dart';
|
||||
import 'package:universal_ble_example/peripheral_details/widgets/services_list_widget.dart';
|
||||
import 'package:universal_ble_example/peripheral_details/widgets/services_side_widget.dart';
|
||||
import 'package:universal_ble_example/widgets/company_info_widget.dart';
|
||||
import 'package:universal_ble_example/widgets/responsive_view.dart';
|
||||
|
||||
class PeripheralDetailPage extends StatefulWidget {
|
||||
@@ -1032,6 +1033,20 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
CompanyInfoWidget(
|
||||
companyId: data.companyId,
|
||||
colorScheme: colorScheme,
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
nameStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
if (data.payloadRadix16.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_ble_example/data/company_identifier_service.dart';
|
||||
|
||||
/// A reusable widget that displays company information for a given company ID.
|
||||
///
|
||||
/// This widget fetches the company name from the CompanyIdentifierService
|
||||
/// and displays it in a consistent format. If no company name is found,
|
||||
/// the widget returns an empty SizedBox.
|
||||
class CompanyInfoWidget extends StatelessWidget {
|
||||
/// The company ID to look up
|
||||
final int companyId;
|
||||
|
||||
/// Optional text style for the "Company:" label
|
||||
final TextStyle? labelStyle;
|
||||
|
||||
/// Optional text style for the company name
|
||||
final TextStyle? nameStyle;
|
||||
|
||||
/// Optional padding around the widget
|
||||
final EdgeInsets? padding;
|
||||
|
||||
/// Optional color scheme. If not provided, will be obtained from Theme
|
||||
final ColorScheme? colorScheme;
|
||||
|
||||
const CompanyInfoWidget({
|
||||
super.key,
|
||||
required this.companyId,
|
||||
this.labelStyle,
|
||||
this.nameStyle,
|
||||
this.padding,
|
||||
this.colorScheme,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final companyName = CompanyIdentifierService.instance.getCompanyName(companyId);
|
||||
|
||||
if (companyName == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final effectiveColorScheme = colorScheme ?? Theme.of(context).colorScheme;
|
||||
final effectiveLabelStyle = labelStyle ??
|
||||
TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: effectiveColorScheme.onSecondaryContainer,
|
||||
);
|
||||
final effectiveNameStyle = nameStyle ??
|
||||
TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: effectiveColorScheme.onSecondaryContainer,
|
||||
);
|
||||
final effectivePadding = padding ?? const EdgeInsets.only(top: 4);
|
||||
|
||||
return Padding(
|
||||
padding: effectivePadding,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Company: ',
|
||||
style: effectiveLabelStyle,
|
||||
),
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
companyName,
|
||||
style: effectiveNameStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -754,7 +754,7 @@ packages:
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
|
||||
@@ -16,6 +16,7 @@ dependencies:
|
||||
package_info_plus: ^9.0.0
|
||||
url_launcher: ^6.3.2
|
||||
path_provider: ^2.1.1
|
||||
yaml: ^3.1.3
|
||||
universal_ble:
|
||||
path: ../
|
||||
device_preview: ^1.3.1
|
||||
@@ -37,3 +38,4 @@ flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/icon.png
|
||||
- assets/company_identifiers.yaml
|
||||
|
||||
Reference in New Issue
Block a user