• 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

72.86
/lib/features/routines/widgets/forms/session.dart
1
/*
2
 * This file is part of wger Workout Manager <https://github.com/wger-project>.
3
 * Copyright (c) 2020 - 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:clock/clock.dart';
20
import 'package:flutter/material.dart';
21
import 'package:flutter_riverpod/flutter_riverpod.dart';
22
import 'package:logging/logging.dart';
23
import 'package:wger/core/snackbar.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/session.dart';
27
import 'package:wger/features/routines/providers/workout_session_notifier.dart';
28
import 'package:wger/features/routines/validators.dart';
29
import 'package:wger/l10n/generated/app_localizations.dart';
30

31
class SessionForm extends ConsumerStatefulWidget {
32
  final _logger = Logger('SessionForm');
33
  final int? _routineId;
34
  final int? _dayId;
35

36
  /// The session to edit, or null to create a new one.
37
  final WorkoutSession? _session;
38
  final Function()? _onSaved;
39

40
  SessionForm(this._routineId, {Function()? onSaved, WorkoutSession? session, int? dayId})
2✔
41
    : _onSaved = onSaved,
42
      _session = session,
43
      _dayId = dayId;
44

45
  @override
2✔
46
  _SessionFormState createState() => _SessionFormState();
2✔
47
}
48

49
class _SessionFormState extends ConsumerState<SessionForm> {
50
  final _form = GlobalKey<FormState>();
51

52
  final notesController = TextEditingController();
53

54
  /// Editable copy. Seeded once and owned by the form, so parent rebuilds
55
  /// (e.g. stream re-emissions) don't clobber the user's in-progress edits.
56
  late WorkoutSession _draft;
57

58
  @override
2✔
59
  void initState() {
60
    super.initState();
2✔
61
    _draft =
2✔
62
        widget._session ??
4✔
63
        WorkoutSession(routineId: widget._routineId, dayId: widget._dayId, date: clock.now());
×
64
    notesController.text = _draft.notes ?? '';
8✔
65
  }
66

67
  @override
×
68
  void didUpdateWidget(SessionForm oldWidget) {
69
    super.didUpdateWidget(oldWidget);
×
70
    // Adopt a new server identity (e.g. a session created lazily while logging
71
    // arrives only after the form first built) without discarding edits when
72
    // the same session is merely re-emitted by the stream.
73
    if (widget._session != null && widget._session!.id != oldWidget._session?.id) {
×
74
      _draft = widget._session!;
×
75
      notesController.text = _draft.notes ?? '';
×
76
    }
77
  }
78

79
  @override
2✔
80
  void dispose() {
81
    notesController.dispose();
4✔
82
    super.dispose();
2✔
83
  }
84

85
  @override
2✔
86
  Widget build(BuildContext context) {
87
    final sessionProvider = ref.read(workoutSessionProvider.notifier);
8✔
88

89
    return Form(
2✔
90
      key: _form,
2✔
91
      child: Column(
2✔
92
        mainAxisAlignment: MainAxisAlignment.center,
93
        children: [
2✔
94
          ToggleButtons(
2✔
95
            key: const ValueKey('impression-toggle-buttons'),
96
            renderBorder: false,
97
            onPressed: (int index) {
×
98
              setState(() {
×
99
                _draft = _draft.copyWith(impression: WorkoutImpression.values[index]);
×
100
              });
101
            },
102
            isSelected: WorkoutImpression.values.map((e) => e == _draft.impression).toList(),
12✔
103
            children: const [
104
              Icon(Icons.sentiment_very_dissatisfied),
105
              Icon(Icons.sentiment_neutral),
106
              Icon(Icons.sentiment_very_satisfied),
107
            ],
108
          ),
109
          TextFormField(
2✔
110
            decoration: InputDecoration(
2✔
111
              labelText: AppLocalizations.of(context).notes,
4✔
112
            ),
113
            maxLines: 3,
114
            maxLength: WorkoutSession.maxNotesChars,
115
            controller: notesController,
2✔
116
            keyboardType: TextInputType.multiline,
117
            onFieldSubmitted: (_) {},
×
118
            onSaved: (newValue) {
2✔
119
              _draft = _draft.copyWith(notes: newValue);
8✔
120
            },
121
            validator: (value) {
2✔
122
              if (value != null && value.length > WorkoutSession.maxNotesChars) {
4✔
123
                return AppLocalizations.of(
×
124
                  context,
125
                ).enterMaxCharacters(WorkoutSession.maxNotesChars.toString());
×
126
              }
127
              return null;
128
            },
129
          ),
130
          Row(
2✔
131
            spacing: 10,
132
            children: [
2✔
133
              Flexible(
2✔
134
                child: TimeInputWidget(
2✔
135
                  key: const ValueKey('time-start'),
136
                  value: _draft.timeStart,
4✔
137
                  labelText: AppLocalizations.of(context).timeStart,
4✔
138
                  onCleared: () => _draft = _draft.copyWith(timeStart: null),
×
139
                  onChanged: (time) => _draft = _draft.copyWith(timeStart: time),
×
140
                ),
141
              ),
142
              Flexible(
2✔
143
                child: TimeInputWidget(
2✔
144
                  key: const ValueKey('time-end'),
145
                  value: _draft.timeEnd,
4✔
146
                  labelText: AppLocalizations.of(context).timeEnd,
4✔
147
                  onCleared: () => _draft = _draft.copyWith(timeEnd: null),
×
148
                  onChanged: (time) => _draft = _draft.copyWith(timeEnd: time),
×
149
                ),
150
              ),
151
            ],
152
          ),
153
          const SizedBox(height: 5),
154
          FormSubmitButton(
2✔
155
            key: const ValueKey('save-button'),
156
            label: AppLocalizations.of(context).save,
4✔
157
            onPressed: () async {
2✔
158
              if (!_form.currentState!.validate()) {
6✔
159
                return;
160
              }
161
              _form.currentState!.save();
6✔
162

163
              final i18n = AppLocalizations.of(context);
2✔
164
              final error = validateWorkoutSessionTimes(
2✔
165
                timeStart: _draft.timeStart,
4✔
166
                timeEnd: _draft.timeEnd,
4✔
167
                i18n: i18n,
168
              );
169
              if (error != null) {
170
                showSnackbar(context, error);
×
171
                return;
172
              }
173

174
              // A WgerHttpException is surfaced inline by FormSubmitButton.
175
              if (_draft.id == null) {
4✔
176
                widget._logger.fine('Adding new session');
×
177
                await sessionProvider.addEntry(_draft);
×
178
              } else {
179
                widget._logger.fine('Editing existing session with id ${_draft.id}');
12✔
180
                await sessionProvider.updateEntry(_draft);
4✔
181
              }
182

183
              if (context.mounted && widget._onSaved != null) {
6✔
184
                widget._onSaved!();
6✔
185
              }
186
            },
187
          ),
188
        ],
189
      ),
190
    );
191
  }
192
}
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