From a39f81ac2ad8c0927c194cf8a65d180e270e88ca Mon Sep 17 00:00:00 2001 From: guozhigq Date: Mon, 4 Sep 2023 11:10:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BC=B9=E5=B9=95=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/danmaku/view.dart | 37 ++- .../detail/introduction/widgets/menu_row.dart | 101 +++++-- .../video/detail/widgets/header_control.dart | 266 +++++++++++++++++- lib/plugin/pl_player/controller.dart | 32 +++ lib/utils/danmaku.dart | 1 + lib/utils/storage.dart | 14 +- 6 files changed, 414 insertions(+), 37 deletions(-) diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart index 972c96c3..750851d2 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -29,6 +29,11 @@ class _PlDanmakuState extends State { bool danmuPlayStatus = true; Box setting = GStrorage.setting; late bool enableShowDanmaku; + late List blockTypes; + late double showArea; + late double opacityVal; + late double fontSizeVal; + late double danmakuSpeedVal; @override void initState() { @@ -58,6 +63,11 @@ class _PlDanmakuState extends State { } } }); + blockTypes = playerController.blockTypes; + showArea = playerController.showArea; + opacityVal = playerController.opacityVal; + fontSizeVal = playerController.fontSizeVal; + danmakuSpeedVal = playerController.danmakuSpeedVal; } // 播放器状态监听 @@ -77,6 +87,7 @@ class _PlDanmakuState extends State { } PlDanmakuController ctr = _plDanmakuController; int currentPosition = position.inMilliseconds; + blockTypes = playerController.blockTypes; if (!playerController.isOpenDanmu.value) { return; @@ -99,14 +110,17 @@ class _PlDanmakuState extends State { var delta = currentPosition - element.progress; if (delta >= 0 && delta < 200) { - _controller!.addItems([ - DanmakuItem( - element.content, - color: DmUtils.decimalToColor(element.color), - time: element.progress, - type: DmUtils.getPosition(element.mode), - ) - ]); + // 屏蔽彩色弹幕 + if (blockTypes.contains(6) ? element.color == 16777215 : true) { + _controller!.addItems([ + DanmakuItem( + element.content, + color: DmUtils.decimalToColor(element.color), + time: element.progress, + type: DmUtils.getPosition(element.mode), + ) + ]); + } ctr.currentDmIndex++; } else { if (!playerController.isOpenDanmu.value) { @@ -135,9 +149,10 @@ class _PlDanmakuState extends State { widget.playerController.danmakuController = _controller = e; }, option: DanmakuOption( - fontSize: 15, - area: 0.5, - duration: 5, + fontSize: 15 * fontSizeVal, + area: showArea, + opacity: opacityVal, + duration: danmakuSpeedVal * widget.playerController.playbackSpeed, ), statusChanged: (isPlaying) {}, ), diff --git a/lib/pages/video/detail/introduction/widgets/menu_row.dart b/lib/pages/video/detail/introduction/widgets/menu_row.dart index 91af6fba..6f9cf51b 100644 --- a/lib/pages/video/detail/introduction/widgets/menu_row.dart +++ b/lib/pages/video/detail/introduction/widgets/menu_row.dart @@ -17,35 +17,31 @@ class MenuRow extends StatelessWidget { child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row(children: [ - actionRowLineItem( - context, - () => {}, - loadingStatus, - '推荐', - selectStatus: true, - ), - const SizedBox(width: 8), - actionRowLineItem( - context, - () => {}, - loadingStatus, - '弹幕', + ActionRowLineItem( + onTap: () => {}, + loadingStatus: loadingStatus, + text: '推荐', selectStatus: false, ), const SizedBox(width: 8), - actionRowLineItem( - context, - () => {}, - loadingStatus, - '评论列表', + ActionRowLineItem( + onTap: () => {}, + loadingStatus: loadingStatus, + text: '弹幕', selectStatus: false, ), const SizedBox(width: 8), - actionRowLineItem( - context, - () => {}, - loadingStatus, - '播放列表', + ActionRowLineItem( + onTap: () => {}, + loadingStatus: loadingStatus, + text: '评论列表', + selectStatus: false, + ), + const SizedBox(width: 8), + ActionRowLineItem( + onTap: () => {}, + loadingStatus: loadingStatus, + text: '播放列表', selectStatus: false, ), ]), @@ -99,3 +95,62 @@ class MenuRow extends StatelessWidget { ); } } + +class ActionRowLineItem extends StatelessWidget { + final bool? selectStatus; + final Function? onTap; + final bool? loadingStatus; + final String? text; + + const ActionRowLineItem( + {super.key, + this.selectStatus, + this.onTap, + this.text, + this.loadingStatus = false}); + + @override + Widget build(BuildContext context) { + return Material( + color: selectStatus! + ? Theme.of(context).colorScheme.secondaryContainer + : Colors.transparent, + borderRadius: const BorderRadius.all(Radius.circular(30)), + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () => { + feedBack(), + onTap!(), + }, + child: Container( + padding: const EdgeInsets.fromLTRB(13, 5.5, 13, 4.5), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(30)), + border: Border.all( + color: selectStatus! + ? Colors.transparent + : Theme.of(context).colorScheme.secondaryContainer, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedOpacity( + opacity: loadingStatus! ? 0 : 1, + duration: const Duration(milliseconds: 200), + child: Text( + text!, + style: TextStyle( + fontSize: 13, + color: selectStatus! + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.outline), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index 968bbac0..95c3052d 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -2,10 +2,14 @@ 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:hive/hive.dart'; +import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/pages/video/detail/index.dart'; +import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; +import 'package:pilipala/utils/storage.dart'; class HeaderControl extends StatefulWidget implements PreferredSizeWidget { final PlPlayerController? controller; @@ -29,6 +33,7 @@ class _HeaderControlState extends State { TextStyle subTitleStyle = const TextStyle(fontSize: 12); TextStyle titleStyle = const TextStyle(fontSize: 14); Size get preferredSize => const Size(double.infinity, kToolbarHeight); + Box localCache = GStrorage.localCache; @override void initState() { @@ -146,9 +151,8 @@ class _HeaderControlState extends State { // title: Text('播放设置', style: titleStyle), // ), ListTile( - onTap: () {}, + onTap: () => {Get.back(), showSetDanmaku()}, dense: true, - enabled: false, leading: const Icon(Icons.subtitles_outlined, size: 20), title: Text('弹幕设置', style: titleStyle), ), @@ -454,6 +458,246 @@ class _HeaderControlState extends State { ); } + /// 弹幕功能 + void showSetDanmaku() async { + // 屏蔽类型 + List> blockTypesList = [ + {'value': 5, 'label': '顶部'}, + {'value': 2, 'label': '滚动'}, + {'value': 4, 'label': '底部'}, + {'value': 6, 'label': '彩色'}, + ]; + List blockTypes = widget.controller!.blockTypes; + // 显示区域 + List> showAreas = [ + {'value': 0.25, 'label': '1/4屏'}, + {'value': 0.5, 'label': '半屏'}, + {'value': 0.75, 'label': '3/4屏'}, + {'value': 1.0, 'label': '满屏'}, + ]; + double showArea = widget.controller!.showArea; + // 不透明度 + double opacityVal = widget.controller!.opacityVal; + // 字体大小 + double fontSizeVal = widget.controller!.fontSizeVal; + // 弹幕速度 + double danmakuSpeedVal = widget.controller!.danmakuSpeedVal; + + DanmakuController danmakuController = widget.controller!.danmakuController!; + await showModalBottomSheet( + context: context, + elevation: 0, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return StatefulBuilder(builder: (context, StateSetter setState) { + return Container( + width: double.infinity, + height: 580, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + margin: const EdgeInsets.all(12), + padding: const EdgeInsets.only(left: 14, right: 14), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 45, + child: Center(child: Text('弹幕设置', style: titleStyle)), + ), + const SizedBox(height: 10), + const Text('按类型屏蔽'), + Padding( + padding: const EdgeInsets.only(top: 12, bottom: 18), + child: Row( + children: [ + for (var i in blockTypesList) ...[ + ActionRowLineItem( + onTap: () async { + bool isChoose = blockTypes.contains(i['value']); + if (isChoose) { + blockTypes.remove(i['value']); + } else { + blockTypes.add(i['value']); + } + widget.controller!.blockTypes = blockTypes; + setState(() {}); + try { + DanmakuOption currentOption = + danmakuController.option; + DanmakuOption updatedOption = + currentOption.copyWith( + hideTop: blockTypes.contains(5), + hideBottom: blockTypes.contains(4), + hideScroll: blockTypes.contains(2), + // 添加或修改其他需要修改的选项属性 + ); + danmakuController.updateOption(updatedOption); + } catch (_) {} + }, + text: i['label'], + selectStatus: blockTypes.contains(i['value']), + ), + const SizedBox(width: 10), + ] + ], + ), + ), + const Text('显示区域'), + Padding( + padding: const EdgeInsets.only(top: 12, bottom: 18), + child: Row( + children: [ + for (var i in showAreas) ...[ + ActionRowLineItem( + onTap: () { + showArea = i['value']; + widget.controller!.showArea = showArea; + setState(() {}); + try { + DanmakuOption currentOption = + danmakuController.option; + DanmakuOption updatedOption = + currentOption.copyWith(area: i['value']); + danmakuController.updateOption(updatedOption); + } catch (_) {} + }, + text: i['label'], + selectStatus: showArea == i['value'], + ), + const SizedBox(width: 10), + ] + ], + ), + ), + Text('不透明度 ${opacityVal * 100}%'), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: SliderThemeData( + trackShape: MSliderTrackShape(), + thumbColor: Theme.of(context).colorScheme.primary, + activeTrackColor: Theme.of(context).colorScheme.primary, + trackHeight: 10, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 6.0), + ), + child: Slider( + min: 0, + max: 1, + value: opacityVal, + divisions: 10, + label: '${opacityVal * 100}%', + onChanged: (double val) { + opacityVal = val; + widget.controller!.opacityVal = opacityVal; + setState(() {}); + try { + DanmakuOption currentOption = + danmakuController.option; + DanmakuOption updatedOption = + currentOption.copyWith(opacity: val); + danmakuController.updateOption(updatedOption); + } catch (_) {} + }, + ), + ), + ), + Text('字体大小 ${(fontSizeVal * 100).toStringAsFixed(1)}%'), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: SliderThemeData( + trackShape: MSliderTrackShape(), + thumbColor: Theme.of(context).colorScheme.primary, + activeTrackColor: Theme.of(context).colorScheme.primary, + trackHeight: 10, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 6.0), + ), + child: Slider( + min: 0.5, + max: 2.5, + value: fontSizeVal, + divisions: 20, + label: '${(fontSizeVal * 100).toStringAsFixed(1)}%', + onChanged: (double val) { + fontSizeVal = val; + widget.controller!.fontSizeVal = fontSizeVal; + setState(() {}); + try { + DanmakuOption currentOption = + danmakuController.option; + DanmakuOption updatedOption = + currentOption.copyWith( + fontSize: (15 * fontSizeVal).toDouble(), + ); + danmakuController.updateOption(updatedOption); + } catch (_) {} + }, + ), + ), + ), + Text('弹幕时长 ${danmakuSpeedVal.toString()}'), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: SliderThemeData( + trackShape: MSliderTrackShape(), + thumbColor: Theme.of(context).colorScheme.primary, + activeTrackColor: Theme.of(context).colorScheme.primary, + trackHeight: 10, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 6.0), + ), + child: Slider( + min: 1, + max: 6, + value: danmakuSpeedVal, + divisions: 10, + label: danmakuSpeedVal.toString(), + onChanged: (double val) { + danmakuSpeedVal = val; + widget.controller!.danmakuSpeedVal = danmakuSpeedVal; + setState(() {}); + try { + DanmakuOption currentOption = + danmakuController.option; + DanmakuOption updatedOption = + currentOption.copyWith(duration: val); + danmakuController.updateOption(updatedOption); + } catch (_) {} + }, + ), + ), + ), + ], + ), + ), + ); + }); + }, + ); + } + @override Widget build(BuildContext context) { final _ = widget.controller!; @@ -556,3 +800,21 @@ class _HeaderControlState extends State { ); } } + +class MSliderTrackShape extends RoundedRectSliderTrackShape { + @override + Rect getPreferredRect({ + required RenderBox parentBox, + Offset offset = Offset.zero, + SliderThemeData? sliderTheme, + bool isEnabled = false, + bool isDiscrete = false, + }) { + const double trackHeight = 3; + final double trackLeft = offset.dx; + final double trackTop = + offset.dy + (parentBox.size.height - trackHeight) / 2 + 4; + final double trackWidth = parentBox.size.width; + return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight); + } +} diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index fe63cc3d..386524b9 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -21,6 +21,7 @@ import 'package:universal_platform/universal_platform.dart'; Box videoStorage = GStrorage.video; Box setting = GStrorage.setting; +Box localCache = GStrorage.localCache; class PlPlayerController { Player? _videoPlayerController; @@ -199,12 +200,30 @@ class PlPlayerController { Rx isOpenDanmu = false.obs; // 关联弹幕控制器 DanmakuController? danmakuController; + // 弹幕相关配置 + late List blockTypes; + late double showArea; + late double opacityVal; + late double fontSizeVal; + late double danmakuSpeedVal; // 添加一个私有构造函数 PlPlayerController._() { _videoType = videoType; isOpenDanmu.value = setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false); + blockTypes = + localCache.get(LocalCacheKey.danmakuBlockType, defaultValue: []); + showArea = localCache.get(LocalCacheKey.danmakuShowArea, defaultValue: 0.5); + // 不透明度 + opacityVal = + localCache.get(LocalCacheKey.danmakuOpacity, defaultValue: 1.0); + // 字体大小 + fontSizeVal = + localCache.get(LocalCacheKey.danmakuFontScale, defaultValue: 1.0); + // 弹幕速度 + danmakuSpeedVal = + localCache.get(LocalCacheKey.danmakuSpeed, defaultValue: 4.0); // _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) { // if (status == PlayerStatus.playing) { // WakelockPlus.enable(); @@ -524,6 +543,12 @@ class PlPlayerController { /// 设置倍速 Future setPlaybackSpeed(double speed) async { await _videoPlayerController?.setRate(speed); + try { + DanmakuOption currentOption = danmakuController!.option; + DanmakuOption updatedOption = currentOption.copyWith( + duration: (currentOption.duration / speed) * playbackSpeed); + danmakuController!.updateOption(updatedOption); + } catch (_) {} _playbackSpeed.value = speed; } @@ -891,6 +916,13 @@ class PlPlayerController { // playerStatus.status.close(); // dataStatus.status.close(); + /// 缓存本次弹幕选项 + localCache.put(LocalCacheKey.danmakuBlockType, blockTypes); + localCache.put(LocalCacheKey.danmakuShowArea, showArea); + localCache.put(LocalCacheKey.danmakuOpacity, opacityVal); + localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal); + localCache.put(LocalCacheKey.danmakuSpeed, danmakuSpeedVal); + removeListeners(); await _videoPlayerController?.dispose(); _videoPlayerController = null; diff --git a/lib/utils/danmaku.dart b/lib/utils/danmaku.dart index a76cc77f..8305edec 100644 --- a/lib/utils/danmaku.dart +++ b/lib/utils/danmaku.dart @@ -3,6 +3,7 @@ import 'package:ns_danmaku/ns_danmaku.dart'; class DmUtils { static Color decimalToColor(int decimalColor) { + // 16777215 表示白色 int red = (decimalColor >> 16) & 0xFF; int green = (decimalColor >> 8) & 0xFF; int blue = decimalColor & 0xFF; diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index b0be21b0..536714eb 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -34,7 +34,12 @@ class GStrorage { }, ); // 本地缓存 - localCache = await Hive.openBox('localCache'); + localCache = await Hive.openBox( + 'localCache', + compactionStrategy: (entries, deletedEntries) { + return deletedEntries > 4; + }, + ); // 设置 setting = await Hive.openBox('setting'); // 搜索历史 @@ -134,6 +139,13 @@ class LocalCacheKey { // static const String wbiKeys = 'wbiKeys'; static const String timeStamp = 'timeStamp'; + + // 弹幕相关设置 屏蔽类型 显示区域 透明度 字体大小 弹幕速度 + static const String danmakuBlockType = 'danmakuBlockType'; + static const String danmakuShowArea = 'danmakuShowArea'; + static const String danmakuOpacity = 'danmakuOpacity'; + static const String danmakuFontScale = 'danmakuFontScale'; + static const String danmakuSpeed = 'danmakuSpeed'; } class VideoBoxKey {