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..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() { @@ -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 9f16ecf8..7325cfd0 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,14 +52,10 @@ 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未打开自动播放时,播放完成还原进度条,展示控制栏 @@ -73,6 +75,9 @@ class _VideoDetailPageState extends State appbarStream.add(offset); }, ); + + statusBarHeight = localCache.get('statusBarHeight'); + _futureBuilderFuture = videoDetailController.queryVideoUrl(); } 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; @@ -160,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, + ), ), ), ), @@ -189,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 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..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() { @@ -75,6 +78,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 { @@ -141,10 +145,61 @@ 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 @@ -412,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); @@ -443,15 +511,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, @@ -459,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), ), ), ], @@ -656,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 08a3ef9f..32b98a19 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 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: