1
+
2
+ import 'dart:async' ;
3
+
1
4
import 'package:flutter/material.dart' ;
2
5
6
+ import '../generated/l10n/zulip_localizations.dart' ;
7
+ import '../model/binding.dart' ;
8
+ import 'action_sheet.dart' ;
9
+ import 'app.dart' ;
3
10
import 'icons.dart' ;
4
11
import 'inbox.dart' ;
12
+ import 'inset_shadow.dart' ;
5
13
import 'page.dart' ;
6
14
import 'recent_dm_conversations.dart' ;
15
+ import 'store.dart' ;
7
16
import 'subscription_list.dart' ;
17
+ import 'text.dart' ;
8
18
import 'theme.dart' ;
9
19
10
20
class HomePage extends StatefulWidget {
@@ -29,7 +39,6 @@ class _HomePageState extends State<HomePage> {
29
39
SubscriptionListPageBody (),
30
40
// Users
31
41
RecentDmConversationsPageBody (),
32
- // Menu
33
42
];
34
43
35
44
Widget Function (int pageIndex) navigationButtonBuilder (IconData icon, String tooltip) {
@@ -48,7 +57,6 @@ class _HomePageState extends State<HomePage> {
48
57
navigationButtonBuilder (ZulipIcons .hash_italic, 'Channels' ),
49
58
// navigationButtonBuilder(ZulipIcons.contacts, 'Users'),
50
59
navigationButtonBuilder (ZulipIcons .user, 'Direct messages' ),
51
- // navigationButtonBuilder(ZulipIcons.menu, 'Menu'),
52
60
];
53
61
54
62
final designVariables = DesignVariables .of (context);
@@ -73,10 +81,267 @@ class _HomePageState extends State<HomePage> {
73
81
children: [
74
82
for (final (pageIndex, buildButton) in navigationButtonBuilders.indexed)
75
83
Expanded (child: buildButton (pageIndex)),
84
+ Expanded (
85
+ child: NavigationButton (
86
+ icon: ZulipIcons .menu, tooltip: 'Menu' , selected: false ,
87
+ onPressed: () => showMenu (context))),
76
88
])))));
77
89
}
78
90
}
79
91
92
+ void showMenu (BuildContext context) {
93
+ final store = PerAccountStoreWidget .of (context);
94
+ final designVariables = DesignVariables .of (context);
95
+ final menuItems = < Widget > [
96
+ // Search
97
+ // const SizedBox(height: 8),
98
+ _MenuButton (selected: false , pageContext: context),
99
+ _MenuButton (selected: true , pageContext: context),
100
+ _MenuButton (selected: false , pageContext: context),
101
+ _MenuButton (selected: false , pageContext: context),
102
+ _MenuButton (selected: false , pageContext: context),
103
+ _MenuButton (selected: false , pageContext: context),
104
+ _MenuButton (selected: false , pageContext: context),
105
+ _MenuButton (selected: false , pageContext: context),
106
+ _MenuButton (selected: false , pageContext: context),
107
+ _MenuButton (selected: false , pageContext: context),
108
+ _MenuButton (selected: false , pageContext: context),
109
+ _MenuButton (selected: false , pageContext: context),
110
+ _MenuButton (selected: false , pageContext: context),
111
+ _MenuButton (selected: false , pageContext: context),
112
+ _MenuButton (selected: false , pageContext: context),
113
+ _MenuButton (selected: false , pageContext: context),
114
+ // Inbox
115
+ // Recent conversations
116
+ // Mentions
117
+ // Starred messages
118
+ // Drafts
119
+ // Direct messages
120
+ // Streams
121
+ // Users
122
+ // My profile
123
+ // Set my status
124
+ // const SizedBox(height: 8),
125
+ // Settings
126
+ // Notifications
127
+ const SizedBox (height: 8 ),
128
+ const _VersionInfo (),
129
+ ];
130
+
131
+ showModalBottomSheet <void >(
132
+ context: context,
133
+ // Clip.hardEdge looks bad; Clip.antiAliasWithSaveLayer looks pixel-perfect
134
+ // on my iPhone 13 Pro but is marked as "much slower":
135
+ // https://api.flutter.dev/flutter/dart-ui/Clip.html
136
+ clipBehavior: Clip .antiAlias,
137
+ useSafeArea: true ,
138
+ isScrollControlled: true ,
139
+ backgroundColor: designVariables.bgBotBar,
140
+ builder: (BuildContext _) {
141
+ return SafeArea (
142
+ minimum: const EdgeInsets .only (bottom: 8 ),
143
+ child: Column (
144
+ crossAxisAlignment: CrossAxisAlignment .stretch,
145
+ mainAxisSize: MainAxisSize .max,
146
+ children: [
147
+ _MenuHeading (title: store.realmUrl.toString ()),
148
+ Expanded (child: InsetShadowBox (
149
+ top: 8 , bottom: 8 ,
150
+ color: designVariables.bgBotBar,
151
+ child: SingleChildScrollView (
152
+ padding: const EdgeInsets .symmetric (vertical: 8 , horizontal: 8 ),
153
+ child: Column (
154
+ mainAxisAlignment: MainAxisAlignment .start,
155
+ children: menuItems)))),
156
+ const Padding (
157
+ padding: EdgeInsets .symmetric (horizontal: 16 ),
158
+ child: Scaled (
159
+ scaleEnd: 0.95 ,
160
+ duration: Duration (milliseconds: 100 ),
161
+ child: SizedBox (height: 44 , child: ActionSheetCancelButton ()))),
162
+ ]));
163
+ });
164
+ }
165
+
166
+ class _MenuHeading extends StatelessWidget {
167
+ const _MenuHeading ({required this .title});
168
+
169
+ final String title;
170
+
171
+ @override
172
+ Widget build (BuildContext context) {
173
+ final designVariables = DesignVariables .of (context);
174
+
175
+ return ConstrainedBox (
176
+ constraints: const BoxConstraints (minHeight: 46 ),
177
+ child: Padding (
178
+ padding: const EdgeInsets .only (top: 6 ),
179
+ child: Row (children: [
180
+ // TODO: fetch organization icon and display name from the server settings
181
+ Expanded (
182
+ child: Padding (
183
+ padding: const EdgeInsetsDirectional .fromSTEB (12 , 6 , 4 , 6 ),
184
+ child: Row (spacing: 8 , children: [
185
+ Icon (ZulipIcons .smile, size: 28 , color: designVariables.iconSelected),
186
+ Expanded (child: Text (title,
187
+ overflow: TextOverflow .ellipsis,
188
+ style: TextStyle (
189
+ color: designVariables.title,
190
+ fontSize: 20 ,
191
+ height: 24 / 20 ,
192
+ ).merge (weightVariableTextStyle (context, wght: 600 )))),
193
+ ]))),
194
+ ConstrainedBox (constraints: const BoxConstraints (minHeight: 40 ),
195
+ child: const _OrganizationsButton ()),
196
+ ])));
197
+ }
198
+ }
199
+
200
+ class _OrganizationsButton extends StatelessWidget {
201
+ const _OrganizationsButton ();
202
+
203
+ @override
204
+ Widget build (BuildContext context) {
205
+ final designVariables = DesignVariables .of (context);
206
+ return TextButton (onPressed: () =>
207
+ unawaited (Navigator .pushReplacement (
208
+ context, MaterialWidgetRoute (page: const ChooseAccountPage ()))),
209
+ style: TextButton .styleFrom (
210
+ // Placeholder color
211
+ foregroundColor: designVariables.contextMenuCancelText,
212
+ shape: RoundedRectangleBorder (borderRadius: BorderRadius .circular (10 )),
213
+ splashFactory: NoSplash .splashFactory,
214
+ padding: const EdgeInsetsDirectional .fromSTEB (8 , 7 , 14 , 7 )),
215
+ child: Text ('Organizations' , style: TextStyle (
216
+ color: designVariables.icon,
217
+ fontSize: 19 ,
218
+ height: 26 / 19 ,
219
+ ).merge (weightVariableTextStyle (context, wght: 600 ))));
220
+ }
221
+ }
222
+
223
+
224
+ class _MenuButton extends ActionSheetMenuItemButton {
225
+ const _MenuButton ({required this .selected, required super .pageContext});
226
+
227
+ final bool selected;
228
+
229
+ @override
230
+ // TODO: implement icon
231
+ IconData get icon => ZulipIcons .attach_file;
232
+
233
+ @override
234
+ String label (ZulipLocalizations zulipLocalizations) {
235
+ return 'Attach file' ;
236
+ }
237
+
238
+ @override
239
+ void onPressed () {
240
+ // TODO: implement onPressed
241
+ }
242
+
243
+ @override
244
+ Widget build (BuildContext context) {
245
+ final designVariables = DesignVariables .of (context);
246
+ final zulipLocalizations = ZulipLocalizations .of (context);
247
+
248
+ final borderSide = BorderSide (width: 1 ,
249
+ strokeAlign: BorderSide .strokeAlignOutside,
250
+ color: designVariables.borderMenuButtonSelected);
251
+ final buttonStyle = TextButton .styleFrom (
252
+ padding: const EdgeInsets .symmetric (vertical: 9 , horizontal: 8 ),
253
+ foregroundColor: designVariables.labelMenuButton,
254
+ splashFactory: NoSplash .splashFactory,
255
+ shape: RoundedRectangleBorder (borderRadius: BorderRadius .circular (10 )),
256
+ ).copyWith (
257
+ backgroundColor: WidgetStateColor .fromMap ({
258
+ WidgetState .pressed: designVariables.bgMenuButtonActive,
259
+ ~ WidgetState .pressed: (selected) ? designVariables.bgMenuButtonSelected
260
+ : Colors .transparent,
261
+ }),
262
+ side: WidgetStateBorderSide .fromMap ({
263
+ WidgetState .pressed: null ,
264
+ ~ WidgetState .pressed: (selected) ? borderSide : null ,
265
+ }));
266
+
267
+ return Scaled (
268
+ duration: const Duration (milliseconds: 100 ),
269
+ scaleEnd: 0.95 ,
270
+ child: SizedBox (height: 44 ,
271
+ child: TextButton (
272
+ onPressed: onPressed,
273
+ style: buttonStyle,
274
+ child: Row (spacing: 8 , children: [
275
+ Icon (icon, size: 24 ,
276
+ color: (selected) ? designVariables.iconSelected
277
+ : designVariables.icon),
278
+ Expanded (child: Text (label (zulipLocalizations),
279
+ textAlign: TextAlign .start,
280
+ style: const TextStyle (fontSize: 19 , height: 26 / 19 )
281
+ .merge (weightVariableTextStyle (context, wght: (selected) ? 600 : 400 )))),
282
+ const _MenuItemCounter (value: 100 , variant: _MenuItemCounterVariant .quantity),
283
+ ]))));
284
+ }
285
+ }
286
+
287
+ enum _MenuItemCounterVariant {
288
+ unreads,
289
+ quantity,
290
+ }
291
+
292
+ class _MenuItemCounter extends StatelessWidget {
293
+ const _MenuItemCounter ({required this .value, required this .variant});
294
+
295
+ final int value;
296
+ final _MenuItemCounterVariant variant;
297
+
298
+ @override
299
+ Widget build (BuildContext context) {
300
+ final designVariables = DesignVariables .of (context);
301
+ final Color bgColor, textColor;
302
+ switch (variant) {
303
+ case _MenuItemCounterVariant .unreads:
304
+ bgColor = designVariables.bgCounterUnread;
305
+ textColor = designVariables.labelCounterUnread;
306
+ case _MenuItemCounterVariant .quantity:
307
+ bgColor = Colors .transparent;
308
+ textColor = designVariables.labelCounterQuantity;
309
+ }
310
+ return Container (height: 24 ,
311
+ padding: const EdgeInsets .fromLTRB (6 , 1 , 6 , 2 ),
312
+ decoration: ShapeDecoration (color: bgColor,
313
+ shape: RoundedRectangleBorder (borderRadius: BorderRadius .circular (5 ))),
314
+ child: Text (value.toString (), style: TextStyle (
315
+ fontSize: 18 ,
316
+ height: 21 / 18 ,
317
+ color: textColor,
318
+ ).merge (weightVariableTextStyle (context, wght: 600 ))));
319
+ }
320
+ }
321
+
322
+ class _VersionInfo extends StatelessWidget {
323
+ const _VersionInfo ();
324
+
325
+ @override
326
+ Widget build (BuildContext context) {
327
+ final designVariables = DesignVariables .of (context);
328
+ final packageInfo = ZulipBinding .instance.syncPackageInfo;
329
+ final String versionString;
330
+ if (packageInfo == null ) {
331
+ versionString = 'App Version unknown' ;
332
+ } else {
333
+ final PackageInfo (: version, : buildNumber) = packageInfo;
334
+ versionString = 'App Version $version +$buildNumber ' ;
335
+ }
336
+ return Padding (
337
+ padding: const EdgeInsets .symmetric (horizontal: 16 , vertical: 6 ),
338
+ child: Text (versionString, style: TextStyle (
339
+ fontSize: 17 ,
340
+ height: 24 / 17 ,
341
+ color: designVariables.labelSearchPrompt)));
342
+ }
343
+ }
344
+
80
345
class Scaled extends StatefulWidget {
81
346
const Scaled ({
82
347
super .key,
0 commit comments