diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 52221c7c..9ddd59cd 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -17,6 +17,8 @@ import 'package:PiliPalaX/utils/feed_back.dart'; import 'package:PiliPalaX/utils/storage.dart'; import 'package:screen_brightness/screen_brightness.dart'; +import '../../common/widgets/audio_video_progress_bar.dart'; +import '../../utils/utils.dart'; import 'models/bottom_progress_behavior.dart'; import 'widgets/app_bar_ani.dart'; import 'widgets/backward_seek.dart'; @@ -79,6 +81,11 @@ class _PLVideoPlayerState extends State DateTime? lastFullScreenToggleTime; // 记录上一次音量调整值作平均,避免音量调整抖动 double lastVolume = -1.0; + // 是否在调整固定进度条 + RxBool draggingFixedProgressBar = false.obs; + // 阅读器限制 + Timer? _accessibilityDebounce; + double _lastAnnouncedValue = -1; void onDoubleTapSeekBackward() { _ctr.onDoubleTapSeekBackward(); @@ -127,8 +134,8 @@ class _PLVideoPlayerState extends State defaultValue: BtmProgresBehavior.values.first.code); enableQuickDouble = setting.get(SettingBoxKey.enableQuickDouble, defaultValue: true); - fullScreenGestureReverse = setting.get(SettingBoxKey.fullScreenGestureReverse, - defaultValue: false); + fullScreenGestureReverse = setting + .get(SettingBoxKey.fullScreenGestureReverse, defaultValue: false); enableBackgroundPlay = setting.get(SettingBoxKey.enableBackgroundPlay, defaultValue: false); Future.microtask(() async { @@ -281,10 +288,8 @@ class _PLVideoPlayerState extends State children: [ Obx(() { return Text( - _.sliderTempPosition.value.inMinutes >= 60 - ? printDurationWithHours( - _.sliderTempPosition.value) - : printDuration(_.sliderTempPosition.value), + Utils.timeFormat( + _.sliderTempPosition.value.inSeconds), style: textStyle, ); }), @@ -439,123 +444,131 @@ class _PLVideoPlayerState extends State top: 25, right: 15, bottom: 15, - child: GestureDetector( - onTap: () { - _.controls = !_.showControls.value; - }, - onDoubleTapDown: (TapDownDetails details) { - // live模式下禁用 锁定时🔒禁用 - if (_.videoType.value == 'live' || _.controlsLock.value) { - return; - } - RenderBox renderBox = _playerKey.currentContext!.findRenderObject() as RenderBox; - final double totalWidth = renderBox.size.width; - final double tapPosition = details.localPosition.dx; - final double sectionWidth = totalWidth / 3; - String type = 'left'; - if (tapPosition < sectionWidth) { - type = 'left'; - } else if (tapPosition < sectionWidth * 2) { - type = 'center'; - } else { - type = 'right'; - } - doubleTapFuc(type); - }, - onLongPressStart: (LongPressStartDetails detail) { - feedBack(); - _.setDoubleSpeedStatus(true); - }, - onLongPressEnd: (LongPressEndDetails details) { - _.setDoubleSpeedStatus(false); - }, + child: Semantics( + label: '双击开关播放控件,左右滑动调整进度', + child: GestureDetector( + onTap: () { + _.controls = !_.showControls.value; + }, + onDoubleTapDown: (TapDownDetails details) { + // live模式下禁用 锁定时🔒禁用 + if (_.videoType.value == 'live' || _.controlsLock.value) { + return; + } + RenderBox renderBox = + _playerKey.currentContext!.findRenderObject() as RenderBox; + final double totalWidth = renderBox.size.width; + final double tapPosition = details.localPosition.dx; + final double sectionWidth = totalWidth / 3; + String type = 'left'; + if (tapPosition < sectionWidth) { + type = 'left'; + } else if (tapPosition < sectionWidth * 2) { + type = 'center'; + } else { + type = 'right'; + } + doubleTapFuc(type); + }, + onLongPressStart: (LongPressStartDetails detail) { + feedBack(); + _.setDoubleSpeedStatus(true); + }, + onLongPressEnd: (LongPressEndDetails details) { + _.setDoubleSpeedStatus(false); + }, - /// 水平位置 快进 live模式下禁用 - onHorizontalDragUpdate: (DragUpdateDetails details) { - // live模式下禁用 锁定时🔒禁用 - if (_.videoType.value == 'live' || _.controlsLock.value) { - return; - } - // final double tapPosition = details.localPosition.dx; - final int curSliderPosition = - _.sliderPosition.value.inMilliseconds; - RenderBox renderBox = _playerKey.currentContext!.findRenderObject() as RenderBox; - final double scale = 90000 / renderBox.size.width; - final Duration pos = Duration( - milliseconds: - curSliderPosition + (details.delta.dx * scale).round()); - final Duration result = - pos.clamp(Duration.zero, _.duration.value); - _.onUpdatedSliderProgress(result); - _.onChangedSliderStart(); - // _initTapPositoin = tapPosition; - }, - onHorizontalDragEnd: (DragEndDetails details) { - if (_.videoType.value == 'live' || _.controlsLock.value) { - return; - } - _.onChangedSliderEnd(); - _.seekTo(_.sliderPosition.value, type: 'slider'); - }, - // 垂直方向 音量/亮度调节 - onVerticalDragUpdate: (DragUpdateDetails details) async { - RenderBox renderBox = _playerKey.currentContext!.findRenderObject() as RenderBox; - final double totalWidth = renderBox.size.width; - final double tapPosition = details.localPosition.dx; - final double sectionWidth = totalWidth / 3; - final double delta = details.delta.dy; + /// 水平位置 快进 live模式下禁用 + onHorizontalDragUpdate: (DragUpdateDetails details) { + // live模式下禁用 锁定时🔒禁用 + if (_.videoType.value == 'live' || _.controlsLock.value) { + return; + } + // final double tapPosition = details.localPosition.dx; + final int curSliderPosition = + _.sliderPosition.value.inMilliseconds; + RenderBox renderBox = + _playerKey.currentContext!.findRenderObject() as RenderBox; + final double scale = 90000 / renderBox.size.width; + final Duration pos = Duration( + milliseconds: + curSliderPosition + (details.delta.dx * scale).round()); + final Duration result = + pos.clamp(Duration.zero, _.duration.value); + _.onUpdatedSliderProgress(result); + _.onChangedSliderStart(); + // _initTapPositoin = tapPosition; + }, + onHorizontalDragEnd: (DragEndDetails details) { + if (_.videoType.value == 'live' || _.controlsLock.value) { + return; + } + _.onChangedSliderEnd(); + _.seekTo(_.sliderPosition.value, type: 'slider'); + }, + // 垂直方向 音量/亮度调节 + onVerticalDragUpdate: (DragUpdateDetails details) async { + RenderBox renderBox = + _playerKey.currentContext!.findRenderObject() as RenderBox; - /// 锁定时禁用 - if (_.controlsLock.value) { - return; - } - if (lastFullScreenToggleTime != null && - DateTime.now().difference(lastFullScreenToggleTime!) < - const Duration(milliseconds: 500)) { - return; - } - if (tapPosition < sectionWidth) { - // 左边区域 👈 - final double level = renderBox.size.height * 3; - final double brightness = - _ctr.brightnessValue.value - delta / level; - final double 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; // 滑动阈值 - void fullScreenTrigger(bool status) async { - lastFullScreenToggleTime = DateTime.now(); - await widget.controller.triggerFullScreen(status: status); + /// 锁定时禁用 + if (_.controlsLock.value) { + return; } - if (dy > _distance && dy > threshold) { - // 下滑退出全屏/进入全屏 - if (_.isFullScreen.value ^ fullScreenGestureReverse) { - fullScreenTrigger(fullScreenGestureReverse); + final double totalWidth = renderBox.size.width; + final double tapPosition = details.localPosition.dx; + final double sectionWidth = totalWidth / 3; + final double delta = details.delta.dy; + if (lastFullScreenToggleTime != null && + DateTime.now().difference(lastFullScreenToggleTime!) < + const Duration(milliseconds: 500)) { + return; + } + if (tapPosition < sectionWidth) { + // 左边区域 👈 + final double level = renderBox.size.height * 3; + final double brightness = + _ctr.brightnessValue.value - delta / level; + final double 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; // 滑动阈值 + void fullScreenTrigger(bool status) async { + lastFullScreenToggleTime = DateTime.now(); + await widget.controller.triggerFullScreen(status: status); } - _distance = 0.0; - } else if (dy < _distance && dy < -threshold) { - // 上划进入全屏/退出全屏 - if (!_.isFullScreen.value ^ fullScreenGestureReverse) { - fullScreenTrigger(!fullScreenGestureReverse); + + if (dy > _distance && dy > threshold) { + // 下滑退出全屏/进入全屏 + if (_.isFullScreen.value ^ fullScreenGestureReverse) { + fullScreenTrigger(fullScreenGestureReverse); + } + _distance = 0.0; + } else if (dy < _distance && dy < -threshold) { + // 上划进入全屏/退出全屏 + if (!_.isFullScreen.value ^ fullScreenGestureReverse) { + fullScreenTrigger(!fullScreenGestureReverse); + } + _distance = 0.0; } - _distance = 0.0; + _distance = dy; + } else { + // 右边区域 👈 + final double level = renderBox.size.height * 0.5; + if (lastVolume < 0) { + lastVolume = _ctr.volumeValue.value; + } + final double volume = + (lastVolume + _ctr.volumeValue.value - delta / level) / 2; + final double result = volume.clamp(0.0, 1.0); + lastVolume = result; + setVolume(result); } - _distance = dy; - } else { - // 右边区域 👈 - final double level = renderBox.size.height * 0.5; - if(lastVolume < 0) { - lastVolume = _ctr.volumeValue.value; - } - final double volume = (lastVolume + _ctr.volumeValue.value - delta / level)/2; - final double result = volume.clamp(0.0, 1.0); - lastVolume = result; - setVolume(result); - } - }, - onVerticalDragEnd: (DragEndDetails details) {}, + }, + onVerticalDragEnd: (DragEndDetails details) {}, + ), ), ), @@ -605,65 +618,84 @@ class _PLVideoPlayerState extends State } if (defaultBtmProgressBehavior == BtmProgresBehavior.alwaysHide.code) { - return nil; + return Container(); } if (defaultBtmProgressBehavior == BtmProgresBehavior.onlyShowFullScreen.code && !_.isFullScreen.value) { - return nil; + return Container(); } else if (defaultBtmProgressBehavior == BtmProgresBehavior.onlyHideFullScreen.code && _.isFullScreen.value) { - return nil; + return Container(); } if (_.videoType.value == 'live') { - return const SizedBox(); + return Container(); } if (value > max || max <= 0) { - return nil; + return Container(); } return Positioned( - bottom: -1.5, - left: 0, - right: 0, - child: ProgressBar( - progress: Duration(seconds: value), - buffered: Duration(seconds: buffer), - total: Duration(seconds: max), - progressBarColor: colorTheme, - baseBarColor: Colors.white.withOpacity(0.2), - bufferedBarColor: - Theme.of(context).colorScheme.primary.withOpacity(0.4), - timeLabelLocation: TimeLabelLocation.none, - thumbColor: colorTheme, - barHeight: 3, - thumbRadius: 0.0, - // onDragStart: (duration) { - // _.onChangedSliderStart(); - // }, - // onDragEnd: () { - // _.onChangedSliderEnd(); - // }, - // onDragUpdate: (details) { - // print(details); - // }, - // onSeek: (duration) { - // feedBack(); - // _.onChangedSlider(duration.inSeconds.toDouble()); - // _.seekTo(duration); - // }, - ), - // SlideTransition( - // position: Tween( - // begin: Offset.zero, - // end: const Offset(0, -1), - // ).animate(CurvedAnimation( - // parent: animationController, - // curve: Curves.easeInOut, - // )), - // child: ), - ); + bottom: -1, + left: 0, + right: 0, + child: Semantics( + // label: '${(value / max * 100).round()}%', + value: '${(value / max * 100).round()}%', + // enabled: false, + child: ProgressBar( + progress: Duration(seconds: value), + buffered: Duration(seconds: buffer), + total: Duration(seconds: max), + progressBarColor: colorTheme, + baseBarColor: Colors.white.withOpacity(0.2), + bufferedBarColor: + Theme.of(context).colorScheme.primary.withOpacity(0.4), + timeLabelLocation: TimeLabelLocation.none, + thumbColor: colorTheme, + barHeight: 3.5, + thumbRadius: draggingFixedProgressBar.value ? 7 : 4, + onDragStart: (duration) { + draggingFixedProgressBar.value = true; + feedBack(); + _.onChangedSliderStart(); + }, + onDragUpdate: (duration) { + double newProgress = duration.timeStamp.inSeconds / max; + if ((newProgress - _lastAnnouncedValue).abs() > 0.02) { + _accessibilityDebounce?.cancel(); + _accessibilityDebounce = + Timer(const Duration(milliseconds: 200), () { + SemanticsService.announce( + "${(newProgress * 100).round()}%", + TextDirection.ltr); + _lastAnnouncedValue = newProgress; + }); + } + _.onUpdatedSliderProgress(duration.timeStamp); + }, + onSeek: (duration) { + draggingFixedProgressBar.value = false; + _.onChangedSliderEnd(); + _.onChangedSlider(duration.inSeconds.toDouble()); + _.seekTo(Duration(seconds: duration.inSeconds), + type: 'slider'); + SemanticsService.announce( + "${(duration.inSeconds / max * 100).round()}%", + TextDirection.ltr); + }, + ), + // SlideTransition( + // position: Tween( + // begin: Offset.zero, + // end: const Offset(0, -1), + // ).animate(CurvedAnimation( + // parent: animationController, + // curve: Curves.easeInOut, + // )), + // child: ), + )); }, ), @@ -678,6 +710,7 @@ class _PLVideoPlayerState extends State child: Visibility( visible: _.showControls.value, child: ComBtn( + tooltip: _.controlsLock.value ? '解锁' : '锁定', icon: Icon( _.controlsLock.value ? FontAwesomeIcons.lock