From 781598f4c0591c32a6233cba178ee469923fd883 Mon Sep 17 00:00:00 2001 From: chlohal Date: Wed, 7 May 2025 21:25:35 +0100 Subject: [PATCH 1/7] fix: Rework to be buildable --- lib/common/widgets/color_box.dart | 2 +- .../screens/list_filter_settings_screen.dart | 67 ------------------- pubspec.lock | 42 ++++++------ pubspec.yaml | 4 +- 4 files changed, 24 insertions(+), 91 deletions(-) delete mode 100644 lib/settings/screens/list_filter_settings_screen.dart diff --git a/lib/common/widgets/color_box.dart b/lib/common/widgets/color_box.dart index 6eae2f7a..82fe0c04 100644 --- a/lib/common/widgets/color_box.dart +++ b/lib/common/widgets/color_box.dart @@ -7,7 +7,7 @@ class ColorBox extends StatelessWidget { @override Widget build(BuildContext context) { - CardTheme cardTheme = Theme.of(context).cardTheme; + CardThemeData cardTheme = Theme.of(context).cardTheme; return Container( width: 36.0, diff --git a/lib/settings/screens/list_filter_settings_screen.dart b/lib/settings/screens/list_filter_settings_screen.dart deleted file mode 100644 index 2d8cfb58..00000000 --- a/lib/settings/screens/list_filter_settings_screen.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:clock_app/common/types/tag.dart'; -import 'package:clock_app/common/widgets/fab.dart'; -import 'package:clock_app/common/widgets/fields/input_bottom_sheet.dart'; -import 'package:clock_app/common/widgets/list/persistent_list_view.dart'; -import 'package:clock_app/navigation/widgets/app_top_bar.dart'; -import 'package:clock_app/settings/widgets/tag_card.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ListFilterSettingsScreen extends StatefulWidget { - const ListFilterSettingsScreen({ - super.key, - }); - - @override - State createState() => - _ListFilterSettingsScreenState(); -} - -class _ListFilterSettingsScreenState extends State { - final _listController = PersistentListController(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppTopBar(title: AppLocalizations.of(context)!.tagsSetting), - body: Stack( - children: [ - Column( - children: [ - Expanded( - child: PersistentListView( - saveTag: 'tags', - listController: _listController, - itemBuilder: (tag) => TagCard( - key: ValueKey(tag), - tag: tag, - onPressDelete: () => _listController.deleteItem(tag), - onPressDuplicate: () => _listController.duplicateItem(tag), - ), - onTapItem: (tag, index) async { - Tag? newTag = await showTagEditor(tag); - if (newTag == null) return; - tag.copyFrom(newTag); - _listController.changeItems((tags) {}); - }, - // onDeleteItem: _handleDeleteTimer, - placeholderText: "No tags created", - reloadOnPop: true, - isSelectable: true, - ), - ), - ], - ), - FAB( - bottomPadding: 8, - onPressed: () async { - Tag? tag = await showTagEditor(); - if (tag == null) return; - _listController.addItem(tag); - }, - ) - ], - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index a3c9c2ad..f9876ee8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -166,10 +166,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -474,10 +474,10 @@ packages: dependency: "direct main" description: name: flutter_slidable - sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" + sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" flutter_system_ringtones: dependency: "direct main" description: @@ -621,18 +621,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -693,18 +693,18 @@ packages: dependency: "direct main" description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: "direct main" description: @@ -1029,7 +1029,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -1058,10 +1058,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -1074,10 +1074,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" synchronized: dependency: transitive description: @@ -1106,10 +1106,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.3" timer_builder: dependency: "direct main" description: @@ -1226,10 +1226,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.3.0" watcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d9a8519e..8b396f8d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: path_provider: ^2.0.11 path: ^1.8.2 sqflite: ^2.2.2 - flutter_slidable: ^3.1.0 + flutter_slidable: ^3.1.2 flutter_system_ringtones: ^0.0.6 # android_alarm_manager_plus: ^4.0.4 android_alarm_manager_plus: @@ -63,7 +63,7 @@ dependencies: receive_intent: ^0.2.5 watcher: ^1.1.0 dynamic_color: ^1.7.0 - material_color_utilities: ^0.8.0 + material_color_utilities: ^0.11.1 flutter_oss_licenses: ^3.0.2 locale_names: ^1.1.1 # home_widget: ^0.5.0 From 31ef83a5f3dc0ad36ba09f9180413b910bd8b7b1 Mon Sep 17 00:00:00 2001 From: chlohal Date: Thu, 8 May 2025 13:16:23 +0100 Subject: [PATCH 2/7] Add numpad-based number entry field --- lib/alarm/data/alarm_task_schemas.dart | 1 + lib/alarm/types/alarm_task.dart | 2 +- .../widgets/fields/number_picker_field.dart | 78 ++++++ lib/common/widgets/fields/numpad_input.dart | 259 ++++++++++++++++++ lib/settings/logic/get_setting_widget.dart | 7 + lib/settings/widgets/number_setting_card.dart | 40 +++ 6 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 lib/common/widgets/fields/number_picker_field.dart create mode 100644 lib/common/widgets/fields/numpad_input.dart create mode 100644 lib/settings/widgets/number_setting_card.dart diff --git a/lib/alarm/data/alarm_task_schemas.dart b/lib/alarm/data/alarm_task_schemas.dart index 49f9a72a..69cda016 100644 --- a/lib/alarm/data/alarm_task_schemas.dart +++ b/lib/alarm/data/alarm_task_schemas.dart @@ -3,6 +3,7 @@ import 'package:clock_app/alarm/widgets/tasks/math_task.dart'; import 'package:clock_app/alarm/widgets/tasks/memory_task.dart'; import 'package:clock_app/alarm/widgets/tasks/retype_task.dart'; import 'package:clock_app/alarm/widgets/tasks/sequence_task.dart'; +import 'package:clock_app/alarm/widgets/tasks/squat_task.dart'; import 'package:clock_app/settings/types/setting.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/alarm/types/alarm_task.dart b/lib/alarm/types/alarm_task.dart index 24daea1f..cf6954a1 100644 --- a/lib/alarm/types/alarm_task.dart +++ b/lib/alarm/types/alarm_task.dart @@ -10,7 +10,7 @@ enum AlarmTaskType { retype, sequence, shake, - memory, + memory } typedef AlarmTaskBuilder = Widget Function( diff --git a/lib/common/widgets/fields/number_picker_field.dart b/lib/common/widgets/fields/number_picker_field.dart new file mode 100644 index 00000000..6799c9bb --- /dev/null +++ b/lib/common/widgets/fields/number_picker_field.dart @@ -0,0 +1,78 @@ +import 'package:clock_app/common/widgets/fields/numpad_input.dart'; +import 'package:flutter/material.dart'; + +class NumberPickerField extends StatefulWidget { + const NumberPickerField({ + Key? key, + required this.title, + this.description, + required this.onChange, + required this.value, + }) : super(key: key); + + final int value; + final String title; + final String? description; + final void Function(int) onChange; + + @override + State> createState() => _NumberPickerFieldState(); +} + +enum SelectType { color, text } + +class _NumberPickerFieldState extends State> { + @override + void initState() { + super.initState(); + // _currentSelectedIndex = widget.selectedIndex; + } + + @override + Widget build(BuildContext context) { + void showPicker() async { + int? newValue = (await showNumberPicker( + context, + initialNumber: widget.value + )); + if (newValue == null) return; + setState(() { + widget.onChange(newValue); + }); + } + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: showPicker, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 4.0), + Text( + widget.value.toString(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + const Spacer(), + Icon( + Icons.numbers_outlined, + color: + Theme.of(context).colorScheme.onBackground.withOpacity(0.6), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/common/widgets/fields/numpad_input.dart b/lib/common/widgets/fields/numpad_input.dart new file mode 100644 index 00000000..06e9b59a --- /dev/null +++ b/lib/common/widgets/fields/numpad_input.dart @@ -0,0 +1,259 @@ +import 'package:clock_app/common/widgets/modal.dart'; +import 'package:clock_app/theme/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; + +Future showNumberPicker( + BuildContext context, { + String? title, + int initialNumber = 0, +}) async { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; + + return showDialog( + context: context, + builder: (BuildContext context) { + int number = initialNumber; + + return StatefulBuilder( + builder: (context, StateSetter setState) { + return Modal( + onSave: () => Navigator.of(context).pop(number), + // title: "Choose Duration", + child: Builder( + builder: (context) { + // Get available height and width of the build area of this widget. Make a choice depending on the size. + Orientation orientation = MediaQuery.of(context).orientation; + + Widget label() => Text( + title ?? "", + style: textTheme.displayMedium, + ); + + Widget numpad() => NumpadInput( + title: "Select Number", + value: number, + onChange: (int newNumber) { + setState(() { + number = newNumber; + }); + }, + ); + + return orientation == Orientation.portrait + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + if (title != null) label(), + const SizedBox(height: 16), + numpad(), + ], + ) + : Row(children: [ + Expanded( + flex: 1, + child: Column( + // mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + if (title != null) label(), + ], + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 1, + child: Column( + // mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + numpad(), + ], + ), + ), + ]); + }, + ), + ); + }, + ); + }, + ); +} + +class NumpadInput extends StatefulWidget { + NumpadInput( + {super.key, + required this.title, + required this.value, + required this.onChange}); + + final String title; + final int value; + final void Function(int) onChange; + + @override + State createState() => _NumpadInputState(); +} + +class _NumpadInputState extends State { + bool isEmpty = false; + + @override + void initState() { + super.initState(); + } + + List getTimeInput() { + return widget.value.toString().split(""); + } + + void _addDigit(String digit) { + setState(() { + final timeInput = getTimeInput(); + + //remove the final 0 + if (isEmpty) { + timeInput.removeLast(); + isEmpty = false; + } + timeInput.add(digit); + + _update(timeInput); + }); + } + + void _removeDigit() { + setState(() { + final timeInput = getTimeInput(); + if (timeInput.isNotEmpty) { + timeInput.removeLast(); + } + isEmpty = timeInput.isEmpty; + if (isEmpty) { + timeInput.add("0"); + } + _update(timeInput); + }); + } + + void _update(List timeInput) { + widget.onChange(int.parse(timeInput.join(""))); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final textTheme = theme.textTheme; + final labelStyle = textTheme.headlineLarge + ?.copyWith(color: colorScheme.onSurface, height: 1); + + final grayedLabelStyle = + labelStyle?.copyWith(color: colorScheme.onSurface.withOpacity(0.5)); + + double originalWidth = MediaQuery.of(context).size.width; + + final value = NumberFormat.decimalPattern().format(widget.value); + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(value, style: isEmpty ? grayedLabelStyle : labelStyle), + ], + ), + const SizedBox(height: 4), + SizedBox( + width: originalWidth * 0.76, + height: originalWidth * 1.1, + child: GridView.builder( + padding: const EdgeInsets.symmetric(vertical: 12), + shrinkWrap: true, + itemCount: 12, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 6, + mainAxisSpacing: 6, + ), + itemBuilder: (context, index) { + if (index < 9) { + return TimerButton( + label: (index + 1).toString(), + onTap: () => _addDigit((index + 1).toString()), + ); + } else if (index == 9) { + return TimerButton(isSpacer: true, label: "", onTap: () {}); + } else if (index == 10) { + return TimerButton( + label: "0", + onTap: () => _addDigit("0"), + ); + } else { + return TimerButton( + isHighlighted: true, + icon: Icons.backspace_outlined, + onTap: _removeDigit, + ); + } + }, + ), + ), + ], + ); + } +} + +class TimerButton extends StatelessWidget { + final String? label; + final IconData? icon; + final VoidCallback onTap; + final bool isHighlighted; + final bool isSpacer; + + const TimerButton( + {super.key, + this.label, + required this.onTap, + this.icon, + this.isHighlighted = false, + this.isSpacer = false}); + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + TextTheme textTheme = theme.textTheme; + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(100), + child: Container( + decoration: BoxDecoration( + color: isSpacer + ? colorScheme.primary.withOpacity(0.0) + : isHighlighted + ? colorScheme.primary.withOpacity(0.2) + : colorScheme.onBackground.withOpacity(0.1), + borderRadius: BorderRadius.circular(100), + ), + child: Center( + child: label != null + ? Text( + label!, + style: textTheme.titleMedium + ?.copyWith(color: colorScheme.onSurface), + ) + : icon != null + ? Icon(icon, color: colorScheme.onSurface) + : Container()), + ), + ); + } +} diff --git a/lib/settings/logic/get_setting_widget.dart b/lib/settings/logic/get_setting_widget.dart index 71ac63fc..fe02f9d0 100644 --- a/lib/settings/logic/get_setting_widget.dart +++ b/lib/settings/logic/get_setting_widget.dart @@ -14,6 +14,7 @@ import 'package:clock_app/settings/widgets/dynamic_select_setting_card.dart'; import 'package:clock_app/settings/widgets/dynamic_toggle_setting_card.dart'; import 'package:clock_app/settings/widgets/list_setting_card.dart'; import 'package:clock_app/settings/widgets/multi_select_setting_card.dart'; +import 'package:clock_app/settings/widgets/number_setting_card.dart'; import 'package:clock_app/settings/widgets/select_setting_card.dart'; import 'package:clock_app/settings/widgets/setting_action_card.dart'; import 'package:clock_app/settings/widgets/setting_page_link_card.dart'; @@ -173,6 +174,12 @@ Widget? getSettingItemWidget( showAsCard: showAsCard, onChanged: onChanged, ); + } else if (item is NumberSetting) { + return NumberSettingCard( + setting: item, + showAsCard: showAsCard, + onChanged: onChanged, + ); } else if (item is ColorSetting) { return ColorSettingCard( setting: item, diff --git a/lib/settings/widgets/number_setting_card.dart b/lib/settings/widgets/number_setting_card.dart new file mode 100644 index 00000000..8d8b20d5 --- /dev/null +++ b/lib/settings/widgets/number_setting_card.dart @@ -0,0 +1,40 @@ +import 'package:clock_app/common/widgets/card_container.dart'; +import 'package:clock_app/common/widgets/fields/duration_picker_field.dart'; +import 'package:clock_app/common/widgets/fields/number_picker_field.dart'; +import 'package:clock_app/settings/types/setting.dart'; +import 'package:clock_app/timer/types/time_duration.dart'; +import 'package:flutter/material.dart'; + +class NumberSettingCard extends StatefulWidget { + const NumberSettingCard( + {super.key, + required this.setting, + this.showAsCard = false, + this.onChanged}); + + final NumberSetting setting; + final bool showAsCard; + final void Function(double)? onChanged; + + @override + State createState() => _NumberSettingCardState(); +} + +class _NumberSettingCardState extends State { + @override + Widget build(BuildContext context) { + NumberPickerField toggleCard = NumberPickerField( + title: widget.setting.displayName(context), + value: widget.setting.value.round(), + onChange: (value) { + setState(() { + widget.setting.setValue(context, value.toDouble()); + }); + + widget.onChanged?.call(widget.setting.value); + }, + ); + + return widget.showAsCard ? CardContainer(child: toggleCard) : toggleCard; + } +} From 47dda9a238e881c9828a0c10cde0451b4b8775f9 Mon Sep 17 00:00:00 2001 From: chlohal Date: Thu, 8 May 2025 13:41:04 +0100 Subject: [PATCH 3/7] Add framework for squat task by copying math/matching --- lib/alarm/data/alarm_task_schemas.dart | 10 ++ lib/alarm/types/alarm_task.dart | 3 +- lib/alarm/widgets/tasks/squat_task.dart | 140 ++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 lib/alarm/widgets/tasks/squat_task.dart diff --git a/lib/alarm/data/alarm_task_schemas.dart b/lib/alarm/data/alarm_task_schemas.dart index 69cda016..fa5ec7fb 100644 --- a/lib/alarm/data/alarm_task_schemas.dart +++ b/lib/alarm/data/alarm_task_schemas.dart @@ -116,4 +116,14 @@ Map alarmTaskSchemasMap = { return MemoryTask(onSolve: onSolve, settings: settings); }, ), + AlarmTaskType.squat: AlarmTaskSchema( + (context) => "Squats", + SettingGroup("squatSettings", + (context) => "Squats", [ + NumberSetting("numberOfSquats", (context) => "Number of Squats", 10), + ]), + (onSolve, settings) { + return SquatTask(onSolve: onSolve, settings: settings); + }, + ), }; diff --git a/lib/alarm/types/alarm_task.dart b/lib/alarm/types/alarm_task.dart index cf6954a1..a32a3c34 100644 --- a/lib/alarm/types/alarm_task.dart +++ b/lib/alarm/types/alarm_task.dart @@ -10,7 +10,8 @@ enum AlarmTaskType { retype, sequence, shake, - memory + memory, + squat } typedef AlarmTaskBuilder = Widget Function( diff --git a/lib/alarm/widgets/tasks/squat_task.dart b/lib/alarm/widgets/tasks/squat_task.dart new file mode 100644 index 00000000..655fd585 --- /dev/null +++ b/lib/alarm/widgets/tasks/squat_task.dart @@ -0,0 +1,140 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:clock_app/common/widgets/card_container.dart'; +import 'package:clock_app/settings/types/setting_group.dart'; +import 'package:flutter/material.dart'; + +class SquatTask extends StatefulWidget { + const SquatTask({ + super.key, + required this.onSolve, + required this.settings, + }); + + final VoidCallback onSolve; + final SettingGroup settings; + + @override + State createState() => _SquatTaskState(); +} + +class _SquatTaskState extends State with TickerProviderStateMixin { + late final int numberOfSquats = + widget.settings.getSetting("numberOfSquats").value.toInt(); + + late List _cards; + CardModel? _firstCard; + bool _isWaiting = false; + + @override + void initState() { + super.initState(); + _initializeCards(); + } + + + void _onCardTap(CardModel card) { + if (_isWaiting || card.isFlipped) return; + + setState(() { + card.isFlipped = true; + }); + + if (_firstCard == null) { + _firstCard = card; + } else { + if (_firstCard!.value == card.value) { + // Match found + _firstCard!.isCompleted = true; + card.isCompleted = true; + _firstCard = null; + + if (_cards.every((card) => card.isFlipped)) { + // All cards are flipped + Future.delayed(const Duration(seconds: 1), () { + widget.onSolve(); + }); + } + } else { + // No match, flip back after delay + _isWaiting = true; + Future.delayed(const Duration(seconds: 1), () { + setState(() { + card.isFlipped = false; + _firstCard!.isFlipped = false; + _firstCard = null; + _isWaiting = false; + }); + }); + } + } + } + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + TextTheme textTheme = theme.textTheme; + int gridSize = (sqrt(_cards.length)).floor(); + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Text( + "Match card pairs", + style: textTheme.headlineMedium, + ), + const SizedBox(height: 16.0), + SizedBox( + width: double.infinity, + // height: 512, + child: GridView.builder( + itemCount: _cards.length, + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridSize, + ), + itemBuilder: (context, index) { + CardModel card = _cards[index]; + return GestureDetector( + key: ValueKey(card), + onTap: () => _onCardTap(card), + child: FlipCard( + isFlipped: card.isFlipped, + front: CardContainer( + margin: const EdgeInsets.all(4.0), + color: colorScheme.primary, + child: Center( + child: Text( + '?', + style: textTheme.displayMedium?.copyWith( + color: colorScheme.onPrimary, + ), + ), + ), + ), + back: CardContainer( + margin: const EdgeInsets.all(4.0), + color: card.isCompleted ? Colors.green : Colors.orangeAccent, + child: Center( + child: Text( + '${card.value}', + style: textTheme.displayMedium?.copyWith( + color: Colors.white, + + ), + ), + ), + ), + ), + ); + }, + ), + ), + ], + ), + ); + } +} \ No newline at end of file From f61a39b351486ff56ec26081f2064b352130cfae Mon Sep 17 00:00:00 2001 From: chlohal Date: Thu, 8 May 2025 19:46:39 +0100 Subject: [PATCH 4/7] feat: Add squat task --- lib/alarm/widgets/tasks/squat_task.dart | 181 +++++++++++++----------- pubspec.lock | 16 +++ pubspec.yaml | 1 + 3 files changed, 114 insertions(+), 84 deletions(-) diff --git a/lib/alarm/widgets/tasks/squat_task.dart b/lib/alarm/widgets/tasks/squat_task.dart index 655fd585..a0617dfc 100644 --- a/lib/alarm/widgets/tasks/squat_task.dart +++ b/lib/alarm/widgets/tasks/squat_task.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math'; +import 'package:sensors_plus/sensors_plus.dart'; import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:flutter/material.dart'; @@ -22,53 +23,94 @@ class SquatTask extends StatefulWidget { class _SquatTaskState extends State with TickerProviderStateMixin { late final int numberOfSquats = widget.settings.getSetting("numberOfSquats").value.toInt(); + late final StreamSubscription _accelStream; + late final StreamSubscription _barStream; - late List _cards; - CardModel? _firstCard; - bool _isWaiting = false; + final List barometerSamples = []; + final List accelerometerSamples = []; + + late int squatsCompleted = 0; @override void initState() { super.initState(); - _initializeCards(); + _accelStream = accelerometerEventStream( + samplingPeriod: const Duration(milliseconds: 50)) + .listen((AccelerometerEvent event) { + accelerometerSamples.add(event); + _dropOldSensorEvents(); + _updateSquatSensorData(); + }); + _barStream = + barometerEventStream(samplingPeriod: const Duration(milliseconds: 50)) + .listen((BarometerEvent event) { + barometerSamples.add(event); + _dropOldSensorEvents(); + _updateSquatSensorData(); + }); } + double oldAccelSecond = 0.0; - void _onCardTap(CardModel card) { - if (_isWaiting || card.isFlipped) return; + void _updateSquatSensorData() { + double accelSecond = _lowPassAccel(); - setState(() { - card.isFlipped = true; - }); - - if (_firstCard == null) { - _firstCard = card; - } else { - if (_firstCard!.value == card.value) { - // Match found - _firstCard!.isCompleted = true; - card.isCompleted = true; - _firstCard = null; - - if (_cards.every((card) => card.isFlipped)) { - // All cards are flipped - Future.delayed(const Duration(seconds: 1), () { - widget.onSolve(); - }); - } + if (oldAccelSecond > 11.0 && accelSecond <= 11.0) { + final int squats = squatsCompleted + 1; + if (squats >= numberOfSquats) { + widget.onSolve(); } else { - // No match, flip back after delay - _isWaiting = true; - Future.delayed(const Duration(seconds: 1), () { - setState(() { - card.isFlipped = false; - _firstCard!.isFlipped = false; - _firstCard = null; - _isWaiting = false; - }); + setState(() { + squatsCompleted = squats; }); } } + + oldAccelSecond = accelSecond; + } + + double _lowPassAccel() { + //use an average to put a low pass filter on the last second's worth of data + double avg = 0.0; + double count = 0.0; + + DateTime oldest = + DateTime.now().subtract(const Duration(milliseconds: 500)); + + for (AccelerometerEvent accel in accelerometerSamples.reversed) { + count++; + avg += (accel.y - avg) / count; + + if (accel.timestamp.isBefore(oldest)) break; + } + + return avg; + } + + void _dropOldSensorEvents() { + DateTime oldestNonDropped = + DateTime.now().subtract(const Duration(milliseconds: 4000)); + + int accelerometerFirstValidIndex = accelerometerSamples + .indexWhere((samp) => samp.timestamp.isAfter(oldestNonDropped)); + if (accelerometerFirstValidIndex == -1) { + accelerometerFirstValidIndex = accelerometerSamples.length; + } + accelerometerSamples.removeRange(0, accelerometerFirstValidIndex); + + int barometerFirstValidIndex = barometerSamples + .indexWhere((samp) => samp.timestamp.isAfter(oldestNonDropped)); + if (barometerFirstValidIndex == -1) { + barometerFirstValidIndex = barometerSamples.length; + } + barometerSamples.removeRange(0, barometerFirstValidIndex); + } + + @override + void dispose() { + super.dispose(); + _accelStream.cancel(); + _barStream.cancel(); } @override @@ -76,65 +118,36 @@ class _SquatTaskState extends State with TickerProviderStateMixin { ThemeData theme = Theme.of(context); ColorScheme colorScheme = theme.colorScheme; TextTheme textTheme = theme.textTheme; - int gridSize = (sqrt(_cards.length)).floor(); return Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + textBaseline: TextBaseline.alphabetic, + crossAxisAlignment: CrossAxisAlignment.baseline, + children: [ + Text( + squatsCompleted.toString(), + style: textTheme.displayLarge, + ), + Text( + "/", + style: textTheme.displayMedium, + ), + Text( + numberOfSquats.toString(), + style: textTheme.displayMedium, + ), + ]), Text( - "Match card pairs", - style: textTheme.headlineMedium, + "Squats Completed", + style: textTheme.headlineLarge, ), const SizedBox(height: 16.0), - SizedBox( - width: double.infinity, - // height: 512, - child: GridView.builder( - itemCount: _cards.length, - shrinkWrap: true, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: gridSize, - ), - itemBuilder: (context, index) { - CardModel card = _cards[index]; - return GestureDetector( - key: ValueKey(card), - onTap: () => _onCardTap(card), - child: FlipCard( - isFlipped: card.isFlipped, - front: CardContainer( - margin: const EdgeInsets.all(4.0), - color: colorScheme.primary, - child: Center( - child: Text( - '?', - style: textTheme.displayMedium?.copyWith( - color: colorScheme.onPrimary, - ), - ), - ), - ), - back: CardContainer( - margin: const EdgeInsets.all(4.0), - color: card.isCompleted ? Colors.green : Colors.orangeAccent, - child: Center( - child: Text( - '${card.value}', - style: textTheme.displayMedium?.copyWith( - color: Colors.white, - - ), - ), - ), - ), - ), - ); - }, - ), - ), ], ), ); } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index f9876ee8..7d68ef1f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -961,6 +961,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + sensors_plus: + dependency: "direct main" + description: + name: sensors_plus + sha256: "905282c917c6bb731c242f928665c2ea15445aa491249dea9d98d7c79dc8fd39" + url: "https://pub.dev" + source: hosted + version: "6.1.1" + sensors_plus_platform_interface: + dependency: transitive + description: + name: sensors_plus_platform_interface + sha256: "58815d2f5e46c0c41c40fb39375d3f127306f7742efe3b891c0b1c87e2b5cd5d" + url: "https://pub.dev" + source: hosted + version: "2.0.1" shared_preferences: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8b396f8d..7f814733 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,7 @@ dependencies: mime: ^1.0.6 analog_clock: ^0.1.1 animated_analog_clock: ^0.1.0 + sensors_plus: ^6.1.1 # animated_reorderable_list: ^1.1.1 # animated_reorderable_list: # path: "../animated_reorderable_list" From f4da44b1188ac2b01d51daa073ad661fcec89545 Mon Sep 17 00:00:00 2001 From: chlohal Date: Fri, 9 May 2025 23:26:47 +0100 Subject: [PATCH 5/7] Add light sensor task using Android light sensor --- lib/alarm/data/alarm_task_schemas.dart | 11 ++ lib/alarm/types/alarm_task.dart | 3 +- lib/alarm/widgets/tasks/light_task.dart | 174 ++++++++++++++++++++++++ pubspec.lock | 8 ++ pubspec.yaml | 1 + 5 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 lib/alarm/widgets/tasks/light_task.dart diff --git a/lib/alarm/data/alarm_task_schemas.dart b/lib/alarm/data/alarm_task_schemas.dart index fa5ec7fb..df7fe54a 100644 --- a/lib/alarm/data/alarm_task_schemas.dart +++ b/lib/alarm/data/alarm_task_schemas.dart @@ -1,4 +1,5 @@ import 'package:clock_app/alarm/types/alarm_task.dart'; +import 'package:clock_app/alarm/widgets/tasks/light_task.dart'; import 'package:clock_app/alarm/widgets/tasks/math_task.dart'; import 'package:clock_app/alarm/widgets/tasks/memory_task.dart'; import 'package:clock_app/alarm/widgets/tasks/retype_task.dart'; @@ -126,4 +127,14 @@ Map alarmTaskSchemasMap = { return SquatTask(onSolve: onSolve, settings: settings); }, ), + AlarmTaskType.lightSensor: AlarmTaskSchema( + (context) => "Light Sensor", + SettingGroup("lightSensorSettings", + (context) => "Required Light Level (lux)", [ + SliderSetting("targetLux", (context) => "Required Light Level (lux)", 0, 200, 100), + ]), + (onSolve, settings) { + return LightTask(onSolve: onSolve, settings: settings); + }, + ), }; diff --git a/lib/alarm/types/alarm_task.dart b/lib/alarm/types/alarm_task.dart index a32a3c34..c90605c0 100644 --- a/lib/alarm/types/alarm_task.dart +++ b/lib/alarm/types/alarm_task.dart @@ -11,7 +11,8 @@ enum AlarmTaskType { sequence, shake, memory, - squat + squat, + lightSensor } typedef AlarmTaskBuilder = Widget Function( diff --git a/lib/alarm/widgets/tasks/light_task.dart b/lib/alarm/widgets/tasks/light_task.dart new file mode 100644 index 00000000..5268b2bf --- /dev/null +++ b/lib/alarm/widgets/tasks/light_task.dart @@ -0,0 +1,174 @@ +import 'dart:async'; + +import 'package:clock_app/common/widgets/linear_progress_bar.dart'; +import 'package:light_sensor/light_sensor.dart'; +import 'package:clock_app/common/widgets/card_container.dart'; +import 'package:clock_app/settings/types/setting_group.dart'; +import 'package:flutter/material.dart'; + +class LightTask extends StatefulWidget { + const LightTask({ + super.key, + required this.onSolve, + required this.settings, + }); + + final VoidCallback onSolve; + final SettingGroup settings; + + @override + State createState() => _LightTaskState(); +} + +class LightSample { + const LightSample({required this.timestamp, required this.value}); + + final DateTime timestamp; + final int value; +} + +class _LightTaskState extends State with TickerProviderStateMixin { + late final double targetLux = widget.settings.getSetting("targetLux").value; + late final StreamSubscription _luxStream; + final List lightSensorSamples = []; + + double lightValue = 0.0; + + @override + void initState() { + super.initState(); + + //Start a check for whether the sensor exists; + // if it doesn't, then immediately pass. + LightSensor.hasSensor().then((hasSensor) { + if(!hasSensor) { + widget.onSolve(); + } + }); + + _luxStream = LightSensor.luxStream().listen((int lux) { + lightSensorSamples + .add(LightSample(timestamp: DateTime.now(), value: lux)); + _dropOldSensorEvents(); + _updateLightSensorData(); + }); + } + + void _updateLightSensorData() { + double lightSecond = _lowPassLight(); + + if (lightSecond > targetLux) { + widget.onSolve(); + } else { + setState(() { + lightValue = lightSecond; + }); + } + } + + double _lowPassLight() { + //use an average to put a low pass filter on the last second's worth of data + double avg = 0.0; + double count = 0.0; + + DateTime oldest = + DateTime.now().subtract(const Duration(milliseconds: 500)); + + for (LightSample samp in lightSensorSamples.reversed) { + count++; + avg += (samp.value.toDouble() - avg) / count; + + if (samp.timestamp.isBefore(oldest)) break; + } + + return avg; + } + + void _dropOldSensorEvents() { + DateTime oldestNonDropped = + DateTime.now().subtract(const Duration(milliseconds: 4000)); + + int accelerometerFirstValidIndex = lightSensorSamples + .indexWhere((samp) => samp.timestamp.isAfter(oldestNonDropped)); + if (accelerometerFirstValidIndex == -1) { + accelerometerFirstValidIndex = lightSensorSamples.length; + } + lightSensorSamples.removeRange(0, accelerometerFirstValidIndex); + } + + @override + void dispose() { + super.dispose(); + _luxStream.cancel(); + } + + static String _roundLux(double lux) { + final rounded = (lux * 100).roundToDouble() / 100; + + if(rounded.truncateToDouble() == rounded) { + return rounded.toInt().toString(); + } else { + return rounded.toString(); + } + } + + @override + Widget build(BuildContext context) { + ThemeData theme = Theme.of(context); + ColorScheme colorScheme = theme.colorScheme; + TextTheme textTheme = theme.textTheme; + + double percentageThere = lightValue / targetLux; + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + textBaseline: TextBaseline.alphabetic, + crossAxisAlignment: CrossAxisAlignment.baseline, + children: [ + Text( + _roundLux(lightValue), + style: textTheme.displayLarge, + ), + Text( + " lx", + style: textTheme.displaySmall, + ), + ]), + LinearProgressBar( + backgroundColor: colorScheme.onSurface.withOpacity(0.25), + value: percentageThere, + color: colorScheme.secondary, + minHeight: 18, + semanticsLabel: "Percentage to Required lx", + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "0 lx", + style: textTheme.displaySmall, + ), + Text( + "${_roundLux(targetLux)} lx", + style: textTheme.displaySmall, + ), + ]), + const SizedBox(height: 16.0), + Text( + textAlign: TextAlign.center, + lightValue < 10 + ? "Turn on the lights!" + : lightValue < 50 + ? "Face phone screen to the light" + : "Move closer to light", + style: textTheme.headlineLarge, + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 7d68ef1f..0f4ab923 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -641,6 +641,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + light_sensor: + dependency: "direct main" + description: + name: light_sensor + sha256: "84cdab036e87f1e7310bc0f1c30bb3ce661b581c8695fb9d5a4af32c8b0c2c2b" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7f814733..9ce0b32c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,6 +89,7 @@ dependencies: analog_clock: ^0.1.1 animated_analog_clock: ^0.1.0 sensors_plus: ^6.1.1 + light_sensor: ^3.0.1 # animated_reorderable_list: ^1.1.1 # animated_reorderable_list: # path: "../animated_reorderable_list" From 8e149a48f11f642d45bf8dd5425d7e29eadca6a9 Mon Sep 17 00:00:00 2001 From: chlohal Date: Wed, 21 May 2025 05:56:01 +0100 Subject: [PATCH 6/7] Add checks for sleeping people attempting to cheat squat --- lib/alarm/widgets/tasks/squat_task.dart | 106 ++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/lib/alarm/widgets/tasks/squat_task.dart b/lib/alarm/widgets/tasks/squat_task.dart index a0617dfc..56aecc4e 100644 --- a/lib/alarm/widgets/tasks/squat_task.dart +++ b/lib/alarm/widgets/tasks/squat_task.dart @@ -51,18 +51,101 @@ class _SquatTaskState extends State with TickerProviderStateMixin { } double oldAccelSecond = 0.0; + DateTime? lastSquatTime; + String falseSquatAdmonish = ""; + + bool _isFakeSquat(DateTime start, DateTime end) { + double average_accel_z = 0.0; + double average_accel_x = 0.0; + double count = 0.0; + + double avg_jerk_y = 0.0; + + double squatTimeSeconds = + end.difference(start).inMilliseconds.toDouble() / 1000.0; + + double barMin = double.infinity; + double barMax = double.negativeInfinity; + + for (BarometerEvent bar in barometerSamples.reversed) { + if (bar.timestamp.isAfter(end)) continue; + if (bar.timestamp.isBefore(start)) break; + + barMin = min(bar.pressure, barMin); + barMax = max(bar.pressure, barMax); + } + + final double barRange = barMax - barMin; + + AccelerometerEvent nextEvent = accelerometerSamples.last; + for (AccelerometerEvent accel in accelerometerSamples.reversed) { + if (accel.timestamp.isAfter(end)) continue; + if (accel.timestamp.isBefore(start)) break; + + count++; + average_accel_z += (accel.z - average_accel_z) / count; + average_accel_x += (accel.x - average_accel_x) / count; + + final Duration timeDifference = + nextEvent.timestamp.difference(accel.timestamp); + final double dtSeconds = + (timeDifference.inMilliseconds.toDouble() / 1000.0); + + final jerk_y = (nextEvent.y - accel.y) / dtSeconds; + avg_jerk_y += (jerk_y - avg_jerk_y) / count; + + nextEvent = accel; + } + + if(barRange <= 0.05) { + setState(() { + falseSquatAdmonish = "Make sure to have a full range of motion!"; + }); + return true; + } + + if (squatTimeSeconds <= 1.0) { + setState(() { + falseSquatAdmonish = "Move slower!"; + }); + return true; + } + + if (average_accel_z.abs() >= 2 || average_accel_x.abs() >= 2) { + setState(() { + falseSquatAdmonish = "Keep your phone steady and vertical!"; + }); + return true; + } + + if (avg_jerk_y.abs() >= 0.1) { + setState(() { + falseSquatAdmonish = "Be steady with your movement!"; + }); + return true; + } + + setState(() { + falseSquatAdmonish = ""; + }); + return false; + } void _updateSquatSensorData() { double accelSecond = _lowPassAccel(); - if (oldAccelSecond > 11.0 && accelSecond <= 11.0) { - final int squats = squatsCompleted + 1; - if (squats >= numberOfSquats) { - widget.onSolve(); - } else { - setState(() { - squatsCompleted = squats; - }); + if (oldAccelSecond > 10.5 && accelSecond <= 10.5) { + if (lastSquatTime == null || + !_isFakeSquat(lastSquatTime!, DateTime.now())) { + final int squats = squatsCompleted + 1; + lastSquatTime = DateTime.now(); + if (squats >= numberOfSquats) { + widget.onSolve(); + } else { + setState(() { + squatsCompleted = squats; + }); + } } } @@ -75,7 +158,7 @@ class _SquatTaskState extends State with TickerProviderStateMixin { double count = 0.0; DateTime oldest = - DateTime.now().subtract(const Duration(milliseconds: 500)); + DateTime.now().subtract(const Duration(milliseconds: 250)); for (AccelerometerEvent accel in accelerometerSamples.reversed) { count++; @@ -146,6 +229,11 @@ class _SquatTaskState extends State with TickerProviderStateMixin { style: textTheme.headlineLarge, ), const SizedBox(height: 16.0), + Text( + falseSquatAdmonish, + textAlign: TextAlign.center, + style: textTheme.headlineLarge?.copyWith(color: colorScheme.error), + ), ], ), ); From 14b588235b6d43a11bfc934948f809f4a37c00e9 Mon Sep 17 00:00:00 2001 From: chlohal Date: Wed, 21 May 2025 07:00:23 +0100 Subject: [PATCH 7/7] Add localization strings --- lib/alarm/data/alarm_task_schemas.dart | 12 ++--- lib/alarm/widgets/tasks/light_task.dart | 16 +++---- lib/alarm/widgets/tasks/squat_task.dart | 58 +++++++++++++++++-------- lib/l10n/app_en.arb | 16 ++++++- 4 files changed, 69 insertions(+), 33 deletions(-) diff --git a/lib/alarm/data/alarm_task_schemas.dart b/lib/alarm/data/alarm_task_schemas.dart index df7fe54a..b3f15fd7 100644 --- a/lib/alarm/data/alarm_task_schemas.dart +++ b/lib/alarm/data/alarm_task_schemas.dart @@ -118,20 +118,20 @@ Map alarmTaskSchemasMap = { }, ), AlarmTaskType.squat: AlarmTaskSchema( - (context) => "Squats", + (context) => AppLocalizations.of(context)!.squatTask, SettingGroup("squatSettings", - (context) => "Squats", [ - NumberSetting("numberOfSquats", (context) => "Number of Squats", 10), + (context) => AppLocalizations.of(context)!.squatTask, [ + NumberSetting("numberOfSquats", (context) => AppLocalizations.of(context)!.numberOfSquatsSetting, 10), ]), (onSolve, settings) { return SquatTask(onSolve: onSolve, settings: settings); }, ), AlarmTaskType.lightSensor: AlarmTaskSchema( - (context) => "Light Sensor", + (context) => AppLocalizations.of(context)!.lightTask, SettingGroup("lightSensorSettings", - (context) => "Required Light Level (lux)", [ - SliderSetting("targetLux", (context) => "Required Light Level (lux)", 0, 200, 100), + (context) => AppLocalizations.of(context)!.requiredLightLevelSetting, [ + SliderSetting("targetLux", (context) => AppLocalizations.of(context)!.requiredLightLevelSetting, 0, 200, 100), ]), (onSolve, settings) { return LightTask(onSolve: onSolve, settings: settings); diff --git a/lib/alarm/widgets/tasks/light_task.dart b/lib/alarm/widgets/tasks/light_task.dart index 5268b2bf..abad2c8f 100644 --- a/lib/alarm/widgets/tasks/light_task.dart +++ b/lib/alarm/widgets/tasks/light_task.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:clock_app/common/widgets/linear_progress_bar.dart'; import 'package:light_sensor/light_sensor.dart'; -import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class LightTask extends StatefulWidget { const LightTask({ @@ -134,7 +134,7 @@ class _LightTaskState extends State with TickerProviderStateMixin { style: textTheme.displayLarge, ), Text( - " lx", + AppLocalizations.of(context)!.luxUnitSuffix, style: textTheme.displaySmall, ), ]), @@ -143,17 +143,17 @@ class _LightTaskState extends State with TickerProviderStateMixin { value: percentageThere, color: colorScheme.secondary, minHeight: 18, - semanticsLabel: "Percentage to Required lx", + semanticsLabel: AppLocalizations.of(context)!.lightProgressSemanticsLabel, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "0 lx", + _roundLux(0.0) + AppLocalizations.of(context)!.luxUnitSuffix, style: textTheme.displaySmall, ), Text( - "${_roundLux(targetLux)} lx", + _roundLux(targetLux) + AppLocalizations.of(context)!.luxUnitSuffix, style: textTheme.displaySmall, ), ]), @@ -161,10 +161,10 @@ class _LightTaskState extends State with TickerProviderStateMixin { Text( textAlign: TextAlign.center, lightValue < 10 - ? "Turn on the lights!" + ? AppLocalizations.of(context)!.lightNoticeTurnOnLights : lightValue < 50 - ? "Face phone screen to the light" - : "Move closer to light", + ? AppLocalizations.of(context)!.lightNoticeFaceScreen + : AppLocalizations.of(context)!.lightNoticeMoveCloser, style: textTheme.headlineLarge, ), ], diff --git a/lib/alarm/widgets/tasks/squat_task.dart b/lib/alarm/widgets/tasks/squat_task.dart index 56aecc4e..73d615c4 100644 --- a/lib/alarm/widgets/tasks/squat_task.dart +++ b/lib/alarm/widgets/tasks/squat_task.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'dart:math'; import 'package:sensors_plus/sensors_plus.dart'; -import 'package:clock_app/common/widgets/card_container.dart'; import 'package:clock_app/settings/types/setting_group.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class SquatTask extends StatefulWidget { const SquatTask({ @@ -20,6 +20,13 @@ class SquatTask extends StatefulWidget { State createState() => _SquatTaskState(); } +enum SquatAdmonishment { + fullRangeMotion, + squatTime, + keepPhoneVertical, + keepMovementSteady +} + class _SquatTaskState extends State with TickerProviderStateMixin { late final int numberOfSquats = widget.settings.getSetting("numberOfSquats").value.toInt(); @@ -52,14 +59,14 @@ class _SquatTaskState extends State with TickerProviderStateMixin { double oldAccelSecond = 0.0; DateTime? lastSquatTime; - String falseSquatAdmonish = ""; + SquatAdmonishment? falseSquatAdmonish; bool _isFakeSquat(DateTime start, DateTime end) { - double average_accel_z = 0.0; - double average_accel_x = 0.0; + double averageAccelZ = 0.0; + double averageAccelX = 0.0; double count = 0.0; - double avg_jerk_y = 0.0; + double avgJerkY = 0.0; double squatTimeSeconds = end.difference(start).inMilliseconds.toDouble() / 1000.0; @@ -83,50 +90,50 @@ class _SquatTaskState extends State with TickerProviderStateMixin { if (accel.timestamp.isBefore(start)) break; count++; - average_accel_z += (accel.z - average_accel_z) / count; - average_accel_x += (accel.x - average_accel_x) / count; + averageAccelZ += (accel.z - averageAccelZ) / count; + averageAccelX += (accel.x - averageAccelX) / count; final Duration timeDifference = nextEvent.timestamp.difference(accel.timestamp); final double dtSeconds = (timeDifference.inMilliseconds.toDouble() / 1000.0); - final jerk_y = (nextEvent.y - accel.y) / dtSeconds; - avg_jerk_y += (jerk_y - avg_jerk_y) / count; + final jerkY = (nextEvent.y - accel.y) / dtSeconds; + avgJerkY += (jerkY - avgJerkY) / count; nextEvent = accel; } if(barRange <= 0.05) { setState(() { - falseSquatAdmonish = "Make sure to have a full range of motion!"; + falseSquatAdmonish = SquatAdmonishment.fullRangeMotion; }); return true; } if (squatTimeSeconds <= 1.0) { setState(() { - falseSquatAdmonish = "Move slower!"; + falseSquatAdmonish = SquatAdmonishment.squatTime; }); return true; } - if (average_accel_z.abs() >= 2 || average_accel_x.abs() >= 2) { + if (averageAccelZ.abs() >= 2 || averageAccelX.abs() >= 2) { setState(() { - falseSquatAdmonish = "Keep your phone steady and vertical!"; + falseSquatAdmonish = SquatAdmonishment.keepPhoneVertical; }); return true; } - if (avg_jerk_y.abs() >= 0.1) { + if (avgJerkY.abs() >= 0.1) { setState(() { - falseSquatAdmonish = "Be steady with your movement!"; + falseSquatAdmonish = SquatAdmonishment.keepMovementSteady; }); return true; } setState(() { - falseSquatAdmonish = ""; + falseSquatAdmonish = null; }); return false; } @@ -196,6 +203,21 @@ class _SquatTaskState extends State with TickerProviderStateMixin { _barStream.cancel(); } + String _getAdmonishText(SquatAdmonishment? admonishment, BuildContext context) { + switch(admonishment) { + case null: + return ""; + case SquatAdmonishment.fullRangeMotion: + return AppLocalizations.of(context)!.squatAdmonishmentFullRangeMotion; + case SquatAdmonishment.keepMovementSteady: + return AppLocalizations.of(context)!.squatAdmonishmentKeepMovementSteady; + case SquatAdmonishment.keepPhoneVertical: + return AppLocalizations.of(context)!.squatAdmonishmentKeepPhoneVertical; + case SquatAdmonishment.squatTime: + return AppLocalizations.of(context)!.squatAdmonishmentSquatTime; + } + } + @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); @@ -225,12 +247,12 @@ class _SquatTaskState extends State with TickerProviderStateMixin { ), ]), Text( - "Squats Completed", + AppLocalizations.of(context)!.squatsCompleted, style: textTheme.headlineLarge, ), const SizedBox(height: 16.0), Text( - falseSquatAdmonish, + _getAdmonishText(falseSquatAdmonish, context), textAlign: TextAlign.center, style: textTheme.headlineLarge?.copyWith(color: colorScheme.error), ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4fd37495..7bcad143 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -778,5 +778,19 @@ "backgroundServiceIntervalSettingDescription": "Lower interval will help keep the app alive, at the cost of some battery life", "custom": "Custom", "app": "App", - "materialYou": "Material You" + "materialYou": "Material You", + "squatTask": "Squats", + "numberOfSquatsSetting": "Number of Squats", + "squatsCompleted": "Squats Completed", + "squatAdmonishmentKeepMovementSteady": "Be steady with your movement!", + "squatAdmonishmentKeepPhoneVertical": "Keep your phone steady and vertical!", + "squatAdmonishmentSquatTime": "Move slower!", + "squatAdmonishmentFullRangeMotion": "Make sure to have a full range of motion!", + "lightTask": "Light Sensor", + "requiredLightLevelSetting": "Required Light Level (lux)", + "lightProgressSemanticsLabel": "Percentage to Required lx", + "luxUnitSuffix": " lx", + "lightNoticeTurnOnLights": "Turn on the lights!", + "lightNoticeFaceScreen": "Face phone screen to the light", + "lightNoticeMoveCloser": "Move closer to light" }