import "dart:io"; import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import "package:media_kit/media_kit.dart"; import "package:media_kit_video/media_kit_video.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/file/extensions/file_props.dart"; import "package:photos/models/file/file.dart"; import "package:photos/services/files_service.dart"; import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/viewer/file/thumbnail_widget.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/file_util.dart"; import "package:photos/utils/toast_util.dart"; class VideoWidgetNew extends StatefulWidget { final EnteFile file; final String? tagPrefix; const VideoWidgetNew( this.file, { this.tagPrefix, super.key, }); @override State createState() => _VideoWidgetNewState(); } class _VideoWidgetNewState extends State { static const verticalMargin = 100.0; late final player = Player(); VideoController? controller; final _progressNotifier = ValueNotifier(null); @override void initState() { super.initState(); if (widget.file.isRemoteFile) { _loadNetworkVideo(); _setFileSizeIfNull(); } else if (widget.file.isSharedMediaToAppSandbox) { final localFile = File(getSharedMediaFilePath(widget.file)); if (localFile.existsSync()) { _setVideoController(localFile.path); } else if (widget.file.uploadedFileID != null) { _loadNetworkVideo(); } } else { widget.file.getAsset.then((asset) async { if (asset == null || !(await asset.exists)) { if (widget.file.uploadedFileID != null) { _loadNetworkVideo(); } } else { asset.getMediaUrl().then((url) { _setVideoController( url ?? 'https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4', ); }); } }); } } @override void dispose() { player.dispose(); // _progressNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final colorScheme = getEnteColorScheme(context); return Hero( tag: widget.tagPrefix! + widget.file.tag, child: GestureDetector( //This gestureDetector is to stop swiping the pageView when seeking the //video using the seekbar. onHorizontalDragUpdate: (details) {}, child: MaterialVideoControlsTheme( normal: MaterialVideoControlsThemeData( seekBarMargin: const EdgeInsets.only(bottom: verticalMargin), bottomButtonBarMargin: const EdgeInsets.only(bottom: 112), controlsHoverDuration: const Duration(seconds: 3), seekBarHeight: 4, seekBarBufferColor: Colors.transparent, seekBarThumbColor: backgroundElevatedLight, seekBarColor: fillMutedDark, seekBarPositionColor: colorScheme.primary300.withOpacity(0.8), topButtonBarMargin: const EdgeInsets.only(top: verticalMargin), bottomButtonBar: [ const Spacer(), PausePlayAndDuration(controller?.player), const Spacer(), ], primaryButtonBar: [], ), fullscreen: const MaterialVideoControlsThemeData(), child: Center( child: controller != null ? Video( controller: controller!, ) : _getLoadingWidget(), ), ), ), ); } void _loadNetworkVideo() { getFileFromServer( widget.file, progressCallback: (count, total) { _progressNotifier.value = count / (widget.file.fileSize ?? total); if (_progressNotifier.value == 1) { if (mounted) { showShortToast(context, S.of(context).decryptingVideo); } } }, ).then((file) { if (file != null) { _setVideoController(file.path); } }).onError((error, stackTrace) { showErrorDialog(context, "Error", S.of(context).failedToDownloadVideo); }); } void _setFileSizeIfNull() { if (widget.file.fileSize == null && widget.file.canEditMetaInfo) { FilesService.instance .getFileSize(widget.file.uploadedFileID!) .then((value) { widget.file.fileSize = value; if (mounted) { setState(() {}); } }); } } Widget _getLoadingWidget() { return Stack( children: [ _getThumbnail(), Container( color: Colors.black12, constraints: const BoxConstraints.expand(), ), Center( child: SizedBox.fromSize( size: const Size.square(20), child: ValueListenableBuilder( valueListenable: _progressNotifier, builder: (BuildContext context, double? progress, _) { return progress == null || progress == 1 ? const CupertinoActivityIndicator( color: Colors.white, ) : CircularProgressIndicator( backgroundColor: Colors.black, value: progress, valueColor: const AlwaysStoppedAnimation( Color.fromRGBO(45, 194, 98, 1.0), ), ); }, ), ), ), ], ); } Widget _getThumbnail() { return Container( color: Colors.black, constraints: const BoxConstraints.expand(), child: ThumbnailWidget( widget.file, fit: BoxFit.contain, ), ); } void _setVideoController(String url) { if (mounted) { setState(() { controller = VideoController(player); player.open(Media(url)); }); } } } class PausePlayAndDuration extends StatefulWidget { final Player? player; const PausePlayAndDuration(this.player, {super.key}); @override State createState() => _PausePlayAndDurationState(); } class _PausePlayAndDurationState extends State { Color backgroundColor = fillMutedLight; @override Widget build(BuildContext context) { return GestureDetector( onTapDown: (details) { setState(() { backgroundColor = fillMutedDark; }); }, onTapUp: (details) { Future.delayed(const Duration(milliseconds: 175), () { if (mounted) { setState(() { backgroundColor = fillMutedLight; }); } }); }, onTapCancel: () { Future.delayed(const Duration(milliseconds: 175), () { if (mounted) { setState(() { backgroundColor = fillMutedLight; }); } }); }, onTap: () => widget.player!.playOrPause(), child: AnimatedContainer( duration: const Duration(milliseconds: 150), curve: Curves.easeInBack, padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), decoration: BoxDecoration( color: backgroundColor, border: Border.all( color: strokeFaintDark, width: 1, ), borderRadius: BorderRadius.circular(24), ), child: AnimatedSize( duration: const Duration(seconds: 2), curve: Curves.easeInOutExpo, child: Row( children: [ StreamBuilder( builder: (context, snapshot) { final bool isPlaying = snapshot.data ?? false; return AnimatedSwitcher( duration: const Duration(milliseconds: 350), switchInCurve: Curves.easeInOutCirc, switchOutCurve: Curves.easeInOutCirc, child: Icon( key: ValueKey( isPlaying ? "pause_button" : "play_button", ), isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, color: backdropBaseLight, ), ); }, initialData: widget.player?.state.playing, stream: widget.player?.stream.playing, ), const SizedBox(width: 8), MaterialPositionIndicator( style: getEnteTextTheme(context).mini.copyWith( color: textBaseDark, ), ), const SizedBox(width: 10), ], ), ), ), ); } }