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' ;
@@ -19,20 +20,22 @@ import 'store.dart';
19
20
// fly to an image preview with a different URL, following a message edit
20
21
// while the lightbox was open.
21
22
class _LightboxHeroTag {
22
- _LightboxHeroTag ({required this .messageId, required this .src});
23
+ _LightboxHeroTag ({required this .messageId, required this .src, required this .thumbnailUrl });
23
24
24
25
final int messageId;
25
26
final Uri src;
27
+ final Uri ? thumbnailUrl;
26
28
27
29
@override
28
30
bool operator == (Object other) {
29
31
return other is _LightboxHeroTag &&
30
32
other.messageId == messageId &&
31
- other.src == src;
33
+ other.src == src &&
34
+ other.thumbnailUrl == thumbnailUrl;
32
35
}
33
36
34
37
@override
35
- int get hashCode => Object .hash ('_LightboxHeroTag' , messageId, src);
38
+ int get hashCode => Object .hash ('_LightboxHeroTag' , messageId, src, thumbnailUrl );
36
39
}
37
40
38
41
/// Builds a [Hero] from an image in the message list to the lightbox page.
@@ -41,17 +44,19 @@ class LightboxHero extends StatelessWidget {
41
44
super .key,
42
45
required this .message,
43
46
required this .src,
47
+ required this .thumbnailUrl,
44
48
required this .child,
45
49
});
46
50
47
51
final Message message;
48
52
final Uri src;
53
+ final Uri ? thumbnailUrl;
49
54
final Widget child;
50
55
51
56
@override
52
57
Widget build (BuildContext context) {
53
58
return Hero (
54
- tag: _LightboxHeroTag (messageId: message.id, src: src),
59
+ tag: _LightboxHeroTag (messageId: message.id, src: src, thumbnailUrl : thumbnailUrl ),
55
60
flightShuttleBuilder: (
56
61
BuildContext flightContext,
57
62
Animation <double > animation,
@@ -91,12 +96,14 @@ class _LightboxPageLayout extends StatefulWidget {
91
96
const _LightboxPageLayout ({
92
97
required this .routeEntranceAnimation,
93
98
required this .message,
99
+ required this .buildAppBarFooter,
94
100
required this .buildBottomAppBar,
95
101
required this .child,
96
102
});
97
103
98
104
final Animation <double > routeEntranceAnimation;
99
105
final Message message;
106
+ final PreferredSizeWidget Function (BuildContext context)? buildAppBarFooter;
100
107
final Widget ? Function (
101
108
BuildContext context, Color color, double elevation) buildBottomAppBar;
102
109
final Widget child;
@@ -171,7 +178,8 @@ class _LightboxPageLayoutState extends State<_LightboxPageLayout> {
171
178
172
179
// Make smaller, like a subtitle
173
180
style: themeData.textTheme.titleSmall! .copyWith (color: appBarForegroundColor)),
174
- ])));
181
+ ])),
182
+ bottom: widget.buildAppBarFooter == null ? null : widget.buildAppBarFooter !(context));
175
183
}
176
184
177
185
Widget ? bottomAppBar;
@@ -209,17 +217,27 @@ class _ImageLightboxPage extends StatefulWidget {
209
217
required this .routeEntranceAnimation,
210
218
required this .message,
211
219
required this .src,
220
+ required this .thumbnailUrl,
212
221
});
213
222
214
223
final Animation <double > routeEntranceAnimation;
215
224
final Message message;
216
225
final Uri src;
226
+ final Uri ? thumbnailUrl;
217
227
218
228
@override
219
229
State <_ImageLightboxPage > createState () => _ImageLightboxPageState ();
220
230
}
221
231
222
232
class _ImageLightboxPageState extends State <_ImageLightboxPage > {
233
+ double ? _loadingProgress;
234
+
235
+ PreferredSizeWidget _buildAppBarFooter (BuildContext context) {
236
+ return PreferredSize (
237
+ preferredSize: const Size .fromHeight (4.0 ),
238
+ child: LinearProgressIndicator (minHeight: 4.0 , value: _loadingProgress! ));
239
+ }
240
+
223
241
Widget _buildBottomAppBar (BuildContext context, Color color, double elevation) {
224
242
return BottomAppBar (
225
243
color: color,
@@ -234,17 +252,53 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> {
234
252
235
253
@override
236
254
Widget build (BuildContext context) {
255
+ final thumbnailUrl = widget.thumbnailUrl;
237
256
return _LightboxPageLayout (
238
257
routeEntranceAnimation: widget.routeEntranceAnimation,
239
258
message: widget.message,
259
+ buildAppBarFooter: _loadingProgress == null ? null : _buildAppBarFooter,
240
260
buildBottomAppBar: _buildBottomAppBar,
241
261
child: SizedBox .expand (
242
262
child: InteractiveViewer (
243
263
child: SafeArea (
244
264
child: LightboxHero (
245
265
message: widget.message,
246
266
src: widget.src,
247
- child: RealmContentNetworkImage (widget.src, filterQuality: FilterQuality .medium))))));
267
+ thumbnailUrl: thumbnailUrl,
268
+ child: RealmContentNetworkImage (widget.src,
269
+ filterQuality: FilterQuality .medium,
270
+ frameBuilder: thumbnailUrl == null ? null
271
+ : (context, child, frame, wasSynchronouslyLoaded) {
272
+ if (wasSynchronouslyLoaded || frame != null ) {
273
+ // Image was already available or has finished downloading and
274
+ // so it is now available.
275
+ return child;
276
+ }
277
+ return RealmContentNetworkImage (thumbnailUrl,
278
+ filterQuality: FilterQuality .medium);
279
+ },
280
+ loadingBuilder: thumbnailUrl == null ? null
281
+ : (context, child, loadingProgress) {
282
+ // `loadingProgress` is null when Image has finished downloading.
283
+ double ? progress = _loadingProgress;
284
+ if (loadingProgress? .expectedTotalBytes == null ) {
285
+ progress = null ;
286
+ } else {
287
+ progress = loadingProgress! .cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! ;
288
+ }
289
+ if (progress != _loadingProgress) {
290
+ _loadingProgress = progress;
291
+ // This function is called in a build method and setState
292
+ // can't be called in a build method, so delay it.
293
+ SchedulerBinding .instance.addPostFrameCallback ((_) { if (mounted) setState (() {}); });
294
+ }
295
+ return child;
296
+ },
297
+ ),
298
+ ),
299
+ ),
300
+ ),
301
+ ));
248
302
}
249
303
}
250
304
@@ -457,6 +511,7 @@ class _VideoLightboxPageState extends State<VideoLightboxPage> with PerAccountSt
457
511
return _LightboxPageLayout (
458
512
routeEntranceAnimation: widget.routeEntranceAnimation,
459
513
message: widget.message,
514
+ buildAppBarFooter: null ,
460
515
buildBottomAppBar: _buildBottomAppBar,
461
516
child: SafeArea (
462
517
child: Center (
@@ -484,6 +539,7 @@ Route<void> getLightboxRoute({
484
539
BuildContext ? context,
485
540
required Message message,
486
541
required Uri src,
542
+ required Uri ? thumbnailUrl,
487
543
required MediaType mediaType,
488
544
}) {
489
545
return AccountPageRouteBuilder (
@@ -500,7 +556,8 @@ Route<void> getLightboxRoute({
500
556
MediaType .image => _ImageLightboxPage (
501
557
routeEntranceAnimation: animation,
502
558
message: message,
503
- src: src),
559
+ src: src,
560
+ thumbnailUrl: thumbnailUrl),
504
561
MediaType .video => VideoLightboxPage (
505
562
routeEntranceAnimation: animation,
506
563
message: message,
0 commit comments