Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

<h3 align="center">Just Another Workout Timer</h3>


<p align="center">
A simple timer for your workouts, built with Flutter!
<br />
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions lib/layouts/settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,35 @@ class SettingsPageState extends State<SettingsPage> {
subtitle: Text(S.of(context).expanded_setlist_info),
pref: 'expanded_setlist',
),

PrefTitle(
title: Text(
S.of(context).editWorkout,
),
),
PrefDropdown<int>(
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,
Expand Down
5 changes: 5 additions & 0 deletions lib/layouts/workout_builder.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -196,7 +197,9 @@ class BuilderPageState extends State<BuilderPage> {
lowerLimit: 1,
upperLimit: 99,
largeSteps: false,
step: 1,
formatNumber: false,
tapToEdit: false,
value: set.repetitions,
valueChanged: (repetitions) {
setState(() {
Expand Down Expand Up @@ -337,7 +340,9 @@ class BuilderPageState extends State<BuilderPage> {
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(() {
Expand Down
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
86 changes: 64 additions & 22 deletions lib/utils/number_stepper.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:numberpicker/numberpicker.dart';

Expand All @@ -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;
Expand All @@ -24,20 +27,26 @@ class NumberStepper extends StatefulWidget {
int value;
final ValueChanged<int> 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();
}

class CustomStepperState extends State<NumberStepper> {
bool _isEditingText = false;
bool _isSpinnerActive = true;
late TextEditingController _editingController;

@override
void initState() {
super.initState();
_editingController = TextEditingController(text: widget.value.toString());
_isSpinnerActive = !widget.tapToEdit;
}

@override
Expand All @@ -47,12 +56,16 @@ class CustomStepperState extends State<NumberStepper> {
}

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,
Expand All @@ -75,35 +88,64 @@ class CustomStepperState extends State<NumberStepper> {
}
});
},
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),
),
),
),
);
}
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<UserScrollNotification>(
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);
});
},
),
),
),
);
}
Expand Down
Loading