diff --git a/assets/images/run-pokemon.gif b/assets/images/run-pokemon.gif new file mode 100644 index 000000000..bca20afc2 Binary files /dev/null and b/assets/images/run-pokemon.gif differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 65ff98a11..ed152c62b 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 4553046f0..9018c629a 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 d5ab79ff9..597030eda 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() { @@ -109,21 +111,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, + Future playerInit(firstVideo, audioSource, {Duration defaultST = Duration.zero, int duration = 0}) async { - plPlayerController.setDataSource( + await plPlayerController.setDataSource( DataSource( - videoSource: source, + videoSource: firstVideo.baseUrl, audioSource: audioSource, type: DataSourceType.network, httpHeaders: { @@ -137,6 +139,9 @@ class VideoDetailController extends GetxController autoplay: autoPlay.value, seekTo: defaultST, duration: Duration(milliseconds: duration), + // 宽>高 水平 否则 垂直 + direction: + firstVideo.width - firstVideo.height > 0 ? 'horizontal' : 'vertical', ); } @@ -146,14 +151,14 @@ class VideoDetailController extends GetxController } // 视频链接 - queryVideoUrl() async { + Future queryVideoUrl() async { var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid); if (result['status']) { data = result['data']; /// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量 VideoItem firstVideo = data.dash!.video!.first; - String videoUrl = firstVideo.baseUrl!; + // String videoUrl = firstVideo.baseUrl!; // currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!; @@ -162,15 +167,17 @@ class VideoDetailController extends GetxController data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem(); String audioUrl = firstAudio.baseUrl ?? ''; // - currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!; - - playerInit( - videoUrl, + if (firstAudio.id != null) { + currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!; + } + await playerInit( + 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 68edd92aa..7325cfd0c 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,15 @@ class _VideoDetailPageState extends State final ScrollController _extendNestCtr = ScrollController(); late StreamController appbarStream; - bool isPlay = false; 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() { super.initState(); @@ -46,19 +52,16 @@ class _VideoDetailPageState extends State videoDetailController.markHeartBeat(); playerStatus = status; if (status == PlayerStatus.playing) { - isPlay = false; - isShowCover = false; - setState(() {}); + videoDetailController.isShowCover.value = false; videoDetailController.loopHeartBeat(); } else { videoDetailController.timer!.cancel(); - isPlay = true; - setState(() {}); // 播放完成停止 or 切换下一个 if (status == PlayerStatus.completed) { // 当只有1p或多p未打开自动播放时,播放完成还原进度条,展示控制栏 plPlayerController!.seekTo(Duration.zero); plPlayerController!.onLockControl(false); + plPlayerController!.videoPlayerController!.pause(); } } }, @@ -72,6 +75,9 @@ class _VideoDetailPageState extends State appbarStream.add(offset); }, ); + + statusBarHeight = localCache.get('statusBarHeight'); + _futureBuilderFuture = videoDetailController.queryVideoUrl(); } void continuePlay() async { @@ -120,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; @@ -132,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: @@ -149,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; @@ -159,28 +165,39 @@ 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, + ), ), ), ), @@ -188,33 +205,58 @@ 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'), - ), - ), - ), + 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'), + ), + ), + ], + )), ), ], ), diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 8f41c6745..a7c8676a5 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 997776f9f..05cdffadf 100644 --- a/lib/plugin/pl_player/index.dart +++ b/lib/plugin/pl_player/index.dart @@ -7,3 +7,6 @@ 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'; +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 000000000..1080b6c6c --- /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 000000000..4f5ca9489 --- /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 0dac67ebe..077459016 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -1,5 +1,8 @@ +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'; @@ -10,12 +13,14 @@ 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 '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; @@ -43,6 +48,18 @@ 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; + + double _distance = 0.0; + + bool _volumeInterceptEventStream = false; + void onDoubleTapSeekBackward() { setState(() { _mountSeekBackwardButton = true; @@ -61,12 +78,128 @@ 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 { + 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; + }); + } + }); + } + + 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 @@ -90,25 +223,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,69 +232,295 @@ 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) 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 brightness = _brightnessValue - delta / 100.0; + final result = brightness.clamp(0.0, 1.0); + setBrightness(result); + } else if (tapPosition < sectionWidth * 2) { + // 全屏 + 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 volume = _volumeValue - delta / 100.0; + final result = volume.clamp(0.0, 1.0); + setVolume(result); + } + }, + onVerticalDragEnd: (DragEndDetails details) {}, + ), + ), + // 头部、底部控制条 if (_.controlsEnabled) 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, @@ -188,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), ), ), ], @@ -245,26 +587,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 +624,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( @@ -440,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 08a3ef9f5..32b98a195 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -5,11 +5,11 @@ 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); + final Function? triggerFullScreen; + const BottomControl({this.controller, this.triggerFullScreen, Key? key}) + : super(key: key); @override Size get preferredSize => const Size(double.infinity, kToolbarHeight); @@ -138,13 +138,17 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { // ), // const SizedBox(width: 4), // 全屏 - ComBtn( - icon: const Icon( - FontAwesomeIcons.expand, - size: 15, - color: Colors.white, + Obx( + () => ComBtn( + icon: Icon( + _.isFullScreen.value + ? FontAwesomeIcons.a + : FontAwesomeIcons.expand, + size: 15, + color: Colors.white, + ), + fuc: () => triggerFullScreen!(), ), - fuc: () => {}, ), ], ), diff --git a/pubspec.lock b/pubspec.lock index 494f2fe13..db99cf2a3 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 525e0e1da..acc018c79 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: