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,60 @@ 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+ // The [Image.network] API lets us learn progress information only at
276+ // its build time. That's too late for updating the progress indicator,
277+ // so delay that update to the next frame. For discussion, see:
278+ // https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/addPostFrameCallback/near/1893539
279+ // https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/addPostFrameCallback/near/1894124
280+ SchedulerBinding .instance.scheduleFrameCallback ((_) {
281+ if (! mounted) return ;
282+ setState (() {});
283+ });
284+ }
285+ return child;
286+ }
287+
235288 @override
236289 Widget build (BuildContext context) {
237290 return _LightboxPageLayout (
238291 routeEntranceAnimation: widget.routeEntranceAnimation,
239292 message: widget.message,
293+ buildAppBarBottom: _buildAppBarBottom,
240294 buildBottomAppBar: _buildBottomAppBar,
241295 child: SizedBox .expand (
242296 child: InteractiveViewer (
243297 child: SafeArea (
244298 child: LightboxHero (
245299 message: widget.message,
246300 src: widget.src,
247- child: RealmContentNetworkImage (widget.src, filterQuality: FilterQuality .medium))))));
301+ child: RealmContentNetworkImage (widget.src,
302+ filterQuality: FilterQuality .medium,
303+ frameBuilder: _frameBuilder,
304+ loadingBuilder: _loadingBuilder),
305+ ),
306+ ),
307+ ),
308+ ));
248309 }
249310}
250311
@@ -457,6 +518,7 @@ class _VideoLightboxPageState extends State<VideoLightboxPage> with PerAccountSt
457518 return _LightboxPageLayout (
458519 routeEntranceAnimation: widget.routeEntranceAnimation,
459520 message: widget.message,
521+ buildAppBarBottom: (context) => null ,
460522 buildBottomAppBar: _buildBottomAppBar,
461523 child: SafeArea (
462524 child: Center (
@@ -484,6 +546,7 @@ Route<void> getLightboxRoute({
484546 BuildContext ? context,
485547 required Message message,
486548 required Uri src,
549+ required Uri ? thumbnailUrl,
487550 required MediaType mediaType,
488551}) {
489552 return AccountPageRouteBuilder (
@@ -500,7 +563,8 @@ Route<void> getLightboxRoute({
500563 MediaType .image => _ImageLightboxPage (
501564 routeEntranceAnimation: animation,
502565 message: message,
503- src: src),
566+ src: src,
567+ thumbnailUrl: thumbnailUrl),
504568 MediaType .video => VideoLightboxPage (
505569 routeEntranceAnimation: animation,
506570 message: message,
0 commit comments