diff --git a/lib/pages/live_room/widgets/bottom_control.dart b/lib/pages/live_room/widgets/bottom_control.dart index 4c32c4d59..f1f8e4eb0 100644 --- a/lib/pages/live_room/widgets/bottom_control.dart +++ b/lib/pages/live_room/widgets/bottom_control.dart @@ -1,5 +1,7 @@ import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/pages/live_room/controller.dart'; +import 'package:PiliPlus/pages/video/widgets/header_control.dart' + show HeaderMixin; 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'; @@ -10,7 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -class BottomControl extends StatelessWidget { +class BottomControl extends StatefulWidget { const BottomControl({ super.key, required this.plPlayerController, @@ -27,6 +29,15 @@ class BottomControl extends StatelessWidget { final TextStyle subTitleStyle; final TextStyle titleStyle; + @override + State createState() => _BottomControlState(); +} + +class _BottomControlState extends State with HeaderMixin { + late final LiveRoomController liveRoomCtr = widget.liveRoomCtr; + @override + late final PlPlayerController plPlayerController = widget.plPlayerController; + @override Widget build(BuildContext context) { final isFullScreen = plPlayerController.isFullScreen.value; @@ -47,7 +58,7 @@ class BottomControl extends StatelessWidget { size: 18, color: Colors.white, ), - onTap: onRefresh, + onTap: widget.onRefresh, ), const Spacer(), ComBtn( @@ -102,6 +113,15 @@ class BottomControl extends StatelessWidget { ); }, ), + ComBtn( + tooltip: '弹幕设置', + icon: const Icon( + size: 18, + CustomIcons.dm_settings, + color: Colors.white, + ), + onTap: showSetDanmaku, + ), Obx( () => PopupMenuButton( tooltip: '画面比例', diff --git a/lib/pages/live_room/widgets/header_control.dart b/lib/pages/live_room/widgets/header_control.dart index 05916119a..0925528fc 100644 --- a/lib/pages/live_room/widgets/header_control.dart +++ b/lib/pages/live_room/widgets/header_control.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:PiliPlus/pages/video/widgets/header_control.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/widgets/common_btn.dart'; import 'package:PiliPlus/utils/page_utils.dart'; @@ -145,6 +146,18 @@ class LiveHeaderControl extends StatelessWidget { color: Colors.white, ), ), + ComBtn( + tooltip: '播放信息', + onTap: () => HeaderControlState.showPlayerInfo( + context, + plPlayerController: plPlayerController, + ), + icon: const Icon( + size: 18, + Icons.info_outline, + color: Colors.white, + ), + ), ], ), ); diff --git a/lib/pages/video/send_danmaku/view.dart b/lib/pages/video/send_danmaku/view.dart index db48b858a..9e3a9a26d 100644 --- a/lib/pages/video/send_danmaku/view.dart +++ b/lib/pages/video/send_danmaku/view.dart @@ -186,7 +186,13 @@ class _SendDanmakuPanelState extends CommonTextPubPageState { const SizedBox(height: 12), Row( children: [ - const Text('弹幕字号', style: TextStyle(fontSize: 15)), + Text( + '弹幕字号', + style: TextStyle( + fontSize: 15, + color: themeData.colorScheme.onSurface, + ), + ), const SizedBox(width: 16), _buildFontSizeItem(18, '小'), const SizedBox(width: 5), @@ -196,7 +202,13 @@ class _SendDanmakuPanelState extends CommonTextPubPageState { const SizedBox(height: 12), Row( children: [ - const Text('弹幕样式', style: TextStyle(fontSize: 15)), + Text( + '弹幕样式', + style: TextStyle( + fontSize: 15, + color: themeData.colorScheme.onSurface, + ), + ), const SizedBox(width: 16), _buildPositionItem(1, '滚动'), const SizedBox(width: 5), @@ -209,7 +221,13 @@ class _SendDanmakuPanelState extends CommonTextPubPageState { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('弹幕颜色', style: TextStyle(fontSize: 15)), + Text( + '弹幕颜色', + style: TextStyle( + fontSize: 15, + color: themeData.colorScheme.onSurface, + ), + ), const SizedBox(width: 16), _buildColorPanel, ], diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index 5bb6649ea..f781f69b9 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -49,7 +49,7 @@ import 'package:dio/dio.dart'; import 'package:file_picker/file_picker.dart'; import 'package:floating/floating.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide showBottomSheet; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -57,6 +57,627 @@ import 'package:hive/hive.dart'; import 'package:intl/intl.dart' show DateFormat; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +mixin HeaderMixin on State { + PlPlayerController get plPlayerController; + + bool get isFullScreen => plPlayerController.isFullScreen.value; + + void showBottomSheet(StatefulWidgetBuilder builder, {double? padding}) { + PageUtils.showVideoBottomSheet( + context, + isFullScreen: () => isFullScreen, + padding: padding, + child: StatefulBuilder( + builder: (context, setState) => plPlayerController.darkVideoPage + ? Theme( + data: Theme.of(this.context), + child: builder(this.context, setState), + ) + : builder(context, setState), + ), + ); + } + + Widget resetBtn( + ThemeData theme, + Object def, + VoidCallback onPressed, { + bool isDanmaku = true, + }) { + return iconButton( + tooltip: '默认值: $def', + icon: const Icon(Icons.refresh), + onPressed: () { + onPressed(); + if (isDanmaku) { + plPlayerController.putDanmakuSettings(); + } else { + plPlayerController.putSubtitleSettings(); + } + }, + iconColor: theme.colorScheme.outline, + size: 24, + iconSize: 24, + ); + } + + /// 弹幕功能 + void showSetDanmaku() { + // 屏蔽类型 + const List<({int value, String label})> blockTypesList = [ + (value: 5, label: '顶部'), + (value: 2, label: '滚动'), + (value: 4, label: '底部'), + (value: 6, label: '彩色'), + ]; + final blockTypes = plPlayerController.blockTypes; + // 智能云屏蔽 + int danmakuWeight = plPlayerController.danmakuWeight; + // 显示区域 + double showArea = plPlayerController.showArea; + // 不透明度 + double danmakuOpacity = plPlayerController.danmakuOpacity.value; + // 字体大小 + double danmakuFontScale = plPlayerController.danmakuFontScale; + // 全屏字体大小 + double danmakuFontScaleFS = plPlayerController.danmakuFontScaleFS; + double danmakuLineHeight = plPlayerController.danmakuLineHeight; + // 弹幕速度 + double danmakuDuration = plPlayerController.danmakuDuration; + double danmakuStaticDuration = plPlayerController.danmakuStaticDuration; + // 弹幕描边 + double danmakuStrokeWidth = plPlayerController.danmakuStrokeWidth; + // 字体粗细 + int danmakuFontWeight = plPlayerController.danmakuFontWeight; + bool massiveMode = plPlayerController.massiveMode; + + final DanmakuController? danmakuController = + plPlayerController.danmakuController; + + final isFullScreen = plPlayerController.isFullScreen.value; + + showBottomSheet( + (context, setState) { + final theme = Theme.of(context); + + final sliderTheme = SliderThemeData( + trackHeight: 10, + trackShape: const MSliderTrackShape(), + thumbColor: theme.colorScheme.primary, + activeTrackColor: theme.colorScheme.primary, + inactiveTrackColor: theme.colorScheme.onInverseSurface, + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6.0), + ); + + void updateLineHeight(double val) { + plPlayerController.danmakuLineHeight = danmakuLineHeight = val + .toPrecision(1); + setState(() {}); + try { + danmakuController?.updateOption( + danmakuController.option.copyWith( + lineHeight: danmakuLineHeight, + ), + ); + } catch (_) {} + } + + void updateDuration(double val) { + plPlayerController.danmakuDuration = danmakuDuration = val + .toPrecision(1); + setState(() {}); + try { + danmakuController?.updateOption( + danmakuController.option.copyWith( + duration: danmakuDuration / plPlayerController.playbackSpeed, + ), + ); + } catch (_) {} + } + + void updateStaticDuration(double val) { + plPlayerController.danmakuStaticDuration = danmakuStaticDuration = val + .toPrecision(1); + setState(() {}); + try { + danmakuController?.updateOption( + danmakuController.option.copyWith( + staticDuration: + danmakuStaticDuration / plPlayerController.playbackSpeed, + ), + ); + } catch (_) {} + } + + void updateFontSizeFS(double val) { + plPlayerController.danmakuFontScaleFS = danmakuFontScaleFS = val; + setState(() {}); + if (isFullScreen) { + try { + danmakuController?.updateOption( + danmakuController.option.copyWith( + fontSize: (15 * danmakuFontScaleFS).toDouble(), + ), + ); + } catch (_) {} + } + } + + void updateFontSize(double val) { + plPlayerController.danmakuFontScale = danmakuFontScale = val; + setState(() {}); + if (!isFullScreen) { + try { + danmakuController?.updateOption( + danmakuController.option.copyWith( + fontSize: (15 * danmakuFontScale).toDouble(), + ), + ); + } catch (_) {} + } + } + + void updateStrokeWidth(double val) { + plPlayerController.danmakuStrokeWidth = danmakuStrokeWidth = val; + setState(() {}); + try { + danmakuController?.updateOption( + danmakuController.option.copyWith( + strokeWidth: danmakuStrokeWidth, + ), + ); + } catch (_) {} + } + + void updateFontWeight(double val) { + plPlayerController.danmakuFontWeight = danmakuFontWeight = val + .toInt(); + setState(() {}); + try { + danmakuController?.updateOption( + danmakuController.option.copyWith(fontWeight: danmakuFontWeight), + ); + } catch (_) {} + } + + void updateOpacity(double val) { + plPlayerController.danmakuOpacity.value = danmakuOpacity = val; + setState(() {}); + } + + void updateShowArea(double val) { + plPlayerController.showArea = showArea = val.toPrecision(1); + setState(() {}); + try { + danmakuController?.updateOption( + danmakuController.option.copyWith(area: showArea), + ); + } catch (_) {} + } + + void updateDanmakuWeight(double val) { + plPlayerController.danmakuWeight = danmakuWeight = val.toInt(); + setState(() {}); + } + + return Padding( + padding: const EdgeInsets.all(12), + child: Material( + clipBehavior: Clip.hardEdge, + color: theme.colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: ListView( + padding: EdgeInsets.zero, + children: [ + const SizedBox( + height: 45, + child: Center( + child: Text('弹幕设置', style: TextStyle(fontSize: 14)), + ), + ), + const SizedBox(height: 10), + Row( + children: [ + Text('智能云屏蔽 $danmakuWeight 级'), + const Spacer(), + TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () => Get + ..back() + ..toNamed( + '/danmakuBlock', + arguments: plPlayerController, + ), + child: Text( + "屏蔽管理(${plPlayerController.filters.count})", + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: sliderTheme, + child: Slider( + min: 0, + max: 10, + value: danmakuWeight.toDouble(), + divisions: 10, + label: '$danmakuWeight', + onChanged: updateDanmakuWeight, + onChangeEnd: (_) => + plPlayerController.putDanmakuSettings(), + ), + ), + ), + const Text('按类型屏蔽'), + Padding( + padding: const EdgeInsets.only(top: 12), + child: Row( + children: [ + for (final (value: value, label: label) + in blockTypesList) ...[ + ActionRowLineItem( + onTap: () { + if (blockTypes.contains(value)) { + blockTypes.remove(value); + } else { + blockTypes.add(value); + } + plPlayerController + ..blockTypes = blockTypes + ..blockColorful = blockTypes.contains(6) + ..putDanmakuSettings(); + setState(() {}); + try { + danmakuController?.updateOption( + danmakuController.option.copyWith( + hideTop: blockTypes.contains(5), + hideBottom: blockTypes.contains(4), + hideScroll: blockTypes.contains(2), + // 添加或修改其他需要修改的选项属性 + ), + ); + } catch (_) {} + }, + text: label, + selectStatus: blockTypes.contains(value), + ), + const SizedBox(width: 10), + ], + ], + ), + ), + SetSwitchItem( + title: '海量弹幕', + contentPadding: EdgeInsets.zero, + titleStyle: const TextStyle(fontSize: 14), + defaultVal: massiveMode, + setKey: SettingBoxKey.danmakuMassiveMode, + onChanged: (value) { + massiveMode = value; + plPlayerController.massiveMode = value; + setState(() {}); + try { + danmakuController?.updateOption( + danmakuController.option.copyWith(massiveMode: value), + ); + } catch (_) {} + }, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('显示区域 ${showArea * 100}%'), + resetBtn( + theme, + '50.0%', + () => updateShowArea(0.5), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: sliderTheme, + child: Slider( + min: 0.1, + max: 1, + value: showArea, + divisions: 9, + label: '${showArea * 100}%', + onChanged: updateShowArea, + onChangeEnd: (_) => + plPlayerController.putDanmakuSettings(), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('不透明度 ${danmakuOpacity * 100}%'), + resetBtn( + theme, + '100.0%', + () => updateOpacity(1.0), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: sliderTheme, + child: Slider( + min: 0, + max: 1, + value: danmakuOpacity, + divisions: 10, + label: '${danmakuOpacity * 100}%', + onChanged: updateOpacity, + onChangeEnd: (_) => + plPlayerController.putDanmakuSettings(), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('字体粗细 ${danmakuFontWeight + 1}(可能无法精确调节)'), + resetBtn( + theme, + 6, + () => updateFontWeight(5), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: sliderTheme, + child: Slider( + min: 0, + max: 8, + value: danmakuFontWeight.toDouble(), + divisions: 8, + label: '${danmakuFontWeight + 1}', + onChanged: updateFontWeight, + onChangeEnd: (_) => + plPlayerController.putDanmakuSettings(), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('描边粗细 $danmakuStrokeWidth'), + resetBtn( + theme, + 1.5, + () => updateStrokeWidth(1.5), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: sliderTheme, + child: Slider( + min: 0, + max: 5, + value: danmakuStrokeWidth, + divisions: 10, + label: '$danmakuStrokeWidth', + onChanged: updateStrokeWidth, + onChangeEnd: (_) => + plPlayerController.putDanmakuSettings(), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '字体大小 ${(danmakuFontScale * 100).toStringAsFixed(1)}%', + ), + resetBtn( + theme, + '100.0%', + () => updateFontSize(1.0), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: sliderTheme, + child: Slider( + min: 0.5, + max: 2.5, + value: danmakuFontScale, + divisions: 20, + label: + '${(danmakuFontScale * 100).toStringAsFixed(1)}%', + onChanged: updateFontSize, + onChangeEnd: (_) => + plPlayerController.putDanmakuSettings(), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '全屏字体大小 ${(danmakuFontScaleFS * 100).toStringAsFixed(1)}%', + ), + resetBtn( + theme, + '120.0%', + () => updateFontSizeFS(1.2), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: sliderTheme, + child: Slider( + min: 0.5, + max: 2.5, + value: danmakuFontScaleFS, + divisions: 20, + label: + '${(danmakuFontScaleFS * 100).toStringAsFixed(1)}%', + onChanged: updateFontSizeFS, + onChangeEnd: (_) => + plPlayerController.putDanmakuSettings(), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('滚动弹幕时长 $danmakuDuration 秒'), + resetBtn( + theme, + 7.0, + () => updateDuration(7.0), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: sliderTheme, + child: Slider( + min: 1, + max: 50, + value: danmakuDuration, + divisions: 49, + label: danmakuDuration.toString(), + onChanged: updateDuration, + onChangeEnd: (_) => + plPlayerController.putDanmakuSettings(), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('静态弹幕时长 $danmakuStaticDuration 秒'), + resetBtn( + theme, + 4.0, + () => updateStaticDuration(4.0), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: sliderTheme, + child: Slider( + min: 1, + max: 50, + value: danmakuStaticDuration, + divisions: 49, + label: danmakuStaticDuration.toString(), + onChanged: updateStaticDuration, + onChangeEnd: (_) => + plPlayerController.putDanmakuSettings(), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('弹幕行高 $danmakuLineHeight'), + resetBtn( + theme, + 1.6, + () => updateLineHeight(1.6), + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: sliderTheme, + child: Slider( + min: 1.0, + max: 3.0, + value: danmakuLineHeight, + onChanged: updateLineHeight, + onChangeEnd: (_) => + plPlayerController.putDanmakuSettings(), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} + class HeaderControl extends StatefulWidget { const HeaderControl({ required this.isPortrait, @@ -196,7 +817,8 @@ class HeaderControl extends StatefulWidget { } } -class HeaderControlState extends State { +class HeaderControlState extends State with HeaderMixin { + @override late final PlPlayerController plPlayerController = widget.controller; late final VideoDetailController videoDetailCtr = widget.videoDetailCtr; late final PlayUrlModel videoInfo = videoDetailCtr.data; @@ -218,7 +840,6 @@ class HeaderControlState extends State { RxString now = ''.obs; Timer? clock; - bool get isFullScreen => plPlayerController.isFullScreen.value; Box setting = GStorage.setting; late final provider = ContextSingleTicker(context, autoStart: false); @@ -240,22 +861,6 @@ class HeaderControlState extends State { super.dispose(); } - void showBottomSheet(StatefulWidgetBuilder builder, {double? padding}) { - PageUtils.showVideoBottomSheet( - context, - isFullScreen: () => isFullScreen, - padding: padding, - child: StatefulBuilder( - builder: (context, setState) => plPlayerController.darkVideoPage - ? Theme( - data: Theme.of(this.context), - child: builder(this.context, setState), - ) - : builder(context, setState), - ), - ); - } - /// 设置面板 void showSettingSheet() { showBottomSheet( @@ -668,155 +1273,10 @@ class HeaderControlState extends State { dense: true, title: const Text('播放信息', style: titleStyle), leading: const Icon(Icons.info_outline, size: 20), - onTap: () async { - final player = plPlayerController.videoPlayerController; - if (player == null) { - SmartDialog.showToast('播放器未初始化'); - return; - } - final hwdec = await player.platform!.getProperty( - 'hwdec-current', - ); - if (!context.mounted) return; - showDialog( - context: context, - builder: (context) { - final state = player.state; - return AlertDialog( - title: const Text('播放信息'), - contentPadding: const EdgeInsets.only(top: 16), - constraints: const BoxConstraints(maxWidth: 425), - content: Material( - type: MaterialType.transparency, - child: ListTileTheme( - contentPadding: const EdgeInsets.symmetric( - horizontal: 24, - ), - child: SingleChildScrollView( - child: Column( - children: [ - ListTile( - dense: true, - title: const Text("Resolution"), - subtitle: Text( - '${state.width}x${state.height}', - ), - onTap: () => Utils.copyText( - 'Resolution\n${state.width}x${state.height}', - ), - ), - ListTile( - dense: true, - title: const Text("VideoParams"), - subtitle: Text( - state.videoParams.toString(), - ), - onTap: () => Utils.copyText( - 'VideoParams\n${state.videoParams}', - ), - ), - ListTile( - dense: true, - title: const Text("AudioParams"), - subtitle: Text( - state.audioParams.toString(), - ), - onTap: () => Utils.copyText( - 'AudioParams\n${state.audioParams}', - ), - ), - ListTile( - dense: true, - title: const Text("Media"), - subtitle: Text( - state.playlist.toString(), - ), - onTap: () => Utils.copyText( - 'Media\n${state.playlist}', - ), - ), - ListTile( - dense: true, - title: const Text("AudioTrack"), - subtitle: Text( - state.track.audio.toString(), - ), - onTap: () => Utils.copyText( - 'AudioTrack\n${state.track.audio}', - ), - ), - ListTile( - dense: true, - title: const Text("VideoTrack"), - subtitle: Text( - state.track.video.toString(), - ), - onTap: () => Utils.copyText( - 'VideoTrack\n${state.track.audio}', - ), - ), - ListTile( - dense: true, - title: const Text("pitch"), - subtitle: Text(state.pitch.toString()), - onTap: () => Utils.copyText( - 'pitch\n${state.pitch}', - ), - ), - ListTile( - dense: true, - title: const Text("rate"), - subtitle: Text(state.rate.toString()), - onTap: () => - Utils.copyText('rate\n${state.rate}'), - ), - ListTile( - dense: true, - title: const Text("AudioBitrate"), - subtitle: Text( - state.audioBitrate.toString(), - ), - onTap: () => Utils.copyText( - 'AudioBitrate\n${state.audioBitrate}', - ), - ), - ListTile( - dense: true, - title: const Text("Volume"), - subtitle: Text( - state.volume.toString(), - ), - onTap: () => Utils.copyText( - 'Volume\n${state.volume}', - ), - ), - ListTile( - dense: true, - title: const Text('hwdec'), - subtitle: Text(hwdec), - onTap: () => - Utils.copyText('hwdec\n$hwdec'), - ), - ], - ), - ), - ), - ), - actions: [ - TextButton( - onPressed: Get.back, - child: Text( - '确定', - style: TextStyle( - color: theme.colorScheme.outline, - ), - ), - ), - ], - ); - }, - ); - }, + onTap: () => showPlayerInfo( + context, + plPlayerController: plPlayerController, + ), ), ListTile( dense: true, @@ -839,6 +1299,155 @@ class HeaderControlState extends State { ); } + static Future showPlayerInfo( + BuildContext context, { + required PlPlayerController plPlayerController, + }) async { + final player = plPlayerController.videoPlayerController; + if (player == null) { + SmartDialog.showToast('播放器未初始化'); + return; + } + final hwdec = await player.platform!.getProperty( + 'hwdec-current', + ); + if (!context.mounted) return; + showDialog( + context: context, + builder: (context) { + final state = player.state; + return AlertDialog( + title: const Text('播放信息'), + contentPadding: const EdgeInsets.only(top: 16), + constraints: const BoxConstraints(maxWidth: 425), + content: Material( + type: MaterialType.transparency, + child: ListTileTheme( + contentPadding: const EdgeInsets.symmetric( + horizontal: 24, + ), + child: SingleChildScrollView( + child: Column( + children: [ + ListTile( + dense: true, + title: const Text("Resolution"), + subtitle: Text( + '${state.width}x${state.height}', + ), + onTap: () => Utils.copyText( + 'Resolution\n${state.width}x${state.height}', + ), + ), + ListTile( + dense: true, + title: const Text("VideoParams"), + subtitle: Text( + state.videoParams.toString(), + ), + onTap: () => Utils.copyText( + 'VideoParams\n${state.videoParams}', + ), + ), + ListTile( + dense: true, + title: const Text("AudioParams"), + subtitle: Text( + state.audioParams.toString(), + ), + onTap: () => Utils.copyText( + 'AudioParams\n${state.audioParams}', + ), + ), + ListTile( + dense: true, + title: const Text("Media"), + subtitle: Text( + state.playlist.toString(), + ), + onTap: () => Utils.copyText( + 'Media\n${state.playlist}', + ), + ), + ListTile( + dense: true, + title: const Text("AudioTrack"), + subtitle: Text( + state.track.audio.toString(), + ), + onTap: () => Utils.copyText( + 'AudioTrack\n${state.track.audio}', + ), + ), + ListTile( + dense: true, + title: const Text("VideoTrack"), + subtitle: Text( + state.track.video.toString(), + ), + onTap: () => Utils.copyText( + 'VideoTrack\n${state.track.audio}', + ), + ), + ListTile( + dense: true, + title: const Text("pitch"), + subtitle: Text(state.pitch.toString()), + onTap: () => Utils.copyText( + 'pitch\n${state.pitch}', + ), + ), + ListTile( + dense: true, + title: const Text("rate"), + subtitle: Text(state.rate.toString()), + onTap: () => Utils.copyText('rate\n${state.rate}'), + ), + ListTile( + dense: true, + title: const Text("AudioBitrate"), + subtitle: Text( + state.audioBitrate.toString(), + ), + onTap: () => Utils.copyText( + 'AudioBitrate\n${state.audioBitrate}', + ), + ), + ListTile( + dense: true, + title: const Text("Volume"), + subtitle: Text( + state.volume.toString(), + ), + onTap: () => Utils.copyText( + 'Volume\n${state.volume}', + ), + ), + ListTile( + dense: true, + title: const Text('hwdec'), + subtitle: Text(hwdec), + onTap: () => Utils.copyText('hwdec\n$hwdec'), + ), + ], + ), + ), + ), + ), + actions: [ + TextButton( + onPressed: Get.back, + child: Text( + '确定', + style: TextStyle(color: Get.theme.colorScheme.outline), + ), + ), + ], + ); + }, + ); + } + /// 选择画质 void showSetVideoQa() { if (videoInfo.dash == null) { @@ -1522,565 +2131,6 @@ class HeaderControlState extends State { ); } - Widget resetBtn( - ThemeData theme, - Object def, - VoidCallback onPressed, { - bool isDanmaku = true, - }) { - return iconButton( - tooltip: '默认值: $def', - icon: const Icon(Icons.refresh), - onPressed: () { - onPressed(); - if (isDanmaku) { - plPlayerController.putDanmakuSettings(); - } else { - plPlayerController.putSubtitleSettings(); - } - }, - iconColor: theme.colorScheme.outline, - size: 24, - iconSize: 24, - ); - } - - /// 弹幕功能 - void showSetDanmaku() { - // 屏蔽类型 - const List<({int value, String label})> blockTypesList = [ - (value: 5, label: '顶部'), - (value: 2, label: '滚动'), - (value: 4, label: '底部'), - (value: 6, label: '彩色'), - ]; - final blockTypes = plPlayerController.blockTypes; - // 智能云屏蔽 - int danmakuWeight = plPlayerController.danmakuWeight; - // 显示区域 - double showArea = plPlayerController.showArea; - // 不透明度 - double danmakuOpacity = plPlayerController.danmakuOpacity.value; - // 字体大小 - double danmakuFontScale = plPlayerController.danmakuFontScale; - // 全屏字体大小 - double danmakuFontScaleFS = plPlayerController.danmakuFontScaleFS; - double danmakuLineHeight = plPlayerController.danmakuLineHeight; - // 弹幕速度 - double danmakuDuration = plPlayerController.danmakuDuration; - double danmakuStaticDuration = plPlayerController.danmakuStaticDuration; - // 弹幕描边 - double danmakuStrokeWidth = plPlayerController.danmakuStrokeWidth; - // 字体粗细 - int danmakuFontWeight = plPlayerController.danmakuFontWeight; - bool massiveMode = plPlayerController.massiveMode; - - final DanmakuController? danmakuController = - plPlayerController.danmakuController; - - showBottomSheet( - (context, setState) { - final theme = Theme.of(context); - - final sliderTheme = SliderThemeData( - trackHeight: 10, - trackShape: const MSliderTrackShape(), - thumbColor: theme.colorScheme.primary, - activeTrackColor: theme.colorScheme.primary, - inactiveTrackColor: theme.colorScheme.onInverseSurface, - thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6.0), - ); - - void updateLineHeight(double val) { - plPlayerController.danmakuLineHeight = danmakuLineHeight = val - .toPrecision(1); - setState(() {}); - try { - danmakuController?.updateOption( - danmakuController.option.copyWith( - lineHeight: danmakuLineHeight, - ), - ); - } catch (_) {} - } - - void updateDuration(double val) { - plPlayerController.danmakuDuration = danmakuDuration = val - .toPrecision(1); - setState(() {}); - try { - danmakuController?.updateOption( - danmakuController.option.copyWith( - duration: danmakuDuration / plPlayerController.playbackSpeed, - ), - ); - } catch (_) {} - } - - void updateStaticDuration(double val) { - plPlayerController.danmakuStaticDuration = danmakuStaticDuration = val - .toPrecision(1); - setState(() {}); - try { - danmakuController?.updateOption( - danmakuController.option.copyWith( - staticDuration: - danmakuStaticDuration / plPlayerController.playbackSpeed, - ), - ); - } catch (_) {} - } - - void updateFontSizeFS(double val) { - plPlayerController.danmakuFontScaleFS = danmakuFontScaleFS = val; - setState(() {}); - if (isFullScreen) { - try { - danmakuController?.updateOption( - danmakuController.option.copyWith( - fontSize: (15 * danmakuFontScaleFS).toDouble(), - ), - ); - } catch (_) {} - } - } - - void updateFontSize(double val) { - plPlayerController.danmakuFontScale = danmakuFontScale = val; - setState(() {}); - if (!isFullScreen) { - try { - danmakuController?.updateOption( - danmakuController.option.copyWith( - fontSize: (15 * danmakuFontScale).toDouble(), - ), - ); - } catch (_) {} - } - } - - void updateStrokeWidth(double val) { - plPlayerController.danmakuStrokeWidth = danmakuStrokeWidth = val; - setState(() {}); - try { - danmakuController?.updateOption( - danmakuController.option.copyWith( - strokeWidth: danmakuStrokeWidth, - ), - ); - } catch (_) {} - } - - void updateFontWeight(double val) { - plPlayerController.danmakuFontWeight = danmakuFontWeight = val - .toInt(); - setState(() {}); - try { - danmakuController?.updateOption( - danmakuController.option.copyWith(fontWeight: danmakuFontWeight), - ); - } catch (_) {} - } - - void updateOpacity(double val) { - plPlayerController.danmakuOpacity.value = danmakuOpacity = val; - setState(() {}); - } - - void updateShowArea(double val) { - plPlayerController.showArea = showArea = val.toPrecision(1); - setState(() {}); - try { - danmakuController?.updateOption( - danmakuController.option.copyWith(area: showArea), - ); - } catch (_) {} - } - - void updateDanmakuWeight(double val) { - plPlayerController.danmakuWeight = danmakuWeight = val.toInt(); - setState(() {}); - } - - return Padding( - padding: const EdgeInsets.all(12), - child: Material( - clipBehavior: Clip.hardEdge, - color: theme.colorScheme.surface, - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), - child: ListView( - padding: EdgeInsets.zero, - children: [ - const SizedBox( - height: 45, - child: Center(child: Text('弹幕设置', style: titleStyle)), - ), - const SizedBox(height: 10), - Row( - children: [ - Text('智能云屏蔽 $danmakuWeight 级'), - const Spacer(), - TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - onPressed: () => Get - ..back() - ..toNamed( - '/danmakuBlock', - arguments: plPlayerController, - ), - child: Text( - "屏蔽管理(${plPlayerController.filters.count})", - ), - ), - ], - ), - Padding( - padding: const EdgeInsets.only( - top: 0, - bottom: 6, - left: 10, - right: 10, - ), - child: SliderTheme( - data: sliderTheme, - child: Slider( - min: 0, - max: 10, - value: danmakuWeight.toDouble(), - divisions: 10, - label: '$danmakuWeight', - onChanged: updateDanmakuWeight, - onChangeEnd: (_) => - plPlayerController.putDanmakuSettings(), - ), - ), - ), - const Text('按类型屏蔽'), - Padding( - padding: const EdgeInsets.only(top: 12), - child: Row( - children: [ - for (final (value: value, label: label) - in blockTypesList) ...[ - ActionRowLineItem( - onTap: () { - if (blockTypes.contains(value)) { - blockTypes.remove(value); - } else { - blockTypes.add(value); - } - plPlayerController - ..blockTypes = blockTypes - ..blockColorful = blockTypes.contains(6) - ..putDanmakuSettings(); - setState(() {}); - try { - danmakuController?.updateOption( - danmakuController.option.copyWith( - hideTop: blockTypes.contains(5), - hideBottom: blockTypes.contains(4), - hideScroll: blockTypes.contains(2), - // 添加或修改其他需要修改的选项属性 - ), - ); - } catch (_) {} - }, - text: label, - selectStatus: blockTypes.contains(value), - ), - const SizedBox(width: 10), - ], - ], - ), - ), - SetSwitchItem( - title: '海量弹幕', - contentPadding: EdgeInsets.zero, - titleStyle: const TextStyle(fontSize: 14), - defaultVal: massiveMode, - setKey: SettingBoxKey.danmakuMassiveMode, - onChanged: (value) { - massiveMode = value; - plPlayerController.massiveMode = value; - setState(() {}); - try { - danmakuController?.updateOption( - danmakuController.option.copyWith(massiveMode: value), - ); - } catch (_) {} - }, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('显示区域 ${showArea * 100}%'), - resetBtn(theme, '50.0%', () => updateShowArea(0.5)), - ], - ), - Padding( - padding: const EdgeInsets.only( - top: 0, - bottom: 6, - left: 10, - right: 10, - ), - child: SliderTheme( - data: sliderTheme, - child: Slider( - min: 0.1, - max: 1, - value: showArea, - divisions: 9, - label: '${showArea * 100}%', - onChanged: updateShowArea, - onChangeEnd: (_) => - plPlayerController.putDanmakuSettings(), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('不透明度 ${danmakuOpacity * 100}%'), - resetBtn(theme, '100.0%', () => updateOpacity(1.0)), - ], - ), - Padding( - padding: const EdgeInsets.only( - top: 0, - bottom: 6, - left: 10, - right: 10, - ), - child: SliderTheme( - data: sliderTheme, - child: Slider( - min: 0, - max: 1, - value: danmakuOpacity, - divisions: 10, - label: '${danmakuOpacity * 100}%', - onChanged: updateOpacity, - onChangeEnd: (_) => - plPlayerController.putDanmakuSettings(), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('字体粗细 ${danmakuFontWeight + 1}(可能无法精确调节)'), - resetBtn(theme, 6, () => updateFontWeight(5)), - ], - ), - Padding( - padding: const EdgeInsets.only( - top: 0, - bottom: 6, - left: 10, - right: 10, - ), - child: SliderTheme( - data: sliderTheme, - child: Slider( - min: 0, - max: 8, - value: danmakuFontWeight.toDouble(), - divisions: 8, - label: '${danmakuFontWeight + 1}', - onChanged: updateFontWeight, - onChangeEnd: (_) => - plPlayerController.putDanmakuSettings(), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('描边粗细 $danmakuStrokeWidth'), - resetBtn(theme, 1.5, () => updateStrokeWidth(1.5)), - ], - ), - Padding( - padding: const EdgeInsets.only( - top: 0, - bottom: 6, - left: 10, - right: 10, - ), - child: SliderTheme( - data: sliderTheme, - child: Slider( - min: 0, - max: 5, - value: danmakuStrokeWidth, - divisions: 10, - label: '$danmakuStrokeWidth', - onChanged: updateStrokeWidth, - onChangeEnd: (_) => - plPlayerController.putDanmakuSettings(), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '字体大小 ${(danmakuFontScale * 100).toStringAsFixed(1)}%', - ), - resetBtn(theme, '100.0%', () => updateFontSize(1.0)), - ], - ), - Padding( - padding: const EdgeInsets.only( - top: 0, - bottom: 6, - left: 10, - right: 10, - ), - child: SliderTheme( - data: sliderTheme, - child: Slider( - min: 0.5, - max: 2.5, - value: danmakuFontScale, - divisions: 20, - label: - '${(danmakuFontScale * 100).toStringAsFixed(1)}%', - onChanged: updateFontSize, - onChangeEnd: (_) => - plPlayerController.putDanmakuSettings(), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '全屏字体大小 ${(danmakuFontScaleFS * 100).toStringAsFixed(1)}%', - ), - resetBtn(theme, '120.0%', () => updateFontSizeFS(1.2)), - ], - ), - Padding( - padding: const EdgeInsets.only( - top: 0, - bottom: 6, - left: 10, - right: 10, - ), - child: SliderTheme( - data: sliderTheme, - child: Slider( - min: 0.5, - max: 2.5, - value: danmakuFontScaleFS, - divisions: 20, - label: - '${(danmakuFontScaleFS * 100).toStringAsFixed(1)}%', - onChanged: updateFontSizeFS, - onChangeEnd: (_) => - plPlayerController.putDanmakuSettings(), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('滚动弹幕时长 $danmakuDuration 秒'), - resetBtn(theme, 7.0, () => updateDuration(7.0)), - ], - ), - Padding( - padding: const EdgeInsets.only( - top: 0, - bottom: 6, - left: 10, - right: 10, - ), - child: SliderTheme( - data: sliderTheme, - child: Slider( - min: 1, - max: 50, - value: danmakuDuration, - divisions: 49, - label: danmakuDuration.toString(), - onChanged: updateDuration, - onChangeEnd: (_) => - plPlayerController.putDanmakuSettings(), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('静态弹幕时长 $danmakuStaticDuration 秒'), - resetBtn(theme, 4.0, () => updateStaticDuration(4.0)), - ], - ), - Padding( - padding: const EdgeInsets.only( - top: 0, - bottom: 6, - left: 10, - right: 10, - ), - child: SliderTheme( - data: sliderTheme, - child: Slider( - min: 1, - max: 50, - value: danmakuStaticDuration, - divisions: 49, - label: danmakuStaticDuration.toString(), - onChanged: updateStaticDuration, - onChangeEnd: (_) => - plPlayerController.putDanmakuSettings(), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('弹幕行高 $danmakuLineHeight'), - resetBtn(theme, 1.6, () => updateLineHeight(1.6)), - ], - ), - Padding( - padding: const EdgeInsets.only( - top: 0, - bottom: 6, - left: 10, - right: 10, - ), - child: SliderTheme( - data: sliderTheme, - child: Slider( - min: 1.0, - max: 3.0, - value: danmakuLineHeight, - onChanged: updateLineHeight, - onChangeEnd: (_) => - plPlayerController.putDanmakuSettings(), - ), - ), - ), - ], - ), - ), - ), - ); - }, - ); - } - void showDanmakuPool() { final ctr = plPlayerController.danmakuController; if (ctr == null) return; diff --git a/lib/utils/page_utils.dart b/lib/utils/page_utils.dart index 6f063a6c2..7a0cc616b 100644 --- a/lib/utils/page_utils.dart +++ b/lib/utils/page_utils.dart @@ -644,7 +644,7 @@ abstract class PageUtils { static void showVideoBottomSheet( BuildContext context, { required Widget child, - required Function isFullScreen, + required bool Function() isFullScreen, double? padding, }) { if (!context.mounted) {