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 95a1ff137..da36cc49d 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..d8b7fe432 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 9f16ecf8b..3058282e7 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 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 721d50407..05cdffadf 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 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 5fa5add96..6c51508d2 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 08a3ef9f5..8ce278e6c 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 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: