Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-09-21 16:10:03 +08:00
parent f4e3484b01
commit 2efb04f77e
7 changed files with 164 additions and 99 deletions

View File

@@ -225,9 +225,7 @@ abstract class MarqueeRender extends RenderBox
if (_distance > 0) { if (_distance > 0) {
updateSize(); updateSize();
_ticker _ticker.initIfNeeded(_onTick);
..createTicker(_onTick)
..initStart();
} else { } else {
_ticker.cancel(); _ticker.cancel();
} }
@@ -419,6 +417,13 @@ class ContextSingleTicker implements TickerProvider {
} }
} }
void initIfNeeded(TickerCallback onTick) {
if (_ticker == null) {
createTicker(onTick);
initStart();
}
}
@override @override
Ticker createTicker(TickerCallback onTick) { Ticker createTicker(TickerCallback onTick) {
assert(() { assert(() {

View File

@@ -378,7 +378,7 @@ class LiveRoomController extends GetxController {
case 'SUPER_CHAT_MESSAGE' when (showSuperChat): case 'SUPER_CHAT_MESSAGE' when (showSuperChat):
final item = SuperChatItem.fromJson(obj['data']); final item = SuperChatItem.fromJson(obj['data']);
superChatMsg.insert(0, item); superChatMsg.insert(0, item);
if (isFullScreen) { if (isFullScreen || plPlayerController.isDesktopPip) {
fsSC.value = item; fsSC.value = item;
} }
break; break;

View File

@@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart';
@@ -211,45 +212,48 @@ class _LiveRoomPageState extends State<LiveRoomPage>
Alignment? alignment, Alignment? alignment,
bool needDm = true, bool needDm = true,
}) { }) {
if (!isFullScreen) { if (!isFullScreen && !plPlayerController.isDesktopPip) {
_liveRoomController.fsSC.value = null; _liveRoomController.fsSC.value = null;
} }
_liveRoomController.isFullScreen = isFullScreen; _liveRoomController.isFullScreen = isFullScreen;
Widget player = Obx(() { Widget player = Obx(
if (_liveRoomController.isLoaded.value) { key: playerKey,
final roomInfoH5 = _liveRoomController.roomInfoH5.value; () {
return PLVideoPlayer( if (_liveRoomController.isLoaded.value) {
key: playerKey, final roomInfoH5 = _liveRoomController.roomInfoH5.value;
maxWidth: width, return PLVideoPlayer(
maxHeight: height, maxWidth: width,
fill: fill, maxHeight: height,
alignment: alignment, fill: fill,
plPlayerController: plPlayerController, alignment: alignment,
headerControl: LiveHeaderControl(
title: roomInfoH5?.roomInfo?.title,
upName: roomInfoH5?.anchorInfo?.baseInfo?.uname,
plPlayerController: plPlayerController, plPlayerController: plPlayerController,
onSendDanmaku: _liveRoomController.onSendDanmaku, headerControl: LiveHeaderControl(
onPlayAudio: _liveRoomController.queryLiveUrl, title: roomInfoH5?.roomInfo?.title,
), upName: roomInfoH5?.anchorInfo?.baseInfo?.uname,
bottomControl: BottomControl( plPlayerController: plPlayerController,
plPlayerController: plPlayerController, onSendDanmaku: _liveRoomController.onSendDanmaku,
liveRoomCtr: _liveRoomController, onPlayAudio: _liveRoomController.queryLiveUrl,
onRefresh: _liveRoomController.queryLiveUrl, ),
), bottomControl: BottomControl(
danmuWidget: !needDm plPlayerController: plPlayerController,
? null liveRoomCtr: _liveRoomController,
: LiveDanmaku( onRefresh: _liveRoomController.queryLiveUrl,
liveRoomController: _liveRoomController, ),
plPlayerController: plPlayerController, danmuWidget: !needDm
isFullScreen: isFullScreen, ? null
isPipMode: isPipMode, : LiveDanmaku(
), liveRoomController: _liveRoomController,
); plPlayerController: plPlayerController,
} isFullScreen: isFullScreen,
return const SizedBox.shrink(); isPipMode: isPipMode,
}); ),
if (isFullScreen && _liveRoomController.showSuperChat) { );
}
return const SizedBox.shrink();
},
);
if (_liveRoomController.showSuperChat &&
(isFullScreen || plPlayerController.isDesktopPip)) {
player = Stack( player = Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
@@ -421,6 +425,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
final isFullScreen = this.isFullScreen; final isFullScreen = this.isFullScreen;
final height = maxWidth * 9 / 16; final height = maxWidth * 9 / 16;
final videoHeight = isFullScreen ? maxHeight : height; final videoHeight = isFullScreen ? maxHeight : height;
final bottomHeight = maxHeight - padding.top - height - kToolbarHeight;
return Column( return Column(
children: [ children: [
Offstage( Offstage(
@@ -440,7 +445,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
offstage: isFullScreen, offstage: isFullScreen,
child: SizedBox( child: SizedBox(
width: maxWidth, width: maxWidth,
height: maxHeight - padding.top - height - kToolbarHeight, height: max(0, bottomHeight),
child: _buildBottomWidget, child: _buildBottomWidget,
), ),
), ),

View File

@@ -47,7 +47,7 @@ mixin TripleMixin on GetxController, TickerProvider {
static final _duration = Utils.isMobile static final _duration = Utils.isMobile
? const Duration(milliseconds: 200) ? const Duration(milliseconds: 200)
: const Duration(milliseconds: 230); : const Duration(milliseconds: 255);
void onStartTriple() { void onStartTriple() {
_timer ??= Timer(_duration, () { _timer ??= Timer(_duration, () {

View File

@@ -9,7 +9,7 @@ import 'package:PiliPlus/utils/storage_key.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -131,6 +131,18 @@ class PlayerFocus extends StatelessWidget {
} }
if (event is KeyDownEvent) { 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) { switch (key) {
case LogicalKeyboardKey.space: case LogicalKeyboardKey.space:
if (plPlayerController.isLive || canPlay!()) { if (plPlayerController.isLive || canPlay!()) {
@@ -170,8 +182,10 @@ class PlayerFocus extends StatelessWidget {
} }
return true; return true;
case LogicalKeyboardKey.keyP when (Utils.isDesktop && hasPlayer): case LogicalKeyboardKey.keyP:
plPlayerController.toggleDesktopPip(); if (Utils.isDesktop && hasPlayer) {
plPlayerController.toggleDesktopPip();
}
return true; return true;
case LogicalKeyboardKey.keyM: case LogicalKeyboardKey.keyM:
@@ -185,6 +199,12 @@ class PlayerFocus extends StatelessWidget {
} }
return true; return true;
case LogicalKeyboardKey.keyS:
if (hasPlayer && isFullScreen) {
plPlayerController.takeScreenshot();
}
return true;
case LogicalKeyboardKey.enter: case LogicalKeyboardKey.enter:
onSendDanmaku(); onSendDanmaku();
return true; return true;
@@ -218,6 +238,14 @@ class PlayerFocus extends StatelessWidget {
} }
return true; return true;
case LogicalKeyboardKey.keyL:
if (isFullScreen || plPlayerController.isDesktopPip) {
plPlayerController.onLockControl(
!plPlayerController.controlsLock.value,
);
}
return true;
case LogicalKeyboardKey.bracketLeft: case LogicalKeyboardKey.bracketLeft:
if (introController case final introController?) { if (introController case final introController?) {
if (!introController.prevPlay()) { if (!introController.prevPlay()) {

View File

@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math' show max; import 'dart:math' show max, min;
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:PiliPlus/common/constants.dart'; 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/accounts.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/feed_back.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/page_utils.dart' show PageUtils;
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_key.dart';
@@ -1445,7 +1446,10 @@ class PlPlayerController {
void onLockControl(bool val) { void onLockControl(bool val) {
feedBack(); feedBack();
_controlsLock.value = val; _controlsLock.value = val;
showControls.value = !val; if (!val && _showControls.value) {
_showControls.refresh();
}
controls = !val;
} }
void toggleFullScreen(bool val) { void toggleFullScreen(bool val) {
@@ -1756,4 +1760,49 @@ class PlPlayerController {
if (kDebugMode) debugPrint('getVideoShot: $e'); 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('截图失败');
}
});
}
} }

View File

@@ -829,6 +829,17 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
if (cumulativeDelta.distance < 1) return; if (cumulativeDelta.distance < 1) return;
if (cumulativeDelta.dx.abs() > 3 * cumulativeDelta.dy.abs()) { if (cumulativeDelta.dx.abs() > 3 * cumulativeDelta.dy.abs()) {
_gestureType = GestureType.horizontal; _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()) { } else if (cumulativeDelta.dy.abs() > 3 * cumulativeDelta.dx.abs()) {
if (!plPlayerController.enableSlideVolumeBrightness && if (!plPlayerController.enableSlideVolumeBrightness &&
!plPlayerController.enableSlideFS) { !plPlayerController.enableSlideFS) {
@@ -1023,6 +1034,20 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
plPlayerController.doubleTapFuc(type); 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; final isMobile = Utils.isMobile;
@override @override
@@ -1074,12 +1099,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
onTap: isMobile onTap: isMobile
? () => plPlayerController.controls = ? () => plPlayerController.controls =
!plPlayerController.showControls.value !plPlayerController.showControls.value
: plPlayerController.onDoubleTapCenter, : onTapDesktop,
onDoubleTapDown: isMobile onDoubleTapDown: isMobile ? onDoubleTapDown : onDoubleTapDesktop,
? onDoubleTapDown
: (_) => plPlayerController.triggerFullScreen(
status: !isFullScreen,
),
onLongPressStart: isLive onLongPressStart: isLive
? null ? null
: (_) => plPlayerController.setLongPressStatus(true), : (_) => plPlayerController.setLongPressStatus(true),
@@ -1528,7 +1549,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
// 锁 // 锁
if (!isLive && isFullScreen && plPlayerController.showFsLockBtn) if (!isLive &&
plPlayerController.showFsLockBtn &&
(isFullScreen || plPlayerController.isDesktopPip))
ViewSafeArea( ViewSafeArea(
right: false, right: false,
child: Align( child: Align(
@@ -1597,52 +1620,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
(Platform.isAndroid || kDebugMode) && !isLive (Platform.isAndroid || kDebugMode) && !isLive
? screenshotWebp ? screenshotWebp
: null, : null,
onTap: () { onTap: plPlayerController.takeScreenshot,
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('截图失败');
}
});
},
), ),
), ),
), ),