diff --git a/lib/alarm/data/alarm_task_schemas.dart b/lib/alarm/data/alarm_task_schemas.dart index 49f9a72a..b3f15fd7 100644 --- a/lib/alarm/data/alarm_task_schemas.dart +++ b/lib/alarm/data/alarm_task_schemas.dart @@ -1,8 +1,10 @@ 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'; 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'; @@ -115,4 +117,24 @@ Map alarmTaskSchemasMap = { return MemoryTask(onSolve: onSolve, settings: settings); }, ), + AlarmTaskType.squat: AlarmTaskSchema( + (context) => AppLocalizations.of(context)!.squatTask, + SettingGroup("squatSettings", + (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) => AppLocalizations.of(context)!.lightTask, + SettingGroup("lightSensorSettings", + (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/types/alarm_task.dart b/lib/alarm/types/alarm_task.dart index 24daea1f..c90605c0 100644 --- a/lib/alarm/types/alarm_task.dart +++ b/lib/alarm/types/alarm_task.dart @@ -11,6 +11,8 @@ enum AlarmTaskType { sequence, shake, memory, + 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..abad2c8f --- /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/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({ + 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( + AppLocalizations.of(context)!.luxUnitSuffix, + style: textTheme.displaySmall, + ), + ]), + LinearProgressBar( + backgroundColor: colorScheme.onSurface.withOpacity(0.25), + value: percentageThere, + color: colorScheme.secondary, + minHeight: 18, + semanticsLabel: AppLocalizations.of(context)!.lightProgressSemanticsLabel, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _roundLux(0.0) + AppLocalizations.of(context)!.luxUnitSuffix, + style: textTheme.displaySmall, + ), + Text( + _roundLux(targetLux) + AppLocalizations.of(context)!.luxUnitSuffix, + style: textTheme.displaySmall, + ), + ]), + const SizedBox(height: 16.0), + Text( + textAlign: TextAlign.center, + lightValue < 10 + ? AppLocalizations.of(context)!.lightNoticeTurnOnLights + : lightValue < 50 + ? 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 new file mode 100644 index 00000000..73d615c4 --- /dev/null +++ b/lib/alarm/widgets/tasks/squat_task.dart @@ -0,0 +1,263 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:sensors_plus/sensors_plus.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({ + super.key, + required this.onSolve, + required this.settings, + }); + + final VoidCallback onSolve; + final SettingGroup settings; + + @override + 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(); + late final StreamSubscription _accelStream; + late final StreamSubscription _barStream; + + final List barometerSamples = []; + final List accelerometerSamples = []; + + late int squatsCompleted = 0; + + @override + void initState() { + super.initState(); + _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; + DateTime? lastSquatTime; + SquatAdmonishment? falseSquatAdmonish; + + bool _isFakeSquat(DateTime start, DateTime end) { + double averageAccelZ = 0.0; + double averageAccelX = 0.0; + double count = 0.0; + + double avgJerkY = 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++; + 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 jerkY = (nextEvent.y - accel.y) / dtSeconds; + avgJerkY += (jerkY - avgJerkY) / count; + + nextEvent = accel; + } + + if(barRange <= 0.05) { + setState(() { + falseSquatAdmonish = SquatAdmonishment.fullRangeMotion; + }); + return true; + } + + if (squatTimeSeconds <= 1.0) { + setState(() { + falseSquatAdmonish = SquatAdmonishment.squatTime; + }); + return true; + } + + if (averageAccelZ.abs() >= 2 || averageAccelX.abs() >= 2) { + setState(() { + falseSquatAdmonish = SquatAdmonishment.keepPhoneVertical; + }); + return true; + } + + if (avgJerkY.abs() >= 0.1) { + setState(() { + falseSquatAdmonish = SquatAdmonishment.keepMovementSteady; + }); + return true; + } + + setState(() { + falseSquatAdmonish = null; + }); + return false; + } + + void _updateSquatSensorData() { + double accelSecond = _lowPassAccel(); + + 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; + }); + } + } + } + + 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: 250)); + + 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(); + } + + 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); + ColorScheme colorScheme = theme.colorScheme; + TextTheme textTheme = theme.textTheme; + + 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( + AppLocalizations.of(context)!.squatsCompleted, + style: textTheme.headlineLarge, + ), + const SizedBox(height: 16.0), + Text( + _getAdmonishText(falseSquatAdmonish, context), + textAlign: TextAlign.center, + style: textTheme.headlineLarge?.copyWith(color: colorScheme.error), + ), + ], + ), + ); + } +} 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/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/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" } 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/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/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; + } +} diff --git a/pubspec.lock b/pubspec.lock index a3c9c2ad..0f4ab923 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: @@ -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: @@ -693,18 +701,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: @@ -961,6 +969,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: @@ -1029,7 +1053,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -1058,10 +1082,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 +1098,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 +1130,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 +1250,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..9ce0b32c 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 @@ -88,6 +88,8 @@ dependencies: mime: ^1.0.6 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"