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' ;
6
+ import 'package:video_player/video_player.dart' ;
5
7
8
+ import '../api/core.dart' ;
6
9
import '../api/model/model.dart' ;
10
+ import '../log.dart' ;
7
11
import 'content.dart' ;
12
+ import 'dialog.dart' ;
8
13
import 'page.dart' ;
9
14
import 'clipboard.dart' ;
10
15
import 'store.dart' ;
@@ -238,11 +243,164 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> {
238
243
}
239
244
}
240
245
246
+ class VideoLightboxPage extends StatefulWidget {
247
+ const VideoLightboxPage ({
248
+ super .key,
249
+ required this .routeEntranceAnimation,
250
+ required this .message,
251
+ required this .src,
252
+ });
253
+
254
+ final Animation routeEntranceAnimation;
255
+ final Message message;
256
+ final Uri src;
257
+
258
+ @override
259
+ State <VideoLightboxPage > createState () => _VideoLightboxPageState ();
260
+ }
261
+
262
+ class _VideoLightboxPageState extends State <VideoLightboxPage > {
263
+ VideoPlayerController ? _controller;
264
+
265
+ @override
266
+ void didChangeDependencies () {
267
+ super .didChangeDependencies ();
268
+ assert (debugLog ('_VideoLightboxPageState($hashCode ).didChangeDependencies' ));
269
+
270
+ // We are pretty sure that the dependencies (PerAccountStore & ZulipLocalizations)
271
+ // won't change while user is on the lightbox page but we still
272
+ // handle the reinitialization for correctness.
273
+ if (_controller != null ) _deinitialize ();
274
+ _initialize ();
275
+ }
276
+
277
+ Future <void > _initialize () async {
278
+ final store = PerAccountStoreWidget .of (context);
279
+ final zulipLocalizations = ZulipLocalizations .of (context);
280
+
281
+ assert (debugLog ('VideoPlayerController.networkUrl(${widget .src })' ));
282
+ _controller = VideoPlayerController .networkUrl (widget.src, httpHeaders: {
283
+ if (widget.src.origin == store.account.realmUrl.origin) ...authHeader (
284
+ email: store.account.email,
285
+ apiKey: store.account.apiKey,
286
+ ),
287
+ ...userAgentHeader ()
288
+ });
289
+ _controller! .addListener (_handleVideoControllerUpdates);
290
+
291
+ try {
292
+ await _controller! .initialize ();
293
+ await _controller! .play ();
294
+ } catch (error) { // TODO(log)
295
+ assert (debugLog ("VideoPlayerController.initialize failed: $error " ));
296
+ if (mounted) {
297
+ await showErrorDialog (
298
+ context: context,
299
+ title: zulipLocalizations.errorDialogTitle,
300
+ message: zulipLocalizations.errorVideoPlayerFailed,
301
+ // To avoid showing the disabled video lightbox for the unnsupported
302
+ // video, we make sure user doesn't reach there by dismissing the dialog
303
+ // by clicking around it, user must press the 'OK' button, which will
304
+ // take user back to content message list.
305
+ barrierDismissible: false ,
306
+ onContinue: () {
307
+ Navigator .pop (context); // Pops the dialog
308
+ Navigator .pop (context); // Pops the lightbox
309
+ });
310
+ }
311
+ }
312
+ }
313
+
314
+ @override
315
+ void dispose () {
316
+ _deinitialize ();
317
+ super .dispose ();
318
+ }
319
+
320
+ void _deinitialize () {
321
+ _controller? .removeListener (_handleVideoControllerUpdates);
322
+ _controller? .dispose ();
323
+ _controller = null ;
324
+ }
325
+
326
+ void _handleVideoControllerUpdates () {
327
+ setState (() {});
328
+ }
329
+
330
+ @override
331
+ Widget build (BuildContext context) {
332
+ return _LightboxPageLayout (
333
+ routeEntranceAnimation: widget.routeEntranceAnimation,
334
+ message: widget.message,
335
+ buildBottomAppBar: (context, color, elevation) =>
336
+ _controller == null
337
+ ? null
338
+ : BottomAppBar (
339
+ height: 150 ,
340
+ color: color,
341
+ elevation: elevation,
342
+ child: Column (mainAxisAlignment: MainAxisAlignment .end, children: [
343
+ Row (children: [
344
+ Text (_formatDuration (_controller! .value.position),
345
+ style: const TextStyle (color: Colors .white)),
346
+ Expanded (child: Slider (
347
+ value: _controller! .value.position.inSeconds.toDouble (),
348
+ max: _controller! .value.duration.inSeconds.toDouble (),
349
+ activeColor: Colors .white,
350
+ onChanged: (value) {
351
+ _controller! .seekTo (Duration (seconds: value.toInt ()));
352
+ })),
353
+ Text (_formatDuration (_controller! .value.duration),
354
+ style: const TextStyle (color: Colors .white)),
355
+ ]),
356
+ IconButton (
357
+ onPressed: () {
358
+ if (_controller! .value.isPlaying) {
359
+ _controller! .pause ();
360
+ } else {
361
+ _controller! .play ();
362
+ }
363
+ },
364
+ icon: Icon (
365
+ _controller! .value.isPlaying
366
+ ? Icons .pause_circle_rounded
367
+ : Icons .play_circle_rounded,
368
+ size: 50 )),
369
+ ])),
370
+ child: SafeArea (
371
+ child: Center (
372
+ child: Stack (alignment: Alignment .center, children: [
373
+ if (_controller != null && _controller! .value.isInitialized)
374
+ AspectRatio (
375
+ aspectRatio: _controller! .value.aspectRatio,
376
+ child: VideoPlayer (_controller! )),
377
+ if (_controller == null || ! _controller! .value.isInitialized || _controller! .value.isBuffering)
378
+ const SizedBox (
379
+ width: 32 ,
380
+ height: 32 ,
381
+ child: CircularProgressIndicator (color: Colors .white)),
382
+ ]))));
383
+ }
384
+
385
+ String _formatDuration (Duration value) {
386
+ final hours = value.inHours.toString ().padLeft (2 , '0' );
387
+ final minutes = value.inMinutes.remainder (60 ).toString ().padLeft (2 , '0' );
388
+ final seconds = value.inSeconds.remainder (60 ).toString ().padLeft (2 , '0' );
389
+ return '${hours == '00' ? '' : '$hours :' }$minutes :$seconds ' ;
390
+ }
391
+ }
392
+
393
+ enum MediaType {
394
+ video,
395
+ image
396
+ }
397
+
241
398
Route getLightboxRoute ({
242
399
int ? accountId,
243
400
BuildContext ? context,
244
401
required Message message,
245
402
required Uri src,
403
+ required MediaType mediaType,
246
404
}) {
247
405
return AccountPageRouteBuilder (
248
406
accountId: accountId,
@@ -254,7 +412,16 @@ Route getLightboxRoute({
254
412
Animation <double > secondaryAnimation,
255
413
) {
256
414
// TODO(#40): Drag down to close?
257
- return _ImageLightboxPage (routeEntranceAnimation: animation, message: message, src: src);
415
+ return switch (mediaType) {
416
+ MediaType .image => _ImageLightboxPage (
417
+ routeEntranceAnimation: animation,
418
+ message: message,
419
+ src: src),
420
+ MediaType .video => VideoLightboxPage (
421
+ routeEntranceAnimation: animation,
422
+ message: message,
423
+ src: src),
424
+ };
258
425
},
259
426
transitionsBuilder: (
260
427
BuildContext context,
0 commit comments