11import 'package:flutter/material.dart' ;
2+ import 'package:flutter/scheduler.dart' ;
23import 'package:flutter/services.dart' ;
34import 'package:flutter_gen/gen_l10n/zulip_localizations.dart' ;
45import 'package:intl/intl.dart' ;
@@ -91,12 +92,17 @@ class _LightboxPageLayout extends StatefulWidget {
9192 const _LightboxPageLayout ({
9293 required this .routeEntranceAnimation,
9394 required this .message,
95+ required this .buildAppBarBottom,
9496 required this .buildBottomAppBar,
9597 required this .child,
9698 });
9799
98100 final Animation <double > routeEntranceAnimation;
99101 final Message message;
102+
103+ /// For [AppBar.bottom] .
104+ final PreferredSizeWidget ? Function (BuildContext context) buildAppBarBottom;
105+
100106 final Widget ? Function (
101107 BuildContext context, Color color, double elevation) buildBottomAppBar;
102108 final Widget child;
@@ -171,7 +177,8 @@ class _LightboxPageLayoutState extends State<_LightboxPageLayout> {
171177
172178 // Make smaller, like a subtitle
173179 style: themeData.textTheme.titleSmall! .copyWith (color: appBarForegroundColor)),
174- ])));
180+ ])),
181+ bottom: widget.buildAppBarBottom (context));
175182 }
176183
177184 Widget ? bottomAppBar;
@@ -209,17 +216,30 @@ class _ImageLightboxPage extends StatefulWidget {
209216 required this .routeEntranceAnimation,
210217 required this .message,
211218 required this .src,
219+ required this .thumbnailUrl,
212220 });
213221
214222 final Animation <double > routeEntranceAnimation;
215223 final Message message;
216224 final Uri src;
225+ final Uri ? thumbnailUrl;
217226
218227 @override
219228 State <_ImageLightboxPage > createState () => _ImageLightboxPageState ();
220229}
221230
222231class _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+
223243 Widget _buildBottomAppBar (BuildContext context, Color color, double elevation) {
224244 return BottomAppBar (
225245 color: color,
@@ -232,19 +252,54 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> {
232252 );
233253 }
234254
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+
235282 @override
236283 Widget build (BuildContext context) {
237284 return _LightboxPageLayout (
238285 routeEntranceAnimation: widget.routeEntranceAnimation,
239286 message: widget.message,
287+ buildAppBarBottom: _buildAppBarBottom,
240288 buildBottomAppBar: _buildBottomAppBar,
241289 child: SizedBox .expand (
242290 child: InteractiveViewer (
243291 child: SafeArea (
244292 child: LightboxHero (
245293 message: widget.message,
246294 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+ ));
248303 }
249304}
250305
@@ -457,6 +512,7 @@ class _VideoLightboxPageState extends State<VideoLightboxPage> with PerAccountSt
457512 return _LightboxPageLayout (
458513 routeEntranceAnimation: widget.routeEntranceAnimation,
459514 message: widget.message,
515+ buildAppBarBottom: (context) => null ,
460516 buildBottomAppBar: _buildBottomAppBar,
461517 child: SafeArea (
462518 child: Center (
@@ -484,6 +540,7 @@ Route<void> getLightboxRoute({
484540 BuildContext ? context,
485541 required Message message,
486542 required Uri src,
543+ required Uri ? thumbnailUrl,
487544 required MediaType mediaType,
488545}) {
489546 return AccountPageRouteBuilder (
@@ -500,7 +557,8 @@ Route<void> getLightboxRoute({
500557 MediaType .image => _ImageLightboxPage (
501558 routeEntranceAnimation: animation,
502559 message: message,
503- src: src),
560+ src: src,
561+ thumbnailUrl: thumbnailUrl),
504562 MediaType .video => VideoLightboxPage (
505563 routeEntranceAnimation: animation,
506564 message: message,
0 commit comments