diff --git a/lib/common/widgets/marquee.dart b/lib/common/widgets/marquee.dart index feab4917b..e3406f883 100644 --- a/lib/common/widgets/marquee.dart +++ b/lib/common/widgets/marquee.dart @@ -225,9 +225,7 @@ abstract class MarqueeRender extends RenderBox if (_distance > 0) { updateSize(); - _ticker - ..createTicker(_onTick) - ..initStart(); + _ticker.initIfNeeded(_onTick); } else { _ticker.cancel(); } @@ -419,6 +417,13 @@ class ContextSingleTicker implements TickerProvider { } } + void initIfNeeded(TickerCallback onTick) { + if (_ticker == null) { + createTicker(onTick); + initStart(); + } + } + @override Ticker createTicker(TickerCallback onTick) { assert(() { diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 6b8c61475..301a54512 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -378,7 +378,7 @@ class LiveRoomController extends GetxController { case 'SUPER_CHAT_MESSAGE' when (showSuperChat): final item = SuperChatItem.fromJson(obj['data']); superChatMsg.insert(0, item); - if (isFullScreen) { + if (isFullScreen || plPlayerController.isDesktopPip) { fsSC.value = item; } break; diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index d0700c16d..74e712b1e 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'dart:ui'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; @@ -211,45 +212,48 @@ class _LiveRoomPageState extends State Alignment? alignment, bool needDm = true, }) { - if (!isFullScreen) { + if (!isFullScreen && !plPlayerController.isDesktopPip) { _liveRoomController.fsSC.value = null; } _liveRoomController.isFullScreen = isFullScreen; - Widget player = Obx(() { - if (_liveRoomController.isLoaded.value) { - final roomInfoH5 = _liveRoomController.roomInfoH5.value; - return PLVideoPlayer( - key: playerKey, - maxWidth: width, - maxHeight: height, - fill: fill, - alignment: alignment, - plPlayerController: plPlayerController, - headerControl: LiveHeaderControl( - title: roomInfoH5?.roomInfo?.title, - upName: roomInfoH5?.anchorInfo?.baseInfo?.uname, + Widget player = Obx( + key: playerKey, + () { + if (_liveRoomController.isLoaded.value) { + final roomInfoH5 = _liveRoomController.roomInfoH5.value; + return PLVideoPlayer( + maxWidth: width, + maxHeight: height, + fill: fill, + alignment: alignment, plPlayerController: plPlayerController, - onSendDanmaku: _liveRoomController.onSendDanmaku, - onPlayAudio: _liveRoomController.queryLiveUrl, - ), - bottomControl: BottomControl( - plPlayerController: plPlayerController, - liveRoomCtr: _liveRoomController, - onRefresh: _liveRoomController.queryLiveUrl, - ), - danmuWidget: !needDm - ? null - : LiveDanmaku( - liveRoomController: _liveRoomController, - plPlayerController: plPlayerController, - isFullScreen: isFullScreen, - isPipMode: isPipMode, - ), - ); - } - return const SizedBox.shrink(); - }); - if (isFullScreen && _liveRoomController.showSuperChat) { + headerControl: LiveHeaderControl( + title: roomInfoH5?.roomInfo?.title, + upName: roomInfoH5?.anchorInfo?.baseInfo?.uname, + plPlayerController: plPlayerController, + onSendDanmaku: _liveRoomController.onSendDanmaku, + onPlayAudio: _liveRoomController.queryLiveUrl, + ), + bottomControl: BottomControl( + plPlayerController: plPlayerController, + liveRoomCtr: _liveRoomController, + onRefresh: _liveRoomController.queryLiveUrl, + ), + danmuWidget: !needDm + ? null + : LiveDanmaku( + liveRoomController: _liveRoomController, + plPlayerController: plPlayerController, + isFullScreen: isFullScreen, + isPipMode: isPipMode, + ), + ); + } + return const SizedBox.shrink(); + }, + ); + if (_liveRoomController.showSuperChat && + (isFullScreen || plPlayerController.isDesktopPip)) { player = Stack( clipBehavior: Clip.none, children: [ @@ -421,6 +425,7 @@ class _LiveRoomPageState extends State final isFullScreen = this.isFullScreen; final height = maxWidth * 9 / 16; final videoHeight = isFullScreen ? maxHeight : height; + final bottomHeight = maxHeight - padding.top - height - kToolbarHeight; return Column( children: [ Offstage( @@ -440,7 +445,7 @@ class _LiveRoomPageState extends State offstage: isFullScreen, child: SizedBox( width: maxWidth, - height: maxHeight - padding.top - height - kToolbarHeight, + height: max(0, bottomHeight), child: _buildBottomWidget, ), ), diff --git a/lib/pages/video/introduction/ugc/widgets/triple_mixin.dart b/lib/pages/video/introduction/ugc/widgets/triple_mixin.dart index 67fcc07aa..6e243fade 100644 --- a/lib/pages/video/introduction/ugc/widgets/triple_mixin.dart +++ b/lib/pages/video/introduction/ugc/widgets/triple_mixin.dart @@ -47,7 +47,7 @@ mixin TripleMixin on GetxController, TickerProvider { static final _duration = Utils.isMobile ? const Duration(milliseconds: 200) - : const Duration(milliseconds: 230); + : const Duration(milliseconds: 255); void onStartTriple() { _timer ??= Timer(_duration, () { diff --git a/lib/pages/video/widgets/player_focus.dart b/lib/pages/video/widgets/player_focus.dart index 7c4fa8434..86ee5322a 100644 --- a/lib/pages/video/widgets/player_focus.dart +++ b/lib/pages/video/widgets/player_focus.dart @@ -9,7 +9,7 @@ import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' - show KeyDownEvent, KeyUpEvent, LogicalKeyboardKey; + show KeyDownEvent, KeyUpEvent, LogicalKeyboardKey, HardwareKeyboard; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -131,6 +131,18 @@ class PlayerFocus extends StatelessWidget { } if (event is KeyDownEvent) { + final isDigit1 = key == LogicalKeyboardKey.digit1; + if (isDigit1 || key == LogicalKeyboardKey.digit2) { + if (HardwareKeyboard.instance.isShiftPressed && hasPlayer) { + final speed = isDigit1 ? 1.0 : 2.0; + if (speed != plPlayerController.playbackSpeed) { + plPlayerController.setPlaybackSpeed(speed); + SmartDialog.showToast('${speed}x播放'); + } + } + return true; + } + switch (key) { case LogicalKeyboardKey.space: if (plPlayerController.isLive || canPlay!()) { @@ -170,8 +182,10 @@ class PlayerFocus extends StatelessWidget { } return true; - case LogicalKeyboardKey.keyP when (Utils.isDesktop && hasPlayer): - plPlayerController.toggleDesktopPip(); + case LogicalKeyboardKey.keyP: + if (Utils.isDesktop && hasPlayer) { + plPlayerController.toggleDesktopPip(); + } return true; case LogicalKeyboardKey.keyM: @@ -185,6 +199,12 @@ class PlayerFocus extends StatelessWidget { } return true; + case LogicalKeyboardKey.keyS: + if (hasPlayer && isFullScreen) { + plPlayerController.takeScreenshot(); + } + return true; + case LogicalKeyboardKey.enter: onSendDanmaku(); return true; @@ -218,6 +238,14 @@ class PlayerFocus extends StatelessWidget { } return true; + case LogicalKeyboardKey.keyL: + if (isFullScreen || plPlayerController.isDesktopPip) { + plPlayerController.onLockControl( + !plPlayerController.controlsLock.value, + ); + } + return true; + case LogicalKeyboardKey.bracketLeft: if (introController case final introController?) { if (!introController.prevPlay()) { diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index e1a4edcee..7ca3da5a4 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'dart:math' show max; +import 'dart:math' show max, min; import 'dart:ui' as ui; import 'package:PiliPlus/common/constants.dart'; @@ -32,6 +32,7 @@ import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/feed_back.dart'; +import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart' show PageUtils; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; @@ -1445,7 +1446,10 @@ class PlPlayerController { void onLockControl(bool val) { feedBack(); _controlsLock.value = val; - showControls.value = !val; + if (!val && _showControls.value) { + _showControls.refresh(); + } + controls = !val; } void toggleFullScreen(bool val) { @@ -1756,4 +1760,49 @@ class PlPlayerController { if (kDebugMode) debugPrint('getVideoShot: $e'); } } + + void takeScreenshot() { + SmartDialog.showToast('截图中'); + videoPlayerController?.screenshot(format: 'image/png').then((value) { + if (value != null) { + SmartDialog.showToast('点击弹窗保存截图'); + Get.dialog( + GestureDetector( + onTap: () { + Get.back(); + ImageUtils.saveByteImg( + bytes: value, + fileName: 'screenshot_${ImageUtils.time}', + ); + }, + child: Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: min(Get.width / 3, 350), + ), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + width: 5, + color: Get.theme.colorScheme.surface, + ), + ), + child: Padding( + padding: const EdgeInsets.all(5), + child: Image.memory(value), + ), + ), + ), + ), + ), + ), + ); + } else { + SmartDialog.showToast('截图失败'); + } + }); + } } diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 29ac20fd4..52acb3bce 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -829,6 +829,17 @@ class _PLVideoPlayerState extends State if (cumulativeDelta.distance < 1) return; if (cumulativeDelta.dx.abs() > 3 * cumulativeDelta.dy.abs()) { _gestureType = GestureType.horizontal; + if (Utils.isMobile) { + late final isFullScreen = this.isFullScreen; + final progressType = plPlayerController.progressType; + if (progressType == BtmProgressBehavior.alwaysHide || + (isFullScreen && + progressType == BtmProgressBehavior.onlyHideFullScreen) || + (!isFullScreen && + progressType == BtmProgressBehavior.onlyShowFullScreen)) { + plPlayerController.controls = true; + } + } } else if (cumulativeDelta.dy.abs() > 3 * cumulativeDelta.dx.abs()) { if (!plPlayerController.enableSlideVolumeBrightness && !plPlayerController.enableSlideFS) { @@ -1023,6 +1034,20 @@ class _PLVideoPlayerState extends State plPlayerController.doubleTapFuc(type); } + void onTapDesktop() { + if (plPlayerController.controlsLock.value) { + return; + } + plPlayerController.onDoubleTapCenter(); + } + + void onDoubleTapDesktop([_]) { + if (plPlayerController.controlsLock.value) { + return; + } + plPlayerController.triggerFullScreen(status: !isFullScreen); + } + final isMobile = Utils.isMobile; @override @@ -1074,12 +1099,8 @@ class _PLVideoPlayerState extends State onTap: isMobile ? () => plPlayerController.controls = !plPlayerController.showControls.value - : plPlayerController.onDoubleTapCenter, - onDoubleTapDown: isMobile - ? onDoubleTapDown - : (_) => plPlayerController.triggerFullScreen( - status: !isFullScreen, - ), + : onTapDesktop, + onDoubleTapDown: isMobile ? onDoubleTapDown : onDoubleTapDesktop, onLongPressStart: isLive ? null : (_) => plPlayerController.setLongPressStatus(true), @@ -1528,7 +1549,9 @@ class _PLVideoPlayerState extends State ), // 锁 - if (!isLive && isFullScreen && plPlayerController.showFsLockBtn) + if (!isLive && + plPlayerController.showFsLockBtn && + (isFullScreen || plPlayerController.isDesktopPip)) ViewSafeArea( right: false, child: Align( @@ -1597,52 +1620,7 @@ class _PLVideoPlayerState extends State (Platform.isAndroid || kDebugMode) && !isLive ? screenshotWebp : null, - onTap: () { - SmartDialog.showToast('截图中'); - plPlayerController.videoPlayerController - ?.screenshot(format: 'image/png') - .then((value) { - if (value != null && context.mounted) { - SmartDialog.showToast('点击弹窗保存截图'); - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - // title: const Text('点击保存'), - titlePadding: EdgeInsets.zero, - contentPadding: const EdgeInsets.all( - 8, - ), - insetPadding: EdgeInsets.only( - left: maxWidth / 2, - ), - //移除圆角 - shape: const RoundedRectangleBorder(), - content: GestureDetector( - onTap: () { - Get.back(); - ImageUtils.saveByteImg( - bytes: value, - fileName: - 'screenshot_${ImageUtils.time}', - ); - }, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: maxWidth / 3, - maxHeight: maxHeight / 3, - ), - child: Image.memory(value), - ), - ), - ); - }, - ); - } else { - SmartDialog.showToast('截图失败'); - } - }); - }, + onTap: plPlayerController.takeScreenshot, ), ), ),