// ignore_for_file: implementation_imports, unused_element import 'dart:async'; import 'package:chewie/chewie.dart'; import 'package:chewie/src/animated_play_pause.dart'; import 'package:chewie/src/helpers/utils.dart'; import 'package:chewie/src/material/material_progress_bar.dart'; import 'package:flutter/material.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_state.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart'; import 'package:video_player/video_player.dart'; import 'center_play_button.dart'; class VideoCustomControls extends StatefulWidget { const VideoCustomControls({required this.downloadFn, Key? key}) : super(key: key); final Future Function() downloadFn; @override State createState() { return _VideoCustomControlsState(); } } class _VideoCustomControlsState extends TIMUIKitState with SingleTickerProviderStateMixin { late VideoPlayerValue _latestValue; bool _hideStuff = true; Timer? _hideTimer; Timer? _initTimer; Timer? _showAfterExpandCollapseTimer; bool _dragging = false; bool _displayTapped = false; bool isLoading = false; final barHeight = 48.0; final marginSize = 5.0; late VideoPlayerController controller; ChewieController? _chewieController; // We know that _chewieController is set in didChangeDependencies ChewieController get chewieController => _chewieController!; @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { if (_latestValue.hasError) { return Container( color: Colors.transparent, child: chewieController.errorBuilder?.call( context, chewieController.videoPlayerController.value.errorDescription!, ) ?? const Center( child: Icon( Icons.error, color: Colors.white, size: 42, ), ), ); } return MouseRegion( onHover: (_) { _cancelAndRestartTimer(); }, child: GestureDetector( onTap: () => _cancelAndRestartTimer(), child: AbsorbPointer( absorbing: _hideStuff, child: Stack( alignment: Alignment.center, children: [ if (_latestValue.isBuffering) const Center(child: CircularProgressIndicator(color: Colors.white)) else _buildHitArea(), Positioned( bottom: 0, width: MediaQuery.of(context).size.width, child: Column(children: [_buildVideoControlBar(context), _buildBottomBar()]), ), if (isLoading) Container( child: LoadingAnimationWidget.staggeredDotsWave( size: 35, color: Colors.white, ), padding: const EdgeInsets.all(30), decoration: const BoxDecoration( color: Color(0xB22b2b2b), borderRadius: BorderRadius.all(Radius.circular(20)), ), ), ], ), ), ), ); } @override void dispose() { _dispose(); super.dispose(); } void _dispose() { controller.removeListener(_updateState); _hideTimer?.cancel(); _initTimer?.cancel(); _showAfterExpandCollapseTimer?.cancel(); } @override void didChangeDependencies() { final _oldController = _chewieController; _chewieController = ChewieController.of(context); controller = chewieController.videoPlayerController; if (_oldController != chewieController) { _dispose(); _initialize(); } super.didChangeDependencies(); } Widget _buildBottomBar() { return Material( color: Colors.transparent, child: Container( height: barHeight, margin: const EdgeInsets.fromLTRB(20, 0, 20, 20), child: Row( children: [ SizedBox( width: 48, height: 48, child: IconButton( icon: Image.asset( 'images/close.png', package: 'tencent_cloud_chat_uikit', ), iconSize: 30, onPressed: () { if (_latestValue.isPlaying) { _playPause(); } Navigator.of(context).pop(); }, ), ), Expanded(child: Container()), SizedBox( width: 48, height: 48, child: IconButton( icon: Image.asset( 'images/download.png', package: 'tencent_cloud_chat_uikit', ), iconSize: 30, onPressed: () async { setState(() { isLoading = true; }); await widget.downloadFn(); Future.delayed( const Duration(milliseconds: 200), () { setState( () { isLoading = false; }, ); }, ); }, ), ) ], ), ), ); } AnimatedOpacity _buildVideoControlBar( BuildContext context, ) { const iconColor = Colors.white; return AnimatedOpacity( opacity: _hideStuff ? 0.0 : 1.0, duration: const Duration(milliseconds: 300), child: SizedBox( height: barHeight, child: Row( children: [ _buildPlayPause(controller, iconColor), if (chewieController.isLive) const Expanded(child: Text('LIVE')) else _buildPositionStart(iconColor), if (chewieController.isLive) const SizedBox() else _buildProgressBar(), if (!chewieController.isLive) _buildPositionEnd(iconColor), ], ), ), ); } Widget _buildHitArea() { // final bool isFinished = _latestValue.position >= _latestValue.duration; return GestureDetector( onTap: () { if (_latestValue.isPlaying) { if (_displayTapped) { setState(() { _hideStuff = true; }); } else { _cancelAndRestartTimer(); } } else { _playPause(); setState(() { _hideStuff = true; }); } }, child: CenterPlayButton( isPlaying: controller.value.isPlaying, show: !_latestValue.isPlaying && !_dragging, onPressed: _playPause, )); } GestureDetector _buildPlayPause(VideoPlayerController controller, Color color) { return GestureDetector( onTap: _playPause, child: Container( height: barHeight, color: Colors.transparent, margin: const EdgeInsets.only(left: 8.0, right: 4.0), padding: const EdgeInsets.only( left: 12.0, right: 12.0, ), child: AnimatedPlayPause( playing: controller.value.isPlaying, color: color, ), ), ); } Widget _buildPositionStart(Color? iconColor) { final position = _latestValue.position; return Padding( padding: const EdgeInsets.only(right: 24.0), child: Text( formatDuration(position), style: TextStyle(fontSize: 14.0, color: iconColor), ), ); } Widget _buildPositionEnd(Color? iconColor) { final duration = _latestValue.duration; return Padding( padding: const EdgeInsets.only(right: 24.0), child: Text( formatDuration(duration), style: TextStyle(fontSize: 14.0, color: iconColor), ), ); } void _cancelAndRestartTimer() { _hideTimer?.cancel(); _startHideTimer(); setState(() { _hideStuff = false; _displayTapped = true; }); } Future _initialize() async { controller.addListener(_updateState); _updateState(); if (controller.value.isPlaying || chewieController.autoPlay) { _startHideTimer(); } if (chewieController.showControlsOnInitialize) { _initTimer = Timer(const Duration(milliseconds: 200), () { setState(() { _hideStuff = false; }); }); } } void _onExpandCollapse() { setState(() { _hideStuff = true; chewieController.toggleFullScreen(); _showAfterExpandCollapseTimer = Timer(const Duration(milliseconds: 300), () { setState(() { _cancelAndRestartTimer(); }); }); }); } void _playPause() { final isFinished = _latestValue.position >= _latestValue.duration; setState(() { if (controller.value.isPlaying) { _hideStuff = false; _hideTimer?.cancel(); controller.pause(); } else { _cancelAndRestartTimer(); if (!controller.value.isInitialized) { controller.initialize().then((_) { controller.play(); }); } else { if (isFinished) { controller.seekTo(const Duration()); } Timer(const Duration(milliseconds: 100), () => controller.play()); } } }); } void _startHideTimer() { _hideTimer = Timer(const Duration(seconds: 3), () { setState(() { _hideStuff = true; }); }); } void _updateState() { if (!mounted) return; setState(() { _latestValue = controller.value; }); } Widget _buildProgressBar() { return Expanded( child: Padding( padding: const EdgeInsets.only(right: 20.0), child: MaterialVideoProgressBar( controller, onDragStart: () { setState(() { _dragging = true; }); _hideTimer?.cancel(); }, onDragEnd: () { setState(() { _dragging = false; }); _startHideTimer(); }, colors: chewieController.materialProgressColors ?? ChewieProgressColors(playedColor: Colors.white, handleColor: Colors.white, bufferedColor: Colors.white38, backgroundColor: Colors.white24), ), ), ); } } class _PlaybackSpeedDialog extends TIMUIKitStatelessWidget { _PlaybackSpeedDialog({ Key? key, required List speeds, required double selected, }) : _speeds = speeds, _selected = selected, super(key: key); final List _speeds; final double _selected; @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; final Color selectedColor = theme.primaryColor ?? Theme.of(context).primaryColor; return ListView.builder( shrinkWrap: true, physics: const ScrollPhysics(), itemBuilder: (context, index) { final _speed = _speeds[index]; return ListTile( dense: true, title: Row( children: [ if (_speed == _selected) Icon( Icons.check, size: 20.0, color: selectedColor, ) else Container(width: 20.0), const SizedBox(width: 16.0), Text(_speed.toString()), ], ), selected: _speed == _selected, onTap: () { Navigator.of(context).pop(_speed); }, ); }, itemCount: _speeds.length, ); } }