• 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

46.75
/lib/features/gallery/widgets/forms.dart
1
/*
2
 * This file is part of wger Workout Manager <https://github.com/wger-project>.
3
 * Copyright (C) 2020, 2021 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
 * wger Workout Manager 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 'dart:io';
20

21
import 'package:flutter/material.dart';
22
import 'package:flutter_riverpod/flutter_riverpod.dart';
23
import 'package:image_picker/image_picker.dart';
24
import 'package:wger/core/consts.dart';
25
import 'package:wger/core/network/network_provider.dart';
26
import 'package:wger/core/widgets/datetime_input.dart';
27
import 'package:wger/core/widgets/form_submit_button.dart';
28
import 'package:wger/core/widgets/wger_image.dart';
29
import 'package:wger/features/gallery/models/image.dart';
30
import 'package:wger/features/gallery/providers/gallery_notifier.dart';
31
import 'package:wger/l10n/generated/app_localizations.dart';
32

33
class ImageForm extends ConsumerStatefulWidget {
34
  late final GalleryImage _image;
35

36
  ImageForm([GalleryImage? image]) {
1✔
37
    _image = image ?? GalleryImage.empty();
1✔
38
  }
39

40
  @override
1✔
41
  ConsumerState<ImageForm> createState() => _ImageFormState();
1✔
42
}
43

44
class _ImageFormState extends ConsumerState<ImageForm> {
45
  final _form = GlobalKey<FormState>();
46

47
  XFile? _file;
48

49
  final TextEditingController descriptionController = TextEditingController();
50

51
  @override
1✔
52
  void dispose() {
53
    descriptionController.dispose();
2✔
54
    super.dispose();
1✔
55
  }
56

57
  @override
1✔
58
  void initState() {
59
    super.initState();
1✔
60

61
    descriptionController.text = widget._image.description;
5✔
62
  }
63

64
  void _showPicker(ImageSource source) async {
×
65
    final picker = ImagePicker();
×
66
    final file = await picker.pickImage(source: source);
×
67

68
    setState(() {
×
69
      _file = file;
×
70
    });
71
  }
72

73
  /// Returns widget with current picture, depending on whether the user is
74
  /// editing an existing entry or adding a new one. A text message is shown if
75
  /// neither is available
76
  Widget getPicture() {
1✔
77
    // An image file was selected, use it
78
    if (_file != null) {
1✔
79
      return Image(image: FileImage(File(_file!.path)));
×
80
    }
81

82
    // We are editing an existing entry
83
    if (widget._image.imagePath != null) {
3✔
84
      return WgerImage(
1✔
85
        mediaPath: widget._image.imagePath,
3✔
86
        fit: BoxFit.contain,
87
      );
88
    }
89

90
    // No picture available, show a message to the user
91
    return Column(
×
92
      mainAxisAlignment: MainAxisAlignment.center,
93
      children: [
×
94
        Text(AppLocalizations.of(context).selectImage),
×
95
        const SizedBox(height: 8),
96
        const Icon(Icons.photo_camera),
97
      ],
98
    );
99
  }
100

101
  @override
1✔
102
  Widget build(BuildContext context) {
103
    final isOnline = ref.watch(networkStatusProvider);
3✔
104
    // Creating an image or replacing its photo is a binary REST upload and
105
    // needs connectivity. A metadata-only edit syncs through PowerSync and
106
    // works offline.
107
    final requiresUpload = widget._image.id == null || _file != null;
4✔
108

109
    return Form(
1✔
110
      key: _form,
1✔
111
      child: Column(
1✔
112
        mainAxisSize: MainAxisSize.min,
113
        children: [
1✔
114
          Expanded(
1✔
115
            child: GestureDetector(
1✔
116
              onTap: () async {
×
117
                showModalBottomSheet(
×
118
                  context: context,
119
                  builder: (context) {
×
120
                    return SizedBox(
×
121
                      height: 150,
122
                      child: Column(
×
123
                        children: [
×
124
                          ListTile(
×
125
                            onTap: () {
×
126
                              Navigator.of(context).pop();
×
127
                              _showPicker(ImageSource.camera);
×
128
                            },
129
                            leading: const Icon(Icons.photo_camera),
130
                            title: Text(AppLocalizations.of(context).takePicture),
×
131
                          ),
132
                          ListTile(
×
133
                            onTap: () {
×
134
                              Navigator.of(context).pop();
×
135
                              _showPicker(ImageSource.gallery);
×
136
                            },
137
                            leading: const Icon(Icons.photo_library),
138
                            title: Text(
×
139
                              AppLocalizations.of(context).chooseFromLibrary,
×
140
                            ),
141
                          ),
142
                        ],
143
                      ),
144
                    );
145
                  },
146
                );
147
              },
148
              child: getPicture(),
1✔
149
            ),
150
          ),
151
          DateInputWidget(
1✔
152
            key: const Key('field-date'),
153
            value: widget._image.date,
3✔
154
            labelText: AppLocalizations.of(context).date,
2✔
155
            firstDate: DateTime.now().subtract(const Duration(days: 3000)),
2✔
156
            lastDate: DateTime.now(),
1✔
157
            onChanged: (date) => widget._image.date = date,
×
158
            validator: (value) {
×
159
              if (widget._image.id == null && _file == null) {
×
160
                return AppLocalizations.of(context).selectImage;
×
161
              }
162
              return null;
163
            },
164
          ),
165
          TextFormField(
1✔
166
            key: const Key('field-description'),
167
            decoration: InputDecoration(
1✔
168
              labelText: AppLocalizations.of(context).description,
2✔
169
            ),
170
            minLines: 3,
171
            maxLines: 10,
172
            maxLength: GalleryImage.MAX_LENGTH_DESCRIPTION,
173
            controller: descriptionController,
1✔
174
            onSaved: (newValue) {
×
175
              widget._image.description = newValue!;
×
176
            },
177
          ),
178
          FormSubmitButton(
1✔
179
            key: const Key(SUBMIT_BUTTON_KEY_NAME),
180
            enabled: !(requiresUpload && !isOnline),
181
            label: AppLocalizations.of(context).save,
2✔
182
            onPressed: () async {
×
183
              // Validate and save
184
              final isValid = _form.currentState!.validate();
×
185
              if (!isValid) {
186
                return;
187
              }
188
              _form.currentState!.save();
×
189

190
              final notifier = ref.read(galleryProvider.notifier);
×
191
              if (widget._image.id == null) {
×
192
                await notifier.addImage(widget._image, _file!);
×
193
              } else {
194
                await notifier.editImage(widget._image, _file);
×
195
              }
196
              if (context.mounted) {
×
197
                Navigator.of(context).pop();
×
198
              }
199
            },
200
          ),
201
        ],
202
      ),
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