• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wger-project / flutter / 30854254088

03 Aug 2026 09:21PM UTC coverage: 40.895% (-15.0%) from 55.93%
30854254088

Pull #1260

github

web-flow
Merge d93794de5 into e3affdd4f
Pull Request #1260: WIP: health sync

9980 of 24404 relevant lines covered (40.89%)

1.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.91
/lib/features/routines/widgets/forms/routine.dart
1
/*
2
 * This file is part of wger Workout Manager <https://github.com/wger-project>.
3
 * Copyright (c)  2026 wger Team
4
 *
5
 * wger Workout Manager is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU Affero General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU Affero General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Affero General Public License
16
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 */
18

19
import 'package:flutter/material.dart';
20
import 'package:flutter_riverpod/flutter_riverpod.dart';
21
import 'package:wger/core/consts.dart';
22
import 'package:wger/core/network/network_provider.dart';
23
import 'package:wger/core/validators.dart';
24
import 'package:wger/core/widgets/datetime_input.dart';
25
import 'package:wger/core/widgets/form_submit_button.dart';
26
import 'package:wger/features/routines/models/routine.dart';
27
import 'package:wger/features/routines/providers/routines_notifier.dart';
28
import 'package:wger/features/routines/screens/routine_edit_screen.dart';
29
import 'package:wger/l10n/generated/app_localizations.dart';
30

31
class RoutineForm extends ConsumerStatefulWidget {
32
  final Routine _routine;
33
  final bool useListView;
34

35
  const RoutineForm(this._routine, {this.useListView = false});
1✔
36

37
  @override
1✔
38
  _RoutineFormState createState() => _RoutineFormState();
1✔
39
}
40

