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,33 @@ 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+ // return const PreferredSize(
236+ // preferredSize: Size.fromHeight(4.0),
237+ // child: LinearProgressIndicator(minHeight: 4.0));
238+ if (_loadingProgress == null ) {
239+ return null ;
240+ }
241+ return PreferredSize (
242+ preferredSize: const Size .fromHeight (4.0 ),
243+ child: LinearProgressIndicator (minHeight: 4.0 , value: _loadingProgress));
244+ }
245+
223246 Widget _buildBottomAppBar (BuildContext context, Color color, double elevation) {
224247 return BottomAppBar (
225248 color: color,
@@ -232,19 +255,60 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> {
232255 );
233256 }
234257
258+ Widget _frameBuilder (BuildContext context, Widget child, int ? frame, bool wasSynchronouslyLoaded) {
259+ if (widget.thumbnailUrl == null ) {
260+ return child;
261+ }
262+ if (frame != null ) {
263+ // Image was already available or has finished downloading and
264+ // so it is now available.
265+ return child;
266+ }
267+ return RealmContentNetworkImage (widget.thumbnailUrl! ,
268+ filterQuality: FilterQuality .medium);
269+ }
270+
271+ Widget _loadingBuilder (BuildContext context, Widget child, ImageChunkEvent ? loadingProgress) {
272+ if (widget.thumbnailUrl == null ) {
273+ return child;
274+ }
275+ // `loadingProgress` becomes null when Image has finished downloading.
276+ final double ? progress;
277+ if (loadingProgress? .expectedTotalBytes == null ) {
278+ progress = null ;
279+ } else {
280+ progress = loadingProgress! .cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! ;
281+ }
282+ if (progress != _loadingProgress) {
283+ _loadingProgress = progress;
284+ // This function is called in a build method and setState
285+ // can't be called in a build method, so delay it.
286+ SchedulerBinding .instance.scheduleFrameCallback ((_) { if (mounted) setState (() {}); });
287+ }
288+ return child;
289+ }
290+
235291 @override
236292 Widget build (BuildContext context) {
237293 return _LightboxPageLayout (
238294 routeEntranceAnimation: widget.routeEntranceAnimation,
239295 message: widget.message,
296+ buildAppBarBottom: _buildAppBarBottom,
240297 buildBottomAppBar: _buildBottomAppBar,
241298 child: SizedBox .expand (
242299 child: InteractiveViewer (
243300 child: SafeArea (
244301 child: LightboxHero (
245302 message: widget.message,
246303 src: widget.src,
247- child: RealmContentNetworkImage (widget.src, filterQuality: FilterQuality .medium))))));
304+ child: RealmContentNetworkImage (widget.src,
305+ filterQuality: FilterQuality .medium,
306+ frameBuilder: _frameBuilder,
307+ loadingBuilder: _loadingBuilder),
308+ ),
309+ ),
310+ ),
311+ ));
248312 }
249313}
250314
@@ -457,6 +521,7 @@ class _VideoLightboxPageState extends State<VideoLightboxPage> with PerAccountSt
457521 return _LightboxPageLayout (
458522 routeEntranceAnimation: widget.routeEntranceAnimation,
459523 message: widget.message,
524+ buildAppBarBottom: (context) => null ,
460525 buildBottomAppBar: _buildBottomAppBar,
461526 child: SafeArea (
462527 child: Center (
@@ -484,6 +549,7 @@ Route<void> getLightboxRoute({
484549 BuildContext ? context,
485550 required Message message,
486551 required Uri src,
552+ required Uri ? thumbnailUrl,
487553 required MediaType mediaType,
488554}) {
489555 return AccountPageRouteBuilder (
@@ -500,7 +566,8 @@ Route<void> getLightboxRoute({
500566 MediaType .image => _ImageLightboxPage (
501567 routeEntranceAnimation: animation,
502568 message: message,
503- src: src),
569+ src: src,
570+ thumbnailUrl: thumbnailUrl),
504571 MediaType .video => VideoLightboxPage (
505572 routeEntranceAnimation: animation,
506573 message: message,
0 commit comments