Compare commits

..

22 Commits

Author SHA1 Message Date
bggRGjQaUbCoE
70aecd1e38 mod: view point
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-04 14:38:06 +08:00
bggRGjQaUbCoE
a40c773491 fix: interceptor
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-04 13:09:52 +08:00
bggRGjQaUbCoE
b4abb58a41 mod: seg bar, dyn decorate
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-04 11:33:23 +08:00
bggRGjQaUbCoE
e368436bc6 feat: reply: sync to dyn
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-03 11:43:42 +08:00
bggRGjQaUbCoE
6c96b3a7f5 fix: check reply url
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-03 10:49:25 +08:00
bggRGjQaUbCoE
149f0c082d fix: reply2reply mode
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-03 10:20:30 +08:00
bggRGjQaUbCoE
994199b5a2 fix: check reply
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-02 23:44:47 +08:00
bggRGjQaUbCoE
8db3d80151 fix: onVideoDetailChange
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-02 22:29:25 +08:00
bggRGjQaUbCoE
93af1e7c44 opt: reply check
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-02 22:29:10 +08:00
dom
54e90bd986 feat: comment antifraud (#193)
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-02 21:24:07 +08:00
bggRGjQaUbCoE
ca16551917 mod: dm chart height
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-02 17:18:21 +08:00
bggRGjQaUbCoE
f4977d2855 mod: def hardwareDecoding
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-01 18:04:16 +08:00
bggRGjQaUbCoE
bd91fb7c6d mod: show volume when hiding sysui for ios
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-02-01 17:59:22 +08:00
bggRGjQaUbCoE
e1805896f4 Revert "opt: dm chart"
This reverts commit 31a639400e.
2025-01-31 20:40:18 +08:00
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
30 changed files with 808 additions and 361 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

@@ -22,7 +22,7 @@ class Segment {
class SegmentProgressBar extends CustomPainter {
final List<Segment> segmentColors;
late double _defHeight;
double? _defHeight;
SegmentProgressBar({
required this.segmentColors,
@@ -42,6 +42,18 @@ class SegmentProgressBar extends CustomPainter {
if (segmentColors[i].title != null) {
double fontSize = 10;
_defHeight ??= (TextPainter(
text: TextSpan(
text: segmentColors[i].title,
style: TextStyle(
fontSize: fontSize,
),
),
textDirection: TextDirection.ltr,
)..layout())
.height +
2;
TextPainter getTextPainter() => TextPainter(
text: TextSpan(
text: segmentColors[i].title,
@@ -51,14 +63,12 @@ class SegmentProgressBar extends CustomPainter {
height: 1,
),
),
strutStyle: StrutStyle(height: 1, leading: 0),
strutStyle:
StrutStyle(leading: 0, height: 1, fontSize: fontSize),
textDirection: TextDirection.ltr,
)..layout();
TextPainter textPainter = getTextPainter();
if (i == 0) {
_defHeight = textPainter.height;
}
late double prevStart;
if (i != 0) {
@@ -75,7 +85,7 @@ class SegmentProgressBar extends CustomPainter {
canvas.drawRect(
Rect.fromLTRB(
0,
-_defHeight - 2,
-_defHeight!,
size.width,
0,
),
@@ -86,9 +96,9 @@ class SegmentProgressBar extends CustomPainter {
canvas.drawRect(
Rect.fromLTWH(
segmentStart,
-_defHeight - 2,
-_defHeight!,
segmentEnd == segmentStart ? 2 : segmentEnd - segmentStart,
size.height + _defHeight + 2,
size.height + _defHeight!,
),
paint,
);
@@ -98,7 +108,7 @@ class SegmentProgressBar extends CustomPainter {
: (segmentStart - prevStart - textPainter.width) / 2 +
prevStart +
1;
double textY = (-_defHeight - textPainter.height) / 2 - 1;
double textY = (-_defHeight! - textPainter.height) / 2;
textPainter.paint(canvas, Offset(textX, textY));
} else {
canvas.drawRect(

View File

@@ -14,7 +14,6 @@ import '../utils/utils.dart';
import 'api.dart';
import 'constants.dart';
import 'interceptor.dart';
import 'interceptor_anonymity.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart' as web;
class Request {
@@ -37,7 +36,7 @@ class Request {
);
cookieManager = CookieManager(cookieJar);
dio.interceptors.add(cookieManager);
dio.interceptors.add(AnonymityInterceptor());
dio.interceptors.add(ApiInterceptor());
final List<Cookie> cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl));
for (Cookie item in cookies) {
@@ -175,15 +174,12 @@ class Request {
);
}
//添加拦截器
dio.interceptors.add(ApiInterceptor());
// 日志拦截器 输出请求、响应内容
dio.interceptors.add(LogInterceptor(
request: false,
requestHeader: false,
responseHeader: false,
));
// dio.interceptors.add(LogInterceptor(
// request: false,
// requestHeader: false,
// responseHeader: false,
// ));
dio.transformer = BackgroundTransformer();
dio.options.validateStatus = (int? status) {

View File

@@ -1,17 +1,67 @@
import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/pages/mine/controller.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
class ApiInterceptor extends Interceptor {
// @override
// void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// debugPrint("请求之前");
// // 在请求之前添加头部或认证信息
// options.headers['Authorization'] = 'Bearer token';
// options.headers['Content-Type'] = 'application/json';
// handler.next(options);
// }
static const List<String> anonymityList = [
Api.videoUrl,
Api.videoIntro,
Api.relatedList,
Api.replyList,
Api.replyReplyList,
Api.searchSuggest,
Api.searchByType,
Api.heartBeat,
Api.ab2c,
Api.bangumiInfo,
Api.liveRoomInfo,
Api.onlineTotal,
Api.webDanmaku,
Api.dynamicDetail,
Api.aiConclusion,
Api.getSeasonDetailApi,
Api.liveRoomDmToken,
Api.liveRoomDmPrefetch,
];
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
void onRemoveCookie() {
options.headers.remove('x-bili-mid');
options.headers.remove('x-bili-aurora-eid');
options.headers.remove('x-bili-aurora-zone');
options.headers['cookie'] = '';
options.queryParameters.remove('access_key');
options.queryParameters.remove('csrf');
options.queryParameters.remove('csrf_token');
if (options.data is Map) {
options.data.remove('access_key');
options.data.remove('csrf');
options.data.remove('csrf_token');
}
}
if (options.extra['clearCookie'] == true) {
onRemoveCookie();
} else if (MineController.anonymity.value) {
String uri = options.uri.toString();
for (var i in anonymityList) {
// 如果请求的url包含无痕列表中的url则清空cookie
// 但需要保证匹配到url的后半部分不再出现/符号,否则会误伤
int index = uri.indexOf(i);
if (index == -1) continue;
if (uri.lastIndexOf('/') >= index + i.length) continue;
//SmartDialog.showToast('触发无痕模式\n\n$i\n\n${options.uri}');
onRemoveCookie();
break;
}
}
handler.next(options);
}
// @override
// void onResponse(Response response, ResponseInterceptorHandler handler) {

View File

@@ -1,51 +0,0 @@
// ignore_for_file: avoid_print
import 'dart:io';
import 'package:dio/dio.dart';
// import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import '../pages/mine/controller.dart';
import 'api.dart';
class AnonymityInterceptor extends Interceptor {
static const List<String> anonymityList = [
Api.videoUrl,
Api.videoIntro,
Api.relatedList,
Api.replyList,
Api.replyReplyList,
Api.searchSuggest,
Api.searchByType,
Api.heartBeat,
Api.ab2c,
Api.bangumiInfo,
Api.liveRoomInfo,
Api.onlineTotal,
Api.webDanmaku,
Api.dynamicDetail,
Api.aiConclusion,
Api.getSeasonDetailApi,
Api.liveRoomDmToken,
Api.liveRoomDmPrefetch,
];
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (MineController.anonymity.value) {
String uri = options.uri.toString();
for (var i in anonymityList) {
// 如果请求的url包含无痕列表中的url则清空cookie
// 但需要保证匹配到url的后半部分不再出现/符号,否则会误伤
int index = uri.indexOf(i);
if (index == -1) continue;
if (uri.lastIndexOf('/') >= index + i.length) continue;
//SmartDialog.showToast('触发无痕模式\n\n$i\n\n${options.uri}');
options.headers[HttpHeaders.cookieHeader] = "";
if (options.data != null && options.data.csrf != null) {
options.data.csrf = "";
}
break;
}
}
handler.next(options);
}
}

View File

@@ -1,19 +1,17 @@
import 'dart:io';
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/grpc/grpc_repo.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:dio/dio.dart';
import '../models/video/reply/data.dart';
import '../models/video/reply/emote.dart';
import 'api.dart';
import 'constants.dart';
import 'init.dart';
class ReplyHttp {
static Options get _options => Options(extra: {'clearCookie': true});
static Future<LoadingState> replyList({
required bool isLogin,
required int oid,
@@ -23,13 +21,9 @@ class ReplyHttp {
int sort = 1,
required String banWordForReply,
}) async {
Options? options = !isLogin
? Options(
headers: {HttpHeaders.cookieHeader: "buvid3= ; b_nut= ; sid= "})
: null;
var res = !isLogin
? await Request().get(
'${HttpString.apiBaseUrl}${Api.replyList}/main',
'${Api.replyList}/main',
queryParameters: {
'oid': oid,
'type': type,
@@ -37,10 +31,10 @@ class ReplyHttp {
'{"offset":"${nextOffset.replaceAll('"', '\\"')}"}',
'mode': sort + 2, //2:按时间排序3按热度排序
},
options: options,
options: isLogin.not ? _options : null,
)
: await Request().get(
'${HttpString.apiBaseUrl}${Api.replyList}',
Api.replyList,
queryParameters: {
'oid': oid,
'type': type,
@@ -48,7 +42,7 @@ class ReplyHttp {
'pn': page,
'ps': 20,
},
options: options,
options: isLogin.not ? _options : null,
);
if (res.data['code'] == 0) {
ReplyData replyData = ReplyData.fromJson(res.data['data']);
@@ -134,28 +128,25 @@ class ReplyHttp {
}
static Future<LoadingState> replyReplyList({
required bool isLogin,
required int oid,
required int root,
required int pageNum,
required int type,
int sort = 1,
required String banWordForReply,
bool? isCheck,
}) async {
Options? options = GStorage.userInfo.get('userInfoCache') == null
? Options(
headers: {HttpHeaders.cookieHeader: "buvid3= ; b_nut= ; sid= "})
: null;
var res = await Request().get(
'${HttpString.apiBaseUrl}${Api.replyReplyList}',
Api.replyReplyList,
queryParameters: {
'oid': oid,
'root': root,
'pn': pageNum,
'type': type,
'sort': 1,
'csrf': await Request.getCsrf(),
if (isLogin) 'csrf': await Request.getCsrf(),
},
options: options,
options: isLogin.not ? _options : null,
);
if (res.data['code'] == 0) {
ReplyReplyData replyData = ReplyReplyData.fromJson(res.data['data']);
@@ -168,7 +159,11 @@ class ReplyHttp {
}
return LoadingState.success(replyData);
} else {
return LoadingState.error(res.data['message']);
return LoadingState.error(
isCheck == true
? '${res.data['code']}${res.data['message']}'
: res.data['message'],
);
}
}

View File

@@ -759,6 +759,7 @@ class VideoHttp {
int? root,
int? parent,
List? pictures,
bool? syncToDynamic,
}) async {
if (message == '') {
return {'status': false, 'data': [], 'msg': '请输入评论内容'};
@@ -766,10 +767,11 @@ class VideoHttp {
Map<String, dynamic> data = {
'type': type.index,
'oid': oid,
'root': root == null || root == 0 ? '' : root,
'parent': parent == null || parent == 0 ? '' : parent,
if (root != null && root != 0) 'root': root,
if (parent != null && parent != 0) 'parent': parent,
'message': message,
if (pictures != null) 'pictures': jsonEncode(pictures),
if (syncToDynamic == true) 'sync_to_dynamic': 1,
'csrf': await Request.getCsrf(),
};
var res = await Request().post(

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

@@ -1,5 +1,6 @@
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/reply.dart';
import 'package:PiliPlus/models/common/reply_type.dart';
import 'package:PiliPlus/models/video/reply/data.dart';
import 'package:PiliPlus/pages/common/common_controller.dart';
@@ -9,6 +10,7 @@ import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/models/common/reply_sort_type.dart';
import 'package:PiliPlus/models/video/reply/item.dart';
@@ -27,10 +29,11 @@ abstract class ReplyController extends CommonController {
late final bool isLogin = GStorage.userInfo.get('userInfoCache') != null;
CursorReply? cursor;
late Mode mode = Mode.MAIN_LIST_HOT;
late Rx<Mode> mode = Mode.MAIN_LIST_HOT.obs;
late bool hasUpTop = false;
late final banWordForReply = GStorage.banWordForReply;
late final enableCommAntifraud = GStorage.enableCommAntifraud;
@override
void onInit() {
@@ -43,7 +46,7 @@ abstract class ReplyController extends CommonController {
}
sortType.value = ReplySortType.values[defaultReplySortIndex];
if (sortType.value == ReplySortType.time) {
mode = Mode.MAIN_LIST_TIME;
mode.value = Mode.MAIN_LIST_TIME;
}
}
@@ -95,7 +98,7 @@ abstract class ReplyController extends CommonController {
hasUpTop = true;
}
}
if (response.response.topReplies != null) {
if ((response.response.topReplies as List?)?.isNotEmpty == true) {
replies.insertAll(0, response.response.topReplies);
hasUpTop = true;
}
@@ -117,11 +120,11 @@ abstract class ReplyController extends CommonController {
switch (sortType.value) {
case ReplySortType.time:
sortType.value = ReplySortType.like;
mode = Mode.MAIN_LIST_HOT;
mode.value = Mode.MAIN_LIST_HOT;
break;
case ReplySortType.like:
sortType.value = ReplySortType.time;
mode = Mode.MAIN_LIST_TIME;
mode.value = Mode.MAIN_LIST_TIME;
break;
}
nextOffset = '';
@@ -204,21 +207,44 @@ abstract class ReplyController extends CommonController {
}
count.value += 1;
loadingState.value = LoadingState.success(response);
if (enableCommAntifraud && context.mounted) {
checkReply(
context,
oid ?? replyItem.oid.toInt(),
replyItem?.id.toInt(),
replyItem?.type.toInt() ??
replyType?.index ??
ReplyType.video.index,
replyInfo.id.toInt(),
replyInfo.content.message,
);
}
} else {
ReplyData response = loadingState.value is Success
? (loadingState.value as Success).response
: ReplyData();
response.replies ??= <ReplyItemModel>[];
ReplyItemModel replyInfo = ReplyItemModel.fromJson(res, '');
if (oid != null) {
response.replies
?.insert(hasUpTop ? 1 : 0, ReplyItemModel.fromJson(res, ''));
response.replies?.insert(hasUpTop ? 1 : 0, replyInfo);
} else {
response.replies?[index].replies ??= <ReplyItemModel>[];
response.replies?[index].replies
?.add(ReplyItemModel.fromJson(res, ''));
response.replies?[index].replies?.add(replyInfo);
}
count.value += 1;
loadingState.value = LoadingState.success(response);
if (enableCommAntifraud && context.mounted) {
checkReply(
context,
oid ?? replyItem.oid,
replyItem?.rpid,
replyItem?.type.toInt() ??
replyType?.index ??
ReplyType.video.index,
replyInfo.rpid ?? 0,
replyInfo.content?.message ?? '',
);
}
}
}
},
@@ -262,4 +288,186 @@ abstract class ReplyController extends CommonController {
loadingState.value = LoadingState.success(response);
}
}
// ref https://github.com/freedom-introvert/biliSendCommAntifraud
void checkReply(
BuildContext context,
dynamic oid,
dynamic rpid,
int replyType,
int replyId,
String message,
) async {
void showReplyCheckResult(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('评论检查结果'),
content: SelectableText(message),
),
);
}
await Future.delayed(const Duration(seconds: 5));
if (context.mounted.not) return;
// root reply
if (rpid == null) {
// no cookie check
dynamic res = await ReplyHttp.replyList(
isLogin: false,
oid: oid,
nextOffset: '',
type: replyType,
sort: ReplySortType.time.index,
page: 1,
banWordForReply: '',
);
if (context.mounted.not) return;
if (res is Error) {
SmartDialog.showToast('获取评论主列表时发生错误:${res.errMsg}');
return;
} else if (res is Success) {
ReplyData replies = res.response;
int index =
replies.replies?.indexWhere((item) => item.rpid == replyId) ?? -1;
if (index != -1) {
// found
if (context.mounted) {
showReplyCheckResult(
'无账号状态下找到了你的评论,评论正常!\n\n你的评论:$message',
);
}
} else {
// not found
if (context.mounted.not) return;
// cookie check
dynamic res1 = await ReplyHttp.replyReplyList(
isLogin: isLogin,
oid: oid,
root: rpid ?? replyId,
pageNum: 1,
type: replyType,
banWordForReply: '',
);
if (context.mounted.not) return;
if (res1 is Error) {
// not found
if (context.mounted) {
showReplyCheckResult(
'无法找到你的评论。\n\n你的评论:$message',
);
}
} else if (res1 is Success) {
// found
if (context.mounted.not) return;
// no cookie check
dynamic res2 = await ReplyHttp.replyReplyList(
isLogin: false,
oid: oid,
root: rpid ?? replyId,
pageNum: 1,
type: replyType,
banWordForReply: '',
isCheck: true,
);
if (context.mounted.not) return;
if (res2 is Error) {
// not found
if (context.mounted) {
showReplyCheckResult(
res2.errMsg.startsWith('12022')
? '你的评论被shadow ban仅自己可见\n\n你的评论: $message'
: '评论不可见(${res2.errMsg}): $message',
);
}
} else if (res2 is Success) {
// found
if (context.mounted) {
showReplyCheckResult('''
你评论状态有点可疑,虽然无账号翻找评论区获取不到你的评论,但是无账号可通过
https://api.bilibili.com/x/v2/reply/reply?oid=$oid&pn=1&ps=20&root=${rpid ?? replyId}&type=$replyType
获取你的评论,疑似评论区被戒严或者这是你的视频。
你的评论:$message''');
}
}
}
}
}
} else {
for (int i = 1; true; i++) {
if (context.mounted.not) return;
dynamic res3 = await ReplyHttp.replyReplyList(
isLogin: false,
oid: oid,
root: rpid ?? replyId,
pageNum: i,
type: replyType,
banWordForReply: '',
isCheck: true,
);
if (res3 is Error) {
break;
} else if (res3 is Success) {
ReplyReplyData data = res3.response;
if (data.replies.isNullOrEmpty) {
break;
}
int index =
data.replies?.indexWhere((item) => item.rpid == replyId) ?? -1;
if (index == -1) {
// not found
} else {
// found
if (context.mounted) {
showReplyCheckResult(
'无账号状态下找到了你的评论,评论正常!\n\n你的评论:$message',
);
}
return;
}
}
}
for (int i = 1; true; i++) {
if (context.mounted.not) return;
dynamic res4 = await ReplyHttp.replyReplyList(
isLogin: true,
oid: oid,
root: rpid ?? replyId,
pageNum: i,
type: replyType,
banWordForReply: '',
isCheck: true,
);
if (res4 is Error) {
break;
} else if (res4 is Success) {
ReplyReplyData data = res4.response;
if (data.replies.isNullOrEmpty) {
break;
}
int index =
data.replies?.indexWhere((item) => item.rpid == replyId) ?? -1;
if (index == -1) {
// not found
} else {
// found
if (context.mounted) {
showReplyCheckResult(
'你的评论被shadow ban仅自己可见\n\n你的评论: $message',
);
}
return;
}
}
}
if (context.mounted) {
showReplyCheckResult(
'评论不可见: $message',
);
}
}
}
}

View File

@@ -45,7 +45,7 @@ class DynamicDetailController extends ReplyController {
oid: oid!,
cursor: CursorReq(
next: cursor?.next ?? $fixnum.Int64(0),
mode: mode,
mode: mode.value,
),
banWordForReply: banWordForReply,
)

View File

@@ -110,78 +110,84 @@ class AuthorPanel extends StatelessWidget {
)
],
),
// const Spacer(),
// if (source != 'detail' && item.modules?.moduleTag?.text != null)
// Container(
// padding:
// const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
// decoration: BoxDecoration(
// color: Theme.of(context).colorScheme.surface,
// borderRadius: const BorderRadius.all(Radius.circular(4)),
// border: Border.all(
// width: 1.25,
// color: Theme.of(context).colorScheme.primary,
// ),
// ),
// child: Text(
// item.modules.moduleTag.text,
// style: TextStyle(
// height: 1,
// fontSize: 12,
// color: Theme.of(context).colorScheme.primary,
// ),
// strutStyle: const StrutStyle(
// leading: 0,
// height: 1,
// fontSize: 12,
// ),
// ),
// ),
],
),
),
Align(
alignment: Alignment.centerRight,
child: item.modules.moduleAuthor.decorate != null
child: source != 'detail' && item.modules?.moduleTag?.text != null
? Row(
mainAxisSize: MainAxisSize.min,
children: [
// GestureDetector(
// onTap:
// item.modules.moduleAuthor.decorate['jump_url'] != null
// ? () {
// Get.toNamed(
// '/webview',
// parameters: {
// 'url':
// '${item.modules.moduleAuthor.decorate['jump_url']}'
// },
// );
// }
// : null,
// child:
Stack(
clipBehavior: Clip.none,
alignment: Alignment.centerRight,
children: [
CachedNetworkImage(
height: 32,
imageUrl:
item.modules.moduleAuthor.decorate['card_url'],
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4, vertical: 2),
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(4)),
border: Border.all(
width: 1.25,
color: Theme.of(context).colorScheme.primary,
),
if ((item.modules.moduleAuthor.decorate?['fan']
?['num_str'] as String?)
?.isNotEmpty ==
true)
Padding(
padding: const EdgeInsets.only(right: 32),
child: Text(
'${item.modules.moduleAuthor.decorate['fan']['num_str']}',
style: TextStyle(
fontSize: 11,
fontFamily: 'digital_id_num',
color:
(item.modules.moduleAuthor.decorate?['fan']
),
child: Text(
item.modules.moduleTag.text,
style: TextStyle(
height: 1,
fontSize: 12,
color: Theme.of(context).colorScheme.primary,
),
strutStyle: const StrutStyle(
leading: 0,
height: 1,
fontSize: 12,
),
),
),
_moreWidget(context),
],
)
: item.modules.moduleAuthor.decorate != null
? Row(
mainAxisSize: MainAxisSize.min,
children: [
// GestureDetector(
// onTap:
// item.modules.moduleAuthor.decorate['jump_url'] != null
// ? () {
// Get.toNamed(
// '/webview',
// parameters: {
// 'url':
// '${item.modules.moduleAuthor.decorate['jump_url']}'
// },
// );
// }
// : null,
// child:
Stack(
clipBehavior: Clip.none,
alignment: Alignment.centerRight,
children: [
CachedNetworkImage(
height: 32,
imageUrl: item
.modules.moduleAuthor.decorate['card_url'],
),
if ((item.modules.moduleAuthor.decorate?['fan']
?['num_str'] as String?)
?.isNotEmpty ==
true)
Padding(
padding: const EdgeInsets.only(right: 32),
child: Text(
'${item.modules.moduleAuthor.decorate['fan']['num_str']}',
style: TextStyle(
height: 1,
fontSize: 11,
fontFamily: 'digital_id_num',
color: (item.modules.moduleAuthor
.decorate?['fan']
?['color'] as String?)
?.startsWith('#') ==
true
@@ -193,16 +199,16 @@ class AuthorPanel extends StatelessWidget {
),
)
: null,
),
),
),
),
),
],
),
// ),
_moreWidget(context),
],
),
// ),
_moreWidget(context),
],
)
: _moreWidget(context),
)
: _moreWidget(context),
)
],
);
@@ -306,6 +312,25 @@ class AuthorPanel extends StatelessWidget {
},
minLeadingWidth: 0,
),
if (GStorage.isLogin)
ListTile(
title: Text(
'举报',
style: Theme.of(context).textTheme.titleSmall!.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
leading: Icon(
Icons.error_outline_outlined,
size: 19,
color: Theme.of(context).colorScheme.error,
),
onTap: () {
Get.back();
_showReportDynDialog(context);
},
minLeadingWidth: 0,
),
if (item.modules.moduleAuthor.mid ==
GStorage.userInfo.get('userInfoCache')?.mid &&
onRemove != null)
@@ -345,25 +370,6 @@ class AuthorPanel extends StatelessWidget {
.titleSmall!
.copyWith(color: Theme.of(context).colorScheme.error)),
),
if (GStorage.isLogin)
ListTile(
title: Text(
'举报',
style: Theme.of(context).textTheme.titleSmall!.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
leading: Icon(
Icons.error_outline_outlined,
size: 19,
color: Theme.of(context).colorScheme.error,
),
onTap: () {
Get.back();
_showReportDynDialog(context);
},
minLeadingWidth: 0,
),
const Divider(thickness: 0.1, height: 1),
ListTile(
onTap: Get.back,

View File

@@ -55,7 +55,7 @@ class HtmlRenderController extends ReplyController {
oid: oid.value,
cursor: CursorReq(
next: cursor?.next ?? $fixnum.Int64(0),
mode: mode,
mode: mode.value,
),
banWordForReply: banWordForReply,
)

View File

@@ -66,17 +66,7 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
Request()
.get(
'$_blockServer/api/status/uptime',
options: Options(
headers: {
'env': '',
'app-key': '',
'x-bili-mid': '',
'x-bili-aurora-eid': '',
'x-bili-aurora-zone': '',
'cookie':
'buvid3= ; SESSDATA= ; bili_jct= ; DedeUserID= ; DedeUserID__ckMd5= ; sid= ',
},
),
options: Options(extra: {'clearCookie': true}),
)
.then((res) {
setState(() {

View File

@@ -3,7 +3,7 @@ import 'dart:math';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart'
show kDragContainerExtentPercentage, displacement;
import 'package:PiliPlus/http/interceptor_anonymity.dart';
import 'package:PiliPlus/http/interceptor.dart';
import 'package:PiliPlus/models/common/audio_normalization.dart';
import 'package:PiliPlus/models/common/dynamic_badge_mode.dart';
import 'package:PiliPlus/models/common/dynamics_type.dart';
@@ -1359,7 +1359,7 @@ List<SettingsModel> get privacySettings => [
builder: (context) {
return AlertDialog(
title: const Text('查看详情'),
content: Text(AnonymityInterceptor.anonymityList.join('\n')),
content: Text(ApiInterceptor.anonymityList.join('\n')),
actions: [
TextButton(
onPressed: () async {
@@ -1929,6 +1929,32 @@ 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,
title: '发评反诈',
subtitle: '发送评论后检查评论是否可见',
leading: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.shield),
Icon(
Icons.reply,
size: 16,
color: Theme.of(Get.context!).colorScheme.surface,
),
],
),
setKey: SettingBoxKey.enableCommAntifraud,
defaultVal: false,
),
SettingsModel(
settingsType: SettingsType.sw1tch,
enableFeedback: true,

View File

@@ -77,7 +77,7 @@ class VideoDetailController extends GetxController
RxBool isShowCover = true.obs;
// 硬解
RxBool enableHA = true.obs;
RxString hwdec = 'auto-safe'.obs;
RxString hwdec = GStorage.hardwareDecoding.obs;
RxInt oid = 0.obs;
@@ -194,8 +194,6 @@ class VideoDetailController extends GetxController
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: false);
if (autoPlay.value) isShowCover.value = false;
enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true);
hwdec.value = setting.get(SettingBoxKey.hardwareDecoding,
defaultValue: Platform.isAndroid ? 'auto-safe' : 'auto');
if (userInfo == null ||
GStorage.localCache.get(LocalCacheKey.historyPause) == true) {
enableHeart = false;
@@ -621,20 +619,13 @@ class VideoDetailController extends GetxController
);
}
Options get _options => Options(
headers: {
'env': '',
'app-key': '',
'x-bili-mid': '',
'x-bili-aurora-eid': '',
'x-bili-aurora-zone': '',
HttpHeaders.cookieHeader:
'buvid3= ; SESSDATA= ; bili_jct= ; DedeUserID= ; DedeUserID__ckMd5= ; sid= ',
},
);
Options get _options => Options(extra: {'clearCookie': true});
Future _querySponsorBlock() async {
positionSubscription?.cancel();
videoLabel.value = '';
segmentList.clear();
_segmentProgressList = null;
dynamic result = await Request().get(
'${GStorage.blockServer}/api/skipSegments',
queryParameters: {
@@ -643,9 +634,6 @@ class VideoDetailController extends GetxController
},
options: _options,
);
videoLabel.value = '';
segmentList.clear();
_segmentProgressList = null;
_handleSBData(result);
}
@@ -1004,6 +992,7 @@ class VideoDetailController extends GetxController
vttSubtitles: _vttSubtitles,
vttSubtitlesIndex: vttSubtitlesIndex,
showVP: showVP,
dmTrend: dmTrend,
// 硬解
enableHA: enableHA.value,
hwdec: hwdec.value,
@@ -1037,6 +1026,10 @@ class VideoDetailController extends GetxController
_getSubtitle();
}
if (showDmChart && dmTrend == null) {
_getDmTrend();
}
/// 开启自动全屏时在player初始化完成后立即传入headerControl
plPlayerController.headerControl = headerControl;
@@ -1359,7 +1352,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 +1681,9 @@ class VideoDetailController extends GetxController
'userID': GStorage.blockUserID,
'userAgent': Constants.userAgent,
'videoDuration': plPlayerController
.durationSeconds.value,
.durationSeconds
.value
.inSeconds,
},
data: {
'segments': list!
@@ -1960,4 +1956,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

@@ -25,7 +25,7 @@ class VideoReplyController extends ReplyController {
oid: aid!,
cursor: CursorReq(
next: cursor?.next ?? $fixnum.Int64(0),
mode: mode,
mode: mode.value,
),
banWordForReply: banWordForReply,
)

View File

@@ -34,6 +34,8 @@ class ReplyPage extends CommonPublishPage {
}
class _ReplyPageState extends CommonPublishPageState<ReplyPage> {
RxBool _syncToDynamic = false.obs;
@override
Widget build(BuildContext context) {
return MediaQuery.removePadding(
@@ -177,7 +179,7 @@ class _ReplyPageState extends CommonPublishPageState<ReplyPage> {
selected: selectKeyboard.value,
),
),
const SizedBox(width: 20),
const SizedBox(width: 10),
Obx(
() => ToolbarIconButton(
tooltip: '表情',
@@ -192,7 +194,7 @@ class _ReplyPageState extends CommonPublishPageState<ReplyPage> {
),
),
if (widget.root == 0) ...[
const SizedBox(width: 20),
const SizedBox(width: 10),
ToolbarIconButton(
tooltip: '图片',
selected: false,
@@ -201,6 +203,32 @@ class _ReplyPageState extends CommonPublishPageState<ReplyPage> {
),
],
const Spacer(),
Obx(
() => TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 13),
visualDensity: const VisualDensity(
horizontal: -2,
vertical: -2,
),
foregroundColor: _syncToDynamic.value
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.outline,
),
onPressed: () {
_syncToDynamic.value = !_syncToDynamic.value;
},
icon: Icon(
_syncToDynamic.value
? Icons.check_box
: Icons.check_box_outline_blank,
size: 22,
),
label: const Text('转发至动态'),
),
),
const Spacer(),
Obx(
() => FilledButton.tonal(
onPressed: enablePublish.value ? onPublish : null,
@@ -234,6 +262,7 @@ class _ReplyPageState extends CommonPublishPageState<ReplyPage> {
? ' 回复 @${GlobalData().grpcReply ? widget.replyItem.member.name : widget.replyItem.member.uname} : $message'
: message,
pictures: pictures,
syncToDynamic: _syncToDynamic.value,
);
if (result['status']) {
SmartDialog.showToast(result['data']['success_toast']);

View File

@@ -1,7 +1,7 @@
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/video/reply/item.dart';
import 'package:PiliPlus/pages/common/common_controller.dart';
import 'package:PiliPlus/pages/common/reply_controller.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart';
@@ -10,7 +10,7 @@ import 'package:PiliPlus/http/reply.dart';
import 'package:PiliPlus/models/common/reply_type.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class VideoReplyReplyController extends CommonController
class VideoReplyReplyController extends ReplyController
with GetTickerProviderStateMixin {
VideoReplyReplyController({
required this.hasRoot,
@@ -32,9 +32,6 @@ class VideoReplyReplyController extends CommonController
int? rpid;
ReplyType replyType; // = ReplyType.video;
CursorReply? cursor;
Rx<Mode> mode = Mode.MAIN_LIST_TIME.obs;
RxInt count = (-1).obs;
int? upMid;
dynamic firstFloor;
@@ -45,11 +42,10 @@ class VideoReplyReplyController extends CommonController
late final horizontalPreview = GStorage.horizontalPreview;
late final banWordForReply = GStorage.banWordForReply;
@override
void onInit() {
super.onInit();
mode.value = Mode.MAIN_LIST_TIME;
queryData();
}
@@ -203,6 +199,7 @@ class VideoReplyReplyController extends CommonController
banWordForReply: banWordForReply,
)
: ReplyHttp.replyReplyList(
isLogin: isLogin,
oid: oid!,
root: rpid!,
pageNum: currentPage,
@@ -210,6 +207,7 @@ class VideoReplyReplyController extends CommonController
banWordForReply: banWordForReply,
);
@override
queryBySort() {
mode.value = mode.value == Mode.MAIN_LIST_HOT
? Mode.MAIN_LIST_TIME

View File

@@ -27,7 +27,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
this.dialog,
this.firstFloor,
this.source,
this.replyType,
required this.replyType,
this.isDialogue = false,
this.isTop = false,
this.onViewImage,
@@ -39,7 +39,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
final int? dialog;
final dynamic firstFloor;
final String? source;
final ReplyType? replyType;
final ReplyType replyType;
final bool isDialogue;
final bool isTop;
final VoidCallback? onViewImage;
@@ -73,7 +73,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
oid: widget.oid,
rpid: widget.rpid,
dialog: widget.dialog,
replyType: widget.replyType!,
replyType: widget.replyType,
isDialogue: widget.isDialogue,
),
tag: '${widget.rpid}${widget.dialog}${widget.isDialogue}',
@@ -352,15 +352,36 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
_videoReplyReplyController.count.value += 1;
_videoReplyReplyController.loadingState.value =
LoadingState.success(list);
if (_videoReplyReplyController.enableCommAntifraud && mounted) {
_videoReplyReplyController.checkReply(
context,
oid,
root,
widget.replyType.index,
replyInfo.id.toInt(),
replyInfo.content.message,
);
}
} else {
List list = _videoReplyReplyController.loadingState.value is Success
? (_videoReplyReplyController.loadingState.value as Success)
.response
: <ReplyItemModel>[];
list.insert(index + 1, ReplyItemModel.fromJson(res, ''));
ReplyItemModel replyInfo = ReplyItemModel.fromJson(res, '');
list.insert(index + 1, replyInfo);
_videoReplyReplyController.count.value += 1;
_videoReplyReplyController.loadingState.value =
LoadingState.success(list);
if (_videoReplyReplyController.enableCommAntifraud && mounted) {
_videoReplyReplyController.checkReply(
context,
oid,
root,
widget.replyType.index,
replyInfo.rpid ?? 0,
replyInfo.content?.message ?? '',
);
}
}
}
});

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

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:PiliPlus/common/widgets/segment_progress_bar.dart';
@@ -9,6 +10,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';
@@ -174,6 +176,15 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
FlutterVolumeController.addListener((double value) {
if (mounted && !_volumeInterceptEventStream.value) {
_volumeValue.value = value;
if (Platform.isIOS && FlutterVolumeController.showSystemUI.not) {
_volumeIndicator.value = true;
_volumeTimer?.cancel();
_volumeTimer = Timer(const Duration(milliseconds: 800), () {
if (mounted) {
_volumeIndicator.value = false;
}
});
}
}
});
} catch (_) {}
@@ -321,7 +332,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 +341,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 +350,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 +567,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 +635,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 +753,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 +908,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 +1121,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 +1161,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, 4.25),
ProgressBar(
progress: Duration(seconds: value),
buffered: Duration(seconds: buffer),
@@ -1499,6 +1527,57 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
}
}
Widget buildDmChart(
BuildContext context,
PlPlayerController plPlayerController, [
double offset = 0,
]) {
return IgnorePointer(
child: Container(
height: 12,
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 +1661,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 +1680,32 @@ Widget buildSeekPreviewWidget(PlPlayerController plPlayerController) {
});
});
}
Widget buildViewPointWidget(
PlPlayerController plPlayerController, double offset) {
return LayoutBuilder(
builder: (context, constraints) {
return Container(
height: 16,
margin: EdgeInsets.only(bottom: offset),
child: Listener(
behavior: HitTestBehavior.opaque,
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,37 +57,12 @@ 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');
}
},
),
);
},
),
buildViewPointWidget(controller!, 8.75),
ProgressBar(
progress: Duration(seconds: value),
buffered: Duration(seconds: buffer),

View File

@@ -54,7 +54,6 @@ import '../pages/setting/style_setting.dart';
import '../pages/subscription/index.dart';
import '../pages/subscription_detail/index.dart';
import '../pages/video/detail/index.dart';
import '../pages/video/detail/reply_reply/index.dart';
import '../pages/whisper/index.dart';
import '../pages/whisper_detail/index.dart';
@@ -99,8 +98,8 @@ class Routes {
CustomGetPage(name: '/member', page: () => const MemberPageNew()),
CustomGetPage(name: '/memberSearch', page: () => const MemberSearchPage()),
// 二级回复
CustomGetPage(
name: '/replyReply', page: () => const VideoReplyReplyPanel()),
// CustomGetPage(
// name: '/replyReply', page: () => const VideoReplyReplyPanel()),
// 推荐流设置
CustomGetPage(
name: '/recommendSetting', page: () => const RecommendSetting()),

View File

@@ -4,6 +4,7 @@ import 'package:PiliPlus/models/bangumi/info.dart';
import 'package:PiliPlus/models/video_detail_res.dart';
import 'package:PiliPlus/plugin/pl_player/index.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:get/get_utils/get_utils.dart';
Future<VideoPlayerServiceHandler> initAudioService() async {
return await AudioService.init(
@@ -112,7 +113,8 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
MediaItem? mediaItem;
if (data is VideoDetailData) {
if ((data.pages?.length ?? 0) > 1) {
final current = data.pages?.firstWhere((element) => element.cid == cid);
final current =
data.pages?.firstWhereOrNull((element) => element.cid == cid);
mediaItem = MediaItem(
id: UniqueKey().toString(),
title: current?.pagePart ?? "",
@@ -132,7 +134,7 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
}
} else if (data is BangumiInfoModel) {
final current =
data.episodes?.firstWhere((element) => element.cid == cid);
data.episodes?.firstWhereOrNull((element) => element.cid == cid);
mediaItem = MediaItem(
id: UniqueKey().toString(),
title: current?.longTitle ?? "",

View File

@@ -161,10 +161,8 @@ class GStorage {
defaultValue: VideoDecodeFormats.values[1].code,
);
static String get hardwareDecoding => setting.get(
SettingBoxKey.hardwareDecoding,
defaultValue: Platform.isAndroid ? 'auto-safe' : 'auto',
);
static String get hardwareDecoding =>
setting.get(SettingBoxKey.hardwareDecoding, defaultValue: 'auto');
static String get videoSync => setting.get(
SettingBoxKey.videoSync,
@@ -360,6 +358,12 @@ 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 bool get enableCommAntifraud => GStorage.setting
.get(SettingBoxKey.enableCommAntifraud, defaultValue: false);
static List<double> get dynamicDetailRatio => List<double>.from(setting
.get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0]));
@@ -589,6 +593,8 @@ class SettingBoxKey {
showDynDecorate = 'showDynDecorate',
enableLivePhoto = 'enableLivePhoto',
showSeekPreview = 'showSeekPreview',
showDmChart = 'showDmChart',
enableCommAntifraud = 'enableCommAntifraud',
// 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