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);
+ });
+ },
+ ),
+ ),
),
);
}