From 34531913afd01fd9676a8d5b97ae5f5b2daa477e Mon Sep 17 00:00:00 2001 From: Dennis <45356478+DennisMoschina@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:16:02 +0200 Subject: [PATCH 1/3] feat(labels): add persistent recording labels --- open_wearable/lib/main.dart | 39 ++- .../models/labels/file_label_set_storage.dart | 51 +++ open_wearable/lib/models/labels/label.dart | 39 +++ .../lib/models/labels/label_sensor.dart | 111 ++++++ .../lib/models/labels/label_set.dart | 42 +++ .../lib/models/labels/label_set_manager.dart | 66 ++++ .../lib/models/labels/label_set_storage.dart | 9 + .../models/labels/web_label_set_storage.dart | 38 +++ .../lib/view_models/label_provider.dart | 63 ++++ .../lib/view_models/label_set_provider.dart | 57 ++++ .../labels/active_label_bar.dart | 66 ++++ .../labels/label_set_selector.dart | 74 ++++ .../labels/label_sets_page.dart | 139 ++++++++ .../labels/labelset_dropdown.dart | 76 +++++ .../labels/labelset_editor_page.dart | 318 ++++++++++++++++++ open_wearable/pubspec.lock | 2 +- open_wearable/pubspec.yaml | 1 + 17 files changed, 1187 insertions(+), 4 deletions(-) create mode 100644 open_wearable/lib/models/labels/file_label_set_storage.dart create mode 100644 open_wearable/lib/models/labels/label.dart create mode 100644 open_wearable/lib/models/labels/label_sensor.dart create mode 100644 open_wearable/lib/models/labels/label_set.dart create mode 100644 open_wearable/lib/models/labels/label_set_manager.dart create mode 100644 open_wearable/lib/models/labels/label_set_storage.dart create mode 100644 open_wearable/lib/models/labels/web_label_set_storage.dart create mode 100644 open_wearable/lib/view_models/label_provider.dart create mode 100644 open_wearable/lib/view_models/label_set_provider.dart create mode 100644 open_wearable/lib/widgets/sensors/local_recorder/labels/active_label_bar.dart create mode 100644 open_wearable/lib/widgets/sensors/local_recorder/labels/label_set_selector.dart create mode 100644 open_wearable/lib/widgets/sensors/local_recorder/labels/label_sets_page.dart create mode 100644 open_wearable/lib/widgets/sensors/local_recorder/labels/labelset_dropdown.dart create mode 100644 open_wearable/lib/widgets/sensors/local_recorder/labels/labelset_editor_page.dart 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/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