Compare commits

..

8 Commits

Author SHA1 Message Date
bggRGjQaUbCoE
31a639400e opt: dm chart
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-01-31 16:20:59 +08:00
bggRGjQaUbCoE
d6b24561fa fix: dm chart x
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-01-31 13:32:07 +08:00
dom
7ba9646d38 feat: danmaku chart (#192)
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-01-31 11:36:05 +08:00
bggRGjQaUbCoE
58a7cf1e75 fix: image preview
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-01-30 18:03:15 +08:00
bggRGjQaUbCoE
1a327198f7 opt: video: onreset
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-01-30 15:52:59 +08:00
bggRGjQaUbCoE
e4fe91ef92 Update README.md
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-01-30 14:22:20 +08:00
bggRGjQaUbCoE
afcf817c4f fix: video duration
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-01-30 13:57:21 +08:00
bggRGjQaUbCoE
21550815db fix: seek preview image
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-01-30 12:11:44 +08:00
13 changed files with 283 additions and 108 deletions

View File

@@ -47,6 +47,14 @@
## feat
- [x] 滑动跳转预览视频缩略图
- [x] Live Photo
- [x] 复制/移动收藏夹/稍后再看视频
- [x] 超分辨率
- [x] 合并弹幕
- [x] 会员彩色弹幕
- [x] 播放全部/继续播放/倒序播放
- [x] Cookie登录
- [x] 显示视频分段信息
- [x] 调节字幕大小
- [x] 调节全屏弹幕大小
@@ -74,7 +82,7 @@
- [x] 转发动态
- [x] 合集图片
- [x] 删除/置顶私信
- [x] 举报用户/评论/视频
- [x] 举报用户/评论/视频/动态
- [x] 删除/发布文本/图片动态
- [x] 其他

View File

@@ -254,7 +254,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
}
String _getActualUrl(int index) => _thumbList[index] && _quality != 100
? '${widget.sources[index]}@${_quality}q.webp'.http2https
? '${widget.sources[index].url}@${_quality}q.webp'.http2https
: widget.sources[index].url.http2https;
void onClose() {

View File

@@ -418,11 +418,7 @@ class BangumiIntroController extends CommonController {
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
..plPlayerController.pause()
..makeHeartBeat()
..playedTime = null
..videoUrl = null
..audioUrl = null
..vttSubtitlesIndex = null
..savedDanmaku = null
..onReset()
..epId = epId
..bvid = bvid
..cid.value = cid

View File

@@ -1929,6 +1929,14 @@ List<SettingsModel> 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,

View File

@@ -635,6 +635,9 @@ class VideoDetailController extends GetxController
Future _querySponsorBlock() async {
positionSubscription?.cancel();
videoLabel.value = '';
segmentList.clear();
_segmentProgressList = null;
dynamic result = await Request().get(
'${GStorage.blockServer}/api/skipSegments',
queryParameters: {
@@ -643,9 +646,6 @@ class VideoDetailController extends GetxController
},
options: _options,
);
videoLabel.value = '';
segmentList.clear();
_segmentProgressList = null;
_handleSBData(result);
}
@@ -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;
@@ -1359,7 +1364,8 @@ class VideoDetailController extends GetxController
duration += split[i] * pow(60, i).toInt();
}
if (duration <=
plPlayerController.durationSeconds.value) {
plPlayerController
.durationSeconds.value.inSeconds) {
setState(() {
updateSegment(
isFirst: isFirst,
@@ -1687,7 +1693,9 @@ class VideoDetailController extends GetxController
'userID': GStorage.blockUserID,
'userAgent': Constants.userAgent,
'videoDuration': plPlayerController
.durationSeconds.value,
.durationSeconds
.value
.inSeconds,
},
data: {
'segments': list!
@@ -1960,4 +1968,54 @@ class VideoDetailController extends GetxController
tabCtr.dispose();
super.onClose();
}
onReset() {
playedTime = null;
videoUrl = null;
audioUrl = null;
// danmaku
dmTrend = null;
savedDanmaku = null;
// subtitle
vttSubtitlesIndex = null;
_vttSubtitles.clear();
// view point
viewPointList.clear();
// sponsor block
positionSubscription?.cancel();
videoLabel.value = '';
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');
}
}
}

View File

@@ -583,12 +583,8 @@ class VideoIntroController extends GetxController
final videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag)
..plPlayerController.pause()
..makeHeartBeat()
..playedTime = null
..videoUrl = null
..audioUrl = null
..updateMediaListHistory(aid)
..vttSubtitlesIndex = null
..savedDanmaku = null
..onReset()
..bvid = bvid
..oid.value = aid ?? IdUtils.bv2av(bvid)
..cid.value = cid

View File

@@ -58,7 +58,7 @@ class PlPlayerController {
// 展示使用
final Rx<Duration> _sliderTempPosition = Rx(Duration.zero);
final Rx<Duration> _duration = Rx(Duration.zero);
final RxInt durationSeconds = 0.obs;
final Rx<Duration> durationSeconds = Duration.zero.obs;
final Rx<Duration> _buffered = Rx(Duration.zero);
final RxInt bufferedSeconds = 0.obs;
@@ -330,9 +330,8 @@ class PlPlayerController {
}
void updateDurationSecond() {
int newSecond = _duration.value.inSeconds;
if (durationSeconds.value != newSecond) {
durationSeconds.value = newSecond;
if (durationSeconds.value != _duration.value) {
durationSeconds.value = _duration.value;
}
}
@@ -462,6 +461,7 @@ class PlPlayerController {
List<Map<String, String>>? vttSubtitles,
int? vttSubtitlesIndex,
bool? showVP,
List? dmTrend,
bool autoplay = true,
// 默认不循环
PlaylistMode looping = PlaylistMode.none,
@@ -494,6 +494,7 @@ class PlPlayerController {
this.vttSubtitles.value = vttSubtitles ?? <Map<String, String>>[];
this.vttSubtitlesIndex.value = vttSubtitlesIndex ?? 0;
this.showVP.value = showVP ?? true;
this.dmTrend.value = dmTrend ?? [];
_autoPlay = autoplay;
_looping = looping;
// 初始化视频倍速
@@ -1437,7 +1438,7 @@ class PlPlayerController {
}
bool isComplete = playerStatus.status.value == PlayerStatus.completed ||
type == 'completed';
if ((duration.value - position.value).inMilliseconds > 1000) {
if ((durationSeconds.value - position.value).inMilliseconds > 1000) {
isComplete = false;
}
// 播放状态变化时,更新
@@ -1582,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;
}

View File

@@ -12,4 +12,5 @@ enum BottomControlType {
custom,
viewPoints,
superResolution,
dmChart,
}

View File

@@ -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';
@@ -321,7 +322,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
}),
Obx(
() => Text(
Utils.timeFormat(plPlayerController.durationSeconds.value),
Utils.timeFormat(
plPlayerController.durationSeconds.value.inSeconds),
style: const TextStyle(
color: Color(0xFFD0D0D0),
fontSize: 10,
@@ -329,7 +331,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
fontFeatures: [FontFeature.tabularFigures()],
),
semanticsLabel:
'${Utils.durationReadFormat(Utils.timeFormat(plPlayerController.durationSeconds.value))}',
'${Utils.durationReadFormat(Utils.timeFormat(plPlayerController.durationSeconds.value.inSeconds))}',
),
),
],
@@ -338,7 +340,43 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 空白占位
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(
@@ -519,8 +557,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
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: '字幕',
@@ -585,6 +625,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
if (anySeason) BottomControlType.pre,
if (anySeason) BottomControlType.next,
BottomControlType.space,
BottomControlType.dmChart,
BottomControlType.superResolution,
BottomControlType.viewPoints,
if (anySeason) BottomControlType.episode,
@@ -702,7 +743,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
if (plPlayerController.showSeekPreview) {
try {
plPlayerController.previewDx.value = result.inMilliseconds /
plPlayerController.duration.value.inMilliseconds *
plPlayerController
.durationSeconds.value.inMilliseconds *
context.size!.width;
if (plPlayerController.showPreview.value.not) {
plPlayerController.showPreview.value = true;
@@ -856,11 +898,13 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
const SizedBox(width: 2),
Obx(
() => Text(
plPlayerController.duration.value.inMinutes >= 60
plPlayerController
.durationSeconds.value.inMinutes >=
60
? printDurationWithHours(
plPlayerController.duration.value)
plPlayerController.durationSeconds.value)
: printDuration(
plPlayerController.duration.value),
plPlayerController.durationSeconds.value),
style: textStyle,
),
),
@@ -1067,11 +1111,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
/// 进度条 live模式下禁用
Obx(
() {
final int value = plPlayerController.sliderPositionSeconds.value;
final int max = plPlayerController.durationSeconds.value;
final int max = plPlayerController.durationSeconds.value.inSeconds;
final int buffer = plPlayerController.bufferedSeconds.value;
if (plPlayerController.showControls.value) {
return const SizedBox.shrink();
@@ -1108,37 +1151,12 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
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),
@@ -1499,6 +1517,57 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
}
}
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 - 1).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: 1,
color: Theme.of(context).colorScheme.primary,
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) {
@@ -1582,6 +1651,7 @@ Widget buildSeekPreviewWidget(PlPlayerController plPlayerController) {
heightFactor: 0.1,
alignment: alignment,
child: CachedNetworkImage(
fit: BoxFit.fill,
width: 480 * scale,
height: 270 * scale,
imageUrl: parseUrl(plPlayerController.videoShot!['data']
@@ -1600,3 +1670,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');
}
},
),
);
},
);
}

View File

@@ -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';
@@ -38,7 +42,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
Obx(
() {
final int value = controller!.sliderPositionSeconds.value;
final int max = controller!.durationSeconds.value;
final int max = controller!.durationSeconds.value.inSeconds;
final int buffer = controller!.bufferedSeconds.value;
if (value > max || max <= 0) {
return nil;
@@ -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),

View File

@@ -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<double> get dynamicDetailRatio => List<double>.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',

View File

@@ -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:

View File

@@ -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