diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index c1df7807c5..3394450cd0 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -14,6 +14,7 @@ import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart'; import 'package:PiliPlus/models_new/live/live_superchat/item.dart'; import 'package:PiliPlus/pages/danmaku/danmaku_model.dart'; import 'package:PiliPlus/pages/live_room/send_danmaku/view.dart'; +import 'package:PiliPlus/pages/live_room/widgets/header_control.dart'; import 'package:PiliPlus/pages/video/widgets/header_control.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_source.dart'; @@ -113,7 +114,7 @@ class LiveRoomController extends GetxController { final showSuperChat = Pref.showSuperChat; - final headerKey = GlobalKey(); + final headerKey = GlobalKey(); final RxString title = ''.obs; diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index b0310754b5..0936f083cd 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -16,7 +16,8 @@ import 'package:PiliPlus/pages/live_room/superchat/superchat_card.dart'; import 'package:PiliPlus/pages/live_room/superchat/superchat_panel.dart'; import 'package:PiliPlus/pages/live_room/widgets/bottom_control.dart'; import 'package:PiliPlus/pages/live_room/widgets/chat_panel.dart'; -import 'package:PiliPlus/pages/live_room/widgets/header_control.dart'; +import 'package:PiliPlus/pages/live_room/widgets/header_control.dart' + show LiveHeaderControl, LiveHeaderControlState; import 'package:PiliPlus/pages/video/widgets/player_focus.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; @@ -199,6 +200,12 @@ class _LiveRoomPageState extends State child = PlayerFocus( plPlayerController: plPlayerController, onSendDanmaku: _liveRoomController.onSendDanmaku, + onShowMenu: () { + final headerState = _liveRoomController.headerKey.currentState; + if (headerState is LiveHeaderControlState) { + headerState.showSettingSheet(); + } + }, child: child, ); } diff --git a/lib/pages/live_room/widgets/header_control.dart b/lib/pages/live_room/widgets/header_control.dart index f311968444..5dc824be8c 100644 --- a/lib/pages/live_room/widgets/header_control.dart +++ b/lib/pages/live_room/widgets/header_control.dart @@ -1,14 +1,22 @@ import 'dart:io'; +import 'dart:math' as math; +import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/marquee.dart'; import 'package:PiliPlus/pages/live_room/controller.dart'; -import 'package:PiliPlus/pages/video/widgets/header_control.dart'; +import 'package:PiliPlus/pages/video/introduction/ugc/widgets/menu_row.dart'; +import 'package:PiliPlus/pages/video/widgets/header_control.dart' + show HeaderMixin, HeaderControlState, TimeBatteryMixin; import 'package:PiliPlus/plugin/pl_player/controller.dart'; +import 'package:PiliPlus/plugin/pl_player/models/video_fit_type.dart'; import 'package:PiliPlus/plugin/pl_player/widgets/common_btn.dart'; import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -38,7 +46,7 @@ class LiveHeaderControl extends StatefulWidget { } class _LiveHeaderControlState extends State - with TimeBatteryMixin { + with TimeBatteryMixin, HeaderMixin { @override late final plPlayerController = widget.plPlayerController; @@ -51,6 +59,349 @@ class _LiveHeaderControlState extends State @override bool get isPortrait => widget.isPortrait; + static const TextStyle subTitleStyle = TextStyle(fontSize: 12); + static const TextStyle titleStyle = TextStyle(fontSize: 14); + + /// 设置面板 + void showSettingSheet() { + final liveController = widget.liveController; + showBottomSheet( + (context, setState) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.all(12), + child: Material( + clipBehavior: Clip.hardEdge, + color: theme.colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 14), + children: [ + ListTile( + dense: true, + onTap: () { + Get.back(); + widget.onSendDanmaku(); + }, + leading: const Icon(Icons.comment_outlined, size: 20), + title: const Text('发弹幕', style: titleStyle), + ), + Obx( + () { + final onlyPlayAudio = plPlayerController.onlyPlayAudio.value; + return ListTile( + dense: true, + onTap: () { + plPlayerController.onlyPlayAudio.value = !onlyPlayAudio; + widget.onPlayAudio(); + setState(() {}); + }, + leading: onlyPlayAudio + ? const Icon(MdiIcons.musicCircle, size: 20) + : const Icon(MdiIcons.musicCircleOutline, size: 20), + title: const Text('仅播放音频', style: titleStyle), + trailing: onlyPlayAudio + ? Icon( + Icons.done, + color: theme.colorScheme.primary, + ) + : null, + ); + }, + ), + if (Platform.isAndroid || (Utils.isDesktop && !isFullScreen)) + ListTile( + dense: true, + onTap: () async { + Get.back(); + if (Utils.isDesktop) { + plPlayerController.toggleDesktopPip(); + return; + } + if (await Floating().isPipAvailable) { + plPlayerController + ..showControls.value = false + ..enterPip(); + } + }, + leading: const Icon(Icons.picture_in_picture_outlined, size: 20), + title: const Text('画中画', style: titleStyle), + ), + ListTile( + dense: true, + onTap: () { + Get.back(); + PageUtils.scheduleExit(context, isFullScreen, true); + }, + leading: const Icon(Icons.schedule, size: 20), + title: const Text('定时关闭', style: titleStyle), + ), + ListTile( + dense: true, + onTap: () { + Get.back(); + HeaderControlState.showPlayerInfo( + context, + plPlayerController: plPlayerController, + ); + }, + leading: const Icon(Icons.info_outline, size: 20), + title: const Text('播放信息', style: titleStyle), + ), + ListTile( + dense: true, + onTap: () { + Get.back(); + liveController.queryLiveUrl(); + }, + leading: const Icon(Icons.refresh, size: 20), + title: const Text('刷新', style: titleStyle), + ), + ListTile( + dense: true, + onTap: () { + Get.back(); + if (liveController.isLogin) { + Get.toNamed( + '/liveDmBlockPage', + parameters: { + 'roomId': liveController.roomId.toString(), + }, + ); + } else { + SmartDialog.showToast('账号未登录'); + } + }, + leading: const Icon(Icons.block, size: 20), + title: const Text('屏蔽', style: titleStyle), + ), + SwitchListTile( + value: plPlayerController.enableShowDanmaku.value, + onChanged: (value) { + setState(() { + plPlayerController.enableShowDanmaku.value = value; + if (!plPlayerController.tempPlayerConf) { + GStorage.setting.put( + SettingBoxKey.enableShowLiveDanmaku, + value, + ); + } + }); + }, + secondary: const Icon(Icons.comment_outlined, size: 20), + title: const Text('弹幕', style: titleStyle), + ), + ListTile( + dense: true, + onTap: () { + Get.back(); + showSetDanmaku(isLive: true); + }, + leading: const Icon(CustomIcons.dm_settings, size: 20), + title: const Text('弹幕设置', style: titleStyle), + ), + ListTile( + dense: true, + leading: const Icon(Icons.aspect_ratio_outlined, size: 20), + title: Row( + children: [ + const Text( + '画面比例', + strutStyle: StrutStyle(leading: 0, height: 1), + style: TextStyle( + height: 1, + fontSize: 14, + ), + ), + const SizedBox(width: 10), + Obx( + () => PopupMenuButton( + initialValue: plPlayerController.videoFit.value, + child: Padding( + padding: const EdgeInsets.all(4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + plPlayerController.videoFit.value.desc, + strutStyle: const StrutStyle( + leading: 0, + height: 1, + ), + style: TextStyle( + height: 1, + fontSize: 14, + color: theme.colorScheme.secondary, + ), + ), + Icon( + MdiIcons.unfoldMoreHorizontal, + size: MediaQuery.textScalerOf( + context, + ).scale(14), + color: theme.colorScheme.secondary, + ), + ], + ), + ), + onSelected: (value) { + plPlayerController.toggleVideoFit(value); + if (context.mounted) { + (context as Element).markNeedsBuild(); + } + }, + itemBuilder: (context) => VideoFitType.values + .map( + (item) => PopupMenuItem( + value: item, + child: Text(item.desc), + ), + ) + .toList(), + ), + ), + ], + ), + ), + ListTile( + dense: true, + leading: const Icon(Icons.play_circle_outline, size: 20), + title: Row( + children: [ + const Text( + '画质', + strutStyle: StrutStyle(leading: 0, height: 1), + style: TextStyle( + height: 1, + fontSize: 14, + ), + ), + const SizedBox(width: 10), + Obx( + () => PopupMenuButton( + initialValue: liveController.currentQn, + child: Padding( + padding: const EdgeInsets.all(4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + liveController.currentQnDesc.value, + strutStyle: const StrutStyle( + leading: 0, + height: 1, + ), + style: TextStyle( + height: 1, + fontSize: 14, + color: theme.colorScheme.secondary, + ), + ), + Icon( + MdiIcons.unfoldMoreHorizontal, + size: MediaQuery.textScalerOf( + context, + ).scale(14), + color: theme.colorScheme.secondary, + ), + ], + ), + ), + onSelected: (value) { + liveController.changeQn(value); + if (context.mounted) { + (context as Element).markNeedsBuild(); + } + }, + itemBuilder: (context) => liveController.acceptQnList + .map( + (item) => PopupMenuItem( + value: item.code, + child: Text(item.desc), + ), + ) + .toList(), + ), + ), + ], + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + spacing: 10, + children: [ + Obx( + () { + final flipX = plPlayerController.flipX.value; + return ActionRowLineItem( + iconData: Icons.flip, + onTap: () => + plPlayerController.flipX.value = !flipX, + text: " 左右翻转 ", + selectStatus: flipX, + ); + }, + ), + Obx( + () { + final flipY = plPlayerController.flipY.value; + return ActionRowLineItem( + icon: Transform.rotate( + angle: math.pi / 2, + child: Icon( + Icons.flip, + size: 13, + color: flipY + ? theme.colorScheme.onSecondaryContainer + : theme.colorScheme.outline, + ), + ), + onTap: () { + plPlayerController.flipY.value = !flipY; + }, + text: " 上下翻转 ", + selectStatus: flipY, + ); + }, + ), + Obx( + () => ActionRowLineItem( + iconData: Icons.play_circle_outline, + onTap: plPlayerController.setContinuePlayInBackground, + text: " 后台播放 ", + selectStatus: + plPlayerController.continuePlayInBackground.value, + ), + ), + ], + ), + ), + ListTile( + dense: true, + onTap: () { + Get.back(); + }, + leading: const Icon(Icons.arrow_back, size: 20), + title: const Text('返回', style: titleStyle), + ), + ListTile( + dense: true, + onTap: () { + Get.until((route) => route.isFirst); + }, + leading: const Icon(Icons.home, size: 20), + title: const Text('返回主页', style: titleStyle), + ), + ], + ), + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { final isFullScreen = this.isFullScreen; @@ -199,8 +550,21 @@ class _LiveHeaderControlState extends State color: Colors.white, ), ), + ComBtn( + height: 30, + tooltip: "更多设置", + icon: const Icon( + Icons.more_vert_outlined, + size: 19, + color: Colors.white, + ), + onTap: showSettingSheet, + ), ], ), ); } } + +// Export the private state class for access via GlobalKey +typedef LiveHeaderControlState = _LiveHeaderControlState;