diff --git a/open_wearable/lib/main.dart b/open_wearable/lib/main.dart index 7abab9c5..0b5979ec 100644 --- a/open_wearable/lib/main.dart +++ b/open_wearable/lib/main.dart @@ -14,6 +14,7 @@ import 'package:open_wearable/models/app_upgrade_coordinator.dart'; import 'package:open_wearable/models/app_upgrade_highlight.dart'; import 'package:open_wearable/models/auto_connect_preferences.dart'; import 'package:open_wearable/models/connector_settings.dart'; +import 'package:open_wearable/models/labels/label_sensor.dart'; import 'package:open_wearable/models/log_file_manager.dart'; import 'package:open_wearable/models/fota_post_update_verification.dart'; import 'package:open_wearable/models/permissions_helper.dart'; @@ -35,6 +36,8 @@ import 'package:open_wearable/widgets/onboarding/permissions_onboarding_page.dar import 'models/bluetooth_auto_connector.dart'; import 'models/logger.dart'; import 'view_models/app_banner_controller.dart'; +import 'view_models/label_provider.dart'; +import 'view_models/label_set_provider.dart'; import 'view_models/wearables_provider.dart'; void main() async { @@ -54,12 +57,42 @@ void main() async { ChangeNotifierProvider( create: (context) => FirmwareUpdateRequestProvider(), ), - ChangeNotifierProxyProvider( + ChangeNotifierProvider( + create: (context) => LabelSetProvider(), + ), + ChangeNotifierProxyProvider( + create: (context) => LabelProvider(null), + update: (context, labelSetProvider, labelProvider) { + labelProvider?.setLabelSet(labelSetProvider.selectedLabelSet); + return labelProvider!; + }, + ), + ChangeNotifierProxyProvider2( create: (context) => SensorRecorderProvider(), - update: (context, wearablesProvider, recorderProvider) { + update: ( + context, + wearablesProvider, + labelSetProvider, + recorderProvider, + ) { final provider = recorderProvider ?? SensorRecorderProvider(); + final labelSet = labelSetProvider.selectedLabelSet; + final wearables = [...wearablesProvider.wearables]; + if (labelSet != null) { + final labelProvider = context.read(); + wearables.add( + LabelWearable( + labelSet: labelSet, + labelStream: labelProvider.activeLabelStream, + ), + ); + } + logger.t( + 'Updating SensorRecorderProvider with label set: $labelSet', + ); provider.synchronizeConnectedWearables( - wearablesProvider.wearables, + wearables, ); return provider; }, diff --git a/open_wearable/lib/models/app_upgrade_registry.dart b/open_wearable/lib/models/app_upgrade_registry.dart index d5d04ab8..a72f88da 100644 --- a/open_wearable/lib/models/app_upgrade_registry.dart +++ b/open_wearable/lib/models/app_upgrade_registry.dart @@ -179,6 +179,44 @@ class AppUpgradeRegistry { ), ], ), + AppUpgradeHighlight( + version: '1.5.0', + eyebrow: 'OpenWearables 1.5.0', + title: 'Add labels while\nyou record', + summary: + 'Mark important moments in local recordings and find devices faster.', + heroDescription: + 'OpenWearables 1.5.0 brings reusable recording labels to the local recorder. ' + 'Create label sets, choose one before recording, and tap labels during a session so key events are saved with your sensor data.', + accentColor: Color(0xFF8F6A67), + useHeroGradient: false, + features: [ + AppUpgradeFeatureHighlight( + icon: Icons.label_rounded, + title: 'Recording labels', + description: + 'Create reusable label sets and mark events while recording. Label states are saved alongside sensor data with timestamps for easier review and analysis.', + ), + AppUpgradeFeatureHighlight( + icon: Icons.filter_alt_rounded, + title: 'Device name filter', + description: + 'Filter the available device list by name, starting with OpenEarable, so nearby Bluetooth devices are easier to scan and connect.', + ), + AppUpgradeFeatureHighlight( + icon: Icons.verified_rounded, + title: 'Smarter firmware matching', + description: + 'Firmware lists and post-update checks now better recognize installed versions, including beta and pull-request builds.', + ), + AppUpgradeFeatureHighlight( + icon: Icons.auto_fix_high_rounded, + title: 'Workflow polish', + description: + 'Small fixes make label-set editing, delete confirmations, selected dropdowns, color indicators, and update verification feel more predictable.', + ), + ], + ), ]; /// Returns the configured highlight for [version], if any. diff --git a/open_wearable/lib/models/labels/file_label_set_storage.dart b/open_wearable/lib/models/labels/file_label_set_storage.dart new file mode 100644 index 00000000..34613b67 --- /dev/null +++ b/open_wearable/lib/models/labels/file_label_set_storage.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +import 'label_set.dart'; +import 'label_set_storage.dart'; + +class FileLabelSetStorage implements LabelSetStorage { + FileLabelSetStorage({this.fileName = 'label_sets.json'}); + + final String fileName; + + Future _getFile() async { + final Directory dir = await getApplicationDocumentsDirectory(); + return File(p.join(dir.path, fileName)); + } + + @override + Future> loadLabelSets() async { + try { + final file = await _getFile(); + if (!await file.exists()) { + return []; + } + final content = await file.readAsString(); + if (content.trim().isEmpty) { + return []; + } + final jsonList = jsonDecode(content) as List; + return jsonList + .map((e) => LabelSet.fromJson(e as Map)) + .toList(); + } catch (e) { + // In case of any error, return empty list instead of crashing. + return []; + } + } + + @override + Future saveLabelSets(List sets) async { + final file = await _getFile(); + final jsonList = sets.map((s) => s.toJson()).toList(); + final content = jsonEncode(jsonList); + await file.writeAsString(content); + } +} + +/// Factory used by conditional import. +LabelSetStorage createLabelSetStorage() => FileLabelSetStorage(); diff --git a/open_wearable/lib/models/labels/label.dart b/open_wearable/lib/models/labels/label.dart new file mode 100644 index 00000000..90a06996 --- /dev/null +++ b/open_wearable/lib/models/labels/label.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +/// A single label with a name and a color. +class Label { + final String name; + final Color color; + + const Label({ + required this.name, + required this.color, + }); + + /// Create a modified copy (useful for editing). + Label copyWith({ + String? name, + Color? color, + }) { + return Label( + name: name ?? this.name, + color: color ?? this.color, + ); + } + + /// JSON -> Label + factory Label.fromJson(Map json) { + return Label( + name: json['name'] as String, + color: Color(int.parse(json['color'] as String)), + ); + } + + /// Label -> JSON + Map toJson() { + return { + 'name': name, + 'color': '0x${color.toARGB32().toRadixString(16)}', + }; + } +} diff --git a/open_wearable/lib/models/labels/label_sensor.dart b/open_wearable/lib/models/labels/label_sensor.dart new file mode 100644 index 00000000..b8f908e0 --- /dev/null +++ b/open_wearable/lib/models/labels/label_sensor.dart @@ -0,0 +1,111 @@ +import 'package:open_earable_flutter/open_earable_flutter.dart'; +import 'package:open_wearable/models/labels/label_set.dart'; + +import 'label.dart'; + +/// A wearable that provides labeling functionality. +/// +/// This is used as an Adapter for the RecorderProvider to record labels +/// alongside sensor data. +class LabelWearable extends Wearable implements SensorManager { + LabelWearable({ + required this.labelSet, + required Stream<(int, List