• 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

0.0
/lib/features/auth/widgets/advanced_sheet.dart
1
/*
2
 * This file is part of wger Workout Manager <https://github.com/wger-project>.
3
 * Copyright (c) 2026 - 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:wger/core/consts.dart';
21
import 'package:wger/features/auth/widgets/server_field.dart';
22
import 'package:wger/l10n/generated/app_localizations.dart';
23

24
/// Opens the advanced bottom sheet (server + sign-in-method selection).
25
///
26
/// [onChanged] fires whenever the user picks a different option so the
27
/// parent screen can reflect the choice live behind the sheet.
28
Future<void> showAdvancedSheet({
×
29
  required BuildContext context,
30
  required bool initialHideCustomServer,
31
  required bool initialUsePassword,
32
  required bool initialAllowSelfSignedCerts,
33
  required bool loginMode,
34
  required TextEditingController serverUrlController,
35
  required void Function(bool hideCustomServer, bool usePassword, bool allowSelfSignedCerts)
36
  onChanged,
37
}) {
38
  return showModalBottomSheet<void>(
×
39
    context: context,
40
    isScrollControlled: true,
41
    showDragHandle: true,
42
    builder: (_) => AdvancedSheet(
×
43
      initialHideCustomServer: initialHideCustomServer,
44
      initialUsePassword: initialUsePassword,
45
      initialAllowSelfSignedCerts: initialAllowSelfSignedCerts,
46
      loginMode: loginMode,
47
      serverUrlController: serverUrlController,
48
      onChanged: onChanged,
49
    ),
50
  );
51
}
52

53
class AdvancedSheet extends StatefulWidget {
54
  final bool initialHideCustomServer;
55
  final bool initialUsePassword;
56
  final bool initialAllowSelfSignedCerts;
57
  final bool loginMode;
58
  final TextEditingController serverUrlController;
59
  final void Function(bool hideCustomServer, bool usePassword, bool allowSelfSignedCerts) onChanged;
60

61
  const AdvancedSheet({
×
62
    required this.initialHideCustomServer,
63
    required this.initialUsePassword,
64
    required this.initialAllowSelfSignedCerts,
65
    required this.loginMode,
66
    required this.serverUrlController,
67
    required this.onChanged,
68
    super.key,
69
  });
70

71
  @override
×
72
  State<AdvancedSheet> createState() => _AdvancedSheetState();
×
73
}
74

75
class _AdvancedSheetState extends State<AdvancedSheet> {
76
  late bool _hideCustomServer;
77
  late bool _usePassword;
78
  late bool _allowSelfSignedCerts;
79
  final _formKey = GlobalKey<FormState>();
80

81
  @override
×
82
  void initState() {
83
    super.initState();
×
84
    _hideCustomServer = widget.initialHideCustomServer;
×
85
    _usePassword = widget.initialUsePassword;
×
86
    _allowSelfSignedCerts = widget.initialAllowSelfSignedCerts;
×
87
  }
88

89
  void _set(VoidCallback change) {
×
90
    setState(change);
×
91
    widget.onChanged(_hideCustomServer, _usePassword, _allowSelfSignedCerts);
×
92
  }
93

94
  @override
×
95
  Widget build(BuildContext context) {
96
    final i18n = AppLocalizations.of(context);
×
97
    final theme = Theme.of(context);
×
98
    final defaultServerName = Uri.parse(DEFAULT_SERVER_PROD).host;
×
99

100
    return SingleChildScrollView(
×
101
      child: Padding(
×
102
        padding: EdgeInsets.fromLTRB(
×
103
          22,
104
          0,
105
          22,
106
          26 + MediaQuery.viewInsetsOf(context).bottom,
×
107
        ),
108
        child: Form(
×
109
          key: _formKey,
×
110
          child: Column(
×
111
            mainAxisSize: MainAxisSize.min,
112
            crossAxisAlignment: CrossAxisAlignment.stretch,
113
            children: [
×
114
              Row(
×
115
                children: [
×
116
                  Expanded(
×
117
                    child: Column(
×
118
                      crossAxisAlignment: CrossAxisAlignment.start,
119
                      children: [
×
120
                        Text(i18n.advanced, style: theme.textTheme.titleLarge),
×
121
                        Text(
×
122
                          i18n.advancedSubtitle,
×
123
                          style: theme.textTheme.bodySmall?.copyWith(
×
124
                            color: theme.colorScheme.onSurfaceVariant,
×
125
                          ),
126
                        ),
127
                      ],
128
                    ),
129
                  ),
130
                  IconButton(
×
131
                    key: const Key('advancedCloseButton'),
132
                    icon: const Icon(Icons.close),
133
                    onPressed: () => Navigator.pop(context),
×
134
                  ),
135
                ],
136
              ),
137
              _SectionLabel(text: i18n.serverSectionLabel),
×
138
              _OptionRow(
×
139
                icon: Icons.public_outlined,
140
                title: defaultServerName,
141
                detail: i18n.serverOptionDefaultDetail,
×
142
                selected: _hideCustomServer,
×
143
                onTap: () => _set(() {
×
144
                  _hideCustomServer = true;
×
145
                  // Selecting the official server always points at wger.de
146
                  widget.serverUrlController.text = DEFAULT_SERVER_PROD;
×
147
                }),
148
              ),
149
              _OptionRow(
×
150
                icon: Icons.dns_outlined,
151
                title: i18n.serverOptionSelfHostedTitle,
×
152
                detail: i18n.serverOptionSelfHostedDetail,
×
153
                selected: !_hideCustomServer,
×
154
                onTap: () => _set(() => _hideCustomServer = false),
×
155
              ),
156
              if (!_hideCustomServer)
×
157
                Padding(
×
158
                  padding: const EdgeInsets.only(top: 2, bottom: 4),
159
                  child: ServerField(controller: widget.serverUrlController),
×
160
                ),
161
              // Stays visible for the official server so the option is
162
              // discoverable, but disabled: wger.de has a valid certificate and
163
              // is never trusted through this setting. The stored preference is
164
              // kept, so switching back to self-hosted restores it.
165
              SwitchListTile(
×
166
                key: const Key('allowSelfSignedCertsSwitch'),
167
                contentPadding: EdgeInsets.zero,
168
                title: Text(i18n.allowSelfSignedCertsTitle),
×
169
                subtitle: Text(
×
170
                  i18n.allowSelfSignedCertsDetail,
×
171
                  style: theme.textTheme.bodySmall?.copyWith(
×
172
                    color: _hideCustomServer
×
173
                        ? theme.colorScheme.onSurfaceVariant
×
174
                        : theme.colorScheme.error,
×
175
                  ),
176
                ),
177
                value: !_hideCustomServer && _allowSelfSignedCerts,
×
178
                onChanged: _hideCustomServer
×
179
                    ? null
180
                    : (value) => _set(() => _allowSelfSignedCerts = value),
×
181
              ),
182
              if (widget.loginMode) ...[
×
183
                _SectionLabel(text: i18n.signInMethodSectionLabel),
×
184
                _OptionRow(
×
185
                  icon: Icons.person_outline,
186
                  title: i18n.authOptionPasswordTitle,
×
187
                  selected: _usePassword,
×
188
                  onTap: () => _set(() => _usePassword = true),
×
189
                ),
190
                _OptionRow(
×
191
                  icon: Icons.key_outlined,
192
                  title: i18n.refreshToken,
×
193
                  detail: i18n.tokenSubtitle,
×
194
                  selected: !_usePassword,
×
195
                  onTap: () => _set(() => _usePassword = false),
×
196
                ),
197
              ],
198
              const SizedBox(height: 16),
×
199
              SizedBox(
×
200
                height: 45,
201
                child: ElevatedButton(
×
202
                  key: const Key('advancedDoneButton'),
203
                  onPressed: () {
×
204
                    if (_formKey.currentState?.validate() ?? true) {
×
205
                      Navigator.pop(context);
×
206
                    }
207
                  },
208
                  style: ElevatedButton.styleFrom(
×
209
                    backgroundColor: theme.colorScheme.primary,
×
210
                  ),
211
                  child: Text(
×
212
                    i18n.done,
×
213
                    style: const TextStyle(
214
                      color: Colors.white,
215
                      fontWeight: FontWeight.w600,
216
                    ),
217
                  ),
218
                ),
219
              ),
220
            ],
221
          ),
222
        ),
223
      ),
224
    );
225
  }
226
}
227

228
class _SectionLabel extends StatelessWidget {
229
  final String text;
230
  const _SectionLabel({required this.text});
×
231

232
  @override
×
233
  Widget build(BuildContext context) {
234
    final theme = Theme.of(context);
×
235
    return Padding(
×
236
      padding: const EdgeInsets.only(top: 14, bottom: 8),
237
      child: Text(
×
238
        text.toUpperCase(),
×
239
        style: theme.textTheme.labelSmall?.copyWith(
×
240
          color: theme.colorScheme.outline,
×
241
          letterSpacing: 0.6,
242
          fontWeight: FontWeight.w600,
243
        ),
244
      ),
245
    );
246
  }
247
}
248

249
class _OptionRow extends StatelessWidget {
250
  final IconData icon;
251
  final String title;
252
  final String? detail;
253
  final bool selected;
254
  final VoidCallback onTap;
255

256
  const _OptionRow({
×
257
    required this.icon,
258
    required this.title,
259
    this.detail,
260
    required this.selected,
261
    required this.onTap,
262
  });
263

264
  @override
×
265
  Widget build(BuildContext context) {
266
    final scheme = Theme.of(context).colorScheme;
×
267
    final textTheme = Theme.of(context).textTheme;
×
268
    return Padding(
×
269
      padding: const EdgeInsets.only(bottom: 8),
270
      child: Ink(
×
271
        decoration: BoxDecoration(
×
272
          color: selected ? scheme.primaryContainer : scheme.surfaceContainerLow,
×
273
          borderRadius: BorderRadius.circular(14),
×
274
          border: Border.all(
×
275
            color: selected ? scheme.primary : Colors.transparent,
×
276
            width: 1,
277
          ),
278
        ),
279
        child: InkWell(
×
280
          borderRadius: BorderRadius.circular(14),
×
281
          onTap: onTap,
×
282
          child: Padding(
×
283
            padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
284
            child: Row(
×
285
              children: [
×
286
                Container(
×
287
                  width: 36,
288
                  height: 36,
289
                  alignment: Alignment.center,
290
                  decoration: BoxDecoration(
×
291
                    color: selected ? scheme.primary : scheme.surface,
×
292
                    borderRadius: BorderRadius.circular(10),
×
293
                    border: selected ? null : Border.all(color: scheme.outlineVariant),
×
294
                  ),
295
                  child: Icon(
×
296
                    icon,
×
297
                    size: 18,
298
                    color: selected ? scheme.onPrimary : scheme.primary,
×
299
                  ),
300
                ),
301
                const SizedBox(width: 12),
302
                Expanded(
×
303
                  child: Column(
×
304
                    crossAxisAlignment: CrossAxisAlignment.start,
305
                    children: [
×
306
                      Text(title, style: textTheme.titleSmall),
×
307
                      const SizedBox(height: 2),
308
                      if (detail != null)
×
309
                        Text(
×
310
                          detail!,
×
311
                          style: textTheme.bodySmall?.copyWith(
×
312
                            color: scheme.onSurfaceVariant,
×
313
                          ),
314
                        ),
315
                    ],
316
                  ),
317
                ),
318
                const SizedBox(width: 8),
319
                Container(
×
320
                  width: 20,
321
                  height: 20,
322
                  alignment: Alignment.center,
323
                  decoration: BoxDecoration(
×
324
                    shape: BoxShape.circle,
325
                    color: selected ? scheme.primary : Colors.transparent,
×
326
                    border: Border.all(
×
327
                      color: selected ? scheme.primary : scheme.outline,
×
328
                      width: 2,
329
                    ),
330
                  ),
331
                  child: selected ? Icon(Icons.check, size: 12, color: scheme.onPrimary) : null,
×
332
                ),
333
              ],
334
            ),
335
          ),
336
        ),
337
      ),
338
    );
339
  }
340
}
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