From 7ba9646d38961ed6c4d84a9d8dfcb395539486c1 Mon Sep 17 00:00:00 2001 From: dom Date: Fri, 31 Jan 2025 11:36:05 +0800 Subject: [PATCH] feat: danmaku chart (#192) Signed-off-by: bggRGjQaUbCoE --- lib/pages/setting/widgets/model.dart | 8 + lib/pages/video/detail/controller.dart | 33 ++++ lib/plugin/pl_player/controller.dart | 41 +++-- .../pl_player/models/bottom_control_type.dart | 1 + lib/plugin/pl_player/view.dart | 157 ++++++++++++++---- .../pl_player/widgets/bottom_control.dart | 40 ++--- lib/utils/storage.dart | 4 + pubspec.lock | 16 ++ pubspec.yaml | 1 + 9 files changed, 223 insertions(+), 78 deletions(-) diff --git a/lib/pages/setting/widgets/model.dart b/lib/pages/setting/widgets/model.dart index 8ddb9699..a8346cc9 100644 --- a/lib/pages/setting/widgets/model.dart +++ b/lib/pages/setting/widgets/model.dart @@ -1929,6 +1929,14 @@ List get extraSettings => [ setKey: SettingBoxKey.showSeekPreview, defaultVal: true, ), + SettingsModel( + settingsType: SettingsType.sw1tch, + title: '显示高能进度条', + subtitle: '高能进度条反应了在时域上,单位时间内弹幕发送量的变化趋势', + leading: Icon(Icons.show_chart), + setKey: SettingBoxKey.showDmChart, + defaultVal: false, + ), SettingsModel( settingsType: SettingsType.sw1tch, enableFeedback: true, diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 7d16fc7a..9438a1a9 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -1004,6 +1004,7 @@ class VideoDetailController extends GetxController vttSubtitles: _vttSubtitles, vttSubtitlesIndex: vttSubtitlesIndex, showVP: showVP, + dmTrend: dmTrend, // 硬解 enableHA: enableHA.value, hwdec: hwdec.value, @@ -1037,6 +1038,10 @@ class VideoDetailController extends GetxController _getSubtitle(); } + if (showDmChart && dmTrend == null) { + _getDmTrend(); + } + /// 开启自动全屏时,在player初始化完成后立即传入headerControl plPlayerController.headerControl = headerControl; @@ -1970,6 +1975,7 @@ class VideoDetailController extends GetxController audioUrl = null; // danmaku + dmTrend = null; savedDanmaku = null; // subtitle @@ -1985,4 +1991,31 @@ class VideoDetailController extends GetxController segmentList.clear(); _segmentProgressList = null; } + + late final showDmChart = GStorage.showDmChart; + List? dmTrend; + + void _getDmTrend() async { + dmTrend = []; + try { + dynamic res = await Request().get( + 'https://bvc.bilivideo.com/pbp/data', + queryParameters: { + 'bvid': bvid, + 'cid': cid.value, + }, + ); + + int stepSec = (res.data['step_sec'] as num?)?.toInt() ?? 0; + late List events = (res.data['events']['default'] as List?) ?? []; + if (stepSec != 0 && events.isNotEmpty) { + dmTrend = events; + if (plPlayerController.dmTrend.isEmpty) { + plPlayerController.dmTrend.value = events; + } + } + } catch (e) { + debugPrint('_getDmTrend: $e'); + } + } } diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 08c24afb..f82dcc20 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -461,6 +461,7 @@ class PlPlayerController { List>? vttSubtitles, int? vttSubtitlesIndex, bool? showVP, + List? dmTrend, bool autoplay = true, // 默认不循环 PlaylistMode looping = PlaylistMode.none, @@ -493,6 +494,7 @@ class PlPlayerController { this.vttSubtitles.value = vttSubtitles ?? >[]; this.vttSubtitlesIndex.value = vttSubtitlesIndex ?? 0; this.showVP.value = showVP ?? true; + this.dmTrend.value = dmTrend ?? []; _autoPlay = autoplay; _looping = looping; // 初始化视频倍速 @@ -1581,23 +1583,30 @@ class PlPlayerController { return; } _isQueryingVideoShot = true; - dynamic res = await Request().get( - 'https://api.bilibili.com/x/player/videoshot', - queryParameters: { - // 'aid': IdUtils.bv2av(_bvid), - 'bvid': _bvid, - 'cid': _cid, - 'index': 1, - }, - ); - if (res.data['code'] == 0) { - videoShot = { - 'status': true, - 'data': res.data['data'], - }; - } else { - videoShot = {'status': false}; + try { + dynamic res = await Request().get( + 'https://api.bilibili.com/x/player/videoshot', + queryParameters: { + // 'aid': IdUtils.bv2av(_bvid), + 'bvid': _bvid, + 'cid': _cid, + 'index': 1, + }, + ); + if (res.data['code'] == 0) { + videoShot = { + 'status': true, + 'data': res.data['data'], + }; + } else { + videoShot = {'status': false}; + } + } catch (e) { + debugPrint('getVideoShot: $e'); } _isQueryingVideoShot = false; } + + late final RxList dmTrend = [].obs; + late final RxBool showDmChart = true.obs; } diff --git a/lib/plugin/pl_player/models/bottom_control_type.dart b/lib/plugin/pl_player/models/bottom_control_type.dart index 7d1c6e22..456d0cd8 100644 --- a/lib/plugin/pl_player/models/bottom_control_type.dart +++ b/lib/plugin/pl_player/models/bottom_control_type.dart @@ -12,4 +12,5 @@ enum BottomControlType { custom, viewPoints, superResolution, + dmChart, } diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 80760807..5d5c8817 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -9,6 +9,7 @@ import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_debounce/easy_throttle.dart'; +import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; @@ -339,7 +340,43 @@ class _PLVideoPlayerState extends State /// 空白占位 BottomControlType.space: const Spacer(), - /// 分段信息 + /// 高能进度条 + BottomControlType.dmChart: Obx(() => plPlayerController.dmTrend.isEmpty + ? const SizedBox.shrink() + : Container( + width: widgetWidth, + height: 30, + alignment: Alignment.center, + child: ComBtn( + icon: plPlayerController.showDmChart.value + ? Icon( + Icons.show_chart, + size: 22, + color: Colors.white, + ) + : Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.show_chart, + size: 22, + color: Colors.white, + ), + Icon( + Icons.hide_source, + size: 22, + color: Colors.white, + ), + ], + ), + fuc: () { + plPlayerController.showDmChart.value = + !plPlayerController.showDmChart.value; + }, + ), + )), + + /// 超分辨率 BottomControlType.superResolution: Get.parameters['type'] == '1' || Get.parameters['type'] == '4' ? Container( @@ -520,8 +557,10 @@ class _PLVideoPlayerState extends State width: 35, height: 30, alignment: Alignment.center, - child: const Icon( - Icons.closed_caption_off_outlined, + child: Icon( + plPlayerController.vttSubtitlesIndex.value == 0 + ? Icons.closed_caption_off_outlined + : Icons.closed_caption_off_rounded, size: 22, color: Colors.white, semanticLabel: '字幕', @@ -586,6 +625,7 @@ class _PLVideoPlayerState extends State if (anySeason) BottomControlType.pre, if (anySeason) BottomControlType.next, BottomControlType.space, + BottomControlType.dmChart, BottomControlType.superResolution, BottomControlType.viewPoints, if (anySeason) BottomControlType.episode, @@ -1071,7 +1111,6 @@ class _PLVideoPlayerState extends State ), /// 进度条 live模式下禁用 - Obx( () { final int value = plPlayerController.sliderPositionSeconds.value; @@ -1112,37 +1151,12 @@ class _PLVideoPlayerState extends State clipBehavior: Clip.none, alignment: Alignment.bottomCenter, children: [ + if (plPlayerController.dmTrend.isNotEmpty && + plPlayerController.showDmChart.value) + buildDmChart(context, plPlayerController), if (plPlayerController.viewPointList.isNotEmpty && plPlayerController.showVP.value) - LayoutBuilder( - builder: (context, constraints) { - return SizedBox( - height: 20, - child: Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: (event) { - try { - double seg = event.localPosition.dx / - constraints.maxWidth; - Segment item = plPlayerController - .viewPointList - .where((item) { - return item.start >= seg; - }).reduce((a, b) => - a.start < b.start ? a : b); - if (item.from != null) { - plPlayerController.seekTo( - Duration(seconds: item.from!)); - } - // debugPrint('${item.title},,${item.from}'); - } catch (e) { - debugPrint('$e'); - } - }, - ), - ); - }, - ), + buildViewPointWidget(plPlayerController), ProgressBar( progress: Duration(seconds: value), buffered: Duration(seconds: buffer), @@ -1503,6 +1517,56 @@ class _PLVideoPlayerState extends State } } +Widget buildDmChart( + BuildContext context, + PlPlayerController plPlayerController, [ + double offset = 0, +]) { + return IgnorePointer( + child: Container( + height: 14, + margin: EdgeInsets.only( + bottom: plPlayerController.viewPointList.isNotEmpty && + plPlayerController.showVP.value + ? 20.25 + offset + : 4.25 + offset, + ), + child: LineChart( + LineChartData( + titlesData: const FlTitlesData(show: false), + lineTouchData: const LineTouchData(enabled: false), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + minX: 0, + maxX: plPlayerController.dmTrend.length.toDouble(), + minY: 0, + maxY: plPlayerController.dmTrend + .reduce((a, b) => a > b ? a : b) + .toDouble(), + lineBarsData: [ + LineChartBarData( + spots: List.generate( + plPlayerController.dmTrend.length, + (index) => FlSpot( + index.toDouble(), + plPlayerController.dmTrend[index].toDouble(), + ), + ), + isCurved: true, + barWidth: 0, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + color: Theme.of(context).colorScheme.primary.withOpacity(0.4), + ), + ), + ], + ), + ), + ), + ); +} + Widget buildSeekPreviewWidget(PlPlayerController plPlayerController) { return Obx(() { if (plPlayerController.showPreview.value.not) { @@ -1605,3 +1669,30 @@ Widget buildSeekPreviewWidget(PlPlayerController plPlayerController) { }); }); } + +Widget buildViewPointWidget(PlPlayerController plPlayerController) { + return LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + height: 20, + child: Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: (event) { + try { + double seg = event.localPosition.dx / constraints.maxWidth; + Segment item = plPlayerController.viewPointList.where((item) { + return item.start >= seg; + }).reduce((a, b) => a.start < b.start ? a : b); + if (item.from != null) { + plPlayerController.seekTo(Duration(seconds: item.from!)); + } + // debugPrint('${item.title},,${item.from}'); + } catch (e) { + debugPrint('$e'); + } + }, + ), + ); + }, + ); +} diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index 8d64c702..e068f90d 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -7,7 +7,11 @@ import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:nil/nil.dart'; import 'package:PiliPlus/plugin/pl_player/index.dart' - show PlPlayerController, buildSeekPreviewWidget; + show + PlPlayerController, + buildSeekPreviewWidget, + buildDmChart, + buildViewPointWidget; import 'package:PiliPlus/utils/feed_back.dart'; import '../../../common/widgets/audio_video_progress_bar.dart'; @@ -53,36 +57,14 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { clipBehavior: Clip.none, alignment: Alignment.bottomCenter, children: [ + if (controller?.dmTrend.isNotEmpty == true && + controller?.showDmChart.value == true) + buildDmChart(context, controller!, 4.5), if (controller?.viewPointList.isNotEmpty == true && controller?.showVP.value == true) - LayoutBuilder( - builder: (context, constraints) { - return Container( - height: 20, - margin: const EdgeInsets.only(bottom: 5.25), - child: Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: (event) { - try { - double seg = event.localPosition.dx / - constraints.maxWidth; - Segment? item = controller?.viewPointList - .where((item) { - return item.start >= seg; - }).reduce((a, b) => - a.start < b.start ? a : b); - if (item?.from != null) { - controller?.seekTo( - Duration(seconds: item!.from!)); - } - // debugPrint('${item?.title},,${item?.from}'); - } catch (e) { - debugPrint('$e'); - } - }, - ), - ); - }, + Padding( + padding: const EdgeInsets.only(bottom: 5.25), + child: buildViewPointWidget(controller!), ), ProgressBar( progress: Duration(seconds: value), diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 89678373..c3fe706b 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -360,6 +360,9 @@ class GStorage { static bool get showSeekPreview => GStorage.setting.get(SettingBoxKey.showSeekPreview, defaultValue: true); + static bool get showDmChart => + GStorage.setting.get(SettingBoxKey.showDmChart, defaultValue: false); + static List get dynamicDetailRatio => List.from(setting .get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0])); @@ -589,6 +592,7 @@ class SettingBoxKey { showDynDecorate = 'showDynDecorate', enableLivePhoto = 'enableLivePhoto', showSeekPreview = 'showSeekPreview', + showDmChart = 'showDmChart', // Sponsor Block enableSponsorBlock = 'enableSponsorBlock', diff --git a/pubspec.lock b/pubspec.lock index 2a7c4069..26dbc132 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -473,6 +473,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" expandable: dependency: "direct main" description: @@ -570,6 +578,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08" + url: "https://pub.dev" + source: hosted + version: "0.69.2" flex_seed_scheme: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 56c8a692..8e9a8cd5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -181,6 +181,7 @@ dependencies: expandable: ^5.0.1 flex_seed_scheme: ^3.4.1 live_photo_maker: ^0.0.6 + fl_chart: ^0.69.2 dependency_overrides: screen_brightness: ^2.0.1