From d512bc0bfd6b79c1241a726e9fcf09b31988a435 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 2 Aug 2023 16:14:09 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E9=9F=B3=E9=87=8F=E3=80=81?= =?UTF-8?q?=E4=BA=AE=E5=BA=A6=E8=B0=83=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/images/run-pokemon.gif | Bin 0 -> 46050 bytes lib/pages/video/detail/view.dart | 1 + lib/plugin/pl_player/index.dart | 1 + lib/plugin/pl_player/view.dart | 466 ++++++++++++++++++++++--------- 4 files changed, 343 insertions(+), 125 deletions(-) create mode 100644 assets/images/run-pokemon.gif diff --git a/assets/images/run-pokemon.gif b/assets/images/run-pokemon.gif new file mode 100644 index 0000000000000000000000000000000000000000..bca20afc29f2fbeeb8099249dcad7a83699943af GIT binary patch literal 46050 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 Cr*Cus literal 0 HcmV?d00001 diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 68edd92a..9f16ecf8 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -59,6 +59,7 @@ class _VideoDetailPageState extends State // 当只有1p或多p未打开自动播放时,播放完成还原进度条,展示控制栏 plPlayerController!.seekTo(Duration.zero); plPlayerController!.onLockControl(false); + plPlayerController!.videoPlayerController!.pause(); } } }, diff --git a/lib/plugin/pl_player/index.dart b/lib/plugin/pl_player/index.dart index 997776f9..721d5040 100644 --- a/lib/plugin/pl_player/index.dart +++ b/lib/plugin/pl_player/index.dart @@ -7,3 +7,4 @@ export './models/play_status.dart'; export './models/data_status.dart'; export './widgets/common_btn.dart'; export './models/play_speed.dart'; +export './widgets/app_bar_ani.dart'; diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 0dac67eb..5fa5add9 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -10,6 +12,8 @@ import 'package:pilipala/plugin/pl_player/models/duration.dart'; import 'package:pilipala/plugin/pl_player/models/play_status.dart'; import 'package:pilipala/plugin/pl_player/utils.dart'; import 'package:pilipala/utils/feed_back.dart'; +import 'package:screen_brightness/screen_brightness.dart'; +import 'package:volume_controller/volume_controller.dart'; import 'widgets/backward_seek.dart'; import 'widgets/bottom_control.dart'; @@ -43,6 +47,16 @@ class _PLVideoPlayerState extends State bool _hideSeekBackwardButton = false; bool _hideSeekForwardButton = false; + double _brightnessValue = 0.0; + bool _brightnessIndicator = false; + Timer? _brightnessTimer; + + double _volumeValue = 0.0; + bool _volumeIndicator = false; + Timer? _volumeTimer; + + bool _volumeInterceptEventStream = false; + void onDoubleTapSeekBackward() { setState(() { _mountSeekBackwardButton = true; @@ -61,6 +75,70 @@ class _PLVideoPlayerState extends State animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300)); videoController = widget.controller.videoController!; + + Future.microtask(() async { + try { + VolumeController().showSystemUI = false; + _volumeValue = await VolumeController().getVolume(); + VolumeController().listener((value) { + if (mounted && !_volumeInterceptEventStream) { + setState(() { + _volumeValue = value; + }); + } + }); + } catch (_) {} + }); + + Future.microtask(() async { + try { + _brightnessValue = await ScreenBrightness().current; + ScreenBrightness().onCurrentBrightnessChanged.listen((value) { + if (mounted) { + setState(() { + _brightnessValue = value; + }); + } + }); + } catch (_) {} + }); + } + + Future setVolume(double value) async { + try { + VolumeController().setVolume(value); + } catch (_) {} + setState(() { + _volumeValue = value; + _volumeIndicator = true; + _volumeInterceptEventStream = true; + }); + _volumeTimer?.cancel(); + _volumeTimer = Timer(const Duration(milliseconds: 200), () { + if (mounted) { + setState(() { + _volumeIndicator = false; + _volumeInterceptEventStream = false; + }); + } + }); + } + + Future setBrightness(double value) async { + try { + await ScreenBrightness().setScreenBrightness(value); + } catch (_) {} + setState(() { + _brightnessIndicator = true; + }); + _brightnessTimer?.cancel(); + _brightnessTimer = Timer(const Duration(milliseconds: 200), () { + if (mounted) { + setState(() { + _brightnessIndicator = false; + }); + } + }); } @override @@ -90,25 +168,6 @@ class _PLVideoPlayerState extends State clipBehavior: Clip.hardEdge, fit: StackFit.passthrough, children: [ - // Wrap [Video] widget with [MaterialVideoControlsTheme]. - // MaterialVideoControlsTheme( - // normal: MaterialVideoControlsThemeData( - // // Modify theme options: - // buttonBarButtonSize: 24.0, - // buttonBarButtonColor: Colors.white, - // ), - // fullscreen: const MaterialVideoControlsThemeData( - // // Modify theme options: - // displaySeekBar: false, - // automaticallyImplySkipNextButton: false, - // automaticallyImplySkipPreviousButton: false, - // ), - // child: Scaffold( - // body: Video( - // controller: videoController, - // ), - // ), - // ), Video( controller: videoController, controls: NoVideoControls, @@ -118,55 +177,267 @@ class _PLVideoPlayerState extends State padding: const EdgeInsets.all(24.0), ), ), - Padding( - padding: const EdgeInsets.only(top: 20, bottom: 15), - child: GestureDetector( - onTap: () { - _.controls = !_.showControls.value; - }, - // onDoubleTap: () { - // if (_.playerStatus.status.value == PlayerStatus.playing) { - // _.togglePlay(); - // } else { - // _.play(); - // } - // }, - onDoubleTapDown: (details) { - final totalWidth = MediaQuery.of(context).size.width; - final tapPosition = details.localPosition.dx; - final sectionWidth = totalWidth / 3; - if (tapPosition < sectionWidth) { - // 双击左边区域 👈 - onDoubleTapSeekBackward(); - } else if (tapPosition < sectionWidth * 2) { - if (_.playerStatus.status.value == PlayerStatus.playing) { - _.togglePlay(); - } else { - _.play(); - } - } else { - // 双击右边区域 👈 - onDoubleTapSeekForward(); - } - }, - onLongPressStart: (detail) { - feedBack(); - double currentSpeed = _.playbackSpeed; - _.setDoubleSpeedStatus(true); - _.setPlaybackSpeed(currentSpeed * 2); - }, - onLongPressEnd: (details) { - double currentSpeed = _.playbackSpeed; - _.setDoubleSpeedStatus(false); - _.setPlaybackSpeed(currentSpeed / 2); - }, - // 水平位置 快进 - onHorizontalDragUpdate: (DragUpdateDetails details) {}, - onHorizontalDragEnd: (DragEndDetails details) {}, - // 垂直方向 音量/亮度调节 - onVerticalDragUpdate: (DragUpdateDetails details) {}, - onVerticalDragEnd: (DragEndDetails details) {}), + + /// 长按倍速 + Obx( + () => Align( + alignment: Alignment.topCenter, + child: FractionalTranslation( + translation: const Offset(0.0, 1), // 上下偏移量(负数向上偏移) + child: AnimatedOpacity( + curve: Curves.easeInOut, + opacity: _.doubleSpeedStatus.value ? 1.0 : 0.0, + duration: const Duration(milliseconds: 150), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0x88000000), + borderRadius: BorderRadius.circular(64.0), + ), + height: 34.0, + width: 86.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(width: 3), + Image.asset( + 'assets/images/run-pokemon.gif', + height: 20, + ), + const Text( + '倍速中', + style: TextStyle(color: Colors.white, fontSize: 12), + ), + const SizedBox(width: 4), + ], + ), + ), + ), + ), + ), ), + + /// 时间进度 + Obx( + () => Align( + alignment: Alignment.topCenter, + child: FractionalTranslation( + translation: const Offset(0.0, 1.0), // 上下偏移量(负数向上偏移) + child: AnimatedOpacity( + curve: Curves.easeInOut, + opacity: _.isSliderMoving.value ? 1.0 : 0.0, + duration: const Duration(milliseconds: 150), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0x88000000), + borderRadius: BorderRadius.circular(64.0), + ), + height: 34.0, + width: 100.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Obx(() { + return Text( + _.sliderTempPosition.value.inMinutes >= 60 + ? printDurationWithHours( + _.sliderTempPosition.value) + : printDuration(_.sliderTempPosition.value), + style: textStyle, + ); + }), + const SizedBox(width: 2), + const Text('/', style: textStyle), + const SizedBox(width: 2), + Obx( + () => Text( + _.duration.value.inMinutes >= 60 + ? printDurationWithHours(_.duration.value) + : printDuration(_.duration.value), + style: textStyle, + ), + ), + ], + ), + ), + ), + ), + ), + ), + + /// 音量🔊 控制条展示 + Align( + alignment: Alignment.center, + child: AnimatedOpacity( + curve: Curves.easeInOut, + opacity: _volumeIndicator ? 1.0 : 0.0, + duration: const Duration(milliseconds: 150), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0x88000000), + borderRadius: BorderRadius.circular(64.0), + ), + height: 34.0, + width: 70.0, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 34.0, + width: 28.0, + alignment: Alignment.centerRight, + child: Icon( + _volumeValue == 0.0 + ? Icons.volume_off + : _volumeValue < 0.5 + ? Icons.volume_down + : Icons.volume_up, + color: const Color(0xFFFFFFFF), + size: 20.0, + ), + ), + Expanded( + child: Text( + '${(_volumeValue * 100.0).round()}%', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 13.0, + color: Color(0xFFFFFFFF), + ), + ), + ), + const SizedBox(width: 6.0), + ], + ), + ), + ), + ), + + /// 亮度🌞 控制条展示 + Align( + alignment: Alignment.center, + child: AnimatedOpacity( + curve: Curves.easeInOut, + opacity: _brightnessIndicator ? 1.0 : 0.0, + duration: const Duration(milliseconds: 150), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0x88000000), + borderRadius: BorderRadius.circular(64.0), + ), + height: 34.0, + width: 70.0, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 30.0, + width: 28.0, + alignment: Alignment.centerRight, + child: Icon( + _brightnessValue < 1.0 / 3.0 + ? Icons.brightness_low + : _brightnessValue < 2.0 / 3.0 + ? Icons.brightness_medium + : Icons.brightness_high, + color: const Color(0xFFFFFFFF), + size: 18.0, + ), + ), + const SizedBox(width: 2.0), + Expanded( + child: Text( + '${(_brightnessValue * 100.0).round()}%', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 13.0, + color: Color(0xFFFFFFFF), + ), + ), + ), + const SizedBox(width: 6.0), + ], + ), + ), + ), + ), + + /// 手势 + Positioned.fill( + left: 16, + top: 25, + right: 15, + bottom: 15, + child: GestureDetector( + onTap: () { + _.controls = !_.showControls.value; + }, + onDoubleTapDown: (details) { + final totalWidth = MediaQuery.of(context).size.width; + final tapPosition = details.localPosition.dx; + final sectionWidth = totalWidth / 3; + if (tapPosition < sectionWidth) { + // 双击左边区域 👈 + onDoubleTapSeekBackward(); + } else if (tapPosition < sectionWidth * 2) { + if (_.playerStatus.status.value == PlayerStatus.playing) { + _.togglePlay(); + } else { + _.play(); + } + } else { + // 双击右边区域 👈 + onDoubleTapSeekForward(); + } + }, + onLongPressStart: (detail) { + feedBack(); + double currentSpeed = _.playbackSpeed; + _.setDoubleSpeedStatus(true); + _.setPlaybackSpeed(currentSpeed * 2); + }, + onLongPressEnd: (details) { + double currentSpeed = _.playbackSpeed; + _.setDoubleSpeedStatus(false); + _.setPlaybackSpeed(currentSpeed / 2); + }, + // 水平位置 快进 + onHorizontalDragUpdate: (DragUpdateDetails details) {}, + onHorizontalDragEnd: (DragEndDetails details) {}, + // 垂直方向 音量/亮度调节 + onVerticalDragUpdate: (DragUpdateDetails details) { + final totalWidth = MediaQuery.of(context).size.width; + final tapPosition = details.localPosition.dx; + final sectionWidth = totalWidth / 3; + + if (tapPosition < sectionWidth) { + // 左边区域 👈 + final delta = details.delta.dy; + final brightness = _brightnessValue - delta / 100.0; + final result = brightness.clamp(0.0, 1.0); + setBrightness(result); + } else if (tapPosition < sectionWidth * 2) { + // 全屏 + print('全屏'); + } else { + // 右边区域 👈 + final delta = details.delta.dy; + final volume = _volumeValue - delta / 100.0; + final result = volume.clamp(0.0, 1.0); + setVolume(result); + } + }, + onVerticalDragEnd: (DragEndDetails details) {}, + ), + ), + // 头部、底部控制条 if (_.controlsEnabled) Obx( @@ -245,26 +516,7 @@ class _PLVideoPlayerState extends State ); }, ), - // 长按倍速 - Obx( - () => Align( - alignment: Alignment.topCenter, - child: FractionalTranslation( - translation: const Offset(0.0, 1.5), // 上下偏移量(负数向上偏移) - child: Visibility( - visible: _.doubleSpeedStatus.value, - child: const Text( - '** 倍速中 **', - style: TextStyle( - fontSize: 13, - backgroundColor: Color(0xaa000000), - color: Colors.white, - ), - ), - ), - ), - ), - ), + // 锁 if (_.controlsEnabled) Obx( @@ -301,44 +553,8 @@ class _PLVideoPlayerState extends State return Container(); } }), - // 时间进度 - /// TDDO 样式 - Obx( - () => Align( - alignment: Alignment.topCenter, - child: FractionalTranslation( - translation: const Offset(0.0, 2.5), // 上下偏移量(负数向上偏移) - child: Visibility( - visible: _.isSliderMoving.value, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Obx(() { - return Text( - _.sliderTempPosition.value.inMinutes >= 60 - ? printDurationWithHours(_.sliderTempPosition.value) - : printDuration(_.sliderTempPosition.value), - style: textStyle, - ); - }), - const SizedBox(width: 2), - const Text('/', style: textStyle), - const SizedBox(width: 2), - Obx( - () => Text( - _.duration.value.inMinutes >= 60 - ? printDurationWithHours(_.duration.value) - : printDuration(_.duration.value), - style: textStyle, - ), - ), - ], - ), - ), - ), - ), - ), - // 点击 快进/快退 + + /// 点击 快进/快退 if (_mountSeekBackwardButton || _mountSeekForwardButton) Positioned.fill( child: Row( From ffd1f0a24a24db1f128e8b9577d954ef11ed70ca Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 2 Aug 2023 23:42:36 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E5=85=A8=E5=B1=8F=E6=92=AD?= =?UTF-8?q?=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Podfile.lock | 6 ++ lib/pages/main/view.dart | 2 + lib/pages/video/detail/controller.dart | 21 +++--- lib/pages/video/detail/view.dart | 71 ++++++++++--------- lib/plugin/pl_player/controller.dart | 21 ++++++ lib/plugin/pl_player/index.dart | 2 + .../pl_player/models/fullscreen_mode.dart | 9 +++ lib/plugin/pl_player/utils/fullscreen.dart | 40 +++++++++++ lib/plugin/pl_player/view.dart | 20 +++--- .../pl_player/widgets/bottom_control.dart | 56 ++++++++++++--- pubspec.lock | 8 +++ pubspec.yaml | 1 + 12 files changed, 197 insertions(+), 60 deletions(-) create mode 100644 lib/plugin/pl_player/models/fullscreen_mode.dart create mode 100644 lib/plugin/pl_player/utils/fullscreen.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 65ff98a1..ed152c62 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - auto_orientation (0.0.1): + - Flutter - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift @@ -41,6 +43,7 @@ PODS: - Flutter DEPENDENCIES: + - auto_orientation (from `.symlinks/plugins/auto_orientation/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) @@ -65,6 +68,8 @@ SPEC REPOS: - ReachabilitySwift EXTERNAL SOURCES: + auto_orientation: + :path: ".symlinks/plugins/auto_orientation/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: @@ -101,6 +106,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: + auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 95a1ff13..da36cc49 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -98,10 +98,12 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { Box localCache = GStrorage.localCache; + double statusBarHeight = MediaQuery.of(context).padding.top; double sheetHeight = MediaQuery.of(context).size.height - MediaQuery.of(context).padding.top - MediaQuery.of(context).size.width * 9 / 16; localCache.put('sheetHeight', sheetHeight); + localCache.put('statusBarHeight', statusBarHeight); return Scaffold( body: FadeTransition( opacity: _fadeAnimation!, diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index d5ab79ff..d8b7fe43 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -109,21 +109,21 @@ class VideoDetailController extends GetxController /// 根据currentVideoQa 重新设置videoUrl VideoItem firstVideo = data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code); - String videoUrl = firstVideo.baseUrl!; + // String videoUrl = firstVideo.baseUrl!; /// 根据currentAudioQa 重新设置audioUrl AudioItem firstAudio = data.dash!.audio!.firstWhere((i) => i.id == currentAudioQa.code); String audioUrl = firstAudio.baseUrl ?? ''; - playerInit(videoUrl, audioUrl, defaultST: position); + playerInit(firstVideo, audioUrl, defaultST: position); } - playerInit(source, audioSource, + playerInit(firstVideo, audioSource, {Duration defaultST = Duration.zero, int duration = 0}) async { plPlayerController.setDataSource( DataSource( - videoSource: source, + videoSource: firstVideo.baseUrl, audioSource: audioSource, type: DataSourceType.network, httpHeaders: { @@ -137,6 +137,9 @@ class VideoDetailController extends GetxController autoplay: autoPlay.value, seekTo: defaultST, duration: Duration(milliseconds: duration), + // 宽>高 水平 否则 垂直 + direction: + firstVideo.width - firstVideo.height > 0 ? 'horizontal' : 'vertical', ); } @@ -153,7 +156,7 @@ class VideoDetailController extends GetxController /// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量 VideoItem firstVideo = data.dash!.video!.first; - String videoUrl = firstVideo.baseUrl!; + // String videoUrl = firstVideo.baseUrl!; // currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!; @@ -162,15 +165,17 @@ class VideoDetailController extends GetxController data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem(); String audioUrl = firstAudio.baseUrl ?? ''; // - currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!; - + if (firstAudio.id != null) { + currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!; + } playerInit( - videoUrl, + firstVideo, audioUrl, defaultST: Duration(milliseconds: data.lastPlayTime!), duration: data.timeLength ?? 0, ); } + return result; } void loopHeartBeat() { diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 9f16ecf8..3058282e 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/sliver_header.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart'; @@ -11,6 +12,7 @@ import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/introduction/index.dart'; import 'package:pilipala/pages/video/detail/related/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; +import 'package:pilipala/utils/storage.dart'; import 'widgets/app_bar.dart'; import 'widgets/header_control.dart'; @@ -32,11 +34,14 @@ class _VideoDetailPageState extends State final ScrollController _extendNestCtr = ScrollController(); late StreamController appbarStream; - bool isPlay = false; PlayerStatus playerStatus = PlayerStatus.playing; bool isShowCover = true; double doubleOffset = 0; + Box localCache = GStrorage.localCache; + late double statusBarHeight; + final videoHeight = Get.size.width * 9 / 16; + @override void initState() { super.initState(); @@ -46,14 +51,12 @@ class _VideoDetailPageState extends State videoDetailController.markHeartBeat(); playerStatus = status; if (status == PlayerStatus.playing) { - isPlay = false; isShowCover = false; setState(() {}); videoDetailController.loopHeartBeat(); } else { videoDetailController.timer!.cancel(); - isPlay = true; - setState(() {}); + // setState(() {}); // 播放完成停止 or 切换下一个 if (status == PlayerStatus.completed) { // 当只有1p或多p未打开自动播放时,播放完成还原进度条,展示控制栏 @@ -73,6 +76,8 @@ class _VideoDetailPageState extends State appbarStream.add(offset); }, ); + + statusBarHeight = localCache.get('statusBarHeight'); } void continuePlay() async { @@ -121,7 +126,6 @@ class _VideoDetailPageState extends State @override Widget build(BuildContext context) { - final double statusBarHeight = MediaQuery.of(context).padding.top; final videoHeight = MediaQuery.of(context).size.width * 9 / 16; final double pinnedHeaderHeight = statusBarHeight + kToolbarHeight + videoHeight; @@ -133,7 +137,9 @@ class _VideoDetailPageState extends State Scaffold( resizeToAvoidBottomInset: false, key: videoDetailController.scaffoldKey, - backgroundColor: Colors.transparent, + // fix 1px black line + // backgroundColor: Colors.transparent, + backgroundColor: Theme.of(context).colorScheme.background, body: ExtendedNestedScrollView( controller: _extendNestCtr, headerSliverBuilder: @@ -150,8 +156,7 @@ class _VideoDetailPageState extends State backgroundColor: Theme.of(context).colorScheme.background, flexibleSpace: FlexibleSpaceBar( background: Padding( - padding: EdgeInsets.only( - top: MediaQuery.of(context).padding.top), + padding: EdgeInsets.only(top: statusBarHeight), child: LayoutBuilder( builder: (context, boxConstraints) { double maxWidth = boxConstraints.maxWidth; @@ -187,33 +192,31 @@ class _VideoDetailPageState extends State ), /// 关闭自动播放时 手动播放 - Obx( - () => Visibility( - visible: isShowCover && - videoDetailController - .isEffective.value && - !videoDetailController.autoPlay.value, - child: Positioned( - right: 12, - bottom: 6, - child: TextButton.icon( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty - .resolveWith((states) { - return Theme.of(context) - .colorScheme - .primaryContainer; - }), - ), - onPressed: () => videoDetailController - .handlePlay(), - icon: const Icon( - Icons.play_circle_outline, - size: 20, - ), - label: const Text('Play'), + Visibility( + visible: isShowCover && + videoDetailController + .isEffective.value && + !videoDetailController.autoPlay.value, + child: Positioned( + right: 12, + bottom: 6, + child: TextButton.icon( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.resolveWith( + (states) { + return Theme.of(context) + .colorScheme + .primaryContainer; + }), ), + onPressed: () => + videoDetailController.handlePlay(), + icon: const Icon( + Icons.play_circle_outline, + size: 20, + ), + label: const Text('Play'), ), ), ), diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 8f41c674..a7c8676a 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:flutter/material.dart'; import 'package:flutter/painting.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; @@ -53,6 +54,9 @@ class PlPlayerController { final Rx _showBrightnessStatus = false.obs; final Rx _doubleSpeedStatus = false.obs; final Rx _controlsLock = false.obs; + final Rx _isFullScreen = false.obs; + + final Rx _direction = 'horizontal'.obs; Rx videoFitChanged = false.obs; final Rx _videoFit = Rx(BoxFit.fill); @@ -82,6 +86,8 @@ class PlPlayerController { BoxFit.scaleDown ]; + PreferredSizeWidget? headerControl; + /// 数据加载监听 Stream get onDataStatusChanged => dataStatus.status.stream; @@ -160,6 +166,12 @@ class PlPlayerController { /// 屏幕锁 为true时,关闭控制栏 Rx get controlsLock => _controlsLock; + /// 全屏状态 + Rx get isFullScreen => _isFullScreen; + + /// 全屏方向 + Rx get direction => _direction; + PlPlayerController({ // 直播间 传false 关闭控制栏 this.controlsEnabled = true, @@ -197,6 +209,9 @@ class PlPlayerController { double? width, double? height, Duration? duration, + // 方向 + String? direction, + // 全屏模式 }) async { try { _autoPlay = autoplay; @@ -207,6 +222,8 @@ class PlPlayerController { _playbackSpeed.value = speed; // 初始化数据加载状态 dataStatus.status.value = DataStatus.loading; + // 初始化全屏方向 + _direction.value = direction ?? 'horizontal'; if (_videoPlayerController != null && _videoPlayerController!.state.playing) { @@ -624,6 +641,10 @@ class PlPlayerController { showControls.value = !val; } + void toggleFullScreen(bool val) { + _isFullScreen.value = val; + } + /// 截屏 Future screenshot() async { final Uint8List? screenshot = diff --git a/lib/plugin/pl_player/index.dart b/lib/plugin/pl_player/index.dart index 721d5040..05cdffad 100644 --- a/lib/plugin/pl_player/index.dart +++ b/lib/plugin/pl_player/index.dart @@ -8,3 +8,5 @@ export './models/data_status.dart'; export './widgets/common_btn.dart'; export './models/play_speed.dart'; export './widgets/app_bar_ani.dart'; +export './utils/fullscreen.dart'; +export './utils.dart'; diff --git a/lib/plugin/pl_player/models/fullscreen_mode.dart b/lib/plugin/pl_player/models/fullscreen_mode.dart new file mode 100644 index 00000000..1080b6c6 --- /dev/null +++ b/lib/plugin/pl_player/models/fullscreen_mode.dart @@ -0,0 +1,9 @@ +// 全屏模式 +enum FullScreenMode { + // 根据内容自适应 + auto, + // 始终竖屏 + vertical, + // 始终横屏 + horizontal +} diff --git a/lib/plugin/pl_player/utils/fullscreen.dart b/lib/plugin/pl_player/utils/fullscreen.dart new file mode 100644 index 00000000..4f5ca948 --- /dev/null +++ b/lib/plugin/pl_player/utils/fullscreen.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:auto_orientation/auto_orientation.dart'; +import 'package:flutter/services.dart'; + +//横屏 +/// 低版本xcode不支持auto_orientation +Future landScape() async { + if (Platform.isAndroid || Platform.isIOS) { + await AutoOrientation.landscapeAutoMode(forceSensor: true); + } +} + +//竖屏 +Future verticalScreen() async { + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + ]); +} + +Future enterFullScreen() async { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.immersiveSticky, + ); +} + +//退出全屏显示 +Future exitFullScreen() async { + late SystemUiMode mode; + if ((Platform.isAndroid && + (await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) || + !Platform.isAndroid) { + mode = SystemUiMode.edgeToEdge; + } else { + mode = SystemUiMode.manual; + } + await SystemChrome.setEnabledSystemUIMode(mode, + overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom]); +} diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 5fa5add9..6c51508d 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -75,6 +75,7 @@ class _PLVideoPlayerState extends State animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300)); videoController = widget.controller.videoController!; + widget.controller.headerControl = widget.headerControl; Future.microtask(() async { try { @@ -149,6 +150,7 @@ class _PLVideoPlayerState extends State @override Widget build(BuildContext context) { + print('🌹🌹🌹🌹🌹:33333'); final _ = widget.controller; Color colorTheme = Theme.of(context).colorScheme.primary; TextStyle subTitleStyle = const TextStyle( @@ -387,6 +389,7 @@ class _PLVideoPlayerState extends State // 双击左边区域 👈 onDoubleTapSeekBackward(); } else if (tapPosition < sectionWidth * 2) { + print('🌹🌹🌹🌹🌹:333356555553'); if (_.playerStatus.status.value == PlayerStatus.playing) { _.togglePlay(); } else { @@ -443,15 +446,16 @@ class _PLVideoPlayerState extends State Obx( () => Column( children: [ - ClipRect( - clipBehavior: Clip.hardEdge, - child: AppBarAni( - controller: animationController, - visible: !_.controlsLock.value && _.showControls.value, - position: 'top', - child: widget.headerControl!, + if (widget.headerControl != null) + ClipRect( + clipBehavior: Clip.hardEdge, + child: AppBarAni( + controller: animationController, + visible: !_.controlsLock.value && _.showControls.value, + position: 'top', + child: widget.headerControl!, + ), ), - ), const Spacer(), ClipRect( clipBehavior: Clip.hardEdge, diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index 08a3ef9f..8ce278e6 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -5,8 +5,6 @@ import 'package:get/get.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/widgets/play_pause_btn.dart'; -import '../utils.dart'; - class BottomControl extends StatelessWidget implements PreferredSizeWidget { final PlPlayerController? controller; const BottomControl({this.controller, Key? key}) : super(key: key); @@ -138,14 +136,52 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { // ), // const SizedBox(width: 4), // 全屏 - ComBtn( - icon: const Icon( - FontAwesomeIcons.expand, - size: 15, - color: Colors.white, - ), - fuc: () => {}, - ), + Obx(() => ComBtn( + icon: Icon( + _.isFullScreen.value + ? FontAwesomeIcons.a + : FontAwesomeIcons.expand, + size: 15, + color: Colors.white, + ), + fuc: () async { + if (!_.isFullScreen.value) { + /// 按照视频宽高比决定全屏方向 + if (_.direction.value == 'horizontal') { + /// 进入全屏 + await enterFullScreen(); + // 横屏 + await landScape(); + } else { + // 竖屏 + await verticalScreen(); + } + + _.toggleFullScreen(true); + var result = await showDialog( + context: Get.context!, + builder: (context) => Dialog.fullscreen( + backgroundColor: Colors.black, + child: PLVideoPlayer( + controller: _, + headerControl: _.headerControl, + ), + ), + ); + if (result == null) { + // 退出全屏 + exitFullScreen(); + await verticalScreen(); + _.toggleFullScreen(false); + } + } else { + Get.back(); + exitFullScreen(); + await verticalScreen(); + _.toggleFullScreen(false); + } + }, + )), ], ), ], diff --git a/pubspec.lock b/pubspec.lock index 494f2fe1..db99cf2a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + auto_orientation: + dependency: "direct main" + description: + name: auto_orientation + sha256: cd56bb59b36fa54cc28ee254bc600524f022a4862f31d5ab20abd7bb1c54e678 + url: "https://pub.dev" + source: hosted + version: "2.3.1" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 525e0e1d..acc018c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -107,6 +107,7 @@ dependencies: universal_platform: ^1.0.0+1 # 进度条 audio_video_progress_bar: ^1.0.1 + auto_orientation: ^2.3.1 dev_dependencies: flutter_test: From 99a3ff81e0c4b651233a74c1f0dc4e71c4b6b279 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 3 Aug 2023 22:54:37 +0800 Subject: [PATCH 3/4] =?UTF-8?q?mod:=20=E6=BB=91=E5=8A=A8=E6=89=8B=E5=8A=BF?= =?UTF-8?q?=E8=BF=9B=E5=85=A5/=E9=80=80=E5=87=BA=E5=85=A8=E5=B1=8F?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/plugin/pl_player/view.dart | 102 +++++++++++++----- .../pl_player/widgets/bottom_control.dart | 62 +++-------- 2 files changed, 92 insertions(+), 72 deletions(-) diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 6c51508d..07745901 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:media_kit/media_kit.dart'; @@ -15,11 +16,11 @@ import 'package:pilipala/utils/feed_back.dart'; import 'package:screen_brightness/screen_brightness.dart'; import 'package:volume_controller/volume_controller.dart'; +import 'utils/fullscreen.dart'; import 'widgets/backward_seek.dart'; import 'widgets/bottom_control.dart'; import 'widgets/common_btn.dart'; import 'widgets/forward_seek.dart'; -import 'widgets/play_pause_btn.dart'; class PLVideoPlayer extends StatefulWidget { final PlPlayerController controller; @@ -55,6 +56,8 @@ class _PLVideoPlayerState extends State bool _volumeIndicator = false; Timer? _volumeTimer; + double _distance = 0.0; + bool _volumeInterceptEventStream = false; void onDoubleTapSeekBackward() { @@ -142,15 +145,65 @@ class _PLVideoPlayerState extends State }); } + Future triggerFullScreen() async { + PlPlayerController _ = widget.controller; + if (!_.isFullScreen.value) { + /// 按照视频宽高比决定全屏方向 + if (_.direction.value == 'horizontal') { + /// 进入全屏 + await enterFullScreen(); + // 横屏 + await landScape(); + } else { + // 竖屏 + await verticalScreen(); + } + + _.toggleFullScreen(true); + var result = await showDialog( + context: Get.context!, + useSafeArea: false, + builder: (context) => Dialog.fullscreen( + child: Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + primary: false, + toolbarHeight: 0, + backgroundColor: Colors.black, + systemOverlayStyle: SystemUiOverlayStyle.light, + ), + body: SafeArea( + bottom: true, + child: PLVideoPlayer( + controller: _, + headerControl: _.headerControl, + ), + ), + ), + ), + ); + if (result == null) { + // 退出全屏 + exitFullScreen(); + await verticalScreen(); + _.toggleFullScreen(false); + } + } else { + Get.back(); + exitFullScreen(); + await verticalScreen(); + _.toggleFullScreen(false); + } + } + @override void dispose() { - super.dispose(); animationController.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { - print('🌹🌹🌹🌹🌹:33333'); final _ = widget.controller; Color colorTheme = Theme.of(context).colorScheme.primary; TextStyle subTitleStyle = const TextStyle( @@ -389,7 +442,6 @@ class _PLVideoPlayerState extends State // 双击左边区域 👈 onDoubleTapSeekBackward(); } else if (tapPosition < sectionWidth * 2) { - print('🌹🌹🌹🌹🌹:333356555553'); if (_.playerStatus.status.value == PlayerStatus.playing) { _.togglePlay(); } else { @@ -415,23 +467,36 @@ class _PLVideoPlayerState extends State onHorizontalDragUpdate: (DragUpdateDetails details) {}, onHorizontalDragEnd: (DragEndDetails details) {}, // 垂直方向 音量/亮度调节 - onVerticalDragUpdate: (DragUpdateDetails details) { + onVerticalDragUpdate: (DragUpdateDetails details) async { final totalWidth = MediaQuery.of(context).size.width; final tapPosition = details.localPosition.dx; final sectionWidth = totalWidth / 3; - + final delta = details.delta.dy; if (tapPosition < sectionWidth) { // 左边区域 👈 - final delta = details.delta.dy; final brightness = _brightnessValue - delta / 100.0; final result = brightness.clamp(0.0, 1.0); setBrightness(result); } else if (tapPosition < sectionWidth * 2) { // 全屏 - print('全屏'); + final double dy = details.delta.dy; + const double threshold = 7.0; // 滑动阈值 + if (dy > _distance && dy > threshold) { + if (!_.isFullScreen.value) { + await triggerFullScreen(); + } + _distance = 0.0; + } else if (dy < _distance && dy < -threshold) { + if (_.isFullScreen.value) { + await triggerFullScreen(); + } + _distance = 0.0; + } + _distance = dy; + + // triggerFullScreen(); } else { // 右边区域 👈 - final delta = details.delta.dy; final volume = _volumeValue - delta / 100.0; final result = volume.clamp(0.0, 1.0); setVolume(result); @@ -463,7 +528,9 @@ class _PLVideoPlayerState extends State controller: animationController, visible: !_.controlsLock.value && _.showControls.value, position: 'bottom', - child: BottomControl(controller: widget.controller), + child: BottomControl( + controller: widget.controller, + triggerFullScreen: triggerFullScreen), ), ), ], @@ -660,18 +727,3 @@ class _PLVideoPlayerState extends State ); } } - -class MSliderTrackShape extends RoundedRectSliderTrackShape { - @override - Rect getPreferredRect({ - required RenderBox parentBox, - Offset offset = Offset.zero, - SliderThemeData? sliderTheme, - bool isEnabled = false, - bool isDiscrete = false, - }) { - final double trackLeft = offset.dx; - final double trackWidth = parentBox.size.width; - return Rect.fromLTWH(trackLeft, -1, trackWidth, 3); - } -} diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index 8ce278e6..32b98a19 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -7,7 +7,9 @@ import 'package:pilipala/plugin/pl_player/widgets/play_pause_btn.dart'; class BottomControl extends StatelessWidget implements PreferredSizeWidget { final PlPlayerController? controller; - const BottomControl({this.controller, Key? key}) : super(key: key); + final Function? triggerFullScreen; + const BottomControl({this.controller, this.triggerFullScreen, Key? key}) + : super(key: key); @override Size get preferredSize => const Size(double.infinity, kToolbarHeight); @@ -136,52 +138,18 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { // ), // const SizedBox(width: 4), // 全屏 - Obx(() => ComBtn( - icon: Icon( - _.isFullScreen.value - ? FontAwesomeIcons.a - : FontAwesomeIcons.expand, - size: 15, - color: Colors.white, - ), - fuc: () async { - if (!_.isFullScreen.value) { - /// 按照视频宽高比决定全屏方向 - if (_.direction.value == 'horizontal') { - /// 进入全屏 - await enterFullScreen(); - // 横屏 - await landScape(); - } else { - // 竖屏 - await verticalScreen(); - } - - _.toggleFullScreen(true); - var result = await showDialog( - context: Get.context!, - builder: (context) => Dialog.fullscreen( - backgroundColor: Colors.black, - child: PLVideoPlayer( - controller: _, - headerControl: _.headerControl, - ), - ), - ); - if (result == null) { - // 退出全屏 - exitFullScreen(); - await verticalScreen(); - _.toggleFullScreen(false); - } - } else { - Get.back(); - exitFullScreen(); - await verticalScreen(); - _.toggleFullScreen(false); - } - }, - )), + Obx( + () => ComBtn( + icon: Icon( + _.isFullScreen.value + ? FontAwesomeIcons.a + : FontAwesomeIcons.expand, + size: 15, + color: Colors.white, + ), + fuc: () => triggerFullScreen!(), + ), + ), ], ), ], From df41a53eff169700c09627ce8275aeb0e93a32a2 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 3 Aug 2023 22:55:16 +0800 Subject: [PATCH 4/4] =?UTF-8?q?mod:=20=E8=A7=86=E9=A2=91=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E9=A1=B5=E4=BB=A3=E7=A0=81=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/video/detail/controller.dart | 12 ++- lib/pages/video/detail/view.dart | 144 ++++++++++++++++--------- 2 files changed, 98 insertions(+), 58 deletions(-) diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index d8b7fe43..597030ed 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -54,6 +54,8 @@ class VideoDetailController extends GetxController RxBool autoPlay = true.obs; // 视频资源是否有效 RxBool isEffective = true.obs; + // 封面图的展示 + RxBool isShowCover = true.obs; @override void onInit() { @@ -74,7 +76,7 @@ class VideoDetailController extends GetxController heroTag = Get.arguments['heroTag']; } tabCtr = TabController(length: 2, vsync: this); - queryVideoUrl(); + // queryVideoUrl(); } showReplyReplyPanel() { @@ -119,9 +121,9 @@ class VideoDetailController extends GetxController playerInit(firstVideo, audioUrl, defaultST: position); } - playerInit(firstVideo, audioSource, + Future playerInit(firstVideo, audioSource, {Duration defaultST = Duration.zero, int duration = 0}) async { - plPlayerController.setDataSource( + await plPlayerController.setDataSource( DataSource( videoSource: firstVideo.baseUrl, audioSource: audioSource, @@ -149,7 +151,7 @@ class VideoDetailController extends GetxController } // 视频链接 - queryVideoUrl() async { + Future queryVideoUrl() async { var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid); if (result['status']) { data = result['data']; @@ -168,7 +170,7 @@ class VideoDetailController extends GetxController if (firstAudio.id != null) { currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!; } - playerInit( + await playerInit( firstVideo, audioUrl, defaultST: Duration(milliseconds: data.lastPlayTime!), diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 3058282e..7325cfd0 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -35,12 +35,13 @@ class _VideoDetailPageState extends State late StreamController appbarStream; PlayerStatus playerStatus = PlayerStatus.playing; - bool isShowCover = true; + // bool isShowCover = true; double doubleOffset = 0; Box localCache = GStrorage.localCache; late double statusBarHeight; final videoHeight = Get.size.width * 9 / 16; + late Future _futureBuilderFuture; @override void initState() { @@ -51,12 +52,10 @@ class _VideoDetailPageState extends State videoDetailController.markHeartBeat(); playerStatus = status; if (status == PlayerStatus.playing) { - isShowCover = false; - setState(() {}); + videoDetailController.isShowCover.value = false; videoDetailController.loopHeartBeat(); } else { videoDetailController.timer!.cancel(); - // setState(() {}); // 播放完成停止 or 切换下一个 if (status == PlayerStatus.completed) { // 当只有1p或多p未打开自动播放时,播放完成还原进度条,展示控制栏 @@ -78,6 +77,7 @@ class _VideoDetailPageState extends State ); statusBarHeight = localCache.get('statusBarHeight'); + _futureBuilderFuture = videoDetailController.queryVideoUrl(); } void continuePlay() async { @@ -165,60 +165,98 @@ class _VideoDetailPageState extends State tag: videoDetailController.heroTag, child: Stack( children: [ - if (plPlayerController! - .videoPlayerController != - null) - PLVideoPlayer( - controller: plPlayerController!, - headerControl: HeaderControl( - controller: plPlayerController, - videoDetailCtr: videoDetailController, - ), - ), - Visibility( - visible: isShowCover, - child: Positioned( - top: 0, - left: 0, - right: 0, - child: NetworkImgLayer( - type: 'emote', - src: videoDetailController - .videoItem['pic'], - width: maxWidth, - height: maxHeight, + FutureBuilder( + future: _futureBuilderFuture, + builder: ((context, snapshot) { + if (snapshot.hasData && + snapshot.data['status']) { + return PLVideoPlayer( + controller: plPlayerController!, + headerControl: HeaderControl( + controller: plPlayerController, + videoDetailCtr: + videoDetailController, + ), + ); + } else { + return const SizedBox(); + } + }), + ), + Obx( + () => Visibility( + visible: videoDetailController + .isShowCover.value, + child: Positioned( + top: 0, + left: 0, + right: 0, + child: NetworkImgLayer( + type: 'emote', + src: videoDetailController + .videoItem['pic'], + width: maxWidth, + height: maxHeight, + ), ), ), ), /// 关闭自动播放时 手动播放 - Visibility( - visible: isShowCover && - videoDetailController - .isEffective.value && - !videoDetailController.autoPlay.value, - child: Positioned( - right: 12, - bottom: 6, - child: TextButton.icon( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.resolveWith( - (states) { - return Theme.of(context) - .colorScheme - .primaryContainer; - }), - ), - onPressed: () => - videoDetailController.handlePlay(), - icon: const Icon( - Icons.play_circle_outline, - size: 20, - ), - label: const Text('Play'), - ), - ), + Obx( + () => Visibility( + visible: videoDetailController + .isShowCover.value && + videoDetailController + .isEffective.value && + !videoDetailController + .autoPlay.value, + child: Stack( + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + child: AppBar( + primary: false, + backgroundColor: + Colors.transparent, + actions: [ + /// TODO + IconButton( + tooltip: '稍后再看', + onPressed: () {}, + icon: const Icon(Icons + .history_outlined)) + ], + ), + ), + Positioned( + right: 12, + bottom: 6, + child: TextButton.icon( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty + .resolveWith( + (states) { + return Theme.of(context) + .colorScheme + .primaryContainer; + }), + ), + onPressed: () => + videoDetailController + .handlePlay(), + icon: const Icon( + Icons.play_circle_outline, + size: 20, + ), + label: const Text('Play'), + ), + ), + ], + )), ), ], ),