@@ -34,7 +34,7 @@ class FigmaToggle extends StatelessWidget {
34
34
final theme = Theme .of (context);
35
35
final colorScheme = theme.colorScheme;
36
36
37
- // Figma-specified dimensions
37
+ // Exact Figma-specified dimensions
38
38
final trackWidth = value ? 48.0 : 46.0 ;
39
39
final trackHeight = value ? 28.0 : 26.0 ;
40
40
final thumbRadius = value ? 10.0 : 7.0 ;
@@ -48,6 +48,13 @@ class FigmaToggle extends StatelessWidget {
48
48
final trackColor = value ? effectiveActiveColor : effectiveInactiveColor;
49
49
final thumbColor = value ? effectiveActiveThumbColor : effectiveInactiveThumbColor;
50
50
51
+ // Calculate thumb positioning with proper padding
52
+ final thumbDiameter = thumbRadius * 2 ;
53
+ final horizontalPadding = 4.0 ;
54
+ final thumbLeftPosition = value
55
+ ? trackWidth - thumbDiameter - horizontalPadding
56
+ : horizontalPadding;
57
+
51
58
return GestureDetector (
52
59
onTap: onChanged != null ? () => onChanged !(! value) : null ,
53
60
child: AnimatedContainer (
@@ -64,21 +71,19 @@ class FigmaToggle extends StatelessWidget {
64
71
AnimatedPositioned (
65
72
duration: const Duration (milliseconds: 200 ),
66
73
curve: Curves .easeInOut,
67
- left: value
68
- ? trackWidth - (thumbRadius * 2 ) - 4.0 // 4px padding from edge
69
- : 4.0 , // 4px padding from edge
70
- top: (trackHeight - (thumbRadius * 2 )) / 2 ,
74
+ left: thumbLeftPosition,
75
+ top: (trackHeight - thumbDiameter) / 2 ,
71
76
child: AnimatedContainer (
72
77
duration: const Duration (milliseconds: 200 ),
73
78
curve: Curves .easeInOut,
74
- width: thumbRadius * 2 ,
75
- height: thumbRadius * 2 ,
79
+ width: thumbDiameter ,
80
+ height: thumbDiameter ,
76
81
decoration: BoxDecoration (
77
82
shape: BoxShape .circle,
78
83
color: thumbColor,
79
84
boxShadow: [
80
85
BoxShadow (
81
- color: Colors .black.withOpacity ( 0.2 ),
86
+ color: Colors .black.withValues (alpha : 0.2 ),
82
87
blurRadius: 4 ,
83
88
offset: const Offset (0 , 2 ),
84
89
),
@@ -105,32 +110,46 @@ class SettingsPage extends StatelessWidget {
105
110
106
111
static AccountRoute <void > buildRoute ({required BuildContext context}) {
107
112
return MaterialAccountWidgetRoute (
108
- context: context, page: const SettingsPage ());
113
+ context: context,
114
+ page: const SettingsPage (),
115
+ );
109
116
}
110
117
111
118
@override
112
119
Widget build (BuildContext context) {
113
120
final zulipLocalizations = ZulipLocalizations .of (context);
114
121
return Scaffold (
115
122
appBar: ZulipAppBar (
116
- title: Text (zulipLocalizations.settingsPageTitle)),
117
- body: Column (children: [
118
- const _ThemeSetting (),
119
- const _BrowserPreferenceSetting (),
120
- const _VisitFirstUnreadSetting (),
121
- const _MarkReadOnScrollSetting (),
122
- if (GlobalSettingsStore .experimentalFeatureFlags.isNotEmpty)
123
- ListTile (
124
- title: Text (zulipLocalizations.experimentalFeatureSettingsPageTitle),
125
- onTap: () => Navigator .push (context,
126
- ExperimentalFeaturesPage .buildRoute ()))
127
- ]));
123
+ title: Text (zulipLocalizations.settingsPageTitle),
124
+ ),
125
+ body: Column (
126
+ children: [
127
+ const _ThemeSetting (),
128
+ const _BrowserPreferenceSetting (),
129
+ const _VisitFirstUnreadSetting (),
130
+ const _MarkReadOnScrollSetting (),
131
+ if (GlobalSettingsStore .experimentalFeatureFlags.isNotEmpty)
132
+ ListTile (
133
+ title: Text (zulipLocalizations.experimentalFeatureSettingsPageTitle),
134
+ onTap: () => Navigator .push (
135
+ context,
136
+ ExperimentalFeaturesPage .buildRoute (),
137
+ ),
138
+ ),
139
+ ],
140
+ ),
141
+ );
128
142
}
129
143
}
130
144
131
- class _ThemeSetting extends StatelessWidget {
145
+ class _ThemeSetting extends StatefulWidget {
132
146
const _ThemeSetting ();
133
147
148
+ @override
149
+ State <_ThemeSetting > createState () => _ThemeSettingState ();
150
+ }
151
+
152
+ class _ThemeSettingState extends State <_ThemeSetting > {
134
153
void _handleChange (BuildContext context, ThemeSetting ? newThemeSetting) {
135
154
final globalSettings = GlobalStoreWidget .settingsOf (context);
136
155
globalSettings.setThemeSetting (newThemeSetting);
@@ -143,18 +162,24 @@ class _ThemeSetting extends StatelessWidget {
143
162
return Column (
144
163
children: [
145
164
ListTile (title: Text (zulipLocalizations.themeSettingTitle)),
146
- for (final themeSettingOption in [null , ...ThemeSetting .values])
147
- RadioListTile <ThemeSetting ?>.adaptive (
148
- title: Text (ThemeSetting .displayName (
149
- themeSetting: themeSettingOption,
150
- zulipLocalizations: zulipLocalizations)),
151
- value: themeSettingOption,
152
- // TODO(#1545) stop using the deprecated members
153
- // ignore: deprecated_member_use
154
- groupValue: globalSettings.themeSetting,
155
- // ignore: deprecated_member_use
156
- onChanged: (newValue) => _handleChange (context, newValue)),
157
- ]);
165
+ RadioGroup <ThemeSetting ?>(
166
+ groupValue: globalSettings.themeSetting,
167
+ onChanged: (newValue) => _handleChange (context, newValue),
168
+ child: Column (
169
+ children: [
170
+ for (final themeSettingOption in [null , ...ThemeSetting .values])
171
+ RadioListTile <ThemeSetting ?>(
172
+ title: Text (ThemeSetting .displayName (
173
+ themeSetting: themeSettingOption,
174
+ zulipLocalizations: zulipLocalizations,
175
+ )),
176
+ value: themeSettingOption,
177
+ ),
178
+ ],
179
+ ),
180
+ ),
181
+ ],
182
+ );
158
183
}
159
184
}
160
185
@@ -165,7 +190,8 @@ class _BrowserPreferenceSetting extends StatelessWidget {
165
190
final globalSettings = GlobalStoreWidget .settingsOf (context);
166
191
globalSettings.setBrowserPreference (
167
192
newOpenLinksWithInAppBrowser ? BrowserPreference .inApp
168
- : BrowserPreference .external );
193
+ : BrowserPreference .external ,
194
+ );
169
195
}
170
196
171
197
@override
@@ -194,9 +220,14 @@ class _VisitFirstUnreadSetting extends StatelessWidget {
194
220
return ListTile (
195
221
title: Text (zulipLocalizations.initialAnchorSettingTitle),
196
222
subtitle: Text (VisitFirstUnreadSettingPage ._valueDisplayName (
197
- globalSettings.visitFirstUnread, zulipLocalizations: zulipLocalizations)),
198
- onTap: () => Navigator .push (context,
199
- VisitFirstUnreadSettingPage .buildRoute ()));
223
+ globalSettings.visitFirstUnread,
224
+ zulipLocalizations: zulipLocalizations,
225
+ )),
226
+ onTap: () => Navigator .push (
227
+ context,
228
+ VisitFirstUnreadSettingPage .buildRoute (),
229
+ ),
230
+ );
200
231
}
201
232
}
202
233
@@ -207,7 +238,8 @@ class VisitFirstUnreadSettingPage extends StatelessWidget {
207
238
return MaterialWidgetRoute (page: const VisitFirstUnreadSettingPage ());
208
239
}
209
240
210
- static String _valueDisplayName (VisitFirstUnreadSetting value, {
241
+ static String _valueDisplayName (
242
+ VisitFirstUnreadSetting value, {
211
243
required ZulipLocalizations zulipLocalizations,
212
244
}) {
213
245
return switch (value) {
@@ -221,7 +253,7 @@ class VisitFirstUnreadSettingPage extends StatelessWidget {
221
253
}
222
254
223
255
void _handleChange (BuildContext context, VisitFirstUnreadSetting ? value) {
224
- if (value == null ) return ; // TODO(log); can this actually happen? how?
256
+ if (value == null ) return ;
225
257
final globalSettings = GlobalStoreWidget .settingsOf (context);
226
258
globalSettings.setVisitFirstUnread (value);
227
259
}
@@ -232,19 +264,28 @@ class VisitFirstUnreadSettingPage extends StatelessWidget {
232
264
final globalSettings = GlobalStoreWidget .settingsOf (context);
233
265
return Scaffold (
234
266
appBar: AppBar (title: Text (zulipLocalizations.initialAnchorSettingTitle)),
235
- body: Column (children: [
236
- ListTile (title: Text (zulipLocalizations.initialAnchorSettingDescription)),
237
- for (final value in VisitFirstUnreadSetting .values)
238
- RadioListTile .adaptive (
239
- title: Text (_valueDisplayName (value,
240
- zulipLocalizations: zulipLocalizations)),
241
- value: value,
242
- // TODO(#1545) stop using the deprecated members
243
- // ignore: deprecated_member_use
267
+ body: Column (
268
+ children: [
269
+ ListTile (title: Text (zulipLocalizations.initialAnchorSettingDescription)),
270
+ RadioGroup <VisitFirstUnreadSetting >(
244
271
groupValue: globalSettings.visitFirstUnread,
245
- // ignore: deprecated_member_use
246
- onChanged: (newValue) => _handleChange (context, newValue)),
247
- ]));
272
+ onChanged: (newValue) => _handleChange (context, newValue),
273
+ child: Column (
274
+ children: [
275
+ for (final value in VisitFirstUnreadSetting .values)
276
+ RadioListTile .adaptive (
277
+ title: Text (_valueDisplayName (
278
+ value,
279
+ zulipLocalizations: zulipLocalizations,
280
+ )),
281
+ value: value,
282
+ ),
283
+ ],
284
+ ),
285
+ ),
286
+ ],
287
+ ),
288
+ );
248
289
}
249
290
}
250
291
@@ -258,9 +299,14 @@ class _MarkReadOnScrollSetting extends StatelessWidget {
258
299
return ListTile (
259
300
title: Text (zulipLocalizations.markReadOnScrollSettingTitle),
260
301
subtitle: Text (MarkReadOnScrollSettingPage ._valueDisplayName (
261
- globalSettings.markReadOnScroll, zulipLocalizations: zulipLocalizations)),
262
- onTap: () => Navigator .push (context,
263
- MarkReadOnScrollSettingPage .buildRoute ()));
302
+ globalSettings.markReadOnScroll,
303
+ zulipLocalizations: zulipLocalizations,
304
+ )),
305
+ onTap: () => Navigator .push (
306
+ context,
307
+ MarkReadOnScrollSettingPage .buildRoute (),
308
+ ),
309
+ );
264
310
}
265
311
}
266
312
@@ -271,7 +317,8 @@ class MarkReadOnScrollSettingPage extends StatelessWidget {
271
317
return MaterialWidgetRoute (page: const MarkReadOnScrollSettingPage ());
272
318
}
273
319
274
- static String _valueDisplayName (MarkReadOnScrollSetting value, {
320
+ static String _valueDisplayName (
321
+ MarkReadOnScrollSetting value, {
275
322
required ZulipLocalizations zulipLocalizations,
276
323
}) {
277
324
return switch (value) {
@@ -284,7 +331,8 @@ class MarkReadOnScrollSettingPage extends StatelessWidget {
284
331
};
285
332
}
286
333
287
- static String ? _valueDescription (MarkReadOnScrollSetting value, {
334
+ static String ? _valueDescription (
335
+ MarkReadOnScrollSetting value, {
288
336
required ZulipLocalizations zulipLocalizations,
289
337
}) {
290
338
return switch (value) {
@@ -296,7 +344,7 @@ class MarkReadOnScrollSettingPage extends StatelessWidget {
296
344
}
297
345
298
346
void _handleChange (BuildContext context, MarkReadOnScrollSetting ? value) {
299
- if (value == null ) return ; // TODO(log); can this actually happen? how?
347
+ if (value == null ) return ;
300
348
final globalSettings = GlobalStoreWidget .settingsOf (context);
301
349
globalSettings.setMarkReadOnScroll (value);
302
350
}
@@ -307,24 +355,35 @@ class MarkReadOnScrollSettingPage extends StatelessWidget {
307
355
final globalSettings = GlobalStoreWidget .settingsOf (context);
308
356
return Scaffold (
309
357
appBar: AppBar (title: Text (zulipLocalizations.markReadOnScrollSettingTitle)),
310
- body: Column (children: [
311
- ListTile (title: Text (zulipLocalizations.markReadOnScrollSettingDescription)),
312
- for (final value in MarkReadOnScrollSetting .values)
313
- RadioListTile .adaptive (
314
- title: Text (_valueDisplayName (value,
315
- zulipLocalizations: zulipLocalizations)),
316
- subtitle: () {
317
- final result = _valueDescription (value,
318
- zulipLocalizations: zulipLocalizations);
319
- return result == null ? null : Text (result);
320
- }(),
321
- value: value,
322
- // TODO(#1545) stop using the deprecated members
323
- // ignore: deprecated_member_use
358
+ body: Column (
359
+ children: [
360
+ ListTile (title: Text (zulipLocalizations.markReadOnScrollSettingDescription)),
361
+ RadioGroup <MarkReadOnScrollSetting >(
324
362
groupValue: globalSettings.markReadOnScroll,
325
- // ignore: deprecated_member_use
326
- onChanged: (newValue) => _handleChange (context, newValue)),
327
- ]));
363
+ onChanged: (newValue) => _handleChange (context, newValue),
364
+ child: Column (
365
+ children: [
366
+ for (final value in MarkReadOnScrollSetting .values)
367
+ RadioListTile .adaptive (
368
+ title: Text (_valueDisplayName (
369
+ value,
370
+ zulipLocalizations: zulipLocalizations,
371
+ )),
372
+ subtitle: () {
373
+ final result = _valueDescription (
374
+ value,
375
+ zulipLocalizations: zulipLocalizations,
376
+ );
377
+ return result == null ? null : Text (result);
378
+ }(),
379
+ value: value,
380
+ ),
381
+ ],
382
+ ),
383
+ ),
384
+ ],
385
+ ),
386
+ );
328
387
}
329
388
}
330
389
@@ -343,18 +402,23 @@ class ExperimentalFeaturesPage extends StatelessWidget {
343
402
assert (flags.isNotEmpty);
344
403
return Scaffold (
345
404
appBar: AppBar (
346
- title: Text (zulipLocalizations.experimentalFeatureSettingsPageTitle)),
347
- body: Column (children: [
348
- ListTile (
349
- title: Text (zulipLocalizations.experimentalFeatureSettingsWarning)),
350
- for (final flag in flags)
405
+ title: Text (zulipLocalizations.experimentalFeatureSettingsPageTitle),
406
+ ),
407
+ body: Column (
408
+ children: [
351
409
ListTile (
352
- title: Text (flag.name), // no i18n; these are developer-facing settings
353
- trailing: FigmaToggle (
354
- value: globalSettings.getBool (flag),
355
- onChanged: (value) => globalSettings.setBool (flag, value),
356
- ),
410
+ title: Text (zulipLocalizations.experimentalFeatureSettingsWarning),
357
411
),
358
- ]));
412
+ for (final flag in flags)
413
+ ListTile (
414
+ title: Text (flag.name), // no i18n; these are developer-facing settings
415
+ trailing: FigmaToggle (
416
+ value: globalSettings.getBool (flag),
417
+ onChanged: (value) => globalSettings.setBool (flag, value),
418
+ ),
419
+ ),
420
+ ],
421
+ ),
422
+ );
359
423
}
360
424
}
0 commit comments