From 9cb676cfc17ad6ee911a18fc0cf02f1c28bdac7a Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Tue, 12 Dec 2023 21:37:30 +0100 Subject: [PATCH 001/104] Add Jump Height Test tab to apps --- open_earable/ios/Podfile.lock | 8 +------- open_earable/lib/apps_tab.dart | 11 +++++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/open_earable/ios/Podfile.lock b/open_earable/ios/Podfile.lock index ea196ab..67f9ad9 100644 --- a/open_earable/ios/Podfile.lock +++ b/open_earable/ios/Podfile.lock @@ -5,8 +5,6 @@ PODS: - three3d_egl (~> 0.1.3) - flutter_native_splash (0.0.1): - Flutter - - location (0.0.1): - - Flutter - open_file (0.0.1): - Flutter - path_provider_foundation (0.0.1): @@ -26,7 +24,6 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_gl (from `.symlinks/plugins/flutter_gl/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - - location (from `.symlinks/plugins/location/ios`) - open_file (from `.symlinks/plugins/open_file/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) @@ -45,8 +42,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_gl/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" - location: - :path: ".symlinks/plugins/location/ios" open_file: :path: ".symlinks/plugins/open_file/ios" path_provider_foundation: @@ -60,7 +55,6 @@ SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_gl: 5a5603f35db897697f064027864a32b15d0c421d flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef - location: d5cf8598915965547c3f36761ae9cc4f4e87d22e open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 @@ -71,4 +65,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189 -COCOAPODS: 1.11.3 +COCOAPODS: 1.14.3 diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 17e5a27..2787829 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -45,6 +45,17 @@ class AppsTab extends StatelessWidget { MaterialPageRoute( builder: (context) => Recorder(_openEarable))); }), + AppInfo( + iconData: Icons.arrow_upward, + title: "Jump Height Test", + description: "Test your maximum jump height.", + onTap: () { + Navigator.push( + context, + // TODO: Change PageRoute + MaterialPageRoute( + builder: (context) => Recorder(_openEarable))); + }), // ... similarly for other apps ]; } From 9265e593c3fe64673f8d13a4c8e221bc2c006937 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Tue, 12 Dec 2023 22:07:04 +0100 Subject: [PATCH 002/104] Change tab icon for Jump Height Test --- open_earable/lib/apps_tab.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 2787829..39ac81b 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; import 'package:open_earable/apps/recorder.dart'; +import 'package:open_earable/apps/jump_height_test.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class AppInfo { @@ -46,7 +47,7 @@ class AppsTab extends StatelessWidget { builder: (context) => Recorder(_openEarable))); }), AppInfo( - iconData: Icons.arrow_upward, + iconData: Icons.height, title: "Jump Height Test", description: "Test your maximum jump height.", onTap: () { @@ -54,7 +55,7 @@ class AppsTab extends StatelessWidget { context, // TODO: Change PageRoute MaterialPageRoute( - builder: (context) => Recorder(_openEarable))); + builder: (context) => JumpHeightTest(_openEarable))); }), // ... similarly for other apps ]; From 48f4d6b27eda9642221014c95388f6bdfa6f1365 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Thu, 21 Dec 2023 12:06:08 +0100 Subject: [PATCH 003/104] Implement basic functionality of Jump Height Test --- .../ios/Runner.xcodeproj/project.pbxproj | 18 +-- open_earable/ios/Runner/Info.plist | 119 ++++++++-------- open_earable/ios/Runner/Runner.entitlements | 5 +- .../ios/Runner/RunnerDebug.entitlements | 5 +- open_earable/lib/apps/jump_height_test.dart | 133 ++++++++++++++++++ 5 files changed, 201 insertions(+), 79 deletions(-) create mode 100644 open_earable/lib/apps/jump_height_test.dart diff --git a/open_earable/ios/Runner.xcodeproj/project.pbxproj b/open_earable/ios/Runner.xcodeproj/project.pbxproj index d696e14..dc894e3 100644 --- a/open_earable/ios/Runner.xcodeproj/project.pbxproj +++ b/open_earable/ios/Runner.xcodeproj/project.pbxproj @@ -480,7 +480,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6DCQ69GP5G; + DEVELOPMENT_TEAM = Z74Z97JQJL; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenEarable; @@ -490,7 +490,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = edu.kit.teco.openEarable; + PRODUCT_BUNDLE_IDENTIFIER = "jump-height-test"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -505,7 +505,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6DCQ69GP5G; + DEVELOPMENT_TEAM = Z74Z97JQJL; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -525,7 +525,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6DCQ69GP5G; + DEVELOPMENT_TEAM = Z74Z97JQJL; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -543,7 +543,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6DCQ69GP5G; + DEVELOPMENT_TEAM = Z74Z97JQJL; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -669,7 +669,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6DCQ69GP5G; + DEVELOPMENT_TEAM = Z74Z97JQJL; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenEarable; @@ -679,7 +679,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = edu.kit.teco.openEarable; + PRODUCT_BUNDLE_IDENTIFIER = "jump-height-test"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -696,7 +696,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 6DCQ69GP5G; + DEVELOPMENT_TEAM = Z74Z97JQJL; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenEarable; @@ -706,7 +706,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = edu.kit.teco.openEarable; + PRODUCT_BUNDLE_IDENTIFIER = "jump-height-test"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/open_earable/ios/Runner/Info.plist b/open_earable/ios/Runner/Info.plist index f5e62dd..18d65a8 100644 --- a/open_earable/ios/Runner/Info.plist +++ b/open_earable/ios/Runner/Info.plist @@ -1,66 +1,61 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - OpenEarable - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - OpenEarable - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - ITSAppUsesNonExemptEncryption - - LSApplicationCategoryType - aps-environment - LSRequiresIPhoneOS - - NSBluetoothAlwaysUsageDescription - This app uses bluetooth to connect to earable devices - NSBluetoothPeripheralUsageDescription - This app uses bluetooth to connect to earable devices - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - fetch - remote-notification - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - UIStatusBarHidden - - + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + OpenEarable + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + OpenEarable + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + + LSApplicationCategoryType + aps-environment + LSRequiresIPhoneOS + + NSBluetoothAlwaysUsageDescription + This app uses bluetooth to connect to earable devices + NSBluetoothPeripheralUsageDescription + This app uses bluetooth to connect to earable devices + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + diff --git a/open_earable/ios/Runner/Runner.entitlements b/open_earable/ios/Runner/Runner.entitlements index 903def2..0c67376 100644 --- a/open_earable/ios/Runner/Runner.entitlements +++ b/open_earable/ios/Runner/Runner.entitlements @@ -1,8 +1,5 @@ - - aps-environment - development - + diff --git a/open_earable/ios/Runner/RunnerDebug.entitlements b/open_earable/ios/Runner/RunnerDebug.entitlements index 903def2..0c67376 100644 --- a/open_earable/ios/Runner/RunnerDebug.entitlements +++ b/open_earable/ios/Runner/RunnerDebug.entitlements @@ -1,8 +1,5 @@ - - aps-environment - development - + diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test.dart new file mode 100644 index 0000000..addb832 --- /dev/null +++ b/open_earable/lib/apps/jump_height_test.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; + +class JumpHeightTest extends StatefulWidget { + final OpenEarable _openEarable; + JumpHeightTest(this._openEarable); + @override + _JumpHeightTestState createState() => _JumpHeightTestState(_openEarable); +} + +class _JumpHeightTestState extends State { + DateTime? _startTime; + double _jumpHeight = 0.0; + bool _isJumping = false; + final OpenEarable _openEarable; + StreamSubscription? _imuSubscription; + _JumpHeightTestState(this._openEarable); + double _maxJumpHeight = 0.0; // Variable to keep track of maximum jump height + + + @override + void initState() { + super.initState(); + if (_openEarable.bleManager.connected) { + _setupListeners(); + } + } + + List _accelerations = []; // Store relative accelerations + double _lambda = 1.4; // Correction factor, adjust as needed + + _setupListeners() { + _imuSubscription = + _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { + double currentAcc = double.parse(data["ACC"]["Y"].toString()); + + if (_accelerations.isNotEmpty) { + double relativeAcc = currentAcc - _accelerations.last; + _accelerations.add(relativeAcc); + } else { + _accelerations.add(currentAcc); + } + }); + } + + _calculateJumpHeight() { + double height = 0.0; + // TODO: timeSlice = 1 / samplingRate + double timeSlice = 0.04; // Ensure this matches your data sampling rate + + print("Acc length: ${_accelerations.length}"); // Debug log + for (int i = 0; i < _accelerations.length; i++) { + height += 0.5 * _accelerations[i] * timeSlice * timeSlice; + } + height *= _lambda; + + print("Calculated Height: $height"); // Debug log + + if (height > _maxJumpHeight) { + _maxJumpHeight = height; // Update max height if current height is greater + } + + setState(() { + _jumpHeight = height; + }); + } + + + @override + void dispose() { + super.dispose(); + _imuSubscription?.cancel(); + } + + void _startJump() { + _startTime = DateTime.now(); + setState(() { + _isJumping = true; + _maxJumpHeight = 0.0; // Reset max height on starting a new jump + }); + // Set sampling rate to maximum + _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); + } + + void _stopJump() { + if (_isJumping) { + // Calculate final jump height + _calculateJumpHeight(); + + // Resetting the state for the next jump + _accelerations.clear(); + setState(() { + _isJumping = false; + }); + } + // Here, _maxJumpHeight holds the maximum height reached during the jump + print("Maximum Jump Height: $_maxJumpHeight meters"); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Jump Height Test'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Jump Height: ${_jumpHeight.toStringAsFixed(2)} meters', + style: Theme.of(context).textTheme.headlineMedium, + ), + SizedBox(height: 20), + ElevatedButton( + onPressed: _isJumping ? _stopJump : _startJump, + child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), + ), + ], + ), + ), + ); + } + + OpenEarableSensorConfig _buildSensorConfig() { + return OpenEarableSensorConfig( + sensorId: 0, + samplingRate: 30, + latency: 0, + ); + } +} \ No newline at end of file From 1c320200777f99d3b2283350ed07942133794d65 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sat, 23 Dec 2023 22:18:23 +0100 Subject: [PATCH 004/104] Add line chart for jump height data, add comments and make small changes --- open_earable/lib/apps/jump_height_test.dart | 226 ++++++++++++++++---- 1 file changed, 183 insertions(+), 43 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test.dart index addb832..84153df 100644 --- a/open_earable/lib/apps/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test.dart @@ -1,105 +1,226 @@ import 'package:flutter/material.dart'; import 'dart:async'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:simple_kalman/simple_kalman.dart'; +import 'dart:math'; +/// An app that lets you test your jump height using an OpenEarable device. class JumpHeightTest extends StatefulWidget { final OpenEarable _openEarable; + + /// Constructs a JumpHeightTest widget with a given OpenEarable device. JumpHeightTest(this._openEarable); + @override _JumpHeightTestState createState() => _JumpHeightTestState(_openEarable); } +/// A class representing a jump with a time and height. +class Jump { + final DateTime _time; + final double _height; + + /// Constructs a Jump object with a time and height. + Jump(this._time, this._height); +} + +/// A stateless widget to display jump heights in a bar chart. +class HeightChart extends StatelessWidget { + final List _seriesList; + final bool _animate; + + /// Constructs a HeightChart widget with given series list and animate flag. + HeightChart(this._seriesList, {required bool animate}) : _animate = animate; + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + _seriesList.cast>(), + animate: _animate, + ); + } +} + +/// State class for JumpHeightTest widget. class _JumpHeightTestState extends State { + /// Stores the start time of a jump test. DateTime? _startTime; - double _jumpHeight = 0.0; + /// Current height calculated from sensor data. + double _height = 0.0; + // List to store each jump's data. + List _jumpData = []; + // Flag to indicate if jump measurement is ongoing. bool _isJumping = false; + /// Instance of OpenEarable device. final OpenEarable _openEarable; + /// Subscription to IMU sensor data. StreamSubscription? _imuSubscription; - _JumpHeightTestState(this._openEarable); - double _maxJumpHeight = 0.0; // Variable to keep track of maximum jump height + /// Stores the maximum height achieved in a jump. + double _maxHeight = 0.0; // Variable to keep track of maximum jump height + /// Error measure for Kalman filter. + final _errorMeasureAcc = 5.0; + /// Kalman filters for accelerometer data. + late SimpleKalman _kalmanX, _kalmanY, _kalmanZ; + /// Current velocity calculated from acceleration. + double _velocity = 0.0; + /// Sampling rate time slice (inverse of frequency). + double _timeSlice = 1 / 30.0; + /// Standard gravity in m/s^2. + double _gravity = 9.81; + /// X-axis acceleration. + double _accX = 0.0; + /// Y-axis acceleration. + double _accY = 0.0; + /// Z-axis acceleration. + double _accZ = 0.0; + /// Pitch angle in radians. + double _pitch = 0.0; + /// Constructs a _JumpHeightTestState object with a given OpenEarable device. + _JumpHeightTestState(this._openEarable); @override void initState() { super.initState(); + // Initialize Kalman filters. + _initializeKalmanFilters(); + // Set up listeners for sensor data. if (_openEarable.bleManager.connected) { _setupListeners(); } } - List _accelerations = []; // Store relative accelerations - double _lambda = 1.4; // Correction factor, adjust as needed + /// Initializes Kalman filters for accelerometer data. + void _initializeKalmanFilters() { + _kalmanX = SimpleKalman( + errorMeasure: _errorMeasureAcc, + errorEstimate: _errorMeasureAcc, + q: 0.9); + _kalmanY = SimpleKalman( + errorMeasure: _errorMeasureAcc, + errorEstimate: _errorMeasureAcc, + q: 0.9); + _kalmanZ = SimpleKalman( + errorMeasure: _errorMeasureAcc, + errorEstimate: _errorMeasureAcc, + q: 0.9); + } + /// Sets up listeners to receive sensor data from the OpenEarable device. _setupListeners() { _imuSubscription = _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { - double currentAcc = double.parse(data["ACC"]["Y"].toString()); - - if (_accelerations.isNotEmpty) { - double relativeAcc = currentAcc - _accelerations.last; - _accelerations.add(relativeAcc); - } else { - _accelerations.add(currentAcc); - } - }); + // Only process sensor data if jump measurement is ongoing. + if (!_isJumping) { + return; + } + _processSensorData(data); + }); } - _calculateJumpHeight() { - double height = 0.0; - // TODO: timeSlice = 1 / samplingRate - double timeSlice = 0.04; // Ensure this matches your data sampling rate + /// Processes incoming sensor data and updates jump height. + void _processSensorData(Map data) { + /// Kalman filtered accelerometer data for X. + _accX = _kalmanX.filtered(data["ACC"]["X"]); + /// Kalman filtered accelerometer data for Y. + _accY = _kalmanY.filtered(data["ACC"]["Y"]); + /// Kalman filtered accelerometer data for Z. + _accZ = _kalmanZ.filtered(data["ACC"]["Z"]); + /// Pitch angle in radians. + _pitch = data["EULER"]["PITCH"]; + // Calculates the current vertical acceleration. + // It adjusts the Z-axis acceleration with the pitch angle to account for the device's orientation. + double currentAcc = _accZ * cos(_pitch) + _accX * sin(_pitch); + // Subtract gravity to get acceleration due to movement. + currentAcc -= _gravity; - print("Acc length: ${_accelerations.length}"); // Debug log - for (int i = 0; i < _accelerations.length; i++) { - height += 0.5 * _accelerations[i] * timeSlice * timeSlice; - } - height *= _lambda; - - print("Calculated Height: $height"); // Debug log + _updateHeight(currentAcc); + } - if (height > _maxJumpHeight) { - _maxJumpHeight = height; // Update max height if current height is greater - } + /// Checks if the device is stationary based on acceleration magnitude. + bool _deviceIsStationary(double threshold) { + double accMagnitude = sqrt(_accX * _accX + _accY * _accY + _accZ * _accZ); + bool isStationary = (accMagnitude > _gravity - threshold) && (accMagnitude < _gravity + threshold); + return isStationary; + } + /// Updates the current height based on the current acceleration. + /// If the device is stationary, the velocity is reset to 0. + /// Otherwise, it integrates the current acceleration to update velocity and height. + _updateHeight(double currentAcc) { setState(() { - _jumpHeight = height; + if (_deviceIsStationary(0.3)) { + _velocity = 0.0; + } else { + // Integrate acceleration to get velocity. + _velocity += currentAcc * _timeSlice; + + // Integrate velocity to get height. + _height += _velocity * _timeSlice; + } + + // Prevent height from going negative. + _height = max(0, _height); + + // Update maximum height if the current height is greater. + if (_height > _maxHeight) { + _maxHeight = _height; + } + + _jumpData.add(Jump(DateTime.now(), _height)); }); + // For debugging. + // print("Stationary: ${deviceIsStationary(0.3)}, Acc: $currentAcc, Vel: $velocity, Height: $height"); } - @override void dispose() { super.dispose(); _imuSubscription?.cancel(); } + /// Starts the jump height measurement process. + /// It sets the sampling rate, initializes or resets variables, and begins listening to sensor data. void _startJump() { + // Set sampling rate to maximum. + _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); _startTime = DateTime.now(); + setState(() { + // Clear data from previous jump. + _jumpData.clear(); _isJumping = true; - _maxJumpHeight = 0.0; // Reset max height on starting a new jump + _height = 0.0; + _velocity = 0.0; + // Reset max height on starting a new jump + _maxHeight = 0.0; }); - // Set sampling rate to maximum - _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); } + /// Stops the jump height measurement process. void _stopJump() { if (_isJumping) { - // Calculate final jump height - _calculateJumpHeight(); - - // Resetting the state for the next jump - _accelerations.clear(); setState(() { _isJumping = false; }); } - // Here, _maxJumpHeight holds the maximum height reached during the jump - print("Maximum Jump Height: $_maxJumpHeight meters"); } + /// Builds the UI for the jump height test. + /// It displays a line chart of jump height over time and the maximum jump height achieved. @override Widget build(BuildContext context) { + List> jumpDataSeries = [ + charts.Series( + id: "Jumps", + data: _jumpData, + // X-axis: time in milliseconds since the start of the jump. + domainFn: (Jump series, _) => series._time.difference(_startTime!).inMilliseconds, + measureFn: (Jump series, _) => series._height, + colorFn: (Jump series, _) => charts.MaterialPalette.cyan.shadeDefault, + ) + ]; return Scaffold( appBar: AppBar( title: Text('Jump Height Test'), @@ -108,11 +229,28 @@ class _JumpHeightTestState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + Expanded( + child: charts.LineChart( + jumpDataSeries, + animate: false, + behaviors: [ + new charts.ChartTitle('Time (ms)', + behaviorPosition: charts.BehaviorPosition.bottom, + titleStyleSpec: charts.TextStyleSpec(color: charts.MaterialPalette.white), + titleOutsideJustification: charts.OutsideJustification.middleDrawArea), + new charts.ChartTitle('Height (m)', + behaviorPosition: charts.BehaviorPosition.start, + titleStyleSpec: charts.TextStyleSpec(color: charts.MaterialPalette.white), + titleOutsideJustification: charts.OutsideJustification.middleDrawArea) + ], + // Include timeline points in line. + defaultRenderer: charts.LineRendererConfig(includePoints: true), + ), + ), Text( - 'Jump Height: ${_jumpHeight.toStringAsFixed(2)} meters', + 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', style: Theme.of(context).textTheme.headlineMedium, ), - SizedBox(height: 20), ElevatedButton( onPressed: _isJumping ? _stopJump : _startJump, child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), @@ -123,6 +261,8 @@ class _JumpHeightTestState extends State { ); } + /// Builds a sensor configuration for the OpenEarable device. + /// Sets the sensor ID, sampling rate, and latency. OpenEarableSensorConfig _buildSensorConfig() { return OpenEarableSensorConfig( sensorId: 0, @@ -130,4 +270,4 @@ class _JumpHeightTestState extends State { latency: 0, ); } -} \ No newline at end of file +} From 0bbf296be63baca8393d62094b2088d6e33afc78 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sat, 23 Dec 2023 22:44:26 +0100 Subject: [PATCH 005/104] Add error message if no OpenEarable device is connected and change font size of axes labels --- open_earable/lib/apps/jump_height_test.dart | 43 ++++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test.dart index 84153df..32f16f3 100644 --- a/open_earable/lib/apps/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test.dart @@ -54,6 +54,8 @@ class _JumpHeightTestState extends State { bool _isJumping = false; /// Instance of OpenEarable device. final OpenEarable _openEarable; + /// Flag to indicate if an OpenEarable device is connected. + bool _earableConnected = false; /// Subscription to IMU sensor data. StreamSubscription? _imuSubscription; /// Stores the maximum height achieved in a jump. @@ -88,6 +90,7 @@ class _JumpHeightTestState extends State { // Set up listeners for sensor data. if (_openEarable.bleManager.connected) { _setupListeners(); + _earableConnected = true; } } @@ -234,13 +237,21 @@ class _JumpHeightTestState extends State { jumpDataSeries, animate: false, behaviors: [ + // X-axis label. new charts.ChartTitle('Time (ms)', behaviorPosition: charts.BehaviorPosition.bottom, - titleStyleSpec: charts.TextStyleSpec(color: charts.MaterialPalette.white), + titleStyleSpec: charts.TextStyleSpec( + color: charts.MaterialPalette.white, + fontSize: 10 + ), titleOutsideJustification: charts.OutsideJustification.middleDrawArea), + // Y-axis label. new charts.ChartTitle('Height (m)', behaviorPosition: charts.BehaviorPosition.start, - titleStyleSpec: charts.TextStyleSpec(color: charts.MaterialPalette.white), + titleStyleSpec: charts.TextStyleSpec( + color: charts.MaterialPalette.white, + fontSize: 10 + ), titleOutsideJustification: charts.OutsideJustification.middleDrawArea) ], // Include timeline points in line. @@ -251,10 +262,30 @@ class _JumpHeightTestState extends State { 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', style: Theme.of(context).textTheme.headlineMedium, ), - ElevatedButton( - onPressed: _isJumping ? _stopJump : _startJump, - child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), - ), + Column(children: [ + ElevatedButton( + onPressed: _earableConnected ? () { _isJumping ? _stopJump() : _startJump(); } : null, + style: ElevatedButton.styleFrom( + backgroundColor: !_isJumping ? Colors.greenAccent : Colors.red, + foregroundColor: Colors.black, + ), + child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), + ), + Visibility( + // Show error message if no OpenEarable device is connected. + visible: !_earableConnected, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: Text( + "No Earable Connected", + style: TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ) + ]) ], ), ), From 6354eb986d07461723999afbc85b77c3e3803fe5 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sat, 23 Dec 2023 22:51:36 +0100 Subject: [PATCH 006/104] Reorganize code structure --- open_earable/lib/apps/jump_height_test.dart | 92 +++++++++++---------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test.dart index 32f16f3..52222f6 100644 --- a/open_earable/lib/apps/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test.dart @@ -82,6 +82,7 @@ class _JumpHeightTestState extends State { /// Constructs a _JumpHeightTestState object with a given OpenEarable device. _JumpHeightTestState(this._openEarable); + /// Initializes state and sets up listeners for sensor data. @override void initState() { super.initState(); @@ -94,6 +95,52 @@ class _JumpHeightTestState extends State { } } + /// Disposes IMU data subscription when the state object is removed. + @override + void dispose() { + super.dispose(); + _imuSubscription?.cancel(); + } + + /// Sets up listeners to receive sensor data from the OpenEarable device. + _setupListeners() { + _imuSubscription = + _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { + // Only process sensor data if jump measurement is ongoing. + if (!_isJumping) { + return; + } + _processSensorData(data); + }); + } + + /// Starts the jump height measurement process. + /// It sets the sampling rate, initializes or resets variables, and begins listening to sensor data. + void _startJump() { + // Set sampling rate to maximum. + _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); + _startTime = DateTime.now(); + + setState(() { + // Clear data from previous jump. + _jumpData.clear(); + _isJumping = true; + _height = 0.0; + _velocity = 0.0; + // Reset max height on starting a new jump + _maxHeight = 0.0; + }); + } + + /// Stops the jump height measurement process. + void _stopJump() { + if (_isJumping) { + setState(() { + _isJumping = false; + }); + } + } + /// Initializes Kalman filters for accelerometer data. void _initializeKalmanFilters() { _kalmanX = SimpleKalman( @@ -110,18 +157,6 @@ class _JumpHeightTestState extends State { q: 0.9); } - /// Sets up listeners to receive sensor data from the OpenEarable device. - _setupListeners() { - _imuSubscription = - _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { - // Only process sensor data if jump measurement is ongoing. - if (!_isJumping) { - return; - } - _processSensorData(data); - }); - } - /// Processes incoming sensor data and updates jump height. void _processSensorData(Map data) { /// Kalman filtered accelerometer data for X. @@ -177,39 +212,6 @@ class _JumpHeightTestState extends State { // print("Stationary: ${deviceIsStationary(0.3)}, Acc: $currentAcc, Vel: $velocity, Height: $height"); } - @override - void dispose() { - super.dispose(); - _imuSubscription?.cancel(); - } - - /// Starts the jump height measurement process. - /// It sets the sampling rate, initializes or resets variables, and begins listening to sensor data. - void _startJump() { - // Set sampling rate to maximum. - _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); - _startTime = DateTime.now(); - - setState(() { - // Clear data from previous jump. - _jumpData.clear(); - _isJumping = true; - _height = 0.0; - _velocity = 0.0; - // Reset max height on starting a new jump - _maxHeight = 0.0; - }); - } - - /// Stops the jump height measurement process. - void _stopJump() { - if (_isJumping) { - setState(() { - _isJumping = false; - }); - } - } - /// Builds the UI for the jump height test. /// It displays a line chart of jump height over time and the maximum jump height achieved. @override From 64ae36a3692861516256fefa7e1af50a47e490e2 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sat, 23 Dec 2023 23:08:24 +0100 Subject: [PATCH 007/104] Refactor the build method into more modular parts --- open_earable/lib/apps/jump_height_test.dart | 144 +++++++++++--------- 1 file changed, 81 insertions(+), 63 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test.dart index 52222f6..301e578 100644 --- a/open_earable/lib/apps/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test.dart @@ -214,8 +214,28 @@ class _JumpHeightTestState extends State { /// Builds the UI for the jump height test. /// It displays a line chart of jump height over time and the maximum jump height achieved. + // This build function is getting a little too big. Consider refactoring. @override Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Jump Height Test'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildChart(), + _buildText(), + _buildButtons(), + ], + ), + ), + ); + } + + /// Builds a line chart to display jump height over time. + Widget _buildChart() { List> jumpDataSeries = [ charts.Series( id: "Jumps", @@ -226,74 +246,72 @@ class _JumpHeightTestState extends State { colorFn: (Jump series, _) => charts.MaterialPalette.cyan.shadeDefault, ) ]; - return Scaffold( - appBar: AppBar( - title: Text('Jump Height Test'), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: charts.LineChart( - jumpDataSeries, - animate: false, - behaviors: [ - // X-axis label. - new charts.ChartTitle('Time (ms)', - behaviorPosition: charts.BehaviorPosition.bottom, - titleStyleSpec: charts.TextStyleSpec( - color: charts.MaterialPalette.white, - fontSize: 10 - ), - titleOutsideJustification: charts.OutsideJustification.middleDrawArea), - // Y-axis label. - new charts.ChartTitle('Height (m)', - behaviorPosition: charts.BehaviorPosition.start, - titleStyleSpec: charts.TextStyleSpec( - color: charts.MaterialPalette.white, - fontSize: 10 - ), - titleOutsideJustification: charts.OutsideJustification.middleDrawArea) - ], - // Include timeline points in line. - defaultRenderer: charts.LineRendererConfig(includePoints: true), - ), - ), - Text( - 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', - style: Theme.of(context).textTheme.headlineMedium, - ), - Column(children: [ - ElevatedButton( - onPressed: _earableConnected ? () { _isJumping ? _stopJump() : _startJump(); } : null, - style: ElevatedButton.styleFrom( - backgroundColor: !_isJumping ? Colors.greenAccent : Colors.red, - foregroundColor: Colors.black, - ), - child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), + + return Expanded( + child: charts.LineChart( + jumpDataSeries, + animate: false, + behaviors: [ + // X-axis label. + new charts.ChartTitle('Time (ms)', + behaviorPosition: charts.BehaviorPosition.bottom, + titleStyleSpec: charts.TextStyleSpec( + color: charts.MaterialPalette.white, + fontSize: 10 ), - Visibility( - // Show error message if no OpenEarable device is connected. - visible: !_earableConnected, - maintainState: true, - maintainAnimation: true, - maintainSize: true, - child: Text( - "No Earable Connected", - style: TextStyle( - color: Colors.red, - fontSize: 12, - ), - ), - ) - ]) - ], - ), + titleOutsideJustification: charts.OutsideJustification.middleDrawArea), + // Y-axis label. + new charts.ChartTitle('Height (m)', + behaviorPosition: charts.BehaviorPosition.start, + titleStyleSpec: charts.TextStyleSpec( + color: charts.MaterialPalette.white, + fontSize: 10 + ), + titleOutsideJustification: charts.OutsideJustification.middleDrawArea) + ], + // Include timeline points in line. + defaultRenderer: charts.LineRendererConfig(includePoints: true), ), ); } + /// Builds a text widget to display the maximum jump height achieved. + Widget _buildText() { + return Text( + 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', + style: + Theme.of(context).textTheme.headlineMedium + ); + } + + /// Builds buttons to start and stop the jump height measurement process. + Widget _buildButtons() { + return Column(children: [ + ElevatedButton( + onPressed: _earableConnected ? () { _isJumping ? _stopJump() : _startJump(); } : null, + style: ElevatedButton.styleFrom( + backgroundColor: !_isJumping ? Colors.greenAccent : Colors.red, + foregroundColor: Colors.black, + ), + child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), + ), + Visibility( + // Show error message if no OpenEarable device is connected. + visible: !_earableConnected, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: Text( + "No Earable Connected", + style: TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ) + ]); + } + /// Builds a sensor configuration for the OpenEarable device. /// Sets the sensor ID, sampling rate, and latency. OpenEarableSensorConfig _buildSensorConfig() { From 2ba4691d17eeae731ab2a81652d6c28410495f16 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sat, 23 Dec 2023 23:19:58 +0100 Subject: [PATCH 008/104] Slightly change widget structure --- open_earable/lib/apps/jump_height_test.dart | 98 +++++++++++---------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test.dart index 301e578..5ccfc57 100644 --- a/open_earable/lib/apps/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test.dart @@ -228,6 +228,19 @@ class _JumpHeightTestState extends State { _buildChart(), _buildText(), _buildButtons(), + Visibility( + // Show error message if no OpenEarable device is connected. + visible: !_earableConnected, + maintainState: true, + maintainAnimation: true, + child: Text( + "No Earable Connected", + style: TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ), ], ), ), @@ -248,70 +261,63 @@ class _JumpHeightTestState extends State { ]; return Expanded( - child: charts.LineChart( - jumpDataSeries, - animate: false, - behaviors: [ - // X-axis label. - new charts.ChartTitle('Time (ms)', - behaviorPosition: charts.BehaviorPosition.bottom, - titleStyleSpec: charts.TextStyleSpec( - color: charts.MaterialPalette.white, - fontSize: 10 - ), - titleOutsideJustification: charts.OutsideJustification.middleDrawArea), - // Y-axis label. - new charts.ChartTitle('Height (m)', - behaviorPosition: charts.BehaviorPosition.start, - titleStyleSpec: charts.TextStyleSpec( - color: charts.MaterialPalette.white, - fontSize: 10 - ), - titleOutsideJustification: charts.OutsideJustification.middleDrawArea) - ], - // Include timeline points in line. - defaultRenderer: charts.LineRendererConfig(includePoints: true), + child: Container( + child: charts.LineChart( + jumpDataSeries, + animate: false, + behaviors: [ + // X-axis label. + charts.ChartTitle('Time (ms)', + behaviorPosition: charts.BehaviorPosition.bottom, + titleStyleSpec: charts.TextStyleSpec( + color: charts.MaterialPalette.white, + fontSize: 10, + ), + titleOutsideJustification: charts.OutsideJustification.middleDrawArea), + // Y-axis label. + charts.ChartTitle('Height (m)', + behaviorPosition: charts.BehaviorPosition.start, + titleStyleSpec: charts.TextStyleSpec( + color: charts.MaterialPalette.white, + fontSize: 10, + ), + titleOutsideJustification: charts.OutsideJustification.middleDrawArea), + ], + // Include timeline points in line. + defaultRenderer: charts.LineRendererConfig(includePoints: true), + ), ), ); } /// Builds a text widget to display the maximum jump height achieved. Widget _buildText() { - return Text( - 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', - style: - Theme.of(context).textTheme.headlineMedium + return Container( + child: Text( + 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', + style: Theme.of(context).textTheme.headlineSmall, + ), ); } /// Builds buttons to start and stop the jump height measurement process. Widget _buildButtons() { - return Column(children: [ - ElevatedButton( - onPressed: _earableConnected ? () { _isJumping ? _stopJump() : _startJump(); } : null, + return Flexible( + child: ElevatedButton( + onPressed: _earableConnected + ? () { + _isJumping ? _stopJump() : _startJump(); + } + : null, style: ElevatedButton.styleFrom( backgroundColor: !_isJumping ? Colors.greenAccent : Colors.red, foregroundColor: Colors.black, ), child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), ), - Visibility( - // Show error message if no OpenEarable device is connected. - visible: !_earableConnected, - maintainState: true, - maintainAnimation: true, - maintainSize: true, - child: Text( - "No Earable Connected", - style: TextStyle( - color: Colors.red, - fontSize: 12, - ), - ), - ) - ]); + ); } - + /// Builds a sensor configuration for the OpenEarable device. /// Sets the sensor ID, sampling rate, and latency. OpenEarableSensorConfig _buildSensorConfig() { From 395f714576ca3030df2673477fb2f393774f7dbe Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sun, 24 Dec 2023 15:37:58 +0100 Subject: [PATCH 009/104] Remove TODO --- open_earable/lib/apps_tab.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 39ac81b..85dec37 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; import 'package:open_earable/apps/recorder.dart'; -import 'package:open_earable/apps/jump_height_test.dart'; +import 'package:open_earable/apps/jump_height_test/jump_height_test.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class AppInfo { @@ -53,7 +53,6 @@ class AppsTab extends StatelessWidget { onTap: () { Navigator.push( context, - // TODO: Change PageRoute MaterialPageRoute( builder: (context) => JumpHeightTest(_openEarable))); }), From 5956f04bb917cbfde5526f2c2fc036fc67216cbc Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sun, 24 Dec 2023 15:38:22 +0100 Subject: [PATCH 010/104] Add charts for raw acc. and filtered acc. in real-time --- .../jump_height_test/jump_height_chart.dart | 318 ++++++++++++++++++ .../jump_height_data_tab.dart | 99 ++++++ .../jump_height_test.dart | 87 ++++- 3 files changed, 489 insertions(+), 15 deletions(-) create mode 100644 open_earable/lib/apps/jump_height_test/jump_height_chart.dart create mode 100644 open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart rename open_earable/lib/apps/{ => jump_height_test}/jump_height_test.dart (82%) diff --git a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart new file mode 100644 index 0000000..6caad19 --- /dev/null +++ b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart @@ -0,0 +1,318 @@ +import 'dart:async'; + +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:simple_kalman/simple_kalman.dart'; +import 'package:collection/collection.dart'; +import 'dart:math'; +import 'dart:core'; + +class JumpHeightChart extends StatefulWidget { + final OpenEarable _openEarable; + final String _title; + JumpHeightChart(this._openEarable, this._title); + @override + _JumpHeightChartState createState() => + _JumpHeightChartState(_openEarable, _title); +} + +class _JumpHeightChartState extends State { + final OpenEarable _openEarable; + final String _title; + late List _data; + StreamSubscription? _dataSubscription; + _JumpHeightChartState(this._openEarable, this._title); + late int _minX = 0; + late int _maxX = 0; + late List colors; + List> seriesList = []; + late double _minY; + late double _maxY; + final _errorMeasureAcc = 5.0; + late SimpleKalman _kalmanX, _kalmanY, _kalmanZ; + int _numDatapoints = 200; + + double _velocity = 0.0; + /// Sampling rate time slice (inverse of frequency). + double _timeSlice = 1 / 30.0; + /// Standard gravity in m/s^2. + double _gravity = 9.81; + /// Pitch angle in radians. + double _pitch = 0.0; + double _height = 0.0; + + _setupListeners() { + _kalmanX = SimpleKalman( + errorMeasure: _errorMeasureAcc, + errorEstimate: _errorMeasureAcc, + q: 0.9); + _kalmanY = SimpleKalman( + errorMeasure: _errorMeasureAcc, + errorEstimate: _errorMeasureAcc, + q: 0.9); + _kalmanZ = SimpleKalman( + errorMeasure: _errorMeasureAcc, + errorEstimate: _errorMeasureAcc, + q: 0.9); + _dataSubscription = + _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { + int timestamp = data["timestamp"]; + XYZValue rawAccelerometerValue = XYZValue( + timestamp: timestamp, + x: data["ACC"]["X"], + y: data["ACC"]["Y"], + z: data["ACC"]["Z"], + units: data["ACC"]["units"] + ); + XYZValue filteredAccelerometerValue = XYZValue( + timestamp: timestamp, + x: _kalmanX.filtered(data["ACC"]["X"]), + y: _kalmanY.filtered(data["ACC"]["Y"]), + z: _kalmanZ.filtered(data["ACC"]["Z"]), + units: data["ACC"]["units"] + ); + + if (_title == "Height Data") { + DataValue height = _calculateHeightData(filteredAccelerometerValue); + _updateData(height); + } + if (_title == "Raw Acceleration Data") { + _updateData(rawAccelerometerValue); + } else if (_title == "Filtered Acceleration Data") { + _updateData(filteredAccelerometerValue); + } + // double pitch = data["EULER"]["PITCH"]; + }); + } + + DataValue _calculateHeightData(XYZValue accValue) { + double currentAcc = accValue.z * cos(_pitch) + accValue.x * sin(_pitch); + // Subtract gravity to get acceleration due to movement. + currentAcc -= _gravity; + double threshold = 0.3; + double accMagnitude = sqrt(accValue.x * accValue.x + accValue.y * accValue.y + accValue.z * accValue.z); + bool isStationary = (accMagnitude > _gravity - threshold) && (accMagnitude < _gravity + threshold); + /// Checks if the device is stationary based on acceleration magnitude. + if (isStationary) { + _velocity = 0.0; + } else { + // Integrate acceleration to get velocity. + _velocity += currentAcc * _timeSlice; + + // Integrate velocity to get height. + _height += _velocity * _timeSlice; + } + + // Prevent height from going negative. + _height = max(0, _height); + return Jump(DateTime.fromMillisecondsSinceEpoch(accValue.timestamp), _height); + } + + _updateData(DataValue value) { + setState(() { + _data.add(value); + _checkLength(_data); + DataValue? maxXYZValue = maxBy(_data, (DataValue b) => b.getMax()); + DataValue? minXYZValue = minBy(_data, (DataValue b) => b.getMin()); + if (maxXYZValue == null || minXYZValue == null) { + return; + } + double maxAbsValue = + max(maxXYZValue.getMax().abs(), minXYZValue.getMin().abs()); + _maxY = maxAbsValue; + + _minY = -maxAbsValue; + _maxX = value.timestamp; + _minX = _data[0].timestamp; + }); + } + + _getColor(String title) { + if (title == "Height Data") { + return ['#FF6347', '#3CB371', '#1E90FF']; + } else if (title == "Raw Acceleration Data") { + return ['#FFD700', '#FF4500', '#D8BFD8']; + } else if (title == "Filtered Acceleration Data") { + return ['#F08080', '#98FB98', '#ADD8E6']; + } + } + + @override + void initState() { + super.initState(); + _data = []; + colors = _getColor(_title); + _minY = -25; + _maxY = 25; + _setupListeners(); + } + + @override + void dispose() { + super.dispose(); + _dataSubscription?.cancel(); + } + + _checkLength(data) { + if (data.length > _numDatapoints) { + data.removeRange(0, data.length - _numDatapoints); + } + } + + @override + Widget build(BuildContext context) { + if (_title == "Height Data") { + seriesList = [ + charts.Series( + id: 'Height (m)', + colorFn: (_, __) => charts.Color.fromHex(code: colors[0]), + domainFn: (DataValue data, _) => data.timestamp, + measureFn: (DataValue data, _) => (data as Jump).height, + data: _data, + ), + ]; + } else { + seriesList = [ + charts.Series( + id: 'X${_data.isNotEmpty ? " (${_data[0].units['X']})" : ""}', + colorFn: (_, __) => charts.Color.fromHex(code: colors[0]), + domainFn: (DataValue data, _) => data.timestamp, + measureFn: (DataValue data, _) => (data as XYZValue).x, + data: _data, + ), + charts.Series( + id: 'Y${_data.isNotEmpty ? " (${_data[0].units['Y']})" : ""}', + colorFn: (_, __) => charts.Color.fromHex(code: colors[1]), + domainFn: (DataValue data, _) => data.timestamp, + measureFn: (DataValue data, _) => (data as XYZValue).y, + data: _data, + ), + charts.Series( + id: 'Z${_data.isNotEmpty ? " (${_data[0].units['Z']})" : ""}', + colorFn: (_, __) => charts.Color.fromHex(code: colors[2]), + domainFn: (DataValue data, _) => data.timestamp, + measureFn: (DataValue data, _) => (data as XYZValue).z, + data: _data, + ), + ]; + } + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + ), + Expanded( + child: charts.LineChart( + seriesList, + animate: false, + behaviors: [ + charts.SeriesLegend( + position: charts.BehaviorPosition + .bottom, // To position the legend at the end (bottom). You can change this as per requirement. + outsideJustification: charts.OutsideJustification + .middleDrawArea, // To justify the position. + horizontalFirst: false, // To stack items horizontally. + desiredMaxRows: + 1, // Optional if you want to define max rows for the legend. + entryTextStyle: charts.TextStyleSpec( + // Optional styling for the text. + color: charts.Color(r: 255, g: 255, b: 255), + fontSize: 12, + ), + ) + ], + primaryMeasureAxis: charts.NumericAxisSpec( + renderSpec: charts.GridlineRendererSpec( + labelStyle: charts.TextStyleSpec( + fontSize: 14, + color: charts.MaterialPalette.white, // Set the color here + ), + ), + viewport: charts.NumericExtents(_minY, _maxY), + ), + domainAxis: charts.NumericAxisSpec( + renderSpec: charts.GridlineRendererSpec( + labelStyle: charts.TextStyleSpec( + fontSize: 14, + color: charts.MaterialPalette.white, // Set the color here + ), + ), + viewport: charts.NumericExtents(_minX, _maxX)), + ), + ), + ], + ); + } +} + +abstract class DataValue { + final int timestamp; + final Map units; + double getMin(); + double getMax(); + DataValue({required this.timestamp, required this.units}); +} + +class XYZValue extends DataValue { + final double x; + final double y; + final double z; + + XYZValue( + {required timestamp, + required this.x, + required this.y, + required this.z, + required units}) + : super(timestamp: timestamp, units: units); + + @override + double getMax() { + return max(x, max(y, z)); + } + + @override + double getMin() { + return min(x, min(y, z)); + } + + @override + String toString() { + return "timestamp: $timestamp\nx: $x, y: $y, z: $z"; + } +} + +/// A class representing a jump with a time and height. +class Jump extends DataValue { + final DateTime _time; + final double _height; + + /// Constructs a Jump object with a time and height. + Jump(DateTime time, double height) + : _time = time, + _height = height, + super( + timestamp: time.millisecondsSinceEpoch, + units: {'height': 'meters'} // Providing default units + ); + + @override + double getMin() { + // Implement logic for min value + // For example, it might always be 0 for a jump. + return 0.0; + } + + @override + double getMax() { + // Implement logic for max value + // For Jump, it's likely the height. + return _height; + } + + // Optionally, if you need to access time and height outside, consider adding getters. + DateTime get time => _time; + double get height => _height; +} diff --git a/open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart b/open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart new file mode 100644 index 0000000..8a6badd --- /dev/null +++ b/open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart @@ -0,0 +1,99 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:open_earable/sensor_data_tab/earable_3d_model.dart'; +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:open_earable/sensor_data_tab/sensor_chart.dart'; + +class JumpHeightDataTab extends StatefulWidget { + final OpenEarable _openEarable; + JumpHeightDataTab(this._openEarable); + @override + _SensorDataTabState createState() => _SensorDataTabState(_openEarable); +} + +class _SensorDataTabState extends State + with SingleTickerProviderStateMixin { + //late EarableModel _earableModel; + final OpenEarable _openEarable; + late TabController _tabController; + + StreamSubscription? _batteryLevelSubscription; + StreamSubscription? _buttonStateSubscription; + List accelerometerData = []; + List gyroscopeData = []; + List magnetometerData = []; + List barometerData = []; + + _SensorDataTabState(this._openEarable); + + @override + void initState() { + super.initState(); + _tabController = TabController(vsync: this, length: 5); + if (_openEarable.bleManager.connected) { + _setupListeners(); + } + } + + int lastTimestamp = 0; + _setupListeners() { + _batteryLevelSubscription = + _openEarable.sensorManager.getBatteryLevelStream().listen((data) { + print("Battery level is ${data[0]}"); + }); + _buttonStateSubscription = + _openEarable.sensorManager.getButtonStateStream().listen((data) { + print("Button State is ${data[0]}"); + }); + } + + @override + void dispose() { + super.dispose(); + _buttonStateSubscription?.cancel(); + _batteryLevelSubscription?.cancel(); + } + + @override + Widget build(BuildContext context) { + if (!_openEarable.bleManager.connected) { + return _notConnectedWidget(); + } else { + return _buildSensorDataTabs(); + } + } + + Widget _notConnectedWidget() { + return Stack( + fit: StackFit.expand, + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.warning, + size: 48, + color: Colors.red, + ), + SizedBox(height: 16), + Center( + child: Text( + "Not connected to\nOpenEarable device", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ], + ); + } + + +} diff --git a/open_earable/lib/apps/jump_height_test.dart b/open_earable/lib/apps/jump_height_test/jump_height_test.dart similarity index 82% rename from open_earable/lib/apps/jump_height_test.dart rename to open_earable/lib/apps/jump_height_test/jump_height_test.dart index 5ccfc57..27694a9 100644 --- a/open_earable/lib/apps/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_test.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:open_earable/apps/jump_height_test/jump_height_chart.dart'; import 'dart:async'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import 'package:charts_flutter/flutter.dart' as charts; @@ -12,7 +13,6 @@ class JumpHeightTest extends StatefulWidget { /// Constructs a JumpHeightTest widget with a given OpenEarable device. JumpHeightTest(this._openEarable); - @override _JumpHeightTestState createState() => _JumpHeightTestState(_openEarable); } @@ -43,7 +43,8 @@ class HeightChart extends StatelessWidget { } /// State class for JumpHeightTest widget. -class _JumpHeightTestState extends State { +class _JumpHeightTestState extends State + with SingleTickerProviderStateMixin { /// Stores the start time of a jump test. DateTime? _startTime; /// Current height calculated from sensor data. @@ -78,6 +79,7 @@ class _JumpHeightTestState extends State { double _accZ = 0.0; /// Pitch angle in radians. double _pitch = 0.0; + late TabController _tabController; /// Constructs a _JumpHeightTestState object with a given OpenEarable device. _JumpHeightTestState(this._openEarable); @@ -86,6 +88,9 @@ class _JumpHeightTestState extends State { @override void initState() { super.initState(); + // Set sampling rate to maximum. + _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); + _tabController = TabController(vsync: this, length: 3); // Initialize Kalman filters. _initializeKalmanFilters(); // Set up listeners for sensor data. @@ -117,8 +122,6 @@ class _JumpHeightTestState extends State { /// Starts the jump height measurement process. /// It sets the sampling rate, initializes or resets variables, and begins listening to sensor data. void _startJump() { - // Set sampling rate to maximum. - _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); _startTime = DateTime.now(); setState(() { @@ -218,17 +221,31 @@ class _JumpHeightTestState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, appBar: AppBar( title: Text('Jump Height Test'), ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildChart(), - _buildText(), - _buildButtons(), - Visibility( + body: Column( + children: [ + TabBar( + controller: _tabController, + indicatorColor: Colors.white, // Color of the underline indicator + labelColor: Colors.white, // Color of the active tab label + unselectedLabelColor: Colors.grey, // Color of the inactive tab labels + tabs: [ + Tab(text: 'Height'), + Tab(text: 'Raw Acc.'), + Tab(text: 'Filtered Acc.'), + ], + ), + Expanded( + child: (!_openEarable.bleManager.connected) + ? _notConnectedWidget() + : _buildJumpHeightDataTabs(), + ), + _buildText(), + _buildButtons(), + Visibility( // Show error message if no OpenEarable device is connected. visible: !_earableConnected, maintainState: true, @@ -241,12 +258,52 @@ class _JumpHeightTestState extends State { ), ), ), - ], - ), + ], ), ); } + Widget _notConnectedWidget() { + return Stack( + fit: StackFit.expand, + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.warning, + size: 48, + color: Colors.red, + ), + SizedBox(height: 16), + Center( + child: Text( + "Not connected to\nOpenEarable device", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildJumpHeightDataTabs() { + return TabBarView( + controller: _tabController, + children: [ + JumpHeightChart(_openEarable, "Height Data"), + JumpHeightChart(_openEarable, "Raw Acceleration Data"), + JumpHeightChart(_openEarable, "Filtered Acceleration Data") + ], + ); + } /// Builds a line chart to display jump height over time. Widget _buildChart() { List> jumpDataSeries = [ @@ -313,7 +370,7 @@ class _JumpHeightTestState extends State { backgroundColor: !_isJumping ? Colors.greenAccent : Colors.red, foregroundColor: Colors.black, ), - child: Text(_isJumping ? 'Stop Jump' : 'Start Jump'), + child: Text(_isJumping ? 'Stop Jump' : 'Set Baseline & Start Jump'), ), ); } From 8656efecb981899172a006fba5c82e9177293ede Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Sun, 24 Dec 2023 17:06:19 +0100 Subject: [PATCH 011/104] Minor code improvements and add jump timer --- .../jump_height_test/jump_height_chart.dart | 63 +++++------ .../jump_height_test/jump_height_test.dart | 105 +++++------------- 2 files changed, 61 insertions(+), 107 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart index 6caad19..483944a 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart @@ -58,55 +58,56 @@ class _JumpHeightChartState extends State { _dataSubscription = _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { int timestamp = data["timestamp"]; - XYZValue rawAccelerometerValue = XYZValue( - timestamp: timestamp, - x: data["ACC"]["X"], - y: data["ACC"]["Y"], - z: data["ACC"]["Z"], - units: data["ACC"]["units"] - ); - XYZValue filteredAccelerometerValue = XYZValue( - timestamp: timestamp, - x: _kalmanX.filtered(data["ACC"]["X"]), - y: _kalmanY.filtered(data["ACC"]["Y"]), - z: _kalmanZ.filtered(data["ACC"]["Z"]), - units: data["ACC"]["units"] - ); + _pitch = data["EULER"]["PITCH"]; + + XYZValue rawAccData = XYZValue( + timestamp: timestamp, + x: data["ACC"]["X"], + y: data["ACC"]["Y"], + z: data["ACC"]["Z"], + units: {"X": "m/s²", "Y": "m/s²", "Z": "m/s²"} + ); + XYZValue filteredAccData = XYZValue( + timestamp: timestamp, + x: _kalmanX.filtered(data["ACC"]["X"]), + y: _kalmanY.filtered(data["ACC"]["Y"]), + z: _kalmanZ.filtered(data["ACC"]["Z"]), + units: {"X": "m/s²", "Y": "m/s²", "Z": "m/s²"} + ); if (_title == "Height Data") { - DataValue height = _calculateHeightData(filteredAccelerometerValue); + DataValue height = _calculateHeightData(filteredAccData); _updateData(height); } if (_title == "Raw Acceleration Data") { - _updateData(rawAccelerometerValue); + _updateData(rawAccData); } else if (_title == "Filtered Acceleration Data") { - _updateData(filteredAccelerometerValue); + _updateData(filteredAccData); } - // double pitch = data["EULER"]["PITCH"]; }); } DataValue _calculateHeightData(XYZValue accValue) { - double currentAcc = accValue.z * cos(_pitch) + accValue.x * sin(_pitch); // Subtract gravity to get acceleration due to movement. - currentAcc -= _gravity; + double currentAcc = accValue.z * cos(_pitch) + accValue.x * sin(_pitch) - _gravity;; + double threshold = 0.3; double accMagnitude = sqrt(accValue.x * accValue.x + accValue.y * accValue.y + accValue.z * accValue.z); bool isStationary = (accMagnitude > _gravity - threshold) && (accMagnitude < _gravity + threshold); /// Checks if the device is stationary based on acceleration magnitude. - if (isStationary) { - _velocity = 0.0; - } else { - // Integrate acceleration to get velocity. - _velocity += currentAcc * _timeSlice; + if (isStationary) { + _velocity = 0.0; + } else { + // Integrate acceleration to get velocity. + _velocity += currentAcc * _timeSlice; - // Integrate velocity to get height. - _height += _velocity * _timeSlice; - } + // Integrate velocity to get height. + _height += _velocity * _timeSlice; + } - // Prevent height from going negative. - _height = max(0, _height); - return Jump(DateTime.fromMillisecondsSinceEpoch(accValue.timestamp), _height); + // Prevent height from going negative. + _height = max(0, _height); + return Jump(DateTime.fromMillisecondsSinceEpoch(accValue.timestamp), _height); } _updateData(DataValue value) { diff --git a/open_earable/lib/apps/jump_height_test/jump_height_test.dart b/open_earable/lib/apps/jump_height_test/jump_height_test.dart index 27694a9..7d5e4ba 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_test.dart @@ -16,37 +16,14 @@ class JumpHeightTest extends StatefulWidget { _JumpHeightTestState createState() => _JumpHeightTestState(_openEarable); } -/// A class representing a jump with a time and height. -class Jump { - final DateTime _time; - final double _height; - - /// Constructs a Jump object with a time and height. - Jump(this._time, this._height); -} - -/// A stateless widget to display jump heights in a bar chart. -class HeightChart extends StatelessWidget { - final List _seriesList; - final bool _animate; - - /// Constructs a HeightChart widget with given series list and animate flag. - HeightChart(this._seriesList, {required bool animate}) : _animate = animate; - - @override - Widget build(BuildContext context) { - return new charts.BarChart( - _seriesList.cast>(), - animate: _animate, - ); - } -} - /// State class for JumpHeightTest widget. class _JumpHeightTestState extends State with SingleTickerProviderStateMixin { /// Stores the start time of a jump test. - DateTime? _startTime; + Timer? _timer; + Duration _jumpDuration = Duration.zero; + DateTime? _startOfJump; + DateTime? _endOfJump; /// Current height calculated from sensor data. double _height = 0.0; // List to store each jump's data. @@ -115,6 +92,9 @@ class _JumpHeightTestState extends State if (!_isJumping) { return; } + setState(() { + _jumpDuration = DateTime.now().difference(_startOfJump!); + }); _processSensorData(data); }); } @@ -122,7 +102,7 @@ class _JumpHeightTestState extends State /// Starts the jump height measurement process. /// It sets the sampling rate, initializes or resets variables, and begins listening to sensor data. void _startJump() { - _startTime = DateTime.now(); + _startOfJump = DateTime.now(); setState(() { // Clear data from previous jump. @@ -137,6 +117,7 @@ class _JumpHeightTestState extends State /// Stops the jump height measurement process. void _stopJump() { + _endOfJump = DateTime.now(); if (_isJumping) { setState(() { _isJumping = false; @@ -215,6 +196,11 @@ class _JumpHeightTestState extends State // print("Stationary: ${deviceIsStationary(0.3)}, Acc: $currentAcc, Vel: $velocity, Height: $height"); } + String _prettyDuration(Duration duration) { + var seconds = duration.inMilliseconds / 1000; + return '${seconds.toStringAsFixed(2)} s'; + } + /// Builds the UI for the jump height test. /// It displays a line chart of jump height over time and the maximum jump height achieved. // This build function is getting a little too big. Consider refactoring. @@ -243,8 +229,10 @@ class _JumpHeightTestState extends State ? _notConnectedWidget() : _buildJumpHeightDataTabs(), ), - _buildText(), + SizedBox(height: 20), // Margin between chart and button _buildButtons(), + SizedBox(height: 20), // Margin between button and text + _buildText(), Visibility( // Show error message if no OpenEarable device is connected. visible: !_earableConnected, @@ -304,59 +292,24 @@ class _JumpHeightTestState extends State ], ); } - /// Builds a line chart to display jump height over time. - Widget _buildChart() { - List> jumpDataSeries = [ - charts.Series( - id: "Jumps", - data: _jumpData, - // X-axis: time in milliseconds since the start of the jump. - domainFn: (Jump series, _) => series._time.difference(_startTime!).inMilliseconds, - measureFn: (Jump series, _) => series._height, - colorFn: (Jump series, _) => charts.MaterialPalette.cyan.shadeDefault, - ) - ]; - - return Expanded( - child: Container( - child: charts.LineChart( - jumpDataSeries, - animate: false, - behaviors: [ - // X-axis label. - charts.ChartTitle('Time (ms)', - behaviorPosition: charts.BehaviorPosition.bottom, - titleStyleSpec: charts.TextStyleSpec( - color: charts.MaterialPalette.white, - fontSize: 10, - ), - titleOutsideJustification: charts.OutsideJustification.middleDrawArea), - // Y-axis label. - charts.ChartTitle('Height (m)', - behaviorPosition: charts.BehaviorPosition.start, - titleStyleSpec: charts.TextStyleSpec( - color: charts.MaterialPalette.white, - fontSize: 10, - ), - titleOutsideJustification: charts.OutsideJustification.middleDrawArea), - ], - // Include timeline points in line. - defaultRenderer: charts.LineRendererConfig(includePoints: true), - ), - ), - ); - } - - /// Builds a text widget to display the maximum jump height achieved. + Widget _buildText() { return Container( - child: Text( - 'Max Height: ${_maxHeight.toStringAsFixed(2)} m', - style: Theme.of(context).textTheme.headlineSmall, + child: Column( + children: [ + Text( + 'Max height: ${_maxHeight.toStringAsFixed(2)} m', + style: Theme.of(context).textTheme.headlineMedium, + ), + Text('Jump time: ${_prettyDuration(_jumpDuration)}', + style: Theme.of(context).textTheme.headlineSmall, + ), + ], ), ); } + /// Builds buttons to start and stop the jump height measurement process. Widget _buildButtons() { return Flexible( From f59eef7ee27b17ba741788231641364b52e3acc8 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Mon, 25 Dec 2023 23:00:30 +0100 Subject: [PATCH 012/104] Minor code improvements --- .../jump_height_test/jump_height_chart.dart | 168 +++++++++++------- .../jump_height_data_tab.dart | 99 ----------- .../jump_height_test/jump_height_test.dart | 18 +- 3 files changed, 117 insertions(+), 168 deletions(-) delete mode 100644 open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart diff --git a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart index 483944a..4aba41b 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart @@ -8,40 +8,68 @@ import 'package:collection/collection.dart'; import 'dart:math'; import 'dart:core'; +/// A class representing a Chart for Jump Height. class JumpHeightChart extends StatefulWidget { + /// The OpenEarable object. final OpenEarable _openEarable; + /// The title of the chart. final String _title; + + /// Constructs a JumpHeightChart object with an OpenEarable object and a title. JumpHeightChart(this._openEarable, this._title); + @override - _JumpHeightChartState createState() => - _JumpHeightChartState(_openEarable, _title); + _JumpHeightChartState createState() => _JumpHeightChartState(_openEarable, _title); } +/// A class representing the state of a JumpHeightChart. class _JumpHeightChartState extends State { + /// The OpenEarable object. final OpenEarable _openEarable; + /// The title of the chart. final String _title; + /// The data of the chart. late List _data; + /// The subscription to the data. StreamSubscription? _dataSubscription; - _JumpHeightChartState(this._openEarable, this._title); + /// The minimum x value of the chart. late int _minX = 0; + /// The maximum x value of the chart. late int _maxX = 0; + /// The colors of the chart. late List colors; + /// The series of the chart. List> seriesList = []; + /// The minimum y value of the chart. late double _minY; + /// The maximum y value of the chart. late double _maxY; + /// The error measure of the Kalman filter. final _errorMeasureAcc = 5.0; - late SimpleKalman _kalmanX, _kalmanY, _kalmanZ; + /// The Kalman filter for the x value. + late SimpleKalman _kalmanX; + /// The Kalman filter for the y value. + late SimpleKalman _kalmanY; + /// The Kalman filter for the z value. + late SimpleKalman _kalmanZ; + /// The number of datapoints to display on the chart. int _numDatapoints = 200; + /// The velocity of the device. double _velocity = 0.0; /// Sampling rate time slice (inverse of frequency). - double _timeSlice = 1 / 30.0; + double _timeSlice = 1.0 / 30.0; /// Standard gravity in m/s^2. double _gravity = 9.81; /// Pitch angle in radians. double _pitch = 0.0; + /// The height of the jump. double _height = 0.0; + /// Constructs a _JumpHeightChartState object with an OpenEarable object and a title. + _JumpHeightChartState(this._openEarable, this._title); + + /// Sets up the listeners for the data. _setupListeners() { _kalmanX = SimpleKalman( errorMeasure: _errorMeasureAcc, @@ -75,24 +103,29 @@ class _JumpHeightChartState extends State { units: {"X": "m/s²", "Y": "m/s²", "Z": "m/s²"} ); - if (_title == "Height Data") { - DataValue height = _calculateHeightData(filteredAccData); - _updateData(height); - } - if (_title == "Raw Acceleration Data") { - _updateData(rawAccData); - } else if (_title == "Filtered Acceleration Data") { - _updateData(filteredAccData); + switch (_title) { + case "Height Data": + DataValue height = _calculateHeightData(filteredAccData); + _updateData(height); + break; + case "Raw Acceleration Data": + _updateData(rawAccData); + break; + case "Filtered Acceleration Data": + _updateData(filteredAccData); + break; + default: + throw ArgumentError("Invalid tab title."); } }); } DataValue _calculateHeightData(XYZValue accValue) { // Subtract gravity to get acceleration due to movement. - double currentAcc = accValue.z * cos(_pitch) + accValue.x * sin(_pitch) - _gravity;; + double currentAcc = accValue._z * cos(_pitch) + accValue._x * sin(_pitch) - _gravity;; double threshold = 0.3; - double accMagnitude = sqrt(accValue.x * accValue.x + accValue.y * accValue.y + accValue.z * accValue.z); + double accMagnitude = sqrt(accValue._x * accValue._x + accValue._y * accValue._y + accValue._z * accValue._z); bool isStationary = (accMagnitude > _gravity - threshold) && (accMagnitude < _gravity + threshold); /// Checks if the device is stationary based on acceleration magnitude. if (isStationary) { @@ -104,10 +137,10 @@ class _JumpHeightChartState extends State { // Integrate velocity to get height. _height += _velocity * _timeSlice; } - // Prevent height from going negative. _height = max(0, _height); - return Jump(DateTime.fromMillisecondsSinceEpoch(accValue.timestamp), _height); + + return Jump(DateTime.fromMillisecondsSinceEpoch(accValue._timestamp), _height); } _updateData(DataValue value) { @@ -124,18 +157,24 @@ class _JumpHeightChartState extends State { _maxY = maxAbsValue; _minY = -maxAbsValue; - _maxX = value.timestamp; - _minX = _data[0].timestamp; + _maxX = value._timestamp; + _minX = _data[0]._timestamp; }); } _getColor(String title) { - if (title == "Height Data") { - return ['#FF6347', '#3CB371', '#1E90FF']; - } else if (title == "Raw Acceleration Data") { - return ['#FFD700', '#FF4500', '#D8BFD8']; - } else if (title == "Filtered Acceleration Data") { - return ['#F08080', '#98FB98', '#ADD8E6']; + switch (title) { + case "Height Data": + // Blue, Orange, and Teal - Good for colorblindness + return ['#007bff', '#ff7f0e', '#2ca02c']; + case "Raw Acceleration Data": + // Purple, Magenta, and Cyan - Diverse hue and brightness + return ['#9467bd', '#d62728', '#17becf']; + case "Filtered Acceleration Data": + // Olive, Brown, and Navy - High contrast + return ['#8c564b', '#e377c2', '#1f77b4']; + default: + throw ArgumentError("Invalid tab title."); } } @@ -168,32 +207,32 @@ class _JumpHeightChartState extends State { charts.Series( id: 'Height (m)', colorFn: (_, __) => charts.Color.fromHex(code: colors[0]), - domainFn: (DataValue data, _) => data.timestamp, - measureFn: (DataValue data, _) => (data as Jump).height, + domainFn: (DataValue data, _) => data._timestamp, + measureFn: (DataValue data, _) => (data as Jump)._height, data: _data, ), ]; } else { seriesList = [ charts.Series( - id: 'X${_data.isNotEmpty ? " (${_data[0].units['X']})" : ""}', + id: 'X${_data.isNotEmpty ? " (${_data[0]._units['X']})" : ""}', colorFn: (_, __) => charts.Color.fromHex(code: colors[0]), - domainFn: (DataValue data, _) => data.timestamp, - measureFn: (DataValue data, _) => (data as XYZValue).x, + domainFn: (DataValue data, _) => data._timestamp, + measureFn: (DataValue data, _) => (data as XYZValue)._x, data: _data, ), charts.Series( - id: 'Y${_data.isNotEmpty ? " (${_data[0].units['Y']})" : ""}', + id: 'Y${_data.isNotEmpty ? " (${_data[0]._units['Y']})" : ""}', colorFn: (_, __) => charts.Color.fromHex(code: colors[1]), - domainFn: (DataValue data, _) => data.timestamp, - measureFn: (DataValue data, _) => (data as XYZValue).y, + domainFn: (DataValue data, _) => data._timestamp, + measureFn: (DataValue data, _) => (data as XYZValue)._y, data: _data, ), charts.Series( - id: 'Z${_data.isNotEmpty ? " (${_data[0].units['Z']})" : ""}', + id: 'Z${_data.isNotEmpty ? " (${_data[0]._units['Z']})" : ""}', colorFn: (_, __) => charts.Color.fromHex(code: colors[2]), - domainFn: (DataValue data, _) => data.timestamp, - measureFn: (DataValue data, _) => (data as XYZValue).z, + domainFn: (DataValue data, _) => data._timestamp, + measureFn: (DataValue data, _) => (data as XYZValue)._z, data: _data, ), ]; @@ -248,72 +287,81 @@ class _JumpHeightChartState extends State { } } +/// A class representing a generic data value. abstract class DataValue { - final int timestamp; - final Map units; + /// The timestamp of the data. + final int _timestamp; + /// The units of the data. + final Map _units; + + /// Returns the minimum value of the data. double getMin(); + /// Returns the maximum value of the data. double getMax(); - DataValue({required this.timestamp, required this.units}); + + /// Constructs a DataValue object with a timestamp and units. + DataValue({required int timestamp, required Map units}) : _units = units, _timestamp = timestamp; } +/// A class representing a generic XYZ value. class XYZValue extends DataValue { - final double x; - final double y; - final double z; + /// The x value of the data. + final double _x; + /// The y value of the data. + final double _y; + /// The z value of the data. + final double _z; + /// Constructs a XYZValue object with a timestamp, x, y, z, and units. XYZValue( {required timestamp, - required this.x, - required this.y, - required this.z, + required double x, + required double y, + required double z, required units}) - : super(timestamp: timestamp, units: units); + : _z = z, _y = y, _x = x, super(timestamp: timestamp, units: units); @override double getMax() { - return max(x, max(y, z)); + return max(_x, max(_y, _z)); } @override double getMin() { - return min(x, min(y, z)); + return min(_x, min(_y, _z)); } @override String toString() { - return "timestamp: $timestamp\nx: $x, y: $y, z: $z"; + return "timestamp: $_timestamp\nx: $_x, y: $_y, z: $_z"; } } /// A class representing a jump with a time and height. class Jump extends DataValue { + /// The time of the jump. final DateTime _time; + /// The height of the jump. final double _height; /// Constructs a Jump object with a time and height. Jump(DateTime time, double height) : _time = time, _height = height, - super( - timestamp: time.millisecondsSinceEpoch, - units: {'height': 'meters'} // Providing default units - ); + super(timestamp: time.millisecondsSinceEpoch, units: {'height': 'meters'}); @override double getMin() { - // Implement logic for min value - // For example, it might always be 0 for a jump. return 0.0; } @override double getMax() { - // Implement logic for max value - // For Jump, it's likely the height. return _height; } - // Optionally, if you need to access time and height outside, consider adding getters. - DateTime get time => _time; - double get height => _height; + @override + String toString() { + return "timestamp: ${_time.millisecondsSinceEpoch}\nheight $_height"; + } } diff --git a/open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart b/open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart deleted file mode 100644 index 8a6badd..0000000 --- a/open_earable/lib/apps/jump_height_test/jump_height_data_tab.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:open_earable/sensor_data_tab/earable_3d_model.dart'; -import 'package:open_earable_flutter/src/open_earable_flutter.dart'; -import 'package:open_earable/sensor_data_tab/sensor_chart.dart'; - -class JumpHeightDataTab extends StatefulWidget { - final OpenEarable _openEarable; - JumpHeightDataTab(this._openEarable); - @override - _SensorDataTabState createState() => _SensorDataTabState(_openEarable); -} - -class _SensorDataTabState extends State - with SingleTickerProviderStateMixin { - //late EarableModel _earableModel; - final OpenEarable _openEarable; - late TabController _tabController; - - StreamSubscription? _batteryLevelSubscription; - StreamSubscription? _buttonStateSubscription; - List accelerometerData = []; - List gyroscopeData = []; - List magnetometerData = []; - List barometerData = []; - - _SensorDataTabState(this._openEarable); - - @override - void initState() { - super.initState(); - _tabController = TabController(vsync: this, length: 5); - if (_openEarable.bleManager.connected) { - _setupListeners(); - } - } - - int lastTimestamp = 0; - _setupListeners() { - _batteryLevelSubscription = - _openEarable.sensorManager.getBatteryLevelStream().listen((data) { - print("Battery level is ${data[0]}"); - }); - _buttonStateSubscription = - _openEarable.sensorManager.getButtonStateStream().listen((data) { - print("Button State is ${data[0]}"); - }); - } - - @override - void dispose() { - super.dispose(); - _buttonStateSubscription?.cancel(); - _batteryLevelSubscription?.cancel(); - } - - @override - Widget build(BuildContext context) { - if (!_openEarable.bleManager.connected) { - return _notConnectedWidget(); - } else { - return _buildSensorDataTabs(); - } - } - - Widget _notConnectedWidget() { - return Stack( - fit: StackFit.expand, - children: [ - Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.warning, - size: 48, - color: Colors.red, - ), - SizedBox(height: 16), - Center( - child: Text( - "Not connected to\nOpenEarable device", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - ], - ); - } - - -} diff --git a/open_earable/lib/apps/jump_height_test/jump_height_test.dart b/open_earable/lib/apps/jump_height_test/jump_height_test.dart index 7d5e4ba..9fb8fce 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_test.dart @@ -2,17 +2,18 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/jump_height_test/jump_height_chart.dart'; import 'dart:async'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; -import 'package:charts_flutter/flutter.dart' as charts; import 'package:simple_kalman/simple_kalman.dart'; import 'dart:math'; /// An app that lets you test your jump height using an OpenEarable device. class JumpHeightTest extends StatefulWidget { + /// Instance of OpenEarable device. final OpenEarable _openEarable; /// Constructs a JumpHeightTest widget with a given OpenEarable device. JumpHeightTest(this._openEarable); + /// Creates a state for JumpHeightTest widget. _JumpHeightTestState createState() => _JumpHeightTestState(_openEarable); } @@ -20,10 +21,9 @@ class JumpHeightTest extends StatefulWidget { class _JumpHeightTestState extends State with SingleTickerProviderStateMixin { /// Stores the start time of a jump test. - Timer? _timer; - Duration _jumpDuration = Duration.zero; DateTime? _startOfJump; - DateTime? _endOfJump; + /// Stores the duration of a jump test. + Duration _jumpDuration = Duration.zero; /// Current height calculated from sensor data. double _height = 0.0; // List to store each jump's data. @@ -56,6 +56,7 @@ class _JumpHeightTestState extends State double _accZ = 0.0; /// Pitch angle in radians. double _pitch = 0.0; + /// Manages the [TabBar]. late TabController _tabController; /// Constructs a _JumpHeightTestState object with a given OpenEarable device. @@ -65,13 +66,13 @@ class _JumpHeightTestState extends State @override void initState() { super.initState(); - // Set sampling rate to maximum. - _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); _tabController = TabController(vsync: this, length: 3); - // Initialize Kalman filters. - _initializeKalmanFilters(); // Set up listeners for sensor data. if (_openEarable.bleManager.connected) { + // Set sampling rate to maximum. + _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); + // Initialize Kalman filters. + _initializeKalmanFilters(); _setupListeners(); _earableConnected = true; } @@ -117,7 +118,6 @@ class _JumpHeightTestState extends State /// Stops the jump height measurement process. void _stopJump() { - _endOfJump = DateTime.now(); if (_isJumping) { setState(() { _isJumping = false; From bd24516817b14ff2adeab9bc2af60c4e8dc05c8b Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Mon, 25 Dec 2023 23:16:36 +0100 Subject: [PATCH 013/104] Add comments --- open_earable/lib/apps/jump_height_test/jump_height_chart.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart index 4aba41b..bdf6b99 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart @@ -120,6 +120,7 @@ class _JumpHeightChartState extends State { }); } + /// Calculates the height of the jump. DataValue _calculateHeightData(XYZValue accValue) { // Subtract gravity to get acceleration due to movement. double currentAcc = accValue._z * cos(_pitch) + accValue._x * sin(_pitch) - _gravity;; @@ -143,6 +144,7 @@ class _JumpHeightChartState extends State { return Jump(DateTime.fromMillisecondsSinceEpoch(accValue._timestamp), _height); } + /// Updates the data of the chart. _updateData(DataValue value) { setState(() { _data.add(value); @@ -162,6 +164,7 @@ class _JumpHeightChartState extends State { }); } + /// Gets the color of the chart lines. _getColor(String title) { switch (title) { case "Height Data": @@ -194,6 +197,7 @@ class _JumpHeightChartState extends State { _dataSubscription?.cancel(); } + /// Checks the length of the data an removes the oldest data if it is too long. _checkLength(data) { if (data.length > _numDatapoints) { data.removeRange(0, data.length - _numDatapoints); From 1dec4d2e45628c989ef7683842b4f1826b60fa26 Mon Sep 17 00:00:00 2001 From: Lukas Probst Date: Tue, 26 Dec 2023 14:05:57 +0100 Subject: [PATCH 014/104] Minor code improvements --- .../lib/apps/jump_height_test/jump_height_chart.dart | 5 +++-- open_earable/lib/apps/jump_height_test/jump_height_test.dart | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart index bdf6b99..41b1652 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_chart.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_chart.dart @@ -123,14 +123,15 @@ class _JumpHeightChartState extends State { /// Calculates the height of the jump. DataValue _calculateHeightData(XYZValue accValue) { // Subtract gravity to get acceleration due to movement. - double currentAcc = accValue._z * cos(_pitch) + accValue._x * sin(_pitch) - _gravity;; + double currentAcc = accValue._z * cos(_pitch) + accValue._x * sin(_pitch) - _gravity; double threshold = 0.3; double accMagnitude = sqrt(accValue._x * accValue._x + accValue._y * accValue._y + accValue._z * accValue._z); bool isStationary = (accMagnitude > _gravity - threshold) && (accMagnitude < _gravity + threshold); - /// Checks if the device is stationary based on acceleration magnitude. + // Checks if the device is stationary based on acceleration magnitude. if (isStationary) { _velocity = 0.0; + _height = 0.0; } else { // Integrate acceleration to get velocity. _velocity += currentAcc * _timeSlice; diff --git a/open_earable/lib/apps/jump_height_test/jump_height_test.dart b/open_earable/lib/apps/jump_height_test/jump_height_test.dart index 9fb8fce..86c9d00 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_test.dart @@ -174,6 +174,7 @@ class _JumpHeightTestState extends State setState(() { if (_deviceIsStationary(0.3)) { _velocity = 0.0; + _height = 0.0; } else { // Integrate acceleration to get velocity. _velocity += currentAcc * _timeSlice; @@ -231,8 +232,6 @@ class _JumpHeightTestState extends State ), SizedBox(height: 20), // Margin between chart and button _buildButtons(), - SizedBox(height: 20), // Margin between button and text - _buildText(), Visibility( // Show error message if no OpenEarable device is connected. visible: !_earableConnected, @@ -246,6 +245,8 @@ class _JumpHeightTestState extends State ), ), ), + SizedBox(height: 20), // Margin between button and text + _buildText() ], ), ); From fed5a6aa08698dcd5216d18a1da7b95a5772006a Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sat, 30 Dec 2023 18:39:28 +0100 Subject: [PATCH 015/104] add cupertino design to controls page. Color picker not working yet --- .../lib/controls_tab/controls_tab.dart | 5 +- .../models/open_earable_settings.dart | 49 +- .../lib/controls_tab/views/audio_player.dart | 452 +++++++++++------- .../lib/controls_tab/views/connect.dart | 58 +-- .../lib/controls_tab/views/led_color.dart | 191 +++++--- .../views/sensor_configuration.dart | 222 ++++++--- open_earable/lib/main.dart | 330 ++++++++----- .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- open_earable/pubspec.lock | 18 +- open_earable/pubspec.yaml | 2 +- 11 files changed, 872 insertions(+), 459 deletions(-) diff --git a/open_earable/lib/controls_tab/controls_tab.dart b/open_earable/lib/controls_tab/controls_tab.dart index dfe3187..496b995 100644 --- a/open_earable/lib/controls_tab/controls_tab.dart +++ b/open_earable/lib/controls_tab/controls_tab.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'dart:io'; import 'views/sensor_configuration.dart'; import 'views/connect.dart'; import 'views/led_color.dart'; @@ -70,7 +71,9 @@ class _ControlTabState extends State { Widget build(BuildContext context) { return SingleChildScrollView( child: GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), + onTap: () => Platform.isIOS + ? FocusScope.of(context).requestFocus(FocusNode()) + : FocusScope.of(context).unfocus(), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/open_earable/lib/controls_tab/models/open_earable_settings.dart b/open_earable/lib/controls_tab/models/open_earable_settings.dart index 0e5a4a2..4557f7a 100644 --- a/open_earable/lib/controls_tab/models/open_earable_settings.dart +++ b/open_earable/lib/controls_tab/models/open_earable_settings.dart @@ -22,22 +22,22 @@ class OpenEarableSettings { "50000", "62500" ]; - final Map jingleMap = { - 0: 'IDLE', - 1: 'NOTIFICATION', - 2: 'SUCCESS', - 3: 'ERROR', - 4: 'ALARM', - 5: 'PING', - 6: 'OPEN', - 7: 'CLOSE', - 8: 'CLICK', + final Map jingleMap = { + 'IDLE': 0, + 'NOTIFICATION': 1, + 'SUCCESS': 2, + 'ERROR': 3, + 'ALARM': 4, + 'PING': 5, + 'OPEN': 6, + 'CLOSE': 7, + 'CLICK': 8, }; - final Map waveFormMap = { - 1: 'SINE', - 2: 'SQUARE', - 3: 'TRIANGLE', - 4: 'SAW', + final Map waveFormMap = { + 'SINE': 0, + 'SQUARE': 1, + 'TRIANGLE': 2, + 'SAW': 3, }; late bool imuSettingSelected; @@ -64,9 +64,9 @@ class OpenEarableSettings { selectedBarometerOption = "0"; selectedMicrophoneOption = "0"; - selectedAudioPlayerRadio = 3; // no radio is selected - selectedJingle = jingleMap[1]!; - selectedWaveForm = waveFormMap[1]!; + selectedAudioPlayerRadio = 0; + selectedJingle = jingleMap.keys.first; + selectedWaveForm = waveFormMap.keys.first; selectedFilename = "filename.wav"; selectedFrequency = "440"; selectedFrequencyVolume = "50"; @@ -76,19 +76,10 @@ class OpenEarableSettings { } int getWaveFormIndex(String value) { - return _getKeyFromValue(value, waveFormMap); + return waveFormMap[value] ?? 0; } int getJingleIndex(String value) { - return _getKeyFromValue(value, jingleMap); - } - - int _getKeyFromValue(String value, Map map) { - for (var entry in map.entries) { - if (entry.value == value) { - return entry.key; - } - } - return 1; + return jingleMap[value] ?? 0; } } diff --git a/open_earable/lib/controls_tab/views/audio_player.dart b/open_earable/lib/controls_tab/views/audio_player.dart index f5224b4..76c2933 100644 --- a/open_earable/lib/controls_tab/views/audio_player.dart +++ b/open_earable/lib/controls_tab/views/audio_player.dart @@ -1,3 +1,5 @@ +import 'dart:io'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import '../models/open_earable_settings.dart'; @@ -16,6 +18,7 @@ class _AudioPlayerCardState extends State { late TextEditingController _filenameTextController; late TextEditingController _jingleTextController; + late TextEditingController _frequencyTextController; late TextEditingController _frequencyVolumeTextController; late TextEditingController _waveFormTextController; @@ -25,31 +28,23 @@ class _AudioPlayerCardState extends State { super.initState(); _filenameTextController = TextEditingController( text: "${OpenEarableSettings().selectedFilename}"); - _jingleTextController = - TextEditingController(text: OpenEarableSettings().selectedJingle); _frequencyTextController = TextEditingController( text: "${OpenEarableSettings().selectedFrequency}"); _frequencyVolumeTextController = TextEditingController( text: "${OpenEarableSettings().selectedFrequencyVolume}"); - _waveFormTextController = - TextEditingController(text: OpenEarableSettings().selectedWaveForm); } void updateText() { if (_openEarable.bleManager.connected) { OpenEarableSettings().selectedFilename = _filenameTextController.text; - OpenEarableSettings().selectedJingle = _jingleTextController.text; OpenEarableSettings().selectedFrequency = _frequencyTextController.text; OpenEarableSettings().selectedFrequencyVolume = _frequencyVolumeTextController.text; - OpenEarableSettings().selectedWaveForm = _waveFormTextController.text; } else { _filenameTextController.text = OpenEarableSettings().selectedFilename; - _jingleTextController.text = OpenEarableSettings().selectedJingle; _frequencyTextController.text = OpenEarableSettings().selectedFrequency; _frequencyVolumeTextController.text = OpenEarableSettings().selectedFrequencyVolume; - _waveFormTextController.text = OpenEarableSettings().selectedWaveForm; } } @@ -184,7 +179,9 @@ class _AudioPlayerCardState extends State { padding: const EdgeInsets.symmetric(horizontal: 5.0), child: Card( //Audio Player Card - color: Theme.of(context).colorScheme.primary, + color: Platform.isIOS + ? CupertinoTheme.of(context).primaryContrastingColor + : Theme.of(context).colorScheme.primary, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -201,7 +198,10 @@ class _AudioPlayerCardState extends State { _getFileNameRow(), _getJingleRow(), _getFrequencyRow(), - _getButtonRow(), + SizedBox(height: 4), + Platform.isIOS + ? _getCupertinoButtonRow() + : _getMaterialButtonRow(), ], ), ), @@ -210,23 +210,43 @@ class _AudioPlayerCardState extends State { } Widget _getAudioPlayerRadio(int index) { - return Radio( - value: index, - groupValue: OpenEarableSettings().selectedAudioPlayerRadio, - onChanged: !_openEarable.bleManager.connected - ? null - : (int? value) { - setState(() { - OpenEarableSettings().selectedAudioPlayerRadio = value ?? 0; - }); - }, - fillColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { - return Theme.of(context).colorScheme.secondary; - } - return Colors.grey; - }), - ); + if (Platform.isIOS) { + return Padding( + padding: EdgeInsets.all(12), + child: CupertinoRadio( + value: index, + groupValue: OpenEarableSettings().selectedAudioPlayerRadio, + onChanged: !_openEarable.bleManager.connected + ? null + : (int? value) { + setState(() { + OpenEarableSettings().selectedAudioPlayerRadio = + value ?? 0; + }); + }, + activeColor: CupertinoTheme.of(context).primaryColor, + fillColor: CupertinoTheme.of(context).primaryContrastingColor, + inactiveColor: CupertinoTheme.of(context).primaryContrastingColor, + )); + } else { + return Radio( + value: index, + groupValue: OpenEarableSettings().selectedAudioPlayerRadio, + onChanged: !_openEarable.bleManager.connected + ? null + : (int? value) { + setState(() { + OpenEarableSettings().selectedAudioPlayerRadio = value ?? 0; + }); + }, + fillColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return Theme.of(context).colorScheme.secondary; + } + return Colors.grey; + }), + ); + } } Widget _getFileNameRow() { @@ -235,35 +255,71 @@ class _AudioPlayerCardState extends State { _getAudioPlayerRadio(0), Expanded( child: SizedBox( - height: 37.0, - child: TextField( - controller: _filenameTextController, - obscureText: false, - enabled: _openEarable.bleManager.connected, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), - decoration: InputDecoration( - contentPadding: EdgeInsets.all(10), - border: OutlineInputBorder(), - floatingLabelBehavior: FloatingLabelBehavior.never, - labelStyle: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), - filled: true, - fillColor: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey[200], - ), - ), - ), + height: 37.0, + child: _fileNameTextField( + _filenameTextController, TextInputType.text, null, null)), ), ], ); } + Widget _fileNameTextField(TextEditingController textController, + TextInputType keyboardType, String? placeholder, int? maxLength) { + if (Platform.isIOS) { + return CupertinoTextField( + cursorColor: Colors.blue, + controller: textController, + obscureText: false, + placeholder: placeholder, + style: TextStyle( + color: _openEarable.bleManager.connected ? Colors.black : Colors.grey, + ), + padding: EdgeInsets.fromLTRB(8, 0, 0, 0), + textAlignVertical: TextAlignVertical.center, + textInputAction: TextInputAction.done, + onSubmitted: (_) { + FocusScope.of(context).requestFocus(FocusNode()); + }, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4.0), + ), + placeholderStyle: TextStyle( + color: _openEarable.bleManager.connected ? Colors.black : Colors.grey, + ), + keyboardType: keyboardType, + maxLength: maxLength, + maxLines: 1, + ); + } else { + return TextField( + controller: textController, + obscureText: false, + enabled: _openEarable.bleManager.connected, + style: TextStyle( + color: + _openEarable.bleManager.connected ? Colors.black : Colors.grey), + decoration: InputDecoration( + labelText: placeholder, + contentPadding: EdgeInsets.all(8), + border: OutlineInputBorder(), + floatingLabelBehavior: FloatingLabelBehavior.never, + labelStyle: TextStyle( + color: _openEarable.bleManager.connected + ? Colors.black + : Colors.grey), + filled: true, + fillColor: _openEarable.bleManager.connected + ? Colors.white + : Colors.grey[200], + ), + keyboardType: keyboardType, + maxLength: maxLength, + maxLines: 1, + ); + } + } + Widget _getJingleRow() { return Row( children: [ @@ -271,30 +327,15 @@ class _AudioPlayerCardState extends State { Expanded( child: SizedBox( height: 37.0, - child: InkWell( - onTap: _openEarable.bleManager.connected - ? () { - _showSoundPicker(context, OpenEarableSettings().jingleMap, - _jingleTextController); - } - : null, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 12.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - _jingleTextController.text, - style: TextStyle(fontSize: 16.0), - ), - Icon(Icons.arrow_drop_down), - ], - ), - ), - ), + child: _valuePicker( + context, + OpenEarableSettings().jingleMap.keys.toList(), + OpenEarableSettings().selectedJingle, + (_) => null, (newValue) { + setState(() { + OpenEarableSettings().selectedJingle = newValue; + }); + }), ), ), ], @@ -306,35 +347,12 @@ class _AudioPlayerCardState extends State { children: [ _getAudioPlayerRadio(2), SizedBox( - height: 37.0, - width: 75, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 0), - child: TextField( - controller: _frequencyTextController, - textAlign: TextAlign.end, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), - decoration: InputDecoration( - contentPadding: EdgeInsets.all(10), - floatingLabelBehavior: FloatingLabelBehavior.never, - border: OutlineInputBorder(), - labelText: '440', - filled: true, - labelStyle: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), - fillColor: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey[200], - ), - keyboardType: TextInputType.number, - ), - ), - ), + height: 37.0, + width: 75, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 0), + child: _fileNameTextField(_frequencyTextController, + TextInputType.number, "440", null))), Padding( padding: const EdgeInsets.symmetric(horizontal: 5), child: Text( @@ -347,37 +365,10 @@ class _AudioPlayerCardState extends State { ), Spacer(), SizedBox( - height: 37.0, - width: 52, - child: TextField( - controller: _frequencyVolumeTextController, - textAlign: TextAlign.end, - autofocus: false, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), - decoration: InputDecoration( - contentPadding: EdgeInsets.all(10), - floatingLabelBehavior: FloatingLabelBehavior.never, - border: OutlineInputBorder(), - labelText: '50', - filled: true, - isDense: true, - counterText: "", - labelStyle: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), - fillColor: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey[200], - ), - maxLength: 3, - maxLines: 1, - keyboardType: TextInputType.number, - ), - ), + height: 37.0, + width: 52, + child: _fileNameTextField( + _frequencyVolumeTextController, TextInputType.number, "50", 3)), Padding( padding: const EdgeInsets.symmetric(horizontal: 5), child: Text( @@ -392,40 +383,128 @@ class _AudioPlayerCardState extends State { SizedBox( height: 37.0, width: 107, - child: InkWell( - onTap: _openEarable.bleManager.connected - ? () { - _showSoundPicker(context, OpenEarableSettings().waveFormMap, - _waveFormTextController); - } - : null, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 12.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), + child: _valuePicker( + context, + OpenEarableSettings().waveFormMap.keys.toList(), + OpenEarableSettings().selectedWaveForm, + (_) => null, (newValue) { + setState(() { + OpenEarableSettings().selectedWaveForm = newValue; + }); + }), + ), + ], + ); + } + + Widget _valuePicker( + BuildContext context, + List options, + String currentValue, + Function(bool?) changeBool, + Function(String) changeSelection) { + if (Platform.isIOS) { + return CupertinoButton( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + color: Colors.white, + padding: EdgeInsets.fromLTRB(8, 0, 0, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + currentValue, + style: TextStyle( + color: _openEarable.bleManager.connected + ? Colors.black + : Colors.grey, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - _waveFormTextController.text, - style: TextStyle(fontSize: 16.0), - overflow: TextOverflow.ellipsis, - softWrap: false, - ), - ), - Icon(Icons.arrow_drop_down), - ], + ), + ], + ), + onPressed: () => _showCupertinoPicker( + context, options, currentValue, changeBool, changeSelection), + ); + } else { + return DropdownButton( + dropdownColor: + _openEarable.bleManager.connected ? Colors.white : Colors.grey[200], + alignment: Alignment.centerRight, + value: currentValue, + onChanged: (String? newValue) { + setState(() { + changeSelection(newValue!); + if (int.parse(newValue) != 0) { + changeBool(true); + } else { + changeBool(false); + } + }); + }, + items: options.map((String value) { + return DropdownMenuItem( + alignment: Alignment.centerRight, + value: value, + child: Text( + value, + style: TextStyle( + color: _openEarable.bleManager.connected + ? Colors.black + : Colors.grey, ), + textAlign: TextAlign.end, ), - ), + ); + }).toList(), + underline: Container(), + icon: Icon( + Icons.arrow_drop_down, + color: _openEarable.bleManager.connected ? Colors.black : Colors.grey, ), - ], + ); + } + } + + void _showCupertinoPicker(context, List options, String currentValue, + Function(bool?) changeBool, Function(String) changeSelection) { + showCupertinoModalPopup( + context: context, + builder: (_) => Container( + height: 200, + color: Colors.white, + child: CupertinoPicker( + backgroundColor: _openEarable.bleManager.connected + ? Colors.white + : Colors.grey[200], + itemExtent: 32, // Height of each item + onSelectedItemChanged: (int index) { + setState(() { + String newValue = options[index]; + changeSelection(newValue); + if (int.parse(newValue) != 0) { + changeBool(true); + } else { + changeBool(false); + } + }); + }, + children: options + .map((String value) => Center( + child: Text( + value, + style: TextStyle( + color: _openEarable.bleManager.connected + ? Colors.black + : Colors.grey, + ), + ), + )) + .toList(), + ), + ), ); } - Widget _getButtonRow() { + Widget _getMaterialButtonRow() { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, // Align buttons to the space between @@ -473,4 +552,53 @@ class _AudioPlayerCardState extends State { ], ); } + + Widget _getCupertinoButtonRow() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 120, + child: CupertinoButton( + padding: EdgeInsets.all(0), + onPressed: _openEarable.bleManager.connected + ? _setSourceButtonPressed + : null, + color: Color(0xff53515b), + child: Text( + 'Set\nSource', + style: TextStyle(color: Colors.white, fontSize: 15), + ), + ), + ), + SizedBox(width: 4), + Expanded( + child: CupertinoButton( + padding: EdgeInsets.all(0), + onPressed: + _openEarable.bleManager.connected ? _playButtonPressed : null, + color: CupertinoTheme.of(context).primaryColor, + child: Icon(CupertinoIcons.play), + )), + SizedBox(width: 4), + Expanded( + child: CupertinoButton( + padding: EdgeInsets.zero, + onPressed: + _openEarable.bleManager.connected ? _pauseButtonPressed : null, + color: Color(0xffe0f277), + child: Icon(CupertinoIcons.pause), + )), + SizedBox(width: 4), + Expanded( + child: CupertinoButton( + padding: EdgeInsets.zero, + onPressed: + _openEarable.bleManager.connected ? _stopButtonPressed : null, + color: Color(0xfff27777), + child: Icon(CupertinoIcons.stop), + )), + ], + ); + } } diff --git a/open_earable/lib/controls_tab/views/connect.dart b/open_earable/lib/controls_tab/views/connect.dart index 7da1668..326187c 100644 --- a/open_earable/lib/controls_tab/views/connect.dart +++ b/open_earable/lib/controls_tab/views/connect.dart @@ -1,5 +1,7 @@ +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:flutter/cupertino.dart'; import '../../ble.dart'; class ConnectCard extends StatelessWidget { @@ -13,7 +15,9 @@ class ConnectCard extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 5.0), child: Card( - color: Theme.of(context).colorScheme.primary, + color: Platform.isIOS + ? CupertinoTheme.of(context).primaryContrastingColor + : Theme.of(context).colorScheme.primary, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -27,8 +31,9 @@ class ConnectCard extends StatelessWidget { fontWeight: FontWeight.bold, ), ), - SizedBox(height: 5), + SizedBox(height: 8), _getEarableInfo(), + SizedBox(height: 8), _getConnectButton(context), ], ), @@ -76,33 +81,30 @@ class ConnectCard extends StatelessWidget { Widget _getConnectButton(BuildContext context) { return Visibility( visible: !_openEarable.bleManager.connected, - child: Column( - children: [ - SizedBox(height: 10), - Row( - children: [ - Expanded( - child: SizedBox( - height: 37.0, - child: ElevatedButton( - onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => BLEPage(_openEarable))); - }, - style: ElevatedButton.styleFrom( - backgroundColor: !_openEarable.bleManager.connected - ? Color(0xff77F2A1) - : Color(0xfff27777), - foregroundColor: Colors.black, - ), - child: Text("Connect"), + child: Container( + height: 37, + width: double.infinity, + child: !Platform.isIOS + ? ElevatedButton( + onPressed: () => _connectButtonAction(context), + style: ElevatedButton.styleFrom( + backgroundColor: !_openEarable.bleManager.connected + ? Color(0xff77F2A1) + : Color(0xfff27777), + foregroundColor: Colors.black, ), - ), - ), - ], - ), - ], - ), + child: Text("Connect"), + ) + : CupertinoButton( + padding: EdgeInsets.zero, + color: CupertinoTheme.of(context).primaryColor, + child: Text("Connect"), + onPressed: () => _connectButtonAction(context))), ); } + + _connectButtonAction(BuildContext context) { + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) => BLEPage(_openEarable))); + } } diff --git a/open_earable/lib/controls_tab/views/led_color.dart b/open_earable/lib/controls_tab/views/led_color.dart index 253d450..97f32f1 100644 --- a/open_earable/lib/controls_tab/views/led_color.dart +++ b/open_earable/lib/controls_tab/views/led_color.dart @@ -1,8 +1,10 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:open_earable/controls_tab/models/open_earable_settings.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import 'dart:async'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; +import 'dart:io'; class LEDColorCard extends StatefulWidget { final OpenEarable _openEarable; @@ -113,35 +115,72 @@ class _LEDColorCardState extends State { */ void _openColorPicker() { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Pick a color for the RGB LED'), - content: SingleChildScrollView( - child: ColorPicker( - pickerColor: OpenEarableSettings().selectedColor, - onColorChanged: (color) { - setState(() { - OpenEarableSettings().selectedColor = color; - }); - }, - showLabel: true, - pickerAreaHeightPercent: 0.8, - enableAlpha: false, + if (Platform.isAndroid) { + showDialog( + context: context, + builder: (BuildContext context) { + return Material( + child: AlertDialog( + title: const Text('Pick a color for the RGB LED'), + content: SingleChildScrollView( + child: ColorPicker( + pickerColor: OpenEarableSettings().selectedColor, + onColorChanged: (color) { + setState(() { + OpenEarableSettings().selectedColor = color; + }); + }, + showLabel: true, + pickerAreaHeightPercent: 0.8, + enableAlpha: false, + ), ), - ), - actions: [ - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Done'), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Done'), + ), + ], + )); + }, + ); + } else { + showCupertinoDialog( + context: context, + builder: (BuildContext context) { + return CupertinoAlertDialog( + title: Text('Pick a color for the RGB LED'), + content: SingleChildScrollView( + child: Material( + // Wrap with Material + child: ColorPicker( + pickerColor: OpenEarableSettings().selectedColor, + onColorChanged: (color) { + // Your color change logic + setState(() { + OpenEarableSettings().selectedColor = color; + }); + }, + showLabel: true, + pickerAreaHeightPercent: 0.8, + enableAlpha: false, + ), + ), ), - ], - ); - }, - ); + actions: [ + CupertinoDialogAction( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text('Done'), + ), + ], + ); + }, + ); + } } @override @@ -150,7 +189,9 @@ class _LEDColorCardState extends State { padding: const EdgeInsets.symmetric(horizontal: 5.0), child: Card( //LED Color Picker Card - color: Theme.of(context).colorScheme.primary, + color: Platform.isIOS + ? CupertinoTheme.of(context).primaryContrastingColor + : Theme.of(context).colorScheme.primary, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -182,39 +223,73 @@ class _LEDColorCardState extends State { SizedBox(width: 5), SizedBox( width: 66, - child: ElevatedButton( - onPressed: _openEarable.bleManager.connected - ? _setLEDColor - : null, - style: ElevatedButton.styleFrom( - backgroundColor: Color( - 0xff53515b), // Set the background color to grey - foregroundColor: Colors.white), - child: Text('Set'), - ), + height: 36, + child: Platform.isIOS + ? CupertinoButton( + padding: EdgeInsets.zero, + child: Text('Set', + style: TextStyle(color: Colors.white)), + onPressed: _openEarable.bleManager.connected + ? _setLEDColor + : null, + color: Color(0xff53515b), + ) + : ElevatedButton( + onPressed: _openEarable.bleManager.connected + ? _setLEDColor + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Color( + 0xff53515b), // Set the background color to grey + foregroundColor: Colors.white), + child: Text('Set'), + ), ), SizedBox(width: 5), - ElevatedButton( - onPressed: _openEarable.bleManager.connected - ? _startRainbowMode - : null, - style: ElevatedButton.styleFrom( - backgroundColor: Color( - 0xff53515b), // Set the background color to grey - foregroundColor: Colors.white), - child: Text("🦄"), - ), + SizedBox( + width: 66, + height: 36, + child: Platform.isIOS + ? CupertinoButton( + onPressed: _openEarable.bleManager.connected + ? _startRainbowMode + : null, + color: Color(0xff53515b), + padding: EdgeInsets.zero, + child: Text("🦄")) + : ElevatedButton( + onPressed: _openEarable.bleManager.connected + ? _startRainbowMode + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Color( + 0xff53515b), // Set the background color to grey + foregroundColor: Colors.white), + child: Text("🦄"), + )), Spacer(), - ElevatedButton( - onPressed: _openEarable.bleManager.connected - ? _turnLEDoff - : null, - style: ElevatedButton.styleFrom( - backgroundColor: Color(0xfff27777), - foregroundColor: Colors.black, - ), - child: Text('Off'), - ), + SizedBox( + width: 66, + height: 36, + child: Platform.isIOS + ? CupertinoButton( + onPressed: _openEarable.bleManager.connected + ? _turnLEDoff + : null, + padding: EdgeInsets.zero, + color: Color(0xfff27777), + child: Text('Off')) + : ElevatedButton( + onPressed: _openEarable.bleManager.connected + ? _turnLEDoff + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xfff27777), + foregroundColor: Colors.black, + ), + child: Text('Off'), + ), + ) ], ), ], diff --git a/open_earable/lib/controls_tab/views/sensor_configuration.dart b/open_earable/lib/controls_tab/views/sensor_configuration.dart index eee6266..a866621 100644 --- a/open_earable/lib/controls_tab/views/sensor_configuration.dart +++ b/open_earable/lib/controls_tab/views/sensor_configuration.dart @@ -1,5 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'dart:io'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import '../models/open_earable_settings.dart'; @@ -98,7 +99,9 @@ class _SensorConfigurationCardState extends State { padding: const EdgeInsets.symmetric(horizontal: 5.0), child: Card( //Audio Player Card - color: Theme.of(context).colorScheme.primary, + color: Platform.isIOS + ? CupertinoTheme.of(context).primaryContrastingColor + : Theme.of(context).colorScheme.primary, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -156,20 +159,33 @@ class _SensorConfigurationCardState extends State { children: [ Expanded( child: SizedBox( - height: 37.0, - child: ElevatedButton( - onPressed: _openEarable.bleManager.connected - ? _writeSensorConfigs - : null, - style: ElevatedButton.styleFrom( - backgroundColor: _openEarable.bleManager.connected - ? Theme.of(context).colorScheme.secondary - : Colors.grey, - foregroundColor: Colors.black, - enableFeedback: _openEarable.bleManager.connected, - ), - child: Text("Set Configuration"), - ), + height: 37, + child: Platform.isIOS + ? CupertinoButton( + padding: EdgeInsets.zero, + onPressed: _openEarable.bleManager.connected + ? () => _writeSensorConfigs() + : null, + color: _openEarable.bleManager.connected + ? CupertinoTheme.of(context).primaryColor + : Colors.grey, + child: Text("Set Configuration"), + ) + : ElevatedButton( + onPressed: _openEarable.bleManager.connected + ? () => _writeSensorConfigs() + : null, + style: ElevatedButton.styleFrom( + backgroundColor: _openEarable + .bleManager.connected + ? Theme.of(context).colorScheme.secondary + : Colors.grey, + foregroundColor: Colors.black, + enableFeedback: + _openEarable.bleManager.connected, + ), + child: Text("Set Configuration"), + ), ), ), ], @@ -190,13 +206,29 @@ class _SensorConfigurationCardState extends State { Function(String) changeSelection) { return Row( children: [ - Checkbox( - checkColor: Theme.of(context).colorScheme.primary, - fillColor: MaterialStateProperty.resolveWith(_getCheckboxColor), - value: settingSelected, - onChanged: _openEarable.bleManager.connected ? changeBool : null, + Platform.isIOS + ? CupertinoCheckbox( + value: settingSelected, + onChanged: + _openEarable.bleManager.connected ? changeBool : null, + activeColor: settingSelected + ? CupertinoTheme.of(context).primaryColor + : CupertinoTheme.of(context).primaryContrastingColor, + checkColor: CupertinoTheme.of(context).primaryContrastingColor, + ) + : Checkbox( + checkColor: Theme.of(context).colorScheme.primary, + fillColor: MaterialStateProperty.resolveWith(_getCheckboxColor), + value: settingSelected, + onChanged: + _openEarable.bleManager.connected ? changeBool : null, + ), + Text( + sensorName, + style: TextStyle( + color: Color.fromRGBO(168, 168, 172, 1.0), + ), ), - Text(sensorName), Spacer(), Container( decoration: BoxDecoration( @@ -206,52 +238,122 @@ class _SensorConfigurationCardState extends State { borderRadius: BorderRadius.circular(4.0), ), child: SizedBox( - height: 37, width: 100, + height: 37, child: Container( alignment: Alignment.centerRight, - child: DropdownButton( - dropdownColor: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey[200], - alignment: Alignment.centerRight, - value: currentValue, - onChanged: (String? newValue) { - setState(() { - changeSelection(newValue!); - if (int.parse(newValue) != 0) { - changeBool(true); - } else { - changeBool(false); - } - }); - }, - items: options.map((String value) { - return DropdownMenuItem( - alignment: Alignment.centerRight, - value: value, - child: Text( - value, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey, - ), - textAlign: TextAlign.end, - ), - ); - }).toList(), - underline: Container(), - icon: Icon( - Icons.arrow_drop_down, + child: _valuePicker(context, options, currentValue, + changeBool, changeSelection)))), + SizedBox(width: 8), + Text("Hz", style: TextStyle(color: Color.fromRGBO(168, 168, 172, 1.0))), + ], + ); + } + + Widget _valuePicker( + BuildContext context, + List options, + String currentValue, + Function(bool?) changeBool, + Function(String) changeSelection) { + if (Platform.isIOS) { + return CupertinoButton( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + color: Colors.white, + padding: EdgeInsets.fromLTRB(8, 0, 0, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + currentValue, + style: TextStyle( + color: _openEarable.bleManager.connected + ? Colors.black + : Colors.grey, + ), + ), + ], + ), + onPressed: () => _showCupertinoPicker( + context, options, currentValue, changeBool, changeSelection), + ); + } else { + return DropdownButton( + dropdownColor: + _openEarable.bleManager.connected ? Colors.white : Colors.grey[200], + alignment: Alignment.centerRight, + value: currentValue, + onChanged: (String? newValue) { + setState(() { + changeSelection(newValue!); + if (int.parse(newValue) != 0) { + changeBool(true); + } else { + changeBool(false); + } + }); + }, + items: options.map((String value) { + return DropdownMenuItem( + alignment: Alignment.centerRight, + value: value, + child: Text( + value, + style: TextStyle( + color: _openEarable.bleManager.connected + ? Colors.black + : Colors.grey, + ), + textAlign: TextAlign.end, + ), + ); + }).toList(), + underline: Container(), + icon: Icon( + Icons.arrow_drop_down, + color: _openEarable.bleManager.connected ? Colors.black : Colors.grey, + ), + ); + } + } + + void _showCupertinoPicker(context, List options, String currentValue, + Function(bool?) changeBool, Function(String) changeSelection) { + showCupertinoModalPopup( + context: context, + builder: (_) => Container( + height: 200, + color: Colors.white, + child: CupertinoPicker( + backgroundColor: _openEarable.bleManager.connected + ? Colors.white + : Colors.grey[200], + itemExtent: 32, // Height of each item + onSelectedItemChanged: (int index) { + setState(() { + String newValue = options[index]; + changeSelection(newValue); + if (int.parse(newValue) != 0) { + changeBool(true); + } else { + changeBool(false); + } + }); + }, + children: options + .map((String value) => Center( + child: Text( + value, + style: TextStyle( color: _openEarable.bleManager.connected ? Colors.black : Colors.grey, ), - )))), - SizedBox(width: 8), - Text("Hz"), - ], + ), + )) + .toList(), + ), + ), ); } } diff --git a/open_earable/lib/main.dart b/open_earable/lib/main.dart index 55f4f11..7eec192 100644 --- a/open_earable/lib/main.dart +++ b/open_earable/lib/main.dart @@ -1,6 +1,6 @@ +import 'dart:io'; import 'dart:async'; - -import 'package:provider/provider.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:open_earable/open_earable_icon_icons.dart'; import 'controls_tab/controls_tab.dart'; @@ -9,36 +9,56 @@ import 'ble.dart'; import 'apps_tab.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:open_earable/controls_tab/models/open_earable_settings.dart'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:app_settings/app_settings.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { + final ThemeData materialTheme = ThemeData( + useMaterial3: false, + colorScheme: ColorScheme( + brightness: Brightness.dark, + primary: + Color.fromARGB(255, 54, 53, 59), //Color.fromARGB(255, 22, 22, 24) + onPrimary: Colors.white, + secondary: Color.fromARGB(255, 119, 242, 161), + onSecondary: Colors.white, + error: Colors.red, + onError: Colors.black, + background: + Color.fromARGB(255, 22, 22, 24), //Color.fromARGB(255, 54, 53, 59) + onBackground: Colors.white, + surface: Color.fromARGB(255, 22, 22, 24), + onSurface: Colors.white), + secondaryHeaderColor: Colors.black); + + final CupertinoThemeData cupertinoTheme = CupertinoThemeData( + brightness: Brightness.dark, + primaryColor: Color.fromARGB(255, 119, 242, 161), + primaryContrastingColor: Color.fromARGB(255, 54, 53, 59), + barBackgroundColor: Color.fromARGB(255, 22, 22, 24), + scaffoldBackgroundColor: Color.fromARGB(255, 22, 22, 24), + textTheme: CupertinoTextThemeData( + primaryColor: Colors.white, + ), + ); + @override Widget build(BuildContext context) { - return MaterialApp( - title: '🦻 OpenEarable', - theme: ThemeData( - useMaterial3: false, - colorScheme: ColorScheme( - brightness: Brightness.dark, - primary: Color.fromARGB( - 255, 54, 53, 59), //Color.fromARGB(255, 22, 22, 24) - onPrimary: Colors.white, - secondary: Color.fromARGB(255, 119, 242, 161), - onSecondary: Colors.white, - error: Colors.red, - onError: Colors.black, - background: Color.fromARGB( - 255, 22, 22, 24), //Color.fromARGB(255, 54, 53, 59) - onBackground: Colors.white, - surface: Color.fromARGB(255, 22, 22, 24), - onSurface: Colors.white), - secondaryHeaderColor: Colors.black), - home: MyHomePage(), - ); + if (Platform.isIOS) { + return CupertinoApp( + title: '🦻 OpenEarable', + theme: cupertinoTheme, + home: MyHomePage(), + ); + } else { + return MaterialApp( + title: '🦻 OpenEarable', + theme: materialTheme, + home: MyHomePage(), + ); + } } } @@ -92,40 +112,77 @@ class _MyHomePageState extends State { } void _showBluetoothAlert(BuildContext context) { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return AlertDialog( - title: Text("Bluetooth disabled"), - content: Text( - "Please make sure your device's bluetooth and location services are turned on and this app has been granted permission to use them in the app's settings.\nThis alert can only be closed if these requirements are fulfilled."), - actions: [ - TextButton( - child: Text( - 'Open App Settings', - style: TextStyle( - color: Theme.of(context).colorScheme.onBackground), - ), - onPressed: () { - AppSettings.openAppSettings(); - }, - ), - TextButton( - child: Text('OK', + if (Platform.isIOS) { + showCupertinoModalPopup( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => CupertinoTheme( + data: CupertinoThemeData(), + child: CupertinoAlertDialog( + title: const Text('Bluetooth disabled'), + content: const Text( + "Please make sure your device's bluetooth and location services are turned on and this app has been granted permission to use them in the app's settings.\nThis alert can only be closed if these requirements are fulfilled."), + actions: [ + CupertinoDialogAction( + isDefaultAction: false, + onPressed: () { + AppSettings.openAppSettings(); + }, + child: Text( + 'Open App Settings', + ), + ), + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () { + if (flutterReactiveBle.status == BleStatus.ready) { + alertOpen = false; + Navigator.of(context).pop(); + } + }, + child: Text( + 'OK', + ), + ), + ], + ), + )); + } else { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text("Bluetooth disabled"), + content: Text( + "Please make sure your device's bluetooth and location services are turned on and this app has been granted permission to use them in the app's settings.\nThis alert can only be closed if these requirements are fulfilled."), + actions: [ + TextButton( + child: Text( + 'Open App Settings', style: TextStyle( - color: Theme.of(context).colorScheme.onBackground)), - onPressed: () { - if (flutterReactiveBle.status == BleStatus.ready) { - alertOpen = false; - Navigator.of(context).pop(); - } - }, - ), - ], - ); - }, - ); + color: Theme.of(context).colorScheme.onBackground), + ), + onPressed: () { + AppSettings.openAppSettings(); + }, + ), + TextButton( + child: Text('OK', + style: TextStyle( + color: Theme.of(context).colorScheme.onBackground)), + onPressed: () { + if (flutterReactiveBle.status == BleStatus.ready) { + alertOpen = false; + Navigator.of(context).pop(); + } + }, + ), + ], + ); + }, + ); + } } void _onItemTapped(int index) { @@ -136,62 +193,117 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, - appBar: AppBar( - title: Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox(width: 50), - Image.asset( - 'assets/earable_logo.png', // Replace with your image path - width: 24, // Adjust the width as needed - height: 24, // Adjust the height as needed + if (Platform.isIOS) { + return CupertinoTabScaffold( + tabBar: CupertinoTabBar( + backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor, + items: [ + BottomNavigationBarItem( + icon: Icon(CupertinoIcons.gear), + label: 'Controls', + ), + BottomNavigationBarItem( + icon: Icon(CupertinoIcons.heart), + label: 'Sensor Data', + ), + BottomNavigationBarItem( + icon: Icon(CupertinoIcons.square_grid_2x2), + label: 'Apps', ), - SizedBox(width: 8), // Add spacing between the image and text - Text('OpenEarable'), ], - )), - actions: [ - IconButton( - icon: Icon(Icons.bluetooth, - color: Theme.of(context).colorScheme.secondary), - onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => BLEPage(_openEarable))); - }, - ), - ], - ), - body: _widgetOptions.elementAt(_selectedIndex), - bottomNavigationBar: BottomNavigationBar( + currentIndex: _selectedIndex, + onTap: _onItemTapped, + ), + tabBuilder: (BuildContext context, int index) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/earable_logo.png', + width: 24, + height: 24, + ), + SizedBox(width: 8), + Text('OpenEarable'), + ], + ), + trailing: CupertinoButton( + padding: EdgeInsets.zero, + child: Icon( + CupertinoIcons.bluetooth, + color: CupertinoTheme.of(context).primaryColor, + ), + onPressed: () { + Navigator.of(context).push( + CupertinoPageRoute( + builder: (context) => BLEPage(_openEarable)), + ); + }, + ), + ), + backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor, + child: _widgetOptions.elementAt(index), + ); + }, + ); + } else { + return Scaffold( backgroundColor: Theme.of(context).colorScheme.background, - items: const [ - BottomNavigationBarItem( - icon: Padding( - padding: EdgeInsets.only( - bottom: - 3.0), // Adjust the bottom padding to your desired value - child: Icon( - OpenEarableIcon.icon, - size: 20.0, // Change the size to your desired value + appBar: AppBar( + title: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(width: 50), + Image.asset( + 'assets/earable_logo.png', + width: 24, + height: 24, ), + SizedBox(width: 8), + Text('OpenEarable'), + ], + )), + actions: [ + IconButton( + icon: Icon(Icons.bluetooth, + color: Theme.of(context).colorScheme.secondary), + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => BLEPage(_openEarable))); + }, ), - label: 'Controls', - ), - BottomNavigationBarItem( - icon: Icon(Icons.monitor_heart_outlined), - label: 'Sensor Data', - ), - BottomNavigationBarItem( - icon: Icon(Icons.apps), - label: 'Apps', - ), - ], - currentIndex: _selectedIndex, - onTap: _onItemTapped, - ), - ); + ], + ), + body: _widgetOptions.elementAt(_selectedIndex), + bottomNavigationBar: BottomNavigationBar( + backgroundColor: Theme.of(context).colorScheme.background, + items: const [ + BottomNavigationBarItem( + icon: Padding( + padding: EdgeInsets.only(bottom: 3.0), + child: Icon( + OpenEarableIcon.icon, + size: 20.0, + ), + ), + label: 'Controls', + ), + BottomNavigationBarItem( + icon: Icon(Icons.monitor_heart_outlined), + label: 'Sensor Data', + ), + BottomNavigationBarItem( + icon: Icon(Icons.apps), + label: 'Apps', + ), + ], + currentIndex: _selectedIndex, + onTap: _onItemTapped, + ), + ); + } } } diff --git a/open_earable/macos/Runner.xcodeproj/project.pbxproj b/open_earable/macos/Runner.xcodeproj/project.pbxproj index 843b5e3..84a9d1b 100644 --- a/open_earable/macos/Runner.xcodeproj/project.pbxproj +++ b/open_earable/macos/Runner.xcodeproj/project.pbxproj @@ -227,7 +227,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/open_earable/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/open_earable/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a028d7a..7722b11 100644 --- a/open_earable/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/open_earable/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.2.0-194.0.dev <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.16.0" diff --git a/open_earable/pubspec.yaml b/open_earable/pubspec.yaml index f709a98..d75da34 100644 --- a/open_earable/pubspec.yaml +++ b/open_earable/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 - + english_words: ^4.0.0 ditredi: ^2.0.0 vector_math: ^2.1.4 From 5ef1f2dd55f6b5f70f99743735337dc46757f130 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Fri, 5 Jan 2024 16:46:48 +0000 Subject: [PATCH 016/104] device hardware versionis now shown on controls page --- open_earable/lib/controls_tab/views/connect.dart | 9 ++++++++- open_earable/pubspec.lock | 14 ++++++-------- open_earable/pubspec.yaml | 3 +-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/open_earable/lib/controls_tab/views/connect.dart b/open_earable/lib/controls_tab/views/connect.dart index 326187c..b646249 100644 --- a/open_earable/lib/controls_tab/views/connect.dart +++ b/open_earable/lib/controls_tab/views/connect.dart @@ -57,7 +57,14 @@ class ConnectCard extends StatelessWidget { ), ), Text( - "Firmware ${_openEarable.deviceFirmwareVersion ?? "0.0.0"}", + "Firmware: ${_openEarable.deviceFirmwareVersion ?? "not available"}", + style: TextStyle( + color: Color.fromRGBO(168, 168, 172, 1.0), + fontSize: 15.0, + ), + ), + Text( + "Hardware: ${_openEarable.deviceHardwareVersion ?? "not available"}", style: TextStyle( color: Color.fromRGBO(168, 168, 172, 1.0), fontSize: 15.0, diff --git a/open_earable/pubspec.lock b/open_earable/pubspec.lock index 5b213c2..194ab6a 100644 --- a/open_earable/pubspec.lock +++ b/open_earable/pubspec.lock @@ -356,12 +356,10 @@ packages: open_earable_flutter: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: "6841c0b704dffbe559961ea80dccf73abf0c293a" - url: "https://github.com/OpenEarable/open_earable_flutter.git" - source: git - version: "0.0.2" + path: "../../open_earable_flutter" + relative: true + source: path + version: "0.0.1" open_file: dependency: "direct main" description: @@ -398,10 +396,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_foundation: dependency: transitive description: diff --git a/open_earable/pubspec.yaml b/open_earable/pubspec.yaml index d75da34..cb792ad 100644 --- a/open_earable/pubspec.yaml +++ b/open_earable/pubspec.yaml @@ -31,8 +31,7 @@ dependencies: flutter: sdk: flutter open_earable_flutter: - git: - url: https://github.com/OpenEarable/open_earable_flutter.git + path: ../../open_earable_flutter permission_handler: ^11.1.0 flutter_reactive_ble: ^5.2.0 app_settings: ^5.1.1 From 77e4822bcd04f2917b99cbbe15459c9a202525bc Mon Sep 17 00:00:00 2001 From: Polaris Date: Fri, 8 Dec 2023 11:56:52 +0100 Subject: [PATCH 017/104] Add .idea to gitignore rules --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 9e6aaf3..a2ed22c 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,6 @@ Temporary Items .history .ionide .vscode/settings.json + +# idea folder +.idea From 5430eea7bd24e7a47dfb33c9f4ab4418251730d5 Mon Sep 17 00:00:00 2001 From: Polaris Date: Fri, 8 Dec 2023 14:53:31 +0100 Subject: [PATCH 018/104] Add NeckStretchView and NeckStretchViewModel to the project + add the prototype app to the list of apps --- .../view/neck_stretch_view.dart | 121 ++++++++++++++++++ .../view_model/neck_stretch_view_model.dart | 48 +++++++ open_earable/lib/apps_tab.dart | 12 ++ 3 files changed, 181 insertions(+) create mode 100644 open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart create mode 100644 open_earable/lib/apps/posture_tracker/view_model/neck_stretch_view_model.dart diff --git a/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart b/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart new file mode 100644 index 0000000..fec779d --- /dev/null +++ b/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart @@ -0,0 +1,121 @@ +// ignore_for_file: unnecessary_this + +import 'package:flutter/material.dart'; +import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; +import 'package:open_earable/apps/posture_tracker/view/posture_roll_view.dart'; +import 'package:open_earable/apps/posture_tracker/view_model/neck_stretch_view_model.dart'; +import 'package:provider/provider.dart'; + +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; + + +class NeckStretchView extends StatefulWidget { + final AttitudeTracker _tracker; + final OpenEarable _openEarable; + + NeckStretchView(this._tracker, this._openEarable); + + @override + State createState() => _NeckStretchViewState(); +} + +class _NeckStretchViewState extends State { + late final NeckStretchViewModel _viewModel; + + @override + void initState() { + super.initState(); + this._viewModel = NeckStretchViewModel(widget._tracker); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: _viewModel, + builder: (context, child) => Consumer( + builder: (context, neckStretchViewModel, child) => Scaffold( + appBar: AppBar( + title: const Text("Guided Neck Relaxation"), + ), + body: Center( + child: this._buildContentView(neckStretchViewModel), + ), + ) + ) + ); + } + + Widget _buildContentView(NeckStretchViewModel neckStretchViewModel) { + var headViews = this._createHeadViews(neckStretchViewModel); + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...headViews.map((e) => FractionallySizedBox( + widthFactor: .7, + child: e, + )), + this._buildTrackingButton(neckStretchViewModel), + ] + ); + } + + Widget _buildHeadView(String headAssetPath, String neckAssetPath, AlignmentGeometry headAlignment, double roll, double angleThreshold) { + return Padding( + padding: const EdgeInsets.all(5), + child: PostureRollView( + roll: roll, + angleThreshold: angleThreshold * 3.14 / 180, + headAssetPath: headAssetPath, + neckAssetPath: neckAssetPath, + headAlignment: headAlignment, + ), + ); + } + + List _createHeadViews(neckStretchViewModel) { + return [ + this._buildHeadView( + "assets/posture_tracker/Head_Front.png", + "assets/posture_tracker/Neck_Front.png", + Alignment.center.add(Alignment(0, 0.3)), + neckStretchViewModel.attitude.roll, + 4.0 + ), + this._buildHeadView( + "assets/posture_tracker/Head_Side.png", + "assets/posture_tracker/Neck_Side.png", + Alignment.center.add(Alignment(0, 0.3)), + -neckStretchViewModel.attitude.pitch, + 16.0 + ), + ]; + } + + Widget _buildTrackingButton(NeckStretchViewModel postureTrackerViewModel) { + return Column(children: [ + ElevatedButton( + onPressed: postureTrackerViewModel.isAvailable + ? () { postureTrackerViewModel.isTracking ? this._viewModel.stopTracking() : this._viewModel.startTracking(); } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: !postureTrackerViewModel.isTracking ? Color(0xff77F2A1) : Color(0xfff27777), + foregroundColor: Colors.black, + ), + child: postureTrackerViewModel.isTracking ? const Text("Stop Tracking") : const Text("Start Tracking"), + ), + Visibility( + visible: !postureTrackerViewModel.isAvailable, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: Text( + "No Earable Connected", + style: TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ) + ]); + } +} diff --git a/open_earable/lib/apps/posture_tracker/view_model/neck_stretch_view_model.dart b/open_earable/lib/apps/posture_tracker/view_model/neck_stretch_view_model.dart new file mode 100644 index 0000000..f620b3e --- /dev/null +++ b/open_earable/lib/apps/posture_tracker/view_model/neck_stretch_view_model.dart @@ -0,0 +1,48 @@ +import "package:flutter/material.dart"; +import "package:open_earable/apps/posture_tracker/model/attitude.dart"; +import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; + +class NeckStretchViewModel extends ChangeNotifier { + Attitude _attitude = Attitude(); + Attitude get attitude => _attitude; + + bool get isTracking => _attitudeTracker.isTracking; + bool get isAvailable => _attitudeTracker.isAvailable; + + AttitudeTracker _attitudeTracker; + + NeckStretchViewModel(this._attitudeTracker) { + _attitudeTracker.didChangeAvailability = (_) { + notifyListeners(); + }; + + _attitudeTracker.listen((attitude) { + _attitude = Attitude( + roll: attitude.roll, + pitch: attitude.pitch, + yaw: attitude.yaw + ); + notifyListeners(); + }); + } + + void startTracking() { + _attitudeTracker.start(); + notifyListeners(); + } + + void stopTracking() { + _attitudeTracker.stop(); + notifyListeners(); + } + + void calibrate() { + _attitudeTracker.calibrateToCurrentAttitude(); + } + + @override + void dispose() { + _attitudeTracker.cancle(); + super.dispose(); + } +} \ No newline at end of file diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 17e5a27..0ea0af0 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; import 'package:open_earable/apps/recorder.dart'; +import 'package:open_earable/apps/posture_tracker/view/neck_stretch_view.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class AppInfo { @@ -45,6 +46,17 @@ class AppsTab extends StatelessWidget { MaterialPageRoute( builder: (context) => Recorder(_openEarable))); }), + AppInfo( + iconData: Icons.fiber_smart_record, + title: "Guided neck relaxation", + description: "Use the OpenEarable to get a guided neck relaxation", + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NeckStretchView( + EarableAttitudeTracker(_openEarable), _openEarable))); + }), // ... similarly for other apps ]; } From 74412f0778328bb79bbb03ffd09968b5827be331 Mon Sep 17 00:00:00 2001 From: Polaris Date: Fri, 8 Dec 2023 15:07:02 +0100 Subject: [PATCH 019/104] Add new icon and subtext for the NeckStretch app --- .../lib/apps/posture_tracker/view/neck_stretch_view.dart | 2 +- open_earable/lib/apps_tab.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart b/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart index fec779d..ac1548a 100644 --- a/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart +++ b/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart @@ -101,7 +101,7 @@ class _NeckStretchViewState extends State { backgroundColor: !postureTrackerViewModel.isTracking ? Color(0xff77F2A1) : Color(0xfff27777), foregroundColor: Colors.black, ), - child: postureTrackerViewModel.isTracking ? const Text("Stop Tracking") : const Text("Start Tracking"), + child: postureTrackerViewModel.isTracking ? const Text("Stop Meditation") : const Text("Start Meditation"), ), Visibility( visible: !postureTrackerViewModel.isAvailable, diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 0ea0af0..23af397 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -47,9 +47,9 @@ class AppsTab extends StatelessWidget { builder: (context) => Recorder(_openEarable))); }), AppInfo( - iconData: Icons.fiber_smart_record, + iconData: Icons.self_improvement, title: "Guided neck relaxation", - description: "Use the OpenEarable to get a guided neck relaxation", + description: "Relax your neck using the OpenEarble.", onTap: () { Navigator.push( context, From a3cc67d9329b2b2e571c5917056ef68d61ddd15a Mon Sep 17 00:00:00 2001 From: Polaris Date: Sun, 10 Dec 2023 13:59:39 +0100 Subject: [PATCH 020/104] Add assets for neck stretch and enable visibility control of neck stretch widgets depending on (enum) state --- .../assets/neck_stretch/Neck_Left_Stretch.png | Bin 0 -> 20010 bytes .../assets/neck_stretch/Neck_Main_Stretch.png | Bin 0 -> 21923 bytes .../neck_stretch/Neck_Right_Stretch.png | Bin 0 -> 20134 bytes .../assets/neck_stretch/Neck_Side_Stretch.png | Bin 0 -> 22433 bytes .../view/neck_stretch_view.dart | 116 +++++++++++++----- 5 files changed, 84 insertions(+), 32 deletions(-) create mode 100644 open_earable/assets/neck_stretch/Neck_Left_Stretch.png create mode 100644 open_earable/assets/neck_stretch/Neck_Main_Stretch.png create mode 100644 open_earable/assets/neck_stretch/Neck_Right_Stretch.png create mode 100644 open_earable/assets/neck_stretch/Neck_Side_Stretch.png diff --git a/open_earable/assets/neck_stretch/Neck_Left_Stretch.png b/open_earable/assets/neck_stretch/Neck_Left_Stretch.png new file mode 100644 index 0000000000000000000000000000000000000000..8562cfb3c322d5bdb19d79f24c570643f886acc9 GIT binary patch literal 20010 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

e%Ew~0sC1q6vI9(|OQepiO_5R_2J%@{>y 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 literal 0 HcmV?d00001 diff --git a/open_earable/assets/neck_stretch/Neck_Main_Stretch.png b/open_earable/assets/neck_stretch/Neck_Main_Stretch.png new file mode 100644 index 0000000000000000000000000000000000000000..4aee89ff144af453eacfa8b1d8a1685fecd1b6e7 GIT binary patch literal 21923 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 T1W=j=L1zrj4D$cDdiVbUS(jwB literal 0 HcmV?d00001 diff --git a/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart b/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart index ac1548a..c1c6c12 100644 --- a/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart +++ b/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart @@ -8,6 +8,22 @@ import 'package:provider/provider.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +enum MeditationState { mainNeckStretch, leftNeckStretch, rightNeckStretch } + +extension MeditationStateExtension on MeditationState { + String get display { + switch (this) { + case MeditationState.mainNeckStretch: + return 'Main neck area'; + case MeditationState.leftNeckStretch: + return 'Right neck area'; + case MeditationState.rightNeckStretch: + return 'Left neck area'; + default: + return 'Invalid State'; + } + } +} class NeckStretchView extends StatefulWidget { final AttitudeTracker _tracker; @@ -21,6 +37,7 @@ class NeckStretchView extends StatefulWidget { class _NeckStretchViewState extends State { late final NeckStretchViewModel _viewModel; + var _stretchState = MeditationState.mainNeckStretch; @override void initState() { @@ -34,32 +51,35 @@ class _NeckStretchViewState extends State { value: _viewModel, builder: (context, child) => Consumer( builder: (context, neckStretchViewModel, child) => Scaffold( - appBar: AppBar( - title: const Text("Guided Neck Relaxation"), - ), - body: Center( - child: this._buildContentView(neckStretchViewModel), - ), - ) - ) - ); + appBar: AppBar( + title: const Text("Guided Neck Relaxation"), + ), + body: Center( + child: this._buildContentView(neckStretchViewModel), + ), + ))); } Widget _buildContentView(NeckStretchViewModel neckStretchViewModel) { var headViews = this._createHeadViews(neckStretchViewModel); - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ...headViews.map((e) => FractionallySizedBox( + var stretchString = this._stretchState.display; + return Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Padding( + padding: EdgeInsets.all(2), + child: Text( + "Currently stretching: $stretchString", + ), + ), + ...headViews.map((e) => FractionallySizedBox( widthFactor: .7, child: e, )), - this._buildTrackingButton(neckStretchViewModel), - ] - ); + this._buildTrackingButton(neckStretchViewModel), + ]); } - Widget _buildHeadView(String headAssetPath, String neckAssetPath, AlignmentGeometry headAlignment, double roll, double angleThreshold) { + Widget _buildHeadView(String headAssetPath, String neckAssetPath, + AlignmentGeometry headAlignment, double roll, double angleThreshold) { return Padding( padding: const EdgeInsets.all(5), child: PostureRollView( @@ -74,19 +94,43 @@ class _NeckStretchViewState extends State { List _createHeadViews(neckStretchViewModel) { return [ - this._buildHeadView( - "assets/posture_tracker/Head_Front.png", - "assets/posture_tracker/Neck_Front.png", - Alignment.center.add(Alignment(0, 0.3)), - neckStretchViewModel.attitude.roll, - 4.0 + // Visible Widgets for the main stretch + Visibility( + visible: _stretchState == MeditationState.mainNeckStretch, + child: this._buildHeadView( + "assets/posture_tracker/Head_Front.png", + "assets/neck_stretch/Neck_Side_Stretch.png", + Alignment.center.add(Alignment(0, 0.3)), + neckStretchViewModel.attitude.roll, + 4.0), + ), + Visibility( + visible: _stretchState == MeditationState.mainNeckStretch, + child: this._buildHeadView( + "assets/posture_tracker/Head_Side.png", + "assets/neck_stretch/Neck_Main_Stretch.png", + Alignment.center.add(Alignment(0, 0.3)), + -neckStretchViewModel.attitude.pitch, + 16.0), ), - this._buildHeadView( - "assets/posture_tracker/Head_Side.png", - "assets/posture_tracker/Neck_Side.png", - Alignment.center.add(Alignment(0, 0.3)), - -neckStretchViewModel.attitude.pitch, - 16.0 + // Visible Widgets for the side stretch + Visibility( + visible: _stretchState != MeditationState.mainNeckStretch, + child: this._buildHeadView( + "assets/posture_tracker/Head_Front.png", + "assets/neck_stretch/Neck_Side_Stretch.png", + Alignment.center.add(Alignment(0, 0.3)), + neckStretchViewModel.attitude.roll, + 4.0), + ), + Visibility( + visible: _stretchState != MeditationState.mainNeckStretch, + child: this._buildHeadView( + "assets/posture_tracker/Head_Side.png", + "assets/neck_stretch/Neck_Main_Stretch.png", + Alignment.center.add(Alignment(0, 0.3)), + -neckStretchViewModel.attitude.pitch, + 16.0), ), ]; } @@ -95,13 +139,21 @@ class _NeckStretchViewState extends State { return Column(children: [ ElevatedButton( onPressed: postureTrackerViewModel.isAvailable - ? () { postureTrackerViewModel.isTracking ? this._viewModel.stopTracking() : this._viewModel.startTracking(); } + ? () { + postureTrackerViewModel.isTracking + ? this._viewModel.stopTracking() + : this._viewModel.startTracking(); + } : null, style: ElevatedButton.styleFrom( - backgroundColor: !postureTrackerViewModel.isTracking ? Color(0xff77F2A1) : Color(0xfff27777), + backgroundColor: !postureTrackerViewModel.isTracking + ? Color(0xff77F2A1) + : Color(0xfff27777), foregroundColor: Colors.black, ), - child: postureTrackerViewModel.isTracking ? const Text("Stop Meditation") : const Text("Start Meditation"), + child: postureTrackerViewModel.isTracking + ? const Text("Stop Meditation") + : const Text("Start Meditation"), ), Visibility( visible: !postureTrackerViewModel.isAvailable, From 28d0204d27731b5ba8a0d82916a56f40b193e05d Mon Sep 17 00:00:00 2001 From: Polaris Date: Sun, 10 Dec 2023 15:16:11 +0100 Subject: [PATCH 021/104] Extract meditation state in own file and start to add more styling to UI --- .../model/meditation_state.dart | 23 +++ .../view/neck_stretch_view.dart | 169 +++++++++++------- .../view/posture_roll_view.dart | 68 ++++--- 3 files changed, 166 insertions(+), 94 deletions(-) create mode 100644 open_earable/lib/apps/posture_tracker/model/meditation_state.dart diff --git a/open_earable/lib/apps/posture_tracker/model/meditation_state.dart b/open_earable/lib/apps/posture_tracker/model/meditation_state.dart new file mode 100644 index 0000000..03d9d5e --- /dev/null +++ b/open_earable/lib/apps/posture_tracker/model/meditation_state.dart @@ -0,0 +1,23 @@ +enum MeditationState { + mainNeckStretch, + leftNeckStretch, + rightNeckStretch, + noStretch +} + +extension MeditationStateExtension on MeditationState { + String get display { + switch (this) { + case MeditationState.mainNeckStretch: + return 'Main Neck Area'; + case MeditationState.leftNeckStretch: + return 'Right Neck Area'; + case MeditationState.rightNeckStretch: + return 'Left Neck Area'; + case MeditationState.noStretch: + return 'Not Stretching'; + default: + return 'Invalid State'; + } + } +} diff --git a/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart b/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart index c1c6c12..128acc4 100644 --- a/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart +++ b/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart @@ -5,26 +5,10 @@ import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_roll_view.dart'; import 'package:open_earable/apps/posture_tracker/view_model/neck_stretch_view_model.dart'; import 'package:provider/provider.dart'; +import 'package:open_earable/apps/posture_tracker/model/meditation_state.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; -enum MeditationState { mainNeckStretch, leftNeckStretch, rightNeckStretch } - -extension MeditationStateExtension on MeditationState { - String get display { - switch (this) { - case MeditationState.mainNeckStretch: - return 'Main neck area'; - case MeditationState.leftNeckStretch: - return 'Right neck area'; - case MeditationState.rightNeckStretch: - return 'Left neck area'; - default: - return 'Invalid State'; - } - } -} - class NeckStretchView extends StatefulWidget { final AttitudeTracker _tracker; final OpenEarable _openEarable; @@ -63,21 +47,48 @@ class _NeckStretchViewState extends State { Widget _buildContentView(NeckStretchViewModel neckStretchViewModel) { var headViews = this._createHeadViews(neckStretchViewModel); var stretchString = this._stretchState.display; - return Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Padding( - padding: EdgeInsets.all(2), - child: Text( - "Currently stretching: $stretchString", + return Column( + children: [ + Padding( + padding: EdgeInsets.all(5), + child: Visibility( + visible: _stretchState != MeditationState.noStretch, + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: "Currently Stretching: \n", + ), + TextSpan( + text: "$stretchString", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Color.fromARGB(255, 0, 186, 255), + ), + ) + ], + ), + ), + ), ), - ), - ...headViews.map((e) => FractionallySizedBox( - widthFactor: .7, + ...headViews.map( + (e) => FractionallySizedBox( + widthFactor: .6, child: e, - )), - this._buildTrackingButton(neckStretchViewModel), - ]); + ), + ), + // Used to place the Meditation-Button always at the bottom + Expanded( + child: Container(), + ), + this._buildMeditationButton(neckStretchViewModel), + ], + ); } + /// Builds the actual head views using the PostureRollView Widget _buildHeadView(String headAssetPath, String neckAssetPath, AlignmentGeometry headAlignment, double roll, double angleThreshold) { return Padding( @@ -92,14 +103,15 @@ class _NeckStretchViewState extends State { ); } + /// Creates the Head Views that display depending on the MeditationState. List _createHeadViews(neckStretchViewModel) { return [ - // Visible Widgets for the main stretch + /// Visible Widgets for the main stretch Visibility( visible: _stretchState == MeditationState.mainNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", - "assets/neck_stretch/Neck_Side_Stretch.png", + "assets/posture_tracker/Neck_Front.png", Alignment.center.add(Alignment(0, 0.3)), neckStretchViewModel.attitude.roll, 4.0), @@ -113,9 +125,10 @@ class _NeckStretchViewState extends State { -neckStretchViewModel.attitude.pitch, 16.0), ), - // Visible Widgets for the side stretch + + /// Visible Widgets for the left stretch Visibility( - visible: _stretchState != MeditationState.mainNeckStretch, + visible: _stretchState == MeditationState.leftNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", "assets/neck_stretch/Neck_Side_Stretch.png", @@ -124,50 +137,74 @@ class _NeckStretchViewState extends State { 4.0), ), Visibility( - visible: _stretchState != MeditationState.mainNeckStretch, + visible: _stretchState == MeditationState.leftNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", - "assets/neck_stretch/Neck_Main_Stretch.png", + "assets/posture_tracker/Neck_Side.png", Alignment.center.add(Alignment(0, 0.3)), -neckStretchViewModel.attitude.pitch, 16.0), ), - ]; - } - Widget _buildTrackingButton(NeckStretchViewModel postureTrackerViewModel) { - return Column(children: [ - ElevatedButton( - onPressed: postureTrackerViewModel.isAvailable - ? () { - postureTrackerViewModel.isTracking - ? this._viewModel.stopTracking() - : this._viewModel.startTracking(); - } - : null, - style: ElevatedButton.styleFrom( - backgroundColor: !postureTrackerViewModel.isTracking - ? Color(0xff77F2A1) - : Color(0xfff27777), - foregroundColor: Colors.black, - ), - child: postureTrackerViewModel.isTracking - ? const Text("Stop Meditation") - : const Text("Start Meditation"), + /// Visible Widgets for the right stretch + Visibility( + visible: _stretchState == MeditationState.rightNeckStretch, + child: this._buildHeadView( + "assets/posture_tracker/Head_Front.png", + "assets/neck_stretch/Neck_Side_Stretch.png", + Alignment.center.add(Alignment(0, 0.3)), + neckStretchViewModel.attitude.roll, + 4.0), ), Visibility( - visible: !postureTrackerViewModel.isAvailable, - maintainState: true, - maintainAnimation: true, - maintainSize: true, - child: Text( - "No Earable Connected", - style: TextStyle( - color: Colors.red, - fontSize: 12, + visible: _stretchState == MeditationState.rightNeckStretch, + child: this._buildHeadView( + "assets/posture_tracker/Head_Side.png", + "assets/posture_tracker/Neck_Side.png", + Alignment.center.add(Alignment(0, 0.3)), + -neckStretchViewModel.attitude.pitch, + 16.0), + ), + ]; + } + + // Creates the Button used to start the meditation + Widget _buildMeditationButton(NeckStretchViewModel neckStretchViewModel) { + return Padding( + padding: EdgeInsets.all(5), + child: Column(children: [ + ElevatedButton( + onPressed: neckStretchViewModel.isAvailable + ? () { + neckStretchViewModel.isTracking + ? this._viewModel.stopTracking() + : this._viewModel.startTracking(); + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: !neckStretchViewModel.isTracking + ? Color(0xff77F2A1) + : Color(0xfff27777), + foregroundColor: Colors.black, ), + child: neckStretchViewModel.isTracking + ? const Text("Stop Meditation") + : const Text("Start Meditation"), ), - ) - ]); + Visibility( + visible: !neckStretchViewModel.isAvailable, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: Text( + "No Earable Connected", + style: TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ) + ]), + ); } } diff --git a/open_earable/lib/apps/posture_tracker/view/posture_roll_view.dart b/open_earable/lib/apps/posture_tracker/view/posture_roll_view.dart index 24051f1..7106609 100644 --- a/open_earable/lib/apps/posture_tracker/view/posture_roll_view.dart +++ b/open_earable/lib/apps/posture_tracker/view/posture_roll_view.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/view/arc_painter.dart'; +import 'package:open_earable/apps/posture_tracker/model/meditation_state.dart'; /// A widget that displays the roll of the head and neck. class PostureRollView extends StatelessWidget { @@ -17,43 +18,54 @@ class PostureRollView extends StatelessWidget { final String neckAssetPath; final AlignmentGeometry headAlignment; + // Checks whether the arc has different properties due to meditation state + final MeditationState meditation; + const PostureRollView({Key? key, - required this.roll, - this.angleThreshold = 0, - required this.headAssetPath, - required this.neckAssetPath, - this.headAlignment = Alignment.center}) : super(key: key); + required this.roll, + this.angleThreshold = 0, + required this.headAssetPath, + required this.neckAssetPath, + this.headAlignment = Alignment.center, + this.meditation = MeditationState.noStretch}) : super(key: key); @override Widget build(BuildContext context) { return Column(children: [ Text( - "${(this.roll * 180 / 3.14).abs().toStringAsFixed(0)}°", - style: TextStyle( - // use proper color matching the background - color: Theme.of(context).colorScheme.onBackground, - fontSize: 30, - fontWeight: FontWeight.bold - ) + "${(this.roll * 180 / 3.14).abs().toStringAsFixed(0)}°", + style: TextStyle( + // use proper color matching the background + color: Theme + .of(context) + .colorScheme + .onBackground, + fontSize: 30, + fontWeight: FontWeight.bold + ) ), CustomPaint( - painter: ArcPainter(angle: this.roll, angleThreshold: this.angleThreshold), - child: Padding( - padding: EdgeInsets.all(10), - child: ClipOval( - child: Container( - color: roll.abs() > _MAX_ROLL ? Colors.red.withOpacity(0.5) : Colors.transparent, - child: Stack(children: [ - Image.asset(this.neckAssetPath), - Transform.rotate( - angle: this.roll.isFinite ? roll.abs() < _MAX_ROLL ? this.roll : roll.sign * _MAX_ROLL : 0, - alignment: this.headAlignment, - child: Image.asset(this.headAssetPath) - ), - ]) - ) + painter: ArcPainter( + angle: this.roll, angleThreshold: this.angleThreshold), + child: Padding( + padding: EdgeInsets.all(10), + child: ClipOval( + child: Container( + color: roll.abs() > _MAX_ROLL ? Colors.red.withOpacity( + 0.5) : Colors.transparent, + child: Stack(children: [ + Image.asset(this.neckAssetPath), + Transform.rotate( + angle: this.roll.isFinite ? roll.abs() < _MAX_ROLL + ? this.roll + : roll.sign * _MAX_ROLL : 0, + alignment: this.headAlignment, + child: Image.asset(this.headAssetPath) + ), + ]) + ) + ) ) - ) ), ]); } From eae413d97267662fca6262360493b54fd603346b Mon Sep 17 00:00:00 2001 From: Polaris Date: Sun, 10 Dec 2023 16:37:44 +0100 Subject: [PATCH 022/104] Add assets for neck stretch to pubspec and extract code for meditation into its own files and classes and adapt consistent naming scheme --- .../view/meditation_arc_painter.dart | 93 +++++++++++++++++++ .../view/meditation_roll_view.dart | 66 +++++++++++++ ...view.dart => meditation_tracker_view.dart} | 71 ++++++++++---- .../view/posture_roll_view.dart | 7 +- open_earable/lib/apps_tab.dart | 4 +- open_earable/pubspec.yaml | 1 + 6 files changed, 217 insertions(+), 25 deletions(-) create mode 100644 open_earable/lib/apps/posture_tracker/view/meditation_arc_painter.dart create mode 100644 open_earable/lib/apps/posture_tracker/view/meditation_roll_view.dart rename open_earable/lib/apps/posture_tracker/view/{neck_stretch_view.dart => meditation_tracker_view.dart} (75%) diff --git a/open_earable/lib/apps/posture_tracker/view/meditation_arc_painter.dart b/open_earable/lib/apps/posture_tracker/view/meditation_arc_painter.dart new file mode 100644 index 0000000..e3fe29c --- /dev/null +++ b/open_earable/lib/apps/posture_tracker/view/meditation_arc_painter.dart @@ -0,0 +1,93 @@ +// ignore_for_file: unnecessary_this + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:open_earable/apps/posture_tracker/model/meditation_state.dart'; +import 'package:open_earable/apps/posture_tracker/view/arc_painter.dart'; + +class MeditationArcPainter extends CustomPainter { + /// the angle of rotation + final double angle; + final double angleThreshold; + final MeditationState meditationState; + + MeditationArcPainter({required this.angle, this.angleThreshold = 0, this.meditationState = MeditationState.noStretch}); + + @override + void paint(Canvas canvas, Size size) { + Paint circlePaint = Paint() + ..color = const Color.fromARGB(255, 195, 195, 195) + ..style = PaintingStyle.stroke + ..strokeWidth = 5.0; + + Path circlePath = Path(); + circlePath.addOval(Rect.fromCircle(center: Offset(size.width / 2, size.height / 2), radius: min(size.width, size.height) / 2)); + canvas.drawPath(circlePath, circlePaint); + + // Create a paint object with purple color and stroke style + Paint anglePaint = Paint() + ..color = Colors.purpleAccent + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeWidth = 5.0; + + // Create a path object to draw the arc + Path anglePath = Path(); + + // Calculate the center and radius of the circle + Offset center = Offset(size.width / 2, size.height / 2); + double radius = min(size.width, size.height) / 2; + + // Calculate the start and end angles of the arc + double startAngle = -pi / 2; // start from the top of the circle + double endAngle = angle; + + // Add an arc to the path + anglePath.addArc( + Rect.fromCircle(center: center, radius: radius), // create a rectangle from the center and radius + startAngle, // start angle + endAngle, // sweep angle + ); + + Path angleOvershootPath = Path(); + angleOvershootPath.addArc( + Rect.fromCircle(center: center, radius: radius), // create a rectangle from the center and radius + startAngle + angle.sign * angleThreshold, // start angle + angle.sign * (angle.abs() - angleThreshold), // sweep angle + ); + + Paint angleOvershootPaint = Paint() + ..color = this.meditationState == MeditationState.noStretch ? Colors.red : Color.fromARGB(255, 0, 186, 255) + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeWidth = 5.0; + + Path thresholdPath = Path(); + thresholdPath.addArc( + Rect.fromCircle(center: center, radius: radius), // create a rectangle from the center and radius + startAngle - angleThreshold, // start angle + 2 * angleThreshold, // sweep angle + ); + + Paint thresholdPaint = Paint() + ..color = this.meditationState == MeditationState.noStretch ? Colors.purpleAccent[100]! : Colors.redAccent[100]! + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeWidth = 5.0; + + + // Draw the path on the canvas + canvas.drawPath(thresholdPath, thresholdPaint); + canvas.drawPath(anglePath, anglePaint); + if (angle.abs() > angleThreshold.abs()) { + canvas.drawPath(angleOvershootPath, angleOvershootPaint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + // check if oldDelegate is an ArcPainter and if the angle is the same + return oldDelegate is ArcPainter && oldDelegate.angle != this.angle; + } +} diff --git a/open_earable/lib/apps/posture_tracker/view/meditation_roll_view.dart b/open_earable/lib/apps/posture_tracker/view/meditation_roll_view.dart new file mode 100644 index 0000000..7cf6070 --- /dev/null +++ b/open_earable/lib/apps/posture_tracker/view/meditation_roll_view.dart @@ -0,0 +1,66 @@ +// ignore_for_file: unnecessary_this + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:open_earable/apps/posture_tracker/model/meditation_state.dart'; +import 'package:open_earable/apps/posture_tracker/view/meditation_arc_painter.dart'; + +/// A widget that displays the roll of the head and neck for the meditation. +class MeditationRollView extends StatelessWidget { + static final double _MAX_ROLL = pi / 2; + + /// The roll of the head and neck in radians. + final double roll; + final double angleThreshold; + + final String headAssetPath; + final String neckAssetPath; + final AlignmentGeometry headAlignment; + + // Checks whether the arc has different properties due to meditation state + final MeditationState meditationState; + + const MeditationRollView( + {Key? key, + required this.roll, + this.angleThreshold = 0, + required this.headAssetPath, + required this.neckAssetPath, + this.headAlignment = Alignment.center, + this.meditationState = MeditationState.noStretch}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Column(children: [ + Text("${(this.roll * 180 / 3.14).abs().toStringAsFixed(0)}°", + style: TextStyle( + // use proper color matching the background + color: Theme.of(context).colorScheme.onBackground, + fontSize: 30, + fontWeight: FontWeight.bold)), + CustomPaint( + painter: + MeditationArcPainter(angle: this.roll, angleThreshold: this.angleThreshold, meditationState: this.meditationState), + child: Padding( + padding: EdgeInsets.all(10), + child: ClipOval( + child: Container( + color: roll.abs() > _MAX_ROLL + ? Colors.red.withOpacity(0.5) + : Colors.transparent, + child: Stack(children: [ + Image.asset(this.neckAssetPath), + Transform.rotate( + angle: this.roll.isFinite + ? roll.abs() < _MAX_ROLL + ? this.roll + : roll.sign * _MAX_ROLL + : 0, + alignment: this.headAlignment, + child: Image.asset(this.headAssetPath)), + ]))))), + ]); + } +} diff --git a/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart b/open_earable/lib/apps/posture_tracker/view/meditation_tracker_view.dart similarity index 75% rename from open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart rename to open_earable/lib/apps/posture_tracker/view/meditation_tracker_view.dart index 128acc4..26b0c8b 100644 --- a/open_earable/lib/apps/posture_tracker/view/neck_stretch_view.dart +++ b/open_earable/lib/apps/posture_tracker/view/meditation_tracker_view.dart @@ -2,26 +2,26 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; -import 'package:open_earable/apps/posture_tracker/view/posture_roll_view.dart'; +import 'package:open_earable/apps/posture_tracker/view/meditation_roll_view.dart'; import 'package:open_earable/apps/posture_tracker/view_model/neck_stretch_view_model.dart'; import 'package:provider/provider.dart'; import 'package:open_earable/apps/posture_tracker/model/meditation_state.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; -class NeckStretchView extends StatefulWidget { +class MeditationTrackerView extends StatefulWidget { final AttitudeTracker _tracker; final OpenEarable _openEarable; - NeckStretchView(this._tracker, this._openEarable); + MeditationTrackerView(this._tracker, this._openEarable); @override - State createState() => _NeckStretchViewState(); + State createState() => _MeditationTrackerViewState(); } -class _NeckStretchViewState extends State { +class _MeditationTrackerViewState extends State { late final NeckStretchViewModel _viewModel; - var _stretchState = MeditationState.mainNeckStretch; + var _stretchState = MeditationState.noStretch; @override void initState() { @@ -52,6 +52,9 @@ class _NeckStretchViewState extends State { Padding( padding: EdgeInsets.all(5), child: Visibility( + maintainSize: true, + maintainState: true, + maintainAnimation: true, visible: _stretchState != MeditationState.noStretch, child: RichText( textAlign: TextAlign.center, @@ -89,16 +92,22 @@ class _NeckStretchViewState extends State { } /// Builds the actual head views using the PostureRollView - Widget _buildHeadView(String headAssetPath, String neckAssetPath, - AlignmentGeometry headAlignment, double roll, double angleThreshold) { + Widget _buildHeadView( + String headAssetPath, + String neckAssetPath, + AlignmentGeometry headAlignment, + double roll, + double angleThreshold, + MeditationState state) { return Padding( padding: const EdgeInsets.all(5), - child: PostureRollView( + child: MeditationRollView( roll: roll, angleThreshold: angleThreshold * 3.14 / 180, headAssetPath: headAssetPath, neckAssetPath: neckAssetPath, headAlignment: headAlignment, + meditationState: state, ), ); } @@ -106,6 +115,28 @@ class _NeckStretchViewState extends State { /// Creates the Head Views that display depending on the MeditationState. List _createHeadViews(neckStretchViewModel) { return [ + // Visible Head-Displays when not stretching + Visibility( + visible: _stretchState == MeditationState.noStretch, + child: this._buildHeadView( + "assets/posture_tracker/Head_Front.png", + "assets/posture_tracker/Neck_Front.png", + Alignment.center.add(Alignment(0, 0.3)), + neckStretchViewModel.attitude.roll, + 0, + MeditationState.mainNeckStretch), + ), + Visibility( + visible: _stretchState == MeditationState.noStretch, + child: this._buildHeadView( + "assets/posture_tracker/Head_Side.png", + "assets/posture_tracker/Neck_Side.png", + Alignment.center.add(Alignment(0, 0.3)), + -neckStretchViewModel.attitude.pitch, + 0, + MeditationState.mainNeckStretch), + ), + /// Visible Widgets for the main stretch Visibility( visible: _stretchState == MeditationState.mainNeckStretch, @@ -114,7 +145,8 @@ class _NeckStretchViewState extends State { "assets/posture_tracker/Neck_Front.png", Alignment.center.add(Alignment(0, 0.3)), neckStretchViewModel.attitude.roll, - 4.0), + 7.0, + MeditationState.mainNeckStretch), ), Visibility( visible: _stretchState == MeditationState.mainNeckStretch, @@ -123,7 +155,8 @@ class _NeckStretchViewState extends State { "assets/neck_stretch/Neck_Main_Stretch.png", Alignment.center.add(Alignment(0, 0.3)), -neckStretchViewModel.attitude.pitch, - 16.0), + 50.0, + MeditationState.mainNeckStretch), ), /// Visible Widgets for the left stretch @@ -131,10 +164,11 @@ class _NeckStretchViewState extends State { visible: _stretchState == MeditationState.leftNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", - "assets/neck_stretch/Neck_Side_Stretch.png", + "assets/neck_stretch/Neck_Left_Stretch.png", Alignment.center.add(Alignment(0, 0.3)), neckStretchViewModel.attitude.roll, - 4.0), + 30.0, + MeditationState.leftNeckStretch), ), Visibility( visible: _stretchState == MeditationState.leftNeckStretch, @@ -143,7 +177,8 @@ class _NeckStretchViewState extends State { "assets/posture_tracker/Neck_Side.png", Alignment.center.add(Alignment(0, 0.3)), -neckStretchViewModel.attitude.pitch, - 16.0), + 15.0, + MeditationState.leftNeckStretch), ), /// Visible Widgets for the right stretch @@ -151,10 +186,11 @@ class _NeckStretchViewState extends State { visible: _stretchState == MeditationState.rightNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", - "assets/neck_stretch/Neck_Side_Stretch.png", + "assets/neck_stretch/Neck_Right_Stretch.png", Alignment.center.add(Alignment(0, 0.3)), neckStretchViewModel.attitude.roll, - 4.0), + 30.0, + MeditationState.rightNeckStretch), ), Visibility( visible: _stretchState == MeditationState.rightNeckStretch, @@ -163,7 +199,8 @@ class _NeckStretchViewState extends State { "assets/posture_tracker/Neck_Side.png", Alignment.center.add(Alignment(0, 0.3)), -neckStretchViewModel.attitude.pitch, - 16.0), + 15.0, + MeditationState.rightNeckStretch), ), ]; } diff --git a/open_earable/lib/apps/posture_tracker/view/posture_roll_view.dart b/open_earable/lib/apps/posture_tracker/view/posture_roll_view.dart index 7106609..b245e0f 100644 --- a/open_earable/lib/apps/posture_tracker/view/posture_roll_view.dart +++ b/open_earable/lib/apps/posture_tracker/view/posture_roll_view.dart @@ -4,7 +4,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/view/arc_painter.dart'; -import 'package:open_earable/apps/posture_tracker/model/meditation_state.dart'; /// A widget that displays the roll of the head and neck. class PostureRollView extends StatelessWidget { @@ -18,16 +17,12 @@ class PostureRollView extends StatelessWidget { final String neckAssetPath; final AlignmentGeometry headAlignment; - // Checks whether the arc has different properties due to meditation state - final MeditationState meditation; - const PostureRollView({Key? key, required this.roll, this.angleThreshold = 0, required this.headAssetPath, required this.neckAssetPath, - this.headAlignment = Alignment.center, - this.meditation = MeditationState.noStretch}) : super(key: key); + this.headAlignment = Alignment.center}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 23af397..d2f37dd 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; import 'package:open_earable/apps/recorder.dart'; -import 'package:open_earable/apps/posture_tracker/view/neck_stretch_view.dart'; +import 'package:open_earable/apps/posture_tracker/view/meditation_tracker_view.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class AppInfo { @@ -54,7 +54,7 @@ class AppsTab extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => NeckStretchView( + builder: (context) => MeditationTrackerView( EarableAttitudeTracker(_openEarable), _openEarable))); }), // ... similarly for other apps diff --git a/open_earable/pubspec.yaml b/open_earable/pubspec.yaml index 80b0e37..9a0b748 100644 --- a/open_earable/pubspec.yaml +++ b/open_earable/pubspec.yaml @@ -123,6 +123,7 @@ flutter: assets: - assets/ - assets/posture_tracker/ + - assets/neck_stretch/ fonts: - family: OpenEarableIcon From 1c83b19ec57dd729f0968f68a5e17aa8d352103a Mon Sep 17 00:00:00 2001 From: Polaris Date: Tue, 12 Dec 2023 15:08:05 +0100 Subject: [PATCH 023/104] Add settings for meditation duration and the dynamic view adaptation via timers and the Start Meditation Button --- .../model/meditation_state.dart | 80 ++++++++ .../view/meditation_arc_painter.dart | 2 - .../view/meditation_roll_view.dart | 2 - .../view/meditation_settings_view.dart | 185 ++++++++++++++++++ .../view/meditation_tracker_view.dart | 99 ++++++---- ..._model.dart => meditation_view_model.dart} | 14 +- 6 files changed, 337 insertions(+), 45 deletions(-) create mode 100644 open_earable/lib/apps/posture_tracker/view/meditation_settings_view.dart rename open_earable/lib/apps/posture_tracker/view_model/{neck_stretch_view_model.dart => meditation_view_model.dart} (64%) diff --git a/open_earable/lib/apps/posture_tracker/model/meditation_state.dart b/open_earable/lib/apps/posture_tracker/model/meditation_state.dart index 03d9d5e..cc03c30 100644 --- a/open_earable/lib/apps/posture_tracker/model/meditation_state.dart +++ b/open_earable/lib/apps/posture_tracker/model/meditation_state.dart @@ -1,3 +1,8 @@ +import 'dart:async'; + +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; + +/// Enum for the Meditation States enum MeditationState { mainNeckStretch, leftNeckStretch, @@ -5,6 +10,7 @@ enum MeditationState { noStretch } +/// Used to get a String representation for Display of the current meditation state extension MeditationStateExtension on MeditationState { String get display { switch (this) { @@ -21,3 +27,77 @@ extension MeditationStateExtension on MeditationState { } } } + +class MeditationSettings { + MeditationState state; + + /// Duration for the main neck relaxation + Duration mainNeckRelaxation; + + /// Duration for the left neck relaxation + Duration leftNeckRelaxation; + + /// Duration for the right neck relaxation + Duration rightNeckRelaxation; + + MeditationSettings( + {this.state = MeditationState.noStretch, + required this.mainNeckRelaxation, + required this.leftNeckRelaxation, + required this.rightNeckRelaxation}); +} + +class NeckMeditation { + MeditationSettings _settings = MeditationSettings( + mainNeckRelaxation: Duration(seconds: 30), + leftNeckRelaxation: Duration(seconds: 30), + rightNeckRelaxation: Duration(seconds: 30)); + + final OpenEarable _openEarable; + + /// Stores the current active timer for state transition + var currentTimer; + + MeditationSettings get settings => _settings; + + NeckMeditation(this._openEarable); + + void setSettings(MeditationSettings settings) { + _settings = settings; + } + + startMeditation() { + _settings.state = MeditationState.mainNeckStretch; + currentTimer = Timer(_settings.mainNeckRelaxation, _setNextState); + } + + stopMeditation() { + _settings.state = MeditationState.noStretch; + currentTimer?.cancel(); + } + + /// Used to set the next meditation state; + void _setNextState() { + switch (_settings.state) { + case MeditationState.noStretch: + _settings.state = MeditationState.mainNeckStretch; + return; + case MeditationState.mainNeckStretch: + _settings.state = MeditationState.leftNeckStretch; + currentTimer = Timer(_settings.rightNeckRelaxation, _setNextState); + _openEarable.audioPlayer.jingle(2); + return; + case MeditationState.leftNeckStretch: + _settings.state = MeditationState.rightNeckStretch; + currentTimer = Timer(_settings.rightNeckRelaxation, _setNextState); + _openEarable.audioPlayer.jingle(2); + return; + case MeditationState.rightNeckStretch: + _settings.state = MeditationState.noStretch; + _openEarable.audioPlayer.jingle(2); + return; + default: + return; + } + } +} diff --git a/open_earable/lib/apps/posture_tracker/view/meditation_arc_painter.dart b/open_earable/lib/apps/posture_tracker/view/meditation_arc_painter.dart index e3fe29c..ccda82a 100644 --- a/open_earable/lib/apps/posture_tracker/view/meditation_arc_painter.dart +++ b/open_earable/lib/apps/posture_tracker/view/meditation_arc_painter.dart @@ -1,5 +1,3 @@ -// ignore_for_file: unnecessary_this - import 'dart:math'; import 'package:flutter/material.dart'; diff --git a/open_earable/lib/apps/posture_tracker/view/meditation_roll_view.dart b/open_earable/lib/apps/posture_tracker/view/meditation_roll_view.dart index 7cf6070..19a54c7 100644 --- a/open_earable/lib/apps/posture_tracker/view/meditation_roll_view.dart +++ b/open_earable/lib/apps/posture_tracker/view/meditation_roll_view.dart @@ -1,5 +1,3 @@ -// ignore_for_file: unnecessary_this - import 'dart:math'; import 'package:flutter/material.dart'; diff --git a/open_earable/lib/apps/posture_tracker/view/meditation_settings_view.dart b/open_earable/lib/apps/posture_tracker/view/meditation_settings_view.dart new file mode 100644 index 0000000..ecaea9a --- /dev/null +++ b/open_earable/lib/apps/posture_tracker/view/meditation_settings_view.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:open_earable/apps/posture_tracker/model/meditation_state.dart'; +import 'package:open_earable/apps/posture_tracker/view_model/meditation_view_model.dart'; +import 'package:provider/provider.dart'; + +class SettingsView extends StatefulWidget { + final MeditationViewModel _viewModel; + + SettingsView(this._viewModel); + + @override + State createState() => _SettingsViewState(); +} + +class _SettingsViewState extends State { + late final TextEditingController _mainNeckDuration; + late final TextEditingController _leftNeckDuration; + late final TextEditingController _rightNeckDuration; + + late final MeditationViewModel _viewModel; + + @override + void initState() { + super.initState(); + this._viewModel = widget._viewModel; + _mainNeckDuration = TextEditingController( + text: _viewModel.meditationSettings.mainNeckRelaxation.inSeconds.toString()); + _leftNeckDuration = TextEditingController( + text: _viewModel.meditationSettings.leftNeckRelaxation.inSeconds.toString()); + _rightNeckDuration = TextEditingController( + text: _viewModel.meditationSettings.rightNeckRelaxation.inSeconds.toString()); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Guided Neck Meditation Settings")), + body: ChangeNotifierProvider.value( + value: _viewModel, + builder: (context, child) => Consumer( + builder: (context, postureTrackerViewModel, child) => + _buildSettingsView(), + )), + ); + } + + Widget _buildSettingsView() { + return Column( + children: [ + Card( + color: Theme.of(context).colorScheme.primary, + child: ListTile( + title: Text("Status"), + trailing: Text(_viewModel.isTracking + ? "Tracking" + : _viewModel.isAvailable + ? "Available" + : "Unavailable"), + ), + ), + Card( + color: Theme.of(context).colorScheme.primary, + child: Column( + children: [ + // add a switch to control the `isActive` property of the `BadPostureSettings` + ListTile( + title: Text("Guided Neck Relaxation"), + ), + ListTile( + title: Text("Main Neck Relaxation Duration (in seconds)"), + trailing: SizedBox( + height: 37.0, + width: 52, + child: TextField( + controller: _mainNeckDuration, + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(10), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: OutlineInputBorder(), + labelText: 'Seconds', + filled: true, + labelStyle: TextStyle(color: Colors.black), + fillColor: Colors.white), + keyboardType: TextInputType.number, + onChanged: (_) { + _updateMeditationSettings(); + }, + ), + ), + ), + ListTile( + title: Text("Left Neck Relaxation Duration (in seconds)"), + trailing: SizedBox( + height: 37.0, + width: 52, + child: TextField( + controller: _leftNeckDuration, + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(10), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: OutlineInputBorder(), + labelText: 'Seconds', + filled: true, + labelStyle: TextStyle(color: Colors.black), + fillColor: Colors.white), + keyboardType: TextInputType.number, + onChanged: (_) { + _updateMeditationSettings(); + }, + ), + ), + ), + ListTile( + title: Text("Right Neck Relaxation Duration (in seconds)"), + trailing: SizedBox( + height: 37.0, + width: 52, + child: TextField( + controller: _rightNeckDuration, + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(10), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: OutlineInputBorder(), + labelText: 'Seconds', + filled: true, + labelStyle: TextStyle(color: Colors.black), + fillColor: Colors.white), + keyboardType: TextInputType.number, + onChanged: (_) { + _updateMeditationSettings(); + }, + ), + ), + ), + ], + ), + ), + + Padding( + padding: EdgeInsets.all(8.0), + child: Row(children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: _viewModel.isTracking + ? Colors.green[300] + : Colors.blue[300], + foregroundColor: Colors.black, + ), + onPressed: _viewModel.isTracking + ? () { + _viewModel.calibrate(); + Navigator.of(context).pop(); + } + : () => _viewModel.startTracking(), + child: Text(_viewModel.isTracking + ? "Calibrate as Main Posture" + : "Start Calibration"), + ), + ) + ]), + ), + ], + ); + } + + void _updateMeditationSettings() { + MeditationSettings settings = _viewModel.meditationSettings; + settings.mainNeckRelaxation = Duration(seconds:int.parse(_mainNeckDuration.text)); + settings.rightNeckRelaxation = Duration(seconds:int.parse(_rightNeckDuration.text)); + settings.leftNeckRelaxation = Duration(seconds:int.parse(_leftNeckDuration.text)); + _viewModel.setMeditationSettings(settings); + } + + @override + void dispose() { + super.dispose(); + } +} diff --git a/open_earable/lib/apps/posture_tracker/view/meditation_tracker_view.dart b/open_earable/lib/apps/posture_tracker/view/meditation_tracker_view.dart index 26b0c8b..80bc798 100644 --- a/open_earable/lib/apps/posture_tracker/view/meditation_tracker_view.dart +++ b/open_earable/lib/apps/posture_tracker/view/meditation_tracker_view.dart @@ -1,11 +1,10 @@ -// ignore_for_file: unnecessary_this - import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/meditation_roll_view.dart'; -import 'package:open_earable/apps/posture_tracker/view_model/neck_stretch_view_model.dart'; +import 'package:open_earable/apps/posture_tracker/view_model/meditation_view_model.dart'; import 'package:provider/provider.dart'; import 'package:open_earable/apps/posture_tracker/model/meditation_state.dart'; +import 'package:open_earable/apps/posture_tracker/view/meditation_settings_view.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; @@ -20,33 +19,45 @@ class MeditationTrackerView extends StatefulWidget { } class _MeditationTrackerViewState extends State { - late final NeckStretchViewModel _viewModel; - var _stretchState = MeditationState.noStretch; + late final MeditationViewModel _viewModel; + + // Used to store references for the timers used for the meditation + var mainNeckTimer; + var leftNeckTimer; + var rightNeckTimer; @override void initState() { super.initState(); - this._viewModel = NeckStretchViewModel(widget._tracker); + this._viewModel = MeditationViewModel(widget._tracker, NeckMeditation(widget._openEarable)); } @override Widget build(BuildContext context) { - return ChangeNotifierProvider.value( + return ChangeNotifierProvider.value( value: _viewModel, - builder: (context, child) => Consumer( - builder: (context, neckStretchViewModel, child) => Scaffold( - appBar: AppBar( - title: const Text("Guided Neck Relaxation"), - ), - body: Center( - child: this._buildContentView(neckStretchViewModel), - ), - ))); + builder: (context, child) => + Consumer( + builder: (context, neckStretchViewModel, child) => + Scaffold( + appBar: AppBar( + title: const Text("Guided Neck Relaxation"), + actions: [ + IconButton( + onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => SettingsView(this._viewModel))), + icon: Icon(Icons.settings) + ), + ], + ), + body: Center( + child: this._buildContentView(neckStretchViewModel), + ), + ))); } - Widget _buildContentView(NeckStretchViewModel neckStretchViewModel) { + Widget _buildContentView(MeditationViewModel neckStretchViewModel) { var headViews = this._createHeadViews(neckStretchViewModel); - var stretchString = this._stretchState.display; + var stretchString = this._viewModel.meditationSettings.state.display; return Column( children: [ Padding( @@ -55,7 +66,7 @@ class _MeditationTrackerViewState extends State { maintainSize: true, maintainState: true, maintainAnimation: true, - visible: _stretchState != MeditationState.noStretch, + visible: this._viewModel.meditationSettings.state != MeditationState.noStretch, child: RichText( textAlign: TextAlign.center, text: TextSpan( @@ -77,10 +88,11 @@ class _MeditationTrackerViewState extends State { ), ), ...headViews.map( - (e) => FractionallySizedBox( - widthFactor: .6, - child: e, - ), + (e) => + FractionallySizedBox( + widthFactor: .6, + child: e, + ), ), // Used to place the Meditation-Button always at the bottom Expanded( @@ -92,8 +104,7 @@ class _MeditationTrackerViewState extends State { } /// Builds the actual head views using the PostureRollView - Widget _buildHeadView( - String headAssetPath, + Widget _buildHeadView(String headAssetPath, String neckAssetPath, AlignmentGeometry headAlignment, double roll, @@ -117,7 +128,7 @@ class _MeditationTrackerViewState extends State { return [ // Visible Head-Displays when not stretching Visibility( - visible: _stretchState == MeditationState.noStretch, + visible: this._viewModel.meditationSettings.state == MeditationState.noStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", "assets/posture_tracker/Neck_Front.png", @@ -127,7 +138,7 @@ class _MeditationTrackerViewState extends State { MeditationState.mainNeckStretch), ), Visibility( - visible: _stretchState == MeditationState.noStretch, + visible: this._viewModel.meditationSettings.state == MeditationState.noStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", "assets/posture_tracker/Neck_Side.png", @@ -139,7 +150,7 @@ class _MeditationTrackerViewState extends State { /// Visible Widgets for the main stretch Visibility( - visible: _stretchState == MeditationState.mainNeckStretch, + visible: this._viewModel.meditationSettings.state == MeditationState.mainNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", "assets/posture_tracker/Neck_Front.png", @@ -149,7 +160,7 @@ class _MeditationTrackerViewState extends State { MeditationState.mainNeckStretch), ), Visibility( - visible: _stretchState == MeditationState.mainNeckStretch, + visible: this._viewModel.meditationSettings.state == MeditationState.mainNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", "assets/neck_stretch/Neck_Main_Stretch.png", @@ -161,7 +172,7 @@ class _MeditationTrackerViewState extends State { /// Visible Widgets for the left stretch Visibility( - visible: _stretchState == MeditationState.leftNeckStretch, + visible: this._viewModel.meditationSettings.state == MeditationState.leftNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", "assets/neck_stretch/Neck_Left_Stretch.png", @@ -171,7 +182,7 @@ class _MeditationTrackerViewState extends State { MeditationState.leftNeckStretch), ), Visibility( - visible: _stretchState == MeditationState.leftNeckStretch, + visible: this._viewModel.meditationSettings.state == MeditationState.leftNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", "assets/posture_tracker/Neck_Side.png", @@ -183,7 +194,7 @@ class _MeditationTrackerViewState extends State { /// Visible Widgets for the right stretch Visibility( - visible: _stretchState == MeditationState.rightNeckStretch, + visible: this._viewModel.meditationSettings.state == MeditationState.rightNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", "assets/neck_stretch/Neck_Right_Stretch.png", @@ -193,7 +204,7 @@ class _MeditationTrackerViewState extends State { MeditationState.rightNeckStretch), ), Visibility( - visible: _stretchState == MeditationState.rightNeckStretch, + visible: this._viewModel.meditationSettings.state == MeditationState.rightNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", "assets/posture_tracker/Neck_Side.png", @@ -205,18 +216,30 @@ class _MeditationTrackerViewState extends State { ]; } + /// Used to start the meditation via the button + void _startMeditation() { + this._viewModel.startTracking(); + this._viewModel.meditation.startMeditation(); + } + + /// Used to stop the meditation via the button + void _stopMeditation() { + this._viewModel.stopTracking(); + this._viewModel.meditation.stopMeditation(); + } + // Creates the Button used to start the meditation - Widget _buildMeditationButton(NeckStretchViewModel neckStretchViewModel) { + Widget _buildMeditationButton(MeditationViewModel neckStretchViewModel) { return Padding( padding: EdgeInsets.all(5), child: Column(children: [ ElevatedButton( onPressed: neckStretchViewModel.isAvailable ? () { - neckStretchViewModel.isTracking - ? this._viewModel.stopTracking() - : this._viewModel.startTracking(); - } + neckStretchViewModel.isTracking + ? _stopMeditation() + : _startMeditation(); + } : null, style: ElevatedButton.styleFrom( backgroundColor: !neckStretchViewModel.isTracking diff --git a/open_earable/lib/apps/posture_tracker/view_model/neck_stretch_view_model.dart b/open_earable/lib/apps/posture_tracker/view_model/meditation_view_model.dart similarity index 64% rename from open_earable/lib/apps/posture_tracker/view_model/neck_stretch_view_model.dart rename to open_earable/lib/apps/posture_tracker/view_model/meditation_view_model.dart index f620b3e..6e1050e 100644 --- a/open_earable/lib/apps/posture_tracker/view_model/neck_stretch_view_model.dart +++ b/open_earable/lib/apps/posture_tracker/view_model/meditation_view_model.dart @@ -1,17 +1,21 @@ import "package:flutter/material.dart"; import "package:open_earable/apps/posture_tracker/model/attitude.dart"; -import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; +import "package:open_earable/apps/posture_tracker/model/attitude_tracker.dart"; +import "package:open_earable/apps/posture_tracker/model/meditation_state.dart"; -class NeckStretchViewModel extends ChangeNotifier { +class MeditationViewModel extends ChangeNotifier { Attitude _attitude = Attitude(); Attitude get attitude => _attitude; bool get isTracking => _attitudeTracker.isTracking; bool get isAvailable => _attitudeTracker.isAvailable; + NeckMeditation get meditation => _meditation; + MeditationSettings get meditationSettings => _meditation.settings; AttitudeTracker _attitudeTracker; + NeckMeditation _meditation; - NeckStretchViewModel(this._attitudeTracker) { + MeditationViewModel(this._attitudeTracker, this._meditation) { _attitudeTracker.didChangeAvailability = (_) { notifyListeners(); }; @@ -40,6 +44,10 @@ class NeckStretchViewModel extends ChangeNotifier { _attitudeTracker.calibrateToCurrentAttitude(); } + void setMeditationSettings(MeditationSettings settings) { + _meditation.setSettings(settings); + } + @override void dispose() { _attitudeTracker.cancle(); From d65d1414500b96d60aa6a29b56c708f9ea44dd0c Mon Sep 17 00:00:00 2001 From: Polaris Date: Tue, 12 Dec 2023 16:54:12 +0100 Subject: [PATCH 024/104] Refactor code to be in its own folder structure --- .../model/meditation_state.dart | 0 .../view/meditation_arc_painter.dart | 2 +- .../view/meditation_roll_view.dart | 4 ++-- .../view/meditation_settings_view.dart | 4 ++-- .../view/meditation_tracker_view.dart | 8 ++++---- .../view_model/meditation_view_model.dart | 2 +- open_earable/lib/apps_tab.dart | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) rename open_earable/lib/apps/{posture_tracker => neck_meditation}/model/meditation_state.dart (100%) rename open_earable/lib/apps/{posture_tracker => neck_meditation}/view/meditation_arc_painter.dart (98%) rename open_earable/lib/apps/{posture_tracker => neck_meditation}/view/meditation_roll_view.dart (94%) rename open_earable/lib/apps/{posture_tracker => neck_meditation}/view/meditation_settings_view.dart (98%) rename open_earable/lib/apps/{posture_tracker => neck_meditation}/view/meditation_tracker_view.dart (97%) rename open_earable/lib/apps/{posture_tracker => neck_meditation}/view_model/meditation_view_model.dart (94%) diff --git a/open_earable/lib/apps/posture_tracker/model/meditation_state.dart b/open_earable/lib/apps/neck_meditation/model/meditation_state.dart similarity index 100% rename from open_earable/lib/apps/posture_tracker/model/meditation_state.dart rename to open_earable/lib/apps/neck_meditation/model/meditation_state.dart diff --git a/open_earable/lib/apps/posture_tracker/view/meditation_arc_painter.dart b/open_earable/lib/apps/neck_meditation/view/meditation_arc_painter.dart similarity index 98% rename from open_earable/lib/apps/posture_tracker/view/meditation_arc_painter.dart rename to open_earable/lib/apps/neck_meditation/view/meditation_arc_painter.dart index ccda82a..eefbdff 100644 --- a/open_earable/lib/apps/posture_tracker/view/meditation_arc_painter.dart +++ b/open_earable/lib/apps/neck_meditation/view/meditation_arc_painter.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:open_earable/apps/posture_tracker/model/meditation_state.dart'; +import 'package:open_earable/apps/neck_meditation/model/meditation_state.dart'; import 'package:open_earable/apps/posture_tracker/view/arc_painter.dart'; class MeditationArcPainter extends CustomPainter { diff --git a/open_earable/lib/apps/posture_tracker/view/meditation_roll_view.dart b/open_earable/lib/apps/neck_meditation/view/meditation_roll_view.dart similarity index 94% rename from open_earable/lib/apps/posture_tracker/view/meditation_roll_view.dart rename to open_earable/lib/apps/neck_meditation/view/meditation_roll_view.dart index 19a54c7..2624639 100644 --- a/open_earable/lib/apps/posture_tracker/view/meditation_roll_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/meditation_roll_view.dart @@ -1,8 +1,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:open_earable/apps/posture_tracker/model/meditation_state.dart'; -import 'package:open_earable/apps/posture_tracker/view/meditation_arc_painter.dart'; +import 'package:open_earable/apps/neck_meditation/model/meditation_state.dart'; +import 'package:open_earable/apps/neck_meditation/view/meditation_arc_painter.dart'; /// A widget that displays the roll of the head and neck for the meditation. class MeditationRollView extends StatelessWidget { diff --git a/open_earable/lib/apps/posture_tracker/view/meditation_settings_view.dart b/open_earable/lib/apps/neck_meditation/view/meditation_settings_view.dart similarity index 98% rename from open_earable/lib/apps/posture_tracker/view/meditation_settings_view.dart rename to open_earable/lib/apps/neck_meditation/view/meditation_settings_view.dart index ecaea9a..11aaf36 100644 --- a/open_earable/lib/apps/posture_tracker/view/meditation_settings_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/meditation_settings_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:open_earable/apps/posture_tracker/model/meditation_state.dart'; -import 'package:open_earable/apps/posture_tracker/view_model/meditation_view_model.dart'; +import 'package:open_earable/apps/neck_meditation/model/meditation_state.dart'; +import 'package:open_earable/apps/neck_meditation/view_model/meditation_view_model.dart'; import 'package:provider/provider.dart'; class SettingsView extends StatefulWidget { diff --git a/open_earable/lib/apps/posture_tracker/view/meditation_tracker_view.dart b/open_earable/lib/apps/neck_meditation/view/meditation_tracker_view.dart similarity index 97% rename from open_earable/lib/apps/posture_tracker/view/meditation_tracker_view.dart rename to open_earable/lib/apps/neck_meditation/view/meditation_tracker_view.dart index 80bc798..9b0d804 100644 --- a/open_earable/lib/apps/posture_tracker/view/meditation_tracker_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/meditation_tracker_view.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; -import 'package:open_earable/apps/posture_tracker/view/meditation_roll_view.dart'; -import 'package:open_earable/apps/posture_tracker/view_model/meditation_view_model.dart'; +import 'package:open_earable/apps/neck_meditation/view/meditation_roll_view.dart'; +import 'package:open_earable/apps/neck_meditation/view_model/meditation_view_model.dart'; import 'package:provider/provider.dart'; -import 'package:open_earable/apps/posture_tracker/model/meditation_state.dart'; -import 'package:open_earable/apps/posture_tracker/view/meditation_settings_view.dart'; +import 'package:open_earable/apps/neck_meditation/model/meditation_state.dart'; +import 'package:open_earable/apps/neck_meditation/view/meditation_settings_view.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; diff --git a/open_earable/lib/apps/posture_tracker/view_model/meditation_view_model.dart b/open_earable/lib/apps/neck_meditation/view_model/meditation_view_model.dart similarity index 94% rename from open_earable/lib/apps/posture_tracker/view_model/meditation_view_model.dart rename to open_earable/lib/apps/neck_meditation/view_model/meditation_view_model.dart index 6e1050e..183d1ab 100644 --- a/open_earable/lib/apps/posture_tracker/view_model/meditation_view_model.dart +++ b/open_earable/lib/apps/neck_meditation/view_model/meditation_view_model.dart @@ -1,7 +1,7 @@ import "package:flutter/material.dart"; import "package:open_earable/apps/posture_tracker/model/attitude.dart"; import "package:open_earable/apps/posture_tracker/model/attitude_tracker.dart"; -import "package:open_earable/apps/posture_tracker/model/meditation_state.dart"; +import 'package:open_earable/apps/neck_meditation/model/meditation_state.dart'; class MeditationViewModel extends ChangeNotifier { Attitude _attitude = Attitude(); diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index d2f37dd..79bb209 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; import 'package:open_earable/apps/recorder.dart'; -import 'package:open_earable/apps/posture_tracker/view/meditation_tracker_view.dart'; +import 'package:open_earable/apps/neck_meditation/view/meditation_tracker_view.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class AppInfo { From ea42812345a67e038d6bb875b77bdb6a400b98d4 Mon Sep 17 00:00:00 2001 From: Polaris Date: Tue, 12 Dec 2023 23:18:17 +0100 Subject: [PATCH 025/104] Add timer text in the button when meditating + Add more refined UI elements + Add reset of head position values when finishing meditation + Small bugfix when putting invalid values --- .../assets/neck_stretch/Neck_Left_Stretch.png | Bin 20010 -> 20134 bytes .../neck_stretch/Neck_Right_Stretch.png | Bin 20134 -> 20010 bytes .../model/meditation_state.dart | 47 +++- .../view/meditation_settings_view.dart | 37 ++- .../view/meditation_tracker_view.dart | 211 ++++++++++-------- .../view_model/meditation_view_model.dart | 26 ++- 6 files changed, 205 insertions(+), 116 deletions(-) diff --git a/open_earable/assets/neck_stretch/Neck_Left_Stretch.png b/open_earable/assets/neck_stretch/Neck_Left_Stretch.png index 8562cfb3c322d5bdb19d79f24c570643f886acc9..3408e49f0574b6d24d7f4aaa680395e09e36ec50 100644 GIT binary patch literal 20134 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

e%Ew~0sC1q6vI9(|OQepiO_5R_2J%@{>y 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 diff --git a/open_earable/assets/neck_stretch/Neck_Right_Stretch.png b/open_earable/assets/neck_stretch/Neck_Right_Stretch.png index 3408e49f0574b6d24d7f4aaa680395e09e36ec50..8562cfb3c322d5bdb19d79f24c570643f886acc9 100644 GIT binary patch literal 20010 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

e%Ew~0sC1q6vI9(|OQepiO_5R_2J%@{>y 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 literal 20134 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 _settings; - NeckMeditation(this._openEarable); + NeckMeditation(this._openEarable, this._viewModel); void setSettings(MeditationSettings settings) { _settings = settings; } + // Gets the rest duration of the current meditation timer + Duration getRestDuration() { + return _restDuration; + } + + /// Starts the Meditation with the according timers startMeditation() { _settings.state = MeditationState.mainNeckStretch; - currentTimer = Timer(_settings.mainNeckRelaxation, _setNextState); + _restDuration = _settings.mainNeckRelaxation; + _currentTimer = Timer(_settings.mainNeckRelaxation, _setNextState); + _restDurationTimer = Timer.periodic(Duration(seconds: 1), (timer) { + _restDuration -= Duration(seconds: 1); + }); } + /// Stops the current Meditation stopMeditation() { _settings.state = MeditationState.noStretch; - currentTimer?.cancel(); + _currentTimer.cancel(); + _restDurationTimer.cancel(); + _restDuration = Duration(seconds: 0); } - /// Used to set the next meditation state; + /// Used to set the next meditation state and set the correct Timer void _setNextState() { switch (_settings.state) { case MeditationState.noStretch: + case MeditationState.doneStretching: _settings.state = MeditationState.mainNeckStretch; return; case MeditationState.mainNeckStretch: _settings.state = MeditationState.leftNeckStretch; - currentTimer = Timer(_settings.rightNeckRelaxation, _setNextState); + _currentTimer = Timer(_settings.leftNeckRelaxation, _setNextState); + _restDuration = _settings.leftNeckRelaxation; _openEarable.audioPlayer.jingle(2); return; case MeditationState.leftNeckStretch: _settings.state = MeditationState.rightNeckStretch; - currentTimer = Timer(_settings.rightNeckRelaxation, _setNextState); + _currentTimer = Timer(_settings.rightNeckRelaxation, _setNextState); + _restDuration = _settings.rightNeckRelaxation; _openEarable.audioPlayer.jingle(2); return; case MeditationState.rightNeckStretch: - _settings.state = MeditationState.noStretch; + _settings.state = MeditationState.doneStretching; + _viewModel.stopTracking(); + _restDurationTimer.cancel(); + _restDuration = Duration(seconds: 0); _openEarable.audioPlayer.jingle(2); return; default: diff --git a/open_earable/lib/apps/neck_meditation/view/meditation_settings_view.dart b/open_earable/lib/apps/neck_meditation/view/meditation_settings_view.dart index 11aaf36..a0d86b9 100644 --- a/open_earable/lib/apps/neck_meditation/view/meditation_settings_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/meditation_settings_view.dart @@ -24,11 +24,14 @@ class _SettingsViewState extends State { super.initState(); this._viewModel = widget._viewModel; _mainNeckDuration = TextEditingController( - text: _viewModel.meditationSettings.mainNeckRelaxation.inSeconds.toString()); + text: _viewModel.meditationSettings.mainNeckRelaxation.inSeconds + .toString()); _leftNeckDuration = TextEditingController( - text: _viewModel.meditationSettings.leftNeckRelaxation.inSeconds.toString()); + text: _viewModel.meditationSettings.leftNeckRelaxation.inSeconds + .toString()); _rightNeckDuration = TextEditingController( - text: _viewModel.meditationSettings.rightNeckRelaxation.inSeconds.toString()); + text: _viewModel.meditationSettings.rightNeckRelaxation.inSeconds + .toString()); } @override @@ -64,10 +67,10 @@ class _SettingsViewState extends State { children: [ // add a switch to control the `isActive` property of the `BadPostureSettings` ListTile( - title: Text("Guided Neck Relaxation"), + title: Text("Meditation Settings"), ), ListTile( - title: Text("Main Neck Relaxation Duration (in seconds)"), + title: Text("Main Neck Relaxation Duration\n(in seconds)"), trailing: SizedBox( height: 37.0, width: 52, @@ -91,7 +94,7 @@ class _SettingsViewState extends State { ), ), ListTile( - title: Text("Left Neck Relaxation Duration (in seconds)"), + title: Text("Left Neck Relaxation Duration\n(in seconds)"), trailing: SizedBox( height: 37.0, width: 52, @@ -115,7 +118,7 @@ class _SettingsViewState extends State { ), ), ListTile( - title: Text("Right Neck Relaxation Duration (in seconds)"), + title: Text("Right Neck Relaxation Duration\n(in seconds)"), trailing: SizedBox( height: 37.0, width: 52, @@ -141,7 +144,6 @@ class _SettingsViewState extends State { ], ), ), - Padding( padding: EdgeInsets.all(8.0), child: Row(children: [ @@ -170,11 +172,24 @@ class _SettingsViewState extends State { ); } + /// Returns the new duration acquired from the Text. + /// Checks if the string is valid (doesn't contain '-' or '.'. + Duration getNewDuration(Duration duration, String newDuration) { + if (newDuration.contains('.') || newDuration.contains('-')) + return duration; + + return Duration(seconds: int.parse(newDuration)); + } + + /// Update the Meditation Settings according to values, if field is empty set that timer Duration to 0 void _updateMeditationSettings() { MeditationSettings settings = _viewModel.meditationSettings; - settings.mainNeckRelaxation = Duration(seconds:int.parse(_mainNeckDuration.text)); - settings.rightNeckRelaxation = Duration(seconds:int.parse(_rightNeckDuration.text)); - settings.leftNeckRelaxation = Duration(seconds:int.parse(_leftNeckDuration.text)); + settings.mainNeckRelaxation = + getNewDuration(settings.mainNeckRelaxation, _mainNeckDuration.text); + settings.rightNeckRelaxation = + getNewDuration(settings.rightNeckRelaxation, _rightNeckDuration.text); + settings.leftNeckRelaxation = + getNewDuration(settings.leftNeckRelaxation, _leftNeckDuration.text); _viewModel.setMeditationSettings(settings); } diff --git a/open_earable/lib/apps/neck_meditation/view/meditation_tracker_view.dart b/open_earable/lib/apps/neck_meditation/view/meditation_tracker_view.dart index 9b0d804..28ac8be 100644 --- a/open_earable/lib/apps/neck_meditation/view/meditation_tracker_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/meditation_tracker_view.dart @@ -21,78 +21,125 @@ class MeditationTrackerView extends StatefulWidget { class _MeditationTrackerViewState extends State { late final MeditationViewModel _viewModel; - // Used to store references for the timers used for the meditation - var mainNeckTimer; - var leftNeckTimer; - var rightNeckTimer; - @override void initState() { super.initState(); - this._viewModel = MeditationViewModel(widget._tracker, NeckMeditation(widget._openEarable)); + this._viewModel = MeditationViewModel(widget._tracker, widget._openEarable); + } + + /// Used to start the meditation via the button + void _startMeditation() { + this._viewModel.startTracking(); + this._viewModel.meditation.startMeditation(); + } + + /// Used to stop the meditation via the button + void _stopMeditation() { + this._viewModel.stopTracking(); + this._viewModel.meditation.stopMeditation(); + } + + TextSpan _getStatusText() { + if (!_viewModel.isAvailable) + return TextSpan( + text: "Connect an Earable to start Stretching!", + style: TextStyle( + color: Colors.red, + fontSize: 12, + ), + ); + + if (_viewModel.meditationState == MeditationState.noStretch) + return TextSpan(text: "Click the Button below\n to start Meditating!"); + + if (_viewModel.meditationState == MeditationState.doneStretching) + return TextSpan(children: [ + TextSpan(text: "You are done stretching.\n"), + TextSpan( + text: "Well done!", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Color.fromARGB(255, 0, 186, 255), + )), + ]); + + return TextSpan(children: [ + TextSpan( + text: "Currently Stretching: \n", + ), + TextSpan( + text: this._viewModel.meditationState.display, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Color.fromARGB(255, 0, 186, 255), + ), + ) + ]); + } + + Text _getButtonText() { + if (!_viewModel.isTracking) return Text('Start Meditation'); + + if (_viewModel.meditationState == MeditationState.doneStretching || + _viewModel.meditationState == MeditationState.noStretch) + return Text('Stop Meditation'); + + return Text(_viewModel.getRestDuration().toString().substring(2, 7)); } @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _viewModel, - builder: (context, child) => - Consumer( - builder: (context, neckStretchViewModel, child) => - Scaffold( - appBar: AppBar( - title: const Text("Guided Neck Relaxation"), - actions: [ - IconButton( - onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => SettingsView(this._viewModel))), - icon: Icon(Icons.settings) - ), - ], - ), - body: Center( - child: this._buildContentView(neckStretchViewModel), - ), - ))); + builder: (context, child) => Consumer( + builder: (context, neckStretchViewModel, child) => Scaffold( + appBar: AppBar( + title: const Text("Guided Neck Relaxation"), + actions: [ + IconButton( + onPressed: (this._viewModel.meditationState == + MeditationState.noStretch || + this._viewModel.meditationState == + MeditationState.doneStretching) + ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + SettingsView(this._viewModel))) + : null, + icon: Icon(Icons.settings)), + ], + ), + body: Center( + child: this._buildContentView(neckStretchViewModel), + ), + ))); } Widget _buildContentView(MeditationViewModel neckStretchViewModel) { var headViews = this._createHeadViews(neckStretchViewModel); - var stretchString = this._viewModel.meditationSettings.state.display; return Column( children: [ Padding( padding: EdgeInsets.all(5), - child: Visibility( - maintainSize: true, - maintainState: true, - maintainAnimation: true, - visible: this._viewModel.meditationSettings.state != MeditationState.noStretch, - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - children: [ - TextSpan( - text: "Currently Stretching: \n", - ), - TextSpan( - text: "$stretchString", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - color: Color.fromARGB(255, 0, 186, 255), - ), - ) - ], + child: Container( + height: 40, + child: Padding( + padding: EdgeInsets.fromLTRB(0, 8, 0, 0), + child: RichText( + textAlign: TextAlign.center, + text: _getStatusText(), ), ), ), ), + ...headViews.map( - (e) => - FractionallySizedBox( - widthFactor: .6, - child: e, - ), + (e) => FractionallySizedBox( + widthFactor: .6, + child: e, + ), ), // Used to place the Meditation-Button always at the bottom Expanded( @@ -104,7 +151,8 @@ class _MeditationTrackerViewState extends State { } /// Builds the actual head views using the PostureRollView - Widget _buildHeadView(String headAssetPath, + Widget _buildHeadView( + String headAssetPath, String neckAssetPath, AlignmentGeometry headAlignment, double roll, @@ -128,7 +176,8 @@ class _MeditationTrackerViewState extends State { return [ // Visible Head-Displays when not stretching Visibility( - visible: this._viewModel.meditationSettings.state == MeditationState.noStretch, + visible: this._viewModel.meditationState == MeditationState.noStretch || + this._viewModel.meditationState == MeditationState.doneStretching, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", "assets/posture_tracker/Neck_Front.png", @@ -138,7 +187,8 @@ class _MeditationTrackerViewState extends State { MeditationState.mainNeckStretch), ), Visibility( - visible: this._viewModel.meditationSettings.state == MeditationState.noStretch, + visible: this._viewModel.meditationState == MeditationState.noStretch || + this._viewModel.meditationState == MeditationState.doneStretching, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", "assets/posture_tracker/Neck_Side.png", @@ -150,7 +200,8 @@ class _MeditationTrackerViewState extends State { /// Visible Widgets for the main stretch Visibility( - visible: this._viewModel.meditationSettings.state == MeditationState.mainNeckStretch, + visible: + this._viewModel.meditationState == MeditationState.mainNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", "assets/posture_tracker/Neck_Front.png", @@ -160,7 +211,8 @@ class _MeditationTrackerViewState extends State { MeditationState.mainNeckStretch), ), Visibility( - visible: this._viewModel.meditationSettings.state == MeditationState.mainNeckStretch, + visible: + this._viewModel.meditationState == MeditationState.mainNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", "assets/neck_stretch/Neck_Main_Stretch.png", @@ -172,7 +224,8 @@ class _MeditationTrackerViewState extends State { /// Visible Widgets for the left stretch Visibility( - visible: this._viewModel.meditationSettings.state == MeditationState.leftNeckStretch, + visible: + this._viewModel.meditationState == MeditationState.leftNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", "assets/neck_stretch/Neck_Left_Stretch.png", @@ -182,7 +235,8 @@ class _MeditationTrackerViewState extends State { MeditationState.leftNeckStretch), ), Visibility( - visible: this._viewModel.meditationSettings.state == MeditationState.leftNeckStretch, + visible: + this._viewModel.meditationState == MeditationState.leftNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", "assets/posture_tracker/Neck_Side.png", @@ -194,7 +248,8 @@ class _MeditationTrackerViewState extends State { /// Visible Widgets for the right stretch Visibility( - visible: this._viewModel.meditationSettings.state == MeditationState.rightNeckStretch, + visible: + this._viewModel.meditationState == MeditationState.rightNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", "assets/neck_stretch/Neck_Right_Stretch.png", @@ -204,7 +259,8 @@ class _MeditationTrackerViewState extends State { MeditationState.rightNeckStretch), ), Visibility( - visible: this._viewModel.meditationSettings.state == MeditationState.rightNeckStretch, + visible: + this._viewModel.meditationState == MeditationState.rightNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", "assets/posture_tracker/Neck_Side.png", @@ -216,18 +272,6 @@ class _MeditationTrackerViewState extends State { ]; } - /// Used to start the meditation via the button - void _startMeditation() { - this._viewModel.startTracking(); - this._viewModel.meditation.startMeditation(); - } - - /// Used to stop the meditation via the button - void _stopMeditation() { - this._viewModel.stopTracking(); - this._viewModel.meditation.stopMeditation(); - } - // Creates the Button used to start the meditation Widget _buildMeditationButton(MeditationViewModel neckStretchViewModel) { return Padding( @@ -236,10 +280,10 @@ class _MeditationTrackerViewState extends State { ElevatedButton( onPressed: neckStretchViewModel.isAvailable ? () { - neckStretchViewModel.isTracking - ? _stopMeditation() - : _startMeditation(); - } + neckStretchViewModel.isTracking + ? _stopMeditation() + : _startMeditation(); + } : null, style: ElevatedButton.styleFrom( backgroundColor: !neckStretchViewModel.isTracking @@ -247,23 +291,8 @@ class _MeditationTrackerViewState extends State { : Color(0xfff27777), foregroundColor: Colors.black, ), - child: neckStretchViewModel.isTracking - ? const Text("Stop Meditation") - : const Text("Start Meditation"), + child: _getButtonText(), ), - Visibility( - visible: !neckStretchViewModel.isAvailable, - maintainState: true, - maintainAnimation: true, - maintainSize: true, - child: Text( - "No Earable Connected", - style: TextStyle( - color: Colors.red, - fontSize: 12, - ), - ), - ) ]), ); } diff --git a/open_earable/lib/apps/neck_meditation/view_model/meditation_view_model.dart b/open_earable/lib/apps/neck_meditation/view_model/meditation_view_model.dart index 183d1ab..a3eae0d 100644 --- a/open_earable/lib/apps/neck_meditation/view_model/meditation_view_model.dart +++ b/open_earable/lib/apps/neck_meditation/view_model/meditation_view_model.dart @@ -3,33 +3,47 @@ import "package:open_earable/apps/posture_tracker/model/attitude.dart"; import "package:open_earable/apps/posture_tracker/model/attitude_tracker.dart"; import 'package:open_earable/apps/neck_meditation/model/meditation_state.dart'; +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; + class MeditationViewModel extends ChangeNotifier { Attitude _attitude = Attitude(); + Attitude get attitude => _attitude; bool get isTracking => _attitudeTracker.isTracking; + bool get isAvailable => _attitudeTracker.isAvailable; + NeckMeditation get meditation => _meditation; + MeditationSettings get meditationSettings => _meditation.settings; + MeditationState get meditationState => this._meditation.settings.state; + AttitudeTracker _attitudeTracker; - NeckMeditation _meditation; + OpenEarable _openEarable; + late NeckMeditation _meditation; - MeditationViewModel(this._attitudeTracker, this._meditation) { + MeditationViewModel(this._attitudeTracker, this._openEarable) { _attitudeTracker.didChangeAvailability = (_) { notifyListeners(); }; + this._meditation = NeckMeditation(_openEarable, this); _attitudeTracker.listen((attitude) { _attitude = Attitude( - roll: attitude.roll, - pitch: attitude.pitch, - yaw: attitude.yaw + roll: attitude.roll, + pitch: attitude.pitch, + yaw: attitude.yaw ); notifyListeners(); }); } + Duration getRestDuration() { + return _meditation.getRestDuration(); + } + void startTracking() { _attitudeTracker.start(); notifyListeners(); @@ -37,6 +51,7 @@ class MeditationViewModel extends ChangeNotifier { void stopTracking() { _attitudeTracker.stop(); + _attitude = Attitude(); notifyListeners(); } @@ -44,6 +59,7 @@ class MeditationViewModel extends ChangeNotifier { _attitudeTracker.calibrateToCurrentAttitude(); } + /// Used to set the Duration Settings for Meditation void setMeditationSettings(MeditationSettings settings) { _meditation.setSettings(settings); } From 6f1efa4afc070dc711565461bc095d1e4a6088cf Mon Sep 17 00:00:00 2001 From: Polaris Date: Wed, 13 Dec 2023 00:55:00 +0100 Subject: [PATCH 026/104] Rename the files to better fit the app name and actual app purpose. Also add better arc-paintings for the meditation - still bugged left/right side in arcpainter --- ...ditation_state.dart => stretch_state.dart} | 55 +++++------ ..._painter.dart => stretch_arc_painter.dart} | 55 +++++++++-- ..._roll_view.dart => stretch_roll_view.dart} | 19 ++-- ...s_view.dart => stretch_settings_view.dart} | 16 ++-- ...er_view.dart => stretch_tracker_view.dart} | 92 +++++++++---------- ...iew_model.dart => stretch_view_model.dart} | 12 +-- open_earable/lib/apps_tab.dart | 4 +- 7 files changed, 148 insertions(+), 105 deletions(-) rename open_earable/lib/apps/neck_meditation/model/{meditation_state.dart => stretch_state.dart} (68%) rename open_earable/lib/apps/neck_meditation/view/{meditation_arc_painter.dart => stretch_arc_painter.dart} (62%) rename open_earable/lib/apps/neck_meditation/view/{meditation_roll_view.dart => stretch_roll_view.dart} (75%) rename open_earable/lib/apps/neck_meditation/view/{meditation_settings_view.dart => stretch_settings_view.dart} (93%) rename open_earable/lib/apps/neck_meditation/view/{meditation_tracker_view.dart => stretch_tracker_view.dart} (74%) rename open_earable/lib/apps/neck_meditation/view_model/{meditation_view_model.dart => stretch_view_model.dart} (78%) diff --git a/open_earable/lib/apps/neck_meditation/model/meditation_state.dart b/open_earable/lib/apps/neck_meditation/model/stretch_state.dart similarity index 68% rename from open_earable/lib/apps/neck_meditation/model/meditation_state.dart rename to open_earable/lib/apps/neck_meditation/model/stretch_state.dart index a3ec2ba..0e07685 100644 --- a/open_earable/lib/apps/neck_meditation/model/meditation_state.dart +++ b/open_earable/lib/apps/neck_meditation/model/stretch_state.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'package:open_earable/apps/neck_meditation/view_model/meditation_view_model.dart'; +import 'package:open_earable/apps/neck_meditation/view_model/stretch_view_model.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; /// Enum for the Meditation States -enum MeditationState { +enum NeckStretchState { mainNeckStretch, leftNeckStretch, rightNeckStretch, @@ -13,18 +13,18 @@ enum MeditationState { } /// Used to get a String representation for Display of the current meditation state -extension MeditationStateExtension on MeditationState { +extension NeckStretchStateExtension on NeckStretchState { String get display { switch (this) { - case MeditationState.mainNeckStretch: + case NeckStretchState.mainNeckStretch: return 'Main Neck Area'; - case MeditationState.leftNeckStretch: + case NeckStretchState.leftNeckStretch: return 'Right Neck Area'; - case MeditationState.rightNeckStretch: + case NeckStretchState.rightNeckStretch: return 'Left Neck Area'; - case MeditationState.noStretch: + case NeckStretchState.noStretch: return 'Not Stretching'; - case MeditationState.doneStretching: + case NeckStretchState.doneStretching: return 'You are done stretching. Good job!'; default: return 'Invalid State'; @@ -32,8 +32,8 @@ extension MeditationStateExtension on MeditationState { } } -class MeditationSettings { - MeditationState state; +class StretchSettings { + NeckStretchState state; /// Duration for the main neck relaxation Duration mainNeckRelaxation; @@ -44,21 +44,21 @@ class MeditationSettings { /// Duration for the right neck relaxation Duration rightNeckRelaxation; - MeditationSettings( - {this.state = MeditationState.noStretch, + StretchSettings( + {this.state = NeckStretchState.noStretch, required this.mainNeckRelaxation, required this.leftNeckRelaxation, required this.rightNeckRelaxation}); } class NeckMeditation { - MeditationSettings _settings = MeditationSettings( + StretchSettings _settings = StretchSettings( mainNeckRelaxation: Duration(seconds: 30), leftNeckRelaxation: Duration(seconds: 30), rightNeckRelaxation: Duration(seconds: 30)); final OpenEarable _openEarable; - final MeditationViewModel _viewModel; + final StretchViewModel _viewModel; /// Holds the Timer that increments the current Duration var _restDurationTimer; @@ -68,11 +68,12 @@ class NeckMeditation { /// Stores the current active timer for state transition var _currentTimer; - MeditationSettings get settings => _settings; + StretchSettings get settings => _settings; NeckMeditation(this._openEarable, this._viewModel); - void setSettings(MeditationSettings settings) { + /// Setter method for stretchSettings + void setSettings(StretchSettings settings) { _settings = settings; } @@ -83,7 +84,7 @@ class NeckMeditation { /// Starts the Meditation with the according timers startMeditation() { - _settings.state = MeditationState.mainNeckStretch; + _settings.state = NeckStretchState.mainNeckStretch; _restDuration = _settings.mainNeckRelaxation; _currentTimer = Timer(_settings.mainNeckRelaxation, _setNextState); _restDurationTimer = Timer.periodic(Duration(seconds: 1), (timer) { @@ -93,7 +94,7 @@ class NeckMeditation { /// Stops the current Meditation stopMeditation() { - _settings.state = MeditationState.noStretch; + _settings.state = NeckStretchState.noStretch; _currentTimer.cancel(); _restDurationTimer.cancel(); _restDuration = Duration(seconds: 0); @@ -102,24 +103,24 @@ class NeckMeditation { /// Used to set the next meditation state and set the correct Timer void _setNextState() { switch (_settings.state) { - case MeditationState.noStretch: - case MeditationState.doneStretching: - _settings.state = MeditationState.mainNeckStretch; + case NeckStretchState.noStretch: + case NeckStretchState.doneStretching: + _settings.state = NeckStretchState.mainNeckStretch; return; - case MeditationState.mainNeckStretch: - _settings.state = MeditationState.leftNeckStretch; + case NeckStretchState.mainNeckStretch: + _settings.state = NeckStretchState.leftNeckStretch; _currentTimer = Timer(_settings.leftNeckRelaxation, _setNextState); _restDuration = _settings.leftNeckRelaxation; _openEarable.audioPlayer.jingle(2); return; - case MeditationState.leftNeckStretch: - _settings.state = MeditationState.rightNeckStretch; + case NeckStretchState.leftNeckStretch: + _settings.state = NeckStretchState.rightNeckStretch; _currentTimer = Timer(_settings.rightNeckRelaxation, _setNextState); _restDuration = _settings.rightNeckRelaxation; _openEarable.audioPlayer.jingle(2); return; - case MeditationState.rightNeckStretch: - _settings.state = MeditationState.doneStretching; + case NeckStretchState.rightNeckStretch: + _settings.state = NeckStretchState.doneStretching; _viewModel.stopTracking(); _restDurationTimer.cancel(); _restDuration = Duration(seconds: 0); diff --git a/open_earable/lib/apps/neck_meditation/view/meditation_arc_painter.dart b/open_earable/lib/apps/neck_meditation/view/stretch_arc_painter.dart similarity index 62% rename from open_earable/lib/apps/neck_meditation/view/meditation_arc_painter.dart rename to open_earable/lib/apps/neck_meditation/view/stretch_arc_painter.dart index eefbdff..a8a5c9b 100644 --- a/open_earable/lib/apps/neck_meditation/view/meditation_arc_painter.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_arc_painter.dart @@ -1,16 +1,17 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:open_earable/apps/neck_meditation/model/meditation_state.dart'; +import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; import 'package:open_earable/apps/posture_tracker/view/arc_painter.dart'; -class MeditationArcPainter extends CustomPainter { +class StretchArcPainter extends CustomPainter { /// the angle of rotation final double angle; final double angleThreshold; - final MeditationState meditationState; + final NeckStretchState stretchState; + final bool isFront; - MeditationArcPainter({required this.angle, this.angleThreshold = 0, this.meditationState = MeditationState.noStretch}); + StretchArcPainter({required this.angle, this.angleThreshold = 0, this.stretchState = NeckStretchState.noStretch, required this.isFront}); @override void paint(Canvas canvas, Size size) { @@ -56,7 +57,7 @@ class MeditationArcPainter extends CustomPainter { ); Paint angleOvershootPaint = Paint() - ..color = this.meditationState == MeditationState.noStretch ? Colors.red : Color.fromARGB(255, 0, 186, 255) + ..color = Color.fromARGB(255, 0, 186, 255) ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeWidth = 5.0; @@ -64,17 +65,16 @@ class MeditationArcPainter extends CustomPainter { Path thresholdPath = Path(); thresholdPath.addArc( Rect.fromCircle(center: center, radius: radius), // create a rectangle from the center and radius - startAngle - angleThreshold, // start angle - 2 * angleThreshold, // sweep angle + getStartAngle(startAngle, angleThreshold), // start angle + getThreshold(angleThreshold), // sweep angle ); Paint thresholdPaint = Paint() - ..color = this.meditationState == MeditationState.noStretch ? Colors.purpleAccent[100]! : Colors.redAccent[100]! + ..color = Colors.redAccent[100]! ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeWidth = 5.0; - // Draw the path on the canvas canvas.drawPath(thresholdPath, thresholdPaint); canvas.drawPath(anglePath, anglePaint); @@ -83,6 +83,43 @@ class MeditationArcPainter extends CustomPainter { } } + /// Gets the right start angle depending on stretch state + double getStartAngle(double startAngle, double threshold) { + if (!this.isFront) + return startAngle - threshold; + + switch(this.stretchState) { + case NeckStretchState.leftNeckStretch: + return startAngle - threshold; + case NeckStretchState.rightNeckStretch: + return startAngle - threshold - pi/2 - 2/18 * pi; + default: + return startAngle - threshold; + } + } + + /// Gets the right threshold depending on stretch state + double getThreshold(double threshold) { + if (this.isFront) { + switch(this.stretchState) { + case NeckStretchState.rightNeckStretch: + case NeckStretchState.leftNeckStretch: + return 2 * threshold + pi/2 + 2/18 * pi; + default: + return 2 * threshold; + } + } + + switch(this.stretchState) { + case NeckStretchState.mainNeckStretch: + return 2 * threshold + pi/2 + 1/36 * pi; + case NeckStretchState.rightNeckStretch: + return 2 * threshold + pi/2; + default: + return 2 * threshold; + } + } + @override bool shouldRepaint(covariant CustomPainter oldDelegate) { // check if oldDelegate is an ArcPainter and if the angle is the same diff --git a/open_earable/lib/apps/neck_meditation/view/meditation_roll_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_roll_view.dart similarity index 75% rename from open_earable/lib/apps/neck_meditation/view/meditation_roll_view.dart rename to open_earable/lib/apps/neck_meditation/view/stretch_roll_view.dart index 2624639..21fe72a 100644 --- a/open_earable/lib/apps/neck_meditation/view/meditation_roll_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_roll_view.dart @@ -1,11 +1,11 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:open_earable/apps/neck_meditation/model/meditation_state.dart'; -import 'package:open_earable/apps/neck_meditation/view/meditation_arc_painter.dart'; +import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_meditation/view/stretch_arc_painter.dart'; /// A widget that displays the roll of the head and neck for the meditation. -class MeditationRollView extends StatelessWidget { +class StretchRollView extends StatelessWidget { static final double _MAX_ROLL = pi / 2; /// The roll of the head and neck in radians. @@ -17,18 +17,23 @@ class MeditationRollView extends StatelessWidget { final AlignmentGeometry headAlignment; // Checks whether the arc has different properties due to meditation state - final MeditationState meditationState; + final NeckStretchState stretchState; - const MeditationRollView( + const StretchRollView( {Key? key, required this.roll, this.angleThreshold = 0, required this.headAssetPath, required this.neckAssetPath, this.headAlignment = Alignment.center, - this.meditationState = MeditationState.noStretch}) + this.stretchState = NeckStretchState.noStretch}) : super(key: key); + /// Returns true if this is a StretchRollView for a front facing head. False otherwise. + bool _isFront() { + return headAssetPath.contains("Front.png"); + } + @override Widget build(BuildContext context) { return Column(children: [ @@ -40,7 +45,7 @@ class MeditationRollView extends StatelessWidget { fontWeight: FontWeight.bold)), CustomPaint( painter: - MeditationArcPainter(angle: this.roll, angleThreshold: this.angleThreshold, meditationState: this.meditationState), + StretchArcPainter(angle: this.roll, angleThreshold: this.angleThreshold, stretchState: this.stretchState, isFront: _isFront()), child: Padding( padding: EdgeInsets.all(10), child: ClipOval( diff --git a/open_earable/lib/apps/neck_meditation/view/meditation_settings_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart similarity index 93% rename from open_earable/lib/apps/neck_meditation/view/meditation_settings_view.dart rename to open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart index a0d86b9..0152c6f 100644 --- a/open_earable/lib/apps/neck_meditation/view/meditation_settings_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:open_earable/apps/neck_meditation/model/meditation_state.dart'; -import 'package:open_earable/apps/neck_meditation/view_model/meditation_view_model.dart'; +import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_meditation/view_model/stretch_view_model.dart'; import 'package:provider/provider.dart'; class SettingsView extends StatefulWidget { - final MeditationViewModel _viewModel; + final StretchViewModel _viewModel; SettingsView(this._viewModel); @@ -17,7 +17,7 @@ class _SettingsViewState extends State { late final TextEditingController _leftNeckDuration; late final TextEditingController _rightNeckDuration; - late final MeditationViewModel _viewModel; + late final StretchViewModel _viewModel; @override void initState() { @@ -38,9 +38,9 @@ class _SettingsViewState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Guided Neck Meditation Settings")), - body: ChangeNotifierProvider.value( + body: ChangeNotifierProvider.value( value: _viewModel, - builder: (context, child) => Consumer( + builder: (context, child) => Consumer( builder: (context, postureTrackerViewModel, child) => _buildSettingsView(), )), @@ -158,7 +158,7 @@ class _SettingsViewState extends State { onPressed: _viewModel.isTracking ? () { _viewModel.calibrate(); - Navigator.of(context).pop(); + _viewModel.stopTracking(); } : () => _viewModel.startTracking(), child: Text(_viewModel.isTracking @@ -183,7 +183,7 @@ class _SettingsViewState extends State { /// Update the Meditation Settings according to values, if field is empty set that timer Duration to 0 void _updateMeditationSettings() { - MeditationSettings settings = _viewModel.meditationSettings; + StretchSettings settings = _viewModel.meditationSettings; settings.mainNeckRelaxation = getNewDuration(settings.mainNeckRelaxation, _mainNeckDuration.text); settings.rightNeckRelaxation = diff --git a/open_earable/lib/apps/neck_meditation/view/meditation_tracker_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart similarity index 74% rename from open_earable/lib/apps/neck_meditation/view/meditation_tracker_view.dart rename to open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart index 28ac8be..01c1b94 100644 --- a/open_earable/lib/apps/neck_meditation/view/meditation_tracker_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart @@ -1,30 +1,30 @@ import 'package:flutter/material.dart'; -import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; -import 'package:open_earable/apps/neck_meditation/view/meditation_roll_view.dart'; -import 'package:open_earable/apps/neck_meditation/view_model/meditation_view_model.dart'; import 'package:provider/provider.dart'; -import 'package:open_earable/apps/neck_meditation/model/meditation_state.dart'; -import 'package:open_earable/apps/neck_meditation/view/meditation_settings_view.dart'; +import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; +import 'package:open_earable/apps/neck_meditation/view/stretch_roll_view.dart'; +import 'package:open_earable/apps/neck_meditation/view_model/stretch_view_model.dart'; +import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_meditation/view/stretch_settings_view.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; -class MeditationTrackerView extends StatefulWidget { +class StretchTrackerView extends StatefulWidget { final AttitudeTracker _tracker; final OpenEarable _openEarable; - MeditationTrackerView(this._tracker, this._openEarable); + StretchTrackerView(this._tracker, this._openEarable); @override - State createState() => _MeditationTrackerViewState(); + State createState() => _StretchTrackerViewState(); } -class _MeditationTrackerViewState extends State { - late final MeditationViewModel _viewModel; +class _StretchTrackerViewState extends State { + late final StretchViewModel _viewModel; @override void initState() { super.initState(); - this._viewModel = MeditationViewModel(widget._tracker, widget._openEarable); + this._viewModel = StretchViewModel(widget._tracker, widget._openEarable); } /// Used to start the meditation via the button @@ -49,10 +49,10 @@ class _MeditationTrackerViewState extends State { ), ); - if (_viewModel.meditationState == MeditationState.noStretch) + if (_viewModel.meditationState == NeckStretchState.noStretch) return TextSpan(text: "Click the Button below\n to start Meditating!"); - if (_viewModel.meditationState == MeditationState.doneStretching) + if (_viewModel.meditationState == NeckStretchState.doneStretching) return TextSpan(children: [ TextSpan(text: "You are done stretching.\n"), TextSpan( @@ -82,8 +82,8 @@ class _MeditationTrackerViewState extends State { Text _getButtonText() { if (!_viewModel.isTracking) return Text('Start Meditation'); - if (_viewModel.meditationState == MeditationState.doneStretching || - _viewModel.meditationState == MeditationState.noStretch) + if (_viewModel.meditationState == NeckStretchState.doneStretching || + _viewModel.meditationState == NeckStretchState.noStretch) return Text('Stop Meditation'); return Text(_viewModel.getRestDuration().toString().substring(2, 7)); @@ -91,18 +91,18 @@ class _MeditationTrackerViewState extends State { @override Widget build(BuildContext context) { - return ChangeNotifierProvider.value( + return ChangeNotifierProvider.value( value: _viewModel, - builder: (context, child) => Consumer( + builder: (context, child) => Consumer( builder: (context, neckStretchViewModel, child) => Scaffold( appBar: AppBar( title: const Text("Guided Neck Relaxation"), actions: [ IconButton( onPressed: (this._viewModel.meditationState == - MeditationState.noStretch || + NeckStretchState.noStretch || this._viewModel.meditationState == - MeditationState.doneStretching) + NeckStretchState.doneStretching) ? () => Navigator.of(context).push( MaterialPageRoute( builder: (context) => @@ -117,7 +117,7 @@ class _MeditationTrackerViewState extends State { ))); } - Widget _buildContentView(MeditationViewModel neckStretchViewModel) { + Widget _buildContentView(StretchViewModel neckStretchViewModel) { var headViews = this._createHeadViews(neckStretchViewModel); return Column( children: [ @@ -157,16 +157,16 @@ class _MeditationTrackerViewState extends State { AlignmentGeometry headAlignment, double roll, double angleThreshold, - MeditationState state) { + NeckStretchState state) { return Padding( padding: const EdgeInsets.all(5), - child: MeditationRollView( + child: StretchRollView( roll: roll, angleThreshold: angleThreshold * 3.14 / 180, headAssetPath: headAssetPath, neckAssetPath: neckAssetPath, headAlignment: headAlignment, - meditationState: state, + stretchState: state, ), ); } @@ -176,104 +176,104 @@ class _MeditationTrackerViewState extends State { return [ // Visible Head-Displays when not stretching Visibility( - visible: this._viewModel.meditationState == MeditationState.noStretch || - this._viewModel.meditationState == MeditationState.doneStretching, + visible: this._viewModel.meditationState == NeckStretchState.noStretch || + this._viewModel.meditationState == NeckStretchState.doneStretching, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", "assets/posture_tracker/Neck_Front.png", Alignment.center.add(Alignment(0, 0.3)), neckStretchViewModel.attitude.roll, 0, - MeditationState.mainNeckStretch), + NeckStretchState.noStretch), ), Visibility( - visible: this._viewModel.meditationState == MeditationState.noStretch || - this._viewModel.meditationState == MeditationState.doneStretching, + visible: this._viewModel.meditationState == NeckStretchState.noStretch || + this._viewModel.meditationState == NeckStretchState.doneStretching, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", "assets/posture_tracker/Neck_Side.png", Alignment.center.add(Alignment(0, 0.3)), -neckStretchViewModel.attitude.pitch, 0, - MeditationState.mainNeckStretch), + NeckStretchState.noStretch), ), /// Visible Widgets for the main stretch Visibility( visible: - this._viewModel.meditationState == MeditationState.mainNeckStretch, + this._viewModel.meditationState == NeckStretchState.mainNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", "assets/posture_tracker/Neck_Front.png", Alignment.center.add(Alignment(0, 0.3)), neckStretchViewModel.attitude.roll, 7.0, - MeditationState.mainNeckStretch), + NeckStretchState.mainNeckStretch), ), Visibility( visible: - this._viewModel.meditationState == MeditationState.mainNeckStretch, + this._viewModel.meditationState == NeckStretchState.mainNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", "assets/neck_stretch/Neck_Main_Stretch.png", Alignment.center.add(Alignment(0, 0.3)), -neckStretchViewModel.attitude.pitch, 50.0, - MeditationState.mainNeckStretch), + NeckStretchState.mainNeckStretch), ), - /// Visible Widgets for the left stretch + /// Visible Widgets for the right stretch Visibility( visible: - this._viewModel.meditationState == MeditationState.leftNeckStretch, + this._viewModel.meditationState == NeckStretchState.rightNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", - "assets/neck_stretch/Neck_Left_Stretch.png", + "assets/neck_stretch/Neck_Right_Stretch.png", Alignment.center.add(Alignment(0, 0.3)), neckStretchViewModel.attitude.roll, 30.0, - MeditationState.leftNeckStretch), + NeckStretchState.rightNeckStretch), ), Visibility( visible: - this._viewModel.meditationState == MeditationState.leftNeckStretch, + this._viewModel.meditationState == NeckStretchState.rightNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", "assets/posture_tracker/Neck_Side.png", Alignment.center.add(Alignment(0, 0.3)), -neckStretchViewModel.attitude.pitch, 15.0, - MeditationState.leftNeckStretch), + NeckStretchState.rightNeckStretch), ), - /// Visible Widgets for the right stretch + /// Visible Widgets for the left stretch Visibility( visible: - this._viewModel.meditationState == MeditationState.rightNeckStretch, + this._viewModel.meditationState == NeckStretchState.leftNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Front.png", - "assets/neck_stretch/Neck_Right_Stretch.png", + "assets/neck_stretch/Neck_Left_Stretch.png", Alignment.center.add(Alignment(0, 0.3)), neckStretchViewModel.attitude.roll, 30.0, - MeditationState.rightNeckStretch), + NeckStretchState.leftNeckStretch), ), Visibility( visible: - this._viewModel.meditationState == MeditationState.rightNeckStretch, + this._viewModel.meditationState == NeckStretchState.leftNeckStretch, child: this._buildHeadView( "assets/posture_tracker/Head_Side.png", "assets/posture_tracker/Neck_Side.png", Alignment.center.add(Alignment(0, 0.3)), -neckStretchViewModel.attitude.pitch, 15.0, - MeditationState.rightNeckStretch), + NeckStretchState.leftNeckStretch), ), ]; } // Creates the Button used to start the meditation - Widget _buildMeditationButton(MeditationViewModel neckStretchViewModel) { + Widget _buildMeditationButton(StretchViewModel neckStretchViewModel) { return Padding( padding: EdgeInsets.all(5), child: Column(children: [ diff --git a/open_earable/lib/apps/neck_meditation/view_model/meditation_view_model.dart b/open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart similarity index 78% rename from open_earable/lib/apps/neck_meditation/view_model/meditation_view_model.dart rename to open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart index a3eae0d..9bd70a2 100644 --- a/open_earable/lib/apps/neck_meditation/view_model/meditation_view_model.dart +++ b/open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart @@ -1,11 +1,11 @@ import "package:flutter/material.dart"; import "package:open_earable/apps/posture_tracker/model/attitude.dart"; import "package:open_earable/apps/posture_tracker/model/attitude_tracker.dart"; -import 'package:open_earable/apps/neck_meditation/model/meditation_state.dart'; +import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; -class MeditationViewModel extends ChangeNotifier { +class StretchViewModel extends ChangeNotifier { Attitude _attitude = Attitude(); Attitude get attitude => _attitude; @@ -16,15 +16,15 @@ class MeditationViewModel extends ChangeNotifier { NeckMeditation get meditation => _meditation; - MeditationSettings get meditationSettings => _meditation.settings; + StretchSettings get meditationSettings => _meditation.settings; - MeditationState get meditationState => this._meditation.settings.state; + NeckStretchState get meditationState => this._meditation.settings.state; AttitudeTracker _attitudeTracker; OpenEarable _openEarable; late NeckMeditation _meditation; - MeditationViewModel(this._attitudeTracker, this._openEarable) { + StretchViewModel(this._attitudeTracker, this._openEarable) { _attitudeTracker.didChangeAvailability = (_) { notifyListeners(); }; @@ -60,7 +60,7 @@ class MeditationViewModel extends ChangeNotifier { } /// Used to set the Duration Settings for Meditation - void setMeditationSettings(MeditationSettings settings) { + void setMeditationSettings(StretchSettings settings) { _meditation.setSettings(settings); } diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 79bb209..4b2e71d 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; import 'package:open_earable/apps/recorder.dart'; -import 'package:open_earable/apps/neck_meditation/view/meditation_tracker_view.dart'; +import 'package:open_earable/apps/neck_meditation/view/stretch_tracker_view.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class AppInfo { @@ -54,7 +54,7 @@ class AppsTab extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => MeditationTrackerView( + builder: (context) => StretchTrackerView( EarableAttitudeTracker(_openEarable), _openEarable))); }), // ... similarly for other apps From 33adc853acdc74a73ce2c6bf1e35aaf4b0ae0a5e Mon Sep 17 00:00:00 2001 From: Polaris Date: Wed, 13 Dec 2023 17:09:25 +0100 Subject: [PATCH 027/104] Add the fix for the left and right side meditation switch and clean up code and add correct colours and overshoot colors for arc drawings --- .../assets/neck_stretch/Neck_Left_Stretch.png | Bin 20134 -> 20010 bytes .../neck_stretch/Neck_Right_Stretch.png | Bin 20010 -> 20134 bytes .../neck_meditation/model/stretch_state.dart | 95 +++++---- .../view/stretch_arc_painter.dart | 134 ++++++++++--- .../view/stretch_settings_view.dart | 13 +- .../view/stretch_tracker_view.dart | 185 ++++++------------ .../view_model/stretch_view_model.dart | 15 +- 7 files changed, 234 insertions(+), 208 deletions(-) diff --git a/open_earable/assets/neck_stretch/Neck_Left_Stretch.png b/open_earable/assets/neck_stretch/Neck_Left_Stretch.png index 3408e49f0574b6d24d7f4aaa680395e09e36ec50..8562cfb3c322d5bdb19d79f24c570643f886acc9 100644 GIT binary patch literal 20010 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

e%Ew~0sC1q6vI9(|OQepiO_5R_2J%@{>y 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 literal 20134 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

e%Ew~0sC1q6vI9(|OQepiO_5R_2J%@{>y zQ-X~VzY*wU=_V*IK-!Fsy2o#+ee}o99zEyv*m43E$Vl+A0ty}Na@lmb0d@`_n6vS= zhv}*QI5HfUKP}65MKVX>O>R7Ukl#6Ri;#yP3wk&;9}4>zmM8fhday2aHRijQ+^r8H z7da$JkV{~tJM9oPcjpyufgle!I=DJvY8G|Os#XE%t3Ac&uQBkVQDD(kb&Gjfu%l+b z1+kX*p+Louh>-x4DLRt2%G6)u++iCV%bq%R`aQrd2}nNW`mj zy_eI{V>nsAPbu17jv%jpd*y(QtGw7DafA!M?(&;LqzmR(*|!9S7w~6WQJBW`^zr3} zHhbeJwaa~V<()GGJe%5 z9KH8IF!q&P2q6P9Mgq}ot@-$%gcZ*3%ILe@X)yaa^gt5f!RFXue8*=v@N6)8NXOAh zemsfZlOk?JS+2*2BY)aqhI{DVTz<)cwHx|u0#_5Nm#op4GXA7O&i!hI3FMRHfPsI8 zh=C+sTj`2Z7-QO0H)ppaR9})DWWdLYO=hd5}~DXLAqx)-s%Kzne>K@zwt99CgyNlBQa?LS(+L!Ffd}C57N$t%%T>qdm6Ut{*PYEZ%N9K z;v42?CL|0D3=<;F_ie1;TO&Hy!FqP7Dm9oW{XU-_L3&BxgnpOWz+pzK&j_VNTZhIO ztM5+{6xwIz4_tYjIbZYA=>rs-DW~0h&B8KaFF@Ba07R+{b>!?WQ4!@}JE!c2%8Tg# zBN57;ayPBBWt*EGc9>S$YawWN8#ts5h-)HgK2f1dr`oL|4(}nsft~={cRk)theGQ? zD%W&6a!MEXoyUS=0NN0`u2$O@4tC;GVkk!`r-pz|;M$@to56fn+Z+Y-Sn93>q_fOn%DeOpb@l%gXSN2A#q3o=-U$e}xZ$U|kO;xTLdMF2Gmb6P)>wP*Brt}@ z2OoV78!gChnzm0Y%so~^j$^ScZ)U0=-ppV9{g*b&+T`y(i1N|FWlVNH0MvU|1%0-- zb&xETL1AQ!T=`qD!uTPIT=MslS8mvhQQyxM6Z+*`=o?)RMLF7w&LuLkwyM*KgPHUH zs~#>Naj>~{OC~B|<{a(Y;zA}2_x0d0b9esQpR*z;wzfrZ!yy~8O1j%~p3pV(4;Yr` zc{W{Z$hHg!v33bN4ubAyPzkzaPBb@-kfWMhvD~V6#YozCGU?ZnF9vW)@D)^rnzr)w zN(tesDWW6z(3UKluXCU5ZkeNO)InlD3iKyJ5Sp#K{%c-%{pZoz>Lzll$yBUPQ0O_M{HI)z7)zz(6uuja=PBpp-J z;W%EQg-!f9%ugmto*)`@gZfHSbE+lF;vQ|<`>vKSL*dE&nR{7B zM6mHBPtD)#4mYd8CDMMd)GzaiO_X=snA+J7nHu5tjHO$=%kUP1$nuoVGq`Bk@B{g2Ow>V zK_VJ(jPnf%AF2X$TAB@{`Ch~qkZxdN+G)5fgei=L=k@w7S2azR2&34cir9C|2VIi~ zp&ci2Alp9B28=|aX=gC3@;*BKZScFFqtz`D$cwde$@0$dq2n0^o3bf7E;JLl_LwR& zUmFX!urDw4f0`0-ZVOcLa@zi9D3V=n5R74MUuKlgpNYWGFF6C$Rm2q+m=hMPF!k^4L1D#z!wFb@cIu8b5*^|BWDXRmM9K z?*U5Fa3$lkFna6SngQ3QWP7wpK5v?lOn%D!35o#)0H8tAW&$IXjc>75OZsDZv*3~y z=+uF;&&F@9*gc_~8J*%$U`-6$tLUU^N~6fIlE|1iV(8|AGlfR_4GO#o2)Xurd9Z*x z&+F%4Qyjs0>xOD|ouv}F6ddW+4*;&EYAs=xS=;-4tGQIFxRh_bHv|RVf>{XbkLguj z^Y-}QM+yn7Jdqpb7ZU!ME3)+riiKD}yI?V})Zr6Vxt3waN=fW&-GD2fkG+~=_>#MM z>$$A9(Df>L{fz(zuV&kRPQ+y6d01R_*$#b0Gw$m91P1EM<8zqbXq2Y;$0y618}EShpA?9 zvKyMm)l)VY2A|^J0oPw@NOt6aGjZir9>&XuwN2y(0XEV<;G?Px znjXkZKwoZ>YYXoyUCV6? zPf(s+aY&XIs2G3^OSpI>46Q*ve`Ip@P;}tQ_?3*H@eXbHeDoTKNMhlBOO#)rzsI1x z75l*nQP2jrM=866uH11T0E2P^6b`&93>{ex9zIFGJ79b`h3)h{IM-DNn$V#ih7L?U z=~yWS>F36Nv$Umo1wH0PP}6Gaf{n#5ky_|Wx7DbrLuA?p=jvL-(bj9Uy}mx2(>NxVla+vZ|Q`TANB!-Ap4s z7P$Es>8^ESPFV>E;7iHyd;Q2a(+k1FZCA>LkY2-sXC8QP7e_cKHk4^} zic={GtukUd&gUHq*f%2{uo53$$Qu z`|CF*mpD~2N*w`8XvG57F<+i`pB}7rUF)q@#Wb>pc#axl+ z1Zkzq+0R|kP$29o;_P>$MNAj{$gX3g5H2;Lwy5?{BAMiA`%J>Gxw-y@%UKZHSHTe= zQ`zT=#!B7si;|U;prI+M_yA@3j4@OW&X;Z-t3ywEvC}E6>YV$uHY>^7Q@2mauUVW& zP*1vBnbRF$5^%xM(NADv1D`)e>m}CBcm?>VAO#f_6-l8B8_i!KXygm0T?#TQV|?wk zevk2}*Ro=dksNN*C0nGz-f=~EMpWLVD=U7ioe%-e&@X%vwncStUyZ9f z*#Q9P0JWSugVUyI$uA>1d-i%H?a5oA-bcm(Gl@y+}jya3BeIp*G{}?B=JR} z@t2~8ixL%5;eG=R4hp5k-ld$8&f^+}p*6uyr-3fd9)1zwSE+? zol2r-gj=z8`wrOMKAf6G^i|n7l$DN_8TB51d)b49MUb;3n|DXMCbKgv%v_+mEvlty`YqA1}_p{Zbd!J}?o4sq--5 zZS={yER)s9i(l$YW*%zMMctU?wC?c(2Q8sVFRcW=pOh%S_Q-tAh%o=lAn~BKbRz%l zYK#i#atauOStle*C`OlZs*`OrWJ-nj&YGoKuN=A6p;tI~s(mJ*4L>^OiiTUrti517 z3N|fZg&=u3TCMqe#QE%$za`5Lo6;G=YVhn-=MG_~v+;|cjNp!0KRAN=R=9-CbtFYS z)5e}>$@TjOx}#SrKVlO?to!s#j|uKLftq=q?(_;10JsiJiiN27Sm;mo94DNkpI5$m zRv}eyDFGZ39)$+*ZoOF^GU52(ZeD)El^8P#qc z49+{Em4jHn6r52i)_$daCGY4;B?HA!QevUmrBiSBk&+?bF%S{wd%g0ymj;`(5_V{S z5{K+NvGy*wEjwqWa!oIaHrC~8`p#slTPqOJL~=;vR7VcK@^d`MaDE5w>pU6C;-KSg zn)&{+Z6Mj~5V=jDR=?k$xAX;=wC-WxqRS&O4=*MoH7kHDbg8+xyX79PO zQqp>}CvwT6#KM)dWP@`UVa>iJ7W2{hK`?od?+n8hUH;o7oOSU=Fb~_-wdz;-xM{{J zV9J*lt8Rt2_Oe!88D5`EZPRos^AntYcRwxgE6Mdrlj|8w?b^}85`Fcv)94vV4n-Ci z*!>Ja6BVSk9+<1hwuuD=UTcBt*N2BRp=K^Pt zBBtk}7lpohwm>f`sM&i5-_{a!DCePc@!5xRxg}Z|WV>m8__%h+%5YAOL)mjKu68yJ zFEkW?lm9E0JW!*XTLLsOpr37-L%I}&@t;+S3?KOg0wq<{vC%jtMY`BPPZq-1Q^B?DYgs3#;zWeN;17&3LC?m|#Otza{{sIdK9*rG z>~nj&FlB67^adjcwm~?ihhE*7$r`igmdK0BNBTopbZfb_j~V4-j8HgYss#8XeaX__ zIw1JdwOVTPObscAxX?z`kNz+K1s@mQ@cw@~sw zXHcXRQM$ zzw{^F{Wt#=9)6O&&)ZCX`ig1JCy*wk!*%j-g}gqtrs=vzl-aA{Y| zb@-pP7uNde6Hbe7&xT)Pzdn4TR%U-ujZ7_Nw~DkqIrLbj%}89{iOJNZ!Fw%d$zY=D zruly<06`9>pw~KxZ2j5Q{M9P{-PEL-mJ@w)M%uxyjmL7&Myk2_4%fN%Ahm;Nhdf;i z^0~V*`9B7~fCu1w6TtbF#O*C2FWC45uX#R^EXqb;=?7B8m>slCUYzXgo4HQOI66xV8E!Lop!k~JHpeC7yc3G8`{Y`(2)2--p!EwDD2kz^@i-Si}^=uchI=3?=8~$6{{|G*yk)6xYDz zhK2Tg5XVYzXPp=^H7stcR( z0XAh}D^sB(Jn|v>>T1Q_Iz89ko`q@QWFqI+WKjWn?RUsQ=0fQVZ6)IcrF>SQ$AZ2z z*HKmyyHsdrnr2k06%{-*1spDUdHLF_dcE?0oWj*-k3$3D|3{FI%5^CG_|#-RqfV?i z0_3#8M-k~^&vxcXQFLeqYfZW(i(`AeH-#GL4J{eudlvo#MJWF(1bxE7sO!=SR@KQU zs7j4bb9_DKMya`*k_1elxJIRhop|%|Xl5}qQ7n5m={VbIQfbpQbiltJbt4iTwcheM zB)2+uLvKY#saCiqxGJ@VvYUyU`k69&&E=C`xk3LG=TP2RNty#j*f!%kGQ^lI*9cUQ zS2Fih#bXEmYqAqZQnt>uSoK8hiyHQ=PMfQV?lR$`ydZ(niTmXCa3Z!U-xUl|_8Oo4 z*Be4I*-ez`MJMZ$3t!`~$bW>^(zR#ZRvN21LT9?>rIO|a(3>Bq>CNhJjxTSqYAKz5 z0vysG%%*KN!Z_QQmP(%~(QhvO`%3kb)y1cjo0s=TU6?2jWdIWX0SkM*H$OO~$G%$J ztKoX|b z{Y#up$EK}+HRsCCbnMF^@|6J;hdlCcNJ*!qx|XnLl*QC%m7N6&jI*n7zV1v!CpzHh zGO>$48M#>w*HK`&4?F26qq0Bu{h#g6fPB3;o2S7W;?34?2Q~g;vyq|vBoF^)nNi4C zs0xVupMqw#|8S6~VMMK$jV^o6@a^V<1OKj`G)G&RKS9^^j2xf-RNj%I!|ugS^iq2H zcNq)S)WiMliO{~`I-SOXg|fiv-A4XsmRiNc|8XETu%P}YxfISqy9y1oH;#!vlhfGU zE1xGvmu`^{d)@kHw7);@y;JTAb;L8BUk%jP{eR*qJqj3LO==RqvtZ)+iS`5=kl6HH z^vkt7Zqu2xs7#DrZAM+Lj)orNBL&F)f!x2QlN^BSbm277DHsgPiKtWhVzFl?+@M}z z;Lvaye?W#c8j|NYw`<{#5C8lC!;R%G>*N`&7S9{9>}!uKr1Kc=y%!$QCxt@)1cP1w zjHP;#=gaaJAl8NdD`WTN`zkHoPewIJmPU2}?^IDA=4o1Xeul-w7I7sq^?$tSQS029 zo2um*SRWvbS3mx2h9NeSO-+TiIycW4R9g)e*0jel+k`t%^N&t z8$#O}C8lf^e^+VfUEFlSw#*RFO$xLzn58K>ka=rV_h>=pccp)xfDdzL3I@h@=AL$G zyNZ*|2ylF;s30$&uTcr$+T3$pPlR?3awb3fzcHe!CR z?GJQQW_Iq~gA0=y?gfhv}A|{6+Xe<@3G#ZdvlJ#VNR9}E=!3*vcU5W1+saWed8dl%e_F#F~L!Y=SanxRs*=~o*F>6*f zMKw{Eq0Hag^=9V@?L~228A@~+v4!Efh3vY8Cw=cc_g9k>J80oB49|eHxX@8HsZp`6(wWECoNK(PUkuy6HQ3ni8C_q>8Rs8$J5I$&&Wn7(KxDr*TN6#lR zaC8j~@T4h7lF;wxzUwAaeZeJav22t&y1S{*KwpuVZg&)E5GM+SUeL3kN$dF>6DuB- zEgqKO<1Z|Y>lYRK7%mPaEZmc?8~7emq6dp2XWuHNgmL@gxM>osiMW-(g>ga1`EG!U zU0;S$`F%xfuG~x^q1_?Ci=F7BGH~mAAy(nLh!ffY}AVe{e!? zc1q%MK=Yz7o-?;RKlOesj_F)R=Bk(eD3q4kixS62lNw!@hAdemJYazO5o{WYZ&)<) zk3S*i7ZiRF?HD}2g|3Rcb>l~{T4gxJuIsaA1|iFow=*1b`bd!L6A%cgqS-$jP>W7O67O$h; zXSf-4tnG+uQx+1;PK=xNdl7uI1}(nV1|F(8^Lu8AUM-$nQn8lz#+B*VVNRLehQ({V zTaP*JUx4+j^w#G`t%JM5xBlQLuqfcuh)tqYI2IL_D6}j9O?&Ij%IOU)xO=eqj+l;W z(R(y4%kO?aFw+ks|Jp1m8Hz501ENp`qh3$mT1=t8Gh=D>p69FbTyDl6KdL19H`F;# z$S$6^EqO^0dQAZr5G1heI2Qd}(UrMQR13@Vd$^p&_6PM}v>Hvst@@i{!a*~DD-XuE zr+k=kHroV}e#C6WBR+k)vyEF8bHy%uo2sglg`nv3dp8@%Zy3$fdFVAG=Edn8kEL`N zq6lzgA}{)y_0|p+&zGZFXC>**m=i|du-8|%L%rxTSX6#D4{M*TfFOeo#Zh3w{tXIg zv_q?Ohjpxe#+?lTWc6&SfTyw6!(m?bD7J_wPY}wCmJRdrVY;r~urgX;!l>e8n*H?+ zh5uy_bpe!Yf>O0Yj%DpV8fZk}8Lk2E=WzQxU3eu_d(1aT|MR`j)!r2w6$|NrC=D}5 zdw^1gB9hVq*4iy|7gW#Atq55qv373k?^2zhagScfpqdzq^29O$l>~aKwRe@!-o>-M z_tV2Z(35FY;SHI#z1|uvm#Sg(1Yct)h5(~O(5;wvY9?kJvqw9Xe|;Q|vUzv{yxMsH z38A`5F0R^WOS`-J^MoL`0dNrkFGacBd4GGJI`Za+p`W3n(oa;NU~89>1CJLi5L6pb zkO6Z)$Xt=sO6ucx>}2MKWpnTQd^5(Feb7YN`PG1|Zl16WOtG%dKzx$u2(B7n5y#xqDWn5d@ix4dX3cyWU)^)VC6SL@C)6joZ5TbM*UHPCA~nuD3=( z_24D#Iz9FJTx8xi7-`TXJ`0fPrlTlMuH{6J2F9 zI$2^A8L=B7EkIa}cg(9|gT2eY0^qo920$)=k)5|Y^M0a&&*32l0K^7!#m|hS^_?kA7vwLOuYLJG@(X)|F)GwZ5;Yp1) z$Mxt62r6sG5VWEIe}<41J5$5~YCTDG5%?$ggwINTT&OeX9z&X!{0r#s|NgRtU&zHN z!`CuiL2i2{)5^YrhOGj)V}gpWL9@Hc1_7iS#wZDTb`Hmgk}S~xnhMDmcMjR6O88ar`fgD~S_Gwzf>k`%FW<

wANNetu*HR`b_==(9f%s!T3{&X`s;{1mBcFkaAR= z4F^B;nS9Ivs@E{wG5}tdb1l6vGRdY(h8LE(x3p8&0yuTS~7p__vYu$;HpCW?%lf= z1!J+`q2{w0qhRlqWq=SFsMrQcya88vl5;FyfKfWI;;z{Tlrce)h3^1-9iqQ{%K+_@ z22rQz?+GtNb({0*M}H}<5;Z0_2;`VsNb|d4SBf!3QN+da zId9Gfrh{4YRm%BQ1R_TV%zOa$;#j=rWLKHr3D)ij1%c>{-IotX>D%2VtUt3sEp~S1 zI(4d&Z!~X)Fhgri<2FeI7jR1e40W9mB!zbaJQ9HKu0MT@PXWai@J&s9;1xpUZ+-gD zu;dI52}GYQ_U7ztIgS9>eDFg}FPza3?;;xP2eivI$IaWvmaWi0Rvu?j?4X^-SaARW z)(3TpV>WLF3{U`2>Ykk}dG>!JdFtdFwEzPCf@;ZF9Sryb*vQX`5|klv0Wd_bc-j4C zan{|=+E82qVB2Q0g>BBvijyp+yEa1}Cs0}F1PLWDtObZef;4-(gTTT*z}Or36c2DO z>!RSEj^}dW2^1JlMHqpnhr|M0#l;fV6^YR>!#J?E!{2xRqbSWj0NfWk@LTmwedW9e zN7n8L9RV;keRFa3CSs_5eVvi)z6ek73;2OoHkcv+c7qX7OEfPH*OQp7{A4gd?GI=o zeNvjWf`bf%vSKjY-HjYDTh^Z?Hl>FKnM$j`;qHJ&&dL*jIb6ClZMYZU(h*ddb&)NZ z#0P$EA1!hPlBFv`t=js<=jPJZZ$<#S;$J!_+N)Ud{nZiqOK zz0SI$*YMMiG#z`%KXp`A{LE|-wLpRq2DY&P49A)$L zAsDfq4O_Xxu}2B7t7OSFKCtJN%qxsJORn%&NcT5{HgZK2g1XQpVnzVlkPFx{iyvn3 z!5rtBWnNs|Ul_AQ@LdnM$JK%ovskxr5gcJ?%o*Gi1GkE_0g+2hY3ksH0`O}Cy=oOJ z^iw>z3v$L8fJMOp*AArtH#zdTxSpTDsC1IZpSVt-=C5o=B1Tm9;ah?b?98oBL!~O9I#t>`-0O_^1MIObX!X z_2)PR0G$Msey(MgMXk15B|W_fk7@S;>QF!4Tv`J`ezXga{=2s(hy!1rgH12SG+4$s z5B&b00=Ie=NE151d_uB$k0$RX%bmoSZAXCh&LvsDLk`ry3AOSVm1E@H!EFf#}s>evk|fg*d&Qbw!dTF1Kkh3tQC5 z?p@nJpW9r^=mkKuf%_yQ=CiWVXZJ&|>2_Mk{EhEOY3i!v^YDw5v~ys0P$Q0j9oxWE z)Mx@jf`B(dnof4^Q)6|DBnJ^-M5}MT8KsN(A>O(U2Jbx{JQ98OH|VviH0OSrwAeE^ zg}8;1BsNv3{t9*3un^#)0jL_((+dFhGD)3$;S5+V*&G%=_+EtaSxUc{8*_di^crgi zKpY_8SUvOeVQsHOL2i_6OCwu2_SxR#phTrkgBzyaplJH-tqph)!t5c5vnf86CrO?n xO5k>H#_#29p5-AJ4g%T69;WK%0J>bhfMM^$(2s(7_u$_gGqW~*^{3nQ{|Cdq*X{rS diff --git a/open_earable/lib/apps/neck_meditation/model/stretch_state.dart b/open_earable/lib/apps/neck_meditation/model/stretch_state.dart index 0e07685..a333996 100644 --- a/open_earable/lib/apps/neck_meditation/model/stretch_state.dart +++ b/open_earable/lib/apps/neck_meditation/model/stretch_state.dart @@ -19,19 +19,50 @@ extension NeckStretchStateExtension on NeckStretchState { case NeckStretchState.mainNeckStretch: return 'Main Neck Area'; case NeckStretchState.leftNeckStretch: - return 'Right Neck Area'; - case NeckStretchState.rightNeckStretch: return 'Left Neck Area'; + case NeckStretchState.rightNeckStretch: + return 'Right Neck Area'; case NeckStretchState.noStretch: return 'Not Stretching'; - case NeckStretchState.doneStretching: - return 'You are done stretching. Good job!'; default: return 'Invalid State'; } } + + /// Gets the corresponding asset path for the front neck image + String get assetPathNeckFront { + switch (this) { + case NeckStretchState.rightNeckStretch: + return 'assets/neck_stretch/Neck_Right_Stretch.png'; + case NeckStretchState.leftNeckStretch: + return 'assets/neck_stretch/Neck_Left_Stretch.png'; + default: + return 'assets/posture_tracker/Neck_Front.png'; + } + } + + /// Gets the corresponding asset path for the side eck image + String get assetPathNeckSide { + switch (this) { + case NeckStretchState.mainNeckStretch: + return 'assets/neck_stretch/Neck_Main_Stretch.png'; + default: + return 'assets/posture_tracker/Neck_Side.png'; + } + } + + /// Gets the corresponding asset path for the Head Front Image + String get assetPathHeadFront { + return 'assets/posture_tracker/Head_Front.png'; + } + + /// Gets the corresponding asset path for the Head Side Image + String get assetPathHeadSide { + return 'assets/posture_tracker/Head_Side.png'; + } } +/// Stores all settings needed to manage a stretching session class StretchSettings { NeckStretchState state; @@ -44,6 +75,7 @@ class StretchSettings { /// Duration for the right neck relaxation Duration rightNeckRelaxation; + /// The stretch settings containing duration timers and state StretchSettings( {this.state = NeckStretchState.noStretch, required this.mainNeckRelaxation, @@ -51,6 +83,7 @@ class StretchSettings { required this.rightNeckRelaxation}); } +/// Stores all data and functions to manage the guided neck meditation class NeckMeditation { StretchSettings _settings = StretchSettings( mainNeckRelaxation: Duration(seconds: 30), @@ -63,67 +96,61 @@ class NeckMeditation { /// Holds the Timer that increments the current Duration var _restDurationTimer; /// Stores the rest duration of the current timer - var _restDuration; + late Duration _restDuration; /// Stores the current active timer for state transition var _currentTimer; StretchSettings get settings => _settings; + Duration get restDuration => _restDuration; + set settings(StretchSettings settings) => _settings = settings; - NeckMeditation(this._openEarable, this._viewModel); - - /// Setter method for stretchSettings - void setSettings(StretchSettings settings) { - _settings = settings; - } - - // Gets the rest duration of the current meditation timer - Duration getRestDuration() { - return _restDuration; + NeckMeditation(this._openEarable, this._viewModel) { + this._restDuration = Duration(seconds: 0); } /// Starts the Meditation with the according timers - startMeditation() { - _settings.state = NeckStretchState.mainNeckStretch; - _restDuration = _settings.mainNeckRelaxation; - _currentTimer = Timer(_settings.mainNeckRelaxation, _setNextState); + void startMeditation() { + _viewModel.startTracking(); + _settings.state = NeckStretchState.noStretch; + _setNextState(); _restDurationTimer = Timer.periodic(Duration(seconds: 1), (timer) { _restDuration -= Duration(seconds: 1); }); } /// Stops the current Meditation - stopMeditation() { + void stopMeditation() { _settings.state = NeckStretchState.noStretch; _currentTimer.cancel(); _restDurationTimer.cancel(); _restDuration = Duration(seconds: 0); + _viewModel.stopTracking(); + } + + /// Sets the state and timers for the state. + void _setState(NeckStretchState state, Duration stateDuration) { + _settings.state = state; + _currentTimer = Timer(stateDuration, _setNextState); + _restDuration = stateDuration; } /// Used to set the next meditation state and set the correct Timer void _setNextState() { switch (_settings.state) { case NeckStretchState.noStretch: - case NeckStretchState.doneStretching: - _settings.state = NeckStretchState.mainNeckStretch; + _setState(NeckStretchState.mainNeckStretch, _settings.mainNeckRelaxation); return; case NeckStretchState.mainNeckStretch: - _settings.state = NeckStretchState.leftNeckStretch; - _currentTimer = Timer(_settings.leftNeckRelaxation, _setNextState); - _restDuration = _settings.leftNeckRelaxation; + _setState(NeckStretchState.rightNeckStretch, _settings.rightNeckRelaxation); _openEarable.audioPlayer.jingle(2); return; - case NeckStretchState.leftNeckStretch: - _settings.state = NeckStretchState.rightNeckStretch; - _currentTimer = Timer(_settings.rightNeckRelaxation, _setNextState); - _restDuration = _settings.rightNeckRelaxation; + case NeckStretchState.rightNeckStretch: + _setState(NeckStretchState.leftNeckStretch, _settings.leftNeckRelaxation); _openEarable.audioPlayer.jingle(2); return; - case NeckStretchState.rightNeckStretch: - _settings.state = NeckStretchState.doneStretching; - _viewModel.stopTracking(); - _restDurationTimer.cancel(); - _restDuration = Duration(seconds: 0); + case NeckStretchState.leftNeckStretch: + stopMeditation(); _openEarable.audioPlayer.jingle(2); return; default: diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_arc_painter.dart b/open_earable/lib/apps/neck_meditation/view/stretch_arc_painter.dart index a8a5c9b..810bedc 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_arc_painter.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_arc_painter.dart @@ -11,7 +11,10 @@ class StretchArcPainter extends CustomPainter { final NeckStretchState stretchState; final bool isFront; - StretchArcPainter({required this.angle, this.angleThreshold = 0, this.stretchState = NeckStretchState.noStretch, required this.isFront}); + StretchArcPainter({required this.angle, + this.angleThreshold = 0, + this.stretchState = NeckStretchState.noStretch, + required this.isFront}); @override void paint(Canvas canvas, Size size) { @@ -21,7 +24,9 @@ class StretchArcPainter extends CustomPainter { ..strokeWidth = 5.0; Path circlePath = Path(); - circlePath.addOval(Rect.fromCircle(center: Offset(size.width / 2, size.height / 2), radius: min(size.width, size.height) / 2)); + circlePath.addOval(Rect.fromCircle( + center: Offset(size.width / 2, size.height / 2), + radius: min(size.width, size.height) / 2)); canvas.drawPath(circlePath, circlePaint); // Create a paint object with purple color and stroke style @@ -44,55 +49,67 @@ class StretchArcPainter extends CustomPainter { // Add an arc to the path anglePath.addArc( - Rect.fromCircle(center: center, radius: radius), // create a rectangle from the center and radius + Rect.fromCircle(center: center, radius: radius), + // create a rectangle from the center and radius startAngle, // start angle endAngle, // sweep angle ); Path angleOvershootPath = Path(); - angleOvershootPath.addArc( - Rect.fromCircle(center: center, radius: radius), // create a rectangle from the center and radius - startAngle + angle.sign * angleThreshold, // start angle - angle.sign * (angle.abs() - angleThreshold), // sweep angle + + if (_isNegativeOvershoot()) { + angleOvershootPath.addArc( + Rect.fromCircle(center: center, radius: radius), + // create a rectangle from the center and radius + startAngle + angle.sign * angleThreshold, // start angle + angle.sign * (angle.abs() - angleThreshold), // sweep angle + ); + } else { + angleOvershootPath.addArc( + Rect.fromCircle(center: center, radius: radius), + // create a rectangle from the center and radius + startAngle + angle.sign * angleThreshold, // start angle + _isOvershooting() ? angle.sign * (angle.abs() - angleThreshold) : 0, // sweep angle ); + } Paint angleOvershootPaint = Paint() - ..color = Color.fromARGB(255, 0, 186, 255) - ..style = PaintingStyle.stroke - ..strokeCap = StrokeCap.round - ..strokeWidth = 5.0; + ..color = getOvershootColor() + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeWidth = 5.0; Path thresholdPath = Path(); thresholdPath.addArc( - Rect.fromCircle(center: center, radius: radius), // create a rectangle from the center and radius - getStartAngle(startAngle, angleThreshold), // start angle - getThreshold(angleThreshold), // sweep angle + Rect.fromCircle(center: center, radius: radius), + // create a rectangle from the center and radius + getStartAngle(startAngle, angleThreshold), // start angle + getThreshold(angleThreshold), // sweep angle ); Paint thresholdPaint = Paint() - ..color = Colors.redAccent[100]! - ..style = PaintingStyle.stroke - ..strokeCap = StrokeCap.round - ..strokeWidth = 5.0; + ..color = getThresholdColor() + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeWidth = 5.0; // Draw the path on the canvas canvas.drawPath(thresholdPath, thresholdPaint); canvas.drawPath(anglePath, anglePaint); if (angle.abs() > angleThreshold.abs()) { - canvas.drawPath(angleOvershootPath, angleOvershootPaint); + canvas.drawPath(angleOvershootPath, angleOvershootPaint); } } /// Gets the right start angle depending on stretch state double getStartAngle(double startAngle, double threshold) { - if (!this.isFront) - return startAngle - threshold; + if (!this.isFront) return startAngle - threshold; - switch(this.stretchState) { - case NeckStretchState.leftNeckStretch: - return startAngle - threshold; + switch (this.stretchState) { case NeckStretchState.rightNeckStretch: - return startAngle - threshold - pi/2 - 2/18 * pi; + return startAngle - threshold; + case NeckStretchState.leftNeckStretch: + return startAngle - threshold - pi / 2 - 2 / 18 * pi; default: return startAngle - threshold; } @@ -101,27 +118,80 @@ class StretchArcPainter extends CustomPainter { /// Gets the right threshold depending on stretch state double getThreshold(double threshold) { if (this.isFront) { - switch(this.stretchState) { + switch (this.stretchState) { case NeckStretchState.rightNeckStretch: case NeckStretchState.leftNeckStretch: - return 2 * threshold + pi/2 + 2/18 * pi; + return 2 * threshold + pi / 2 + 2 / 18 * pi; default: return 2 * threshold; } } - switch(this.stretchState) { + switch (this.stretchState) { case NeckStretchState.mainNeckStretch: - return 2 * threshold + pi/2 + 1/36 * pi; - case NeckStretchState.rightNeckStretch: - return 2 * threshold + pi/2; + return 2 * threshold + pi / 2 + 1 / 36 * pi; default: return 2 * threshold; } } + /// Determines whether it actually is overshooting the threshold. This is a + /// small hack as the head angle cant go over a certain degree this way we can + /// ensure that the wrong stretch direction drawn part can never be overshot + bool _isOvershooting() { + if (this.isFront) { + switch (this.stretchState) { + case NeckStretchState.rightNeckStretch: + return angle.sign <= 0; + case NeckStretchState.leftNeckStretch: + return angle.sign >= 0; + default: + return true; + } + } + + switch (this.stretchState) { + case NeckStretchState.mainNeckStretch: + return angle.sign <= 0; + default: + return true; + } + } + + /// Detgermines whether the overshoot is negative (shouldnt stretch that part) + /// or is positive (should stretch that part) + bool _isNegativeOvershoot() { + return (this.isFront && + this.stretchState == NeckStretchState.mainNeckStretch) || + (!this.isFront && + (this.stretchState == NeckStretchState.leftNeckStretch || + this.stretchState == NeckStretchState.rightNeckStretch)); + } + + /// Returns the right color for the overshoot depending on stretch state and + /// if its upper or lower head state arc. + Color getOvershootColor() { + if (_isNegativeOvershoot()) { + // Equals Colors.redAccent[100]! + return Color.fromARGB(255, 255, 138, 128); + } + + return Color.fromARGB(255, 0, 186, 255); + } + + /// Returns the right color for the threshold depending on stretch state and + /// if its the upper or lower head state arc. + Color getThresholdColor() { + if (_isNegativeOvershoot()) { + return Color.fromARGB(255, 0, 186, 255); + } + + // Equals Colors.redAccent[100]! + return Color.fromARGB(255, 255, 138, 128); + } + @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { + bool shouldRepaint(CustomPainter oldDelegate) { // check if oldDelegate is an ArcPainter and if the angle is the same return oldDelegate is ArcPainter && oldDelegate.angle != this.angle; } diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart index 0152c6f..0054466 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart @@ -94,12 +94,12 @@ class _SettingsViewState extends State { ), ), ListTile( - title: Text("Left Neck Relaxation Duration\n(in seconds)"), + title: Text("Right Neck Relaxation Duration\n(in seconds)"), trailing: SizedBox( height: 37.0, width: 52, child: TextField( - controller: _leftNeckDuration, + controller: _rightNeckDuration, textAlign: TextAlign.end, style: TextStyle(color: Colors.black), decoration: InputDecoration( @@ -118,12 +118,12 @@ class _SettingsViewState extends State { ), ), ListTile( - title: Text("Right Neck Relaxation Duration\n(in seconds)"), + title: Text("Left Neck Relaxation Duration\n(in seconds)"), trailing: SizedBox( height: 37.0, width: 52, child: TextField( - controller: _rightNeckDuration, + controller: _leftNeckDuration, textAlign: TextAlign.end, style: TextStyle(color: Colors.black), decoration: InputDecoration( @@ -175,8 +175,7 @@ class _SettingsViewState extends State { /// Returns the new duration acquired from the Text. /// Checks if the string is valid (doesn't contain '-' or '.'. Duration getNewDuration(Duration duration, String newDuration) { - if (newDuration.contains('.') || newDuration.contains('-')) - return duration; + if (newDuration.contains('.') || newDuration.contains('-')) return duration; return Duration(seconds: int.parse(newDuration)); } @@ -190,7 +189,7 @@ class _SettingsViewState extends State { getNewDuration(settings.rightNeckRelaxation, _rightNeckDuration.text); settings.leftNeckRelaxation = getNewDuration(settings.leftNeckRelaxation, _leftNeckDuration.text); - _viewModel.setMeditationSettings(settings); + _viewModel.meditationSettings = settings; } @override diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart index 01c1b94..26a30e5 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart @@ -29,13 +29,11 @@ class _StretchTrackerViewState extends State { /// Used to start the meditation via the button void _startMeditation() { - this._viewModel.startTracking(); this._viewModel.meditation.startMeditation(); } /// Used to stop the meditation via the button void _stopMeditation() { - this._viewModel.stopTracking(); this._viewModel.meditation.stopMeditation(); } @@ -52,18 +50,6 @@ class _StretchTrackerViewState extends State { if (_viewModel.meditationState == NeckStretchState.noStretch) return TextSpan(text: "Click the Button below\n to start Meditating!"); - if (_viewModel.meditationState == NeckStretchState.doneStretching) - return TextSpan(children: [ - TextSpan(text: "You are done stretching.\n"), - TextSpan( - text: "Well done!", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - color: Color.fromARGB(255, 0, 186, 255), - )), - ]); - return TextSpan(children: [ TextSpan( text: "Currently Stretching: \n", @@ -86,7 +72,7 @@ class _StretchTrackerViewState extends State { _viewModel.meditationState == NeckStretchState.noStretch) return Text('Stop Meditation'); - return Text(_viewModel.getRestDuration().toString().substring(2, 7)); + return Text(_viewModel.restDuration.toString().substring(2, 7)); } @override @@ -117,6 +103,7 @@ class _StretchTrackerViewState extends State { ))); } + /// Build the actual content you can see in the app Widget _buildContentView(StretchViewModel neckStretchViewModel) { var headViews = this._createHeadViews(neckStretchViewModel); return Column( @@ -150,125 +137,24 @@ class _StretchTrackerViewState extends State { ); } - /// Builds the actual head views using the PostureRollView - Widget _buildHeadView( - String headAssetPath, - String neckAssetPath, - AlignmentGeometry headAlignment, - double roll, - double angleThreshold, - NeckStretchState state) { - return Padding( - padding: const EdgeInsets.all(5), - child: StretchRollView( - roll: roll, - angleThreshold: angleThreshold * 3.14 / 180, - headAssetPath: headAssetPath, - neckAssetPath: neckAssetPath, - headAlignment: headAlignment, - stretchState: state, - ), - ); - } - /// Creates the Head Views that display depending on the MeditationState. - List _createHeadViews(neckStretchViewModel) { + List _createHeadViews(StretchViewModel neckStretchViewModel) { return [ // Visible Head-Displays when not stretching - Visibility( - visible: this._viewModel.meditationState == NeckStretchState.noStretch || - this._viewModel.meditationState == NeckStretchState.doneStretching, - child: this._buildHeadView( - "assets/posture_tracker/Head_Front.png", - "assets/posture_tracker/Neck_Front.png", - Alignment.center.add(Alignment(0, 0.3)), - neckStretchViewModel.attitude.roll, - 0, - NeckStretchState.noStretch), - ), - Visibility( - visible: this._viewModel.meditationState == NeckStretchState.noStretch || - this._viewModel.meditationState == NeckStretchState.doneStretching, - child: this._buildHeadView( - "assets/posture_tracker/Head_Side.png", - "assets/posture_tracker/Neck_Side.png", - Alignment.center.add(Alignment(0, 0.3)), - -neckStretchViewModel.attitude.pitch, - 0, - NeckStretchState.noStretch), - ), + _buildStateViews( + NeckStretchState.noStretch, neckStretchViewModel, 0.0, 0.0), /// Visible Widgets for the main stretch - Visibility( - visible: - this._viewModel.meditationState == NeckStretchState.mainNeckStretch, - child: this._buildHeadView( - "assets/posture_tracker/Head_Front.png", - "assets/posture_tracker/Neck_Front.png", - Alignment.center.add(Alignment(0, 0.3)), - neckStretchViewModel.attitude.roll, - 7.0, - NeckStretchState.mainNeckStretch), - ), - Visibility( - visible: - this._viewModel.meditationState == NeckStretchState.mainNeckStretch, - child: this._buildHeadView( - "assets/posture_tracker/Head_Side.png", - "assets/neck_stretch/Neck_Main_Stretch.png", - Alignment.center.add(Alignment(0, 0.3)), - -neckStretchViewModel.attitude.pitch, - 50.0, - NeckStretchState.mainNeckStretch), - ), + _buildStateViews( + NeckStretchState.mainNeckStretch, neckStretchViewModel, 7.0, 50.0), /// Visible Widgets for the right stretch - Visibility( - visible: - this._viewModel.meditationState == NeckStretchState.rightNeckStretch, - child: this._buildHeadView( - "assets/posture_tracker/Head_Front.png", - "assets/neck_stretch/Neck_Right_Stretch.png", - Alignment.center.add(Alignment(0, 0.3)), - neckStretchViewModel.attitude.roll, - 30.0, - NeckStretchState.rightNeckStretch), - ), - Visibility( - visible: - this._viewModel.meditationState == NeckStretchState.rightNeckStretch, - child: this._buildHeadView( - "assets/posture_tracker/Head_Side.png", - "assets/posture_tracker/Neck_Side.png", - Alignment.center.add(Alignment(0, 0.3)), - -neckStretchViewModel.attitude.pitch, - 15.0, - NeckStretchState.rightNeckStretch), - ), + _buildStateViews( + NeckStretchState.rightNeckStretch, neckStretchViewModel, 30.0, 15.0), /// Visible Widgets for the left stretch - Visibility( - visible: - this._viewModel.meditationState == NeckStretchState.leftNeckStretch, - child: this._buildHeadView( - "assets/posture_tracker/Head_Front.png", - "assets/neck_stretch/Neck_Left_Stretch.png", - Alignment.center.add(Alignment(0, 0.3)), - neckStretchViewModel.attitude.roll, - 30.0, - NeckStretchState.leftNeckStretch), - ), - Visibility( - visible: - this._viewModel.meditationState == NeckStretchState.leftNeckStretch, - child: this._buildHeadView( - "assets/posture_tracker/Head_Side.png", - "assets/posture_tracker/Neck_Side.png", - Alignment.center.add(Alignment(0, 0.3)), - -neckStretchViewModel.attitude.pitch, - 15.0, - NeckStretchState.leftNeckStretch), - ), + _buildStateViews( + NeckStretchState.leftNeckStretch, neckStretchViewModel, 30.0, 15.0), ]; } @@ -296,4 +182,53 @@ class _StretchTrackerViewState extends State { ]), ); } + + /// Builds the actual head views using the StretchRollView + Widget _buildHeadView( + String headAssetPath, + String neckAssetPath, + AlignmentGeometry headAlignment, + double roll, + double angleThreshold, + NeckStretchState state) { + return Padding( + padding: const EdgeInsets.all(5), + child: StretchRollView( + roll: roll, + angleThreshold: angleThreshold * 3.14 / 180, + headAssetPath: headAssetPath, + neckAssetPath: neckAssetPath, + headAlignment: headAlignment, + stretchState: state, + ), + ); + } + + /// Builds the state for a certain state and set thresholds + Visibility _buildStateViews( + NeckStretchState state, + StretchViewModel neckStretchViewModel, + double frontThreshold, + double sideThreshold) { + return Visibility( + visible: this._viewModel.meditationState == state, + child: Column( + children: [ + this._buildHeadView( + state.assetPathHeadFront, + state.assetPathNeckFront, + Alignment.center.add(Alignment(0, 0.3)), + neckStretchViewModel.attitude.roll, + frontThreshold, + state), + this._buildHeadView( + state.assetPathHeadSide, + state.assetPathNeckSide, + Alignment.center.add(Alignment(0, 0.3)), + neckStretchViewModel.attitude.pitch, + sideThreshold, + state), + ], + )); + } } diff --git a/open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart b/open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart index 9bd70a2..4c8aca0 100644 --- a/open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart +++ b/open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart @@ -18,7 +18,11 @@ class StretchViewModel extends ChangeNotifier { StretchSettings get meditationSettings => _meditation.settings; - NeckStretchState get meditationState => this._meditation.settings.state; + NeckStretchState get meditationState => _meditation.settings.state; + + Duration get restDuration => _meditation.restDuration; + + set meditationSettings(StretchSettings settings) => _meditation.settings = settings; AttitudeTracker _attitudeTracker; OpenEarable _openEarable; @@ -40,10 +44,6 @@ class StretchViewModel extends ChangeNotifier { }); } - Duration getRestDuration() { - return _meditation.getRestDuration(); - } - void startTracking() { _attitudeTracker.start(); notifyListeners(); @@ -59,11 +59,6 @@ class StretchViewModel extends ChangeNotifier { _attitudeTracker.calibrateToCurrentAttitude(); } - /// Used to set the Duration Settings for Meditation - void setMeditationSettings(StretchSettings settings) { - _meditation.setSettings(settings); - } - @override void dispose() { _attitudeTracker.cancle(); From 9a51c869bf3df65a3d2a50e4d5ef3fecbf9d78ed Mon Sep 17 00:00:00 2001 From: Polaris Date: Wed, 13 Dec 2023 17:19:08 +0100 Subject: [PATCH 028/104] Readd basic functionality for doneStretching state --- .../neck_meditation/model/stretch_state.dart | 8 ++- .../view/stretch_tracker_view.dart | 58 +++++++++++-------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/open_earable/lib/apps/neck_meditation/model/stretch_state.dart b/open_earable/lib/apps/neck_meditation/model/stretch_state.dart index a333996..9529d84 100644 --- a/open_earable/lib/apps/neck_meditation/model/stretch_state.dart +++ b/open_earable/lib/apps/neck_meditation/model/stretch_state.dart @@ -25,7 +25,7 @@ extension NeckStretchStateExtension on NeckStretchState { case NeckStretchState.noStretch: return 'Not Stretching'; default: - return 'Invalid State'; + return 'Done Stretching'; } } @@ -150,7 +150,11 @@ class NeckMeditation { _openEarable.audioPlayer.jingle(2); return; case NeckStretchState.leftNeckStretch: - stopMeditation(); + _settings.state = NeckStretchState.doneStretching; + _currentTimer.cancel(); + _restDurationTimer.cancel(); + _restDuration = Duration(seconds: 0); + _viewModel.stopTracking(); _openEarable.audioPlayer.jingle(2); return; default: diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart index 26a30e5..fc957e2 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart @@ -47,7 +47,8 @@ class _StretchTrackerViewState extends State { ), ); - if (_viewModel.meditationState == NeckStretchState.noStretch) + if (_viewModel.meditationState == NeckStretchState.noStretch || + _viewModel.meditationState == NeckStretchState.doneStretching) return TextSpan(text: "Click the Button below\n to start Meditating!"); return TextSpan(children: [ @@ -137,27 +138,6 @@ class _StretchTrackerViewState extends State { ); } - /// Creates the Head Views that display depending on the MeditationState. - List _createHeadViews(StretchViewModel neckStretchViewModel) { - return [ - // Visible Head-Displays when not stretching - _buildStateViews( - NeckStretchState.noStretch, neckStretchViewModel, 0.0, 0.0), - - /// Visible Widgets for the main stretch - _buildStateViews( - NeckStretchState.mainNeckStretch, neckStretchViewModel, 7.0, 50.0), - - /// Visible Widgets for the right stretch - _buildStateViews( - NeckStretchState.rightNeckStretch, neckStretchViewModel, 30.0, 15.0), - - /// Visible Widgets for the left stretch - _buildStateViews( - NeckStretchState.leftNeckStretch, neckStretchViewModel, 30.0, 15.0), - ]; - } - // Creates the Button used to start the meditation Widget _buildMeditationButton(StretchViewModel neckStretchViewModel) { return Padding( @@ -183,6 +163,27 @@ class _StretchTrackerViewState extends State { ); } + /// Creates the Head Views that display depending on the MeditationState. + List _createHeadViews(StretchViewModel neckStretchViewModel) { + return [ + // Visible Head-Displays when not stretching + _buildStateViews( + NeckStretchState.noStretch, neckStretchViewModel, 0.0, 0.0), + + /// Visible Widgets for the main stretch + _buildStateViews( + NeckStretchState.mainNeckStretch, neckStretchViewModel, 7.0, 50.0), + + /// Visible Widgets for the right stretch + _buildStateViews( + NeckStretchState.rightNeckStretch, neckStretchViewModel, 30.0, 15.0), + + /// Visible Widgets for the left stretch + _buildStateViews( + NeckStretchState.leftNeckStretch, neckStretchViewModel, 30.0, 15.0), + ]; + } + /// Builds the actual head views using the StretchRollView Widget _buildHeadView( String headAssetPath, @@ -210,8 +211,17 @@ class _StretchTrackerViewState extends State { StretchViewModel neckStretchViewModel, double frontThreshold, double sideThreshold) { + + var visibility; + if (state == NeckStretchState.noStretch) { + visibility = this._viewModel.meditationState == NeckStretchState.noStretch || + this._viewModel.meditationState == NeckStretchState.doneStretching; + } else { + visibility = this._viewModel.meditationState == state; + } + return Visibility( - visible: this._viewModel.meditationState == state, + visible: visibility, child: Column( children: [ this._buildHeadView( @@ -231,4 +241,4 @@ class _StretchTrackerViewState extends State { ], )); } -} +} \ No newline at end of file From 3020c9d31f19b118ae51eb4ac708d7ec036269df Mon Sep 17 00:00:00 2001 From: Polaris Date: Thu, 14 Dec 2023 20:13:30 +0100 Subject: [PATCH 029/104] Add base for tutorial view of the meditation --- .../view/stretch_app_view.dart | 112 ++++++++++++ .../view/stretch_settings_view.dart | 5 +- .../view/stretch_tracker_view.dart | 116 ++++++------ .../view/stretch_tutorial_view.dart | 172 ++++++++++++++++++ open_earable/lib/apps_tab.dart | 4 +- 5 files changed, 348 insertions(+), 61 deletions(-) create mode 100644 open_earable/lib/apps/neck_meditation/view/stretch_app_view.dart create mode 100644 open_earable/lib/apps/neck_meditation/view/stretch_tutorial_view.dart diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_app_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_app_view.dart new file mode 100644 index 0000000..8d0c546 --- /dev/null +++ b/open_earable/lib/apps/neck_meditation/view/stretch_app_view.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:open_earable/apps_tab.dart'; +import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; +import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; +import 'package:open_earable/apps/neck_meditation/view/stretch_tracker_view.dart'; +import 'package:open_earable/apps/neck_meditation/view/stretch_tutorial_view.dart'; +import 'package:open_earable/apps/neck_meditation/view_model/stretch_view_model.dart'; +import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_meditation/view/stretch_settings_view.dart'; + +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; + +class StretchAppView extends StatefulWidget { + final AttitudeTracker _tracker; + final OpenEarable _openEarable; + + StretchAppView(this._tracker, this._openEarable); + + @override + State createState() => _StretchAppViewState(); +} + +/// This class is the initial view you get when opening the Stretch-App +/// It refers to the tutorial page and the actual stretching page +class _StretchAppViewState extends State { + late final StretchViewModel _viewModel; + + @override + void initState() { + super.initState(); + this._viewModel = StretchViewModel(widget._tracker, widget._openEarable); + } + + List meditationApps(BuildContext context) { + return [ + AppInfo( + iconData: Icons.play_circle, + title: "Start Stretching", + description: "Dive directly into the guided neck stretch!", + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + StretchTrackerView( + EarableAttitudeTracker(widget._openEarable), + widget._openEarable))); + }), + AppInfo( + iconData: Icons.help, + title: "How to use this Tool", + description: "Short guide to get started with the neck stretch.", + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + StretchTutorialView( + EarableAttitudeTracker(widget._openEarable), + widget._openEarable))); + }), + // ... similarly for other apps + ]; + } + + @override + Widget build(BuildContext context) { + List apps = meditationApps(context); + + return Scaffold( + appBar: AppBar( + title: const Text("Guided Neck Relaxation"), + actions: [ + IconButton( + onPressed: (this._viewModel.meditationState == + NeckStretchState.noStretch || + this._viewModel.meditationState == + NeckStretchState.doneStretching) + ? () => + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + SettingsView(this._viewModel))) + : null, + icon: Icon(Icons.settings)), + ], + ), + body: ListView.builder( + itemCount: apps.length, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: Card( + color: Theme.of(context).colorScheme.primary, + child: ListTile( + leading: Icon(apps[index].iconData, size: 40.0), + title: Text(apps[index].title), + subtitle: Text(apps[index].description), + trailing: Icon(Icons.arrow_forward_ios, + size: 16.0), + // Arrow icon on the right + onTap: + apps[index].onTap, // Callback when the card is tapped + ), + )); + } + ) + ); + } +} diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart index 0054466..97445f6 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart @@ -174,10 +174,13 @@ class _SettingsViewState extends State { /// Returns the new duration acquired from the Text. /// Checks if the string is valid (doesn't contain '-' or '.'. + /// Maximum allows time of 59 Minute 59 Seconds for UI consistency Duration getNewDuration(Duration duration, String newDuration) { if (newDuration.contains('.') || newDuration.contains('-')) return duration; - return Duration(seconds: int.parse(newDuration)); + int parsed = int.parse(newDuration); + + return parsed > 3599 ? Duration(seconds: 3599) : Duration(seconds: parsed); } /// Update the Meditation Settings according to values, if field is empty set that timer Duration to 0 diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart index fc957e2..86adf34 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart @@ -27,6 +27,34 @@ class _StretchTrackerViewState extends State { this._viewModel = StretchViewModel(widget._tracker, widget._openEarable); } + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: _viewModel, + builder: (context, child) => Consumer( + builder: (context, neckStretchViewModel, child) => Scaffold( + appBar: AppBar( + title: const Text("Guided Neck Relaxation"), + actions: [ + IconButton( + onPressed: (this._viewModel.meditationState == + NeckStretchState.noStretch || + this._viewModel.meditationState == + NeckStretchState.doneStretching) + ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + SettingsView(this._viewModel))) + : null, + icon: Icon(Icons.settings)), + ], + ), + body: Center( + child: this._buildContentView(neckStretchViewModel), + ), + ))); + } + /// Used to start the meditation via the button void _startMeditation() { this._viewModel.meditation.startMeditation(); @@ -76,34 +104,6 @@ class _StretchTrackerViewState extends State { return Text(_viewModel.restDuration.toString().substring(2, 7)); } - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: _viewModel, - builder: (context, child) => Consumer( - builder: (context, neckStretchViewModel, child) => Scaffold( - appBar: AppBar( - title: const Text("Guided Neck Relaxation"), - actions: [ - IconButton( - onPressed: (this._viewModel.meditationState == - NeckStretchState.noStretch || - this._viewModel.meditationState == - NeckStretchState.doneStretching) - ? () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - SettingsView(this._viewModel))) - : null, - icon: Icon(Icons.settings)), - ], - ), - body: Center( - child: this._buildContentView(neckStretchViewModel), - ), - ))); - } - /// Build the actual content you can see in the app Widget _buildContentView(StretchViewModel neckStretchViewModel) { var headViews = this._createHeadViews(neckStretchViewModel); @@ -167,54 +167,33 @@ class _StretchTrackerViewState extends State { List _createHeadViews(StretchViewModel neckStretchViewModel) { return [ // Visible Head-Displays when not stretching - _buildStateViews( + _buildStretchViews( NeckStretchState.noStretch, neckStretchViewModel, 0.0, 0.0), /// Visible Widgets for the main stretch - _buildStateViews( + _buildStretchViews( NeckStretchState.mainNeckStretch, neckStretchViewModel, 7.0, 50.0), /// Visible Widgets for the right stretch - _buildStateViews( + _buildStretchViews( NeckStretchState.rightNeckStretch, neckStretchViewModel, 30.0, 15.0), /// Visible Widgets for the left stretch - _buildStateViews( + _buildStretchViews( NeckStretchState.leftNeckStretch, neckStretchViewModel, 30.0, 15.0), ]; } - /// Builds the actual head views using the StretchRollView - Widget _buildHeadView( - String headAssetPath, - String neckAssetPath, - AlignmentGeometry headAlignment, - double roll, - double angleThreshold, - NeckStretchState state) { - return Padding( - padding: const EdgeInsets.all(5), - child: StretchRollView( - roll: roll, - angleThreshold: angleThreshold * 3.14 / 180, - headAssetPath: headAssetPath, - neckAssetPath: neckAssetPath, - headAlignment: headAlignment, - stretchState: state, - ), - ); - } - - /// Builds the state for a certain state and set thresholds - Visibility _buildStateViews( + /// Builds the head tracking/stretch view parts for a certain state and thresholds + Visibility _buildStretchViews( NeckStretchState state, StretchViewModel neckStretchViewModel, double frontThreshold, double sideThreshold) { - var visibility; if (state == NeckStretchState.noStretch) { - visibility = this._viewModel.meditationState == NeckStretchState.noStretch || + visibility = this._viewModel.meditationState == + NeckStretchState.noStretch || this._viewModel.meditationState == NeckStretchState.doneStretching; } else { visibility = this._viewModel.meditationState == state; @@ -241,4 +220,25 @@ class _StretchTrackerViewState extends State { ], )); } -} \ No newline at end of file + + /// Builds the actual head views using the StretchRollView + Widget _buildHeadView( + String headAssetPath, + String neckAssetPath, + AlignmentGeometry headAlignment, + double roll, + double angleThreshold, + NeckStretchState state) { + return Padding( + padding: const EdgeInsets.all(5), + child: StretchRollView( + roll: roll, + angleThreshold: angleThreshold * 3.14 / 180, + headAssetPath: headAssetPath, + neckAssetPath: neckAssetPath, + headAlignment: headAlignment, + stretchState: state, + ), + ); + } +} diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_tutorial_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_tutorial_view.dart new file mode 100644 index 0000000..2db6423 --- /dev/null +++ b/open_earable/lib/apps/neck_meditation/view/stretch_tutorial_view.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; +import 'package:open_earable/apps/neck_meditation/view/stretch_roll_view.dart'; +import 'package:open_earable/apps/neck_meditation/view_model/stretch_view_model.dart'; +import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_meditation/view/stretch_settings_view.dart'; + +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; + +class StretchTutorialView extends StatefulWidget { + final AttitudeTracker _tracker; + final OpenEarable _openEarable; + + StretchTutorialView(this._tracker, this._openEarable); + + @override + State createState() => _StretchTutorialViewState(); +} + +class _StretchTutorialViewState extends State { + late final StretchViewModel _viewModel; + + @override + void initState() { + super.initState(); + this._viewModel = StretchViewModel(widget._tracker, widget._openEarable); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: _viewModel, + builder: (context, child) => Consumer( + builder: (context, neckStretchViewModel, child) => Scaffold( + appBar: AppBar( + title: const Text("Guided Neck Relaxation"), + actions: [ + IconButton( + onPressed: (this._viewModel.meditationState == + NeckStretchState.noStretch || + this._viewModel.meditationState == + NeckStretchState.doneStretching) + ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + SettingsView(this._viewModel))) + : null, + icon: Icon(Icons.settings)), + ], + ), + body: Center( + child: this._buildContentView(neckStretchViewModel), + ), + ))); + } + + /// Build the actual content you can see in the app + Widget _buildContentView(StretchViewModel neckStretchViewModel) { + return Column( + children: [ + Padding( + padding: EdgeInsets.all(5), + child: Padding( + padding: EdgeInsets.fromLTRB(8, 8, 8, 8), + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: + 'Here the body part, which is currently being stretched, will be displayed.') + ], + ), + ), + ), + ), + + Padding( + padding: EdgeInsets.all(8), + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + text: + "Here both your Front and Side view of your head will be displayed. The blue part shows you what part of your neck should currently be stretched. When starting to stretch you should gently tilt your head towards the instructed direction (the gray area of the circle). Once you feel your neck stretch stop and hold the position till the sound occurs. Then you can start stretching the next part of your neck."), + ), + ), + + FractionallySizedBox( + widthFactor: 0.6, + child: this._buildHeadView( + NeckStretchState.mainNeckStretch.assetPathNeckSide, + NeckStretchState.mainNeckStretch.assetPathHeadSide, + Alignment.center.add(Alignment(0, 0.3)), + neckStretchViewModel.attitude.pitch, + 30, + NeckStretchState.noStretch), + ), + + /// Used to place the Meditation-Button always at the bottom + Expanded( + child: Container(), + ), + + /// Explainer text for the button + Padding( + padding: EdgeInsets.all(8), + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + children: [ + TextSpan( + text: + 'This button will be used to start and to preemptively stop the meditation. If you are currently meditating the button will show you the remaining time for the current stretch.'), + TextSpan(text: ''), + ], + ), + ), + ), + this._buildMeditationButton(neckStretchViewModel), + ], + ); + } + + // Creates the Button used to start the meditation + Widget _buildMeditationButton(StretchViewModel neckStretchViewModel) { + return Padding( + padding: EdgeInsets.all(5), + child: Column(children: [ + ElevatedButton( + onPressed: neckStretchViewModel.isAvailable + ? () { + neckStretchViewModel.isTracking + ? neckStretchViewModel.stopTracking() + : neckStretchViewModel.startTracking(); + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: !neckStretchViewModel.isTracking + ? Color(0xff77F2A1) + : Color(0xfff27777), + foregroundColor: Colors.black, + ), + child: neckStretchViewModel.isTracking + ? const Text("Stop Meditation") + : const Text("Start Meditation"), + ), + ]), + ); + } + + /// Builds the actual head views using the StretchRollView + Widget _buildHeadView( + String headAssetPath, + String neckAssetPath, + AlignmentGeometry headAlignment, + double roll, + double angleThreshold, + NeckStretchState state) { + return Padding( + padding: const EdgeInsets.all(5), + child: StretchRollView( + roll: roll, + angleThreshold: angleThreshold * 3.14 / 180, + headAssetPath: headAssetPath, + neckAssetPath: neckAssetPath, + headAlignment: headAlignment, + stretchState: state, + ), + ); + } +} diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 4b2e71d..6438d8d 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; import 'package:open_earable/apps/recorder.dart'; -import 'package:open_earable/apps/neck_meditation/view/stretch_tracker_view.dart'; +import 'package:open_earable/apps/neck_meditation/view/stretch_app_view.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class AppInfo { @@ -54,7 +54,7 @@ class AppsTab extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => StretchTrackerView( + builder: (context) => StretchAppView( EarableAttitudeTracker(_openEarable), _openEarable))); }), // ... similarly for other apps From 56ae9d8730582b6d414ebc27b42d2727850d2e76 Mon Sep 17 00:00:00 2001 From: Polaris Date: Fri, 15 Dec 2023 16:48:12 +0100 Subject: [PATCH 030/104] Add timer for rest between neck stretch exercises that is editable, also add different button color for that state --- .../neck_meditation/model/stretch_state.dart | 45 ++++++++-- .../view/stretch_settings_view.dart | 37 ++++++++- .../view/stretch_tracker_view.dart | 82 +++++++++++-------- .../view_model/stretch_view_model.dart | 2 + 4 files changed, 119 insertions(+), 47 deletions(-) diff --git a/open_earable/lib/apps/neck_meditation/model/stretch_state.dart b/open_earable/lib/apps/neck_meditation/model/stretch_state.dart index 9529d84..ca0f18c 100644 --- a/open_earable/lib/apps/neck_meditation/model/stretch_state.dart +++ b/open_earable/lib/apps/neck_meditation/model/stretch_state.dart @@ -75,12 +75,16 @@ class StretchSettings { /// Duration for the right neck relaxation Duration rightNeckRelaxation; + /// Time used for resting between each set + Duration restingTime; + /// The stretch settings containing duration timers and state StretchSettings( {this.state = NeckStretchState.noStretch, required this.mainNeckRelaxation, required this.leftNeckRelaxation, - required this.rightNeckRelaxation}); + required this.rightNeckRelaxation, + required this.restingTime}); } /// Stores all data and functions to manage the guided neck meditation @@ -88,13 +92,18 @@ class NeckMeditation { StretchSettings _settings = StretchSettings( mainNeckRelaxation: Duration(seconds: 30), leftNeckRelaxation: Duration(seconds: 30), - rightNeckRelaxation: Duration(seconds: 30)); + rightNeckRelaxation: Duration(seconds: 30), + restingTime: Duration(seconds: 5)); final OpenEarable _openEarable; final StretchViewModel _viewModel; + /// Defines whether you are currently resting between two stretch exercises + bool _resting = false; + /// Holds the Timer that increments the current Duration var _restDurationTimer; + /// Stores the rest duration of the current timer late Duration _restDuration; @@ -102,7 +111,11 @@ class NeckMeditation { var _currentTimer; StretchSettings get settings => _settings; + Duration get restDuration => _restDuration; + + bool get resting => _resting; + set settings(StretchSettings settings) => _settings = settings; NeckMeditation(this._openEarable, this._viewModel) { @@ -111,6 +124,7 @@ class NeckMeditation { /// Starts the Meditation with the according timers void startMeditation() { + _resting = false; _viewModel.startTracking(); _settings.state = NeckStretchState.noStretch; _setNextState(); @@ -121,6 +135,7 @@ class NeckMeditation { /// Stops the current Meditation void stopMeditation() { + _resting = false; _settings.state = NeckStretchState.noStretch; _currentTimer.cancel(); _restDurationTimer.cancel(); @@ -130,26 +145,42 @@ class NeckMeditation { /// Sets the state and timers for the state. void _setState(NeckStretchState state, Duration stateDuration) { - _settings.state = state; + _settings.state = state; + // If you just swapped to this state, first rest for restingTime, then set new state + if (_resting) { + _resting = false; + _currentTimer = Timer(_settings.restingTime, () { + _setState(state, stateDuration); + }); + } else { _currentTimer = Timer(stateDuration, _setNextState); - _restDuration = stateDuration; + } + _restDuration = stateDuration; } /// Used to set the next meditation state and set the correct Timer void _setNextState() { switch (_settings.state) { case NeckStretchState.noStretch: - _setState(NeckStretchState.mainNeckStretch, _settings.mainNeckRelaxation); + case NeckStretchState.doneStretching: + _resting = true; + _setState( + NeckStretchState.mainNeckStretch, _settings.mainNeckRelaxation); return; case NeckStretchState.mainNeckStretch: - _setState(NeckStretchState.rightNeckStretch, _settings.rightNeckRelaxation); + _resting = true; + _setState( + NeckStretchState.rightNeckStretch, _settings.rightNeckRelaxation); _openEarable.audioPlayer.jingle(2); return; case NeckStretchState.rightNeckStretch: - _setState(NeckStretchState.leftNeckStretch, _settings.leftNeckRelaxation); + _resting = true; + _setState( + NeckStretchState.leftNeckStretch, _settings.leftNeckRelaxation); _openEarable.audioPlayer.jingle(2); return; case NeckStretchState.leftNeckStretch: + _resting = false; _settings.state = NeckStretchState.doneStretching; _currentTimer.cancel(); _restDurationTimer.cancel(); diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart index 97445f6..8841889 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart @@ -16,6 +16,7 @@ class _SettingsViewState extends State { late final TextEditingController _mainNeckDuration; late final TextEditingController _leftNeckDuration; late final TextEditingController _rightNeckDuration; + late final TextEditingController _restingDuration; late final StretchViewModel _viewModel; @@ -32,6 +33,9 @@ class _SettingsViewState extends State { _rightNeckDuration = TextEditingController( text: _viewModel.meditationSettings.rightNeckRelaxation.inSeconds .toString()); + _restingDuration = TextEditingController( + text: _viewModel.meditationSettings.rightNeckRelaxation.inSeconds + .toString()); } @override @@ -141,6 +145,30 @@ class _SettingsViewState extends State { ), ), ), + ListTile( + title: Text("Resting Duration between exercises\n(in seconds)"), + trailing: SizedBox( + height: 37.0, + width: 52, + child: TextField( + controller: _restingDuration, + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(10), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: OutlineInputBorder(), + labelText: 'Seconds', + filled: true, + labelStyle: TextStyle(color: Colors.black), + fillColor: Colors.white), + keyboardType: TextInputType.number, + onChanged: (_) { + _updateMeditationSettings(); + }, + ), + ), + ), ], ), ), @@ -175,7 +203,7 @@ class _SettingsViewState extends State { /// Returns the new duration acquired from the Text. /// Checks if the string is valid (doesn't contain '-' or '.'. /// Maximum allows time of 59 Minute 59 Seconds for UI consistency - Duration getNewDuration(Duration duration, String newDuration) { + Duration _getNewDuration(Duration duration, String newDuration) { if (newDuration.contains('.') || newDuration.contains('-')) return duration; int parsed = int.parse(newDuration); @@ -187,12 +215,13 @@ class _SettingsViewState extends State { void _updateMeditationSettings() { StretchSettings settings = _viewModel.meditationSettings; settings.mainNeckRelaxation = - getNewDuration(settings.mainNeckRelaxation, _mainNeckDuration.text); + _getNewDuration(settings.mainNeckRelaxation, _mainNeckDuration.text); settings.rightNeckRelaxation = - getNewDuration(settings.rightNeckRelaxation, _rightNeckDuration.text); + _getNewDuration(settings.rightNeckRelaxation, _rightNeckDuration.text); settings.leftNeckRelaxation = - getNewDuration(settings.leftNeckRelaxation, _leftNeckDuration.text); + _getNewDuration(settings.leftNeckRelaxation, _leftNeckDuration.text); _viewModel.meditationSettings = settings; + _getNewDuration(settings.restingTime, _restingDuration.text); } @override diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart index 86adf34..0dc0f5a 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart @@ -31,28 +31,31 @@ class _StretchTrackerViewState extends State { Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _viewModel, - builder: (context, child) => Consumer( - builder: (context, neckStretchViewModel, child) => Scaffold( - appBar: AppBar( - title: const Text("Guided Neck Relaxation"), - actions: [ - IconButton( - onPressed: (this._viewModel.meditationState == - NeckStretchState.noStretch || + builder: (context, child) => + Consumer( + builder: (context, neckStretchViewModel, child) => + Scaffold( + appBar: AppBar( + title: const Text("Guided Neck Relaxation"), + actions: [ + IconButton( + onPressed: (this._viewModel.meditationState == + NeckStretchState.noStretch || this._viewModel.meditationState == NeckStretchState.doneStretching) - ? () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - SettingsView(this._viewModel))) - : null, - icon: Icon(Icons.settings)), - ], - ), - body: Center( - child: this._buildContentView(neckStretchViewModel), - ), - ))); + ? () => + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + SettingsView(this._viewModel))) + : null, + icon: Icon(Icons.settings)), + ], + ), + body: Center( + child: this._buildContentView(neckStretchViewModel), + ), + ))); } /// Used to start the meditation via the button @@ -124,10 +127,11 @@ class _StretchTrackerViewState extends State { ), ...headViews.map( - (e) => FractionallySizedBox( - widthFactor: .6, - child: e, - ), + (e) => + FractionallySizedBox( + widthFactor: .6, + child: e, + ), ), // Used to place the Meditation-Button always at the bottom Expanded( @@ -138,6 +142,16 @@ class _StretchTrackerViewState extends State { ); } + /// Gets the correct background color for the meditation button + Color _getBackgroundColor(StretchViewModel neckStretchViewModel) { + if (neckStretchViewModel.isResting) { + return Color(0xffffbb3d); + } + + return !neckStretchViewModel.isTracking ? Color(0xff77F2A1) + : Color(0xfff27777); + } + // Creates the Button used to start the meditation Widget _buildMeditationButton(StretchViewModel neckStretchViewModel) { return Padding( @@ -146,15 +160,13 @@ class _StretchTrackerViewState extends State { ElevatedButton( onPressed: neckStretchViewModel.isAvailable ? () { - neckStretchViewModel.isTracking - ? _stopMeditation() - : _startMeditation(); - } + neckStretchViewModel.isTracking + ? _stopMeditation() + : _startMeditation(); + } : null, style: ElevatedButton.styleFrom( - backgroundColor: !neckStretchViewModel.isTracking - ? Color(0xff77F2A1) - : Color(0xfff27777), + backgroundColor: _getBackgroundColor(neckStretchViewModel), foregroundColor: Colors.black, ), child: _getButtonText(), @@ -185,15 +197,14 @@ class _StretchTrackerViewState extends State { } /// Builds the head tracking/stretch view parts for a certain state and thresholds - Visibility _buildStretchViews( - NeckStretchState state, + Visibility _buildStretchViews(NeckStretchState state, StretchViewModel neckStretchViewModel, double frontThreshold, double sideThreshold) { var visibility; if (state == NeckStretchState.noStretch) { visibility = this._viewModel.meditationState == - NeckStretchState.noStretch || + NeckStretchState.noStretch || this._viewModel.meditationState == NeckStretchState.doneStretching; } else { visibility = this._viewModel.meditationState == state; @@ -222,8 +233,7 @@ class _StretchTrackerViewState extends State { } /// Builds the actual head views using the StretchRollView - Widget _buildHeadView( - String headAssetPath, + Widget _buildHeadView(String headAssetPath, String neckAssetPath, AlignmentGeometry headAlignment, double roll, diff --git a/open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart b/open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart index 4c8aca0..825119c 100644 --- a/open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart +++ b/open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart @@ -22,6 +22,8 @@ class StretchViewModel extends ChangeNotifier { Duration get restDuration => _meditation.restDuration; + bool get isResting => _meditation.resting; + set meditationSettings(StretchSettings settings) => _meditation.settings = settings; AttitudeTracker _attitudeTracker; From 903d43e1bdc847fee1bb5d774c13264752cf3367 Mon Sep 17 00:00:00 2001 From: Polaris Date: Fri, 15 Dec 2023 23:34:24 +0100 Subject: [PATCH 031/104] Add flutter-plugin in dependencies to embed yt videos + Add a new overhauled Tutorial view + Fix that settings sync across all views when in neck-relaxation app --- .../neck_meditation/model/stretch_state.dart | 10 +- .../view/stretch_app_view.dart | 60 ++-- .../view/stretch_settings_view.dart | 8 +- .../view/stretch_tracker_view.dart | 125 ++++--- .../view/stretch_tutorial_view.dart | 312 +++++++++++++----- open_earable/lib/apps_tab.dart | 4 +- open_earable/pubspec.yaml | 2 +- 7 files changed, 333 insertions(+), 188 deletions(-) diff --git a/open_earable/lib/apps/neck_meditation/model/stretch_state.dart b/open_earable/lib/apps/neck_meditation/model/stretch_state.dart index ca0f18c..5edbcb6 100644 --- a/open_earable/lib/apps/neck_meditation/model/stretch_state.dart +++ b/open_earable/lib/apps/neck_meditation/model/stretch_state.dart @@ -99,7 +99,7 @@ class NeckMeditation { final StretchViewModel _viewModel; /// Defines whether you are currently resting between two stretch exercises - bool _resting = false; + late bool _resting; /// Holds the Timer that increments the current Duration var _restDurationTimer; @@ -120,6 +120,7 @@ class NeckMeditation { NeckMeditation(this._openEarable, this._viewModel) { this._restDuration = Duration(seconds: 0); + this._resting = false; } /// Starts the Meditation with the according timers @@ -148,14 +149,16 @@ class NeckMeditation { _settings.state = state; // If you just swapped to this state, first rest for restingTime, then set new state if (_resting) { - _resting = false; + _restDuration = _settings.restingTime; _currentTimer = Timer(_settings.restingTime, () { + _resting = false; _setState(state, stateDuration); + _openEarable.audioPlayer.jingle(8); }); } else { + _restDuration = stateDuration; _currentTimer = Timer(stateDuration, _setNextState); } - _restDuration = stateDuration; } /// Used to set the next meditation state and set the correct Timer @@ -163,7 +166,6 @@ class NeckMeditation { switch (_settings.state) { case NeckStretchState.noStretch: case NeckStretchState.doneStretching: - _resting = true; _setState( NeckStretchState.mainNeckStretch, _settings.mainNeckRelaxation); return; diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_app_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_app_view.dart index 8d0c546..c86349d 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_app_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_app_view.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:open_earable/apps_tab.dart'; -import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; import 'package:open_earable/apps/neck_meditation/view/stretch_tracker_view.dart'; import 'package:open_earable/apps/neck_meditation/view/stretch_tutorial_view.dart'; @@ -44,9 +42,7 @@ class _StretchAppViewState extends State { context, MaterialPageRoute( builder: (context) => - StretchTrackerView( - EarableAttitudeTracker(widget._openEarable), - widget._openEarable))); + StretchTrackerView(this._viewModel))); }), AppInfo( iconData: Icons.help, @@ -57,9 +53,7 @@ class _StretchAppViewState extends State { context, MaterialPageRoute( builder: (context) => - StretchTutorialView( - EarableAttitudeTracker(widget._openEarable), - widget._openEarable))); + StretchTutorialView(this._viewModel))); }), // ... similarly for other apps ]; @@ -75,38 +69,32 @@ class _StretchAppViewState extends State { actions: [ IconButton( onPressed: (this._viewModel.meditationState == - NeckStretchState.noStretch || - this._viewModel.meditationState == - NeckStretchState.doneStretching) - ? () => - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - SettingsView(this._viewModel))) + NeckStretchState.noStretch || + this._viewModel.meditationState == + NeckStretchState.doneStretching) + ? () => Navigator.of(context).push(MaterialPageRoute( + builder: (context) => SettingsView(this._viewModel))) : null, icon: Icon(Icons.settings)), ], ), body: ListView.builder( - itemCount: apps.length, - itemBuilder: (BuildContext context, int index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 5), - child: Card( - color: Theme.of(context).colorScheme.primary, - child: ListTile( - leading: Icon(apps[index].iconData, size: 40.0), - title: Text(apps[index].title), - subtitle: Text(apps[index].description), - trailing: Icon(Icons.arrow_forward_ios, - size: 16.0), - // Arrow icon on the right - onTap: - apps[index].onTap, // Callback when the card is tapped - ), - )); - } - ) - ); + itemCount: apps.length, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: Card( + color: Theme.of(context).colorScheme.primary, + child: ListTile( + leading: Icon(apps[index].iconData, size: 40.0), + title: Text(apps[index].title), + subtitle: Text(apps[index].description), + trailing: Icon(Icons.arrow_forward_ios, size: 16.0), + // Arrow icon on the right + onTap: + apps[index].onTap, // Callback when the card is tapped + ), + )); + })); } } diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart index 8841889..0c819e0 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart @@ -34,14 +34,14 @@ class _SettingsViewState extends State { text: _viewModel.meditationSettings.rightNeckRelaxation.inSeconds .toString()); _restingDuration = TextEditingController( - text: _viewModel.meditationSettings.rightNeckRelaxation.inSeconds + text: _viewModel.meditationSettings.restingTime.inSeconds .toString()); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text("Guided Neck Meditation Settings")), + appBar: AppBar(title: const Text("Relaxation Settings")), body: ChangeNotifierProvider.value( value: _viewModel, builder: (context, child) => Consumer( @@ -57,7 +57,7 @@ class _SettingsViewState extends State { Card( color: Theme.of(context).colorScheme.primary, child: ListTile( - title: Text("Status"), + title: Text("OpenEarable"), trailing: Text(_viewModel.isTracking ? "Tracking" : _viewModel.isAvailable @@ -71,7 +71,7 @@ class _SettingsViewState extends State { children: [ // add a switch to control the `isActive` property of the `BadPostureSettings` ListTile( - title: Text("Meditation Settings"), + title: Text("Settings"), ), ListTile( title: Text("Main Neck Relaxation Duration\n(in seconds)"), diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart index 0dc0f5a..2b83840 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart @@ -1,61 +1,75 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; import 'package:open_earable/apps/neck_meditation/view/stretch_roll_view.dart'; import 'package:open_earable/apps/neck_meditation/view_model/stretch_view_model.dart'; import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; import 'package:open_earable/apps/neck_meditation/view/stretch_settings_view.dart'; -import 'package:open_earable_flutter/src/open_earable_flutter.dart'; - class StretchTrackerView extends StatefulWidget { - final AttitudeTracker _tracker; - final OpenEarable _openEarable; + final StretchViewModel _viewModel; - StretchTrackerView(this._tracker, this._openEarable); + StretchTrackerView(this._viewModel); @override State createState() => _StretchTrackerViewState(); } +/// Builds the actual head views using the StretchRollView +Widget buildHeadView( + String headAssetPath, + String neckAssetPath, + AlignmentGeometry headAlignment, + double roll, + double angleThreshold, + NeckStretchState state) { + return Padding( + padding: const EdgeInsets.all(5), + child: StretchRollView( + roll: roll, + angleThreshold: angleThreshold * 3.14 / 180, + headAssetPath: headAssetPath, + neckAssetPath: neckAssetPath, + headAlignment: headAlignment, + stretchState: state, + ), + ); +} + class _StretchTrackerViewState extends State { late final StretchViewModel _viewModel; @override void initState() { super.initState(); - this._viewModel = StretchViewModel(widget._tracker, widget._openEarable); + this._viewModel = widget._viewModel; } @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _viewModel, - builder: (context, child) => - Consumer( - builder: (context, neckStretchViewModel, child) => - Scaffold( - appBar: AppBar( - title: const Text("Guided Neck Relaxation"), - actions: [ - IconButton( - onPressed: (this._viewModel.meditationState == - NeckStretchState.noStretch || + builder: (context, child) => Consumer( + builder: (context, neckStretchViewModel, child) => Scaffold( + appBar: AppBar( + title: const Text("Guided Neck Relaxation"), + actions: [ + IconButton( + onPressed: (this._viewModel.meditationState == + NeckStretchState.noStretch || this._viewModel.meditationState == NeckStretchState.doneStretching) - ? () => - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - SettingsView(this._viewModel))) - : null, - icon: Icon(Icons.settings)), - ], - ), - body: Center( - child: this._buildContentView(neckStretchViewModel), - ), - ))); + ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + SettingsView(this._viewModel))) + : null, + icon: Icon(Icons.settings)), + ], + ), + body: Center( + child: this._buildContentView(neckStretchViewModel), + ), + ))); } /// Used to start the meditation via the button @@ -127,11 +141,10 @@ class _StretchTrackerViewState extends State { ), ...headViews.map( - (e) => - FractionallySizedBox( - widthFactor: .6, - child: e, - ), + (e) => FractionallySizedBox( + widthFactor: .6, + child: e, + ), ), // Used to place the Meditation-Button always at the bottom Expanded( @@ -148,7 +161,8 @@ class _StretchTrackerViewState extends State { return Color(0xffffbb3d); } - return !neckStretchViewModel.isTracking ? Color(0xff77F2A1) + return !neckStretchViewModel.isTracking + ? Color(0xff77F2A1) : Color(0xfff27777); } @@ -160,10 +174,10 @@ class _StretchTrackerViewState extends State { ElevatedButton( onPressed: neckStretchViewModel.isAvailable ? () { - neckStretchViewModel.isTracking - ? _stopMeditation() - : _startMeditation(); - } + neckStretchViewModel.isTracking + ? _stopMeditation() + : _startMeditation(); + } : null, style: ElevatedButton.styleFrom( backgroundColor: _getBackgroundColor(neckStretchViewModel), @@ -197,14 +211,15 @@ class _StretchTrackerViewState extends State { } /// Builds the head tracking/stretch view parts for a certain state and thresholds - Visibility _buildStretchViews(NeckStretchState state, + Visibility _buildStretchViews( + NeckStretchState state, StretchViewModel neckStretchViewModel, double frontThreshold, double sideThreshold) { var visibility; if (state == NeckStretchState.noStretch) { visibility = this._viewModel.meditationState == - NeckStretchState.noStretch || + NeckStretchState.noStretch || this._viewModel.meditationState == NeckStretchState.doneStretching; } else { visibility = this._viewModel.meditationState == state; @@ -214,14 +229,14 @@ class _StretchTrackerViewState extends State { visible: visibility, child: Column( children: [ - this._buildHeadView( + buildHeadView( state.assetPathHeadFront, state.assetPathNeckFront, Alignment.center.add(Alignment(0, 0.3)), neckStretchViewModel.attitude.roll, frontThreshold, state), - this._buildHeadView( + buildHeadView( state.assetPathHeadSide, state.assetPathNeckSide, Alignment.center.add(Alignment(0, 0.3)), @@ -231,24 +246,4 @@ class _StretchTrackerViewState extends State { ], )); } - - /// Builds the actual head views using the StretchRollView - Widget _buildHeadView(String headAssetPath, - String neckAssetPath, - AlignmentGeometry headAlignment, - double roll, - double angleThreshold, - NeckStretchState state) { - return Padding( - padding: const EdgeInsets.all(5), - child: StretchRollView( - roll: roll, - angleThreshold: angleThreshold * 3.14 / 180, - headAssetPath: headAssetPath, - neckAssetPath: neckAssetPath, - headAlignment: headAlignment, - stretchState: state, - ), - ); - } -} +} \ No newline at end of file diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_tutorial_view.dart b/open_earable/lib/apps/neck_meditation/view/stretch_tutorial_view.dart index 2db6423..e41ddd5 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_tutorial_view.dart +++ b/open_earable/lib/apps/neck_meditation/view/stretch_tutorial_view.dart @@ -1,18 +1,15 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; -import 'package:open_earable/apps/neck_meditation/view/stretch_roll_view.dart'; +import 'package:open_earable/apps/neck_meditation/view/stretch_tracker_view.dart'; import 'package:open_earable/apps/neck_meditation/view_model/stretch_view_model.dart'; import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; import 'package:open_earable/apps/neck_meditation/view/stretch_settings_view.dart'; - -import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:youtube_player_flutter/youtube_player_flutter.dart'; class StretchTutorialView extends StatefulWidget { - final AttitudeTracker _tracker; - final OpenEarable _openEarable; + final StretchViewModel _viewModel; - StretchTutorialView(this._tracker, this._openEarable); + StretchTutorialView(this._viewModel); @override State createState() => _StretchTutorialViewState(); @@ -20,11 +17,14 @@ class StretchTutorialView extends StatefulWidget { class _StretchTutorialViewState extends State { late final StretchViewModel _viewModel; + final YoutubePlayerController _ytController = YoutubePlayerController( + initialVideoId: "H5h54Q0wpps", + flags: YoutubePlayerFlags(mute: false, hideThumbnail: true)); @override void initState() { super.initState(); - this._viewModel = StretchViewModel(widget._tracker, widget._openEarable); + this._viewModel = widget._viewModel; } @override @@ -50,7 +50,9 @@ class _StretchTutorialViewState extends State { ], ), body: Center( - child: this._buildContentView(neckStretchViewModel), + child: SingleChildScrollView( + child: this._buildContentView(neckStretchViewModel), + ), ), ))); } @@ -59,65 +61,244 @@ class _StretchTutorialViewState extends State { Widget _buildContentView(StretchViewModel neckStretchViewModel) { return Column( children: [ - Padding( - padding: EdgeInsets.all(5), - child: Padding( - padding: EdgeInsets.fromLTRB(8, 8, 8, 8), - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - children: [ - TextSpan( - text: - 'Here the body part, which is currently being stretched, will be displayed.') + Card( + color: Theme.of(context).colorScheme.primary, + child: Column( + children: [ + // add a switch to control the `isActive` property of the `BadPostureSettings` + ListTile( + title: Text("Video showing the different stretches"), + ), + YoutubePlayer( + controller: _ytController, + bottomActions: [ + CurrentPosition(), + ProgressBar( + isExpanded: true, + ), ], ), - ), + ], ), ), + Card( + color: Theme.of(context).colorScheme.primary, + child: Column( + children: [ + // add a switch to control the `isActive` property of the `BadPostureSettings` + ListTile( + title: Text("Explaining the Tracking Colors"), + ), + + Padding( + padding: EdgeInsets.fromLTRB(16, 0, 16, 0), + child: RichText( + textAlign: TextAlign.justify, + text: TextSpan( + children: [ + TextSpan( + text: + 'With these widgets you can you can track your current head positioning. Depending on the color of the area you are supposed to be inside of it or outside of it.\n', + ), + ], + ), + ), + ), - Padding( - padding: EdgeInsets.all(8), - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - text: - "Here both your Front and Side view of your head will be displayed. The blue part shows you what part of your neck should currently be stretched. When starting to stretch you should gently tilt your head towards the instructed direction (the gray area of the circle). Once you feel your neck stretch stop and hold the position till the sound occurs. Then you can start stretching the next part of your neck."), + Padding( + padding: EdgeInsets.fromLTRB(24, 0, 24, 0), + child: RichText( + textAlign: TextAlign.justify, + text: TextSpan( + children: [ + TextSpan( + text: 'Blue: ', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Color.fromARGB(255, 0, 186, 255), + ), + ), + TextSpan( + text: 'Try to keep your head within this area\n\n', + ), + TextSpan( + text: 'Red: ', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Color(0xfff27777), + ), + ), + TextSpan( + text: + 'You are currently stretching, try to gently move your head into the grey area\n\n', + ), + ], + ), + ), + ), + ], ), ), + Card( + color: Theme.of(context).colorScheme.primary, + child: Column( + children: [ + ListTile( + title: Text('Example: Tracker for Main Neck Stretch'), + titleAlignment: ListTileTitleAlignment.center, + ), + Padding( + padding: EdgeInsets.fromLTRB(0, 0, 0, 16), + child: Column( + children: [ + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: "Currently Stretching: \n", + ), + TextSpan( + text: NeckStretchState.mainNeckStretch.display, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Color.fromARGB(255, 0, 186, 255), + ), + ), + ], + ), + ), - FractionallySizedBox( - widthFactor: 0.6, - child: this._buildHeadView( - NeckStretchState.mainNeckStretch.assetPathNeckSide, - NeckStretchState.mainNeckStretch.assetPathHeadSide, - Alignment.center.add(Alignment(0, 0.3)), - neckStretchViewModel.attitude.pitch, - 30, - NeckStretchState.noStretch), + /// The head views used for meditation + FractionallySizedBox( + widthFactor: 0.6, + child: buildHeadView( + NeckStretchState.mainNeckStretch.assetPathHeadFront, + NeckStretchState.mainNeckStretch.assetPathNeckFront, + Alignment.center.add(Alignment(0, 0.3)), + neckStretchViewModel.attitude.pitch, + 30, + NeckStretchState.mainNeckStretch), + ), + FractionallySizedBox( + widthFactor: 0.6, + child: buildHeadView( + NeckStretchState.mainNeckStretch.assetPathHeadSide, + NeckStretchState.mainNeckStretch.assetPathNeckSide, + Alignment.center.add(Alignment(0, 0.3)), + neckStretchViewModel.attitude.pitch, + 50, + NeckStretchState.mainNeckStretch), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(16, 0, 16, 0), + child: RichText( + textAlign: TextAlign.justify, + text: TextSpan( + children: [ + TextSpan( + text: + 'The area of your neck that is currently being stretched will be colored in '), + TextSpan( + text: 'blue', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Color.fromARGB(255, 0, 186, 255), + ), + ), + TextSpan( + text: + '.\n\nWhenever an exercise is over a sound will play to inform you that you are done with the current stretch. Afterwards you will have a small time to prepare for the next stretch (normally 5 seconds). The button will always keep you up to date with your time limits, and above the tracker is also a text telling you what exactly you are currently stretching.\n\n'), + TextSpan( + text: + 'You can use the button bellow to have a preview of how the tracking will look.\n'), + ], + ), + ), + ), + ], + ), ), + Card( + color: Theme.of(context).colorScheme.primary, + child: Column( + children: [ + // add a switch to control the `isActive` property of the `BadPostureSettings` + ListTile( + title: Text("Explaining the Meditation Button"), + ), - /// Used to place the Meditation-Button always at the bottom - Expanded( - child: Container(), - ), + /// Explainer text for the button + Padding( + padding: EdgeInsets.all(16), + child: RichText( + textAlign: TextAlign.justify, + text: TextSpan( + children: [ + TextSpan( + text: + 'This button is used to start the meditation or to stop it preemptively. Depending on the color you can tell the current state:\n', + ), + ], + ), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(24, 0, 24, 0), + child: RichText( + textAlign: TextAlign.justify, + text: TextSpan( + children: [ + TextSpan( + text: 'Green: ', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Color(0xff77F2A1), + ), + ), + TextSpan( + text: 'You are currently not meditating\n\n', + ), + TextSpan( + text: 'Red: ', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Color(0xfff27777), + ), + ), + TextSpan( + text: + 'You are currently meditating, the button will display the remaining time\n\n', + ), + TextSpan( + text: 'Yellow: ', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: Color(0xffffbb3d), + ), + ), + TextSpan( + text: + 'You are currently having a break between the stretches. The button displays the remaining time.\n\n'), + ], + ), + ), + ), - /// Explainer text for the button - Padding( - padding: EdgeInsets.all(8), - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - children: [ - TextSpan( - text: - 'This button will be used to start and to preemptively stop the meditation. If you are currently meditating the button will show you the remaining time for the current stretch.'), - TextSpan(text: ''), - ], - ), + this._buildMeditationButton(neckStretchViewModel), + ], ), ), - this._buildMeditationButton(neckStretchViewModel), ], ); } @@ -148,25 +329,4 @@ class _StretchTutorialViewState extends State { ]), ); } - - /// Builds the actual head views using the StretchRollView - Widget _buildHeadView( - String headAssetPath, - String neckAssetPath, - AlignmentGeometry headAlignment, - double roll, - double angleThreshold, - NeckStretchState state) { - return Padding( - padding: const EdgeInsets.all(5), - child: StretchRollView( - roll: roll, - angleThreshold: angleThreshold * 3.14 / 180, - headAssetPath: headAssetPath, - neckAssetPath: neckAssetPath, - headAlignment: headAlignment, - stretchState: state, - ), - ); - } } diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 6438d8d..10b874c 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -48,8 +48,8 @@ class AppsTab extends StatelessWidget { }), AppInfo( iconData: Icons.self_improvement, - title: "Guided neck relaxation", - description: "Relax your neck using the OpenEarble.", + title: "Guided Neck Relaxation", + description: "Relax your neck with a stretch.", onTap: () { Navigator.push( context, diff --git a/open_earable/pubspec.yaml b/open_earable/pubspec.yaml index 9a0b748..17a7dc6 100644 --- a/open_earable/pubspec.yaml +++ b/open_earable/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: open_earable_flutter: git: url: https://github.com/OpenEarable/open_earable_flutter.git - + youtube_player_flutter: ^8.1.2 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. From 9baa7c463ea29ffe579a083e8eb8a71777aad5d1 Mon Sep 17 00:00:00 2001 From: Polaris Date: Sun, 17 Dec 2023 15:21:10 +0100 Subject: [PATCH 032/104] Rename app to be called NeckStretch-App instead of Meditation or Relaxation --- .../model/stretch_state.dart | 16 +++++--- .../view/stretch_app_view.dart | 20 +++++----- .../view/stretch_arc_painter.dart | 2 +- .../view/stretch_roll_view.dart | 4 +- .../view/stretch_settings_view.dart | 20 +++++----- .../view/stretch_tracker_view.dart | 38 +++++++++---------- .../view/stretch_tutorial_view.dart | 33 +++++++++++----- .../view_model/stretch_view_model.dart | 18 ++++----- open_earable/lib/apps_tab.dart | 4 +- 9 files changed, 86 insertions(+), 69 deletions(-) rename open_earable/lib/apps/{neck_meditation => neck_stretch}/model/stretch_state.dart (96%) rename open_earable/lib/apps/{neck_meditation => neck_stretch}/view/stretch_app_view.dart (81%) rename open_earable/lib/apps/{neck_meditation => neck_stretch}/view/stretch_arc_painter.dart (98%) rename open_earable/lib/apps/{neck_meditation => neck_stretch}/view/stretch_roll_view.dart (93%) rename open_earable/lib/apps/{neck_meditation => neck_stretch}/view/stretch_settings_view.dart (92%) rename open_earable/lib/apps/{neck_meditation => neck_stretch}/view/stretch_tracker_view.dart (84%) rename open_earable/lib/apps/{neck_meditation => neck_stretch}/view/stretch_tutorial_view.dart (91%) rename open_earable/lib/apps/{neck_meditation => neck_stretch}/view_model/stretch_view_model.dart (70%) diff --git a/open_earable/lib/apps/neck_meditation/model/stretch_state.dart b/open_earable/lib/apps/neck_stretch/model/stretch_state.dart similarity index 96% rename from open_earable/lib/apps/neck_meditation/model/stretch_state.dart rename to open_earable/lib/apps/neck_stretch/model/stretch_state.dart index 5edbcb6..b34f6ab 100644 --- a/open_earable/lib/apps/neck_meditation/model/stretch_state.dart +++ b/open_earable/lib/apps/neck_stretch/model/stretch_state.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:open_earable/apps/neck_meditation/view_model/stretch_view_model.dart'; +import 'package:open_earable/apps/neck_stretch/view_model/stretch_view_model.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; /// Enum for the Meditation States @@ -88,7 +88,7 @@ class StretchSettings { } /// Stores all data and functions to manage the guided neck meditation -class NeckMeditation { +class NeckStretch { StretchSettings _settings = StretchSettings( mainNeckRelaxation: Duration(seconds: 30), leftNeckRelaxation: Duration(seconds: 30), @@ -118,7 +118,7 @@ class NeckMeditation { set settings(StretchSettings settings) => _settings = settings; - NeckMeditation(this._openEarable, this._viewModel) { + NeckStretch(this._openEarable, this._viewModel) { this._restDuration = Duration(seconds: 0); this._resting = false; } @@ -129,9 +129,6 @@ class NeckMeditation { _viewModel.startTracking(); _settings.state = NeckStretchState.noStretch; _setNextState(); - _restDurationTimer = Timer.periodic(Duration(seconds: 1), (timer) { - _restDuration -= Duration(seconds: 1); - }); } /// Stops the current Meditation @@ -144,6 +141,12 @@ class NeckMeditation { _viewModel.stopTracking(); } + void _startCountdown() { + _restDurationTimer = Timer.periodic(Duration(seconds: 1), (timer) { + _restDuration -= Duration(seconds: 1); + }); + } + /// Sets the state and timers for the state. void _setState(NeckStretchState state, Duration stateDuration) { _settings.state = state; @@ -166,6 +169,7 @@ class NeckMeditation { switch (_settings.state) { case NeckStretchState.noStretch: case NeckStretchState.doneStretching: + _startCountdown(); _setState( NeckStretchState.mainNeckStretch, _settings.mainNeckRelaxation); return; diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_app_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart similarity index 81% rename from open_earable/lib/apps/neck_meditation/view/stretch_app_view.dart rename to open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart index c86349d..9bd1bc6 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_app_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps_tab.dart'; import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; -import 'package:open_earable/apps/neck_meditation/view/stretch_tracker_view.dart'; -import 'package:open_earable/apps/neck_meditation/view/stretch_tutorial_view.dart'; -import 'package:open_earable/apps/neck_meditation/view_model/stretch_view_model.dart'; -import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; -import 'package:open_earable/apps/neck_meditation/view/stretch_settings_view.dart'; +import 'package:open_earable/apps/neck_stretch/view/stretch_tracker_view.dart'; +import 'package:open_earable/apps/neck_stretch/view/stretch_tutorial_view.dart'; +import 'package:open_earable/apps/neck_stretch/view_model/stretch_view_model.dart'; +import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_stretch/view/stretch_settings_view.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; @@ -31,7 +31,7 @@ class _StretchAppViewState extends State { this._viewModel = StretchViewModel(widget._tracker, widget._openEarable); } - List meditationApps(BuildContext context) { + List stretchApps(BuildContext context) { return [ AppInfo( iconData: Icons.play_circle, @@ -61,16 +61,16 @@ class _StretchAppViewState extends State { @override Widget build(BuildContext context) { - List apps = meditationApps(context); + List apps = stretchApps(context); return Scaffold( appBar: AppBar( - title: const Text("Guided Neck Relaxation"), + title: const Text("Guided Neck Stretch"), actions: [ IconButton( - onPressed: (this._viewModel.meditationState == + onPressed: (this._viewModel.stretchState == NeckStretchState.noStretch || - this._viewModel.meditationState == + this._viewModel.stretchState == NeckStretchState.doneStretching) ? () => Navigator.of(context).push(MaterialPageRoute( builder: (context) => SettingsView(this._viewModel))) diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_arc_painter.dart b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart similarity index 98% rename from open_earable/lib/apps/neck_meditation/view/stretch_arc_painter.dart rename to open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart index 810bedc..0f9f6b0 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_arc_painter.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; import 'package:open_earable/apps/posture_tracker/view/arc_painter.dart'; class StretchArcPainter extends CustomPainter { diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_roll_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_roll_view.dart similarity index 93% rename from open_earable/lib/apps/neck_meditation/view/stretch_roll_view.dart rename to open_earable/lib/apps/neck_stretch/view/stretch_roll_view.dart index 21fe72a..2e79215 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_roll_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_roll_view.dart @@ -1,8 +1,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; -import 'package:open_earable/apps/neck_meditation/view/stretch_arc_painter.dart'; +import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_stretch/view/stretch_arc_painter.dart'; /// A widget that displays the roll of the head and neck for the meditation. class StretchRollView extends StatelessWidget { diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart similarity index 92% rename from open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart rename to open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart index 0c819e0..88e0ab1 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_settings_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; -import 'package:open_earable/apps/neck_meditation/view_model/stretch_view_model.dart'; +import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_stretch/view_model/stretch_view_model.dart'; import 'package:provider/provider.dart'; class SettingsView extends StatefulWidget { @@ -25,23 +25,23 @@ class _SettingsViewState extends State { super.initState(); this._viewModel = widget._viewModel; _mainNeckDuration = TextEditingController( - text: _viewModel.meditationSettings.mainNeckRelaxation.inSeconds + text: _viewModel.stretchSettings.mainNeckRelaxation.inSeconds .toString()); _leftNeckDuration = TextEditingController( - text: _viewModel.meditationSettings.leftNeckRelaxation.inSeconds + text: _viewModel.stretchSettings.leftNeckRelaxation.inSeconds .toString()); _rightNeckDuration = TextEditingController( - text: _viewModel.meditationSettings.rightNeckRelaxation.inSeconds + text: _viewModel.stretchSettings.rightNeckRelaxation.inSeconds .toString()); _restingDuration = TextEditingController( - text: _viewModel.meditationSettings.restingTime.inSeconds + text: _viewModel.stretchSettings.restingTime.inSeconds .toString()); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text("Relaxation Settings")), + appBar: AppBar(title: const Text("Stretch Settings")), body: ChangeNotifierProvider.value( value: _viewModel, builder: (context, child) => Consumer( @@ -71,7 +71,7 @@ class _SettingsViewState extends State { children: [ // add a switch to control the `isActive` property of the `BadPostureSettings` ListTile( - title: Text("Settings"), + title: Text("Timers"), ), ListTile( title: Text("Main Neck Relaxation Duration\n(in seconds)"), @@ -213,14 +213,14 @@ class _SettingsViewState extends State { /// Update the Meditation Settings according to values, if field is empty set that timer Duration to 0 void _updateMeditationSettings() { - StretchSettings settings = _viewModel.meditationSettings; + StretchSettings settings = _viewModel.stretchSettings; settings.mainNeckRelaxation = _getNewDuration(settings.mainNeckRelaxation, _mainNeckDuration.text); settings.rightNeckRelaxation = _getNewDuration(settings.rightNeckRelaxation, _rightNeckDuration.text); settings.leftNeckRelaxation = _getNewDuration(settings.leftNeckRelaxation, _leftNeckDuration.text); - _viewModel.meditationSettings = settings; + settings.restingTime = _getNewDuration(settings.restingTime, _restingDuration.text); } diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart similarity index 84% rename from open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart rename to open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart index 2b83840..656aaeb 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_tracker_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:open_earable/apps/neck_meditation/view/stretch_roll_view.dart'; -import 'package:open_earable/apps/neck_meditation/view_model/stretch_view_model.dart'; -import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; -import 'package:open_earable/apps/neck_meditation/view/stretch_settings_view.dart'; +import 'package:open_earable/apps/neck_stretch/view/stretch_roll_view.dart'; +import 'package:open_earable/apps/neck_stretch/view_model/stretch_view_model.dart'; +import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_stretch/view/stretch_settings_view.dart'; class StretchTrackerView extends StatefulWidget { final StretchViewModel _viewModel; @@ -54,9 +54,9 @@ class _StretchTrackerViewState extends State { title: const Text("Guided Neck Relaxation"), actions: [ IconButton( - onPressed: (this._viewModel.meditationState == + onPressed: (this._viewModel.stretchState == NeckStretchState.noStretch || - this._viewModel.meditationState == + this._viewModel.stretchState == NeckStretchState.doneStretching) ? () => Navigator.of(context).push( MaterialPageRoute( @@ -74,12 +74,12 @@ class _StretchTrackerViewState extends State { /// Used to start the meditation via the button void _startMeditation() { - this._viewModel.meditation.startMeditation(); + this._viewModel.neckStretch.startMeditation(); } /// Used to stop the meditation via the button void _stopMeditation() { - this._viewModel.meditation.stopMeditation(); + this._viewModel.neckStretch.stopMeditation(); } TextSpan _getStatusText() { @@ -92,16 +92,16 @@ class _StretchTrackerViewState extends State { ), ); - if (_viewModel.meditationState == NeckStretchState.noStretch || - _viewModel.meditationState == NeckStretchState.doneStretching) - return TextSpan(text: "Click the Button below\n to start Meditating!"); + if (_viewModel.stretchState == NeckStretchState.noStretch || + _viewModel.stretchState == NeckStretchState.doneStretching) + return TextSpan(text: "Click the Button below\n to start Stretching!"); return TextSpan(children: [ TextSpan( text: "Currently Stretching: \n", ), TextSpan( - text: this._viewModel.meditationState.display, + text: this._viewModel.stretchState.display, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, @@ -112,11 +112,11 @@ class _StretchTrackerViewState extends State { } Text _getButtonText() { - if (!_viewModel.isTracking) return Text('Start Meditation'); + if (!_viewModel.isTracking) return Text('Start Stretching'); - if (_viewModel.meditationState == NeckStretchState.doneStretching || - _viewModel.meditationState == NeckStretchState.noStretch) - return Text('Stop Meditation'); + if (_viewModel.stretchState == NeckStretchState.doneStretching || + _viewModel.stretchState == NeckStretchState.noStretch) + return Text('Stop Stretching'); return Text(_viewModel.restDuration.toString().substring(2, 7)); } @@ -218,11 +218,11 @@ class _StretchTrackerViewState extends State { double sideThreshold) { var visibility; if (state == NeckStretchState.noStretch) { - visibility = this._viewModel.meditationState == + visibility = this._viewModel.stretchState == NeckStretchState.noStretch || - this._viewModel.meditationState == NeckStretchState.doneStretching; + this._viewModel.stretchState == NeckStretchState.doneStretching; } else { - visibility = this._viewModel.meditationState == state; + visibility = this._viewModel.stretchState == state; } return Visibility( diff --git a/open_earable/lib/apps/neck_meditation/view/stretch_tutorial_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart similarity index 91% rename from open_earable/lib/apps/neck_meditation/view/stretch_tutorial_view.dart rename to open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart index e41ddd5..c081195 100644 --- a/open_earable/lib/apps/neck_meditation/view/stretch_tutorial_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:open_earable/apps/neck_meditation/view/stretch_tracker_view.dart'; -import 'package:open_earable/apps/neck_meditation/view_model/stretch_view_model.dart'; -import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; -import 'package:open_earable/apps/neck_meditation/view/stretch_settings_view.dart'; + import 'package:youtube_player_flutter/youtube_player_flutter.dart'; +import 'package:open_earable/apps/neck_stretch/view/stretch_tracker_view.dart'; +import 'package:open_earable/apps/neck_stretch/view_model/stretch_view_model.dart'; +import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_stretch/view/stretch_settings_view.dart'; + class StretchTutorialView extends StatefulWidget { final StretchViewModel _viewModel; @@ -34,12 +36,23 @@ class _StretchTutorialViewState extends State { builder: (context, child) => Consumer( builder: (context, neckStretchViewModel, child) => Scaffold( appBar: AppBar( - title: const Text("Guided Neck Relaxation"), + /// Override leading back arrow button to stop tracking if + /// user stopped stretching + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + neckStretchViewModel.isTracking + ? neckStretchViewModel.stopTracking() + : () {}; + Navigator.of(context).pop(); + }), + centerTitle: true, + title: const Text("Guided Neck Stretch"), actions: [ IconButton( - onPressed: (this._viewModel.meditationState == + onPressed: (this._viewModel.stretchState == NeckStretchState.noStretch || - this._viewModel.meditationState == + this._viewModel.stretchState == NeckStretchState.doneStretching) ? () => Navigator.of(context).push( MaterialPageRoute( @@ -232,7 +245,7 @@ class _StretchTutorialViewState extends State { children: [ // add a switch to control the `isActive` property of the `BadPostureSettings` ListTile( - title: Text("Explaining the Meditation Button"), + title: Text("Explaining the Stretching Button"), ), /// Explainer text for the button @@ -323,8 +336,8 @@ class _StretchTutorialViewState extends State { foregroundColor: Colors.black, ), child: neckStretchViewModel.isTracking - ? const Text("Stop Meditation") - : const Text("Start Meditation"), + ? const Text("Stop Stretching") + : const Text("Start Stretching"), ), ]), ); diff --git a/open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart b/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart similarity index 70% rename from open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart rename to open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart index 825119c..bb3efc7 100644 --- a/open_earable/lib/apps/neck_meditation/view_model/stretch_view_model.dart +++ b/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart @@ -1,7 +1,7 @@ import "package:flutter/material.dart"; import "package:open_earable/apps/posture_tracker/model/attitude.dart"; import "package:open_earable/apps/posture_tracker/model/attitude_tracker.dart"; -import 'package:open_earable/apps/neck_meditation/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; @@ -14,28 +14,28 @@ class StretchViewModel extends ChangeNotifier { bool get isAvailable => _attitudeTracker.isAvailable; - NeckMeditation get meditation => _meditation; + NeckStretch get neckStretch => _neckStretch; - StretchSettings get meditationSettings => _meditation.settings; + StretchSettings get stretchSettings => _neckStretch.settings; - NeckStretchState get meditationState => _meditation.settings.state; + NeckStretchState get stretchState => _neckStretch.settings.state; - Duration get restDuration => _meditation.restDuration; + Duration get restDuration => _neckStretch.restDuration; - bool get isResting => _meditation.resting; + bool get isResting => _neckStretch.resting; - set meditationSettings(StretchSettings settings) => _meditation.settings = settings; + set stretchSettings(StretchSettings settings) => _neckStretch.settings = settings; AttitudeTracker _attitudeTracker; OpenEarable _openEarable; - late NeckMeditation _meditation; + late NeckStretch _neckStretch; StretchViewModel(this._attitudeTracker, this._openEarable) { _attitudeTracker.didChangeAvailability = (_) { notifyListeners(); }; - this._meditation = NeckMeditation(_openEarable, this); + this._neckStretch = NeckStretch(_openEarable, this); _attitudeTracker.listen((attitude) { _attitude = Attitude( roll: attitude.roll, diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 10b874c..d511684 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; import 'package:open_earable/apps/recorder.dart'; -import 'package:open_earable/apps/neck_meditation/view/stretch_app_view.dart'; +import 'package:open_earable/apps/neck_stretch/view/stretch_app_view.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class AppInfo { @@ -48,7 +48,7 @@ class AppsTab extends StatelessWidget { }), AppInfo( iconData: Icons.self_improvement, - title: "Guided Neck Relaxation", + title: "Guided Neck Stretch", description: "Relax your neck with a stretch.", onTap: () { Navigator.push( From ae7590808054564c569c921e3335c263e054d260 Mon Sep 17 00:00:00 2001 From: Polaris Date: Sun, 17 Dec 2023 15:50:46 +0100 Subject: [PATCH 033/104] Change the colors used for the stretch_arc_painter + bug fixes --- .../neck_stretch/model/stretch_state.dart | 4 +- .../view/stretch_arc_painter.dart | 42 +++++++++--------- .../view/stretch_tracker_view.dart | 43 +++++++++++-------- .../view/stretch_tutorial_view.dart | 2 +- 4 files changed, 49 insertions(+), 42 deletions(-) diff --git a/open_earable/lib/apps/neck_stretch/model/stretch_state.dart b/open_earable/lib/apps/neck_stretch/model/stretch_state.dart index b34f6ab..d22c31a 100644 --- a/open_earable/lib/apps/neck_stretch/model/stretch_state.dart +++ b/open_earable/lib/apps/neck_stretch/model/stretch_state.dart @@ -124,7 +124,7 @@ class NeckStretch { } /// Starts the Meditation with the according timers - void startMeditation() { + void startStretching() { _resting = false; _viewModel.startTracking(); _settings.state = NeckStretchState.noStretch; @@ -132,7 +132,7 @@ class NeckStretch { } /// Stops the current Meditation - void stopMeditation() { + void stopStretching() { _resting = false; _settings.state = NeckStretchState.noStretch; _currentTimer.cancel(); diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart index 0f9f6b0..07faabc 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart @@ -31,7 +31,7 @@ class StretchArcPainter extends CustomPainter { // Create a paint object with purple color and stroke style Paint anglePaint = Paint() - ..color = Colors.purpleAccent + ..color = _isCorrectStretchDirection() ? Colors.redAccent[100]! : Colors.greenAccent[100]! ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeWidth = 5.0; @@ -69,7 +69,7 @@ class StretchArcPainter extends CustomPainter { Rect.fromCircle(center: center, radius: radius), // create a rectangle from the center and radius startAngle + angle.sign * angleThreshold, // start angle - _isOvershooting() ? angle.sign * (angle.abs() - angleThreshold) : 0, // sweep angle + !_isCorrectStretchDirection() ? angle.sign * (angle.abs() - angleThreshold) : 0, // sweep angle ); } @@ -135,27 +135,25 @@ class StretchArcPainter extends CustomPainter { } } - /// Determines whether it actually is overshooting the threshold. This is a - /// small hack as the head angle cant go over a certain degree this way we can - /// ensure that the wrong stretch direction drawn part can never be overshot - bool _isOvershooting() { - if (this.isFront) { - switch (this.stretchState) { - case NeckStretchState.rightNeckStretch: - return angle.sign <= 0; - case NeckStretchState.leftNeckStretch: - return angle.sign >= 0; - default: - return true; - } - } - + /// Determines whether the user is currently stretching in the right direction + bool _isCorrectStretchDirection() { + if (this.isFront) { switch (this.stretchState) { - case NeckStretchState.mainNeckStretch: + case NeckStretchState.rightNeckStretch: + return angle.sign >= 0; + case NeckStretchState.leftNeckStretch: return angle.sign <= 0; default: - return true; + return false; } + } + + switch (this.stretchState) { + case NeckStretchState.mainNeckStretch: + return angle.sign >= 0; + default: + return false; + } } /// Detgermines whether the overshoot is negative (shouldnt stretch that part) @@ -176,18 +174,18 @@ class StretchArcPainter extends CustomPainter { return Color.fromARGB(255, 255, 138, 128); } - return Color.fromARGB(255, 0, 186, 255); + return Color(0xff77F2A1); } /// Returns the right color for the threshold depending on stretch state and /// if its the upper or lower head state arc. Color getThresholdColor() { if (_isNegativeOvershoot()) { - return Color.fromARGB(255, 0, 186, 255); + return Color(0xff77F2A1); } // Equals Colors.redAccent[100]! - return Color.fromARGB(255, 255, 138, 128); + return Color.fromARGB(255, 124, 124, 124); } @override diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart index 656aaeb..94c1b08 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart @@ -51,6 +51,16 @@ class _StretchTrackerViewState extends State { builder: (context, child) => Consumer( builder: (context, neckStretchViewModel, child) => Scaffold( appBar: AppBar( + /// Override leading back arrow button to stop tracking if + /// user stopped stretching + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + neckStretchViewModel.isTracking + ? _stopStretching() + : () {}; + Navigator.of(context).pop(); + }), title: const Text("Guided Neck Relaxation"), actions: [ IconButton( @@ -72,14 +82,14 @@ class _StretchTrackerViewState extends State { ))); } - /// Used to start the meditation via the button - void _startMeditation() { - this._viewModel.neckStretch.startMeditation(); + /// Used to start stretching via the button + void _startStretching() { + this._viewModel.neckStretch.startStretching(); } - /// Used to stop the meditation via the button - void _stopMeditation() { - this._viewModel.neckStretch.stopMeditation(); + /// Used to stop stretching via the button + void _stopStretching() { + this._viewModel.neckStretch.stopStretching(); } TextSpan _getStatusText() { @@ -146,16 +156,16 @@ class _StretchTrackerViewState extends State { child: e, ), ), - // Used to place the Meditation-Button always at the bottom + // Used to place the Stretching Button always at the bottom Expanded( child: Container(), ), - this._buildMeditationButton(neckStretchViewModel), + this._buildStretchButton(neckStretchViewModel), ], ); } - /// Gets the correct background color for the meditation button + /// Gets the correct background color for the stretching button Color _getBackgroundColor(StretchViewModel neckStretchViewModel) { if (neckStretchViewModel.isResting) { return Color(0xffffbb3d); @@ -166,8 +176,8 @@ class _StretchTrackerViewState extends State { : Color(0xfff27777); } - // Creates the Button used to start the meditation - Widget _buildMeditationButton(StretchViewModel neckStretchViewModel) { + // Creates the Button used to start the stretch exercise + Widget _buildStretchButton(StretchViewModel neckStretchViewModel) { return Padding( padding: EdgeInsets.all(5), child: Column(children: [ @@ -175,8 +185,8 @@ class _StretchTrackerViewState extends State { onPressed: neckStretchViewModel.isAvailable ? () { neckStretchViewModel.isTracking - ? _stopMeditation() - : _startMeditation(); + ? _stopStretching() + : _startStretching(); } : null, style: ElevatedButton.styleFrom( @@ -189,7 +199,7 @@ class _StretchTrackerViewState extends State { ); } - /// Creates the Head Views that display depending on the MeditationState. + /// Creates the Head Views that display depending on the stretch state. List _createHeadViews(StretchViewModel neckStretchViewModel) { return [ // Visible Head-Displays when not stretching @@ -218,8 +228,7 @@ class _StretchTrackerViewState extends State { double sideThreshold) { var visibility; if (state == NeckStretchState.noStretch) { - visibility = this._viewModel.stretchState == - NeckStretchState.noStretch || + visibility = this._viewModel.stretchState == NeckStretchState.noStretch || this._viewModel.stretchState == NeckStretchState.doneStretching; } else { visibility = this._viewModel.stretchState == state; @@ -246,4 +255,4 @@ class _StretchTrackerViewState extends State { ], )); } -} \ No newline at end of file +} diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart index c081195..35643e8 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart @@ -21,7 +21,7 @@ class _StretchTutorialViewState extends State { late final StretchViewModel _viewModel; final YoutubePlayerController _ytController = YoutubePlayerController( initialVideoId: "H5h54Q0wpps", - flags: YoutubePlayerFlags(mute: false, hideThumbnail: true)); + flags: YoutubePlayerFlags(mute: false, autoPlay: false)); @override void initState() { From e26e142a6d90f6c3ec003fef1337a5135de11de2 Mon Sep 17 00:00:00 2001 From: Polaris Date: Sun, 17 Dec 2023 16:11:48 +0100 Subject: [PATCH 034/104] Make the used colors publicly available from the arc painter class to use elsewhere --- .../view/stretch_arc_painter.dart | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart index 07faabc..77c1755 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart @@ -4,6 +4,14 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; import 'package:open_earable/apps/posture_tracker/view/arc_painter.dart'; +/// Colors used for bad stretch directions +/// Equivalent with Colors.redAccent[100]! +final Color badStretchColor = Color.fromARGB(255, 255, 138, 128); +final Color badStretchIndicatorColor = Colors.redAccent[100]!; +/// Colors used for good stretch direction +final Color goodStretchColor = Color(0xff77F2A1); +final Color goodStretchIndicatorColor = Colors.greenAccent[100]!; + class StretchArcPainter extends CustomPainter { /// the angle of rotation final double angle; @@ -29,9 +37,9 @@ class StretchArcPainter extends CustomPainter { radius: min(size.width, size.height) / 2)); canvas.drawPath(circlePath, circlePaint); - // Create a paint object with purple color and stroke style + // Create a paint object with the right color for the stretch indicator Paint anglePaint = Paint() - ..color = _isCorrectStretchDirection() ? Colors.redAccent[100]! : Colors.greenAccent[100]! + ..color = _getIndicatorColor() ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeWidth = 5.0; @@ -55,6 +63,7 @@ class StretchArcPainter extends CustomPainter { endAngle, // sweep angle ); + /// Draw the overshooting path Path angleOvershootPath = Path(); if (_isNegativeOvershoot()) { @@ -69,12 +78,12 @@ class StretchArcPainter extends CustomPainter { Rect.fromCircle(center: center, radius: radius), // create a rectangle from the center and radius startAngle + angle.sign * angleThreshold, // start angle - !_isCorrectStretchDirection() ? angle.sign * (angle.abs() - angleThreshold) : 0, // sweep angle + !_isWrongStretchDirection() ? angle.sign * (angle.abs() - angleThreshold) : 0, // sweep angle ); } Paint angleOvershootPaint = Paint() - ..color = getOvershootColor() + ..color = _getOvershootColor() ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeWidth = 5.0; @@ -83,12 +92,12 @@ class StretchArcPainter extends CustomPainter { thresholdPath.addArc( Rect.fromCircle(center: center, radius: radius), // create a rectangle from the center and radius - getStartAngle(startAngle, angleThreshold), // start angle - getThreshold(angleThreshold), // sweep angle + _getStartAngle(startAngle, angleThreshold), // start angle + _getThreshold(angleThreshold), // sweep angle ); Paint thresholdPaint = Paint() - ..color = getThresholdColor() + ..color = _getThresholdColor() ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeWidth = 5.0; @@ -102,7 +111,7 @@ class StretchArcPainter extends CustomPainter { } /// Gets the right start angle depending on stretch state - double getStartAngle(double startAngle, double threshold) { + double _getStartAngle(double startAngle, double threshold) { if (!this.isFront) return startAngle - threshold; switch (this.stretchState) { @@ -116,7 +125,7 @@ class StretchArcPainter extends CustomPainter { } /// Gets the right threshold depending on stretch state - double getThreshold(double threshold) { + double _getThreshold(double threshold) { if (this.isFront) { switch (this.stretchState) { case NeckStretchState.rightNeckStretch: @@ -136,7 +145,7 @@ class StretchArcPainter extends CustomPainter { } /// Determines whether the user is currently stretching in the right direction - bool _isCorrectStretchDirection() { + bool _isWrongStretchDirection() { if (this.isFront) { switch (this.stretchState) { case NeckStretchState.rightNeckStretch: @@ -168,24 +177,37 @@ class StretchArcPainter extends CustomPainter { /// Returns the right color for the overshoot depending on stretch state and /// if its upper or lower head state arc. - Color getOvershootColor() { + Color _getOvershootColor() { if (_isNegativeOvershoot()) { // Equals Colors.redAccent[100]! - return Color.fromARGB(255, 255, 138, 128); + return badStretchColor; } - return Color(0xff77F2A1); + return goodStretchColor; } /// Returns the right color for the threshold depending on stretch state and /// if its the upper or lower head state arc. - Color getThresholdColor() { + Color _getThresholdColor() { if (_isNegativeOvershoot()) { - return Color(0xff77F2A1); + return goodStretchIndicatorColor; } // Equals Colors.redAccent[100]! - return Color.fromARGB(255, 124, 124, 124); + return Colors.black38; + } + + /// Gets the right indicator color depending on stretch angle and part + Color _getIndicatorColor() { + if(_isNegativeOvershoot()) + return goodStretchColor; + + + if(_isWrongStretchDirection()) + return badStretchIndicatorColor; + + + return goodStretchIndicatorColor; } @override From 76390b57cf18f610de021ebc239d850e566dc159 Mon Sep 17 00:00:00 2001 From: Polaris Date: Sun, 17 Dec 2023 16:31:47 +0100 Subject: [PATCH 035/104] Extract colors from the stretch app into it's own dart file for easier adaptibility --- .../neck_stretch/model/stretch_colors.dart | 22 ++++++++++++++++++ .../view/stretch_arc_painter.dart | 13 +++-------- .../view/stretch_tracker_view.dart | 9 ++++---- .../view/stretch_tutorial_view.dart | 23 ++++++++++--------- 4 files changed, 42 insertions(+), 25 deletions(-) create mode 100644 open_earable/lib/apps/neck_stretch/model/stretch_colors.dart diff --git a/open_earable/lib/apps/neck_stretch/model/stretch_colors.dart b/open_earable/lib/apps/neck_stretch/model/stretch_colors.dart new file mode 100644 index 0000000..bf1a000 --- /dev/null +++ b/open_earable/lib/apps/neck_stretch/model/stretch_colors.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +/// Colors used for the arcs around the head +final Color outerRing = Color.fromARGB(255, 195, 195, 195); +final Color wrongAreaIndicator = Color(0xFF7A7A7A); + +/// Colors used for bad stretch directions +/// Equivalent with Colors.redAccent[100]! +final Color badStretchColor = Color.fromARGB(255, 255, 138, 128); +final Color badStretchIndicatorColor = Colors.redAccent[100]!; + +/// Colors used for good stretch direction +final Color goodStretchColor = Color(0xff77F2A1); +final Color goodStretchIndicatorColor = Colors.greenAccent[100]!; + +/// Used to indicate what is currently being stretched +final Color stretchedAreaColor = Color.fromARGB(255, 0, 186, 255); + +/// Colors used for the stretching button +final Color startButtonColor = Color(0xff77F2A1); +final Color stopButtonColor = Color(0xfff27777); +final Color restingButtonColor = Color(0xffffbb3d); diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart index 77c1755..36bd76b 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart @@ -3,14 +3,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; import 'package:open_earable/apps/posture_tracker/view/arc_painter.dart'; +import 'package:open_earable/apps/neck_stretch/model/stretch_colors.dart'; -/// Colors used for bad stretch directions -/// Equivalent with Colors.redAccent[100]! -final Color badStretchColor = Color.fromARGB(255, 255, 138, 128); -final Color badStretchIndicatorColor = Colors.redAccent[100]!; -/// Colors used for good stretch direction -final Color goodStretchColor = Color(0xff77F2A1); -final Color goodStretchIndicatorColor = Colors.greenAccent[100]!; class StretchArcPainter extends CustomPainter { /// the angle of rotation @@ -27,7 +21,7 @@ class StretchArcPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { Paint circlePaint = Paint() - ..color = const Color.fromARGB(255, 195, 195, 195) + ..color = outerRing ..style = PaintingStyle.stroke ..strokeWidth = 5.0; @@ -193,8 +187,7 @@ class StretchArcPainter extends CustomPainter { return goodStretchIndicatorColor; } - // Equals Colors.redAccent[100]! - return Colors.black38; + return wrongAreaIndicator; } /// Gets the right indicator color depending on stretch angle and part diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart index 94c1b08..4a4e4d9 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart @@ -4,6 +4,7 @@ import 'package:open_earable/apps/neck_stretch/view/stretch_roll_view.dart'; import 'package:open_earable/apps/neck_stretch/view_model/stretch_view_model.dart'; import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; import 'package:open_earable/apps/neck_stretch/view/stretch_settings_view.dart'; +import 'package:open_earable/apps/neck_stretch/model/stretch_colors.dart'; class StretchTrackerView extends StatefulWidget { final StretchViewModel _viewModel; @@ -115,7 +116,7 @@ class _StretchTrackerViewState extends State { style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, - color: Color.fromARGB(255, 0, 186, 255), + color: stretchedAreaColor, ), ) ]); @@ -168,12 +169,12 @@ class _StretchTrackerViewState extends State { /// Gets the correct background color for the stretching button Color _getBackgroundColor(StretchViewModel neckStretchViewModel) { if (neckStretchViewModel.isResting) { - return Color(0xffffbb3d); + return restingButtonColor; } return !neckStretchViewModel.isTracking - ? Color(0xff77F2A1) - : Color(0xfff27777); + ? startButtonColor + : stopButtonColor; } // Creates the Button used to start the stretch exercise diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart index 35643e8..4276e3b 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:youtube_player_flutter/youtube_player_flutter.dart'; +import 'package:open_earable/apps/neck_stretch/model/stretch_colors.dart'; import 'package:open_earable/apps/neck_stretch/view/stretch_tracker_view.dart'; import 'package:open_earable/apps/neck_stretch/view_model/stretch_view_model.dart'; import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; @@ -125,22 +126,22 @@ class _StretchTutorialViewState extends State { text: TextSpan( children: [ TextSpan( - text: 'Blue: ', + text: 'Green: ', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, - color: Color.fromARGB(255, 0, 186, 255), + color: goodStretchIndicatorColor, ), ), TextSpan( text: 'Try to keep your head within this area\n\n', ), TextSpan( - text: 'Red: ', + text: 'Dark Grey: ', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, - color: Color(0xfff27777), + color: wrongAreaIndicator, ), ), TextSpan( @@ -178,7 +179,7 @@ class _StretchTutorialViewState extends State { style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, - color: Color.fromARGB(255, 0, 186, 255), + color: stretchedAreaColor, ), ), ], @@ -223,7 +224,7 @@ class _StretchTutorialViewState extends State { style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, - color: Color.fromARGB(255, 0, 186, 255), + color: stretchedAreaColor, ), ), TextSpan( @@ -274,7 +275,7 @@ class _StretchTutorialViewState extends State { style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, - color: Color(0xff77F2A1), + color: startButtonColor, ), ), TextSpan( @@ -285,7 +286,7 @@ class _StretchTutorialViewState extends State { style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, - color: Color(0xfff27777), + color: stopButtonColor, ), ), TextSpan( @@ -297,7 +298,7 @@ class _StretchTutorialViewState extends State { style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, - color: Color(0xffffbb3d), + color: restingButtonColor, ), ), TextSpan( @@ -331,8 +332,8 @@ class _StretchTutorialViewState extends State { : null, style: ElevatedButton.styleFrom( backgroundColor: !neckStretchViewModel.isTracking - ? Color(0xff77F2A1) - : Color(0xfff27777), + ? startButtonColor + : stopButtonColor, foregroundColor: Colors.black, ), child: neckStretchViewModel.isTracking From 1e19fb51f40df13dba4d7654d9a329d25a912775 Mon Sep 17 00:00:00 2001 From: Polaris Date: Sun, 17 Dec 2023 16:42:59 +0100 Subject: [PATCH 036/104] Add text to indicate when someone is done stretching and make the settings view scrollable to prevent overflow when the keyboard opens --- .../view/stretch_settings_view.dart | 282 +++++++++--------- .../view/stretch_tracker_view.dart | 8 +- .../view/stretch_tutorial_view.dart | 8 +- .../view_model/stretch_view_model.dart | 12 +- 4 files changed, 155 insertions(+), 155 deletions(-) diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart index 88e0ab1..e3650c0 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart @@ -25,17 +25,16 @@ class _SettingsViewState extends State { super.initState(); this._viewModel = widget._viewModel; _mainNeckDuration = TextEditingController( - text: _viewModel.stretchSettings.mainNeckRelaxation.inSeconds - .toString()); + text: + _viewModel.stretchSettings.mainNeckRelaxation.inSeconds.toString()); _leftNeckDuration = TextEditingController( - text: _viewModel.stretchSettings.leftNeckRelaxation.inSeconds - .toString()); + text: + _viewModel.stretchSettings.leftNeckRelaxation.inSeconds.toString()); _rightNeckDuration = TextEditingController( text: _viewModel.stretchSettings.rightNeckRelaxation.inSeconds .toString()); _restingDuration = TextEditingController( - text: _viewModel.stretchSettings.restingTime.inSeconds - .toString()); + text: _viewModel.stretchSettings.restingTime.inSeconds.toString()); } @override @@ -52,151 +51,154 @@ class _SettingsViewState extends State { } Widget _buildSettingsView() { - return Column( - children: [ - Card( - color: Theme.of(context).colorScheme.primary, - child: ListTile( - title: Text("OpenEarable"), - trailing: Text(_viewModel.isTracking - ? "Tracking" - : _viewModel.isAvailable - ? "Available" - : "Unavailable"), + return SingleChildScrollView( + child: Column( + children: [ + Card( + color: Theme.of(context).colorScheme.primary, + child: ListTile( + title: Text("OpenEarable"), + trailing: Text(_viewModel.isTracking + ? "Tracking" + : _viewModel.isAvailable + ? "Available" + : "Unavailable"), + ), ), - ), - Card( - color: Theme.of(context).colorScheme.primary, - child: Column( - children: [ - // add a switch to control the `isActive` property of the `BadPostureSettings` - ListTile( - title: Text("Timers"), - ), - ListTile( - title: Text("Main Neck Relaxation Duration\n(in seconds)"), - trailing: SizedBox( - height: 37.0, - width: 52, - child: TextField( - controller: _mainNeckDuration, - textAlign: TextAlign.end, - style: TextStyle(color: Colors.black), - decoration: InputDecoration( - contentPadding: EdgeInsets.all(10), - floatingLabelBehavior: FloatingLabelBehavior.never, - border: OutlineInputBorder(), - labelText: 'Seconds', - filled: true, - labelStyle: TextStyle(color: Colors.black), - fillColor: Colors.white), - keyboardType: TextInputType.number, - onChanged: (_) { - _updateMeditationSettings(); - }, + Card( + color: Theme.of(context).colorScheme.primary, + child: Column( + children: [ + // add a switch to control the `isActive` property of the `BadPostureSettings` + ListTile( + title: Text("Timers"), + ), + ListTile( + title: Text("Main Neck Relaxation Duration\n(in seconds)"), + trailing: SizedBox( + height: 37.0, + width: 52, + child: TextField( + controller: _mainNeckDuration, + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(10), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: OutlineInputBorder(), + labelText: 'Seconds', + filled: true, + labelStyle: TextStyle(color: Colors.black), + fillColor: Colors.white), + keyboardType: TextInputType.number, + onChanged: (_) { + _updateMeditationSettings(); + }, + ), ), ), - ), - ListTile( - title: Text("Right Neck Relaxation Duration\n(in seconds)"), - trailing: SizedBox( - height: 37.0, - width: 52, - child: TextField( - controller: _rightNeckDuration, - textAlign: TextAlign.end, - style: TextStyle(color: Colors.black), - decoration: InputDecoration( - contentPadding: EdgeInsets.all(10), - floatingLabelBehavior: FloatingLabelBehavior.never, - border: OutlineInputBorder(), - labelText: 'Seconds', - filled: true, - labelStyle: TextStyle(color: Colors.black), - fillColor: Colors.white), - keyboardType: TextInputType.number, - onChanged: (_) { - _updateMeditationSettings(); - }, + ListTile( + title: Text("Right Neck Relaxation Duration\n(in seconds)"), + trailing: SizedBox( + height: 37.0, + width: 52, + child: TextField( + controller: _rightNeckDuration, + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(10), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: OutlineInputBorder(), + labelText: 'Seconds', + filled: true, + labelStyle: TextStyle(color: Colors.black), + fillColor: Colors.white), + keyboardType: TextInputType.number, + onChanged: (_) { + _updateMeditationSettings(); + }, + ), ), ), - ), - ListTile( - title: Text("Left Neck Relaxation Duration\n(in seconds)"), - trailing: SizedBox( - height: 37.0, - width: 52, - child: TextField( - controller: _leftNeckDuration, - textAlign: TextAlign.end, - style: TextStyle(color: Colors.black), - decoration: InputDecoration( - contentPadding: EdgeInsets.all(10), - floatingLabelBehavior: FloatingLabelBehavior.never, - border: OutlineInputBorder(), - labelText: 'Seconds', - filled: true, - labelStyle: TextStyle(color: Colors.black), - fillColor: Colors.white), - keyboardType: TextInputType.number, - onChanged: (_) { - _updateMeditationSettings(); - }, + ListTile( + title: Text("Left Neck Relaxation Duration\n(in seconds)"), + trailing: SizedBox( + height: 37.0, + width: 52, + child: TextField( + controller: _leftNeckDuration, + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(10), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: OutlineInputBorder(), + labelText: 'Seconds', + filled: true, + labelStyle: TextStyle(color: Colors.black), + fillColor: Colors.white), + keyboardType: TextInputType.number, + onChanged: (_) { + _updateMeditationSettings(); + }, + ), ), ), - ), - ListTile( - title: Text("Resting Duration between exercises\n(in seconds)"), - trailing: SizedBox( - height: 37.0, - width: 52, - child: TextField( - controller: _restingDuration, - textAlign: TextAlign.end, - style: TextStyle(color: Colors.black), - decoration: InputDecoration( - contentPadding: EdgeInsets.all(10), - floatingLabelBehavior: FloatingLabelBehavior.never, - border: OutlineInputBorder(), - labelText: 'Seconds', - filled: true, - labelStyle: TextStyle(color: Colors.black), - fillColor: Colors.white), - keyboardType: TextInputType.number, - onChanged: (_) { - _updateMeditationSettings(); - }, + ListTile( + title: + Text("Resting Duration between exercises\n(in seconds)"), + trailing: SizedBox( + height: 37.0, + width: 52, + child: TextField( + controller: _restingDuration, + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(10), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: OutlineInputBorder(), + labelText: 'Seconds', + filled: true, + labelStyle: TextStyle(color: Colors.black), + fillColor: Colors.white), + keyboardType: TextInputType.number, + onChanged: (_) { + _updateMeditationSettings(); + }, + ), ), ), - ), - ], + ], + ), ), - ), - Padding( - padding: EdgeInsets.all(8.0), - child: Row(children: [ - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: _viewModel.isTracking - ? Colors.green[300] - : Colors.blue[300], - foregroundColor: Colors.black, + Padding( + padding: EdgeInsets.all(8.0), + child: Row(children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: _viewModel.isTracking + ? Colors.green[300] + : Colors.blue[300], + foregroundColor: Colors.black, + ), + onPressed: _viewModel.isTracking + ? () { + _viewModel.calibrate(); + _viewModel.stopTracking(); + } + : () => _viewModel.startTracking(), + child: Text(_viewModel.isTracking + ? "Calibrate as Main Posture" + : "Start Calibration"), ), - onPressed: _viewModel.isTracking - ? () { - _viewModel.calibrate(); - _viewModel.stopTracking(); - } - : () => _viewModel.startTracking(), - child: Text(_viewModel.isTracking - ? "Calibrate as Main Posture" - : "Start Calibration"), - ), - ) - ]), - ), - ], + ) + ]), + ), + ], + ), ); } diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart index 4a4e4d9..e2cba04 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart @@ -62,7 +62,7 @@ class _StretchTrackerViewState extends State { : () {}; Navigator.of(context).pop(); }), - title: const Text("Guided Neck Relaxation"), + title: const Text("Guided Neck Stretch"), actions: [ IconButton( onPressed: (this._viewModel.stretchState == @@ -103,10 +103,12 @@ class _StretchTrackerViewState extends State { ), ); - if (_viewModel.stretchState == NeckStretchState.noStretch || - _viewModel.stretchState == NeckStretchState.doneStretching) + if (_viewModel.stretchState == NeckStretchState.noStretch) return TextSpan(text: "Click the Button below\n to start Stretching!"); + if (_viewModel.stretchState == NeckStretchState.doneStretching) + return TextSpan(text: "You are done stretching,\n good job!"); + return TextSpan(children: [ TextSpan( text: "Currently Stretching: \n", diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart index 4276e3b..d41fa3f 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart @@ -47,7 +47,6 @@ class _StretchTutorialViewState extends State { : () {}; Navigator.of(context).pop(); }), - centerTitle: true, title: const Text("Guided Neck Stretch"), actions: [ IconButton( @@ -79,7 +78,6 @@ class _StretchTutorialViewState extends State { color: Theme.of(context).colorScheme.primary, child: Column( children: [ - // add a switch to control the `isActive` property of the `BadPostureSettings` ListTile( title: Text("Video showing the different stretches"), ), @@ -99,7 +97,6 @@ class _StretchTutorialViewState extends State { color: Theme.of(context).colorScheme.primary, child: Column( children: [ - // add a switch to control the `isActive` property of the `BadPostureSettings` ListTile( title: Text("Explaining the Tracking Colors"), ), @@ -186,7 +183,7 @@ class _StretchTutorialViewState extends State { ), ), - /// The head views used for meditation + /// The head views used for stretching FractionallySizedBox( widthFactor: 0.6, child: buildHeadView( @@ -244,7 +241,6 @@ class _StretchTutorialViewState extends State { color: Theme.of(context).colorScheme.primary, child: Column( children: [ - // add a switch to control the `isActive` property of the `BadPostureSettings` ListTile( title: Text("Explaining the Stretching Button"), ), @@ -317,7 +313,7 @@ class _StretchTutorialViewState extends State { ); } - // Creates the Button used to start the meditation + // Creates the Button used to start the stretch exercise Widget _buildMeditationButton(StretchViewModel neckStretchViewModel) { return Padding( padding: EdgeInsets.all(5), diff --git a/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart b/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart index bb3efc7..17b5464 100644 --- a/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart +++ b/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart @@ -24,10 +24,13 @@ class StretchViewModel extends ChangeNotifier { bool get isResting => _neckStretch.resting; - set stretchSettings(StretchSettings settings) => _neckStretch.settings = settings; + set stretchSettings(StretchSettings settings) => + _neckStretch.settings = settings; AttitudeTracker _attitudeTracker; OpenEarable _openEarable; + + /// The model class containing all information and logics needed to start and handle a guided neck stretch late NeckStretch _neckStretch; StretchViewModel(this._attitudeTracker, this._openEarable) { @@ -38,10 +41,7 @@ class StretchViewModel extends ChangeNotifier { this._neckStretch = NeckStretch(_openEarable, this); _attitudeTracker.listen((attitude) { _attitude = Attitude( - roll: attitude.roll, - pitch: attitude.pitch, - yaw: attitude.yaw - ); + roll: attitude.roll, pitch: attitude.pitch, yaw: attitude.yaw); notifyListeners(); }); } @@ -66,4 +66,4 @@ class StretchViewModel extends ChangeNotifier { _attitudeTracker.cancle(); super.dispose(); } -} \ No newline at end of file +} From b6b75afc9ad9376d53bf223939b7394b898de983 Mon Sep 17 00:00:00 2001 From: Polaris Date: Sun, 17 Dec 2023 17:17:15 +0100 Subject: [PATCH 037/104] Add some more comments/docs to the classes and methods + fix timers in the stretch_state model for counting down the remaining time --- .../lib/apps/neck_stretch/model/stretch_state.dart | 11 ++++++++--- .../lib/apps/neck_stretch/view/stretch_app_view.dart | 1 + .../neck_stretch/view/stretch_settings_view.dart | 5 +++-- .../apps/neck_stretch/view/stretch_tracker_view.dart | 4 ++++ .../neck_stretch/view/stretch_tutorial_view.dart | 4 ++++ .../neck_stretch/view_model/stretch_view_model.dart | 12 ++++++------ 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/open_earable/lib/apps/neck_stretch/model/stretch_state.dart b/open_earable/lib/apps/neck_stretch/model/stretch_state.dart index d22c31a..590a902 100644 --- a/open_earable/lib/apps/neck_stretch/model/stretch_state.dart +++ b/open_earable/lib/apps/neck_stretch/model/stretch_state.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:open_earable/apps/neck_stretch/view_model/stretch_view_model.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; -/// Enum for the Meditation States +/// Enum for the neck stretch states enum NeckStretchState { mainNeckStretch, leftNeckStretch, @@ -141,6 +141,7 @@ class NeckStretch { _viewModel.stopTracking(); } + /// Starts the countdown for restDuration void _startCountdown() { _restDurationTimer = Timer.periodic(Duration(seconds: 1), (timer) { _restDuration -= Duration(seconds: 1); @@ -152,6 +153,11 @@ class NeckStretch { _settings.state = state; // If you just swapped to this state, first rest for restingTime, then set new state if (_resting) { + /// If we don't restart the timer it results in a weird UI inconsistency + /// for displaying the _restDuration as then the restDuration is already + /// counted down when the next Timer hasn't started yet. + _restDurationTimer.cancel(); + _startCountdown(); _restDuration = _settings.restingTime; _currentTimer = Timer(_settings.restingTime, () { _resting = false; @@ -164,7 +170,7 @@ class NeckStretch { } } - /// Used to set the next meditation state and set the correct Timer + /// Used to set the next stretch state and set the correct Timers void _setNextState() { switch (_settings.state) { case NeckStretchState.noStretch: @@ -186,7 +192,6 @@ class NeckStretch { _openEarable.audioPlayer.jingle(2); return; case NeckStretchState.leftNeckStretch: - _resting = false; _settings.state = NeckStretchState.doneStretching; _currentTimer.cancel(); _restDurationTimer.cancel(); diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart index 9bd1bc6..fa9e783 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart @@ -67,6 +67,7 @@ class _StretchAppViewState extends State { appBar: AppBar( title: const Text("Guided Neck Stretch"), actions: [ + /// Settings button, only active when not stretching IconButton( onPressed: (this._viewModel.stretchState == NeckStretchState.noStretch || diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart index e3650c0..90b2ebb 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart @@ -50,6 +50,7 @@ class _SettingsViewState extends State { ); } + /// Creates the actual settings view Widget _buildSettingsView() { return SingleChildScrollView( child: Column( @@ -69,7 +70,7 @@ class _SettingsViewState extends State { color: Theme.of(context).colorScheme.primary, child: Column( children: [ - // add a switch to control the `isActive` property of the `BadPostureSettings` + /// Settings for all timers used ListTile( title: Text("Timers"), ), @@ -204,7 +205,7 @@ class _SettingsViewState extends State { /// Returns the new duration acquired from the Text. /// Checks if the string is valid (doesn't contain '-' or '.'. - /// Maximum allows time of 59 Minute 59 Seconds for UI consistency + /// Maximum allows time of 59 Minute 59 Seconds for UI consistency, if its more it sets 59 Minutes 59 Seconds Duration _getNewDuration(Duration duration, String newDuration) { if (newDuration.contains('.') || newDuration.contains('-')) return duration; diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart index e2cba04..6398b7a 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart @@ -65,6 +65,7 @@ class _StretchTrackerViewState extends State { title: const Text("Guided Neck Stretch"), actions: [ IconButton( + /// Settings button, only active when not stretching onPressed: (this._viewModel.stretchState == NeckStretchState.noStretch || this._viewModel.stretchState == @@ -93,6 +94,7 @@ class _StretchTrackerViewState extends State { this._viewModel.neckStretch.stopStretching(); } + /// Returns the TextSpan representing the Status Text at the top of the app TextSpan _getStatusText() { if (!_viewModel.isAvailable) return TextSpan( @@ -124,6 +126,8 @@ class _StretchTrackerViewState extends State { ]); } + /// Returns the button text displayed within the button. Used to also display + /// the remaining time of each phase Text _getButtonText() { if (!_viewModel.isTracking) return Text('Start Stretching'); diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart index d41fa3f..1320ab9 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart @@ -74,6 +74,7 @@ class _StretchTutorialViewState extends State { Widget _buildContentView(StretchViewModel neckStretchViewModel) { return Column( children: [ + /// Card with a YoutubePlayer containing a Video explaining all the stretches Card( color: Theme.of(context).colorScheme.primary, child: Column( @@ -93,6 +94,7 @@ class _StretchTutorialViewState extends State { ], ), ), + /// Card used to explain the tracking colors Card( color: Theme.of(context).colorScheme.primary, child: Column( @@ -152,6 +154,7 @@ class _StretchTutorialViewState extends State { ], ), ), + /// Example of the Tracker in Main Neck Stretch state Card( color: Theme.of(context).colorScheme.primary, child: Column( @@ -237,6 +240,7 @@ class _StretchTutorialViewState extends State { ], ), ), + /// Card explaining the stretching button Card( color: Theme.of(context).colorScheme.primary, child: Column( diff --git a/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart b/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart index 17b5464..6627b11 100644 --- a/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart +++ b/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart @@ -8,22 +8,19 @@ import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class StretchViewModel extends ChangeNotifier { Attitude _attitude = Attitude(); + /// Getters for the attitude-Tracker Attitude get attitude => _attitude; - bool get isTracking => _attitudeTracker.isTracking; - bool get isAvailable => _attitudeTracker.isAvailable; + /// Getters for the neck stretch settings and state NeckStretch get neckStretch => _neckStretch; - StretchSettings get stretchSettings => _neckStretch.settings; - NeckStretchState get stretchState => _neckStretch.settings.state; - Duration get restDuration => _neckStretch.restDuration; - bool get isResting => _neckStretch.resting; + /// Setter for the neck stretching settings set stretchSettings(StretchSettings settings) => _neckStretch.settings = settings; @@ -46,17 +43,20 @@ class StretchViewModel extends ChangeNotifier { }); } + /// Starts tracking of the openEarable void startTracking() { _attitudeTracker.start(); notifyListeners(); } + /// Stops tracking of the openEarable and resets the attitude for the headViews void stopTracking() { _attitudeTracker.stop(); _attitude = Attitude(); notifyListeners(); } + /// Used to calibrate the starting point for the head tracking void calibrate() { _attitudeTracker.calibrateToCurrentAttitude(); } From 034a17a128d4057c9ee6857b55d104e6da5b60a2 Mon Sep 17 00:00:00 2001 From: Polaris Date: Thu, 4 Jan 2024 15:45:22 +0100 Subject: [PATCH 038/104] Add settings for custom stretch thresholds --- .../neck_stretch/model/stretch_state.dart | 25 ++++-- .../view/stretch_settings_view.dart | 80 ++++++++++++++++++- .../view/stretch_tracker_view.dart | 8 +- 3 files changed, 98 insertions(+), 15 deletions(-) diff --git a/open_earable/lib/apps/neck_stretch/model/stretch_state.dart b/open_earable/lib/apps/neck_stretch/model/stretch_state.dart index 590a902..9584fbe 100644 --- a/open_earable/lib/apps/neck_stretch/model/stretch_state.dart +++ b/open_earable/lib/apps/neck_stretch/model/stretch_state.dart @@ -78,13 +78,22 @@ class StretchSettings { /// Time used for resting between each set Duration restingTime; + /// Angle used for stretching forward + double forwardStretchAngle; + + /// Angle used for stretching sideways + double sideStretchAngle; + /// The stretch settings containing duration timers and state - StretchSettings( - {this.state = NeckStretchState.noStretch, - required this.mainNeckRelaxation, - required this.leftNeckRelaxation, - required this.rightNeckRelaxation, - required this.restingTime}); + StretchSettings({ + this.state = NeckStretchState.noStretch, + required this.mainNeckRelaxation, + required this.leftNeckRelaxation, + required this.rightNeckRelaxation, + required this.restingTime, + required this.forwardStretchAngle, + required this.sideStretchAngle, + }); } /// Stores all data and functions to manage the guided neck meditation @@ -93,7 +102,9 @@ class NeckStretch { mainNeckRelaxation: Duration(seconds: 30), leftNeckRelaxation: Duration(seconds: 30), rightNeckRelaxation: Duration(seconds: 30), - restingTime: Duration(seconds: 5)); + restingTime: Duration(seconds: 5), + forwardStretchAngle: 45, + sideStretchAngle: 30); final OpenEarable _openEarable; final StretchViewModel _viewModel; diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart index 90b2ebb..123866d 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; import 'package:open_earable/apps/neck_stretch/view_model/stretch_view_model.dart'; import 'package:provider/provider.dart'; +import 'dart:core'; class SettingsView extends StatefulWidget { final StretchViewModel _viewModel; @@ -17,6 +18,8 @@ class _SettingsViewState extends State { late final TextEditingController _leftNeckDuration; late final TextEditingController _rightNeckDuration; late final TextEditingController _restingDuration; + late final TextEditingController _forwardStretchAngle; + late final TextEditingController _sideStretchAngle; late final StretchViewModel _viewModel; @@ -35,6 +38,8 @@ class _SettingsViewState extends State { .toString()); _restingDuration = TextEditingController( text: _viewModel.stretchSettings.restingTime.inSeconds.toString()); + _forwardStretchAngle = TextEditingController(text: _viewModel.stretchSettings.forwardStretchAngle.toString()); + _sideStretchAngle = TextEditingController(text: _viewModel.stretchSettings.sideStretchAngle.toString()); } @override @@ -78,7 +83,7 @@ class _SettingsViewState extends State { title: Text("Main Neck Relaxation Duration\n(in seconds)"), trailing: SizedBox( height: 37.0, - width: 52, + width: 62.0, child: TextField( controller: _mainNeckDuration, textAlign: TextAlign.end, @@ -102,7 +107,7 @@ class _SettingsViewState extends State { title: Text("Right Neck Relaxation Duration\n(in seconds)"), trailing: SizedBox( height: 37.0, - width: 52, + width: 62.0, child: TextField( controller: _rightNeckDuration, textAlign: TextAlign.end, @@ -126,7 +131,7 @@ class _SettingsViewState extends State { title: Text("Left Neck Relaxation Duration\n(in seconds)"), trailing: SizedBox( height: 37.0, - width: 52, + width: 62.0, child: TextField( controller: _leftNeckDuration, textAlign: TextAlign.end, @@ -151,7 +156,7 @@ class _SettingsViewState extends State { Text("Resting Duration between exercises\n(in seconds)"), trailing: SizedBox( height: 37.0, - width: 52, + width: 62.0, child: TextField( controller: _restingDuration, textAlign: TextAlign.end, @@ -174,6 +179,65 @@ class _SettingsViewState extends State { ], ), ), + Card( + color: Theme.of(context).colorScheme.primary, + child: Column( + children: [ + /// Settings for all timers used + ListTile( + title: Text("Stretch Thresholds"), + ), + ListTile( + title: Text("Main Neck Stretch Goal\n(as an angle)"), + trailing: SizedBox( + height: 37.0, + width: 62.0, + child: TextField( + controller: _forwardStretchAngle, + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(10), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: OutlineInputBorder(), + labelText: 'Angle', + filled: true, + labelStyle: TextStyle(color: Colors.black), + fillColor: Colors.white), + keyboardType: TextInputType.number, + onChanged: (_) { + _updateMeditationSettings(); + }, + ), + ), + ), + ListTile( + title: Text("Side Neck Stretch Goal\n(as an angle)"), + trailing: SizedBox( + height: 37.0, + width: 62.0, + child: TextField( + controller: _sideStretchAngle, + textAlign: TextAlign.end, + style: TextStyle(color: Colors.black), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(10), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: OutlineInputBorder(), + labelText: 'Angle', + filled: true, + labelStyle: TextStyle(color: Colors.black), + fillColor: Colors.white), + keyboardType: TextInputType.number, + onChanged: (_) { + _updateMeditationSettings(); + }, + ), + ), + ), + ], + ), + ), Padding( padding: EdgeInsets.all(8.0), child: Row(children: [ @@ -214,6 +278,12 @@ class _SettingsViewState extends State { return parsed > 3599 ? Duration(seconds: 3599) : Duration(seconds: parsed); } + double _parseAngle(double old, String input) { + if (input.contains('-')) return old; + + return double.parse(input); +} + /// Update the Meditation Settings according to values, if field is empty set that timer Duration to 0 void _updateMeditationSettings() { StretchSettings settings = _viewModel.stretchSettings; @@ -225,6 +295,8 @@ class _SettingsViewState extends State { _getNewDuration(settings.leftNeckRelaxation, _leftNeckDuration.text); settings.restingTime = _getNewDuration(settings.restingTime, _restingDuration.text); + settings.forwardStretchAngle = _parseAngle(settings.forwardStretchAngle, _forwardStretchAngle.text); + settings.sideStretchAngle = _parseAngle(settings.sideStretchAngle, _sideStretchAngle.text); } @override diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart index 6398b7a..a50a57f 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart @@ -215,15 +215,15 @@ class _StretchTrackerViewState extends State { /// Visible Widgets for the main stretch _buildStretchViews( - NeckStretchState.mainNeckStretch, neckStretchViewModel, 7.0, 50.0), + NeckStretchState.mainNeckStretch, neckStretchViewModel, 7.0, (neckStretchViewModel.stretchSettings.forwardStretchAngle % 180)), /// Visible Widgets for the right stretch _buildStretchViews( - NeckStretchState.rightNeckStretch, neckStretchViewModel, 30.0, 15.0), + NeckStretchState.rightNeckStretch, neckStretchViewModel, (neckStretchViewModel.stretchSettings.sideStretchAngle % 180), 15.0), /// Visible Widgets for the left stretch _buildStretchViews( - NeckStretchState.leftNeckStretch, neckStretchViewModel, 30.0, 15.0), + NeckStretchState.leftNeckStretch, neckStretchViewModel, (neckStretchViewModel.stretchSettings.sideStretchAngle % 180), 15.0), ]; } @@ -256,7 +256,7 @@ class _StretchTrackerViewState extends State { state.assetPathHeadSide, state.assetPathNeckSide, Alignment.center.add(Alignment(0, 0.3)), - neckStretchViewModel.attitude.pitch, + -neckStretchViewModel.attitude.pitch, sideThreshold, state), ], From d6a666b59d5462be94a63150ecb4d42888995203 Mon Sep 17 00:00:00 2001 From: Polaris Date: Thu, 4 Jan 2024 16:03:54 +0100 Subject: [PATCH 039/104] Fix the stretch arc painter to have the start and ending at the right point no matter the angle --- .../lib/apps/neck_stretch/view/stretch_arc_painter.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart index 36bd76b..db74dda 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart @@ -112,7 +112,7 @@ class StretchArcPainter extends CustomPainter { case NeckStretchState.rightNeckStretch: return startAngle - threshold; case NeckStretchState.leftNeckStretch: - return startAngle - threshold - pi / 2 - 2 / 18 * pi; + return startAngle - (0.775 * pi); default: return startAngle - threshold; } @@ -124,7 +124,7 @@ class StretchArcPainter extends CustomPainter { switch (this.stretchState) { case NeckStretchState.rightNeckStretch: case NeckStretchState.leftNeckStretch: - return 2 * threshold + pi / 2 + 2 / 18 * pi; + return threshold + (0.775 * pi); default: return 2 * threshold; } @@ -132,7 +132,7 @@ class StretchArcPainter extends CustomPainter { switch (this.stretchState) { case NeckStretchState.mainNeckStretch: - return 2 * threshold + pi / 2 + 1 / 36 * pi; + return threshold + (0.8 * pi); default: return 2 * threshold; } From b3af40a5a5895ef1dbd247f6a2158900ba009605 Mon Sep 17 00:00:00 2001 From: Polaris Date: Thu, 4 Jan 2024 16:05:34 +0100 Subject: [PATCH 040/104] Add all left over files for submission --- README-Abgabe.txt | 2 + open_earable/devtools_options.yaml | 1 + open_earable/pubspec.lock | 62 +++++++++++++------ open_earable/windows/flutter/CMakeLists.txt | 7 ++- .../windows/runner/flutter_window.cpp | 5 ++ 5 files changed, 57 insertions(+), 20 deletions(-) create mode 100644 README-Abgabe.txt create mode 100644 open_earable/devtools_options.yaml diff --git a/README-Abgabe.txt b/README-Abgabe.txt new file mode 100644 index 0000000..7c5d371 --- /dev/null +++ b/README-Abgabe.txt @@ -0,0 +1,2 @@ +Man kann den ganzen progress mit commits auch auf meinem GitHub nachschauen: + https://github.com/BasicallyPolaris/oe-app \ No newline at end of file diff --git a/open_earable/devtools_options.yaml b/open_earable/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/open_earable/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/open_earable/pubspec.lock b/open_earable/pubspec.lock index bac6f8b..18e4d94 100644 --- a/open_earable/pubspec.lock +++ b/open_earable/pubspec.lock @@ -215,6 +215,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.4" + flutter_inappwebview: + dependency: transitive + description: + name: flutter_inappwebview + sha256: d198297060d116b94048301ee6749cd2e7d03c1f2689783f52d210a6b7aba350 + url: "https://pub.dev" + source: hosted + version: "5.8.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -350,7 +358,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: eaf50163a03f24822f087a822c767c284f5b3637 + resolved-ref: "81cf0ec5b05f7a0e6bfae12ded21319c30f28216" url: "https://github.com/OpenEarable/open_earable_flutter.git" source: git version: "0.0.1" @@ -430,50 +438,58 @@ packages: dependency: transitive description: name: permission_handler - sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 + sha256: "860c6b871c94c78e202dc69546d4d8fd84bd59faeb36f8fb9888668a53ff4f78" url: "https://pub.dev" source: hosted - version: "10.4.5" + version: "11.1.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" + sha256: "2f1bec180ee2f5665c22faada971a8f024761f632e93ddc23310487df52dcfa6" url: "https://pub.dev" source: hosted - version: "10.3.6" + version: "12.0.1" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + sha256: "1a816084338ada8d574b1cb48390e6e8b19305d5120fe3a37c98825bacc78306" + url: "https://pub.dev" + source: hosted + version: "9.2.0" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "11b762a8c123dced6461933a88ea1edbbe036078c3f9f41b08886e678e7864df" url: "https://pub.dev" source: hosted - version: "9.1.4" + version: "0.1.0+2" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + sha256: d87349312f7eaf6ce0adaf668daf700ac5b06af84338bd8b8574dfbd93ffe1a1 url: "https://pub.dev" source: hosted - version: "3.12.0" + version: "4.0.2" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + sha256: "1e8640c1e39121128da6b816d236e714d2cf17fac5a105dd6acdd3403a628004" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.2.0" petitparser: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" platform: dependency: transitive description: @@ -661,10 +677,10 @@ packages: dependency: transitive description: name: win32 - sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f" + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.1.1" xdg_directories: dependency: transitive description: @@ -677,10 +693,10 @@ packages: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" yaml: dependency: transitive description: @@ -689,6 +705,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + youtube_player_flutter: + dependency: "direct main" + description: + name: youtube_player_flutter + sha256: "72d487e1a1b9155a2dc9d448c137380791101a0ff623723195275ac275ac6942" + url: "https://pub.dev" + source: hosted + version: "8.1.2" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" - flutter: ">=3.7.0" + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/open_earable/windows/flutter/CMakeLists.txt b/open_earable/windows/flutter/CMakeLists.txt index 930d207..903f489 100644 --- a/open_earable/windows/flutter/CMakeLists.txt +++ b/open_earable/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/open_earable/windows/runner/flutter_window.cpp b/open_earable/windows/runner/flutter_window.cpp index b25e363..955ee30 100644 --- a/open_earable/windows/runner/flutter_window.cpp +++ b/open_earable/windows/runner/flutter_window.cpp @@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() { this->Show(); }); + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; } From eb239a16c4a17ebaebf866f2acfb8679f6620308 Mon Sep 17 00:00:00 2001 From: Polaris Date: Thu, 4 Jan 2024 16:21:36 +0100 Subject: [PATCH 041/104] Add new version of Readme-Abgabe.txt --- README-Abgabe.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README-Abgabe.txt b/README-Abgabe.txt index 7c5d371..00077a9 100644 --- a/README-Abgabe.txt +++ b/README-Abgabe.txt @@ -1,2 +1,8 @@ -Man kann den ganzen progress mit commits auch auf meinem GitHub nachschauen: - https://github.com/BasicallyPolaris/oe-app \ No newline at end of file +Da das ganze Projekt zu groß für die Illias-Upload-Grenze sind habe ich hier nur meine lib und assets directory rein gemacht +und meine angepasste pubspec. Diese beinhaltet nun noch eine library für Youtube-Wiedergabe in der App und referenzen für meine neuen assets. + +Falls Sie das ganze Repository herunterladen wollen können sie einfach das Repository von meinem GitHub Repo clonen. +Falls Sie hiermit Probleme haben könnnen Sie mich immer per mail (ukqho@student.kit.edu) erreichen oder auf Discord (Soheel) + +Man kann den ganzen Progress mit commits auch auf meinem GitHub nachschauen: + https://github.com/BasicallyPolaris/oe-app From 288f2fffaf883a703bb55816cd8befcfb8728542 Mon Sep 17 00:00:00 2001 From: Polaris Date: Thu, 4 Jan 2024 23:32:24 +0100 Subject: [PATCH 042/104] Add a stats view to display maximum stretching angle and the stretch duration over the threshold --- .../neck_stretch/model/stretch_state.dart | 37 +++++++++ .../neck_stretch/view/stretch_app_view.dart | 13 ++- .../view/stretch_settings_view.dart | 1 + .../neck_stretch/view/stretch_stats_view.dart | 83 +++++++++++++++++++ .../view_model/stretch_view_model.dart | 67 +++++++++++++++ open_earable/lib/main.dart | 1 + 6 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 open_earable/lib/apps/neck_stretch/view/stretch_stats_view.dart diff --git a/open_earable/lib/apps/neck_stretch/model/stretch_state.dart b/open_earable/lib/apps/neck_stretch/model/stretch_state.dart index 9584fbe..606e829 100644 --- a/open_earable/lib/apps/neck_stretch/model/stretch_state.dart +++ b/open_earable/lib/apps/neck_stretch/model/stretch_state.dart @@ -62,6 +62,43 @@ extension NeckStretchStateExtension on NeckStretchState { } } +/// Stores all data for a stretching session +class StretchStats { + /// Maximum angle reached when doing the main neck stretch + double maxMainAngle; + + /// Maximum angle reached on the left neck stretch + double maxLeftAngle; + + /// Maximum angle reached on the right neck stretch + double maxRightAngle; + + /// Duration over set main angle threshold + double mainStretchDuration; + + /// Duration over set side angle threshold + double leftStretchDuration; + double rightStretchDuration; + + + StretchStats( + {this.maxMainAngle = 0, + this.maxLeftAngle = 0, + this.maxRightAngle = 0, + this.mainStretchDuration = 0, + this.leftStretchDuration = 0, + this.rightStretchDuration = 0}); + + void clear() { + this.maxMainAngle = 0; + this.maxLeftAngle = 0; + this.maxRightAngle = 0; + this.mainStretchDuration = 0; + this.leftStretchDuration = 0; + this.rightStretchDuration = 0; + } +} + /// Stores all settings needed to manage a stretching session class StretchSettings { NeckStretchState state; diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart index fa9e783..adc175c 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart @@ -7,6 +7,7 @@ import 'package:open_earable/apps/neck_stretch/view/stretch_tutorial_view.dart'; import 'package:open_earable/apps/neck_stretch/view_model/stretch_view_model.dart'; import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; import 'package:open_earable/apps/neck_stretch/view/stretch_settings_view.dart'; +import 'package:open_earable/apps/neck_stretch/view/stretch_stats_view.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; @@ -37,12 +38,22 @@ class _StretchAppViewState extends State { iconData: Icons.play_circle, title: "Start Stretching", description: "Dive directly into the guided neck stretch!", + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StretchTrackerView(this._viewModel))); + }), + AppInfo( + iconData: Icons.info, + title: "Last Stretch Stats", + description: "Your stats about your last stretch.", onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => - StretchTrackerView(this._viewModel))); + StretchStatsView(this._viewModel))); }), AppInfo( iconData: Icons.help, diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart index 123866d..9933288 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_settings_view.dart @@ -11,6 +11,7 @@ class SettingsView extends StatefulWidget { @override State createState() => _SettingsViewState(); + } class _SettingsViewState extends State { diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_stats_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_stats_view.dart new file mode 100644 index 0000000..d1bd31f --- /dev/null +++ b/open_earable/lib/apps/neck_stretch/view/stretch_stats_view.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; +import 'package:open_earable/apps/neck_stretch/view_model/stretch_view_model.dart'; + +class StretchStatsView extends StatefulWidget { + final StretchViewModel _viewModel; + + StretchStatsView(this._viewModel); + + @override + State createState() => _StretchStatsViewState(); +} + +class _StretchStatsViewState extends State { + late StretchStats _stats; + + @override + void initState() { + super.initState(); + this._stats = widget._viewModel.stretchStats; + } + + @override + Widget build(BuildContext context) { + print("STRETCH DURATION: ${_stats.mainStretchDuration}"); + return Scaffold( + appBar: AppBar(title: const Text("Stretch Stats")), + body: SingleChildScrollView( + child: Column( + children: [ + Card( + color: Theme.of(context).colorScheme.primary, + child: Column( + children: [ + ListTile( + title: Text("Maximum Stretch Angle Achieved"), + ), + ListTile( + title: Text("Main Stretch Max Angle:"), + trailing: Text( + "${(_stats.maxMainAngle * 180 / 3.14).abs().toStringAsFixed(0)}°"), + ), + ListTile( + title: Text("Right Stretch Max Angle:"), + trailing: Text( + "${(_stats.maxRightAngle * 180 / 3.14).abs().toStringAsFixed(0)}°"), + ), + ListTile( + title: Text("Left Stretch Max Angle:"), + trailing: Text( + "${(_stats.maxLeftAngle * 180 / 3.14).abs().toStringAsFixed(0)}°"), + ), + ], + ), + ), + Card( + color: Theme.of(context).colorScheme.primary, + child: Column( + children: [ + ListTile( + title: Text("Stretch Duration over Threshold Angle"), + ), + ListTile( + title: Text("Main Neck Stretch Duration"), + trailing: Text("${_stats.mainStretchDuration.toStringAsFixed(2)} s"), + ), + ListTile( + title: Text("Right Neck Stretch Duration"), + trailing: Text("${_stats.rightStretchDuration.toStringAsFixed(2)} s"), + ), + ListTile( + title: Text("Left Neck Stretch Duration"), + trailing: Text("${_stats.leftStretchDuration.toStringAsFixed(2)} s"), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart b/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart index 6627b11..3efc2a1 100644 --- a/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart +++ b/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart @@ -1,3 +1,5 @@ +import "dart:core"; +import "dart:async"; import "package:flutter/material.dart"; import "package:open_earable/apps/posture_tracker/model/attitude.dart"; import "package:open_earable/apps/posture_tracker/model/attitude_tracker.dart"; @@ -10,16 +12,24 @@ class StretchViewModel extends ChangeNotifier { /// Getters for the attitude-Tracker Attitude get attitude => _attitude; + bool get isTracking => _attitudeTracker.isTracking; + bool get isAvailable => _attitudeTracker.isAvailable; /// Getters for the neck stretch settings and state NeckStretch get neckStretch => _neckStretch; + StretchSettings get stretchSettings => _neckStretch.settings; + NeckStretchState get stretchState => _neckStretch.settings.state; + Duration get restDuration => _neckStretch.restDuration; + bool get isResting => _neckStretch.resting; + StretchStats get stretchStats => _stretchStats; + /// Setter for the neck stretching settings set stretchSettings(StretchSettings settings) => _neckStretch.settings = settings; @@ -29,6 +39,8 @@ class StretchViewModel extends ChangeNotifier { /// The model class containing all information and logics needed to start and handle a guided neck stretch late NeckStretch _neckStretch; + late StretchStats _stretchStats; + late Timer _settingsTracker; StretchViewModel(this._attitudeTracker, this._openEarable) { _attitudeTracker.didChangeAvailability = (_) { @@ -36,6 +48,8 @@ class StretchViewModel extends ChangeNotifier { }; this._neckStretch = NeckStretch(_openEarable, this); + this._stretchStats = StretchStats(); + _attitudeTracker.listen((attitude) { _attitude = Attitude( roll: attitude.roll, pitch: attitude.pitch, yaw: attitude.yaw); @@ -46,6 +60,10 @@ class StretchViewModel extends ChangeNotifier { /// Starts tracking of the openEarable void startTracking() { _attitudeTracker.start(); + _stretchStats.clear(); + _settingsTracker = Timer.periodic(new Duration(milliseconds: 10), (timer) { + _setStretchStats(); + }); notifyListeners(); } @@ -53,6 +71,7 @@ class StretchViewModel extends ChangeNotifier { void stopTracking() { _attitudeTracker.stop(); _attitude = Attitude(); + _settingsTracker.cancel(); notifyListeners(); } @@ -66,4 +85,52 @@ class StretchViewModel extends ChangeNotifier { _attitudeTracker.cancle(); super.dispose(); } + + /// Set the stretch stats according to current stretch state + void _setStretchStats() { + /// If you are resting, don't track, only last refresh + if (this.isResting) { + return; + } + const toAngle = 180 / 3.14; + switch (_neckStretch.settings.state) { + case NeckStretchState.mainNeckStretch: + _stretchStats.maxMainAngle = + _attitude.pitch > _stretchStats.maxMainAngle + ? _attitude.pitch + : _stretchStats.maxMainAngle; + + /// Sets the stretch duration + if ((_attitude.pitch * toAngle) >= + _neckStretch.settings.forwardStretchAngle) { + _stretchStats.mainStretchDuration += 0.01; + } + return; + case NeckStretchState.rightNeckStretch: + _stretchStats.maxRightAngle = + -_attitude.roll > _stretchStats.maxRightAngle + ? -_attitude.roll + : _stretchStats.maxRightAngle; + + /// Sets the stretch duration + if ((-_attitude.roll * toAngle) >= + _neckStretch.settings.sideStretchAngle) { + _stretchStats.rightStretchDuration += 0.01; + } + return; + case NeckStretchState.leftNeckStretch: + _stretchStats.maxLeftAngle = _attitude.roll > _stretchStats.maxLeftAngle + ? _attitude.roll + : _stretchStats.maxLeftAngle; + + /// Sets the stretch duration + if (_attitude.roll * toAngle >= + _neckStretch.settings.sideStretchAngle) { + _stretchStats.leftStretchDuration += 0.01; + } + return; + default: + return; + } + } } diff --git a/open_earable/lib/main.dart b/open_earable/lib/main.dart index 6aca296..f1dd72e 100644 --- a/open_earable/lib/main.dart +++ b/open_earable/lib/main.dart @@ -16,6 +16,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: '🦻 OpenEarable', + debugShowCheckedModeBanner: false, theme: ThemeData( useMaterial3: false, colorScheme: ColorScheme( From 80c3da548d6bb5ca772b654a0aec9d1675f8b353 Mon Sep 17 00:00:00 2001 From: Polaris Date: Fri, 5 Jan 2024 01:46:53 +0100 Subject: [PATCH 043/104] Add some more documentation --- .../neck_stretch/view/stretch_app_view.dart | 4 +- .../view/stretch_arc_painter.dart | 5 ++- .../neck_stretch/view/stretch_stats_view.dart | 3 +- .../view/stretch_tracker_view.dart | 44 +++++++++---------- .../view/stretch_tutorial_view.dart | 4 +- .../view_model/stretch_view_model.dart | 14 +++--- 6 files changed, 38 insertions(+), 36 deletions(-) diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart index adc175c..985e3a8 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_app_view.dart @@ -22,7 +22,7 @@ class StretchAppView extends StatefulWidget { } /// This class is the initial view you get when opening the Stretch-App -/// It refers to the tutorial page and the actual stretching page +/// It refers to all the submodules of the stretching app class _StretchAppViewState extends State { late final StretchViewModel _viewModel; @@ -46,7 +46,7 @@ class _StretchAppViewState extends State { }), AppInfo( iconData: Icons.info, - title: "Last Stretch Stats", + title: "Stretch Stats", description: "Your stats about your last stretch.", onTap: () { Navigator.push( diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart index db74dda..76fb60d 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart @@ -72,6 +72,7 @@ class StretchArcPainter extends CustomPainter { Rect.fromCircle(center: center, radius: radius), // create a rectangle from the center and radius startAngle + angle.sign * angleThreshold, // start angle + // If you are facing the wrong direction you don't need to draw this !_isWrongStretchDirection() ? angle.sign * (angle.abs() - angleThreshold) : 0, // sweep angle ); } @@ -124,7 +125,7 @@ class StretchArcPainter extends CustomPainter { switch (this.stretchState) { case NeckStretchState.rightNeckStretch: case NeckStretchState.leftNeckStretch: - return threshold + (0.775 * pi); + return threshold + (0.775 * pi); // Will place the dark grey area till the start of the neck default: return 2 * threshold; } @@ -132,7 +133,7 @@ class StretchArcPainter extends CustomPainter { switch (this.stretchState) { case NeckStretchState.mainNeckStretch: - return threshold + (0.8 * pi); + return threshold + (0.8 * pi); // Will place the dark grey area till the start of the neck default: return 2 * threshold; } diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_stats_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_stats_view.dart index d1bd31f..b98cdf4 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_stats_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_stats_view.dart @@ -11,7 +11,9 @@ class StretchStatsView extends StatefulWidget { State createState() => _StretchStatsViewState(); } +/// Stateful Widget to display the current stretching stats of the most recent stretch class _StretchStatsViewState extends State { + /// The stretching stats late StretchStats _stats; @override @@ -22,7 +24,6 @@ class _StretchStatsViewState extends State { @override Widget build(BuildContext context) { - print("STRETCH DURATION: ${_stats.mainStretchDuration}"); return Scaffold( appBar: AppBar(title: const Text("Stretch Stats")), body: SingleChildScrollView( diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart index a50a57f..e34edfc 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tracker_view.dart @@ -13,27 +13,27 @@ class StretchTrackerView extends StatefulWidget { @override State createState() => _StretchTrackerViewState(); -} -/// Builds the actual head views using the StretchRollView -Widget buildHeadView( - String headAssetPath, - String neckAssetPath, - AlignmentGeometry headAlignment, - double roll, - double angleThreshold, - NeckStretchState state) { - return Padding( - padding: const EdgeInsets.all(5), - child: StretchRollView( - roll: roll, - angleThreshold: angleThreshold * 3.14 / 180, - headAssetPath: headAssetPath, - neckAssetPath: neckAssetPath, - headAlignment: headAlignment, - stretchState: state, - ), - ); + /// Builds the actual head views using the StretchRollView + static Widget buildHeadView( + String headAssetPath, + String neckAssetPath, + AlignmentGeometry headAlignment, + double roll, + double angleThreshold, + NeckStretchState state) { + return Padding( + padding: const EdgeInsets.all(5), + child: StretchRollView( + roll: roll, + angleThreshold: angleThreshold * 3.14 / 180, + headAssetPath: headAssetPath, + neckAssetPath: neckAssetPath, + headAlignment: headAlignment, + stretchState: state, + ), + ); + } } class _StretchTrackerViewState extends State { @@ -245,14 +245,14 @@ class _StretchTrackerViewState extends State { visible: visibility, child: Column( children: [ - buildHeadView( + StretchTrackerView.buildHeadView( state.assetPathHeadFront, state.assetPathNeckFront, Alignment.center.add(Alignment(0, 0.3)), neckStretchViewModel.attitude.roll, frontThreshold, state), - buildHeadView( + StretchTrackerView.buildHeadView( state.assetPathHeadSide, state.assetPathNeckSide, Alignment.center.add(Alignment(0, 0.3)), diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart index 1320ab9..e533665 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart @@ -189,7 +189,7 @@ class _StretchTutorialViewState extends State { /// The head views used for stretching FractionallySizedBox( widthFactor: 0.6, - child: buildHeadView( + child: StretchTrackerView.buildHeadView( NeckStretchState.mainNeckStretch.assetPathHeadFront, NeckStretchState.mainNeckStretch.assetPathNeckFront, Alignment.center.add(Alignment(0, 0.3)), @@ -199,7 +199,7 @@ class _StretchTutorialViewState extends State { ), FractionallySizedBox( widthFactor: 0.6, - child: buildHeadView( + child: StretchTrackerView.buildHeadView( NeckStretchState.mainNeckStretch.assetPathHeadSide, NeckStretchState.mainNeckStretch.assetPathNeckSide, Alignment.center.add(Alignment(0, 0.3)), diff --git a/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart b/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart index 3efc2a1..60219a9 100644 --- a/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart +++ b/open_earable/lib/apps/neck_stretch/view_model/stretch_view_model.dart @@ -4,7 +4,6 @@ import "package:flutter/material.dart"; import "package:open_earable/apps/posture_tracker/model/attitude.dart"; import "package:open_earable/apps/posture_tracker/model/attitude_tracker.dart"; import 'package:open_earable/apps/neck_stretch/model/stretch_state.dart'; - import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class StretchViewModel extends ChangeNotifier { @@ -17,7 +16,7 @@ class StretchViewModel extends ChangeNotifier { bool get isAvailable => _attitudeTracker.isAvailable; - /// Getters for the neck stretch settings and state + /// Getters for the neck stretch settings, state and stats NeckStretch get neckStretch => _neckStretch; StretchSettings get stretchSettings => _neckStretch.settings; @@ -40,6 +39,7 @@ class StretchViewModel extends ChangeNotifier { /// The model class containing all information and logics needed to start and handle a guided neck stretch late NeckStretch _neckStretch; late StretchStats _stretchStats; + /// Timer that is used to track the current stretching stats, called every 0.01s late Timer _settingsTracker; StretchViewModel(this._attitudeTracker, this._openEarable) { @@ -57,17 +57,17 @@ class StretchViewModel extends ChangeNotifier { }); } - /// Starts tracking of the openEarable + /// Starts tracking of using OpenEarable void startTracking() { _attitudeTracker.start(); _stretchStats.clear(); _settingsTracker = Timer.periodic(new Duration(milliseconds: 10), (timer) { - _setStretchStats(); + _trackStretchStats(); }); notifyListeners(); } - /// Stops tracking of the openEarable and resets the attitude for the headViews + /// Stops tracking of the OpenEarable and resets the attitude for the headViews void stopTracking() { _attitudeTracker.stop(); _attitude = Attitude(); @@ -86,8 +86,8 @@ class StretchViewModel extends ChangeNotifier { super.dispose(); } - /// Set the stretch stats according to current stretch state - void _setStretchStats() { + /// Track the stretch stats according to current stretch state + void _trackStretchStats() { /// If you are resting, don't track, only last refresh if (this.isResting) { return; From c48cef7e1125c740c7752338d0c185ed62da6e53 Mon Sep 17 00:00:00 2001 From: Polaris Date: Fri, 5 Jan 2024 20:17:57 +0100 Subject: [PATCH 044/104] Pubspec changes --- open_earable/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open_earable/pubspec.lock b/open_earable/pubspec.lock index 57d2390..1e2ef49 100644 --- a/open_earable/pubspec.lock +++ b/open_earable/pubspec.lock @@ -722,5 +722,5 @@ packages: source: hosted version: "8.1.2" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.16.0" From 248f5419a747f06956c2b13914f860130a46b88e Mon Sep 17 00:00:00 2001 From: Polaris Date: Fri, 5 Jan 2024 20:26:58 +0100 Subject: [PATCH 045/104] Fixed the stretch tutorial view to use the right api measurements for the heads --- .../lib/apps/neck_stretch/view/stretch_tutorial_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart index e533665..d496db9 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart @@ -193,7 +193,7 @@ class _StretchTutorialViewState extends State { NeckStretchState.mainNeckStretch.assetPathHeadFront, NeckStretchState.mainNeckStretch.assetPathNeckFront, Alignment.center.add(Alignment(0, 0.3)), - neckStretchViewModel.attitude.pitch, + neckStretchViewModel.attitude.roll, 30, NeckStretchState.mainNeckStretch), ), @@ -203,7 +203,7 @@ class _StretchTutorialViewState extends State { NeckStretchState.mainNeckStretch.assetPathHeadSide, NeckStretchState.mainNeckStretch.assetPathNeckSide, Alignment.center.add(Alignment(0, 0.3)), - neckStretchViewModel.attitude.pitch, + -neckStretchViewModel.attitude.pitch, 50, NeckStretchState.mainNeckStretch), ), From 2fe25b0fc8cbad27f3ba7e8b7fefccb74990c7bc Mon Sep 17 00:00:00 2001 From: Philipp Ochs Date: Mon, 8 Jan 2024 13:32:27 +0100 Subject: [PATCH 046/104] Implemented powernapping Timer --- open_earable/lib/apps/ufiiu/home_screen.dart | 124 ++++++++++++++++++ open_earable/lib/apps/ufiiu/interact.dart | 27 ++++ .../lib/apps/ufiiu/movementTracker.dart | 106 +++++++++++++++ .../lib/apps/ufiiu/sensor_datatypes.dart | 37 ++++++ open_earable/lib/apps/ufiiu/timerscreen.dart | 120 +++++++++++++++++ open_earable/lib/apps_tab.dart | 12 ++ 6 files changed, 426 insertions(+) create mode 100644 open_earable/lib/apps/ufiiu/home_screen.dart create mode 100644 open_earable/lib/apps/ufiiu/interact.dart create mode 100644 open_earable/lib/apps/ufiiu/movementTracker.dart create mode 100644 open_earable/lib/apps/ufiiu/sensor_datatypes.dart create mode 100644 open_earable/lib/apps/ufiiu/timerscreen.dart diff --git a/open_earable/lib/apps/ufiiu/home_screen.dart b/open_earable/lib/apps/ufiiu/home_screen.dart new file mode 100644 index 0000000..1279774 --- /dev/null +++ b/open_earable/lib/apps/ufiiu/home_screen.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:open_earable/apps/ufiiu/timerscreen.dart'; +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; + +import 'interact.dart'; + + +/// Homescreen class for the movement timer application. +class SleepHomeScreen extends StatefulWidget { + final OpenEarable _openEarable; + SleepHomeScreen(this._openEarable); + @override + _HomeScreenState createState() => _HomeScreenState(_openEarable); +} + +/// State for the HomeScreenApplication. +/// +/// Needs the [OpenEarable]-Object to interact. +class _HomeScreenState extends State { + + final OpenEarable _openEarable; + + //Constructor + _HomeScreenState(this._openEarable); + + //Bottom-Navigation-Bar index. + int _currentIndex = 0; + + + + //Build main Widget. + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Timer App'), + ), + + //Body for the widget + body: _getBody(), + + //Bottom Navigation Bar + bottomNavigationBar: BottomNavigationBar( + currentIndex: _currentIndex, + onTap: _onNavBarItemTapped, + items: [ + BottomNavigationBarItem( + icon: Icon(Icons.home), + label: 'Home', + ), + BottomNavigationBarItem( + icon: Icon(Icons.timer), + label: 'Timer', + ), + BottomNavigationBarItem( + icon: Icon(Icons.info), + label: 'Info', + ), + ], + ), + ); + } + + ///Body-Widget for Main Widget. + Widget _getBody() { + switch (_currentIndex) { + case 0: + + //HomeScreenTab + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + //Image-Source + Text( + 'Mit der Powernapping App können Sie einen Timer starten, der ganz automatisch an Ihren Bewegungen erkennt, ' + 'wann Sie wirklich eingeschlafen sind. Der Timer wird automatisch restartet, wenn Sie sich bewegen, ' + 'so können Sie effektiv powernappen und eine gemütliche Position finden ohne das schon die Zeit abläuft!', + style: TextStyle(fontSize: 24), + ), + SizedBox(height: 20), + ], + ) + ); + case 1: + + //Timer Tab + return Center( + child: Text('Wird weitergeleitet...') + ); + case 2: + + //Information Tab + return Center( + child: Text('Diese Sub-App wurde entwickelt von: Philipp Ochs, Matrikelnummer 2284828'), + ); + default: + + //Default + return Center( + child: Text('Ungültiger Index'), + ); + + } + return Container(); + } + + ///Navigation-Bar interaction + /// + /// [Index] is the tab index activated. + void _onNavBarItemTapped(int index) { + setState(() { + _currentIndex = index; + if (index == 1) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => TimerScreen(Interact(_openEarable)), + ), + ); + } + }); + } +} \ No newline at end of file diff --git a/open_earable/lib/apps/ufiiu/interact.dart b/open_earable/lib/apps/ufiiu/interact.dart new file mode 100644 index 0000000..18f9515 --- /dev/null +++ b/open_earable/lib/apps/ufiiu/interact.dart @@ -0,0 +1,27 @@ +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; + +///Interaction class for the earable. All actions executed on the earable are accessible through this class. +///For example rings or led colors. +class Interact { + + final OpenEarable _openEarable; + + //Constructor + Interact(this._openEarable); + + + //Getter for the Earable + OpenEarable getEarable() { + return _openEarable; + } + + + ///Lets the OpenEarable play the jingel-ID: '1'. + void ring() { + try { + _openEarable.audioPlayer.jingle(1); + } catch (e) { + print('ERROR: Jingle konnte nicht gespielt werden!'); + } + } +} \ No newline at end of file diff --git a/open_earable/lib/apps/ufiiu/movementTracker.dart b/open_earable/lib/apps/ufiiu/movementTracker.dart new file mode 100644 index 0000000..373f5d4 --- /dev/null +++ b/open_earable/lib/apps/ufiiu/movementTracker.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import 'package:open_earable/apps/ufiiu/interact.dart'; +import 'package:open_earable/apps/ufiiu/sensor_datatypes.dart'; +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; + +/// Movement Tracker has lgoic for timer & movement validation. +class MovementTracker { + + //Incetaction variables + final Interact _interact; + late final OpenEarable _openEarable; + + Timer? _timer; + + //Stream Subscription + StreamSubscription>? _subscription; + + + //Constructor + MovementTracker(this._interact) { + this._openEarable = _interact.getEarable(); + } + + ///Start Subscription and reset timer. + /// + /// Input: [minutes] for the time before the ring. + /// Input: [updateText] as an void callback function for the textupdate. + void start(int minutes, void Function(SensorDataType s) updateText) { + + //Timer (re-)start + stop(); + _startTimer(minutes); + + //Sets sensor config + _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); + + //Starts listening to the subscription + _subscription = _openEarable.sensorManager.subscribeToSensorData(0).listen((event) { + + //Display update callback + updateText(Gyroscope(event)); + + //Timer update + _update(Gyroscope(event), minutes); + }); + } + + ///(Re-)Starts timer and cancels subscription & calls ring() when finished. + /// + /// Input: int [minutes] for the timer length. + void _startTimer(int minutes) { + _timer?.cancel(); + _timer = Timer(Duration(minutes: minutes), () { + //End of timer: + _interact.ring(); + stop(); + }); + } + + /// Cancels timer and subscription to the Earable sensor stream. + void stop() { + _timer?.cancel(); + _subscription?.cancel(); + } + + /// Update method for restarting timer when movement is tracked. + /// + /// Uses the [SensorDataType] to validate update and int [minutes] to restart the timer. + void _update(SensorDataType dt, int minutes) { + if(_validMovement(dt)) { + _timer?.cancel(); + _startTimer(minutes); + } + } + + /// Validates wether the given sensordata could be interpretet as a movement. + /// + /// Input: [SensorDataType] with the data to be validated. + bool _validMovement(SensorDataType dt) { + + Gyroscope gyro; + + if(dt is Gyroscope) { + gyro = dt; + + //Threshold validating for gyroscope data. + if(gyro.x.abs() > 5 + || gyro.y.abs() > 5 + || gyro.z.abs() > 5 + ) { + return true; + } + } + return false; + } + + ///Sensor Config for the earable. + OpenEarableSensorConfig _buildSensorConfig() { + return OpenEarableSensorConfig( + sensorId: 0, + samplingRate: 30, + latency: 0 + ); + } +} \ No newline at end of file diff --git a/open_earable/lib/apps/ufiiu/sensor_datatypes.dart b/open_earable/lib/apps/ufiiu/sensor_datatypes.dart new file mode 100644 index 0000000..8bff3da --- /dev/null +++ b/open_earable/lib/apps/ufiiu/sensor_datatypes.dart @@ -0,0 +1,37 @@ +/// Sensor Data is the super data typ for possible datatypes used for the movement tracker. +/// It reads the JSON of the Earable and provides the getters for accessing the data. +class SensorDataType { + //Data map + Map data; + + //Constructor + SensorDataType(this.data); + + //Getters for the data + double get x => data["X"]; + double get y => data["Y"]; + double get z => data["Z"]; + + //Units for the given data. + Map get units => data["units"]; +} + +/// Acceleration-sensor data. +class Acceleration extends SensorDataType { + Acceleration(Map data) : super(data["ACC"]); +} + +/// Gyroscope-sensor data. +class Gyroscope extends SensorDataType { + Gyroscope(Map data) : super(data["GYRO"]); +} + +/// EulerAngles-sensor data. +class EulerAngles extends SensorDataType { + EulerAngles(Map data) : super(data["EULER"]); +} + +/// Placeholder data without any information in case no sensor data is available. +class NullData extends SensorDataType { + NullData() : super({"X": 0.0, "Y": 0.0, "Z": 0.0}); +} \ No newline at end of file diff --git a/open_earable/lib/apps/ufiiu/timerscreen.dart b/open_earable/lib/apps/ufiiu/timerscreen.dart new file mode 100644 index 0000000..d408bae --- /dev/null +++ b/open_earable/lib/apps/ufiiu/timerscreen.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:open_earable/apps/ufiiu/movementTracker.dart'; +import 'package:open_earable/apps/ufiiu/sensor_datatypes.dart'; + +import 'interact.dart'; + +/// TimerScreen - Main screen for the movment timer interaction. +class TimerScreen extends StatefulWidget { + final Interact interact; + TimerScreen(this.interact); + @override + State createState() => TimerScreenState(interact); +} + + +/// State for the movement Timer Interaction +class TimerScreenState extends State { + + //Interaction class + final Interact _interact; + + //Movement & timer logic + late final MovementTracker _movementTracker; + + //Input Controller + final TextEditingController _controller = TextEditingController(); + + //Display Data + SensorDataType? _sensorData = NullData(); + + //Constructor + TimerScreenState(this._interact) { + this._movementTracker = MovementTracker(_interact); + } + + //Updates the text data. + void updateText(SensorDataType sensorData) { + setState(() { + _sensorData = sensorData; + }); + } + + + + ///Builds the main Widget + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Timer App'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Image Source + Image.network( + 'https://cdn-icons-png.flaticon.com/512/198/198155.png', + width: 150, + height: 150, + ), + SizedBox(height: 20), + + // Input for Time length + Padding( + padding: EdgeInsets.all(20), + child: TextField( + controller: _controller, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Zeitlänge eingeben (in Minuten)', + ), + ), + ), + SizedBox(height: 20), + + // Start timer button + ElevatedButton( + onPressed: () { + String input = _controller.text; + int minutes = int.tryParse(input) ?? 0; + + _movementTracker.start(minutes, updateText); + }, + child: Text('Starten'), + ), + + //Data table for the live display of the sensor data. + DataTable( + columns: [ + DataColumn(label: Text('Sensor')), + DataColumn(label: Text('Wert')), + ], + rows: [ + DataRow( + cells: [ + DataCell(Text('X')), + DataCell(Text(_sensorData!.x.toStringAsFixed(14))), + ], + ), + DataRow( + cells: [ + DataCell(Text('Y')), + DataCell(Text(_sensorData!.y.toStringAsFixed(14))), + ], + ), + DataRow( + cells: [ + DataCell(Text('Z')), + DataCell(Text(_sensorData!.z.toStringAsFixed(14))), + ], + ), + ], + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 17e5a27..1a1bcd0 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -4,6 +4,8 @@ import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart import 'package:open_earable/apps/recorder.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'apps/ufiiu/home_screen.dart'; + class AppInfo { final IconData iconData; final String title; @@ -35,6 +37,16 @@ class AppsTab extends StatelessWidget { builder: (context) => PostureTrackerView( EarableAttitudeTracker(_openEarable), _openEarable))); }), + AppInfo( + iconData: Icons.face_5, + title: "Powernapper", + description: "Powernapping timer!", + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SleepHomeScreen(_openEarable))); + }), AppInfo( iconData: Icons.fiber_smart_record, title: "Recorder", From 768d43d48cf83a05a572ae15eff50f38188adb6e Mon Sep 17 00:00:00 2001 From: elFuchso Date: Mon, 8 Jan 2024 20:22:24 +0000 Subject: [PATCH 047/104] Update apps_tab.dart --- open_earable/lib/apps_tab.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 17e5a27..1e5c32b 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -45,6 +45,16 @@ class AppsTab extends StatelessWidget { MaterialPageRoute( builder: (context) => Recorder(_openEarable))); }), + AppInfo( + iconData: Icons.music_note, + title: "Tightness Meter", + description: "Track your headbanging.", + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TightnessMeter(_openEarable))); + }), // ... similarly for other apps ]; } From 26e5a292eb7675fce5218b80e220e827f8e2eae1 Mon Sep 17 00:00:00 2001 From: elFuchso Date: Mon, 8 Jan 2024 20:23:03 +0000 Subject: [PATCH 048/104] Update apps_tab.dart --- open_earable/lib/apps_tab.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 1e5c32b..9131c9c 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; +import 'package:open_earable/apps/tightness.dart'; import 'package:open_earable/apps/recorder.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; From d8ae6e4209e894910e8a29474ac924236519639a Mon Sep 17 00:00:00 2001 From: elFuchso Date: Mon, 8 Jan 2024 20:23:34 +0000 Subject: [PATCH 049/104] Add tightness meter --- open_earable/lib/apps/tightness.dart | 455 +++++++++++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 open_earable/lib/apps/tightness.dart diff --git a/open_earable/lib/apps/tightness.dart b/open_earable/lib/apps/tightness.dart new file mode 100644 index 0000000..45b8073 --- /dev/null +++ b/open_earable/lib/apps/tightness.dart @@ -0,0 +1,455 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'dart:math' as math; +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; + +class TightnessMeter extends StatefulWidget { + final OpenEarable _openEarable; + TightnessMeter(this._openEarable); + @override + _TightnessMeterState createState() => _TightnessMeterState(_openEarable); +} + + + +class _TightnessMeterState extends State { + final OpenEarable _openEarable; + StreamSubscription? _imuSubscription; + _TightnessMeterState(this._openEarable); + bool _monitoring = false; + int lastTime = 0; + double x = 0; + double y = 0; + double z = 0; + double magnitude = 0; + double difficulty = 0; + int bpm = 80; + int score = 0; + int streak = 0; + int tightness = 0; + double nodThreshold = 4; // Time frame in milliseconds to consider for a nod + final List bpmList = [80, 100, 120, 170, 200]; + // Variables to keep track of nodding + DateTime lastNodTime = DateTime.now(); + + @override + void initState() { + super.initState(); + if (_openEarable.bleManager.connected) { + print('init'); + // _setupListeners(); + } + } + + @override + void dispose() { + super.dispose(); + _imuSubscription?.cancel(); + } + + _setupListeners() { + _imuSubscription = + _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { + if (!_monitoring) { + return; + } + int timestamp = data["timestamp"]; + setState(() { + lastTime = timestamp; + x = data["ACC"]["X"]; + y = data["ACC"]["Y"]; + z = data["ACC"]["Z"]; + }); + _processAccelerometerData(x, y, z); + }); + } + + void _processAccelerometerData(double x, double y, double z) { + // Calculate the overall acceleration magnitude + //print(x.toString() + y.toString() + z.toString()); + magnitude = _calculateMagnitude(x, y, z); + //print(magnitude); + + // Check if the magnitude exceeds the nodding threshold + if (magnitude > nodThreshold) { + DateTime now = DateTime.now(); + //print("Nod detected! 00000000000000000000000000000000"); + // Check if the last nod was within the time frame + if (now.difference(lastNodTime).inMilliseconds > + _bpmToMilliseconds(bpm) * 0.7) { + // Detected a nod + _isNodTight(lastNodTime, now); + lastNodTime = now; + } + } + } + + // Calculate the magnitude of the acceleration vector + double _calculateMagnitude(double x, double y, double z) { + return math.sqrt(x * x); + } + + // Check if the nod is tight + void _isNodTight(DateTime last, DateTime secondToLast) { + int difference = last.difference(secondToLast).inMilliseconds.abs(); + int expected = _bpmToMilliseconds(bpm); + if (_isWithinMargin(difference, expected, 25.0-difficulty)) { + setState(() { + streak += 1; + }); + _updateScore(); + } else { + setState(() { + streak = 0; + tightness = 0; + + }); + } + } + + void _updateScore() { + setState(() { + score = score + (((10 * streak) + difficulty) / tightness.abs() ).round(); + }); + } + + bool _isWithinMargin( + int givenInterval, int expectedInterval, double marginPercentage) { + double margin = expectedInterval * marginPercentage / 100; + // Calculate the acceptable range + double lowerBound = expectedInterval - margin; + double upperBound = expectedInterval + margin; + setState(() { + tightness = math.min(_bpmToMilliseconds(bpm), (givenInterval - expectedInterval)); + }); + return givenInterval >= lowerBound && givenInterval <= upperBound; + } + + int _bpmToMilliseconds(int bpm) { + if (bpm <= 0) { + throw ArgumentError("BPM must be greater than 0."); + } + return (60000 / bpm).round(); + } + + void startStopMonitoring() async { + if (_monitoring) { + setState(() { + _monitoring = false; + }); + _openEarable.audioPlayer.setState(AudioPlayerState.stop); + } else { + _setupListeners(); + setState(() { + _monitoring = true; + streak = 0; + }); + //start playing music + _setWAV(bpm.toString()); + } + } + + void _setWAV(String bpm) { + String fileName = bpm + ".wav"; + print("Setting source to wav file with file name '" + fileName + "'"); + _openEarable.audioPlayer.wavFile(fileName); + } + + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + appBar: AppBar( + title: Text('Tightness Meter'), + ), + body: Center( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(40.0), + bottomRight: Radius.circular(40.0), + topLeft: Radius.circular(40.0), + bottomLeft: Radius.circular(40.0)), + ), + width: 500, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Card( + margin: EdgeInsets.all(20), + color: Colors.black, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(20.0), + bottomRight: Radius.circular(20.0), + topLeft: Radius.circular(20.0), + bottomLeft: Radius.circular(20.0)), + ), + child: Row( + children: [ + Text( + 'Score:', + style: TextStyle(fontSize: 20), + ), + Spacer(), + Padding( + padding: EdgeInsets.all(5), + child: Text( + _monitoring ? score.toString() : '0', + style: TextStyle(fontSize: 20), + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Container( + decoration: BoxDecoration( + color: (streak > 0) ? Colors.green : Colors.red, + borderRadius: BorderRadius.only( + topRight: Radius.circular(20.0), + bottomRight: Radius.circular(20.0), + topLeft: Radius.circular(20.0), + bottomLeft: Radius.circular(20.0)), + ), + padding: EdgeInsets.all(16), + child: Row( + children: [ + Text( + 'Streak:', + style: TextStyle(fontSize: 20), + ), + Spacer(), + Padding( + padding: EdgeInsets.all(5), + child: Text( + _monitoring ? streak.toString() : '0', + style: TextStyle(fontSize: 20), + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(20.0), + bottomRight: Radius.circular(20.0), + topLeft: Radius.circular(20.0), + bottomLeft: Radius.circular(20.0)), + ), + padding: EdgeInsets.only(top: 16, right: 16, left: 16), + child: Row( + children: [ + Text( + 'Tightness:', + style: TextStyle(fontSize: 20), + ), + Spacer(), + Padding( + padding: EdgeInsets.all(5), + child: Text( + _monitoring ? tightness.toString() : '0', + style: TextStyle(fontSize: 20), + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topRight: Radius.circular(20.0), + bottomRight: Radius.circular(20.0), + topLeft: Radius.circular(20.0), + bottomLeft: Radius.circular(20.0)), + ), + child: Slider( + thumbColor: Colors.purple, + activeColor: Colors.grey, + secondaryActiveColor: Colors.purpleAccent, + inactiveColor: Colors.grey, + value: tightness.toDouble(), + min: -_bpmToMilliseconds(bpm).toDouble(), + max: _bpmToMilliseconds(bpm).toDouble(), + divisions: 2000, + label: tightness.toString(), + onChanged: (double value) { + setState(() { + }); + }), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom:20.0), + child: Row(children: [ + Spacer(), + Text('Early'), + Spacer(flex: 5), + Text('Tight'), + Spacer(flex: 5), + Text('Late'), + Spacer() + ], + ), + ), + ], + ), + ), + Card(margin: EdgeInsets.all(20), + color: Colors.black, + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(20), + child: ElevatedButton( + onPressed: startStopMonitoring, + style: ElevatedButton.styleFrom( + minimumSize: Size(1000, 80), + backgroundColor: _monitoring + ? Color(0xfff27777) + : Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.black, + ), + child: Text( + _monitoring ? 'Stop' : 'Start', + style: TextStyle(fontSize: 30), + ), + ), + ), + ], + ), + ), + Card( + margin: EdgeInsets.all(20), + color: Colors.black, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Spacer(), + Text('BPM', + style: TextStyle(fontSize: 30)), + Spacer(flex: 5), + DropdownButton( + style: TextStyle( + fontSize: 30, + ), + value: bpm, + icon: const Icon(Icons.arrow_drop_down), + onChanged: _monitoring ? null :(int? newValue) { + setState(() { + bpm = newValue!; + }); + }, + items: bpmList.map>((int value) { + return DropdownMenuItem( + value: value, + child: Text(value.toString()), + ); + }).toList(), + ), + Spacer(), + ], + ), + ), + Padding( + padding: EdgeInsets.only(top: 20), + child: Text('Sensitivity', + style: TextStyle(fontSize: 20) + ), + ), + Padding( + padding: EdgeInsets.all(16), + child: Column( + children: [ + Slider( + thumbColor: Colors.purple, + activeColor: Colors.purpleAccent, + secondaryActiveColor: Colors.purpleAccent, + inactiveColor: Colors.grey, + value: nodThreshold, + min: 1, + max: 20, + divisions: 10, + label: nodThreshold.round().toString(), + onChanged: (double value) { + setState(() { + nodThreshold = value; + }); + }), + Row(children: [ + Spacer(), + Text('Cool Nodding'), + Spacer(flex: 10), + Text('Headbanging'), + Spacer() + ], + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only(top: 10), + child: Text('Difficulty', + style: TextStyle(fontSize: 20) + ), + ), + Padding( + padding: EdgeInsets.all(16), + child: Column( + children: [ + Slider( + thumbColor: Colors.purple, + activeColor: Colors.purpleAccent, + secondaryActiveColor: Colors.purpleAccent, + inactiveColor: Colors.grey, + value: difficulty, + min: 0, + max: 25, + divisions: 10, + label: difficulty.round().toString(), + onChanged: (double value) { + setState(() { + difficulty = value; + }); + }), + Row( + children: [ + Spacer(), + Text('Beginner'), + Spacer(flex: 10), + Text('Impossible'), + Spacer() + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} From a2d5db78137dba2d2e47e9705fd6efac79581e1d Mon Sep 17 00:00:00 2001 From: Polaris Date: Tue, 9 Jan 2024 10:29:20 +0100 Subject: [PATCH 050/104] Small change to the tutorial view and renaming one color in the stretch_colors.dart. Also adding a markdown file regarind this app with some documentation --- README-Abgabe.txt | 8 --- StretchApp.md | 53 +++++++++++++++++++ .../neck_stretch/model/stretch_colors.dart | 4 +- .../view/stretch_arc_painter.dart | 4 +- .../view/stretch_tutorial_view.dart | 19 +++++-- 5 files changed, 73 insertions(+), 15 deletions(-) delete mode 100644 README-Abgabe.txt create mode 100644 StretchApp.md diff --git a/README-Abgabe.txt b/README-Abgabe.txt deleted file mode 100644 index 00077a9..0000000 --- a/README-Abgabe.txt +++ /dev/null @@ -1,8 +0,0 @@ -Da das ganze Projekt zu groß für die Illias-Upload-Grenze sind habe ich hier nur meine lib und assets directory rein gemacht -und meine angepasste pubspec. Diese beinhaltet nun noch eine library für Youtube-Wiedergabe in der App und referenzen für meine neuen assets. - -Falls Sie das ganze Repository herunterladen wollen können sie einfach das Repository von meinem GitHub Repo clonen. -Falls Sie hiermit Probleme haben könnnen Sie mich immer per mail (ukqho@student.kit.edu) erreichen oder auf Discord (Soheel) - -Man kann den ganzen Progress mit commits auch auf meinem GitHub nachschauen: - https://github.com/BasicallyPolaris/oe-app diff --git a/StretchApp.md b/StretchApp.md new file mode 100644 index 0000000..62fca30 --- /dev/null +++ b/StretchApp.md @@ -0,0 +1,53 @@ +# Guided Neck Stretch App + +## Goal +This App is used to allow users to easily start a stretching exercise for their neck without a lot of trouble. To ensure that the user doesn't have to look at the screen while enjoying their stretching session, it also has a stats tab which displays valuable information to be viewed after they just stretched. Furthermore the app signals to the user whenever the next stretching session starts and when a break is occuring, so that the user can stay calm and close his eyes without focusing on his own set time constraints. +To ensure a more intimate experience the user can also set his own threshold goals and stretching duration for each of the stretch exercises, and modify the break times in between. If a user is new or unsure how to use this app or to execute the stretch exercises in the right manner, a guide tab is provided to help the user understand the app and it's UI and to show them a video regarding the used stretch exercises. + +## Assets +The assets are modified images from the posture tracker app to ensure consistency within the OpenEarable app. These images always display the stretched area with a blue indicator color. +- `Neck_Stretch_Left.png`: Image displaying the neck with indicators of a "left stretch". +- `Neck_Main_Stretch.png`: Image displaying the neck with indicators of a "main stretch". +- `Neck_Right_Stretch.png`: Image displaying the neck with indicators of a "right stretch". +- `Neck_Side_Stretch.png`: Image displaying the neck with indicators of a stretch of both sides of the neck. + +## Model +### Stretch Colors +This file stores all colors used within the stretch app to assure easy exchangeability and consistency within this app. + +### Stretch State +This file stores all classes used to store stretching information, such as +- `NeckStretchState`: Stores all data concerning a stretching state and it's asset paths +- `StretchStats`: Stores all data concering the most recent stretching session +- `StretchSettings`: Stores all data needed to configure a stretching session +- `NeckStretch`: Provides a one class solution which compromises all of the Model Data into this Class. It provides all functions needed to get and modify the data and also has all the code concerning the stretch state switches. + +## View + +### App View +This file consiststs of the module used to display all the submodules of this app and is built up just like the normal app selector in the open-earable app itself. Notable is that this is a stateful widget which initializes the final `StretchViewModel` object which is used to store and manage all data needed for this app. + +### Stretch Arc Painter +This is a modified version of the `arc_painter` used in the `posture_tracker` app, which draws the right indicators with the right colors from the `stretch_colors.dart` to indicate whether the user is currently stretching in the right direction and to display what area is desireable or undesireable for the current stretch. + +### Stretch Roll View +This is a modified version of the `roll_view` used in the `posture_tracker` app, which is used to draw the whole "head area" of a tracking session. This file is edited to support the different neck stretch types and draw the arcs according to them. + +### Stretch Settings View +This is the view used to display the settings module and edit all the settings for a neck stretching session. It uses the `TextEditingController` to parse any input by the user, which is then used to set the right settings in the `StretchViewModel` for a stretching exercise. + +### Stretch Stats View +This is the view used to display the stats of the most recent stretching exercise. These stats are stored and editied by the `StretchViewModel`. + +### Stretch Tracker View +This module is the view of the stretch tracker module. Here you can start stretching and can track your stretching progess via the UI. The UI is drawn using an modified version of the `posture arc_painter.dart`, the `stretch_arc_painter.dart`. This view also provides certain functions to easily draw the head tracker view in other modules. + +### Stretch Tutorial View +This is the view for the stretch tutorial module, which is used to show the user how to use this app, and has an embedded youtube video (using an [youtube player package](https://pub.dev/packages/youtube_player_flutter)), which shows all of the neck stretches used by this app. + +## View Model +### Stretch View Model +This file stores the `StretchViewModel`, which stores all data used by this app and is used to change it on the fly by the submodules. It also provides the functionality to track the stats of the user for the Stretch Stats View. Furthermore it is used to stop and start the tracking by the earable. + +--- +By Soheel Dario Aghadavoodi Jolfaei - [GitHub](https://github.com/BasicallyPolaris/oe-app) \ No newline at end of file diff --git a/open_earable/lib/apps/neck_stretch/model/stretch_colors.dart b/open_earable/lib/apps/neck_stretch/model/stretch_colors.dart index bf1a000..8c33e3d 100644 --- a/open_earable/lib/apps/neck_stretch/model/stretch_colors.dart +++ b/open_earable/lib/apps/neck_stretch/model/stretch_colors.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; /// Colors used for the arcs around the head -final Color outerRing = Color.fromARGB(255, 195, 195, 195); +final Color rightAreaIndicator = Color.fromARGB(255, 195, 195, 195); final Color wrongAreaIndicator = Color(0xFF7A7A7A); /// Colors used for bad stretch directions @@ -19,4 +19,4 @@ final Color stretchedAreaColor = Color.fromARGB(255, 0, 186, 255); /// Colors used for the stretching button final Color startButtonColor = Color(0xff77F2A1); final Color stopButtonColor = Color(0xfff27777); -final Color restingButtonColor = Color(0xffffbb3d); +final Color restingButtonColor = Color(0xffffbb3d); \ No newline at end of file diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart index 76fb60d..3abab82 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_arc_painter.dart @@ -21,7 +21,7 @@ class StretchArcPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { Paint circlePaint = Paint() - ..color = outerRing + ..color = rightAreaIndicator ..style = PaintingStyle.stroke ..strokeWidth = 5.0; @@ -209,4 +209,4 @@ class StretchArcPainter extends CustomPainter { // check if oldDelegate is an ArcPainter and if the angle is the same return oldDelegate is ArcPainter && oldDelegate.angle != this.angle; } -} +} \ No newline at end of file diff --git a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart index d496db9..1273bc6 100644 --- a/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart +++ b/open_earable/lib/apps/neck_stretch/view/stretch_tutorial_view.dart @@ -94,6 +94,7 @@ class _StretchTutorialViewState extends State { ], ), ), + /// Card used to explain the tracking colors Card( color: Theme.of(context).colorScheme.primary, @@ -102,7 +103,6 @@ class _StretchTutorialViewState extends State { ListTile( title: Text("Explaining the Tracking Colors"), ), - Padding( padding: EdgeInsets.fromLTRB(16, 0, 16, 0), child: RichText( @@ -117,7 +117,6 @@ class _StretchTutorialViewState extends State { ), ), ), - Padding( padding: EdgeInsets.fromLTRB(24, 0, 24, 0), child: RichText( @@ -145,7 +144,19 @@ class _StretchTutorialViewState extends State { ), TextSpan( text: - 'You are currently stretching, try to gently move your head into the grey area\n\n', + 'You are currently stretching, try to gently move your head into the ', + ), + TextSpan( + text: 'light grey ', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: rightAreaIndicator, + ), + ), + TextSpan( + text: + 'area\n\n', ), ], ), @@ -154,6 +165,7 @@ class _StretchTutorialViewState extends State { ], ), ), + /// Example of the Tracker in Main Neck Stretch state Card( color: Theme.of(context).colorScheme.primary, @@ -240,6 +252,7 @@ class _StretchTutorialViewState extends State { ], ), ), + /// Card explaining the stretching button Card( color: Theme.of(context).colorScheme.primary, From 4323265eec5877d1bfe2312eff857358ef4d5b37 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Tue, 9 Jan 2024 14:59:53 +0100 Subject: [PATCH 051/104] add title to audio player sources --- .../lib/controls_tab/views/audio_player.dart | 259 ++++++++++-------- 1 file changed, 145 insertions(+), 114 deletions(-) diff --git a/open_earable/lib/controls_tab/views/audio_player.dart b/open_earable/lib/controls_tab/views/audio_player.dart index 76c2933..653c84b 100644 --- a/open_earable/lib/controls_tab/views/audio_player.dart +++ b/open_earable/lib/controls_tab/views/audio_player.dart @@ -198,7 +198,7 @@ class _AudioPlayerCardState extends State { _getFileNameRow(), _getJingleRow(), _getFrequencyRow(), - SizedBox(height: 4), + SizedBox(height: 12), Platform.isIOS ? _getCupertinoButtonRow() : _getMaterialButtonRow(), @@ -210,57 +210,68 @@ class _AudioPlayerCardState extends State { } Widget _getAudioPlayerRadio(int index) { - if (Platform.isIOS) { - return Padding( - padding: EdgeInsets.all(12), - child: CupertinoRadio( - value: index, - groupValue: OpenEarableSettings().selectedAudioPlayerRadio, - onChanged: !_openEarable.bleManager.connected - ? null - : (int? value) { - setState(() { - OpenEarableSettings().selectedAudioPlayerRadio = - value ?? 0; - }); - }, - activeColor: CupertinoTheme.of(context).primaryColor, - fillColor: CupertinoTheme.of(context).primaryContrastingColor, - inactiveColor: CupertinoTheme.of(context).primaryContrastingColor, - )); - } else { - return Radio( - value: index, - groupValue: OpenEarableSettings().selectedAudioPlayerRadio, - onChanged: !_openEarable.bleManager.connected - ? null - : (int? value) { - setState(() { - OpenEarableSettings().selectedAudioPlayerRadio = value ?? 0; - }); - }, - fillColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { - return Theme.of(context).colorScheme.secondary; - } - return Colors.grey; - }), - ); - } + return SizedBox( + height: 37, + width: 44, + child: Platform.isIOS + ? CupertinoRadio( + value: index, + groupValue: OpenEarableSettings().selectedAudioPlayerRadio, + onChanged: !_openEarable.bleManager.connected + ? null + : (int? value) { + setState(() { + OpenEarableSettings().selectedAudioPlayerRadio = + value ?? 0; + }); + }, + activeColor: CupertinoTheme.of(context).primaryColor, + fillColor: CupertinoTheme.of(context).primaryContrastingColor, + inactiveColor: + CupertinoTheme.of(context).primaryContrastingColor, + ) + : Radio( + value: index, + groupValue: OpenEarableSettings().selectedAudioPlayerRadio, + onChanged: !_openEarable.bleManager.connected + ? null + : (int? value) { + setState(() { + OpenEarableSettings().selectedAudioPlayerRadio = + value ?? 0; + }); + }, + fillColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return Theme.of(context).colorScheme.secondary; + } + return Colors.grey; + }), + )); } Widget _getFileNameRow() { - return Row( - children: [ - _getAudioPlayerRadio(0), - Expanded( - child: SizedBox( - height: 37.0, - child: _fileNameTextField( - _filenameTextController, TextInputType.text, null, null)), - ), - ], - ); + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: EdgeInsets.fromLTRB(44, 0, 0, 0), + child: Text( + "Audio File", + style: TextStyle( + color: Color.fromRGBO(168, 168, 172, 1.0), + ), + )), + Row( + children: [ + _getAudioPlayerRadio(0), + Expanded( + child: SizedBox( + height: 37.0, + child: _fileNameTextField( + _filenameTextController, TextInputType.text, null, null)), + ), + ], + ) + ]); } Widget _fileNameTextField(TextEditingController textController, @@ -321,80 +332,100 @@ class _AudioPlayerCardState extends State { } Widget _getJingleRow() { - return Row( - children: [ - _getAudioPlayerRadio(1), - Expanded( - child: SizedBox( - height: 37.0, - child: _valuePicker( - context, - OpenEarableSettings().jingleMap.keys.toList(), - OpenEarableSettings().selectedJingle, - (_) => null, (newValue) { - setState(() { - OpenEarableSettings().selectedJingle = newValue; - }); - }), + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: EdgeInsets.fromLTRB(44, 8, 0, 0), + child: Text( + "Jingle", + style: TextStyle( + color: Color.fromRGBO(168, 168, 172, 1.0), + ), + )), + Row( + children: [ + _getAudioPlayerRadio(1), + Expanded( + child: SizedBox( + height: 37.0, + child: _valuePicker( + context, + OpenEarableSettings().jingleMap.keys.toList(), + OpenEarableSettings().selectedJingle, + (_) => null, (newValue) { + setState(() { + OpenEarableSettings().selectedJingle = newValue; + }); + }), + ), ), - ), - ], - ); + ], + ) + ]); } Widget _getFrequencyRow() { - return Row( - children: [ - _getAudioPlayerRadio(2), - SizedBox( - height: 37.0, - width: 75, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 0), - child: _fileNameTextField(_frequencyTextController, - TextInputType.number, "440", null))), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 5), + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: EdgeInsets.fromLTRB(44, 8, 0, 0), child: Text( - 'Hz', + "Frequency", style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey), // Set text color to white + color: Color.fromRGBO(168, 168, 172, 1.0), + ), + )), + Row( + children: [ + _getAudioPlayerRadio(2), + SizedBox( + height: 37.0, + width: 75, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 0), + child: _fileNameTextField(_frequencyTextController, + TextInputType.number, "440", null))), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: Text( + 'Hz', + style: TextStyle( + color: _openEarable.bleManager.connected + ? Colors.white + : Colors.grey), // Set text color to white + ), ), - ), - Spacer(), - SizedBox( + Spacer(), + SizedBox( + height: 37.0, + width: 52, + child: _fileNameTextField(_frequencyVolumeTextController, + TextInputType.number, "50", 3)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: Text( + '%', + style: TextStyle( + color: _openEarable.bleManager.connected + ? Colors.white + : Colors.grey), // Set text color to white + ), + ), + Spacer(), + SizedBox( height: 37.0, - width: 52, - child: _fileNameTextField( - _frequencyVolumeTextController, TextInputType.number, "50", 3)), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 5), - child: Text( - '%', - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey), // Set text color to white + width: 107, + child: _valuePicker( + context, + OpenEarableSettings().waveFormMap.keys.toList(), + OpenEarableSettings().selectedWaveForm, + (_) => null, (newValue) { + setState(() { + OpenEarableSettings().selectedWaveForm = newValue; + }); + }), ), - ), - Spacer(), - SizedBox( - height: 37.0, - width: 107, - child: _valuePicker( - context, - OpenEarableSettings().waveFormMap.keys.toList(), - OpenEarableSettings().selectedWaveForm, - (_) => null, (newValue) { - setState(() { - OpenEarableSettings().selectedWaveForm = newValue; - }); - }), - ), - ], - ); + ], + ) + ]); } Widget _valuePicker( From dd7815f787e1ccb34c913d6dbff410369de8f54b Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Tue, 9 Jan 2024 15:51:11 +0100 Subject: [PATCH 052/104] refactor dynamic value picker and set initial value when opening picker to the current value --- .../lib/controls_tab/views/audio_player.dart | 144 +++--------------- .../views/sensor_configuration.dart | 118 ++------------ .../lib/widgets/dynamic_value_picker.dart | 114 ++++++++++++++ 3 files changed, 144 insertions(+), 232 deletions(-) create mode 100644 open_earable/lib/widgets/dynamic_value_picker.dart diff --git a/open_earable/lib/controls_tab/views/audio_player.dart b/open_earable/lib/controls_tab/views/audio_player.dart index 653c84b..9d6cd8f 100644 --- a/open_earable/lib/controls_tab/views/audio_player.dart +++ b/open_earable/lib/controls_tab/views/audio_player.dart @@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import '../models/open_earable_settings.dart'; +import '../../widgets/dynamic_value_picker.dart'; class AudioPlayerCard extends StatefulWidget { final OpenEarable _openEarable; @@ -347,15 +348,18 @@ class _AudioPlayerCardState extends State { Expanded( child: SizedBox( height: 37.0, - child: _valuePicker( - context, - OpenEarableSettings().jingleMap.keys.toList(), - OpenEarableSettings().selectedJingle, - (_) => null, (newValue) { - setState(() { - OpenEarableSettings().selectedJingle = newValue; - }); - }), + child: DynamicValuePicker( + context, + OpenEarableSettings().jingleMap.keys.toList(), + OpenEarableSettings().selectedJingle, + (newValue) { + setState(() { + OpenEarableSettings().selectedJingle = newValue; + }); + }, + (_) => null, + _openEarable.bleManager.connected, + ), ), ), ], @@ -413,128 +417,22 @@ class _AudioPlayerCardState extends State { SizedBox( height: 37.0, width: 107, - child: _valuePicker( + child: DynamicValuePicker( context, OpenEarableSettings().waveFormMap.keys.toList(), - OpenEarableSettings().selectedWaveForm, - (_) => null, (newValue) { - setState(() { - OpenEarableSettings().selectedWaveForm = newValue; - }); - }), + OpenEarableSettings().selectedWaveForm, (newValue) { + setState( + () { + OpenEarableSettings().selectedWaveForm = newValue; + }, + ); + }, (_) => null, _openEarable.bleManager.connected), ), ], ) ]); } - Widget _valuePicker( - BuildContext context, - List options, - String currentValue, - Function(bool?) changeBool, - Function(String) changeSelection) { - if (Platform.isIOS) { - return CupertinoButton( - borderRadius: BorderRadius.all(Radius.circular(4.0)), - color: Colors.white, - padding: EdgeInsets.fromLTRB(8, 0, 0, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - currentValue, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey, - ), - ), - ], - ), - onPressed: () => _showCupertinoPicker( - context, options, currentValue, changeBool, changeSelection), - ); - } else { - return DropdownButton( - dropdownColor: - _openEarable.bleManager.connected ? Colors.white : Colors.grey[200], - alignment: Alignment.centerRight, - value: currentValue, - onChanged: (String? newValue) { - setState(() { - changeSelection(newValue!); - if (int.parse(newValue) != 0) { - changeBool(true); - } else { - changeBool(false); - } - }); - }, - items: options.map((String value) { - return DropdownMenuItem( - alignment: Alignment.centerRight, - value: value, - child: Text( - value, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey, - ), - textAlign: TextAlign.end, - ), - ); - }).toList(), - underline: Container(), - icon: Icon( - Icons.arrow_drop_down, - color: _openEarable.bleManager.connected ? Colors.black : Colors.grey, - ), - ); - } - } - - void _showCupertinoPicker(context, List options, String currentValue, - Function(bool?) changeBool, Function(String) changeSelection) { - showCupertinoModalPopup( - context: context, - builder: (_) => Container( - height: 200, - color: Colors.white, - child: CupertinoPicker( - backgroundColor: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey[200], - itemExtent: 32, // Height of each item - onSelectedItemChanged: (int index) { - setState(() { - String newValue = options[index]; - changeSelection(newValue); - if (int.parse(newValue) != 0) { - changeBool(true); - } else { - changeBool(false); - } - }); - }, - children: options - .map((String value) => Center( - child: Text( - value, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey, - ), - ), - )) - .toList(), - ), - ), - ); - } - Widget _getMaterialButtonRow() { return Row( mainAxisAlignment: diff --git a/open_earable/lib/controls_tab/views/sensor_configuration.dart b/open_earable/lib/controls_tab/views/sensor_configuration.dart index a866621..836eacf 100644 --- a/open_earable/lib/controls_tab/views/sensor_configuration.dart +++ b/open_earable/lib/controls_tab/views/sensor_configuration.dart @@ -1,5 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:open_earable/widgets/dynamic_value_picker.dart'; import 'dart:io'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import '../models/open_earable_settings.dart'; @@ -242,118 +243,17 @@ class _SensorConfigurationCardState extends State { height: 37, child: Container( alignment: Alignment.centerRight, - child: _valuePicker(context, options, currentValue, - changeBool, changeSelection)))), + child: DynamicValuePicker( + context, + options, + currentValue, + changeSelection, + changeBool, + _openEarable.bleManager.connected, + )))), SizedBox(width: 8), Text("Hz", style: TextStyle(color: Color.fromRGBO(168, 168, 172, 1.0))), ], ); } - - Widget _valuePicker( - BuildContext context, - List options, - String currentValue, - Function(bool?) changeBool, - Function(String) changeSelection) { - if (Platform.isIOS) { - return CupertinoButton( - borderRadius: BorderRadius.all(Radius.circular(4.0)), - color: Colors.white, - padding: EdgeInsets.fromLTRB(8, 0, 0, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - currentValue, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey, - ), - ), - ], - ), - onPressed: () => _showCupertinoPicker( - context, options, currentValue, changeBool, changeSelection), - ); - } else { - return DropdownButton( - dropdownColor: - _openEarable.bleManager.connected ? Colors.white : Colors.grey[200], - alignment: Alignment.centerRight, - value: currentValue, - onChanged: (String? newValue) { - setState(() { - changeSelection(newValue!); - if (int.parse(newValue) != 0) { - changeBool(true); - } else { - changeBool(false); - } - }); - }, - items: options.map((String value) { - return DropdownMenuItem( - alignment: Alignment.centerRight, - value: value, - child: Text( - value, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey, - ), - textAlign: TextAlign.end, - ), - ); - }).toList(), - underline: Container(), - icon: Icon( - Icons.arrow_drop_down, - color: _openEarable.bleManager.connected ? Colors.black : Colors.grey, - ), - ); - } - } - - void _showCupertinoPicker(context, List options, String currentValue, - Function(bool?) changeBool, Function(String) changeSelection) { - showCupertinoModalPopup( - context: context, - builder: (_) => Container( - height: 200, - color: Colors.white, - child: CupertinoPicker( - backgroundColor: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey[200], - itemExtent: 32, // Height of each item - onSelectedItemChanged: (int index) { - setState(() { - String newValue = options[index]; - changeSelection(newValue); - if (int.parse(newValue) != 0) { - changeBool(true); - } else { - changeBool(false); - } - }); - }, - children: options - .map((String value) => Center( - child: Text( - value, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey, - ), - ), - )) - .toList(), - ), - ), - ); - } } diff --git a/open_earable/lib/widgets/dynamic_value_picker.dart b/open_earable/lib/widgets/dynamic_value_picker.dart new file mode 100644 index 0000000..ca514ee --- /dev/null +++ b/open_earable/lib/widgets/dynamic_value_picker.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'dart:io'; + +class DynamicValuePicker extends StatelessWidget { + BuildContext context; + final List options; + final String currentValue; + final Function(String) onValueChange; + final Function(bool) onBoolChange; + final bool isConnected; + + DynamicValuePicker( + this.context, + this.options, + this.currentValue, + this.onValueChange, + this.onBoolChange, + this.isConnected, + ); + + @override + Widget build(BuildContext context) { + if (Platform.isIOS) { + return CupertinoButton( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + color: Colors.white, + padding: EdgeInsets.fromLTRB(8, 0, 0, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + currentValue, + style: TextStyle( + color: isConnected ? Colors.black : Colors.grey, + ), + ), + ], + ), + onPressed: () => _showCupertinoPicker(), + ); + } else { + return DropdownButton( + dropdownColor: isConnected ? Colors.white : Colors.grey[200], + alignment: Alignment.centerRight, + value: currentValue, + onChanged: (String? newValue) { + onValueChange(newValue!); + if (int.parse(newValue) != 0) { + onBoolChange(true); + } else { + onBoolChange(false); + } + }, + items: options.map((String value) { + return DropdownMenuItem( + alignment: Alignment.centerRight, + value: value, + child: Text( + value, + style: TextStyle( + color: isConnected ? Colors.black : Colors.grey, + ), + textAlign: TextAlign.end, + ), + ); + }).toList(), + underline: Container(), + icon: Icon( + Icons.arrow_drop_down, + color: isConnected ? Colors.black : Colors.grey, + ), + ); + } + } + + void _showCupertinoPicker() { + var currentIndex = options.indexOf(currentValue); + final FixedExtentScrollController scrollController = + FixedExtentScrollController(initialItem: currentIndex); + showCupertinoModalPopup( + context: context, + builder: (_) => Container( + height: 200, + color: Colors.white, + child: CupertinoPicker( + scrollController: scrollController, + backgroundColor: isConnected ? Colors.white : Colors.grey[200], + itemExtent: 32, // Height of each item + onSelectedItemChanged: (int index) { + String newValue = options[index]; + int? newValueInt = int.tryParse(newValue); + onValueChange(newValue); + if (newValueInt != 0) { + onBoolChange(true); + } else { + onBoolChange(false); + } + }, + children: options + .map((String value) => Center( + child: Text( + value, + style: TextStyle( + color: isConnected ? Colors.black : Colors.grey, + ), + ), + )) + .toList(), + ), + ), + ); + } +} From 25e4bebb5a03a00a1979faf62177bec8103306c2 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Tue, 9 Jan 2024 18:57:28 +0100 Subject: [PATCH 053/104] make ble page Cupertino compatible --- open_earable/lib/ble.dart | 208 ++++++++++-------- .../lib/widgets/dynamic_value_picker.dart | 14 +- 2 files changed, 122 insertions(+), 100 deletions(-) diff --git a/open_earable/lib/ble.dart b/open_earable/lib/ble.dart index 346a0a5..49f92b1 100644 --- a/open_earable/lib/ble.dart +++ b/open_earable/lib/ble.dart @@ -1,5 +1,6 @@ import 'dart:async'; - +import 'dart:io'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; @@ -13,7 +14,8 @@ class BLEPage extends StatefulWidget { } class _BLEPageState extends State { - String _openEarableName = "OpenEarable"; + final String _pageTitle = "Bluetooth Devices"; + final String _openEarableName = "OpenEarable"; late OpenEarable _openEarable; StreamSubscription? _scanSubscription; StreamSubscription? _connectionStateStream; @@ -44,109 +46,129 @@ class _BLEPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, - appBar: AppBar( - title: const Text('Bluetooth Devices'), - ), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.fromLTRB(33, 16, 0, 0), - child: Text( - "SCANNED DEVICES", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12.0, - ), + return Platform.isIOS + ? CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar(middle: Text(_pageTitle)), + child: SafeArea( + child: _getBody(), + )) + : Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + appBar: AppBar( + title: Text(_pageTitle), + ), + body: _getBody()); + } + + Widget _getBody() { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(33, 16, 0, 0), + child: Text( + "SCANNED DEVICES", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12.0, ), ), - Visibility( - visible: discoveredDevices.isNotEmpty, - child: Container( - margin: const EdgeInsets.fromLTRB(16, 0, 16, 0), - decoration: BoxDecoration( - border: Border.all( - color: Colors.grey, - width: 1.0, - ), - borderRadius: BorderRadius.circular(8.0), + ), + Visibility( + visible: discoveredDevices.isNotEmpty, + child: Container( + margin: const EdgeInsets.fromLTRB(16, 0, 16, 0), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey, + width: 1.0, ), - child: ListView.builder( - padding: EdgeInsets.zero, - physics: - const NeverScrollableScrollPhysics(), // Disable scrolling, - shrinkWrap: true, - itemCount: discoveredDevices.length, - itemBuilder: (BuildContext context, int index) { - final device = discoveredDevices[index]; - return Column(children: [ - Material( - type: MaterialType.transparency, - child: ListTile( - selectedTileColor: Colors.grey, - title: Text(device.name), - titleTextStyle: const TextStyle(fontSize: 16), - visualDensity: const VisualDensity( - horizontal: -4, vertical: -4), - trailing: _buildTrailingWidget(device.id), - onTap: () { - _connectToDevice(device); - }, - )), - if (index != discoveredDevices.length - 1) - const Divider( - height: 1.0, - thickness: 1.0, - color: Colors.grey, - indent: 16.0, - endIndent: 0.0, - ), - ]); - }, - ))), - Center( - child: Padding( - padding: EdgeInsets.all(16), - child: Text( - (_openEarable.bleManager.deviceIdentifier != null && - _openEarable.bleManager.deviceFirmwareVersion != - null) - ? "Connected to ${_openEarable.bleManager.deviceIdentifier} ${_openEarable.bleManager.deviceFirmwareVersion}" - : "If your OpenEarable device is not shown here, try restarting it", - style: const TextStyle(fontSize: 16), - textAlign: TextAlign.center, - ))), - Center( - child: ElevatedButton( - onPressed: _startScanning, - style: ButtonStyle( - foregroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.primary), - backgroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.secondary), - ), - child: const Text('Restart Scan'), - ), - ) - ], - )), - ); + borderRadius: BorderRadius.circular(8.0), + ), + child: ListView.builder( + padding: EdgeInsets.zero, + physics: + const NeverScrollableScrollPhysics(), // Disable scrolling, + shrinkWrap: true, + itemCount: discoveredDevices.length, + itemBuilder: (BuildContext context, int index) { + final device = discoveredDevices[index]; + return Column(children: [ + Material( + type: MaterialType.transparency, + child: ListTile( + selectedTileColor: Colors.grey, + title: Text(device.name), + titleTextStyle: const TextStyle(fontSize: 16), + visualDensity: const VisualDensity( + horizontal: -4, vertical: -4), + trailing: _buildTrailingWidget(device.id), + onTap: () { + _connectToDevice(device); + }, + )), + if (index != discoveredDevices.length - 1) + const Divider( + height: 1.0, + thickness: 1.0, + color: Colors.grey, + indent: 16.0, + endIndent: 0.0, + ), + ]); + }, + ))), + Center( + child: Padding( + padding: EdgeInsets.all(16), + child: Text( + (_openEarable.bleManager.deviceIdentifier != null && + _openEarable.bleManager.deviceFirmwareVersion != null) + ? "Connected to ${_openEarable.bleManager.deviceIdentifier} ${_openEarable.bleManager.deviceFirmwareVersion}" + : "If your OpenEarable device is not shown here,\ntry restarting it", + style: const TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ))), + Center( + child: Platform.isIOS + ? CupertinoButton( + padding: EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Text('Restart Scan'), + color: CupertinoTheme.of(context) + .primaryColor, // iOS equivalent for a prominent button color + onPressed: _startScanning, + ) + : ElevatedButton( + onPressed: _startScanning, + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.primary), + backgroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.secondary), + ), + child: const Text('Restart Scan'), + ), + ) + ], + )); } Widget _buildTrailingWidget(String id) { if (_openEarable.bleManager.connectedDevice?.id == id) { return Icon( size: 24, - Icons.check, - color: Theme.of(context).colorScheme.secondary); + Platform.isIOS ? CupertinoIcons.check_mark : Icons.check, + color: Platform.isIOS + ? CupertinoTheme.of(context).primaryColor + : Theme.of(context).colorScheme.secondary); } else if (_openEarable.bleManager.connectingDevice?.id == id) { - return const SizedBox( + return SizedBox( height: 24, width: 24, - child: CircularProgressIndicator(strokeWidth: 2)); + child: Platform.isIOS + ? CupertinoActivityIndicator() + : CircularProgressIndicator(strokeWidth: 2)); } return const SizedBox.shrink(); } diff --git a/open_earable/lib/widgets/dynamic_value_picker.dart b/open_earable/lib/widgets/dynamic_value_picker.dart index ca514ee..1fc615a 100644 --- a/open_earable/lib/widgets/dynamic_value_picker.dart +++ b/open_earable/lib/widgets/dynamic_value_picker.dart @@ -3,11 +3,11 @@ import 'package:flutter/cupertino.dart'; import 'dart:io'; class DynamicValuePicker extends StatelessWidget { - BuildContext context; + final BuildContext context; final List options; final String currentValue; final Function(String) onValueChange; - final Function(bool) onBoolChange; + final Function(bool) onValueNotZero; final bool isConnected; DynamicValuePicker( @@ -15,7 +15,7 @@ class DynamicValuePicker extends StatelessWidget { this.options, this.currentValue, this.onValueChange, - this.onBoolChange, + this.onValueNotZero, this.isConnected, ); @@ -47,9 +47,9 @@ class DynamicValuePicker extends StatelessWidget { onChanged: (String? newValue) { onValueChange(newValue!); if (int.parse(newValue) != 0) { - onBoolChange(true); + onValueNotZero(true); } else { - onBoolChange(false); + onValueNotZero(false); } }, items: options.map((String value) { @@ -92,9 +92,9 @@ class DynamicValuePicker extends StatelessWidget { int? newValueInt = int.tryParse(newValue); onValueChange(newValue); if (newValueInt != 0) { - onBoolChange(true); + onValueNotZero(true); } else { - onBoolChange(false); + onValueNotZero(false); } }, children: options From a37441c57bcb30571d243ca70f89e48493a9dbcb Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sat, 13 Jan 2024 14:12:07 +0100 Subject: [PATCH 054/104] improve chart axes: pressure and temperature charts no longer start at zero, but dynamically adjust between the min and max Y values. --- .../lib/sensor_data_tab/sensor_chart.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/open_earable/lib/sensor_data_tab/sensor_chart.dart b/open_earable/lib/sensor_data_tab/sensor_chart.dart index 9327df4..de37740 100644 --- a/open_earable/lib/sensor_data_tab/sensor_chart.dart +++ b/open_earable/lib/sensor_data_tab/sensor_chart.dart @@ -85,17 +85,19 @@ class _EarableDataChartState extends State { _checkLength(_data); SensorData? maxXYZValue = maxBy(_data, (SensorData b) => b.getMax()); SensorData? minXYZValue = minBy(_data, (SensorData b) => b.getMin()); + if (maxXYZValue == null || minXYZValue == null) { return; } - double maxAbsValue = - max(maxXYZValue.getMax().abs(), minXYZValue.getMin().abs()); + double maxY = maxXYZValue!.getMax(); + double minY = minXYZValue!.getMin(); + double maxAbsValue = max(maxY.abs(), minY.abs()); _maxY = (_title == "Pressure" || _title == "Temperature") - ? max(0, maxXYZValue.getMax()) + ? maxY : maxAbsValue; _minY = (_title == "Pressure" || _title == "Temperature") - ? min(0, minXYZValue.getMin()) + ? minY : -maxAbsValue; _maxX = value.timestamp; _minX = _data[0].timestamp; @@ -225,8 +227,10 @@ class _EarableDataChartState extends State { ) ], primaryMeasureAxis: charts.NumericAxisSpec( - tickProviderSpec: - charts.BasicNumericTickProviderSpec(desiredTickCount: 7), + tickProviderSpec: charts.BasicNumericTickProviderSpec( + desiredTickCount: 7, + zeroBound: false, + dataIsInWholeNumbers: false), renderSpec: charts.GridlineRendererSpec( labelStyle: charts.TextStyleSpec( fontSize: 14, From e6864f480ec53a5a2477706d6b5c371d66bb5f13 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sat, 13 Jan 2024 14:29:05 +0100 Subject: [PATCH 055/104] hard code chart units --- .../lib/sensor_data_tab/sensor_chart.dart | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/open_earable/lib/sensor_data_tab/sensor_chart.dart b/open_earable/lib/sensor_data_tab/sensor_chart.dart index de37740..1b80915 100644 --- a/open_earable/lib/sensor_data_tab/sensor_chart.dart +++ b/open_earable/lib/sensor_data_tab/sensor_chart.dart @@ -34,6 +34,13 @@ class _EarableDataChartState extends State { late SimpleKalman kalmanX, kalmanY, kalmanZ; late String _sensorName; int _numDatapoints = 200; + Map _units = { + "Accelerometer": "m/s\u00B2", + "Gyroscope": "°/s", + "Magnetometer": "µT", + "Pressure": "Pa", + "Temperature": "°C" + }; _setupListeners() { if (_title == "Pressure" || _title == "Temperature") { _dataSubscription = @@ -168,7 +175,7 @@ class _EarableDataChartState extends State { if (_title == 'Pressure' || _title == 'Temperature') { seriesList = [ charts.Series( - id: '$_title${_data.isNotEmpty ? " (${_data[0].units[_title]})" : ""}', + id: '$_title${_data.isNotEmpty ? " (${_units[_title]})" : ""}', colorFn: (_, __) => charts.Color.fromHex(code: colors[0]), domainFn: (SensorData data, _) => data.timestamp, measureFn: (SensorData data, _) => data.values[0], @@ -178,21 +185,21 @@ class _EarableDataChartState extends State { } else { seriesList = [ charts.Series( - id: 'X${_data.isNotEmpty ? " (${_data[0].units['X']})" : ""}', + id: 'X${_data.isNotEmpty ? " (${_units[_title]})" : ""}', colorFn: (_, __) => charts.Color.fromHex(code: colors[0]), domainFn: (SensorData data, _) => data.timestamp, measureFn: (SensorData data, _) => data.values[0], data: _data, ), charts.Series( - id: 'Y${_data.isNotEmpty ? " (${_data[0].units['Y']})" : ""}', + id: 'Y${_data.isNotEmpty ? " (${_units[_title]})" : ""}', colorFn: (_, __) => charts.Color.fromHex(code: colors[1]), domainFn: (SensorData data, _) => data.timestamp, measureFn: (SensorData data, _) => data.values[1], data: _data, ), charts.Series( - id: 'Z${_data.isNotEmpty ? " (${_data[0].units['Z']})" : ""}', + id: 'Z${_data.isNotEmpty ? " (${_units[_title]})" : ""}', colorFn: (_, __) => charts.Color.fromHex(code: colors[2]), domainFn: (SensorData data, _) => data.timestamp, measureFn: (SensorData data, _) => data.values[2], From dae03f7bfe48c4cc9713cd4962cd35663f0fa3aa Mon Sep 17 00:00:00 2001 From: o-bagge <47336932+o-bagge@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:08:48 +0100 Subject: [PATCH 056/104] Update project.pbxproj Revert changes --- .../ios/Runner.xcodeproj/project.pbxproj | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/open_earable/ios/Runner.xcodeproj/project.pbxproj b/open_earable/ios/Runner.xcodeproj/project.pbxproj index dc894e3..d696e14 100644 --- a/open_earable/ios/Runner.xcodeproj/project.pbxproj +++ b/open_earable/ios/Runner.xcodeproj/project.pbxproj @@ -480,7 +480,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Z74Z97JQJL; + DEVELOPMENT_TEAM = 6DCQ69GP5G; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenEarable; @@ -490,7 +490,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "jump-height-test"; + PRODUCT_BUNDLE_IDENTIFIER = edu.kit.teco.openEarable; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -505,7 +505,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = Z74Z97JQJL; + DEVELOPMENT_TEAM = 6DCQ69GP5G; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -525,7 +525,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = Z74Z97JQJL; + DEVELOPMENT_TEAM = 6DCQ69GP5G; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -543,7 +543,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = Z74Z97JQJL; + DEVELOPMENT_TEAM = 6DCQ69GP5G; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; @@ -669,7 +669,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Z74Z97JQJL; + DEVELOPMENT_TEAM = 6DCQ69GP5G; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenEarable; @@ -679,7 +679,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "jump-height-test"; + PRODUCT_BUNDLE_IDENTIFIER = edu.kit.teco.openEarable; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -696,7 +696,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Z74Z97JQJL; + DEVELOPMENT_TEAM = 6DCQ69GP5G; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenEarable; @@ -706,7 +706,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "jump-height-test"; + PRODUCT_BUNDLE_IDENTIFIER = edu.kit.teco.openEarable; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; From f27b7a79c1c2eb5f60c3a556452eca761c4f40e8 Mon Sep 17 00:00:00 2001 From: Philipp Lepold Date: Thu, 18 Jan 2024 16:37:41 +0100 Subject: [PATCH 057/104] possible fix for Xcode Cloud build error --- open_earable/ios/ci_scripts/ci_post_clone.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/open_earable/ios/ci_scripts/ci_post_clone.sh b/open_earable/ios/ci_scripts/ci_post_clone.sh index f888c39..6b7da88 100755 --- a/open_earable/ios/ci_scripts/ci_post_clone.sh +++ b/open_earable/ios/ci_scripts/ci_post_clone.sh @@ -5,8 +5,8 @@ set -e # by default, the execution directory of this script is the ci_scripts directory # CI_WORKSPACE is the directory of your cloned repo -echo "🟩 Navigate from ($PWD) to ($CI_WORKSPACE)" -cd $CI_WORKSPACE +echo "🟩 Navigate from ($PWD) to ($CI_WORKSPACE_PATH)" +cd $CI_WORKSPACE_PATH echo "🟩 Install Flutter" time git clone https://github.com/flutter/flutter.git -b stable $HOME/flutter From 6fd6533d8d9c980b7221021be042c68f0edf091f Mon Sep 17 00:00:00 2001 From: Philipp Lepold Date: Thu, 18 Jan 2024 16:43:01 +0100 Subject: [PATCH 058/104] i hope this fixes it for real --- open_earable/ios/ci_scripts/ci_post_clone.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open_earable/ios/ci_scripts/ci_post_clone.sh b/open_earable/ios/ci_scripts/ci_post_clone.sh index 6b7da88..c30a17d 100755 --- a/open_earable/ios/ci_scripts/ci_post_clone.sh +++ b/open_earable/ios/ci_scripts/ci_post_clone.sh @@ -16,7 +16,7 @@ echo "🟩 Flutter Precache" time flutter precache --ios echo "🟩 Install Flutter Dependencies" -cd open_earable +cd $CI_WORKSPACE_PATH/open_earable time flutter clean time flutter pub get time flutter pub upgrade From 249982cdfd20622b7fb734392f7c909b04ec6b93 Mon Sep 17 00:00:00 2001 From: Philipp Lepold Date: Thu, 18 Jan 2024 16:56:30 +0100 Subject: [PATCH 059/104] still not working. debugging problem --- open_earable/ios/ci_scripts/ci_post_clone.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/open_earable/ios/ci_scripts/ci_post_clone.sh b/open_earable/ios/ci_scripts/ci_post_clone.sh index c30a17d..9daacff 100755 --- a/open_earable/ios/ci_scripts/ci_post_clone.sh +++ b/open_earable/ios/ci_scripts/ci_post_clone.sh @@ -16,7 +16,11 @@ echo "🟩 Flutter Precache" time flutter precache --ios echo "🟩 Install Flutter Dependencies" -cd $CI_WORKSPACE_PATH/open_earable +echo "$(pwd)" +echo "$(ls)" +cd $CI_WORKSPACE_PATH +echo "$(ls)" +cd open_earable time flutter clean time flutter pub get time flutter pub upgrade From 0aced62ed683dec49a72ea1dd6fc227e81b99694 Mon Sep 17 00:00:00 2001 From: Philipp Lepold Date: Thu, 18 Jan 2024 17:08:57 +0100 Subject: [PATCH 060/104] still debugging --- open_earable/ios/ci_scripts/ci_post_clone.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open_earable/ios/ci_scripts/ci_post_clone.sh b/open_earable/ios/ci_scripts/ci_post_clone.sh index 9daacff..0019cb1 100755 --- a/open_earable/ios/ci_scripts/ci_post_clone.sh +++ b/open_earable/ios/ci_scripts/ci_post_clone.sh @@ -18,7 +18,7 @@ time flutter precache --ios echo "🟩 Install Flutter Dependencies" echo "$(pwd)" echo "$(ls)" -cd $CI_WORKSPACE_PATH +cd $CI_WORKSPACE_PATH/repository echo "$(ls)" cd open_earable time flutter clean From ff8e38633b403494d06c985280d1f66ecee61c8a Mon Sep 17 00:00:00 2001 From: Philipp Lepold Date: Thu, 18 Jan 2024 17:53:04 +0100 Subject: [PATCH 061/104] Xcode Cloud error fixed --- open_earable/ios/ci_scripts/ci_post_clone.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/open_earable/ios/ci_scripts/ci_post_clone.sh b/open_earable/ios/ci_scripts/ci_post_clone.sh index 0019cb1..ed9a153 100755 --- a/open_earable/ios/ci_scripts/ci_post_clone.sh +++ b/open_earable/ios/ci_scripts/ci_post_clone.sh @@ -16,11 +16,7 @@ echo "🟩 Flutter Precache" time flutter precache --ios echo "🟩 Install Flutter Dependencies" -echo "$(pwd)" -echo "$(ls)" -cd $CI_WORKSPACE_PATH/repository -echo "$(ls)" -cd open_earable +cd repository/open_earable time flutter clean time flutter pub get time flutter pub upgrade From 9aa7e37ea5005b8e65eaacc1764cbf4eaba0702f Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sun, 21 Jan 2024 14:07:12 +0100 Subject: [PATCH 062/104] add scroll view to fix UI overflowing problems --- open_earable/lib/apps/tightness.dart | 142 +++++++++++++-------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/open_earable/lib/apps/tightness.dart b/open_earable/lib/apps/tightness.dart index 45b8073..e2b0ae8 100644 --- a/open_earable/lib/apps/tightness.dart +++ b/open_earable/lib/apps/tightness.dart @@ -10,8 +10,6 @@ class TightnessMeter extends StatefulWidget { _TightnessMeterState createState() => _TightnessMeterState(_openEarable); } - - class _TightnessMeterState extends State { final OpenEarable _openEarable; StreamSubscription? _imuSubscription; @@ -93,7 +91,7 @@ class _TightnessMeterState extends State { void _isNodTight(DateTime last, DateTime secondToLast) { int difference = last.difference(secondToLast).inMilliseconds.abs(); int expected = _bpmToMilliseconds(bpm); - if (_isWithinMargin(difference, expected, 25.0-difficulty)) { + if (_isWithinMargin(difference, expected, 25.0 - difficulty)) { setState(() { streak += 1; }); @@ -102,14 +100,13 @@ class _TightnessMeterState extends State { setState(() { streak = 0; tightness = 0; - }); } } void _updateScore() { setState(() { - score = score + (((10 * streak) + difficulty) / tightness.abs() ).round(); + score = score + (((10 * streak) + difficulty) / tightness.abs()).round(); }); } @@ -120,7 +117,8 @@ class _TightnessMeterState extends State { double lowerBound = expectedInterval - margin; double upperBound = expectedInterval + margin; setState(() { - tightness = math.min(_bpmToMilliseconds(bpm), (givenInterval - expectedInterval)); + tightness = + math.min(_bpmToMilliseconds(bpm), (givenInterval - expectedInterval)); }); return givenInterval >= lowerBound && givenInterval <= upperBound; } @@ -155,7 +153,6 @@ class _TightnessMeterState extends State { _openEarable.audioPlayer.wavFile(fileName); } - @override Widget build(BuildContext context) { return Scaffold( @@ -163,7 +160,8 @@ class _TightnessMeterState extends State { appBar: AppBar( title: Text('Tightness Meter'), ), - body: Center( + body: SingleChildScrollView( + child: Center( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.only( @@ -280,59 +278,60 @@ class _TightnessMeterState extends State { bottomLeft: Radius.circular(20.0)), ), child: Slider( - thumbColor: Colors.purple, - activeColor: Colors.grey, - secondaryActiveColor: Colors.purpleAccent, - inactiveColor: Colors.grey, - value: tightness.toDouble(), - min: -_bpmToMilliseconds(bpm).toDouble(), - max: _bpmToMilliseconds(bpm).toDouble(), - divisions: 2000, - label: tightness.toString(), - onChanged: (double value) { - setState(() { - }); - }), + thumbColor: Colors.purple, + activeColor: Colors.grey, + secondaryActiveColor: Colors.purpleAccent, + inactiveColor: Colors.grey, + value: tightness.toDouble(), + min: -_bpmToMilliseconds(bpm).toDouble(), + max: _bpmToMilliseconds(bpm).toDouble(), + divisions: 2000, + label: tightness.toString(), + onChanged: (double value) { + setState(() {}); + }), ), ), Padding( - padding: const EdgeInsets.only(bottom:20.0), - child: Row(children: [ - Spacer(), - Text('Early'), - Spacer(flex: 5), - Text('Tight'), - Spacer(flex: 5), - Text('Late'), - Spacer() - ], + padding: const EdgeInsets.only(bottom: 20.0), + child: Row( + children: [ + Spacer(), + Text('Early'), + Spacer(flex: 5), + Text('Tight'), + Spacer(flex: 5), + Text('Late'), + Spacer() + ], ), ), ], ), ), - Card(margin: EdgeInsets.all(20), + Card( + margin: EdgeInsets.all(20), color: Colors.black, child: Column( - children: [ - Padding( - padding: EdgeInsets.all(20), - child: ElevatedButton( - onPressed: startStopMonitoring, - style: ElevatedButton.styleFrom( - minimumSize: Size(1000, 80), - backgroundColor: _monitoring - ? Color(0xfff27777) - : Theme.of(context).colorScheme.secondary, - foregroundColor: Colors.black, - ), - child: Text( - _monitoring ? 'Stop' : 'Start', - style: TextStyle(fontSize: 30), - ), + children: [ + Padding( + padding: EdgeInsets.all(20), + child: ElevatedButton( + onPressed: startStopMonitoring, + style: ElevatedButton.styleFrom( + minimumSize: Size(1000, 80), + backgroundColor: _monitoring + ? Color(0xfff27777) + : Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.black, + ), + child: Text( + _monitoring ? 'Stop' : 'Start', + style: TextStyle(fontSize: 30), ), ), - ], + ), + ], ), ), Card( @@ -345,8 +344,7 @@ class _TightnessMeterState extends State { child: Row( children: [ Spacer(), - Text('BPM', - style: TextStyle(fontSize: 30)), + Text('BPM', style: TextStyle(fontSize: 30)), Spacer(flex: 5), DropdownButton( style: TextStyle( @@ -354,12 +352,15 @@ class _TightnessMeterState extends State { ), value: bpm, icon: const Icon(Icons.arrow_drop_down), - onChanged: _monitoring ? null :(int? newValue) { - setState(() { - bpm = newValue!; - }); - }, - items: bpmList.map>((int value) { + onChanged: _monitoring + ? null + : (int? newValue) { + setState(() { + bpm = newValue!; + }); + }, + items: + bpmList.map>((int value) { return DropdownMenuItem( value: value, child: Text(value.toString()), @@ -372,9 +373,8 @@ class _TightnessMeterState extends State { ), Padding( padding: EdgeInsets.only(top: 20), - child: Text('Sensitivity', - style: TextStyle(fontSize: 20) - ), + child: + Text('Sensitivity', style: TextStyle(fontSize: 20)), ), Padding( padding: EdgeInsets.all(16), @@ -395,22 +395,21 @@ class _TightnessMeterState extends State { nodThreshold = value; }); }), - Row(children: [ - Spacer(), - Text('Cool Nodding'), - Spacer(flex: 10), - Text('Headbanging'), - Spacer() - ], + Row( + children: [ + Spacer(), + Text('Cool Nodding'), + Spacer(flex: 10), + Text('Headbanging'), + Spacer() + ], ), ], ), ), Padding( padding: EdgeInsets.only(top: 10), - child: Text('Difficulty', - style: TextStyle(fontSize: 20) - ), + child: Text('Difficulty', style: TextStyle(fontSize: 20)), ), Padding( padding: EdgeInsets.all(16), @@ -446,10 +445,11 @@ class _TightnessMeterState extends State { ], ), ), + SizedBox(height: 40), ], ), ), - ), + )), ); } } From 416fd358a3a215120585231af4a4047c0b936c91 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Thu, 25 Jan 2024 19:03:07 +0100 Subject: [PATCH 063/104] recorder now shows recordings even if earable is disconnected --- .../ios/Flutter/AppFrameworkInfo.plist | 2 +- open_earable/ios/Podfile | 2 +- open_earable/ios/Podfile.lock | 6 +- .../ios/Runner.xcodeproj/project.pbxproj | 6 +- open_earable/lib/apps/recorder.dart | 114 ++++++++++-------- open_earable/pubspec.lock | 92 +++++++------- 6 files changed, 117 insertions(+), 105 deletions(-) diff --git a/open_earable/ios/Flutter/AppFrameworkInfo.plist b/open_earable/ios/Flutter/AppFrameworkInfo.plist index 9625e10..7c56964 100644 --- a/open_earable/ios/Flutter/AppFrameworkInfo.plist +++ b/open_earable/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/open_earable/ios/Podfile b/open_earable/ios/Podfile index fdcc671..d97f17e 100644 --- a/open_earable/ios/Podfile +++ b/open_earable/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +# platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/open_earable/ios/Podfile.lock b/open_earable/ios/Podfile.lock index fd2fbe4..823e9b1 100644 --- a/open_earable/ios/Podfile.lock +++ b/open_earable/ios/Podfile.lock @@ -58,17 +58,17 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_gl: 5a5603f35db897697f064027864a32b15d0c421d flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 Protobuf: 970f7ee93a3a08e3cf64859b8efd95ee32b4f87f reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c SwiftProtobuf: b70d65f419fbfe61a2d58003456ca5da58e337d6 three3d_egl: de2cd4950ad2d5f2122166c36583bde4c812e7b5 -PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189 +PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 COCOAPODS: 1.12.1 diff --git a/open_earable/ios/Runner.xcodeproj/project.pbxproj b/open_earable/ios/Runner.xcodeproj/project.pbxproj index d696e14..f03d53c 100644 --- a/open_earable/ios/Runner.xcodeproj/project.pbxproj +++ b/open_earable/ios/Runner.xcodeproj/project.pbxproj @@ -463,7 +463,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; @@ -601,7 +601,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -650,7 +650,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; diff --git a/open_earable/lib/apps/recorder.dart b/open_earable/lib/apps/recorder.dart index 8fdfbf6..3e2c89b 100644 --- a/open_earable/lib/apps/recorder.dart +++ b/open_earable/lib/apps/recorder.dart @@ -236,9 +236,7 @@ class _RecorderState extends State { appBar: AppBar( title: Text('Recorder'), ), - body: _openEarable.bleManager.connected - ? _recorderWidget() - : EarableNotConnectedWarning()); + body: _recorderWidget()); } Widget _recorderWidget() { @@ -246,54 +244,68 @@ class _RecorderState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Padding( - padding: EdgeInsets.fromLTRB(16, 16, 16, 0), - child: Text( - _formatDuration(_duration), - style: TextStyle( - fontFamily: 'Digital', // This is a common monospaced font - fontSize: 80, - fontWeight: FontWeight.normal, - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.all(16), - child: ElevatedButton( - onPressed: startStopRecording, - style: ElevatedButton.styleFrom( - minimumSize: Size(200, 36), - backgroundColor: _recording - ? Color(0xfff27777) - : Theme.of(context).colorScheme.secondary, - foregroundColor: Colors.black, - ), - child: Text( - _recording ? "Stop Recording" : "Start Recording", - style: TextStyle(fontSize: 20), - ), - ), - ), - DropdownButton( - value: _selectedLabel, - icon: const Icon(Icons.arrow_drop_down), - onChanged: (String? newValue) { - setState(() { - _selectedLabel = newValue!; - }); - }, - items: _labels.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - ], - ), + Container( + height: 200, + child: !_openEarable.bleManager.connected + ? EarableNotConnectedWarning() + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Text( + _formatDuration(_duration), + style: TextStyle( + fontFamily: + 'Digital', // This is a common monospaced font + fontSize: 80, + fontWeight: FontWeight.normal, + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.all(16), + child: ElevatedButton( + onPressed: startStopRecording, + style: ElevatedButton.styleFrom( + minimumSize: Size(200, 36), + backgroundColor: _recording + ? Color(0xfff27777) + : Theme.of(context) + .colorScheme + .secondary, + foregroundColor: Colors.black, + ), + child: Text( + _recording + ? "Stop Recording" + : "Start Recording", + style: TextStyle(fontSize: 20), + ), + ), + ), + DropdownButton( + value: _selectedLabel, + icon: const Icon(Icons.arrow_drop_down), + onChanged: (String? newValue) { + setState(() { + _selectedLabel = newValue!; + }); + }, + items: _labels.map>( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ], + ), + ])), Text("Recordings", style: TextStyle(fontSize: 20.0)), Divider( thickness: 2, diff --git a/open_earable/pubspec.lock b/open_earable/pubspec.lock index 1e6d936..d373630 100644 --- a/open_earable/pubspec.lock +++ b/open_earable/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: archive - sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.4.9" + version: "3.4.10" args: dependency: transitive description: @@ -133,10 +133,10 @@ packages: dependency: "direct main" description: name: ditredi - sha256: "5bed909dfe632f977d412b7c324a724a49c4f33c221023cf07bb5b1762af59c4" + sha256: "0b6c5334fa4198a3e880d1865eece06189edf91ae894087ba67d347ead5aae4e" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.2" english_words: dependency: "direct main" description: @@ -243,10 +243,10 @@ packages: dependency: "direct main" description: name: flutter_reactive_ble - sha256: "7a0d245412dc8e1b72ce2adc423808583b42ce824b1be74001ff22c8bb5ada48" + sha256: e2184b7793f0da1bd522a6296995f2b7a2a6ca28c57419cb5f5129b4cbdccf02 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -358,7 +358,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "6841c0b704dffbe559961ea80dccf73abf0c293a" + resolved-ref: "3bd982d22902cfa8ba90a290951658c3ad40a183" url: "https://github.com/OpenEarable/open_earable_flutter.git" source: git version: "0.0.2" @@ -390,26 +390,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -422,10 +422,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -438,82 +438,82 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "860c6b871c94c78e202dc69546d4d8fd84bd59faeb36f8fb9888668a53ff4f78" + sha256: "45ff3fbcb99040fde55c528d5e3e6ca29171298a85436274d49c6201002087d6" url: "https://pub.dev" source: hosted - version: "11.1.0" + version: "11.2.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "2f1bec180ee2f5665c22faada971a8f024761f632e93ddc23310487df52dcfa6" + sha256: "758284a0976772f9c744d6384fc5dc4834aa61e3f7aa40492927f244767374eb" url: "https://pub.dev" source: hosted - version: "12.0.1" + version: "12.0.3" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "1a816084338ada8d574b1cb48390e6e8b19305d5120fe3a37c98825bacc78306" + sha256: c6bf440f80acd2a873d3d91a699e4cc770f86e7e6b576dda98759e8b92b39830 url: "https://pub.dev" source: hosted - version: "9.2.0" + version: "9.3.0" permission_handler_html: dependency: transitive description: name: permission_handler_html - sha256: "11b762a8c123dced6461933a88ea1edbbe036078c3f9f41b08886e678e7864df" + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" url: "https://pub.dev" source: hosted - version: "0.1.0+2" + version: "0.1.1" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: d87349312f7eaf6ce0adaf668daf700ac5b06af84338bd8b8574dfbd93ffe1a1 + sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.0" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: "1e8640c1e39121128da6b816d236e714d2cf17fac5a105dd6acdd3403a628004" + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" petitparser: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" platform: dependency: transitive description: name: platform - sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.7.4" protobuf: dependency: transitive description: @@ -534,18 +534,18 @@ packages: dependency: transitive description: name: reactive_ble_mobile - sha256: e4623446d5fd6e641c984892ee1fa7c67499a2bb0971d85a500815e1d05db6fb + sha256: fd4a27cd9753fd50480a6351f7aa75703cff4b504060e4b2dbf7dafde20d03bb url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" reactive_ble_platform_interface: dependency: transitive description: name: reactive_ble_platform_interface - sha256: "8988d16497886dccc69dca1c3eebce28ae387371f3f948a4f1b03dec9954fb05" + sha256: "5a6e7e73d5c3ac778aa72f2c325ed37edfc6cb9029c66d5e2e8cc6a9c9c113ee" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" simple_kalman: dependency: "direct main" description: @@ -677,26 +677,26 @@ packages: dependency: transitive description: name: win32 - sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f" + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.2.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" yaml: dependency: transitive description: @@ -706,5 +706,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.16.0" From a4bafc33928e46c5f4cadaf96a44d5083775b218 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Thu, 25 Jan 2024 19:22:51 +0100 Subject: [PATCH 064/104] recorder page now updates when earable disconnects --- open_earable/lib/apps/recorder.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/open_earable/lib/apps/recorder.dart b/open_earable/lib/apps/recorder.dart index 3e2c89b..bc438bf 100644 --- a/open_earable/lib/apps/recorder.dart +++ b/open_earable/lib/apps/recorder.dart @@ -26,6 +26,7 @@ class _RecorderState extends State { late String _selectedLabel; Timer? _timer; Duration _duration = Duration(); + StreamSubscription? _connectionStateSubscription; @override void initState() { super.initState(); @@ -95,6 +96,7 @@ class _RecorderState extends State { _timer?.cancel(); _imuSubscription?.cancel(); _barometerSubscription?.cancel(); + _connectionStateSubscription?.cancel(); } Future listFilesInDocumentsDirectory() async { @@ -114,6 +116,10 @@ class _RecorderState extends State { } _setupListeners() { + _connectionStateSubscription = + _openEarable.bleManager.connectionStateStream.listen((_) { + setState(() {}); // rebuild page when connection state changes + }); _imuSubscription = _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { if (!_recording) { From f0579d495924c3a9c415b2092e40c6e5a3351b3a Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Wed, 31 Jan 2024 13:08:57 +0100 Subject: [PATCH 065/104] start implementing ble autoconnect --- open_earable/lib/ble.dart | 142 +++++++----------- open_earable/lib/ble_controller.dart | 61 ++++++++ .../lib/controls_tab/views/connect.dart | 71 ++++++++- open_earable/lib/main.dart | 6 +- 4 files changed, 186 insertions(+), 94 deletions(-) create mode 100644 open_earable/lib/ble_controller.dart diff --git a/open_earable/lib/ble.dart b/open_earable/lib/ble.dart index 346a0a5..bf1f452 100644 --- a/open_earable/lib/ble.dart +++ b/open_earable/lib/ble.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:open_earable/ble_controller.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:provider/provider.dart'; class BLEPage extends StatefulWidget { final OpenEarable openEarable; @@ -13,33 +15,12 @@ class BLEPage extends StatefulWidget { } class _BLEPageState extends State { - String _openEarableName = "OpenEarable"; late OpenEarable _openEarable; - StreamSubscription? _scanSubscription; - StreamSubscription? _connectionStateStream; - List discoveredDevices = []; - - @override - void dispose() { - _scanSubscription?.cancel(); - _connectionStateStream?.cancel(); - super.dispose(); - } - @override void initState() { super.initState(); _openEarable = widget.openEarable; - - _startScanning(); - _setupListeners(); - } - - void _setupListeners() async { - _connectionStateStream = - _openEarable.bleManager.connectionStateStream.listen((connected) { - setState(() {}); - }); + Provider.of(context, listen: false).startScanning(); } @override @@ -63,50 +44,52 @@ class _BLEPageState extends State { ), ), ), - Visibility( - visible: discoveredDevices.isNotEmpty, - child: Container( - margin: const EdgeInsets.fromLTRB(16, 0, 16, 0), - decoration: BoxDecoration( - border: Border.all( - color: Colors.grey, - width: 1.0, + Consumer(builder: (context, controller, child) { + return Visibility( + visible: controller.discoveredDevices.isNotEmpty, + child: Container( + margin: const EdgeInsets.fromLTRB(16, 0, 16, 0), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey, + width: 1.0, + ), + borderRadius: BorderRadius.circular(8.0), ), - borderRadius: BorderRadius.circular(8.0), - ), - child: ListView.builder( - padding: EdgeInsets.zero, - physics: - const NeverScrollableScrollPhysics(), // Disable scrolling, - shrinkWrap: true, - itemCount: discoveredDevices.length, - itemBuilder: (BuildContext context, int index) { - final device = discoveredDevices[index]; - return Column(children: [ - Material( - type: MaterialType.transparency, - child: ListTile( - selectedTileColor: Colors.grey, - title: Text(device.name), - titleTextStyle: const TextStyle(fontSize: 16), - visualDensity: const VisualDensity( - horizontal: -4, vertical: -4), - trailing: _buildTrailingWidget(device.id), - onTap: () { - _connectToDevice(device); - }, - )), - if (index != discoveredDevices.length - 1) - const Divider( - height: 1.0, - thickness: 1.0, - color: Colors.grey, - indent: 16.0, - endIndent: 0.0, - ), - ]); - }, - ))), + child: ListView.builder( + padding: EdgeInsets.zero, + physics: + const NeverScrollableScrollPhysics(), // Disable scrolling, + shrinkWrap: true, + itemCount: controller.discoveredDevices.length, + itemBuilder: (BuildContext context, int index) { + final device = controller.discoveredDevices[index]; + return Column(children: [ + Material( + type: MaterialType.transparency, + child: ListTile( + selectedTileColor: Colors.grey, + title: Text(device.name), + titleTextStyle: const TextStyle(fontSize: 16), + visualDensity: const VisualDensity( + horizontal: -4, vertical: -4), + trailing: _buildTrailingWidget(device.id), + onTap: () { + controller.connectToDevice(device); + }, + )), + if (index != controller.discoveredDevices.length - 1) + const Divider( + height: 1.0, + thickness: 1.0, + color: Colors.grey, + indent: 16.0, + endIndent: 0.0, + ), + ]); + }, + ))); + }), Center( child: Padding( padding: EdgeInsets.all(16), @@ -121,7 +104,9 @@ class _BLEPageState extends State { ))), Center( child: ElevatedButton( - onPressed: _startScanning, + onPressed: + Provider.of(context, listen: false) + .startScanning, style: ButtonStyle( foregroundColor: MaterialStateProperty.all( Theme.of(context).colorScheme.primary), @@ -150,29 +135,4 @@ class _BLEPageState extends State { } return const SizedBox.shrink(); } - - void _startScanning() async { - discoveredDevices = []; - if (_openEarable.bleManager.connectedDevice != null) { - discoveredDevices.add(_openEarable.bleManager.connectedDevice); - } - setState(() {}); // Update UI - await _openEarable.bleManager.startScan(); - _scanSubscription?.cancel(); - _scanSubscription = - _openEarable.bleManager.scanStream.listen((incomingDevice) { - if (incomingDevice.name.isNotEmpty && - incomingDevice.name.contains(_openEarableName) && - !discoveredDevices.any((device) => device.id == incomingDevice.id)) { - setState(() { - discoveredDevices.add(incomingDevice); - }); - } - }); - } - - void _connectToDevice(device) { - _scanSubscription?.cancel(); - _openEarable.bleManager.connectToDevice(device); - } } diff --git a/open_earable/lib/ble_controller.dart b/open_earable/lib/ble_controller.dart new file mode 100644 index 0000000..4904c29 --- /dev/null +++ b/open_earable/lib/ble_controller.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; + +class BluetoothController extends ChangeNotifier { + String _openEarableName = "OpenEarable"; + OpenEarable? _openEarable; + StreamSubscription? _scanSubscription; + StreamSubscription? _connectionStateSubscription; + + List _discoveredDevices = []; + List get discoveredDevices => _discoveredDevices; + + void set openEarable(OpenEarable openEarable) { + _openEarable = openEarable; + _connectionStateSubscription?.cancel(); + _connectionStateSubscription = + _openEarable?.bleManager.connectionStateStream.listen((event) { + notifyListeners(); + print("Connections tate stream"); + print(event); + }); + } + + @override + void dispose() { + super.dispose(); + _scanSubscription?.cancel(); + _connectionStateSubscription?.cancel(); + } + + Future startScanning() async { + _discoveredDevices = []; + print("START SCAN"); + if (_openEarable == null) { + return; + } + if (_openEarable?.bleManager.connectedDevice != null) { + discoveredDevices.add((_openEarable?.bleManager.connectedDevice)!); + } + await _openEarable?.bleManager.startScan(); + _scanSubscription?.cancel(); + _scanSubscription = + _openEarable?.bleManager.scanStream.listen((incomingDevice) { + if (incomingDevice.name.isNotEmpty && + incomingDevice.name.contains(_openEarableName) && + !discoveredDevices.any((device) => device.id == incomingDevice.id)) { + discoveredDevices.add(incomingDevice); + print("NEW DEVICE"); + notifyListeners(); + } + }); + } + + void connectToDevice(device) { + _openEarable?.bleManager.connectToDevice(device); + notifyListeners(); + } +} diff --git a/open_earable/lib/controls_tab/views/connect.dart b/open_earable/lib/controls_tab/views/connect.dart index 7da1668..98a9515 100644 --- a/open_earable/lib/controls_tab/views/connect.dart +++ b/open_earable/lib/controls_tab/views/connect.dart @@ -1,13 +1,48 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; +import 'package:open_earable/ble_controller.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:provider/provider.dart'; import '../../ble.dart'; -class ConnectCard extends StatelessWidget { +class ConnectCard extends StatefulWidget { final OpenEarable _openEarable; final int _earableSOC; - ConnectCard(this._openEarable, this._earableSOC); + @override + _ConnectCard createState() => + _ConnectCard(this._openEarable, this._earableSOC); +} + +class _ConnectCard extends State { + final OpenEarable _openEarable; + final int _earableSOC; + bool? _autoConnectEnabled = false; + StreamSubscription? _scanSubscription; + + _ConnectCard(this._openEarable, this._earableSOC); + + @override + void initState() { + super.initState(); + startAutoConnectScan(); + } + + @override + void dispose() { + super.dispose(); + _scanSubscription?.cancel(); + } + + void startAutoConnectScan() { + if (_autoConnectEnabled == true) { + Provider.of(context, listen: false).startScanning(); + } + } + @override Widget build(BuildContext context) { return Padding( @@ -27,6 +62,29 @@ class ConnectCard extends StatelessWidget { fontWeight: FontWeight.bold, ), ), + Consumer( + builder: (context, bleController, child) { + List devices = + bleController.discoveredDevices; + print("notified listeners wihth new devices: $devices"); + tryAutoconnect(devices, bleController); + return Row( + children: [ + Checkbox( + checkColor: Theme.of(context).colorScheme.primary, + //fillColor: Theme.of(context).colorScheme.primary, + value: _autoConnectEnabled, + onChanged: (value) => { + setState(() { + _autoConnectEnabled = value; + startAutoConnectScan(); + }) + }, + ), + Text("Connect to OpenEarable automatically") + ], + ); + }), SizedBox(height: 5), _getEarableInfo(), _getConnectButton(context), @@ -105,4 +163,13 @@ class ConnectCard extends StatelessWidget { ), ); } + + void tryAutoconnect( + List devices, BluetoothController bleController) async { + if (_autoConnectEnabled == true && + devices.isNotEmpty && + _openEarable.bleManager.connectingDevice?.name != devices[0].name) { + _openEarable.bleManager.connectToDevice(devices[0]); + } + } } diff --git a/open_earable/lib/main.dart b/open_earable/lib/main.dart index 55f4f11..ee7f8fa 100644 --- a/open_earable/lib/main.dart +++ b/open_earable/lib/main.dart @@ -12,8 +12,10 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:open_earable/controls_tab/models/open_earable_settings.dart'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:app_settings/app_settings.dart'; +import 'ble_controller.dart'; -void main() => runApp(MyApp()); +void main() => runApp(ChangeNotifierProvider( + create: (context) => BluetoothController(), child: MyApp())); class MyApp extends StatelessWidget { @override @@ -60,6 +62,8 @@ class _MyHomePageState extends State { alertOpen = false; _checkBLEPermission(); _openEarable = OpenEarable(); + Provider.of(context, listen: false).openEarable = + _openEarable; _widgetOptions = [ ControlTab(_openEarable), SensorDataTab(_openEarable), From 8356ec33646d2cac0641184028176d4593b44cf6 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sun, 4 Feb 2024 18:09:59 +0100 Subject: [PATCH 066/104] add autoconnect feature --- open_earable/ios/Podfile.lock | 7 + open_earable/lib/ble_controller.dart | 54 ++++++- .../lib/controls_tab/controls_tab.dart | 40 +----- .../lib/controls_tab/views/audio_player.dart | 136 ++++++++---------- .../lib/controls_tab/views/connect.dart | 107 ++++++++------ .../lib/controls_tab/views/led_color.dart | 105 +++++++------- .../views/sensor_configuration.dart | 44 +++--- .../Flutter/GeneratedPluginRegistrant.swift | 2 + open_earable/pubspec.lock | 64 +++++++++ open_earable/pubspec.yaml | 1 + 10 files changed, 327 insertions(+), 233 deletions(-) diff --git a/open_earable/ios/Podfile.lock b/open_earable/ios/Podfile.lock index 823e9b1..3e0037f 100644 --- a/open_earable/ios/Podfile.lock +++ b/open_earable/ios/Podfile.lock @@ -19,6 +19,9 @@ PODS: - Flutter - Protobuf (~> 3.5) - SwiftProtobuf (~> 1.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - SwiftProtobuf (1.23.0) - three3d_egl (0.1.3) @@ -31,6 +34,7 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - reactive_ble_mobile (from `.symlinks/plugins/reactive_ble_mobile/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) SPEC REPOS: trunk: @@ -55,6 +59,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" reactive_ble_mobile: :path: ".symlinks/plugins/reactive_ble_mobile/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" SPEC CHECKSUMS: app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc @@ -66,6 +72,7 @@ SPEC CHECKSUMS: permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 Protobuf: 970f7ee93a3a08e3cf64859b8efd95ee32b4f87f reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 SwiftProtobuf: b70d65f419fbfe61a2d58003456ca5da58e337d6 three3d_egl: de2cd4950ad2d5f2122166c36583bde4c812e7b5 diff --git a/open_earable/lib/ble_controller.dart b/open_earable/lib/ble_controller.dart index 4904c29..01f667d 100644 --- a/open_earable/lib/ble_controller.dart +++ b/open_earable/lib/ble_controller.dart @@ -1,26 +1,50 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class BluetoothController extends ChangeNotifier { + late SharedPreferences prefs; + + BluetoothController() { + _initializeSharedPreferences(); + } + String _openEarableName = "OpenEarable"; OpenEarable? _openEarable; StreamSubscription? _scanSubscription; StreamSubscription? _connectionStateSubscription; + StreamSubscription? _batteryLevelSubscription; List _discoveredDevices = []; List get discoveredDevices => _discoveredDevices; + bool _scanning = false; + + bool _connected = false; + bool get connected => _connected; + + int? _earableSOC = null; + int? get earableSOC => _earableSOC; + + Future _initializeSharedPreferences() async { + prefs = await SharedPreferences.getInstance(); + } + void set openEarable(OpenEarable openEarable) { _openEarable = openEarable; _connectionStateSubscription?.cancel(); + _connectionStateSubscription = - _openEarable?.bleManager.connectionStateStream.listen((event) { + _openEarable?.bleManager.connectionStateStream.listen((connected) { + _connected = connected; + if (connected) { + _getSOC(); + } notifyListeners(); - print("Connections tate stream"); - print(event); }); } @@ -29,11 +53,24 @@ class BluetoothController extends ChangeNotifier { super.dispose(); _scanSubscription?.cancel(); _connectionStateSubscription?.cancel(); + _batteryLevelSubscription?.cancel(); + } + + void _getSOC() { + _batteryLevelSubscription = _openEarable?.sensorManager + .getBatteryLevelStream() + .listen((batteryLevel) { + _earableSOC = batteryLevel[0].toInt(); + notifyListeners(); + }); } Future startScanning() async { + if (_scanning) { + return; + } + _scanning = true; _discoveredDevices = []; - print("START SCAN"); if (_openEarable == null) { return; } @@ -42,20 +79,25 @@ class BluetoothController extends ChangeNotifier { } await _openEarable?.bleManager.startScan(); _scanSubscription?.cancel(); + _scanning = false; _scanSubscription = _openEarable?.bleManager.scanStream.listen((incomingDevice) { if (incomingDevice.name.isNotEmpty && incomingDevice.name.contains(_openEarableName) && !discoveredDevices.any((device) => device.id == incomingDevice.id)) { discoveredDevices.add(incomingDevice); - print("NEW DEVICE"); notifyListeners(); } }); } void connectToDevice(device) { + if (device.name == _openEarable?.bleManager.connectedDevice?.name) { + return; + } + _scanSubscription?.cancel(); + _scanning = false; _openEarable?.bleManager.connectToDevice(device); - notifyListeners(); + prefs.setString("lastConnectedDeviceName", device.name); } } diff --git a/open_earable/lib/controls_tab/controls_tab.dart b/open_earable/lib/controls_tab/controls_tab.dart index dfe3187..33b6109 100644 --- a/open_earable/lib/controls_tab/controls_tab.dart +++ b/open_earable/lib/controls_tab/controls_tab.dart @@ -5,7 +5,6 @@ import 'views/connect.dart'; import 'views/led_color.dart'; import 'views/audio_player.dart'; import 'dart:async'; -import 'models/open_earable_settings.dart'; class ControlTab extends StatefulWidget { final OpenEarable _openEarable; @@ -20,8 +19,6 @@ class _ControlTabState extends State { StreamSubscription? _connectionStateSubscription; StreamSubscription? _batteryLevelSubscription; - bool connected = false; - int earableSOC = 0; bool earableCharging = false; @override @@ -31,41 +28,6 @@ class _ControlTabState extends State { _batteryLevelSubscription?.cancel(); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _connectionStateSubscription = - _openEarable.bleManager.connectionStateStream.listen((connected) { - OpenEarableSettings().resetState(); - setState(() { - this.connected = connected; - - if (connected) { - getNameAndSOC(); - } - }); - }); - } - - @override - void initState() { - super.initState(); - connected = _openEarable.bleManager.connected; - if (connected) { - getNameAndSOC(); - } - } - - void getNameAndSOC() { - _batteryLevelSubscription = _openEarable.sensorManager - .getBatteryLevelStream() - .listen((batteryLevel) { - setState(() { - earableSOC = batteryLevel[0].toInt(); - }); - }); - } - @override Widget build(BuildContext context) { return SingleChildScrollView( @@ -77,7 +39,7 @@ class _ControlTabState extends State { SizedBox( height: 5, ), - ConnectCard(_openEarable, earableSOC), + ConnectCard(_openEarable), SensorConfigurationCard(_openEarable), AudioPlayerCard(_openEarable), LEDColorCard(_openEarable), diff --git a/open_earable/lib/controls_tab/views/audio_player.dart b/open_earable/lib/controls_tab/views/audio_player.dart index f5224b4..a52fad8 100644 --- a/open_earable/lib/controls_tab/views/audio_player.dart +++ b/open_earable/lib/controls_tab/views/audio_player.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:open_earable/ble_controller.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:provider/provider.dart'; import '../models/open_earable_settings.dart'; class AudioPlayerCard extends StatefulWidget { @@ -19,7 +21,7 @@ class _AudioPlayerCardState extends State { late TextEditingController _frequencyTextController; late TextEditingController _frequencyVolumeTextController; late TextEditingController _waveFormTextController; - + late bool _connected; @override void initState() { super.initState(); @@ -33,10 +35,12 @@ class _AudioPlayerCardState extends State { text: "${OpenEarableSettings().selectedFrequencyVolume}"); _waveFormTextController = TextEditingController(text: OpenEarableSettings().selectedWaveForm); + _connected = + Provider.of(context, listen: false).connected; } void updateText() { - if (_openEarable.bleManager.connected) { + if (_connected) { OpenEarableSettings().selectedFilename = _filenameTextController.text; OpenEarableSettings().selectedJingle = _jingleTextController.text; OpenEarableSettings().selectedFrequency = _frequencyTextController.text; @@ -160,7 +164,7 @@ class _AudioPlayerCardState extends State { mainAxisSize: MainAxisSize.min, children: soundsMap.values.map((String option) { return ListTile( - onTap: _openEarable.bleManager.connected + onTap: _connected ? () { setState(() { textController.text = option; @@ -179,41 +183,45 @@ class _AudioPlayerCardState extends State { @override Widget build(BuildContext context) { - updateText(); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 5.0), - child: Card( - //Audio Player Card - color: Theme.of(context).colorScheme.primary, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Audio Player', - style: TextStyle( - color: Colors.white, - fontSize: 18.0, - fontWeight: FontWeight.bold, + padding: const EdgeInsets.symmetric(horizontal: 5.0), + child: Selector( + selector: (_, bleController) => bleController.connected, + builder: (context, connected, child) { + _connected = connected; + updateText(); + return Card( + //Audio Player Card + color: Theme.of(context).colorScheme.primary, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Audio Player', + style: TextStyle( + color: Colors.white, + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), + ), + _getFileNameRow(), + _getJingleRow(), + _getFrequencyRow(), + _getButtonRow(), + ], + ), ), - ), - _getFileNameRow(), - _getJingleRow(), - _getFrequencyRow(), - _getButtonRow(), - ], - ), - ), - ), - ); + ); + })); } Widget _getAudioPlayerRadio(int index) { return Radio( value: index, groupValue: OpenEarableSettings().selectedAudioPlayerRadio, - onChanged: !_openEarable.bleManager.connected + onChanged: !_connected ? null : (int? value) { setState(() { @@ -239,23 +247,16 @@ class _AudioPlayerCardState extends State { child: TextField( controller: _filenameTextController, obscureText: false, - enabled: _openEarable.bleManager.connected, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), + enabled: _connected, + style: TextStyle(color: _connected ? Colors.black : Colors.grey), decoration: InputDecoration( contentPadding: EdgeInsets.all(10), border: OutlineInputBorder(), floatingLabelBehavior: FloatingLabelBehavior.never, - labelStyle: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), + labelStyle: + TextStyle(color: _connected ? Colors.black : Colors.grey), filled: true, - fillColor: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey[200], + fillColor: _connected ? Colors.white : Colors.grey[200], ), ), ), @@ -272,7 +273,7 @@ class _AudioPlayerCardState extends State { child: SizedBox( height: 37.0, child: InkWell( - onTap: _openEarable.bleManager.connected + onTap: _connected ? () { _showSoundPicker(context, OpenEarableSettings().jingleMap, _jingleTextController); @@ -313,23 +314,16 @@ class _AudioPlayerCardState extends State { child: TextField( controller: _frequencyTextController, textAlign: TextAlign.end, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), + style: TextStyle(color: _connected ? Colors.black : Colors.grey), decoration: InputDecoration( contentPadding: EdgeInsets.all(10), floatingLabelBehavior: FloatingLabelBehavior.never, border: OutlineInputBorder(), labelText: '440', filled: true, - labelStyle: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), - fillColor: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey[200], + labelStyle: + TextStyle(color: _connected ? Colors.black : Colors.grey), + fillColor: _connected ? Colors.white : Colors.grey[200], ), keyboardType: TextInputType.number, ), @@ -340,7 +334,7 @@ class _AudioPlayerCardState extends State { child: Text( 'Hz', style: TextStyle( - color: _openEarable.bleManager.connected + color: _connected ? Colors.white : Colors.grey), // Set text color to white ), @@ -353,10 +347,7 @@ class _AudioPlayerCardState extends State { controller: _frequencyVolumeTextController, textAlign: TextAlign.end, autofocus: false, - style: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), + style: TextStyle(color: _connected ? Colors.black : Colors.grey), decoration: InputDecoration( contentPadding: EdgeInsets.all(10), floatingLabelBehavior: FloatingLabelBehavior.never, @@ -365,13 +356,9 @@ class _AudioPlayerCardState extends State { filled: true, isDense: true, counterText: "", - labelStyle: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), - fillColor: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey[200], + labelStyle: + TextStyle(color: _connected ? Colors.black : Colors.grey), + fillColor: _connected ? Colors.white : Colors.grey[200], ), maxLength: 3, maxLines: 1, @@ -383,7 +370,7 @@ class _AudioPlayerCardState extends State { child: Text( '%', style: TextStyle( - color: _openEarable.bleManager.connected + color: _connected ? Colors.white : Colors.grey), // Set text color to white ), @@ -393,7 +380,7 @@ class _AudioPlayerCardState extends State { height: 37.0, width: 107, child: InkWell( - onTap: _openEarable.bleManager.connected + onTap: _connected ? () { _showSoundPicker(context, OpenEarableSettings().waveFormMap, _waveFormTextController); @@ -433,9 +420,7 @@ class _AudioPlayerCardState extends State { SizedBox( width: 120, child: ElevatedButton( - onPressed: _openEarable.bleManager.connected - ? _setSourceButtonPressed - : null, + onPressed: _connected ? _setSourceButtonPressed : null, style: ElevatedButton.styleFrom( backgroundColor: Color(0xff53515b), foregroundColor: Colors.white, @@ -444,8 +429,7 @@ class _AudioPlayerCardState extends State { ), ), ElevatedButton( - onPressed: - _openEarable.bleManager.connected ? _playButtonPressed : null, + onPressed: _connected ? _playButtonPressed : null, style: ElevatedButton.styleFrom( backgroundColor: Color(0xff77F2A1), foregroundColor: Colors.black, @@ -453,8 +437,7 @@ class _AudioPlayerCardState extends State { child: Icon(Icons.play_arrow_outlined), ), ElevatedButton( - onPressed: - _openEarable.bleManager.connected ? _pauseButtonPressed : null, + onPressed: _connected ? _pauseButtonPressed : null, style: ElevatedButton.styleFrom( backgroundColor: Color(0xffe0f277), foregroundColor: Colors.black, @@ -462,8 +445,7 @@ class _AudioPlayerCardState extends State { child: Icon(Icons.pause), ), ElevatedButton( - onPressed: - _openEarable.bleManager.connected ? _stopButtonPressed : null, + onPressed: _connected ? _stopButtonPressed : null, style: ElevatedButton.styleFrom( backgroundColor: Color(0xfff27777), foregroundColor: Colors.black, diff --git a/open_earable/lib/controls_tab/views/connect.dart b/open_earable/lib/controls_tab/views/connect.dart index 98a9515..760f7da 100644 --- a/open_earable/lib/controls_tab/views/connect.dart +++ b/open_earable/lib/controls_tab/views/connect.dart @@ -6,38 +6,43 @@ import 'package:open_earable/ble_controller.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import 'package:provider/provider.dart'; import '../../ble.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class ConnectCard extends StatefulWidget { final OpenEarable _openEarable; - final int _earableSOC; - ConnectCard(this._openEarable, this._earableSOC); + ConnectCard(this._openEarable); @override - _ConnectCard createState() => - _ConnectCard(this._openEarable, this._earableSOC); + _ConnectCard createState() => _ConnectCard(this._openEarable); } class _ConnectCard extends State { final OpenEarable _openEarable; - final int _earableSOC; bool? _autoConnectEnabled = false; - StreamSubscription? _scanSubscription; + late SharedPreferences prefs; - _ConnectCard(this._openEarable, this._earableSOC); + _ConnectCard(this._openEarable); @override void initState() { super.initState(); - startAutoConnectScan(); + _getPrefs(); + _startAutoConnectScan(); } @override void dispose() { super.dispose(); - _scanSubscription?.cancel(); } - void startAutoConnectScan() { + Future _getPrefs() async { + prefs = await SharedPreferences.getInstance(); + setState(() { + _autoConnectEnabled = prefs.getBool("autoConnectEnabled"); + }); + } + + void _startAutoConnectScan() { if (_autoConnectEnabled == true) { Provider.of(context, listen: false).startScanning(); } @@ -51,24 +56,22 @@ class _ConnectCard extends State { color: Theme.of(context).colorScheme.primary, child: Padding( padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Device', - style: TextStyle( - color: Colors.white, - fontSize: 18.0, - fontWeight: FontWeight.bold, + child: Consumer( + builder: (context, bleController, child) { + List devices = bleController.discoveredDevices; + _tryAutoconnect(devices, bleController); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Device', + style: TextStyle( + color: Colors.white, + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), ), - ), - Consumer( - builder: (context, bleController, child) { - List devices = - bleController.discoveredDevices; - print("notified listeners wihth new devices: $devices"); - tryAutoconnect(devices, bleController); - return Row( + Row( children: [ Checkbox( checkColor: Theme.of(context).colorScheme.primary, @@ -77,25 +80,36 @@ class _ConnectCard extends State { onChanged: (value) => { setState(() { _autoConnectEnabled = value; - startAutoConnectScan(); + _startAutoConnectScan(); + if (value != null) + prefs.setBool("autoConnectEnabled", value); }) }, ), Text("Connect to OpenEarable automatically") ], - ); - }), - SizedBox(height: 5), - _getEarableInfo(), - _getConnectButton(context), - ], - ), + ), + SizedBox(height: 5), + _getEarableInfo(bleController), + _getConnectButton(context), + ], + ); + }), ), ), ); } - Widget _getEarableInfo() { + String _batteryPercentageString(BluetoothController bleController) { + int? percentage = bleController.earableSOC; + if (percentage == null) { + return " (...%)"; + } else { + return " ($percentage%)"; + } + } + + Widget _getEarableInfo(BluetoothController bleController) { return Row( children: [ if (_openEarable.bleManager.connected) @@ -103,7 +117,7 @@ class _ConnectCard extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "${_openEarable.bleManager.connectedDevice?.name ?? ""} (${_earableSOC}%)", + "${_openEarable.bleManager.connectedDevice?.name ?? ""}${_batteryPercentageString(bleController)}", style: TextStyle( color: Color.fromRGBO(168, 168, 172, 1.0), fontSize: 15.0, @@ -164,12 +178,21 @@ class _ConnectCard extends State { ); } - void tryAutoconnect( + void _tryAutoconnect( List devices, BluetoothController bleController) async { - if (_autoConnectEnabled == true && - devices.isNotEmpty && - _openEarable.bleManager.connectingDevice?.name != devices[0].name) { - _openEarable.bleManager.connectToDevice(devices[0]); + if (_autoConnectEnabled != true || + devices.isEmpty || + bleController.connected) { + return; + } + String? lastConnectedDeviceName = + prefs.getString("lastConnectedDeviceName"); + DiscoveredDevice? deviceToConnect = devices.firstWhere( + (device) => device.name == lastConnectedDeviceName, + orElse: () => devices[0]); + if (_openEarable.bleManager.connectingDevice?.name != + deviceToConnect.name) { + bleController.connectToDevice(deviceToConnect); } } } diff --git a/open_earable/lib/controls_tab/views/led_color.dart b/open_earable/lib/controls_tab/views/led_color.dart index 253d450..d39372e 100644 --- a/open_earable/lib/controls_tab/views/led_color.dart +++ b/open_earable/lib/controls_tab/views/led_color.dart @@ -3,6 +3,9 @@ import 'package:open_earable/controls_tab/models/open_earable_settings.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import 'dart:async'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; +import 'package:provider/provider.dart'; + +import '../../ble_controller.dart'; class LEDColorCard extends StatefulWidget { final OpenEarable _openEarable; @@ -164,59 +167,55 @@ class _LEDColorCardState extends State { fontWeight: FontWeight.bold, ), ), - Row( - children: [ - GestureDetector( - onTap: _openEarable.bleManager.connected - ? _openColorPicker - : null, // Open color picker - child: Container( - width: 66, - height: 36, - decoration: BoxDecoration( - color: OpenEarableSettings().selectedColor, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - SizedBox(width: 5), - SizedBox( - width: 66, - child: ElevatedButton( - onPressed: _openEarable.bleManager.connected - ? _setLEDColor - : null, - style: ElevatedButton.styleFrom( - backgroundColor: Color( - 0xff53515b), // Set the background color to grey - foregroundColor: Colors.white), - child: Text('Set'), - ), - ), - SizedBox(width: 5), - ElevatedButton( - onPressed: _openEarable.bleManager.connected - ? _startRainbowMode - : null, - style: ElevatedButton.styleFrom( - backgroundColor: Color( - 0xff53515b), // Set the background color to grey - foregroundColor: Colors.white), - child: Text("🦄"), - ), - Spacer(), - ElevatedButton( - onPressed: _openEarable.bleManager.connected - ? _turnLEDoff - : null, - style: ElevatedButton.styleFrom( - backgroundColor: Color(0xfff27777), - foregroundColor: Colors.black, - ), - child: Text('Off'), - ), - ], - ), + Selector( + selector: (_, bleController) => bleController.connected, + builder: (context, connected, child) => Row( + children: [ + GestureDetector( + onTap: connected + ? _openColorPicker + : null, // Open color picker + child: Container( + width: 66, + height: 36, + decoration: BoxDecoration( + color: OpenEarableSettings().selectedColor, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + SizedBox(width: 5), + SizedBox( + width: 66, + child: ElevatedButton( + onPressed: connected ? _setLEDColor : null, + style: ElevatedButton.styleFrom( + backgroundColor: Color( + 0xff53515b), // Set the background color to grey + foregroundColor: Colors.white), + child: Text('Set'), + ), + ), + SizedBox(width: 5), + ElevatedButton( + onPressed: connected ? _startRainbowMode : null, + style: ElevatedButton.styleFrom( + backgroundColor: Color( + 0xff53515b), // Set the background color to grey + foregroundColor: Colors.white), + child: Text("🦄"), + ), + Spacer(), + ElevatedButton( + onPressed: connected ? _turnLEDoff : null, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xfff27777), + foregroundColor: Colors.black, + ), + child: Text('Off'), + ), + ], + )), ], ), ), diff --git a/open_earable/lib/controls_tab/views/sensor_configuration.dart b/open_earable/lib/controls_tab/views/sensor_configuration.dart index eee6266..34349bd 100644 --- a/open_earable/lib/controls_tab/views/sensor_configuration.dart +++ b/open_earable/lib/controls_tab/views/sensor_configuration.dart @@ -1,6 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:open_earable/ble_controller.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:provider/provider.dart'; import '../models/open_earable_settings.dart'; class SensorConfigurationCard extends StatefulWidget { @@ -158,15 +160,20 @@ class _SensorConfigurationCardState extends State { child: SizedBox( height: 37.0, child: ElevatedButton( - onPressed: _openEarable.bleManager.connected - ? _writeSensorConfigs - : null, + onPressed: + Provider.of(context).connected + ? _writeSensorConfigs + : null, style: ElevatedButton.styleFrom( - backgroundColor: _openEarable.bleManager.connected - ? Theme.of(context).colorScheme.secondary - : Colors.grey, + backgroundColor: + Provider.of(context) + .connected + ? Theme.of(context).colorScheme.secondary + : Colors.grey, foregroundColor: Colors.black, - enableFeedback: _openEarable.bleManager.connected, + enableFeedback: + Provider.of(context) + .connected, ), child: Text("Set Configuration"), ), @@ -194,13 +201,15 @@ class _SensorConfigurationCardState extends State { checkColor: Theme.of(context).colorScheme.primary, fillColor: MaterialStateProperty.resolveWith(_getCheckboxColor), value: settingSelected, - onChanged: _openEarable.bleManager.connected ? changeBool : null, + onChanged: Provider.of(context).connected + ? changeBool + : null, ), Text(sensorName), Spacer(), Container( decoration: BoxDecoration( - color: _openEarable.bleManager.connected + color: Provider.of(context).connected ? Colors.white : Colors.grey[200], borderRadius: BorderRadius.circular(4.0), @@ -211,9 +220,10 @@ class _SensorConfigurationCardState extends State { child: Container( alignment: Alignment.centerRight, child: DropdownButton( - dropdownColor: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey[200], + dropdownColor: + Provider.of(context).connected + ? Colors.white + : Colors.grey[200], alignment: Alignment.centerRight, value: currentValue, onChanged: (String? newValue) { @@ -233,7 +243,8 @@ class _SensorConfigurationCardState extends State { child: Text( value, style: TextStyle( - color: _openEarable.bleManager.connected + color: Provider.of(context) + .connected ? Colors.black : Colors.grey, ), @@ -244,9 +255,10 @@ class _SensorConfigurationCardState extends State { underline: Container(), icon: Icon( Icons.arrow_drop_down, - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey, + color: + Provider.of(context).connected + ? Colors.black + : Colors.grey, ), )))), SizedBox(width: 8), diff --git a/open_earable/macos/Flutter/GeneratedPluginRegistrant.swift b/open_earable/macos/Flutter/GeneratedPluginRegistrant.swift index 2b64fea..1478b26 100644 --- a/open_earable/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/open_earable/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,8 +7,10 @@ import Foundation import flutter_gl_macos import path_provider_foundation +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterGlMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterGlMacosPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/open_earable/pubspec.lock b/open_earable/pubspec.lock index d373630..6738dab 100644 --- a/open_earable/pubspec.lock +++ b/open_earable/pubspec.lock @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" fixnum: dependency: transitive description: @@ -546,6 +554,62 @@ packages: url: "https://pub.dev" source: hosted version: "5.3.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" simple_kalman: dependency: "direct main" description: diff --git a/open_earable/pubspec.yaml b/open_earable/pubspec.yaml index f709a98..8b632c0 100644 --- a/open_earable/pubspec.yaml +++ b/open_earable/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: path_provider: ^2.1.1 open_file: ^3.3.2 provider: ^6.1.1 + shared_preferences: ^2.2.2 dev_dependencies: flutter_test: From b3d5ba901849f32ebf72927f62bbf382a8fd873c Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sun, 4 Feb 2024 18:13:03 +0100 Subject: [PATCH 067/104] fix bug: autoconnect didnt work right after opening the app --- open_earable/lib/controls_tab/views/connect.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open_earable/lib/controls_tab/views/connect.dart b/open_earable/lib/controls_tab/views/connect.dart index 760f7da..fd5d469 100644 --- a/open_earable/lib/controls_tab/views/connect.dart +++ b/open_earable/lib/controls_tab/views/connect.dart @@ -27,7 +27,6 @@ class _ConnectCard extends State { void initState() { super.initState(); _getPrefs(); - _startAutoConnectScan(); } @override @@ -40,6 +39,7 @@ class _ConnectCard extends State { setState(() { _autoConnectEnabled = prefs.getBool("autoConnectEnabled"); }); + _startAutoConnectScan(); } void _startAutoConnectScan() { From 7234a11dd20267fc9b91667b3291f0ed77ac37db Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sun, 4 Feb 2024 20:18:49 +0100 Subject: [PATCH 068/104] improve recorder, split baro and imu csv files, add option to navigate through folders and open csv files --- open_earable/lib/apps/recorder.dart | 386 +++++++++++++++------------- 1 file changed, 212 insertions(+), 174 deletions(-) diff --git a/open_earable/lib/apps/recorder.dart b/open_earable/lib/apps/recorder.dart index bc438bf..f0d6972 100644 --- a/open_earable/lib/apps/recorder.dart +++ b/open_earable/lib/apps/recorder.dart @@ -14,14 +14,17 @@ class Recorder extends StatefulWidget { } class _RecorderState extends State { - List _recordings = []; + List _recordingFolders = []; + Directory? _selectedFolder; final OpenEarable _openEarable; bool _recording = false; StreamSubscription? _imuSubscription; StreamSubscription? _barometerSubscription; _RecorderState(this._openEarable); - CsvWriter? _csvWriter; - late List _csvHeader; + CsvWriter? _imuCsvWriter; + CsvWriter? _barometerCsvWriter; + late List _imuHeader; + late List _barometerHeader; late List _labels; late String _selectedLabel; Timer? _timer; @@ -43,7 +46,7 @@ class _RecorderState extends State { "Label 8", ]; _selectedLabel = "No Label"; - _csvHeader = [ + _imuHeader = [ "time", "sensor_accX[m/s]", "sensor_accY[m/s]", @@ -57,15 +60,21 @@ class _RecorderState extends State { "sensor_yaw[°]", "sensor_pitch[°]", "sensor_roll[°]", + ]; + + _barometerHeader = [ + "time", "sensor_baro[Pa]", "sensor_temp[°C]", ]; - _csvHeader.addAll( + _imuHeader.addAll( + _labels.sublist(1).map((label) => "label_OpenEarable_${label}")); + _barometerHeader.addAll( _labels.sublist(1).map((label) => "label_OpenEarable_${label}")); if (_openEarable.bleManager.connected) { _setupListeners(); } - listFilesInDocumentsDirectory(); + listSubfoldersInDocumentsDirectory(); } void _startTimer() { @@ -99,19 +108,28 @@ class _RecorderState extends State { _connectionStateSubscription?.cancel(); } - Future listFilesInDocumentsDirectory() async { + Future listSubfoldersInDocumentsDirectory() async { Directory documentsDirectory = await getApplicationDocumentsDirectory(); - List files = documentsDirectory.listSync(); - _recordings.clear(); - for (var file in files) { - if (file is File) { - _recordings.add(file); + List subfolders = documentsDirectory.listSync(); + _recordingFolders.clear(); + for (var subfolder in subfolders) { + if (subfolder is Directory) { + _recordingFolders.add(subfolder); } } - _recordings.sort((a, b) { - return b.statSync().changed.compareTo(a.statSync().changed); + _recordingFolders.sort((a, b) { + final folderAName = a.path.split("/").last; + final folderBName = b.path.split("/").last; + return -folderAName.compareTo(folderBName); }); + if (_selectedFolder != null && _selectedFolder!.existsSync()) { + List files = _selectedFolder!.listSync(); + var index = _recordingFolders + .indexWhere((element) => element.path == _selectedFolder!.path); + _recordingFolders.insertAll(index + 1, files); + } + setState(() {}); } @@ -157,11 +175,9 @@ class _RecorderState extends State { eulerYaw, eulerPitch, eulerRoll, - "", - "", ]; imuRow.addAll(_getLabels()); - _csvWriter?.addData(imuRow); + _imuCsvWriter?.addData(imuRow); }); _barometerSubscription = @@ -175,23 +191,11 @@ class _RecorderState extends State { List barometerRow = [ timestamp, - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", pressure, temperature, ]; barometerRow.addAll(_getLabels()); - _csvWriter?.addData(barometerRow); + _barometerCsvWriter?.addData(barometerRow); }); } @@ -210,11 +214,17 @@ class _RecorderState extends State { setState(() { _recording = false; }); - _csvWriter?.cancelTimer(); + _imuCsvWriter?.cancelTimer(); + _barometerCsvWriter?.cancelTimer(); _stopTimer(); } else { - _csvWriter = CsvWriter(listFilesInDocumentsDirectory); - _csvWriter?.addData(_csvHeader); + DateTime startTime = DateTime.now(); + _imuCsvWriter = + CsvWriter("imu", startTime, listSubfoldersInDocumentsDirectory); + _imuCsvWriter?.addData(_imuHeader); + _barometerCsvWriter = + CsvWriter("baro", startTime, listSubfoldersInDocumentsDirectory); + _barometerCsvWriter?.addData(_barometerHeader); _startTimer(); setState(() { _recording = true; @@ -222,17 +232,20 @@ class _RecorderState extends State { } } - void deleteFile(File file) { - if (file.existsSync()) { + void deleteFileSystemEntity(FileSystemEntity entity) { + if (entity.existsSync()) { try { - file.deleteSync(); + entity.deleteSync(recursive: true); } catch (e) { - print('Error deleting file: $e'); + print('Error deleting folder: $e'); } } else { - print('File does not exist.'); + print('Folder does not exist.'); } - listFilesInDocumentsDirectory(); + if (entity is File && entity.parent.listSync().isEmpty) { + deleteFileSystemEntity(entity.parent); + } + listSubfoldersInDocumentsDirectory(); } @override @@ -246,149 +259,166 @@ class _RecorderState extends State { } Widget _recorderWidget() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - height: 200, - child: !_openEarable.bleManager.connected - ? EarableNotConnectedWarning() - : Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.fromLTRB(16, 16, 16, 0), - child: Text( - _formatDuration(_duration), - style: TextStyle( - fontFamily: - 'Digital', // This is a common monospaced font - fontSize: 80, - fontWeight: FontWeight.normal, - ), + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + height: 200, + child: !_openEarable.bleManager.connected + ? EarableNotConnectedWarning() + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Text( + _formatDuration(_duration), + style: TextStyle( + fontFamily: + 'Digital', // This is a common monospaced font + fontSize: 80, + fontWeight: FontWeight.normal, ), ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.all(16), - child: ElevatedButton( - onPressed: startStopRecording, - style: ElevatedButton.styleFrom( - minimumSize: Size(200, 36), - backgroundColor: _recording - ? Color(0xfff27777) - : Theme.of(context) - .colorScheme - .secondary, - foregroundColor: Colors.black, - ), - child: Text( - _recording - ? "Stop Recording" - : "Start Recording", - style: TextStyle(fontSize: 20), - ), - ), - ), - DropdownButton( - value: _selectedLabel, - icon: const Icon(Icons.arrow_drop_down), - onChanged: (String? newValue) { - setState(() { - _selectedLabel = newValue!; - }); - }, - items: _labels.map>( - (String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - ], - ), - ])), - Text("Recordings", style: TextStyle(fontSize: 20.0)), - Divider( - thickness: 2, - ), - Expanded( - child: _recordings.isEmpty - ? Stack( - fit: StackFit.expand, - children: [ - Center( - child: Column( + ), + Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.warning, - size: 48, - color: Colors.red, - ), - SizedBox(height: 16), - Center( - child: Text( - "No recordings found", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, + Padding( + padding: EdgeInsets.all(16), + child: ElevatedButton( + onPressed: startStopRecording, + style: ElevatedButton.styleFrom( + minimumSize: Size(200, 36), + backgroundColor: _recording + ? Color(0xfff27777) + : Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.black, ), - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - ], - ) - : ListView.builder( - itemCount: _recordings.length, - itemBuilder: (context, index) { - return ListTile( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, child: Text( - _recordings[index].path.split("/").last, - maxLines: 1, + _recording + ? "Stop Recording" + : "Start Recording", + style: TextStyle(fontSize: 20), ), ), ), - IconButton( - icon: Icon(Icons.delete, - color: (_recording && index == 0) - ? Color.fromARGB(50, 255, 255, 255) - : Colors.white), - onPressed: () { - (_recording && index == 0) - ? null - : deleteFile(_recordings[index]); + DropdownButton( + value: _selectedLabel, + icon: const Icon(Icons.arrow_drop_down), + onChanged: (String? newValue) { + setState(() { + _selectedLabel = newValue!; + }); }, - splashColor: (_recording && index == 0) - ? Colors.transparent - : Theme.of(context).splashColor, + items: _labels.map>( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), ), ], ), - onTap: () { - OpenFile.open(_recordings[index].path); - }, - ); - }, + ])), + Text("Recordings", style: TextStyle(fontSize: 20.0)), + Divider( + thickness: 2, + ), + _noRecordingsWidget(), + Expanded( + child: ListView.builder( + itemCount: _recordingFolders.length, + itemBuilder: (context, index) { + return ListTile( + title: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _recordingFolders[index] is File + ? Container(width: 40) + : Container(), + Text( + _recordingFolders[index].path.split("/").last, + maxLines: 1, ), - ) - ], - ), + Visibility( + visible: _recordingFolders[index] is Directory, + child: Transform.rotate( + angle: _recordingFolders[index].path == + _selectedFolder?.path + ? 90 * 3.14 / 180 + : 0, + child: Icon(Icons.arrow_right))) + ], + ), + onTap: () { + if (_recordingFolders[index].path == _selectedFolder?.path) { + _selectedFolder = null; + listSubfoldersInDocumentsDirectory(); + } else if (_recordingFolders[index] is Directory) { + Directory d = _recordingFolders[index] as Directory; + _selectedFolder = d; + listSubfoldersInDocumentsDirectory(); + } else if (_recordingFolders[index] is File) { + OpenFile.open(_recordingFolders[index].path); + } + }, + trailing: IconButton( + icon: Icon(Icons.delete, + color: (_recording && index == 0) + ? Color.fromARGB(50, 255, 255, 255) + : Colors.white), + onPressed: () { + (_recording && index == 0) + ? null + : deleteFileSystemEntity(_recordingFolders[index]); + }, + splashColor: (_recording && index == 0) + ? Colors.transparent + : Theme.of(context).splashColor, + ), + splashColor: Colors.transparent, + ); + }, + )), + ], ); } + + Widget _noRecordingsWidget() { + return Visibility( + visible: _recordingFolders.isEmpty, + child: Expanded( + child: Stack( + fit: StackFit.expand, + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.warning, + size: 48, + color: Colors.red, + ), + SizedBox(height: 16), + Center( + child: Text( + "No recordings found", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ], + ))); + } } class CsvWriter { @@ -396,14 +426,15 @@ class CsvWriter { File? file; late Timer _timer; - CsvWriter(void Function() fileCreationClosure) { + CsvWriter(String prefix, DateTime startTime, + void Function() folderCreationClosure) { if (file == null) { - _openFile(fileCreationClosure); + _openFile(prefix, startTime, folderCreationClosure); } _timer = Timer.periodic(Duration(milliseconds: 250), (Timer timer) { if (buffer.isNotEmpty) { if (file == null) { - _openFile(fileCreationClosure); + _openFile(prefix, startTime, folderCreationClosure); } writeBufferToFile(); } @@ -418,17 +449,24 @@ class CsvWriter { buffer.add(data); } - Future _openFile(void Function() fileCreationClosure) async { - DateTime startTime = DateTime.now(); + Future _openFile(String prefix, DateTime startTime, + void Function() folderCreationClosure) async { String formattedDate = startTime.toUtc().toIso8601String().replaceAll(':', '_'); formattedDate = "${formattedDate.substring(0, formattedDate.length - 4)}Z"; - String fileName = 'recording_$formattedDate'; + String fileName = '${prefix}_recording_$formattedDate'; String directory = (await getApplicationDocumentsDirectory()).path; - String filePath = '$directory/$fileName.csv'; + String folderPath = '$directory/$formattedDate'; + + String filePath = '$folderPath/$fileName.csv'; + Directory folder = Directory(folderPath); + if (!await folder.exists()) { + await folder.create(recursive: true); + folderCreationClosure(); + } file = File(filePath); await file?.create(); - fileCreationClosure(); + folderCreationClosure(); } void writeBufferToFile() async { From 5e731735ad6553f8d955a8349e4f6957ee585816 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sun, 4 Feb 2024 20:58:15 +0100 Subject: [PATCH 069/104] fix bug when disconnecting during recording --- open_earable/lib/apps/recorder.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/open_earable/lib/apps/recorder.dart b/open_earable/lib/apps/recorder.dart index f0d6972..23bda97 100644 --- a/open_earable/lib/apps/recorder.dart +++ b/open_earable/lib/apps/recorder.dart @@ -135,8 +135,12 @@ class _RecorderState extends State { _setupListeners() { _connectionStateSubscription = - _openEarable.bleManager.connectionStateStream.listen((_) { - setState(() {}); // rebuild page when connection state changes + _openEarable.bleManager.connectionStateStream.listen((connected) { + setState(() { + if (!connected) { + _recording = false; + } + }); }); _imuSubscription = _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { From 51dca8297c7b45c619121deeb94dfba8ee94d2c2 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Mon, 5 Feb 2024 10:33:19 +0100 Subject: [PATCH 070/104] fix bug --- open_earable/lib/controls_tab/views/connect.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/open_earable/lib/controls_tab/views/connect.dart b/open_earable/lib/controls_tab/views/connect.dart index fd5d469..d0c6de6 100644 --- a/open_earable/lib/controls_tab/views/connect.dart +++ b/open_earable/lib/controls_tab/views/connect.dart @@ -18,7 +18,7 @@ class ConnectCard extends StatefulWidget { class _ConnectCard extends State { final OpenEarable _openEarable; - bool? _autoConnectEnabled = false; + bool _autoConnectEnabled = false; late SharedPreferences prefs; _ConnectCard(this._openEarable); @@ -37,7 +37,7 @@ class _ConnectCard extends State { Future _getPrefs() async { prefs = await SharedPreferences.getInstance(); setState(() { - _autoConnectEnabled = prefs.getBool("autoConnectEnabled"); + _autoConnectEnabled = prefs.getBool("autoConnectEnabled") ?? false; }); _startAutoConnectScan(); } @@ -79,7 +79,7 @@ class _ConnectCard extends State { value: _autoConnectEnabled, onChanged: (value) => { setState(() { - _autoConnectEnabled = value; + _autoConnectEnabled = value ?? false; _startAutoConnectScan(); if (value != null) prefs.setBool("autoConnectEnabled", value); From 52b8b6a4f15e67921900656ae1ffd1f8d7795f40 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Mon, 5 Feb 2024 12:10:30 +0100 Subject: [PATCH 071/104] fix text overflow issue in recorder --- open_earable/lib/apps/recorder.dart | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/open_earable/lib/apps/recorder.dart b/open_earable/lib/apps/recorder.dart index 23bda97..788fcba 100644 --- a/open_earable/lib/apps/recorder.dart +++ b/open_earable/lib/apps/recorder.dart @@ -336,24 +336,30 @@ class _RecorderState extends State { itemCount: _recordingFolders.length, itemBuilder: (context, index) { return ListTile( + contentPadding: _recordingFolders[index] is File + ? EdgeInsets.fromLTRB(40, 0, 16, 0) + : null, title: Row( - mainAxisAlignment: MainAxisAlignment.start, children: [ - _recordingFolders[index] is File - ? Container(width: 40) - : Container(), - Text( + Expanded( + child: Text( _recordingFolders[index].path.split("/").last, maxLines: 1, - ), + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: _recordingFolders[index] is File ? 14 : null, + ), + )), Visibility( - visible: _recordingFolders[index] is Directory, - child: Transform.rotate( - angle: _recordingFolders[index].path == - _selectedFolder?.path + visible: _recordingFolders[index] is Directory, + child: Transform.rotate( + angle: + _recordingFolders[index].path == _selectedFolder?.path ? 90 * 3.14 / 180 : 0, - child: Icon(Icons.arrow_right))) + child: Icon(Icons.arrow_right), + ), + ), ], ), onTap: () { From d10e9ee638a6c458d0c71fbd43f35c2408661297 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sun, 11 Feb 2024 13:31:29 +0100 Subject: [PATCH 072/104] fix bug --- open_earable/lib/ble_controller.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/open_earable/lib/ble_controller.dart b/open_earable/lib/ble_controller.dart index 01f667d..7957ab9 100644 --- a/open_earable/lib/ble_controller.dart +++ b/open_earable/lib/ble_controller.dart @@ -52,6 +52,7 @@ class BluetoothController extends ChangeNotifier { void dispose() { super.dispose(); _scanSubscription?.cancel(); + _scanning = false; _connectionStateSubscription?.cancel(); _batteryLevelSubscription?.cancel(); } @@ -66,6 +67,7 @@ class BluetoothController extends ChangeNotifier { } Future startScanning() async { + print("SCANNING $_scanning"); if (_scanning) { return; } @@ -79,7 +81,6 @@ class BluetoothController extends ChangeNotifier { } await _openEarable?.bleManager.startScan(); _scanSubscription?.cancel(); - _scanning = false; _scanSubscription = _openEarable?.bleManager.scanStream.listen((incomingDevice) { if (incomingDevice.name.isNotEmpty && From 3a147cab03ba7b108694740556d1d2f71621f621 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sun, 11 Feb 2024 14:21:37 +0100 Subject: [PATCH 073/104] add cupertino checkbox to connect card and fix size of button in led card --- .../lib/controls_tab/views/connect.dart | 44 +++++++++++++------ .../lib/controls_tab/views/led_color.dart | 1 + 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/open_earable/lib/controls_tab/views/connect.dart b/open_earable/lib/controls_tab/views/connect.dart index 3f336da..ba4d471 100644 --- a/open_earable/lib/controls_tab/views/connect.dart +++ b/open_earable/lib/controls_tab/views/connect.dart @@ -76,19 +76,37 @@ class _ConnectCard extends State { ), Row( children: [ - Checkbox( - checkColor: Theme.of(context).colorScheme.primary, - //fillColor: Theme.of(context).colorScheme.primary, - value: _autoConnectEnabled, - onChanged: (value) => { - setState(() { - _autoConnectEnabled = value ?? false; - _startAutoConnectScan(); - if (value != null) - prefs.setBool("autoConnectEnabled", value); - }) - }, - ), + Platform.isIOS + ? CupertinoCheckbox( + value: _autoConnectEnabled, + onChanged: (value) => { + setState(() { + _autoConnectEnabled = value ?? false; + _startAutoConnectScan(); + if (value != null) + prefs.setBool("autoConnectEnabled", value); + }) + }, + activeColor: _autoConnectEnabled + ? CupertinoTheme.of(context).primaryColor + : CupertinoTheme.of(context) + .primaryContrastingColor, + checkColor: CupertinoTheme.of(context) + .primaryContrastingColor, + ) + : Checkbox( + checkColor: Theme.of(context).colorScheme.primary, + //fillColor: Theme.of(context).colorScheme.primary, + value: _autoConnectEnabled, + onChanged: (value) => { + setState(() { + _autoConnectEnabled = value ?? false; + _startAutoConnectScan(); + if (value != null) + prefs.setBool("autoConnectEnabled", value); + }) + }, + ), Text("Connect to OpenEarable automatically") ], ), diff --git a/open_earable/lib/controls_tab/views/led_color.dart b/open_earable/lib/controls_tab/views/led_color.dart index 0dbe59a..981a077 100644 --- a/open_earable/lib/controls_tab/views/led_color.dart +++ b/open_earable/lib/controls_tab/views/led_color.dart @@ -228,6 +228,7 @@ class _LEDColorCardState extends State { SizedBox(width: 5), SizedBox( width: 66, + height: 36, child: Platform.isIOS ? CupertinoButton( padding: EdgeInsets.zero, From 21660edce03aea3a94aeb366d3de5956cb81e3b9 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sun, 11 Feb 2024 14:21:49 +0100 Subject: [PATCH 074/104] add cupertino button back to ble page --- open_earable/lib/ble.dart | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/open_earable/lib/ble.dart b/open_earable/lib/ble.dart index 3389983..a64cc23 100644 --- a/open_earable/lib/ble.dart +++ b/open_earable/lib/ble.dart @@ -113,17 +113,28 @@ class _BLEPageState extends State { textAlign: TextAlign.center, ))), Center( - child: ElevatedButton( - onPressed: Provider.of(context, listen: false) - .startScanning, - style: ButtonStyle( - foregroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.primary), - backgroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.secondary), - ), - child: const Text('Restart Scan'), - ), + child: Platform.isIOS + ? CupertinoButton( + padding: EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Text('Restart Scan'), + color: CupertinoTheme.of(context) + .primaryColor, // iOS equivalent for a prominent button color + onPressed: + Provider.of(context, listen: false) + .startScanning, + ) + : ElevatedButton( + onPressed: + Provider.of(context, listen: false) + .startScanning, + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.primary), + backgroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.secondary), + ), + child: const Text('Restart Scan'), + ), ) ], )); From fea7a2bea131f0e8162352d79bf2706ff0d7fc96 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sun, 11 Feb 2024 14:25:35 +0100 Subject: [PATCH 075/104] fix text color of connect card --- open_earable/lib/controls_tab/views/connect.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/open_earable/lib/controls_tab/views/connect.dart b/open_earable/lib/controls_tab/views/connect.dart index ba4d471..8eb110f 100644 --- a/open_earable/lib/controls_tab/views/connect.dart +++ b/open_earable/lib/controls_tab/views/connect.dart @@ -107,7 +107,12 @@ class _ConnectCard extends State { }) }, ), - Text("Connect to OpenEarable automatically") + Text( + "Connect to OpenEarable automatically", + style: TextStyle( + color: Color.fromRGBO(168, 168, 172, 1.0), + ), + ) ], ), SizedBox(height: 5), From e919d343dfe74eba5ed9ffebf31cb6505e602a9a Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sun, 11 Feb 2024 16:32:05 +0100 Subject: [PATCH 076/104] add cupertino design to apps tab and integrate led color picker into cupertino design --- .../model/attitude_tracker.dart | 28 +++++----- .../model/bad_posture_reminder.dart | 51 +++++++++++-------- .../model/earable_attitude_tracker.dart | 26 ++++------ .../model/mock_attitude_tracker.dart | 21 ++++---- .../posture_tracker_view_model.dart | 9 ++-- open_earable/lib/apps_tab.dart | 27 ++++++++-- open_earable/lib/ble.dart | 2 +- .../lib/controls_tab/views/audio_player.dart | 44 ++++++---------- .../lib/controls_tab/views/led_color.dart | 47 ++++++++++------- open_earable/lib/global_theme.dart | 31 +++++++++++ open_earable/lib/main.dart | 40 ++++----------- open_earable/pubspec.lock | 11 ++-- open_earable/pubspec.yaml | 7 ++- 13 files changed, 195 insertions(+), 149 deletions(-) create mode 100644 open_earable/lib/global_theme.dart diff --git a/open_earable/lib/apps/posture_tracker/model/attitude_tracker.dart b/open_earable/lib/apps/posture_tracker/model/attitude_tracker.dart index 2ccf5c1..13cc174 100644 --- a/open_earable/lib/apps/posture_tracker/model/attitude_tracker.dart +++ b/open_earable/lib/apps/posture_tracker/model/attitude_tracker.dart @@ -8,35 +8,37 @@ import 'attitude.dart'; /// An abstract class for attitude trackers. abstract class AttitudeTracker { - StreamController _attitudeStreamController = StreamController.broadcast(); + StreamController _attitudeStreamController = + StreamController.broadcast(); Attitude _rawAttitude = Attitude(); Attitude get rawAttitude => _rawAttitude; Attitude _attitude = Attitude(); Attitude get attitude => _attitude; bool get isTracking; + /// check if tracking is available bool get isAvailable => true; Attitude _referenceAttitude = Attitude(); /// Callback that is called when the tracker changes availability. Takes the tracker as an argument. - Function(AttitudeTracker) didChangeAvailability = (_) { }; + Function(AttitudeTracker) didChangeAvailability = (_) {}; /// Listen to the attitude stream. - /// + /// /// [callback] is called when a new attitude is received. void listen(void Function(Attitude) callback) { this._attitudeStreamController.stream.listen(callback); } /// Start tracking the attitude. - /// + /// /// In order to receive the data, you need to call `listen()` first. void start(); /// Stop tracking the attitude. - /// + /// /// You can resume the tracking by calling `start()` again. void stop(); @@ -46,20 +48,23 @@ abstract class AttitudeTracker { void calibrateToCurrentAttitude() async { _referenceAttitude = _rawAttitude; - print("calibrated to {roll: ${_referenceAttitude.roll}, pitch: ${_referenceAttitude.pitch}, yaw: ${_referenceAttitude.yaw}}"); + print( + "calibrated to {roll: ${_referenceAttitude.roll}, pitch: ${_referenceAttitude.pitch}, yaw: ${_referenceAttitude.yaw}}"); } /// Cancle the stream and close the stream controller. - /// + /// /// If you want to use the tracker again, you need to call listen() again. @mustCallSuper - void cancle() { + void cancel() { this._attitudeStreamController.close(); } - void updateAttitude ({double? roll, double? pitch, double? yaw, Attitude? attitude}) { + void updateAttitude( + {double? roll, double? pitch, double? yaw, Attitude? attitude}) { if (roll == null && pitch == null && yaw == null && attitude == null) { - throw ArgumentError("Either roll, pitch and yaw or attitude must be provided"); + throw ArgumentError( + "Either roll, pitch and yaw or attitude must be provided"); } // Check if attitude is not null, otherwise use the angles attitude ??= Attitude(roll: roll ?? 0, pitch: pitch ?? 0, yaw: yaw ?? 0); @@ -68,5 +73,4 @@ abstract class AttitudeTracker { _attitude = attitude - _referenceAttitude; _attitudeStreamController.add(_attitude); } - -} \ No newline at end of file +} diff --git a/open_earable/lib/apps/posture_tracker/model/bad_posture_reminder.dart b/open_earable/lib/apps/posture_tracker/model/bad_posture_reminder.dart index d494d04..72ccdf3 100644 --- a/open_earable/lib/apps/posture_tracker/model/bad_posture_reminder.dart +++ b/open_earable/lib/apps/posture_tracker/model/bad_posture_reminder.dart @@ -4,7 +4,6 @@ import 'package:open_earable/apps/posture_tracker/model/attitude.dart'; import 'package:open_earable/apps/posture_tracker/model/attitude_tracker.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; - class BadPostureSettings { bool isActive; @@ -20,28 +19,26 @@ class BadPostureSettings { /// The time threshold in seconds for resetting the timer int resetTimeThreshold; - BadPostureSettings({ - this.isActive = true, - required this.rollAngleThreshold, - required this.pitchAngleThreshold, - required this.timeThreshold, - required this.resetTimeThreshold - }); + BadPostureSettings( + {this.isActive = true, + required this.rollAngleThreshold, + required this.pitchAngleThreshold, + required this.timeThreshold, + required this.resetTimeThreshold}); } class PostureTimestamps { DateTime? lastBadPosture; DateTime? lastGoodPosture; - DateTime lastReset =DateTime.now(); + DateTime lastReset = DateTime.now(); } class BadPostureReminder { BadPostureSettings _settings = BadPostureSettings( - rollAngleThreshold: 20, - pitchAngleThreshold: 20, - timeThreshold: 10, - resetTimeThreshold: 1 - ); + rollAngleThreshold: 20, + pitchAngleThreshold: 20, + timeThreshold: 10, + resetTimeThreshold: 1); final OpenEarable _openEarable; final AttitudeTracker _attitudeTracker; PostureTimestamps _timestamps = PostureTimestamps(); @@ -66,7 +63,8 @@ class BadPostureReminder { DateTime now = DateTime.now(); if (_isBadPosture(attitude)) { - if (!(_lastPostureWasBad ?? false)) { + if (!(_lastPostureWasBad ?? false) && + _openEarable.bleManager.connected) { _openEarable.rgbLed.writeLedColor(r: 255, g: 0, b: 0); } @@ -88,7 +86,9 @@ class BadPostureReminder { _timestamps.lastGoodPosture = null; } else { if (_lastPostureWasBad ?? false) { - _openEarable.rgbLed.writeLedColor(r: 0, g: 255, b: 0); + if (_openEarable.bleManager.connected) { + _openEarable.rgbLed.writeLedColor(r: 0, g: 255, b: 0); + } } // If this is the first time the program enters the good state, store the current time @@ -101,7 +101,8 @@ class BadPostureReminder { int duration = now.difference(_timestamps.lastGoodPosture!).inSeconds; // If the duration exceeds the minimum required, reset the last bad state time if (duration >= _settings.resetTimeThreshold) { - print("duration: $duration, reset time threshold: ${_settings.resetTimeThreshold}"); + print( + "duration: $duration, reset time threshold: ${_settings.resetTimeThreshold}"); print("resetting last bad posture time"); _timestamps.lastBadPosture = null; } @@ -114,21 +115,27 @@ class BadPostureReminder { void stop() { _timestamps.lastBadPosture = null; _timestamps.lastGoodPosture = null; - _openEarable.rgbLed.writeLedColor(r: 0, g: 0, b: 0); + if (_openEarable.bleManager.connected) { + _openEarable.rgbLed.writeLedColor(r: 0, g: 0, b: 0); + } _attitudeTracker.stop(); } - + void setSettings(BadPostureSettings settings) { _settings = settings; } bool _isBadPosture(Attitude attitude) { - return attitude.roll.abs() * (360 / (2 * pi)) > _settings.rollAngleThreshold || attitude.pitch.abs() * (360 / (2 * pi)) > _settings.pitchAngleThreshold; + return attitude.roll.abs() * (360 / (2 * pi)) > + _settings.rollAngleThreshold || + attitude.pitch.abs() * (360 / (2 * pi)) > _settings.pitchAngleThreshold; } void alarm() { print("playing jingle to alert of bad posture"); // play jingle - _openEarable.audioPlayer.jingle(4); + if (_openEarable.bleManager.connected) { + _openEarable.audioPlayer.jingle(4); + } } -} \ No newline at end of file +} diff --git a/open_earable/lib/apps/posture_tracker/model/earable_attitude_tracker.dart b/open_earable/lib/apps/posture_tracker/model/earable_attitude_tracker.dart index 0e151e4..bc84324 100644 --- a/open_earable/lib/apps/posture_tracker/model/earable_attitude_tracker.dart +++ b/open_earable/lib/apps/posture_tracker/model/earable_attitude_tracker.dart @@ -7,7 +7,7 @@ import 'package:open_earable_flutter/src/open_earable_flutter.dart'; class EarableAttitudeTracker extends AttitudeTracker { final OpenEarable _openEarable; StreamSubscription>? _subscription; - + @override bool get isTracking => _subscription != null && !_subscription!.isPaused; @override @@ -21,7 +21,7 @@ class EarableAttitudeTracker extends AttitudeTracker { _openEarable.bleManager.connectionStateStream.listen((connected) { didChangeAvailability(this); if (!connected) { - cancle(); + cancel(); } }); } @@ -34,12 +34,12 @@ class EarableAttitudeTracker extends AttitudeTracker { } _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); - _subscription = _openEarable.sensorManager.subscribeToSensorData(0).listen((event) { + _subscription = + _openEarable.sensorManager.subscribeToSensorData(0).listen((event) { updateAttitude( - roll: _rollEWMA.update(event["EULER"]["ROLL"]), - pitch: _pitchEWMA.update(event["EULER"]["PITCH"]), - yaw: _yawEWMA.update(event["EULER"]["YAW"]) - ); + roll: _rollEWMA.update(event["EULER"]["ROLL"]), + pitch: _pitchEWMA.update(event["EULER"]["PITCH"]), + yaw: _yawEWMA.update(event["EULER"]["YAW"])); }); } @@ -49,17 +49,13 @@ class EarableAttitudeTracker extends AttitudeTracker { } @override - void cancle() { + void cancel() { stop(); _subscription?.cancel(); - super.cancle(); + super.cancel(); } OpenEarableSensorConfig _buildSensorConfig() { - return OpenEarableSensorConfig( - sensorId: 0, - samplingRate: 30, - latency: 0 - ); + return OpenEarableSensorConfig(sensorId: 0, samplingRate: 30, latency: 0); } -} \ No newline at end of file +} diff --git a/open_earable/lib/apps/posture_tracker/model/mock_attitude_tracker.dart b/open_earable/lib/apps/posture_tracker/model/mock_attitude_tracker.dart index bfca7e8..1f8359c 100644 --- a/open_earable/lib/apps/posture_tracker/model/mock_attitude_tracker.dart +++ b/open_earable/lib/apps/posture_tracker/model/mock_attitude_tracker.dart @@ -9,21 +9,22 @@ class MockAttitudeTracker extends AttitudeTracker { StreamSubscription? _attitudeSubscription; @override - bool get isTracking => _attitudeSubscription != null && !_attitudeSubscription!.isPaused; + bool get isTracking => + _attitudeSubscription != null && !_attitudeSubscription!.isPaused; bool _isAvailable = false; @override bool get isAvailable => _isAvailable; - MockAttitudeTracker({Function(AttitudeTracker)? didChangeAvailability}) : super() { + MockAttitudeTracker({Function(AttitudeTracker)? didChangeAvailability}) + : super() { _attitudeStream = Stream.periodic(Duration(milliseconds: 100), (count) { return Attitude( - roll: sin(count / 10) * pi / 4, - pitch: sin(count / 20) * pi / 4, - yaw: sin(count / 30) * pi / 4 - ); + roll: sin(count / 10) * pi / 4, + pitch: sin(count / 20) * pi / 4, + yaw: sin(count / 30) * pi / 4); }); - didChangeAvailability = didChangeAvailability ?? (_) { }; + didChangeAvailability = didChangeAvailability ?? (_) {}; didChangeAvailability(this); // wait for 5 seconds before setting the tracker to available @@ -53,8 +54,8 @@ class MockAttitudeTracker extends AttitudeTracker { } @override - void cancle() { + void cancel() { _attitudeSubscription?.cancel(); - super.cancle(); + super.cancel(); } -} \ No newline at end of file +} diff --git a/open_earable/lib/apps/posture_tracker/view_model/posture_tracker_view_model.dart b/open_earable/lib/apps/posture_tracker/view_model/posture_tracker_view_model.dart index 0f07bc2..b863bf9 100644 --- a/open_earable/lib/apps/posture_tracker/view_model/posture_tracker_view_model.dart +++ b/open_earable/lib/apps/posture_tracker/view_model/posture_tracker_view_model.dart @@ -22,10 +22,7 @@ class PostureTrackerViewModel extends ChangeNotifier { _attitudeTracker.listen((attitude) { _attitude = Attitude( - roll: attitude.roll, - pitch: attitude.pitch, - yaw: attitude.yaw - ); + roll: attitude.roll, pitch: attitude.pitch, yaw: attitude.yaw); notifyListeners(); }); } @@ -53,7 +50,7 @@ class PostureTrackerViewModel extends ChangeNotifier { @override void dispose() { stopTracking(); - _attitudeTracker.cancle(); + _attitudeTracker.cancel(); super.dispose(); } -} \ No newline at end of file +} diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 85dec37..073b2a4 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -1,9 +1,13 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; import 'package:open_earable/apps/recorder.dart'; import 'package:open_earable/apps/jump_height_test/jump_height_test.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'global_theme.dart'; class AppInfo { final IconData iconData; @@ -33,8 +37,12 @@ class AppsTab extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => PostureTrackerView( - EarableAttitudeTracker(_openEarable), _openEarable))); + builder: (context) => Material( + child: Theme( + data: materialTheme, + child: PostureTrackerView( + EarableAttitudeTracker(_openEarable), + _openEarable))))); }), AppInfo( iconData: Icons.fiber_smart_record, @@ -44,7 +52,10 @@ class AppsTab extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => Recorder(_openEarable))); + builder: (context) => Material( + child: Theme( + data: materialTheme, + child: Recorder(_openEarable))))); }), AppInfo( iconData: Icons.height, @@ -54,7 +65,9 @@ class AppsTab extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => JumpHeightTest(_openEarable))); + builder: (context) => Theme( + data: materialTheme, + child: Material(child: JumpHeightTest(_openEarable))))); }), // ... similarly for other apps ]; @@ -72,8 +85,12 @@ class AppsTab extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 5), child: Card( - color: Theme.of(context).colorScheme.primary, + color: Platform.isIOS + ? CupertinoTheme.of(context).primaryContrastingColor + : Theme.of(context).colorScheme.primary, child: ListTile( + iconColor: Colors.white, + textColor: Colors.white, leading: Icon(apps[index].iconData, size: 40.0), title: Text(apps[index].title), subtitle: Text(apps[index].description), diff --git a/open_earable/lib/ble.dart b/open_earable/lib/ble.dart index a64cc23..aac5c6a 100644 --- a/open_earable/lib/ble.dart +++ b/open_earable/lib/ble.dart @@ -116,7 +116,7 @@ class _BLEPageState extends State { child: Platform.isIOS ? CupertinoButton( padding: EdgeInsets.fromLTRB(16, 0, 16, 0), - child: Text('Restart Scan'), + child: const Text('Restart Scan'), color: CupertinoTheme.of(context) .primaryColor, // iOS equivalent for a prominent button color onPressed: diff --git a/open_earable/lib/controls_tab/views/audio_player.dart b/open_earable/lib/controls_tab/views/audio_player.dart index 4e2e9bf..0d9c9f5 100644 --- a/open_earable/lib/controls_tab/views/audio_player.dart +++ b/open_earable/lib/controls_tab/views/audio_player.dart @@ -228,7 +228,7 @@ class _AudioPlayerCardState extends State { ? CupertinoRadio( value: index, groupValue: OpenEarableSettings().selectedAudioPlayerRadio, - onChanged: !_openEarable.bleManager.connected + onChanged: !_connected ? null : (int? value) { setState(() { @@ -244,7 +244,7 @@ class _AudioPlayerCardState extends State { : Radio( value: index, groupValue: OpenEarableSettings().selectedAudioPlayerRadio, - onChanged: !_openEarable.bleManager.connected + onChanged: !_connected ? null : (int? value) { setState(() { @@ -294,7 +294,7 @@ class _AudioPlayerCardState extends State { obscureText: false, placeholder: placeholder, style: TextStyle( - color: _openEarable.bleManager.connected ? Colors.black : Colors.grey, + color: _connected ? Colors.black : Colors.grey, ), padding: EdgeInsets.fromLTRB(8, 0, 0, 0), textAlignVertical: TextAlignVertical.center, @@ -307,7 +307,7 @@ class _AudioPlayerCardState extends State { borderRadius: BorderRadius.circular(4.0), ), placeholderStyle: TextStyle( - color: _openEarable.bleManager.connected ? Colors.black : Colors.grey, + color: _connected ? Colors.black : Colors.grey, ), keyboardType: keyboardType, maxLength: maxLength, @@ -317,23 +317,16 @@ class _AudioPlayerCardState extends State { return TextField( controller: textController, obscureText: false, - enabled: _openEarable.bleManager.connected, - style: TextStyle( - color: - _openEarable.bleManager.connected ? Colors.black : Colors.grey), + enabled: _connected, + style: TextStyle(color: _connected ? Colors.black : Colors.grey), decoration: InputDecoration( labelText: placeholder, contentPadding: EdgeInsets.all(8), border: OutlineInputBorder(), floatingLabelBehavior: FloatingLabelBehavior.never, - labelStyle: TextStyle( - color: _openEarable.bleManager.connected - ? Colors.black - : Colors.grey), + labelStyle: TextStyle(color: _connected ? Colors.black : Colors.grey), filled: true, - fillColor: _openEarable.bleManager.connected - ? Colors.white - : Colors.grey[200], + fillColor: _connected ? Colors.white : Colors.grey[200], ), keyboardType: keyboardType, maxLength: maxLength, @@ -368,7 +361,7 @@ class _AudioPlayerCardState extends State { }); }, (_) => null, - _openEarable.bleManager.connected, + _connected, ), ), ), @@ -402,7 +395,7 @@ class _AudioPlayerCardState extends State { child: Text( 'Hz', style: TextStyle( - color: _openEarable.bleManager.connected + color: _connected ? Colors.white : Colors.grey), // Set text color to white ), @@ -418,7 +411,7 @@ class _AudioPlayerCardState extends State { child: Text( '%', style: TextStyle( - color: _openEarable.bleManager.connected + color: _connected ? Colors.white : Colors.grey), // Set text color to white ), @@ -436,7 +429,7 @@ class _AudioPlayerCardState extends State { OpenEarableSettings().selectedWaveForm = newValue; }, ); - }, (_) => null, _openEarable.bleManager.connected), + }, (_) => null, _connected), ), ], ) @@ -495,9 +488,7 @@ class _AudioPlayerCardState extends State { width: 120, child: CupertinoButton( padding: EdgeInsets.all(0), - onPressed: _openEarable.bleManager.connected - ? _setSourceButtonPressed - : null, + onPressed: _connected ? _setSourceButtonPressed : null, color: Color(0xff53515b), child: Text( 'Set\nSource', @@ -509,8 +500,7 @@ class _AudioPlayerCardState extends State { Expanded( child: CupertinoButton( padding: EdgeInsets.all(0), - onPressed: - _openEarable.bleManager.connected ? _playButtonPressed : null, + onPressed: _connected ? _playButtonPressed : null, color: CupertinoTheme.of(context).primaryColor, child: Icon(CupertinoIcons.play), )), @@ -518,8 +508,7 @@ class _AudioPlayerCardState extends State { Expanded( child: CupertinoButton( padding: EdgeInsets.zero, - onPressed: - _openEarable.bleManager.connected ? _pauseButtonPressed : null, + onPressed: _connected ? _pauseButtonPressed : null, color: Color(0xffe0f277), child: Icon(CupertinoIcons.pause), )), @@ -527,8 +516,7 @@ class _AudioPlayerCardState extends State { Expanded( child: CupertinoButton( padding: EdgeInsets.zero, - onPressed: - _openEarable.bleManager.connected ? _stopButtonPressed : null, + onPressed: _connected ? _stopButtonPressed : null, color: Color(0xfff27777), child: Icon(CupertinoIcons.stop), )), diff --git a/open_earable/lib/controls_tab/views/led_color.dart b/open_earable/lib/controls_tab/views/led_color.dart index 981a077..87923a6 100644 --- a/open_earable/lib/controls_tab/views/led_color.dart +++ b/open_earable/lib/controls_tab/views/led_color.dart @@ -6,8 +6,9 @@ import 'dart:async'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'dart:io'; import 'package:provider/provider.dart'; - import '../../ble_controller.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import '../../global_theme.dart'; class LEDColorCard extends StatefulWidget { final OpenEarable _openEarable; @@ -156,22 +157,34 @@ class _LEDColorCardState extends State { return CupertinoAlertDialog( title: Text('Pick a color for the RGB LED'), content: SingleChildScrollView( - child: Material( - // Wrap with Material - child: ColorPicker( - pickerColor: OpenEarableSettings().selectedColor, - onColorChanged: (color) { - // Your color change logic - setState(() { - OpenEarableSettings().selectedColor = color; - }); - }, - showLabel: true, - pickerAreaHeightPercent: 0.8, - enableAlpha: false, - ), - ), - ), + padding: EdgeInsets.zero, + child: Theme( + data: materialTheme, + child: Material( + // Wrap with Material + child: Localizations( + locale: + const Locale('en', 'US'), // Specify the app's locale + delegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + child: ColorPicker( + pickerColor: OpenEarableSettings().selectedColor, + onColorChanged: (color) { + // Your color change logic + setState(() { + OpenEarableSettings().selectedColor = color; + }); + }, + showLabel: true, + pickerAreaHeightPercent: 0.8, + enableAlpha: false, + ), // Your widget that contains the DropdownButton + ), + ), + )), actions: [ CupertinoDialogAction( onPressed: () { diff --git a/open_earable/lib/global_theme.dart b/open_earable/lib/global_theme.dart new file mode 100644 index 0000000..6542a42 --- /dev/null +++ b/open_earable/lib/global_theme.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; + +final ThemeData materialTheme = ThemeData( + useMaterial3: false, + colorScheme: ColorScheme( + brightness: Brightness.dark, + primary: Color.fromARGB(255, 54, 53, 59), + onPrimary: Colors.white, + secondary: Color.fromARGB(255, 119, 242, 161), + onSecondary: Colors.white, + error: Colors.red, + onError: Colors.black, + background: Color.fromARGB(255, 22, 22, 24), + onBackground: Colors.white, + surface: Color.fromARGB(255, 22, 22, 24), + onSurface: Colors.white, + ), + secondaryHeaderColor: Colors.black, +); + +final CupertinoThemeData cupertinoTheme = CupertinoThemeData( + brightness: Brightness.dark, + primaryColor: Color.fromARGB(255, 119, 242, 161), + primaryContrastingColor: Color.fromARGB(255, 54, 53, 59), + barBackgroundColor: Color.fromARGB(255, 22, 22, 24), + scaffoldBackgroundColor: Color.fromARGB(255, 22, 22, 24), + textTheme: CupertinoTextThemeData( + primaryColor: Colors.white, + ), +); diff --git a/open_earable/lib/main.dart b/open_earable/lib/main.dart index 1afc7ae..2c2da64 100644 --- a/open_earable/lib/main.dart +++ b/open_earable/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:open_earable/open_earable_icon_icons.dart'; import 'package:provider/provider.dart'; import 'controls_tab/controls_tab.dart'; @@ -13,44 +14,25 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:app_settings/app_settings.dart'; import 'ble_controller.dart'; +import 'global_theme.dart'; void main() => runApp(ChangeNotifierProvider( create: (context) => BluetoothController(), child: MyApp())); class MyApp extends StatelessWidget { - final ThemeData materialTheme = ThemeData( - useMaterial3: false, - colorScheme: ColorScheme( - brightness: Brightness.dark, - primary: - Color.fromARGB(255, 54, 53, 59), //Color.fromARGB(255, 22, 22, 24) - onPrimary: Colors.white, - secondary: Color.fromARGB(255, 119, 242, 161), - onSecondary: Colors.white, - error: Colors.red, - onError: Colors.black, - background: - Color.fromARGB(255, 22, 22, 24), //Color.fromARGB(255, 54, 53, 59) - onBackground: Colors.white, - surface: Color.fromARGB(255, 22, 22, 24), - onSurface: Colors.white), - secondaryHeaderColor: Colors.black); - - final CupertinoThemeData cupertinoTheme = CupertinoThemeData( - brightness: Brightness.dark, - primaryColor: Color.fromARGB(255, 119, 242, 161), - primaryContrastingColor: Color.fromARGB(255, 54, 53, 59), - barBackgroundColor: Color.fromARGB(255, 22, 22, 24), - scaffoldBackgroundColor: Color.fromARGB(255, 22, 22, 24), - textTheme: CupertinoTextThemeData( - primaryColor: Colors.white, - ), - ); - @override Widget build(BuildContext context) { if (Platform.isIOS) { return CupertinoApp( + locale: const Locale('en', 'US'), + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', 'US'), + ], title: '🦻 OpenEarable', theme: cupertinoTheme, home: MyHomePage(), diff --git a/open_earable/pubspec.lock b/open_earable/pubspec.lock index ea7035f..319104f 100644 --- a/open_earable/pubspec.lock +++ b/open_earable/pubspec.lock @@ -239,6 +239,11 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_native_splash: dependency: "direct main" description: @@ -306,13 +311,13 @@ packages: source: hosted version: "3.3.0" intl: - dependency: transitive + dependency: "direct overridden" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" js: dependency: transitive description: diff --git a/open_earable/pubspec.yaml b/open_earable/pubspec.yaml index cc9ddff..bbfb06c 100644 --- a/open_earable/pubspec.yaml +++ b/open_earable/pubspec.yaml @@ -30,6 +30,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter open_earable_flutter: path: ../../open_earable_flutter permission_handler: ^11.1.0 @@ -64,7 +66,10 @@ dependencies: open_file: ^3.3.2 provider: ^6.1.1 shared_preferences: ^2.2.2 - + +dependency_overrides: + intl: ^0.18.1 + dev_dependencies: flutter_test: sdk: flutter From b22b4292bb175467638afdc18126076a72f05f4c Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Mon, 12 Feb 2024 12:29:47 +0100 Subject: [PATCH 077/104] add jump rope counter without height stats because height calculation did not work --- open_earable/lib/apps/jump_rope_counter.dart | 514 +++++++++++++++++++ open_earable/lib/apps_tab.dart | 11 + 2 files changed, 525 insertions(+) create mode 100644 open_earable/lib/apps/jump_rope_counter.dart diff --git a/open_earable/lib/apps/jump_rope_counter.dart b/open_earable/lib/apps/jump_rope_counter.dart new file mode 100644 index 0000000..db06af1 --- /dev/null +++ b/open_earable/lib/apps/jump_rope_counter.dart @@ -0,0 +1,514 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:open_earable/widgets/earable_not_connected_warning.dart'; +import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'dart:async'; +import 'package:intl/intl.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Result of a jump rope session. +/// Contains the number of jumps, duration of the session and average jump height. +class JumpRecordResult { + final String date; + final int jumps; + final Duration duration; + + /// Constructor for JumpRecordResult. + JumpRecordResult({required this.jumps, required this.duration}) + : date = DateFormat('dd. MMMM yyyy HH:mm').format(DateTime.now()); +} + +/// JumpRopeCounter widget. +class JumpRopeCounter extends StatefulWidget { + /// Instance of OpenEarable device. + final OpenEarable _openEarable; + + /// Constructor for JumpRopeCounter widget. + JumpRopeCounter(this._openEarable); + + _JumpRopeCounterState createState() => _JumpRopeCounterState(_openEarable); +} + +/// The state of the JumpRopeCounter widget. +/// Contains the UI and logic for the JumpRopeCounter widget. +class _JumpRopeCounterState extends State + with SingleTickerProviderStateMixin { + /// Instance of OpenEarable device. + final OpenEarable _openEarable; + + /// Subscription to the IMU sensor. + StreamSubscription? _imuSubscription; + + /// Jump detection. + bool _detectedJump = false; + + bool _firstJump = true; + + /// Sampling rate for the accelerometer. + double _samplingRate = 10.0; + + /// Number of jumps. + int _jumps = 0; + + /// Average jump height. + double _avgJumpHeight = 0.0; + + /// Gravitational acceleration. + final double _gravity = 9.81; + + /// Timer for recording duration. + Timer? _timer; + + /// Recording state. + bool _recording = false; + + /// Duration of the recording. + Duration _duration = Duration(); + + /// Maximum number of saved recordings. + int _maxSavedItems = 50; + + /// List of past recordings. + late List _recordings = []; + + /// Tab controller for the two tabs. + late TabController _tabController; + + /// Amount of tabs. + late int _tabAmount = 2; + + /// Constructor for _JumpRopeCounterState. + _JumpRopeCounterState(this._openEarable); + + /// Initializes state and sets up listeners for sensor data. + @override + void initState() { + super.initState(); + loadJumpRecordings(); + _tabController = + TabController(length: _tabAmount, vsync: this, initialIndex: 0); + if (_openEarable.bleManager.connected) { + /// Set sampling rate to maximum. + _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); + + /// Setup listeners for sensor data. + _setupListeners(); + } + } + + /// Cancels the subscription to the IMU sensor when the widget is disposed. + @override + void dispose() { + super.dispose(); + _imuSubscription?.cancel(); + } + + // Loads the past recordings from shared preferences. + void loadJumpRecordings() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + List? recordings = prefs.getStringList('jumpRecordings'); + setState(() { + _recordings = recordings + ?.map((e) => JumpRecordResult( + jumps: int.parse(e.split(',')[0]), + duration: Duration(seconds: int.parse(e.split(',')[1])))) + .toList() ?? + []; + }); + } + + /// Starts the timer. + void _startTimer() { + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + setState(() { + _duration = _duration + Duration(seconds: 1); + }); + }); + } + + /// Stops the timer. + void _stopTimer() { + if (_timer != null) { + _timer!.cancel(); + _duration = Duration(); + } + } + + /// Starts or stops the recording. + void startStopRecording() { + if (_recording) { + setState(() { + _recording = false; + }); + _saveResult(); + _stopTimer(); + _resetCounter(); + } else { + setState(() { + _recording = true; + _firstJump = true; + _startTimer(); + _setupListeners(); + }); + } + } + + /// Resets all counters to 0. + void _resetCounter() { + _jumps = 0; + _avgJumpHeight = 0.0; + } + + /// Saves the result of the recording. + void _saveResult() { + if (_recordings.length >= _maxSavedItems) { + _recordings.removeAt(0); + } + _recordings.add(JumpRecordResult(jumps: _jumps, duration: _duration)); + + /// Save the recordings to shared preferences. + saveJumpRecordings(); + } + + /// Saves the recordings to shared preferences. + void saveJumpRecordings() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setStringList('jumpRecordings', + _recordings.map((e) => '${e.jumps},${e.duration.inSeconds}').toList()); + } + + /// Formats the duration to a string. + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); + String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); + return "$twoDigitMinutes:$twoDigitSeconds"; + } + + /// Builds the sensor config. + OpenEarableSensorConfig _buildSensorConfig() { + return OpenEarableSensorConfig( + sensorId: 0, + samplingRate: _samplingRate, + latency: 0, + ); + } + + /// Sets up listeners for sensor data. + _setupListeners() { + _imuSubscription = + _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { + /// If the recording is stopped, stop processing sensor data. + if (!_recording) { + return; + } + _processSensorData(data); + }); + } + + /// Processes the sensor data. + void _processSensorData(Map data) { + double _accX = data["ACC"]["X"]; + double _accY = data["ACC"]["Y"]; + double _accZ = data["ACC"]["Z"]; + double accMagnitude = + _accZ.sign * sqrt(_accX * _accX + _accY * _accY + _accZ * _accZ); + double currentAcc = accMagnitude - _gravity; + + _updateJumps(currentAcc); + } + + double maxAcc = -double.infinity; + + /// Updates the number of jumps. + Future _updateJumps(double currentAcc) async { + if (currentAcc > 7.5 && !_detectedJump) { + setState(() { + if (!_firstJump) { + _jumps++; + } else { + _firstJump = false; + } + }); + _detectedJump = true; + } else if (currentAcc < 0) { + _detectedJump = false; + } + } + + /// Builds the UI for the JumpRopeCounter widget. + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: _tabAmount, + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + appBar: AppBar( + title: Text('Jump Rope Counter'), + bottom: TabBar( + controller: _tabController, + indicator: BoxDecoration( + borderRadius: BorderRadius.circular(8), // Creates border + color: Colors.greenAccent), + tabs: [ + Tab(text: "Record"), + Tab(text: "Jump Activity"), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + /// If the earable is not connected, show a warning. Otherwise show the jump counter. + _openEarable.bleManager.connected + ? _ropeCounterWidget() + : EarableNotConnectedWarning(), + _ropeSkipHistoryWidget(), + ], + ), + ), + ); + } + + /// Builds the UI for the jump counter. + Widget _ropeCounterWidget() { + return Center( + child: Column(mainAxisAlignment: MainAxisAlignment.start, children: [ + Column(children: [ + SizedBox(height: 32), + Text( + "Jumps", + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(16, 0, 16, 64), + child: Text( + "$_jumps", + style: TextStyle( + fontFamily: 'Digital', + // This is a common monospaced font + fontSize: 100, + fontWeight: FontWeight.normal, + ), + ), + ), + ]), + Padding( + padding: EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(16, 0, 32, 0), + child: Text("Time")), + Padding( + padding: EdgeInsets.fromLTRB(16, 0, 32, 0), + child: Text( + _formatDuration(_duration), + style: TextStyle( + fontFamily: 'Digital', + // This is a common monospaced font + fontSize: 60, + fontWeight: FontWeight.normal, + ), + ), + ), + ], + ), + ), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: EdgeInsets.fromLTRB(32, 0, 32, 64), + child: ElevatedButton( + onPressed: startStopRecording, + style: ElevatedButton.styleFrom( + fixedSize: Size(250, 70), + backgroundColor: _recording + ? Color(0xfff27777) + : Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.black, + ), + child: Text( + _recording ? "Stop Recording" : "Start Recording", + style: TextStyle(fontSize: 20), + ), + ), + ), + )) + ])); + } + + /// Builds the UI for the jump rope history. + Widget _ropeSkipHistoryWidget() { + return Center( + child: Column( + children: [ + SizedBox(height: 16), + Center( + child: Text( + "Your past $_maxSavedItems sessions are saved here.", + style: TextStyle( + fontSize: 20, + ), + textAlign: TextAlign.center, + ), + ), + SizedBox(height: 16), + Divider( + thickness: 2, + ), + Expanded( + child: _recordings.isEmpty + ? Stack( + fit: StackFit.expand, + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.info, + size: 48, + color: Colors.yellow, + ), + SizedBox(height: 16), + Center( + child: Text( + "No Workouts Found.", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ], + ) + : ListView.separated( + itemCount: _recordings.length, + physics: BouncingScrollPhysics(), + itemBuilder: (context, index) { + int _reverseIndex = _recordings.length - index - 1; + return _listItem(_reverseIndex); + }, + separatorBuilder: (context, index) { + return Divider( + thickness: 2, + ); + }, + ), + ), + ], + ), + ); + } + + /// Builds a list item for the jump rope history. + /// The list item is a dismissible widget. + Widget _listItem(index) { + return Dismissible( + key: UniqueKey(), + direction: DismissDirection.endToStart, + background: Container( + color: Colors.red, + child: Align( + child: Padding( + padding: const EdgeInsets.only(right: 16), + child: Icon(Icons.delete), + ), + alignment: Alignment.centerRight, + ), + ), + confirmDismiss: (direction) async { + if (direction == DismissDirection.endToStart) { + bool delete = true; + final snackbarController = ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Deleted Entry'), + duration: Duration(milliseconds: 2000), + action: SnackBarAction( + label: 'Undo', onPressed: () => delete = false), + ), + ); + + return delete; + } + return true; + }, + onDismissed: (direction) { + setState(() async { + await snackbarController.closed; + _recordings.removeAt(index); + saveJumpRecordings(); + }); + }, + child: ListTile( + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.fromLTRB(0, 0, 0, 20), + child: Text( + _recordings[index].date, + maxLines: 1, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + children: [ + Text( + _recordings[index].jumps.toString(), + maxLines: 1, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + Text( + "jump count", + maxLines: 1, + ), + ], + ), + Column( + children: [ + Text( + _recordings[index].duration.toString().substring(2, 7), + maxLines: 1, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + Text( + "jump time", + maxLines: 1, + ), + ], + ), + ], + ) + ], + ), + ), + ); + } +} diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 85dec37..09177b8 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -4,6 +4,7 @@ import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart import 'package:open_earable/apps/recorder.dart'; import 'package:open_earable/apps/jump_height_test/jump_height_test.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; +import 'package:open_earable/apps/jump_rope_counter.dart'; class AppInfo { final IconData iconData; @@ -56,6 +57,16 @@ class AppsTab extends StatelessWidget { MaterialPageRoute( builder: (context) => JumpHeightTest(_openEarable))); }), + AppInfo( + iconData: Icons.keyboard_double_arrow_up, + title: "Jump Rope Counter", + description: "Counter for rope skipping.", + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => JumpRopeCounter(_openEarable))); + }), // ... similarly for other apps ]; } From fa48bec5d989b9ff022d63c23ef9ef510860f3c9 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Mon, 12 Feb 2024 22:49:57 +0100 Subject: [PATCH 078/104] fix jump rope counter --- open_earable/lib/apps/jump_rope_counter.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/open_earable/lib/apps/jump_rope_counter.dart b/open_earable/lib/apps/jump_rope_counter.dart index db06af1..d505b3f 100644 --- a/open_earable/lib/apps/jump_rope_counter.dart +++ b/open_earable/lib/apps/jump_rope_counter.dart @@ -439,14 +439,13 @@ class _JumpRopeCounterState extends State label: 'Undo', onPressed: () => delete = false), ), ); - + await snackbarController.closed; return delete; } return true; }, onDismissed: (direction) { - setState(() async { - await snackbarController.closed; + setState(() { _recordings.removeAt(index); saveJumpRecordings(); }); From 3ca05852149c84122495ac078c72456b48cdec49 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Mon, 19 Feb 2024 19:52:30 +0100 Subject: [PATCH 079/104] improve power napping app --- .../assets/powernapping_timer/198155.png | Bin 0 -> 10049 bytes .../ios/Flutter/AppFrameworkInfo.plist | 2 +- open_earable/ios/Podfile | 2 +- open_earable/ios/Podfile.lock | 4 +- .../ios/Runner.xcodeproj/project.pbxproj | 6 +- open_earable/lib/apps/ufiiu/home_screen.dart | 72 ++++----- open_earable/lib/apps/ufiiu/timerscreen.dart | 150 +++++++++--------- open_earable/lib/apps_tab.dart | 2 +- open_earable/pubspec.yaml | 1 + 9 files changed, 116 insertions(+), 123 deletions(-) create mode 100644 open_earable/assets/powernapping_timer/198155.png diff --git a/open_earable/assets/powernapping_timer/198155.png b/open_earable/assets/powernapping_timer/198155.png new file mode 100644 index 0000000000000000000000000000000000000000..b9e2514423aef3f2c20ed87323013407f5b32c3c GIT binary patch literal 10049 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 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/open_earable/ios/Podfile b/open_earable/ios/Podfile index fdcc671..d97f17e 100644 --- a/open_earable/ios/Podfile +++ b/open_earable/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +# platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/open_earable/ios/Podfile.lock b/open_earable/ios/Podfile.lock index fd2fbe4..9db7ab7 100644 --- a/open_earable/ios/Podfile.lock +++ b/open_earable/ios/Podfile.lock @@ -58,7 +58,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_gl: 5a5603f35db897697f064027864a32b15d0c421d flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d @@ -69,6 +69,6 @@ SPEC CHECKSUMS: SwiftProtobuf: b70d65f419fbfe61a2d58003456ca5da58e337d6 three3d_egl: de2cd4950ad2d5f2122166c36583bde4c812e7b5 -PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189 +PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 COCOAPODS: 1.12.1 diff --git a/open_earable/ios/Runner.xcodeproj/project.pbxproj b/open_earable/ios/Runner.xcodeproj/project.pbxproj index d696e14..f03d53c 100644 --- a/open_earable/ios/Runner.xcodeproj/project.pbxproj +++ b/open_earable/ios/Runner.xcodeproj/project.pbxproj @@ -463,7 +463,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; @@ -601,7 +601,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -650,7 +650,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; diff --git a/open_earable/lib/apps/ufiiu/home_screen.dart b/open_earable/lib/apps/ufiiu/home_screen.dart index 1279774..3880565 100644 --- a/open_earable/lib/apps/ufiiu/home_screen.dart +++ b/open_earable/lib/apps/ufiiu/home_screen.dart @@ -4,7 +4,6 @@ import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import 'interact.dart'; - /// Homescreen class for the movement timer application. class SleepHomeScreen extends StatefulWidget { final OpenEarable _openEarable; @@ -17,7 +16,6 @@ class SleepHomeScreen extends StatefulWidget { /// /// Needs the [OpenEarable]-Object to interact. class _HomeScreenState extends State { - final OpenEarable _openEarable; //Constructor @@ -26,14 +24,12 @@ class _HomeScreenState extends State { //Bottom-Navigation-Bar index. int _currentIndex = 0; - - //Build main Widget. @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Timer App'), + title: Text('Powernapper Alarm Clock'), ), //Body for the widget @@ -44,10 +40,6 @@ class _HomeScreenState extends State { currentIndex: _currentIndex, onTap: _onNavBarItemTapped, items: [ - BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Home', - ), BottomNavigationBarItem( icon: Icon(Icons.timer), label: 'Timer', @@ -66,43 +58,43 @@ class _HomeScreenState extends State { switch (_currentIndex) { case 0: - //HomeScreenTab - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - //Image-Source - Text( - 'Mit der Powernapping App können Sie einen Timer starten, der ganz automatisch an Ihren Bewegungen erkennt, ' - 'wann Sie wirklich eingeschlafen sind. Der Timer wird automatisch restartet, wenn Sie sich bewegen, ' - 'so können Sie effektiv powernappen und eine gemütliche Position finden ohne das schon die Zeit abläuft!', - style: TextStyle(fontSize: 24), - ), - SizedBox(height: 20), - ], - ) - ); - case 1: - //Timer Tab - return Center( - child: Text('Wird weitergeleitet...') - ); - case 2: + return TimerScreen(Interact(_openEarable)); + case 1: //Information Tab - return Center( - child: Text('Diese Sub-App wurde entwickelt von: Philipp Ochs, Matrikelnummer 2284828'), + return Column( + children: [ + Spacer(), + Padding( + padding: const EdgeInsets.all(18.0), + child: Text( + 'Mit der Powernapping App können Sie einen Timer starten, der ganz ' + 'automatisch an Ihren Bewegungen erkennt, wann Sie wirklich eingeschlafen sind. ' + 'Der Timer wird automatisch restartet, wenn Sie sich bewegen, ' + 'so können Sie effektiv powernappen und eine gemütliche Position finden ' + 'ohne dass schon die Zeit abläuft!', + style: TextStyle(fontSize: 24), + ), + ), + Spacer(), + Padding( + padding: const EdgeInsets.all(18.0), + child: Text( + 'Diese Sub-App wurde entwickelt von: Philipp Ochs, Matrikelnummer 2284828', + style: TextStyle(fontSize: 16), + ), + ), + ], ); + default: //Default return Center( child: Text('Ungültiger Index'), ); - } - return Container(); } ///Navigation-Bar interaction @@ -111,14 +103,6 @@ class _HomeScreenState extends State { void _onNavBarItemTapped(int index) { setState(() { _currentIndex = index; - if (index == 1) { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => TimerScreen(Interact(_openEarable)), - ), - ); - } }); } -} \ No newline at end of file +} diff --git a/open_earable/lib/apps/ufiiu/timerscreen.dart b/open_earable/lib/apps/ufiiu/timerscreen.dart index d408bae..359c041 100644 --- a/open_earable/lib/apps/ufiiu/timerscreen.dart +++ b/open_earable/lib/apps/ufiiu/timerscreen.dart @@ -12,10 +12,8 @@ class TimerScreen extends StatefulWidget { State createState() => TimerScreenState(interact); } - /// State for the movement Timer Interaction class TimerScreenState extends State { - //Interaction class final Interact _interact; @@ -33,6 +31,12 @@ class TimerScreenState extends State { this._movementTracker = MovementTracker(_interact); } + @override + void dispose() { + super.dispose(); + _movementTracker.stop(); + } + //Updates the text data. void updateText(SensorDataType sensorData) { setState(() { @@ -40,81 +44,85 @@ class TimerScreenState extends State { }); } - - ///Builds the main Widget @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Timer App'), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Image Source - Image.network( - 'https://cdn-icons-png.flaticon.com/512/198/198155.png', - width: 150, - height: 150, - ), - SizedBox(height: 20), - - // Input for Time length - Padding( - padding: EdgeInsets.all(20), - child: TextField( - controller: _controller, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: 'Zeitlänge eingeben (in Minuten)', + return GestureDetector( + onTap: () { + FocusScope.of(context).requestFocus(FocusNode()); + }, + child: Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/powernapping_timer/198155.png', + width: 150, + height: 150, + ), + SizedBox(height: 20), + + // Input for Time length + Padding( + padding: EdgeInsets.all(20), + child: TextField( + controller: _controller, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Zeitlänge eingeben (in Minuten)', + labelStyle: TextStyle(color: Colors.grey), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey))), + style: TextStyle(color: Colors.white), + cursorColor: Colors.white, ), ), - ), - SizedBox(height: 20), + SizedBox(height: 20), - // Start timer button - ElevatedButton( - onPressed: () { - String input = _controller.text; - int minutes = int.tryParse(input) ?? 0; + // Start timer button + ElevatedButton( + onPressed: () { + String input = _controller.text; + int minutes = int.tryParse(input) ?? 0; - _movementTracker.start(minutes, updateText); - }, - child: Text('Starten'), - ), + _movementTracker.start(minutes, updateText); + }, + child: Text('Starten'), + ), - //Data table for the live display of the sensor data. - DataTable( - columns: [ - DataColumn(label: Text('Sensor')), - DataColumn(label: Text('Wert')), - ], - rows: [ - DataRow( - cells: [ - DataCell(Text('X')), - DataCell(Text(_sensorData!.x.toStringAsFixed(14))), - ], - ), - DataRow( - cells: [ - DataCell(Text('Y')), - DataCell(Text(_sensorData!.y.toStringAsFixed(14))), - ], - ), - DataRow( - cells: [ - DataCell(Text('Z')), - DataCell(Text(_sensorData!.z.toStringAsFixed(14))), - ], - ), - ], - ), - ], - ), - ), - ); + //Data table for the live display of the sensor data. + DataTable( + columns: [ + DataColumn(label: Text('Sensor')), + DataColumn(label: Text('Wert')), + ], + rows: [ + DataRow( + cells: [ + DataCell(Text('X')), + DataCell(Text(_sensorData!.x.toStringAsFixed(14))), + ], + ), + DataRow( + cells: [ + DataCell(Text('Y')), + DataCell(Text(_sensorData!.y.toStringAsFixed(14))), + ], + ), + DataRow( + cells: [ + DataCell(Text('Z')), + DataCell(Text(_sensorData!.z.toStringAsFixed(14))), + ], + ), + ], + ), + ], + )), + )); } -} \ No newline at end of file +} diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 1a1bcd0..b3d3c24 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -39,7 +39,7 @@ class AppsTab extends StatelessWidget { }), AppInfo( iconData: Icons.face_5, - title: "Powernapper", + title: "Powernapper Alarm Clock", description: "Powernapping timer!", onTap: () { Navigator.push( diff --git a/open_earable/pubspec.yaml b/open_earable/pubspec.yaml index f709a98..51a1da0 100644 --- a/open_earable/pubspec.yaml +++ b/open_earable/pubspec.yaml @@ -125,6 +125,7 @@ flutter: assets: - assets/ - assets/posture_tracker/ + - assets/powernapping_timer/ fonts: - family: OpenEarableIcon From 9b6d4220c1cee923001673b395da7bce789a2838 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Mon, 19 Feb 2024 20:29:31 +0100 Subject: [PATCH 080/104] add check if openearable is connected to powernapping app --- .../198155.png | Bin .../jump_height_test/jump_height_test.dart | 144 ++++++++---------- .../{ufiiu => powernapper}/home_screen.dart | 0 .../apps/{ufiiu => powernapper}/interact.dart | 0 .../movementTracker.dart | 0 .../sensor_datatypes.dart | 0 .../lib/apps/powernapper/timerscreen.dart | 133 ++++++++++++++++ open_earable/lib/apps/ufiiu/timerscreen.dart | 128 ---------------- 8 files changed, 200 insertions(+), 205 deletions(-) rename open_earable/assets/{powernapping_timer => powernapper}/198155.png (100%) rename open_earable/lib/apps/{ufiiu => powernapper}/home_screen.dart (100%) rename open_earable/lib/apps/{ufiiu => powernapper}/interact.dart (100%) rename open_earable/lib/apps/{ufiiu => powernapper}/movementTracker.dart (100%) rename open_earable/lib/apps/{ufiiu => powernapper}/sensor_datatypes.dart (100%) create mode 100644 open_earable/lib/apps/powernapper/timerscreen.dart delete mode 100644 open_earable/lib/apps/ufiiu/timerscreen.dart diff --git a/open_earable/assets/powernapping_timer/198155.png b/open_earable/assets/powernapper/198155.png similarity index 100% rename from open_earable/assets/powernapping_timer/198155.png rename to open_earable/assets/powernapper/198155.png diff --git a/open_earable/lib/apps/jump_height_test/jump_height_test.dart b/open_earable/lib/apps/jump_height_test/jump_height_test.dart index 86c9d00..436ea1a 100644 --- a/open_earable/lib/apps/jump_height_test/jump_height_test.dart +++ b/open_earable/lib/apps/jump_height_test/jump_height_test.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import 'package:simple_kalman/simple_kalman.dart'; import 'dart:math'; +import '../../widgets/earable_not_connected_warning.dart'; /// An app that lets you test your jump height using an OpenEarable device. class JumpHeightTest extends StatefulWidget { @@ -19,43 +20,58 @@ class JumpHeightTest extends StatefulWidget { /// State class for JumpHeightTest widget. class _JumpHeightTestState extends State - with SingleTickerProviderStateMixin { + with SingleTickerProviderStateMixin { /// Stores the start time of a jump test. DateTime? _startOfJump; + /// Stores the duration of a jump test. Duration _jumpDuration = Duration.zero; + /// Current height calculated from sensor data. double _height = 0.0; // List to store each jump's data. List _jumpData = []; // Flag to indicate if jump measurement is ongoing. bool _isJumping = false; + /// Instance of OpenEarable device. final OpenEarable _openEarable; + /// Flag to indicate if an OpenEarable device is connected. bool _earableConnected = false; + /// Subscription to IMU sensor data. StreamSubscription? _imuSubscription; + /// Stores the maximum height achieved in a jump. - double _maxHeight = 0.0; // Variable to keep track of maximum jump height + double _maxHeight = 0.0; // Variable to keep track of maximum jump height /// Error measure for Kalman filter. final _errorMeasureAcc = 5.0; + /// Kalman filters for accelerometer data. late SimpleKalman _kalmanX, _kalmanY, _kalmanZ; + /// Current velocity calculated from acceleration. double _velocity = 0.0; + /// Sampling rate time slice (inverse of frequency). - double _timeSlice = 1 / 30.0; + double _timeSlice = 1 / 30.0; + /// Standard gravity in m/s^2. double _gravity = 9.81; + /// X-axis acceleration. double _accX = 0.0; + /// Y-axis acceleration. double _accY = 0.0; + /// Z-axis acceleration. double _accZ = 0.0; + /// Pitch angle in radians. double _pitch = 0.0; + /// Manages the [TabBar]. late TabController _tabController; @@ -88,18 +104,18 @@ class _JumpHeightTestState extends State /// Sets up listeners to receive sensor data from the OpenEarable device. _setupListeners() { _imuSubscription = - _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { - // Only process sensor data if jump measurement is ongoing. - if (!_isJumping) { - return; - } - setState(() { - _jumpDuration = DateTime.now().difference(_startOfJump!); - }); - _processSensorData(data); + _openEarable.sensorManager.subscribeToSensorData(0).listen((data) { + // Only process sensor data if jump measurement is ongoing. + if (!_isJumping) { + return; + } + setState(() { + _jumpDuration = DateTime.now().difference(_startOfJump!); }); + _processSensorData(data); + }); } - + /// Starts the jump height measurement process. /// It sets the sampling rate, initializes or resets variables, and begins listening to sensor data. void _startJump() { @@ -145,25 +161,29 @@ class _JumpHeightTestState extends State void _processSensorData(Map data) { /// Kalman filtered accelerometer data for X. _accX = _kalmanX.filtered(data["ACC"]["X"]); + /// Kalman filtered accelerometer data for Y. _accY = _kalmanY.filtered(data["ACC"]["Y"]); + /// Kalman filtered accelerometer data for Z. _accZ = _kalmanZ.filtered(data["ACC"]["Z"]); + /// Pitch angle in radians. _pitch = data["EULER"]["PITCH"]; // Calculates the current vertical acceleration. // It adjusts the Z-axis acceleration with the pitch angle to account for the device's orientation. double currentAcc = _accZ * cos(_pitch) + _accX * sin(_pitch); // Subtract gravity to get acceleration due to movement. - currentAcc -= _gravity; - + currentAcc -= _gravity; + _updateHeight(currentAcc); } /// Checks if the device is stationary based on acceleration magnitude. bool _deviceIsStationary(double threshold) { double accMagnitude = sqrt(_accX * _accX + _accY * _accY + _accZ * _accZ); - bool isStationary = (accMagnitude > _gravity - threshold) && (accMagnitude < _gravity + threshold); + bool isStationary = (accMagnitude > _gravity - threshold) && + (accMagnitude < _gravity + threshold); return isStationary; } @@ -173,14 +193,14 @@ class _JumpHeightTestState extends State _updateHeight(double currentAcc) { setState(() { if (_deviceIsStationary(0.3)) { - _velocity = 0.0; - _height = 0.0; + _velocity = 0.0; + _height = 0.0; } else { - // Integrate acceleration to get velocity. - _velocity += currentAcc * _timeSlice; + // Integrate acceleration to get velocity. + _velocity += currentAcc * _timeSlice; - // Integrate velocity to get height. - _height += _velocity * _timeSlice; + // Integrate velocity to get height. + _height += _velocity * _timeSlice; } // Prevent height from going negative. @@ -199,7 +219,7 @@ class _JumpHeightTestState extends State String _prettyDuration(Duration duration) { var seconds = duration.inMilliseconds / 1000; - return '${seconds.toStringAsFixed(2)} s'; + return '${seconds.toStringAsFixed(2)} s'; } /// Builds the UI for the jump height test. @@ -218,7 +238,8 @@ class _JumpHeightTestState extends State controller: _tabController, indicatorColor: Colors.white, // Color of the underline indicator labelColor: Colors.white, // Color of the active tab label - unselectedLabelColor: Colors.grey, // Color of the inactive tab labels + unselectedLabelColor: + Colors.grey, // Color of the inactive tab labels tabs: [ Tab(text: 'Height'), Tab(text: 'Raw Acc.'), @@ -227,73 +248,42 @@ class _JumpHeightTestState extends State ), Expanded( child: (!_openEarable.bleManager.connected) - ? _notConnectedWidget() + ? EarableNotConnectedWarning() : _buildJumpHeightDataTabs(), ), - SizedBox(height: 20), // Margin between chart and button + SizedBox(height: 20), // Margin between chart and button _buildButtons(), Visibility( - // Show error message if no OpenEarable device is connected. - visible: !_earableConnected, - maintainState: true, - maintainAnimation: true, - child: Text( - "No Earable Connected", - style: TextStyle( - color: Colors.red, - fontSize: 12, - ), + // Show error message if no OpenEarable device is connected. + visible: !_earableConnected, + maintainState: true, + maintainAnimation: true, + child: Text( + "No Earable Connected", + style: TextStyle( + color: Colors.red, + fontSize: 12, ), ), - SizedBox(height: 20), // Margin between button and text + ), + SizedBox(height: 20), // Margin between button and text _buildText() ], ), ); } - Widget _notConnectedWidget() { - return Stack( - fit: StackFit.expand, + Widget _buildJumpHeightDataTabs() { + return TabBarView( + controller: _tabController, children: [ - Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.warning, - size: 48, - color: Colors.red, - ), - SizedBox(height: 16), - Center( - child: Text( - "Not connected to\nOpenEarable device", - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - ), - ], - ), - ), + JumpHeightChart(_openEarable, "Height Data"), + JumpHeightChart(_openEarable, "Raw Acceleration Data"), + JumpHeightChart(_openEarable, "Filtered Acceleration Data") ], ); } - Widget _buildJumpHeightDataTabs() { - return TabBarView( - controller: _tabController, - children: [ - JumpHeightChart(_openEarable, "Height Data"), - JumpHeightChart(_openEarable, "Raw Acceleration Data"), - JumpHeightChart(_openEarable, "Filtered Acceleration Data") - ], - ); - } - Widget _buildText() { return Container( child: Column( @@ -302,7 +292,8 @@ class _JumpHeightTestState extends State 'Max height: ${_maxHeight.toStringAsFixed(2)} m', style: Theme.of(context).textTheme.headlineMedium, ), - Text('Jump time: ${_prettyDuration(_jumpDuration)}', + Text( + 'Jump time: ${_prettyDuration(_jumpDuration)}', style: Theme.of(context).textTheme.headlineSmall, ), ], @@ -310,7 +301,6 @@ class _JumpHeightTestState extends State ); } - /// Builds buttons to start and stop the jump height measurement process. Widget _buildButtons() { return Flexible( @@ -328,7 +318,7 @@ class _JumpHeightTestState extends State ), ); } - + /// Builds a sensor configuration for the OpenEarable device. /// Sets the sensor ID, sampling rate, and latency. OpenEarableSensorConfig _buildSensorConfig() { diff --git a/open_earable/lib/apps/ufiiu/home_screen.dart b/open_earable/lib/apps/powernapper/home_screen.dart similarity index 100% rename from open_earable/lib/apps/ufiiu/home_screen.dart rename to open_earable/lib/apps/powernapper/home_screen.dart diff --git a/open_earable/lib/apps/ufiiu/interact.dart b/open_earable/lib/apps/powernapper/interact.dart similarity index 100% rename from open_earable/lib/apps/ufiiu/interact.dart rename to open_earable/lib/apps/powernapper/interact.dart diff --git a/open_earable/lib/apps/ufiiu/movementTracker.dart b/open_earable/lib/apps/powernapper/movementTracker.dart similarity index 100% rename from open_earable/lib/apps/ufiiu/movementTracker.dart rename to open_earable/lib/apps/powernapper/movementTracker.dart diff --git a/open_earable/lib/apps/ufiiu/sensor_datatypes.dart b/open_earable/lib/apps/powernapper/sensor_datatypes.dart similarity index 100% rename from open_earable/lib/apps/ufiiu/sensor_datatypes.dart rename to open_earable/lib/apps/powernapper/sensor_datatypes.dart diff --git a/open_earable/lib/apps/powernapper/timerscreen.dart b/open_earable/lib/apps/powernapper/timerscreen.dart new file mode 100644 index 0000000..bfb57dd --- /dev/null +++ b/open_earable/lib/apps/powernapper/timerscreen.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:open_earable/apps/ufiiu/movementTracker.dart'; +import 'package:open_earable/apps/ufiiu/sensor_datatypes.dart'; +import 'package:open_earable/ble_controller.dart'; +import 'package:open_earable/widgets/earable_not_connected_warning.dart'; +import 'package:provider/provider.dart'; + +import 'interact.dart'; + +/// TimerScreen - Main screen for the movment timer interaction. +class TimerScreen extends StatefulWidget { + final Interact interact; + TimerScreen(this.interact); + @override + State createState() => TimerScreenState(interact); +} + +/// State for the movement Timer Interaction +class TimerScreenState extends State { + //Interaction class + final Interact _interact; + + //Movement & timer logic + late final MovementTracker _movementTracker; + + //Input Controller + final TextEditingController _controller = TextEditingController(); + + //Display Data + SensorDataType? _sensorData = NullData(); + + //Constructor + TimerScreenState(this._interact) { + this._movementTracker = MovementTracker(_interact); + } + + @override + void dispose() { + super.dispose(); + _movementTracker.stop(); + } + + //Updates the text data. + void updateText(SensorDataType sensorData) { + setState(() { + _sensorData = sensorData; + }); + } + + ///Builds the main Widget + @override + Widget build(BuildContext context) { + return Provider.of(context, listen: false).connected + ? GestureDetector( + onTap: () { + FocusScope.of(context).requestFocus(FocusNode()); + }, + child: Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/powernapping_timer/198155.png', + width: 150, + height: 150, + ), + SizedBox(height: 20), + + // Input for Time length + Padding( + padding: EdgeInsets.all(20), + child: TextField( + controller: _controller, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Zeitlänge eingeben (in Minuten)', + labelStyle: TextStyle(color: Colors.grey), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey))), + style: TextStyle(color: Colors.white), + cursorColor: Colors.white, + ), + ), + SizedBox(height: 20), + + // Start timer button + ElevatedButton( + onPressed: () { + String input = _controller.text; + int minutes = int.tryParse(input) ?? 0; + + _movementTracker.start(minutes, updateText); + }, + child: Text('Starten'), + ), + + //Data table for the live display of the sensor data. + DataTable( + columns: [ + DataColumn(label: Text('Sensor')), + DataColumn(label: Text('Wert')), + ], + rows: [ + DataRow( + cells: [ + DataCell(Text('X')), + DataCell(Text(_sensorData!.x.toStringAsFixed(14))), + ], + ), + DataRow( + cells: [ + DataCell(Text('Y')), + DataCell(Text(_sensorData!.y.toStringAsFixed(14))), + ], + ), + DataRow( + cells: [ + DataCell(Text('Z')), + DataCell(Text(_sensorData!.z.toStringAsFixed(14))), + ], + ), + ], + ), + ], + )), + )) + : EarableNotConnectedWarning(); + } +} diff --git a/open_earable/lib/apps/ufiiu/timerscreen.dart b/open_earable/lib/apps/ufiiu/timerscreen.dart deleted file mode 100644 index 359c041..0000000 --- a/open_earable/lib/apps/ufiiu/timerscreen.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:open_earable/apps/ufiiu/movementTracker.dart'; -import 'package:open_earable/apps/ufiiu/sensor_datatypes.dart'; - -import 'interact.dart'; - -/// TimerScreen - Main screen for the movment timer interaction. -class TimerScreen extends StatefulWidget { - final Interact interact; - TimerScreen(this.interact); - @override - State createState() => TimerScreenState(interact); -} - -/// State for the movement Timer Interaction -class TimerScreenState extends State { - //Interaction class - final Interact _interact; - - //Movement & timer logic - late final MovementTracker _movementTracker; - - //Input Controller - final TextEditingController _controller = TextEditingController(); - - //Display Data - SensorDataType? _sensorData = NullData(); - - //Constructor - TimerScreenState(this._interact) { - this._movementTracker = MovementTracker(_interact); - } - - @override - void dispose() { - super.dispose(); - _movementTracker.stop(); - } - - //Updates the text data. - void updateText(SensorDataType sensorData) { - setState(() { - _sensorData = sensorData; - }); - } - - ///Builds the main Widget - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - FocusScope.of(context).requestFocus(FocusNode()); - }, - child: Center( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/powernapping_timer/198155.png', - width: 150, - height: 150, - ), - SizedBox(height: 20), - - // Input for Time length - Padding( - padding: EdgeInsets.all(20), - child: TextField( - controller: _controller, - keyboardType: TextInputType.number, - decoration: InputDecoration( - labelText: 'Zeitlänge eingeben (in Minuten)', - labelStyle: TextStyle(color: Colors.grey), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Colors.grey))), - style: TextStyle(color: Colors.white), - cursorColor: Colors.white, - ), - ), - SizedBox(height: 20), - - // Start timer button - ElevatedButton( - onPressed: () { - String input = _controller.text; - int minutes = int.tryParse(input) ?? 0; - - _movementTracker.start(minutes, updateText); - }, - child: Text('Starten'), - ), - - //Data table for the live display of the sensor data. - DataTable( - columns: [ - DataColumn(label: Text('Sensor')), - DataColumn(label: Text('Wert')), - ], - rows: [ - DataRow( - cells: [ - DataCell(Text('X')), - DataCell(Text(_sensorData!.x.toStringAsFixed(14))), - ], - ), - DataRow( - cells: [ - DataCell(Text('Y')), - DataCell(Text(_sensorData!.y.toStringAsFixed(14))), - ], - ), - DataRow( - cells: [ - DataCell(Text('Z')), - DataCell(Text(_sensorData!.z.toStringAsFixed(14))), - ], - ), - ], - ), - ], - )), - )); - } -} From de9788c307a76ccabd78ce6949db76bf84a37c9e Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Tue, 20 Feb 2024 19:09:36 +0100 Subject: [PATCH 081/104] fix renaming issues --- .../lib/apps/powernapper/home_screen.dart | 2 +- .../lib/apps/powernapper/interact.dart | 5 +--- .../lib/apps/powernapper/movementTracker.dart | 29 ++++++------------- .../apps/powernapper/sensor_datatypes.dart | 2 +- .../lib/apps/powernapper/timerscreen.dart | 4 +-- open_earable/lib/apps_tab.dart | 3 +- 6 files changed, 15 insertions(+), 30 deletions(-) diff --git a/open_earable/lib/apps/powernapper/home_screen.dart b/open_earable/lib/apps/powernapper/home_screen.dart index 3880565..e7f9bb8 100644 --- a/open_earable/lib/apps/powernapper/home_screen.dart +++ b/open_earable/lib/apps/powernapper/home_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:open_earable/apps/ufiiu/timerscreen.dart'; +import 'package:open_earable/apps/powernapper/timerscreen.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import 'interact.dart'; diff --git a/open_earable/lib/apps/powernapper/interact.dart b/open_earable/lib/apps/powernapper/interact.dart index 18f9515..cb3c528 100644 --- a/open_earable/lib/apps/powernapper/interact.dart +++ b/open_earable/lib/apps/powernapper/interact.dart @@ -3,19 +3,16 @@ import 'package:open_earable_flutter/src/open_earable_flutter.dart'; ///Interaction class for the earable. All actions executed on the earable are accessible through this class. ///For example rings or led colors. class Interact { - final OpenEarable _openEarable; //Constructor Interact(this._openEarable); - //Getter for the Earable OpenEarable getEarable() { return _openEarable; } - ///Lets the OpenEarable play the jingel-ID: '1'. void ring() { try { @@ -24,4 +21,4 @@ class Interact { print('ERROR: Jingle konnte nicht gespielt werden!'); } } -} \ No newline at end of file +} diff --git a/open_earable/lib/apps/powernapper/movementTracker.dart b/open_earable/lib/apps/powernapper/movementTracker.dart index 373f5d4..96103b8 100644 --- a/open_earable/lib/apps/powernapper/movementTracker.dart +++ b/open_earable/lib/apps/powernapper/movementTracker.dart @@ -1,12 +1,11 @@ import 'dart:async'; -import 'package:open_earable/apps/ufiiu/interact.dart'; -import 'package:open_earable/apps/ufiiu/sensor_datatypes.dart'; +import 'package:open_earable/apps/powernapper/interact.dart'; +import 'package:open_earable/apps/powernapper/sensor_datatypes.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; /// Movement Tracker has lgoic for timer & movement validation. class MovementTracker { - //Incetaction variables final Interact _interact; late final OpenEarable _openEarable; @@ -16,7 +15,6 @@ class MovementTracker { //Stream Subscription StreamSubscription>? _subscription; - //Constructor MovementTracker(this._interact) { this._openEarable = _interact.getEarable(); @@ -27,7 +25,6 @@ class MovementTracker { /// Input: [minutes] for the time before the ring. /// Input: [updateText] as an void callback function for the textupdate. void start(int minutes, void Function(SensorDataType s) updateText) { - //Timer (re-)start stop(); _startTimer(minutes); @@ -36,8 +33,8 @@ class MovementTracker { _openEarable.sensorManager.writeSensorConfig(_buildSensorConfig()); //Starts listening to the subscription - _subscription = _openEarable.sensorManager.subscribeToSensorData(0).listen((event) { - + _subscription = + _openEarable.sensorManager.subscribeToSensorData(0).listen((event) { //Display update callback updateText(Gyroscope(event)); @@ -68,7 +65,7 @@ class MovementTracker { /// /// Uses the [SensorDataType] to validate update and int [minutes] to restart the timer. void _update(SensorDataType dt, int minutes) { - if(_validMovement(dt)) { + if (_validMovement(dt)) { _timer?.cancel(); _startTimer(minutes); } @@ -78,17 +75,13 @@ class MovementTracker { /// /// Input: [SensorDataType] with the data to be validated. bool _validMovement(SensorDataType dt) { - Gyroscope gyro; - if(dt is Gyroscope) { + if (dt is Gyroscope) { gyro = dt; //Threshold validating for gyroscope data. - if(gyro.x.abs() > 5 - || gyro.y.abs() > 5 - || gyro.z.abs() > 5 - ) { + if (gyro.x.abs() > 5 || gyro.y.abs() > 5 || gyro.z.abs() > 5) { return true; } } @@ -97,10 +90,6 @@ class MovementTracker { ///Sensor Config for the earable. OpenEarableSensorConfig _buildSensorConfig() { - return OpenEarableSensorConfig( - sensorId: 0, - samplingRate: 30, - latency: 0 - ); + return OpenEarableSensorConfig(sensorId: 0, samplingRate: 30, latency: 0); } -} \ No newline at end of file +} diff --git a/open_earable/lib/apps/powernapper/sensor_datatypes.dart b/open_earable/lib/apps/powernapper/sensor_datatypes.dart index 8bff3da..53a9db1 100644 --- a/open_earable/lib/apps/powernapper/sensor_datatypes.dart +++ b/open_earable/lib/apps/powernapper/sensor_datatypes.dart @@ -34,4 +34,4 @@ class EulerAngles extends SensorDataType { /// Placeholder data without any information in case no sensor data is available. class NullData extends SensorDataType { NullData() : super({"X": 0.0, "Y": 0.0, "Z": 0.0}); -} \ No newline at end of file +} diff --git a/open_earable/lib/apps/powernapper/timerscreen.dart b/open_earable/lib/apps/powernapper/timerscreen.dart index bfb57dd..bf3efd1 100644 --- a/open_earable/lib/apps/powernapper/timerscreen.dart +++ b/open_earable/lib/apps/powernapper/timerscreen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:open_earable/apps/ufiiu/movementTracker.dart'; -import 'package:open_earable/apps/ufiiu/sensor_datatypes.dart'; +import 'package:open_earable/apps/powernapper/movementTracker.dart'; +import 'package:open_earable/apps/powernapper/sensor_datatypes.dart'; import 'package:open_earable/ble_controller.dart'; import 'package:open_earable/widgets/earable_not_connected_warning.dart'; import 'package:provider/provider.dart'; diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index f00733a..b541487 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -6,8 +6,7 @@ import 'package:open_earable/apps/recorder.dart'; import 'package:open_earable/apps/jump_height_test/jump_height_test.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import 'package:open_earable/apps/jump_rope_counter.dart'; - -import 'apps/ufiiu/home_screen.dart'; +import 'apps/powernapper/home_screen.dart'; class AppInfo { final IconData iconData; From 7d96c469733aa54fa610b93b43c34ee17a1a18be Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Tue, 20 Feb 2024 19:28:41 +0100 Subject: [PATCH 082/104] update colors in powernapper, reorder apps --- .../198155.png | Bin .../lib/apps/powernapper/home_screen.dart | 3 + .../lib/apps/powernapper/timerscreen.dart | 1 + open_earable/lib/apps_tab.dart | 58 +++++++++--------- open_earable/pubspec.lock | 16 ++--- 5 files changed, 42 insertions(+), 36 deletions(-) rename open_earable/assets/{powernapper => powernapping_timer}/198155.png (100%) diff --git a/open_earable/assets/powernapper/198155.png b/open_earable/assets/powernapping_timer/198155.png similarity index 100% rename from open_earable/assets/powernapper/198155.png rename to open_earable/assets/powernapping_timer/198155.png diff --git a/open_earable/lib/apps/powernapper/home_screen.dart b/open_earable/lib/apps/powernapper/home_screen.dart index e7f9bb8..f5f40c7 100644 --- a/open_earable/lib/apps/powernapper/home_screen.dart +++ b/open_earable/lib/apps/powernapper/home_screen.dart @@ -28,7 +28,9 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.background, title: Text('Powernapper Alarm Clock'), ), @@ -37,6 +39,7 @@ class _HomeScreenState extends State { //Bottom Navigation Bar bottomNavigationBar: BottomNavigationBar( + backgroundColor: Theme.of(context).colorScheme.background, currentIndex: _currentIndex, onTap: _onNavBarItemTapped, items: [ diff --git a/open_earable/lib/apps/powernapper/timerscreen.dart b/open_earable/lib/apps/powernapper/timerscreen.dart index bf3efd1..a6b4373 100644 --- a/open_earable/lib/apps/powernapper/timerscreen.dart +++ b/open_earable/lib/apps/powernapper/timerscreen.dart @@ -64,6 +64,7 @@ class TimerScreenState extends State { 'assets/powernapping_timer/198155.png', width: 150, height: 150, + color: Colors.grey, ), SizedBox(height: 20), diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 1d57072..a831eb5 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -33,9 +33,9 @@ class AppsTab extends StatelessWidget { List sampleApps(BuildContext context) { return [ AppInfo( - iconData: Icons.face_6, - title: "Posture Tracker", - description: "Get feedback on bad posture.", + iconData: Icons.fiber_smart_record, + title: "Recorder", + description: "Record data from OpenEarable.", onTap: () { Navigator.push( context, @@ -43,24 +43,27 @@ class AppsTab extends StatelessWidget { builder: (context) => Material( child: Theme( data: materialTheme, - child: PostureTrackerView( - EarableAttitudeTracker(_openEarable), - _openEarable))))); + child: Recorder(_openEarable))))); }), AppInfo( - iconData: Icons.face_5, - title: "Powernapper Alarm Clock", - description: "Powernapping timer!", + iconData: Icons.face_6, + title: "Posture Tracker", + description: "Get feedback on bad posture.", onTap: () { Navigator.push( context, MaterialPageRoute( - builder: (context) => SleepHomeScreen(_openEarable))); + builder: (context) => Material( + child: Theme( + data: materialTheme, + child: PostureTrackerView( + EarableAttitudeTracker(_openEarable), + _openEarable))))); }), AppInfo( - iconData: Icons.fiber_smart_record, - title: "Recorder", - description: "Record data from OpenEarable.", + iconData: Icons.height, + title: "Jump Height Test", + description: "Test your maximum jump height.", onTap: () { Navigator.push( context, @@ -68,12 +71,13 @@ class AppsTab extends StatelessWidget { builder: (context) => Material( child: Theme( data: materialTheme, - child: Recorder(_openEarable))))); + child: Material( + child: JumpHeightTest(_openEarable)))))); }), AppInfo( - iconData: Icons.music_note, - title: "Tightness Meter", - description: "Track your headbanging.", + iconData: Icons.keyboard_double_arrow_up, + title: "Jump Rope Counter", + description: "Counter for rope skipping.", onTap: () { Navigator.push( context, @@ -81,13 +85,12 @@ class AppsTab extends StatelessWidget { builder: (context) => Material( child: Theme( data: materialTheme, - child: TightnessMeter(_openEarable))))); + child: JumpRopeCounter(_openEarable))))); }), - AppInfo( - iconData: Icons.height, - title: "Jump Height Test", - description: "Test your maximum jump height.", + iconData: Icons.face_5, + title: "Powernapper Alarm Clock", + description: "Powernapping timer!", onTap: () { Navigator.push( context, @@ -95,13 +98,12 @@ class AppsTab extends StatelessWidget { builder: (context) => Material( child: Theme( data: materialTheme, - child: Material( - child: JumpHeightTest(_openEarable)))))); + child: SleepHomeScreen(_openEarable))))); }), AppInfo( - iconData: Icons.keyboard_double_arrow_up, - title: "Jump Rope Counter", - description: "Counter for rope skipping.", + iconData: Icons.music_note, + title: "Tightness Meter", + description: "Track your headbanging.", onTap: () { Navigator.push( context, @@ -109,7 +111,7 @@ class AppsTab extends StatelessWidget { builder: (context) => Material( child: Theme( data: materialTheme, - child: JumpRopeCounter(_openEarable))))); + child: TightnessMeter(_openEarable))))); }), // ... similarly for other apps ]; diff --git a/open_earable/pubspec.lock b/open_earable/pubspec.lock index 319104f..733c3fd 100644 --- a/open_earable/pubspec.lock +++ b/open_earable/pubspec.lock @@ -449,26 +449,26 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "3c84d49f0a5e1915364707159ab71f11b3b8a429532176d3a6248a45718ad4f9" + sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44" url: "https://pub.dev" source: hosted - version: "11.2.1" + version: "11.3.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: a5ebaa420cee8fd880ef10dedd42c6b3f493e7dbe27d7e0a7e1798669373082a + sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474" url: "https://pub.dev" source: hosted - version: "12.0.4" + version: "12.0.5" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "6ca25ee52518a8a26e80aaefe3c71caf6e2dfd809c1b20900d0882df6faed36e" + sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b url: "https://pub.dev" source: hosted - version: "9.3.1" + version: "9.4.0" permission_handler_html: dependency: transitive description: @@ -481,10 +481,10 @@ packages: dependency: transitive description: name: permission_handler_platform_interface - sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c" + sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.0" permission_handler_windows: dependency: transitive description: From 001cdc12d24726d96e664b27bedfe25210258778 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Tue, 20 Feb 2024 19:40:22 +0100 Subject: [PATCH 083/104] change order of apps and fix powernapper alarm clock image, adjust colors --- .../198155.png | Bin .../lib/apps/powernapper/home_screen.dart | 3 ++ .../lib/apps/powernapper/timerscreen.dart | 1 + open_earable/lib/apps_tab.dart | 51 +++++++++--------- 4 files changed, 29 insertions(+), 26 deletions(-) rename open_earable/assets/{powernapper => powernapping_timer}/198155.png (100%) diff --git a/open_earable/assets/powernapper/198155.png b/open_earable/assets/powernapping_timer/198155.png similarity index 100% rename from open_earable/assets/powernapper/198155.png rename to open_earable/assets/powernapping_timer/198155.png diff --git a/open_earable/lib/apps/powernapper/home_screen.dart b/open_earable/lib/apps/powernapper/home_screen.dart index e7f9bb8..f5f40c7 100644 --- a/open_earable/lib/apps/powernapper/home_screen.dart +++ b/open_earable/lib/apps/powernapper/home_screen.dart @@ -28,7 +28,9 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.background, title: Text('Powernapper Alarm Clock'), ), @@ -37,6 +39,7 @@ class _HomeScreenState extends State { //Bottom Navigation Bar bottomNavigationBar: BottomNavigationBar( + backgroundColor: Theme.of(context).colorScheme.background, currentIndex: _currentIndex, onTap: _onNavBarItemTapped, items: [ diff --git a/open_earable/lib/apps/powernapper/timerscreen.dart b/open_earable/lib/apps/powernapper/timerscreen.dart index bf3efd1..42d491b 100644 --- a/open_earable/lib/apps/powernapper/timerscreen.dart +++ b/open_earable/lib/apps/powernapper/timerscreen.dart @@ -64,6 +64,7 @@ class TimerScreenState extends State { 'assets/powernapping_timer/198155.png', width: 150, height: 150, + color: Colors.white, ), SizedBox(height: 20), diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index b541487..1982fd4 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -28,27 +28,6 @@ class AppsTab extends StatelessWidget { List sampleApps(BuildContext context) { return [ - AppInfo( - iconData: Icons.face_6, - title: "Posture Tracker", - description: "Get feedback on bad posture.", - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PostureTrackerView( - EarableAttitudeTracker(_openEarable), _openEarable))); - }), - AppInfo( - iconData: Icons.face_5, - title: "Powernapper Alarm Clock", - description: "Powernapping timer!", - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SleepHomeScreen(_openEarable))); - }), AppInfo( iconData: Icons.fiber_smart_record, title: "Recorder", @@ -60,16 +39,16 @@ class AppsTab extends StatelessWidget { builder: (context) => Recorder(_openEarable))); }), AppInfo( - iconData: Icons.music_note, - title: "Tightness Meter", - description: "Track your headbanging.", + iconData: Icons.face_6, + title: "Posture Tracker", + description: "Get feedback on bad posture.", onTap: () { Navigator.push( context, MaterialPageRoute( - builder: (context) => TightnessMeter(_openEarable))); + builder: (context) => PostureTrackerView( + EarableAttitudeTracker(_openEarable), _openEarable))); }), - AppInfo( iconData: Icons.height, title: "Jump Height Test", @@ -90,6 +69,26 @@ class AppsTab extends StatelessWidget { MaterialPageRoute( builder: (context) => JumpRopeCounter(_openEarable))); }), + AppInfo( + iconData: Icons.face_5, + title: "Powernapper Alarm Clock", + description: "Powernapping timer!", + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SleepHomeScreen(_openEarable))); + }), + AppInfo( + iconData: Icons.music_note, + title: "Tightness Meter", + description: "Track your headbanging.", + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TightnessMeter(_openEarable))); + }), // ... similarly for other apps ]; } From 1e9d41c0acedae8f92ddf100f834358558ec94e0 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Tue, 20 Feb 2024 19:42:43 +0100 Subject: [PATCH 084/104] adjust color of powernapper image --- open_earable/lib/apps/powernapper/timerscreen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/open_earable/lib/apps/powernapper/timerscreen.dart b/open_earable/lib/apps/powernapper/timerscreen.dart index 42d491b..a6b4373 100644 --- a/open_earable/lib/apps/powernapper/timerscreen.dart +++ b/open_earable/lib/apps/powernapper/timerscreen.dart @@ -64,7 +64,7 @@ class TimerScreenState extends State { 'assets/powernapping_timer/198155.png', width: 150, height: 150, - color: Colors.white, + color: Colors.grey, ), SizedBox(height: 20), From 1f6b48dfd9aaf17a85f4d8d4f34e2d4da1689e76 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Tue, 20 Feb 2024 19:46:05 +0100 Subject: [PATCH 085/104] remove duplicated apps caused by merge with main --- open_earable/lib/apps_tab.dart | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 007f89c..a831eb5 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -113,26 +113,6 @@ class AppsTab extends StatelessWidget { data: materialTheme, child: TightnessMeter(_openEarable))))); }), - AppInfo( - iconData: Icons.face_5, - title: "Powernapper Alarm Clock", - description: "Powernapping timer!", - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SleepHomeScreen(_openEarable))); - }), - AppInfo( - iconData: Icons.music_note, - title: "Tightness Meter", - description: "Track your headbanging.", - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TightnessMeter(_openEarable))); - }), // ... similarly for other apps ]; } From a2c9ef78e077cdc661826f4824465de82efb8f90 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Tue, 20 Feb 2024 19:50:32 +0100 Subject: [PATCH 086/104] add material theme to sensor data tab. The sensor data tab still has material design --- open_earable/lib/main.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/open_earable/lib/main.dart b/open_earable/lib/main.dart index 2c2da64..ef30d11 100644 --- a/open_earable/lib/main.dart +++ b/open_earable/lib/main.dart @@ -69,7 +69,9 @@ class _MyHomePageState extends State { _openEarable; _widgetOptions = [ ControlTab(_openEarable), - SensorDataTab(_openEarable), + Material( + child: + Theme(data: materialTheme, child: SensorDataTab(_openEarable))), AppsTab(_openEarable), ]; } From 220304175b177c4a325b11caa18c18efec015f48 Mon Sep 17 00:00:00 2001 From: Philipp Lepold Date: Tue, 20 Feb 2024 20:45:23 +0100 Subject: [PATCH 087/104] open earable flutter library depending on git again --- open_earable/pubspec.lock | 8 +++++--- open_earable/pubspec.yaml | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/open_earable/pubspec.lock b/open_earable/pubspec.lock index 733c3fd..bf8dcac 100644 --- a/open_earable/pubspec.lock +++ b/open_earable/pubspec.lock @@ -369,9 +369,11 @@ packages: open_earable_flutter: dependency: "direct main" description: - path: "../../open_earable_flutter" - relative: true - source: path + path: "." + ref: HEAD + resolved-ref: "35b7a1183efbaf55abe374e1da3a7c4509c385a5" + url: "https://github.com/OpenEarable/open_earable_flutter.git" + source: git version: "0.0.2" open_file: dependency: "direct main" diff --git a/open_earable/pubspec.yaml b/open_earable/pubspec.yaml index c821ec8..c19cdd2 100644 --- a/open_earable/pubspec.yaml +++ b/open_earable/pubspec.yaml @@ -33,7 +33,8 @@ dependencies: flutter_localizations: sdk: flutter open_earable_flutter: - path: ../../open_earable_flutter + git: + url: https://github.com/OpenEarable/open_earable_flutter.git permission_handler: ^11.1.0 flutter_reactive_ble: ^5.2.0 app_settings: ^5.1.1 From f316571106c586a4794e4e4618029b5fa26e7f9e Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Thu, 22 Feb 2024 13:03:39 +0100 Subject: [PATCH 088/104] add logos for recorder and posture tracker app --- .../posture_tracker/assets}/Head_Front.png | Bin .../posture_tracker/assets}/Head_Side.png | Bin .../posture_tracker/assets}/Neck_Front.png | Bin .../posture_tracker/assets}/Neck_Side.png | Bin .../lib/apps/posture_tracker/assets/logo.png | Bin 0 -> 34444 bytes .../view/posture_tracker_view.dart | 8 ++--- open_earable/lib/apps/recorder/assets/REC.png | Bin 0 -> 12438 bytes .../lib/apps/{ => recorder/lib}/recorder.dart | 0 open_earable/lib/apps_tab.dart | 30 ++++++++++++------ open_earable/pubspec.yaml | 3 +- 10 files changed, 26 insertions(+), 15 deletions(-) rename open_earable/{assets/posture_tracker => lib/apps/posture_tracker/assets}/Head_Front.png (100%) rename open_earable/{assets/posture_tracker => lib/apps/posture_tracker/assets}/Head_Side.png (100%) rename open_earable/{assets/posture_tracker => lib/apps/posture_tracker/assets}/Neck_Front.png (100%) rename open_earable/{assets/posture_tracker => lib/apps/posture_tracker/assets}/Neck_Side.png (100%) create mode 100644 open_earable/lib/apps/posture_tracker/assets/logo.png create mode 100644 open_earable/lib/apps/recorder/assets/REC.png rename open_earable/lib/apps/{ => recorder/lib}/recorder.dart (100%) diff --git a/open_earable/assets/posture_tracker/Head_Front.png b/open_earable/lib/apps/posture_tracker/assets/Head_Front.png similarity index 100% rename from open_earable/assets/posture_tracker/Head_Front.png rename to open_earable/lib/apps/posture_tracker/assets/Head_Front.png diff --git a/open_earable/assets/posture_tracker/Head_Side.png b/open_earable/lib/apps/posture_tracker/assets/Head_Side.png similarity index 100% rename from open_earable/assets/posture_tracker/Head_Side.png rename to open_earable/lib/apps/posture_tracker/assets/Head_Side.png diff --git a/open_earable/assets/posture_tracker/Neck_Front.png b/open_earable/lib/apps/posture_tracker/assets/Neck_Front.png similarity index 100% rename from open_earable/assets/posture_tracker/Neck_Front.png rename to open_earable/lib/apps/posture_tracker/assets/Neck_Front.png diff --git a/open_earable/assets/posture_tracker/Neck_Side.png b/open_earable/lib/apps/posture_tracker/assets/Neck_Side.png similarity index 100% rename from open_earable/assets/posture_tracker/Neck_Side.png rename to open_earable/lib/apps/posture_tracker/assets/Neck_Side.png diff --git a/open_earable/lib/apps/posture_tracker/assets/logo.png b/open_earable/lib/apps/posture_tracker/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bd88af7afc702beeacb1732a463847731d2fcc83 GIT binary patch literal 34444 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 literal 0 HcmV?d00001 diff --git a/open_earable/lib/apps/posture_tracker/view/posture_tracker_view.dart b/open_earable/lib/apps/posture_tracker/view/posture_tracker_view.dart index d66b0bc..088a2ff 100644 --- a/open_earable/lib/apps/posture_tracker/view/posture_tracker_view.dart +++ b/open_earable/lib/apps/posture_tracker/view/posture_tracker_view.dart @@ -74,15 +74,15 @@ class _PostureTrackerViewState extends State { List _createHeadViews(postureTrackerViewModel) { return [ this._buildHeadView( - "assets/posture_tracker/Head_Front.png", - "assets/posture_tracker/Neck_Front.png", + "lib/apps/posture_tracker/assets/Head_Front.png", + "lib/apps/posture_tracker/assets/Neck_Front.png", Alignment.center.add(Alignment(0, 0.3)), postureTrackerViewModel.attitude.roll, postureTrackerViewModel.badPostureSettings.rollAngleThreshold .toDouble()), this._buildHeadView( - "assets/posture_tracker/Head_Side.png", - "assets/posture_tracker/Neck_Side.png", + "lib/apps/posture_tracker/assets/Head_Side.png", + "lib/apps/posture_tracker/assets/Neck_Side.png", Alignment.center.add(Alignment(0, 0.3)), -postureTrackerViewModel.attitude.pitch, postureTrackerViewModel.badPostureSettings.pitchAngleThreshold diff --git a/open_earable/lib/apps/recorder/assets/REC.png b/open_earable/lib/apps/recorder/assets/REC.png new file mode 100644 index 0000000000000000000000000000000000000000..2e977f61812c54f83b03a7a938f5cc1dd30be6d7 GIT binary patch literal 12438 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

-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 LigM{i%aH#7*7P@( literal 0 HcmV?d00001 diff --git a/open_earable/lib/apps/recorder.dart b/open_earable/lib/apps/recorder/lib/recorder.dart similarity index 100% rename from open_earable/lib/apps/recorder.dart rename to open_earable/lib/apps/recorder/lib/recorder.dart diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index a831eb5..2348680 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; import 'package:open_earable/apps/tightness.dart'; -import 'package:open_earable/apps/recorder.dart'; +import 'package:open_earable/apps/recorder/lib/recorder.dart'; import 'package:open_earable/apps/jump_height_test/jump_height_test.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; import 'global_theme.dart'; @@ -13,13 +13,13 @@ import 'package:open_earable/apps/jump_rope_counter.dart'; import 'apps/powernapper/home_screen.dart'; class AppInfo { - final IconData iconData; + final String logoPath; final String title; final String description; final VoidCallback onTap; AppInfo( - {required this.iconData, + {required this.logoPath, required this.title, required this.description, required this.onTap}); @@ -33,7 +33,7 @@ class AppsTab extends StatelessWidget { List sampleApps(BuildContext context) { return [ AppInfo( - iconData: Icons.fiber_smart_record, + logoPath: "lib/apps/recorder/assets/REC.png", title: "Recorder", description: "Record data from OpenEarable.", onTap: () { @@ -46,7 +46,8 @@ class AppsTab extends StatelessWidget { child: Recorder(_openEarable))))); }), AppInfo( - iconData: Icons.face_6, + logoPath: + "lib/apps/posture_tracker/assets/logo.png", //iconData: Icons.face_6, title: "Posture Tracker", description: "Get feedback on bad posture.", onTap: () { @@ -61,7 +62,7 @@ class AppsTab extends StatelessWidget { _openEarable))))); }), AppInfo( - iconData: Icons.height, + logoPath: "lib/apps/recorder/assets/REC.png", //Icons.height, title: "Jump Height Test", description: "Test your maximum jump height.", onTap: () { @@ -75,7 +76,8 @@ class AppsTab extends StatelessWidget { child: JumpHeightTest(_openEarable)))))); }), AppInfo( - iconData: Icons.keyboard_double_arrow_up, + logoPath: + "lib/apps/recorder/assets/REC.png", //iconData: Icons.keyboard_double_arrow_up, title: "Jump Rope Counter", description: "Counter for rope skipping.", onTap: () { @@ -88,7 +90,8 @@ class AppsTab extends StatelessWidget { child: JumpRopeCounter(_openEarable))))); }), AppInfo( - iconData: Icons.face_5, + logoPath: + "lib/apps/recorder/assets/REC.png", //iconData: Icons.face_5, title: "Powernapper Alarm Clock", description: "Powernapping timer!", onTap: () { @@ -101,7 +104,8 @@ class AppsTab extends StatelessWidget { child: SleepHomeScreen(_openEarable))))); }), AppInfo( - iconData: Icons.music_note, + logoPath: + "lib/apps/recorder/assets/REC.png", //iconData: Icons.music_note, title: "Tightness Meter", description: "Track your headbanging.", onTap: () { @@ -135,7 +139,13 @@ class AppsTab extends StatelessWidget { child: ListTile( iconColor: Colors.white, textColor: Colors.white, - leading: Icon(apps[index].iconData, size: 40.0), + leading: SizedBox( + height: 50.0, + width: 50.0, + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image.asset(apps[index].logoPath, + fit: BoxFit.cover))), title: Text(apps[index].title), subtitle: Text(apps[index].description), trailing: Icon(Icons.arrow_forward_ios, diff --git a/open_earable/pubspec.yaml b/open_earable/pubspec.yaml index c19cdd2..f0fe2ac 100644 --- a/open_earable/pubspec.yaml +++ b/open_earable/pubspec.yaml @@ -130,8 +130,9 @@ flutter: # - images/a_dot_ham.jpeg assets: - assets/ - - assets/posture_tracker/ - assets/powernapping_timer/ + - lib/apps/recorder/assets/ + - lib/apps/posture_tracker/assets/ fonts: - family: OpenEarableIcon From 890b74c6c9050225d369aa0f31c7437533958869 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Thu, 22 Feb 2024 15:38:58 +0100 Subject: [PATCH 089/104] add jump height test logo --- .../lib/apps/jump_height_test/assets/logo.png | Bin 0 -> 60546 bytes .../apps/recorder/assets/{REC.png => logo.png} | Bin open_earable/lib/apps_tab.dart | 10 +++++----- open_earable/pubspec.yaml | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 open_earable/lib/apps/jump_height_test/assets/logo.png rename open_earable/lib/apps/recorder/assets/{REC.png => logo.png} (100%) diff --git a/open_earable/lib/apps/jump_height_test/assets/logo.png b/open_earable/lib/apps/jump_height_test/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b44a3b4c3133c1405fe9683cbeff858ca59d2cf3 GIT binary patch literal 60546 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

hMLDr5Db<@+E(9*38qQNaujYaMSD+zv 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 literal 0 HcmV?d00001 diff --git a/open_earable/lib/apps/recorder/assets/REC.png b/open_earable/lib/apps/recorder/assets/logo.png similarity index 100% rename from open_earable/lib/apps/recorder/assets/REC.png rename to open_earable/lib/apps/recorder/assets/logo.png diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 2348680..2bc5a1d 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -33,7 +33,7 @@ class AppsTab extends StatelessWidget { List sampleApps(BuildContext context) { return [ AppInfo( - logoPath: "lib/apps/recorder/assets/REC.png", + logoPath: "lib/apps/recorder/assets/logo.png", title: "Recorder", description: "Record data from OpenEarable.", onTap: () { @@ -62,7 +62,7 @@ class AppsTab extends StatelessWidget { _openEarable))))); }), AppInfo( - logoPath: "lib/apps/recorder/assets/REC.png", //Icons.height, + logoPath: "lib/apps/jump_height_test/assets/logo.png", //Icons.height, title: "Jump Height Test", description: "Test your maximum jump height.", onTap: () { @@ -77,7 +77,7 @@ class AppsTab extends StatelessWidget { }), AppInfo( logoPath: - "lib/apps/recorder/assets/REC.png", //iconData: Icons.keyboard_double_arrow_up, + "lib/apps/recorder/assets/logo.png", //iconData: Icons.keyboard_double_arrow_up, title: "Jump Rope Counter", description: "Counter for rope skipping.", onTap: () { @@ -91,7 +91,7 @@ class AppsTab extends StatelessWidget { }), AppInfo( logoPath: - "lib/apps/recorder/assets/REC.png", //iconData: Icons.face_5, + "lib/apps/recorder/assets/logo.png", //iconData: Icons.face_5, title: "Powernapper Alarm Clock", description: "Powernapping timer!", onTap: () { @@ -105,7 +105,7 @@ class AppsTab extends StatelessWidget { }), AppInfo( logoPath: - "lib/apps/recorder/assets/REC.png", //iconData: Icons.music_note, + "lib/apps/recorder/assets/logo.png", //iconData: Icons.music_note, title: "Tightness Meter", description: "Track your headbanging.", onTap: () { diff --git a/open_earable/pubspec.yaml b/open_earable/pubspec.yaml index f0fe2ac..6b4c51e 100644 --- a/open_earable/pubspec.yaml +++ b/open_earable/pubspec.yaml @@ -133,6 +133,7 @@ flutter: - assets/powernapping_timer/ - lib/apps/recorder/assets/ - lib/apps/posture_tracker/assets/ + - lib/apps/jump_height_test/assets/ fonts: - family: OpenEarableIcon From 5e96530147264c2e4dc1a2ad23c09077590c2b70 Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Fri, 23 Feb 2024 12:26:49 +0100 Subject: [PATCH 090/104] add logo for tightness meter --- open_earable/lib/apps/tightness/assets/logo.png | Bin 0 -> 54522 bytes .../lib/apps/{ => tightness}/tightness.dart | 0 open_earable/lib/apps_tab.dart | 4 ++-- open_earable/pubspec.yaml | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 open_earable/lib/apps/tightness/assets/logo.png rename open_earable/lib/apps/{ => tightness}/tightness.dart (100%) diff --git a/open_earable/lib/apps/tightness/assets/logo.png b/open_earable/lib/apps/tightness/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ac475369e7ed63a905171db05e9a1c79aa209cbe GIT binary patch literal 54522 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 z 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 G`TqetE!&C! literal 0 HcmV?d00001 diff --git a/open_earable/lib/apps/tightness.dart b/open_earable/lib/apps/tightness/tightness.dart similarity index 100% rename from open_earable/lib/apps/tightness.dart rename to open_earable/lib/apps/tightness/tightness.dart diff --git a/open_earable/lib/apps_tab.dart b/open_earable/lib/apps_tab.dart index 2bc5a1d..003cc85 100644 --- a/open_earable/lib/apps_tab.dart +++ b/open_earable/lib/apps_tab.dart @@ -4,7 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:open_earable/apps/posture_tracker/model/earable_attitude_tracker.dart'; import 'package:open_earable/apps/posture_tracker/view/posture_tracker_view.dart'; -import 'package:open_earable/apps/tightness.dart'; +import 'package:open_earable/apps/tightness/tightness.dart'; import 'package:open_earable/apps/recorder/lib/recorder.dart'; import 'package:open_earable/apps/jump_height_test/jump_height_test.dart'; import 'package:open_earable_flutter/src/open_earable_flutter.dart'; @@ -105,7 +105,7 @@ class AppsTab extends StatelessWidget { }), AppInfo( logoPath: - "lib/apps/recorder/assets/logo.png", //iconData: Icons.music_note, + "lib/apps/tightness/assets/logo.png", //iconData: Icons.music_note, title: "Tightness Meter", description: "Track your headbanging.", onTap: () { diff --git a/open_earable/pubspec.yaml b/open_earable/pubspec.yaml index 6b4c51e..9cf8141 100644 --- a/open_earable/pubspec.yaml +++ b/open_earable/pubspec.yaml @@ -134,6 +134,7 @@ flutter: - lib/apps/recorder/assets/ - lib/apps/posture_tracker/assets/ - lib/apps/jump_height_test/assets/ + - lib/apps/tightness/assets/ fonts: - family: OpenEarableIcon From 5b821addea486833537dc360bd4b266531631a5c Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Fri, 23 Feb 2024 17:46:42 +0100 Subject: [PATCH 091/104] replace logo of jump height test --- .../lib/apps/jump_height_test/assets/logo.png | Bin 60546 -> 51316 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/open_earable/lib/apps/jump_height_test/assets/logo.png b/open_earable/lib/apps/jump_height_test/assets/logo.png index b44a3b4c3133c1405fe9683cbeff858ca59d2cf3..ac70a5b44b707b0c1b187176893fe270d83c2afb 100644 GIT binary patch delta 50469 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

hMLDr5Db<@+E(9*38qQNaujYaMSD+zv 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 From 0619fe86f78286fb490cdf3adebd3502e0cb949e Mon Sep 17 00:00:00 2001 From: Oliver Bagge Date: Sat, 24 Feb 2024 12:17:54 +0100 Subject: [PATCH 092/104] add powernapping and jump rope logo. Change some app descriptions --- .../apps/jump_rope_counter/assets/logo.png | Bin 0 -> 280820 bytes .../jump_rope_counter.dart | 1 - .../lib/apps/powernapper/assets/logo.png | Bin 0 -> 360464 bytes .../lib/apps/tightness/assets/logo.png | Bin 54522 -> 50635 bytes open_earable/lib/apps_tab.dart | 20 +++++++++--------- open_earable/pubspec.yaml | 3 ++- 6 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 open_earable/lib/apps/jump_rope_counter/assets/logo.png rename open_earable/lib/apps/{ => jump_rope_counter}/jump_rope_counter.dart (99%) create mode 100644 open_earable/lib/apps/powernapper/assets/logo.png diff --git a/open_earable/lib/apps/jump_rope_counter/assets/logo.png b/open_earable/lib/apps/jump_rope_counter/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..470f3e767b18f3474fb7d4220632127e57ab6960 GIT binary patch literal 280820 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�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