diff --git a/README.md b/README.md index f538965c..c9389b98 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@

Just Another Workout Timer

+

A simple timer for your workouts, built with Flutter!
diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index aa73fd1b..03cf3b33 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -57,7 +57,9 @@ "settingHalfway": "Play sound or announcement halfway during exercise", "settings": "Settings", "soundOutput": "Sound output", + "spinnerStep": "Editor time granularity", "startWorkout": "Start workout", + "tapToEdit": "Tap to edit", "title": "Just Another Workout Timer", "tts": "Text-to-Speech (TTS)", "ttsLang": "TTS Language", diff --git a/lib/layouts/settings_page.dart b/lib/layouts/settings_page.dart index 22cf1c4d..f43385ee 100644 --- a/lib/layouts/settings_page.dart +++ b/lib/layouts/settings_page.dart @@ -102,6 +102,35 @@ class SettingsPageState extends State { subtitle: Text(S.of(context).expanded_setlist_info), pref: 'expanded_setlist', ), + + PrefTitle( + title: Text( + S.of(context).editWorkout, + ), + ), + PrefDropdown( + title: Text(S.of(context).spinnerStep), + pref: 'spinner_step', + items: [ + DropdownMenuItem( + value: 5, + child: Text("5 ${S.of(context).seconds}"), + ), + DropdownMenuItem( + value: 10, + child: Text("10 ${S.of(context).seconds}"), + ), + DropdownMenuItem( + value: 15, + child: Text("15 ${S.of(context).seconds}"), + ), + ], + ), + PrefSwitch( + title: Text(S.of(context).tapToEdit), + pref: 'tap_to_edit', + ), + PrefTitle( title: Text( S.of(context).backup, diff --git a/lib/layouts/workout_builder.dart b/lib/layouts/workout_builder.dart index 04c16cc2..5ec71fc0 100644 --- a/lib/layouts/workout_builder.dart +++ b/lib/layouts/workout_builder.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; +import 'package:prefs/prefs.dart'; import 'package:uuid/uuid.dart'; import '../generated/l10n.dart'; @@ -196,7 +197,9 @@ class BuilderPageState extends State { lowerLimit: 1, upperLimit: 99, largeSteps: false, + step: 1, formatNumber: false, + tapToEdit: false, value: set.repetitions, valueChanged: (repetitions) { setState(() { @@ -337,7 +340,9 @@ class BuilderPageState extends State { lowerLimit: 0, upperLimit: 10800, largeSteps: true, + step: Prefs.getInt('spinner_step', 10) , formatNumber: true, + tapToEdit: Prefs.getBool('tap_to_edit', false), value: _workout.sets[setIndex].exercises[exIndex].duration, valueChanged: (duration) { setState(() { diff --git a/lib/main.dart b/lib/main.dart index 78ded611..ecaa9df1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,6 +31,8 @@ void main() async { 'tts_next_announce': true, 'sound': 'tts', 'expanded_setlist': false, + 'spinner_step': 10, + 'tap_to_edit' : false, }, ).then( (service) => Future.wait([ diff --git a/lib/utils/number_stepper.dart b/lib/utils/number_stepper.dart index cd174c7a..b9d1dcc3 100644 --- a/lib/utils/number_stepper.dart +++ b/lib/utils/number_stepper.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:numberpicker/numberpicker.dart'; @@ -16,6 +17,8 @@ class NumberStepper extends StatefulWidget { required this.valueChanged, required this.formatNumber, required this.largeSteps, + required this.step, + required this.tapToEdit, }); final int lowerLimit; @@ -24,7 +27,11 @@ class NumberStepper extends StatefulWidget { int value; final ValueChanged valueChanged; final bool formatNumber; + // if false, shows a number with plus/minus buttons. + // if true, shows the spinner. final bool largeSteps; + final int step; + final bool tapToEdit; @override CustomStepperState createState() => CustomStepperState(); @@ -32,12 +39,14 @@ class NumberStepper extends StatefulWidget { class CustomStepperState extends State { bool _isEditingText = false; + bool _isSpinnerActive = true; late TextEditingController _editingController; @override void initState() { super.initState(); _editingController = TextEditingController(text: widget.value.toString()); + _isSpinnerActive = !widget.tapToEdit; } @override @@ -47,12 +56,16 @@ class CustomStepperState extends State { } Widget _editableTextField() { - if (_isEditingText) { + var isSpinnerValid = widget.value % widget.step == 0; + + if (_isEditingText || !isSpinnerValid) { _editingController.value = TextEditingValue(text: widget.value.toString()); - return Center( + return Container( + width: 122, + alignment: Alignment.centerRight, child: SizedBox( - width: 112, + width: 75, child: TextField( maxLines: 1, maxLengthEnforcement: MaxLengthEnforcement.enforced, @@ -75,9 +88,13 @@ class CustomStepperState extends State { } }); }, - autofocus: true, + // Only autofocus if we're text editing, not for isSpinnerValid + autofocus: _isEditingText, controller: _editingController, - decoration: InputDecoration(suffixText: S.of(context).seconds), + decoration: InputDecoration( + helperText: S.of(context).seconds, + helperStyle: const TextStyle(fontSize: 14), + ), ), ), ); @@ -85,25 +102,50 @@ class CustomStepperState extends State { return InkWell( onTap: () { setState(() { - _isEditingText = true; + if (widget.tapToEdit) { + _isSpinnerActive = true; + } + else { + _isEditingText = true; + } + }); + }, + onLongPress: () { + setState(() { + if (widget.tapToEdit) { + _isEditingText = true; + } }); }, - child: NumberPicker( - itemHeight: 32, - value: widget.value, - minValue: widget.lowerLimit, - step: 10, - itemCount: 3, - haptics: true, - zeroPad: false, - maxValue: widget.upperLimit, - textMapper: (value) => Utils.formatSeconds(int.parse(value)), - onChanged: (value) { - setState(() { - widget.value = value; - widget.valueChanged(value); - }); - }, + child: IgnorePointer( + ignoring: !_isSpinnerActive, + child: NotificationListener( + onNotification: (notification) { + setState(() { + if (notification.direction == ScrollDirection.idle && widget.tapToEdit) { + _isSpinnerActive = false; + } + }); + return true; + }, + child: NumberPicker( + itemHeight: 32, + value: widget.value, + minValue: widget.lowerLimit, + step: widget.step, + itemCount: 3, + haptics: true, + zeroPad: false, + maxValue: widget.upperLimit, + textMapper: (value) => Utils.formatSeconds(int.parse(value)), + onChanged: (value) { + setState(() { + widget.value = value; + widget.valueChanged(value); + }); + }, + ), + ), ), ); }