1
1
import 'package:flutter/material.dart' ;
2
+ import 'package:flutter/scheduler.dart' ;
2
3
import 'package:flutter/services.dart' ;
3
4
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart' ;
4
5
import 'package:intl/intl.dart' ;
@@ -91,12 +92,17 @@ class _LightboxPageLayout extends StatefulWidget {
91
92
const _LightboxPageLayout ({
92
93
required this .routeEntranceAnimation,
93
94
required this .message,
95
+ required this .buildAppBarBottom,
94
96
required this .buildBottomAppBar,
95
97
required this .child,
96
98
});
97
99
98
100
final Animation <double > routeEntranceAnimation;
99
101
final Message message;
102
+
103
+ /// For [AppBar.bottom] .
104
+ final PreferredSizeWidget ? Function (BuildContext context) buildAppBarBottom;
105
+
100
106
final Widget ? Function (
101
107
BuildContext context, Color color, double elevation) buildBottomAppBar;
102
108
final Widget child;
@@ -171,7 +177,8 @@ class _LightboxPageLayoutState extends State<_LightboxPageLayout> {
171
177
172
178
// Make smaller, like a subtitle
173
179
style: themeData.textTheme.titleSmall! .copyWith (color: appBarForegroundColor)),
174
- ])));
180
+ ])),
181
+ bottom: widget.buildAppBarBottom (context));
175
182
}
176
183
177
184
Widget ? bottomAppBar;
@@ -209,17 +216,30 @@ class _ImageLightboxPage extends StatefulWidget {
209
216
required this .routeEntranceAnimation,
210
217
required this .message,
211
218
required this .src,
219
+ required this .thumbnailUrl,
212
220
});
213
221
214
222
final Animation <double > routeEntranceAnimation;
215
223
final Message message;
216
224
final Uri src;
225
+ final Uri ? thumbnailUrl;
217
226
218
227
@override
219
228
State <_ImageLightboxPage > createState () => _ImageLightboxPageState ();
220
229
}
221
230
222
231
class _ImageLightboxPageState extends State <_ImageLightboxPage > {
232
+ double ? _loadingProgress;
233
+
234
+ PreferredSizeWidget ? _buildAppBarBottom (BuildContext context) {
235
+ if (_loadingProgress == null ) {
236
+ return null ;
237
+ }
238
+ return PreferredSize (
239
+ preferredSize: const Size .fromHeight (4.0 ),
240
+ child: LinearProgressIndicator (minHeight: 4.0 , value: _loadingProgress));
241
+ }
242
+
223
243
Widget _buildBottomAppBar (BuildContext context, Color color, double elevation) {
224
244
return BottomAppBar (
225
245
color: color,
@@ -232,19 +252,54 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> {
232
252
);
233
253
}
234
254
255
+ Widget _frameBuilder (BuildContext context, Widget child, int ? frame, bool wasSynchronouslyLoaded) {
256
+ if (widget.thumbnailUrl == null ) return child;
257
+
258
+ // The full image is available, so display it.
259
+ if (frame != null ) return child;
260
+
261
+ // Display the thumbnail image while original image is downloading.
262
+ return RealmContentNetworkImage (widget.thumbnailUrl! ,
263
+ filterQuality: FilterQuality .medium);
264
+ }
265
+
266
+ Widget _loadingBuilder (BuildContext context, Widget child, ImageChunkEvent ? loadingProgress) {
267
+ if (widget.thumbnailUrl == null ) return child;
268
+
269
+ // `loadingProgress` becomes null when Image has finished downloading.
270
+ final double ? progress = loadingProgress? .expectedTotalBytes == null ? null
271
+ : loadingProgress! .cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! ;
272
+
273
+ if (progress != _loadingProgress) {
274
+ _loadingProgress = progress;
275
+ // This function is called in a build method and setState
276
+ // can't be called in a build method, so delay it.
277
+ SchedulerBinding .instance.scheduleFrameCallback ((_) { if (mounted) setState (() {}); });
278
+ }
279
+ return child;
280
+ }
281
+
235
282
@override
236
283
Widget build (BuildContext context) {
237
284
return _LightboxPageLayout (
238
285
routeEntranceAnimation: widget.routeEntranceAnimation,
239
286
message: widget.message,
287
+ buildAppBarBottom: _buildAppBarBottom,
240
288
buildBottomAppBar: _buildBottomAppBar,
241
289
child: SizedBox .expand (
242
290
child: InteractiveViewer (
243
291
child: SafeArea (
244
292
child: LightboxHero (
245
293
message: widget.message,
246
294
src: widget.src,
247
- child: RealmContentNetworkImage (widget.src, filterQuality: FilterQuality .medium))))));
295
+ child: RealmContentNetworkImage (widget.src,
296
+ filterQuality: FilterQuality .medium,
297
+ frameBuilder: _frameBuilder,
298
+ loadingBuilder: _loadingBuilder),
299
+ ),
300
+ ),
301
+ ),
302
+ ));
248
303
}
249
304
}
250
305
@@ -457,6 +512,7 @@ class _VideoLightboxPageState extends State<VideoLightboxPage> with PerAccountSt
457
512
return _LightboxPageLayout (
458
513
routeEntranceAnimation: widget.routeEntranceAnimation,
459
514
message: widget.message,
515
+ buildAppBarBottom: (context) => null ,
460
516
buildBottomAppBar: _buildBottomAppBar,
461
517
child: SafeArea (
462
518
child: Center (
@@ -484,6 +540,7 @@ Route<void> getLightboxRoute({
484
540
BuildContext ? context,
485
541
required Message message,
486
542
required Uri src,
543
+ required Uri ? thumbnailUrl,
487
544
required MediaType mediaType,
488
545
}) {
489
546
return AccountPageRouteBuilder (
@@ -500,7 +557,8 @@ Route<void> getLightboxRoute({
500
557
MediaType .image => _ImageLightboxPage (
501
558
routeEntranceAnimation: animation,
502
559
message: message,
503
- src: src),
560
+ src: src,
561
+ thumbnailUrl: thumbnailUrl),
504
562
MediaType .video => VideoLightboxPage (
505
563
routeEntranceAnimation: animation,
506
564
message: message,
0 commit comments