41
class _RoutineFormState extends ConsumerState<RoutineForm> {
42
  final _form = GlobalKey<FormState>();
43

44
  late bool fitInWeek;
45
  late DateTime startDate;
46
  late DateTime endDate;
47
  final workoutNameController = TextEditingController();
48
  final workoutDescriptionController = TextEditingController();
49

50
  @override
1✔
51
  void initState() {
52
    super.initState();
1✔
53
    fitInWeek = widget._routine.fitInWeek;
4✔
54
    workoutNameController.text = widget._routine.name;
5✔
55
    workoutDescriptionController.text = widget._routine.description;
5✔
56
    startDate = widget._routine.start;
4✔
57
    endDate = widget._routine.end;
4✔
58
  }
59

60
  @override
1✔
61
  void dispose() {
62
    workoutNameController.dispose();
2✔
63
    workoutDescriptionController.dispose();
2✔
64
    super.dispose();
1✔
65
  }
66

67
  @override
1✔
68
  Widget build(BuildContext context) {
69
    final i18n = AppLocalizations.of(context);
1✔
70
    final isOnline = ref.watch(networkStatusProvider);
3✔
71

72
    final children = [
1✔
73
      TextFormField(
1✔
74
        key: const Key('field-name'),
75
        decoration: InputDecoration(labelText: i18n.name),
2✔
76
        controller: workoutNameController,
1✔
77
        validator: (value) {
1✔
78
          if (value!.isEmpty ||
1✔
79
              value.length < Routine.MIN_LENGTH_NAME ||
2✔
80
              value.length > Routine.MAX_LENGTH_NAME) {
2✔
81
            return i18n.enterCharacters(
×
82
              Routine.MIN_LENGTH_NAME.toString(),
×
83
              Routine.MAX_LENGTH_NAME.toString(),
×
84
            );
85
          }
86
          return null;
87
        },
88
        onSaved: (newValue) {
1✔
89
          widget._routine.name = newValue!;
3✔
90
        },
91
      ),
92
      TextFormField(
1✔
93
        key: const Key('field-description'),
94
        decoration: InputDecoration(labelText: i18n.description),
2✔
95
        minLines: 3,
96
        maxLines: 10,
97
        controller: workoutDescriptionController,
1✔
98
        validator: (value) {
1✔
99
          if (value!.length > Routine.MAX_LENGTH_DESCRIPTION) {
2✔
100
            return i18n.enterCharacters(
×
101
              Routine.MIN_LENGTH_DESCRIPTION.toString(),
×
102
              Routine.MAX_LENGTH_DESCRIPTION.toString(),
×
103
            );
104
          }
105
          return null;
106
        },
107
        onSaved: (newValue) {
1✔
108
          widget._routine.description = newValue!;
3✔
109
        },
110
      ),
111
      DateInputWidget(
1✔
112
        key: const Key('field-start-date'),
113
        value: startDate,
1✔
114
        labelText: i18n.startDate,
1✔
115
        firstDate: DateTime.now().subtract(const Duration(days: 365)),
2✔
116
        lastDate: DateTime.now().add(const Duration(days: 365)),
2✔
117
        onChanged: (picked) {
×
118
          widget._routine.start = picked;
×
119
          setState(() {
×
120
            startDate = picked;
×
121
          });
122
        },
123
        validator: (value) {
1✔
124
          final rangeError = validateDateRange(startDate, endDate, i18n);
3✔
125
          if (rangeError != null) {
126
            return rangeError;
127
          }
128
          if (endDate.difference(startDate).inDays < Routine.MIN_DURATION * 7) {
6✔
129
            return i18n.minLengthRoutine(Routine.MIN_DURATION);
×
130
          }
131
          if (endDate.difference(startDate).inDays > Routine.MAX_DURATION * 7) {
6✔
132
            return i18n.maxLengthRoutine(Routine.MAX_DURATION);
×
133
          }
134
          return null;
135
        },
136
      ),
137
      DateInputWidget(
1✔
138
        key: const Key('field-end-date'),
139
        value: endDate,
1✔
140
        labelText: i18n.endDate,
1✔
141
        firstDate: DateTime.now().subtract(const Duration(days: 365)),
2✔
142
        lastDate: DateTime.now().add(const Duration(days: 365)),
2✔
143
        onChanged: (picked) {
×
144
          widget._routine.end = picked;
×
145
          setState(() {
×
146
            endDate = picked;
×
147
          });
148
        },
149
      ),
150
      const SizedBox(height: 5),
151
      SwitchListTile(
1✔
152
        title: Text(i18n.fitInWeek),
2✔
153
        subtitle: Text(i18n.fitInWeekHelp),
2✔
154
        isThreeLine: true,
155
        value: widget._routine.fitInWeek,
3✔
156
        contentPadding: const EdgeInsets.all(4),
157
        onChanged: (bool? value) {
×
158
          if (value == null) {
159
            return;
160
          }
161

162
          widget._routine.fitInWeek = value;
×
163
          if (mounted) {
×
164
            setState(() {
×
165
              fitInWeek = value;
×
166
            });
167
          }
168
        },
169
      ),
170
      const SizedBox(height: 5),
171
      // Creating a routine needs the server to assign an integer PK; editing an
172
      // existing one syncs through PowerSync and works offline.
173
      FormSubmitButton(
1✔
174
        key: const Key(SUBMIT_BUTTON_KEY_NAME),
175
        enabled: !(widget._routine.id == null && !isOnline),
3✔
176
        label: AppLocalizations.of(context).save,
2✔
177
        onPressed: () async {
1✔
178
          // Validate and save
179
          final isValid = _form.currentState!.validate();
3✔
180
          if (!isValid) {
181
            return;
182
          }
183
          _form.currentState!.save();
3✔
184

185
          final routinesProvider = ref.read(routinesRiverpodProvider.notifier);
4✔
186
          if (widget._routine.id != null) {
3✔
187
            await routinesProvider.editRoutine(widget._routine);
3✔
188
          } else {
189
            final routine = await routinesProvider.addRoutine(widget._routine);
×
190
            if (context.mounted) {
×
191
              Navigator.of(context).pushReplacementNamed(
×
192
                RoutineEditScreen.routeName,
193
                arguments: routine.id,
×
194
              );
195
            }
196
          }
197
        },
198
      ),
199
    ];
200
    return Form(
1✔
201
      key: _form,
1✔
202
      child: widget.useListView ? ListView(children: children) : Column(children: children),
3✔
203
    );
204
  }
205
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc