mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-27 22:10:14 +08:00
refa: sb & feat: sb portVideo (WIP) (#1751)
* refa: sb * feat: sb portVideo (WIP) * fix: keep-alive * revert: ua version * fix * tweak [skip ci] Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> --------- Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
committed by
GitHub
parent
d5d95671ff
commit
2be13e7283
@@ -40,8 +40,6 @@ class Constants {
|
|||||||
'{"appId":1,"platform":3,"version":"8.43.0","abtest":""}';
|
'{"appId":1,"platform":3,"version":"8.43.0","abtest":""}';
|
||||||
|
|
||||||
static const baseHeaders = {
|
static const baseHeaders = {
|
||||||
'connection': 'keep-alive',
|
|
||||||
'accept-encoding': 'br,gzip',
|
|
||||||
// 'referer': HttpString.baseUrl,
|
// 'referer': HttpString.baseUrl,
|
||||||
'env': 'prod',
|
'env': 'prod',
|
||||||
'app-key': 'android64',
|
'app-key': 'android64',
|
||||||
|
|||||||
@@ -1,42 +1,46 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
Future<void> showConfirmDialog({
|
Future<bool> showConfirmDialog({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required String title,
|
required String title,
|
||||||
dynamic content,
|
Object? content,
|
||||||
required VoidCallback onConfirm,
|
@Deprecated('use `bool result = await showConfirmDialog()` instead')
|
||||||
}) {
|
VoidCallback? onConfirm,
|
||||||
|
}) async {
|
||||||
assert(content is String? || content is Widget);
|
assert(content is String? || content is Widget);
|
||||||
return showDialog(
|
return await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
content: content is String
|
content: content is String
|
||||||
? Text(content)
|
? Text(content)
|
||||||
: content is Widget
|
: content is Widget
|
||||||
? content
|
? content
|
||||||
: null,
|
: null,
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: Get.back,
|
onPressed: Get.back,
|
||||||
child: Text(
|
child: Text(
|
||||||
'取消',
|
'取消',
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
style: TextStyle(
|
||||||
),
|
color: Theme.of(context).colorScheme.outline,
|
||||||
),
|
),
|
||||||
TextButton(
|
),
|
||||||
onPressed: () {
|
),
|
||||||
Get.back();
|
TextButton(
|
||||||
onConfirm();
|
onPressed: () {
|
||||||
},
|
Get.back(result: true);
|
||||||
child: const Text('确认'),
|
onConfirm?.call();
|
||||||
),
|
},
|
||||||
],
|
child: const Text('确认'),
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void showPgcFollowDialog({
|
void showPgcFollowDialog({
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ class Request {
|
|||||||
//Http请求头.
|
//Http请求头.
|
||||||
headers: {
|
headers: {
|
||||||
'user-agent': 'Dart/3.6 (dart:io)', // Http2Adapter不会自动添加标头
|
'user-agent': 'Dart/3.6 (dart:io)', // Http2Adapter不会自动添加标头
|
||||||
|
if (!Pref.enableHttp2) 'connection': 'keep-alive',
|
||||||
|
'accept-encoding': 'br,gzip',
|
||||||
},
|
},
|
||||||
responseDecoder: _responseDecoder, // Http2Adapter没有自动解压
|
responseDecoder: _responseDecoder, // Http2Adapter没有自动解压
|
||||||
persistentConnection: true,
|
persistentConnection: true,
|
||||||
|
|||||||
222
lib/http/sponsor_block.dart
Normal file
222
lib/http/sponsor_block.dart
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:PiliPlus/build_config.dart';
|
||||||
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
|
import 'package:PiliPlus/http/init.dart';
|
||||||
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
|
import 'package:PiliPlus/http/sponsor_block_api.dart';
|
||||||
|
import 'package:PiliPlus/models/common/sponsor_block/post_segment_model.dart';
|
||||||
|
import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart';
|
||||||
|
import 'package:PiliPlus/models_new/sponsor_block/segment_item.dart';
|
||||||
|
import 'package:PiliPlus/models_new/sponsor_block/user_info.dart';
|
||||||
|
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||||
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
/// https://github.com/hanydd/BilibiliSponsorBlock/wiki/API
|
||||||
|
abstract final class SponsorBlock {
|
||||||
|
static String get blockServer => Pref.blockServer;
|
||||||
|
static final options = Options(
|
||||||
|
followRedirects: true,
|
||||||
|
validateStatus: (status) => true,
|
||||||
|
);
|
||||||
|
|
||||||
|
static Error getErrMsg(Response res) {
|
||||||
|
String statusMessage = switch (res.statusCode) {
|
||||||
|
200 => '意料之外的响应',
|
||||||
|
400 => '参数错误',
|
||||||
|
403 => '被自动审核机制拒绝',
|
||||||
|
404 => '未找到数据',
|
||||||
|
409 => '重复提交',
|
||||||
|
429 => '提交太快(触发速率控制)',
|
||||||
|
500 => '服务器无法获取信息',
|
||||||
|
-1 => res.data['message'].toString(), // DioException
|
||||||
|
_ => res.statusMessage ?? res.statusCode.toString(),
|
||||||
|
};
|
||||||
|
if (res.statusCode != null && res.statusCode != -1) {
|
||||||
|
final data = res.data;
|
||||||
|
if (res.statusCode == 200 ||
|
||||||
|
(data is String && data.isNotEmpty && data.length < 200)) {
|
||||||
|
statusMessage = '$statusMessage:$data';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Error(statusMessage, code: res.statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _api(String url) => '$blockServer/api/$url';
|
||||||
|
|
||||||
|
static Future<LoadingState<List<SegmentItemModel>>> getSkipSegments({
|
||||||
|
required String bvid,
|
||||||
|
required int cid,
|
||||||
|
}) async {
|
||||||
|
final res = await Request().get(
|
||||||
|
_api(SponsorBlockApi.skipSegments),
|
||||||
|
queryParameters: {
|
||||||
|
'videoID': bvid,
|
||||||
|
'cid': cid,
|
||||||
|
},
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
if (res.data case List list) {
|
||||||
|
return Success(list.map((i) => SegmentItemModel.fromJson(i)).toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getErrMsg(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<LoadingState<Null>> voteOnSponsorTime({
|
||||||
|
required String uuid,
|
||||||
|
int? type,
|
||||||
|
SegmentType? category,
|
||||||
|
}) async {
|
||||||
|
assert((type == null) == (category == null));
|
||||||
|
final res = await Request().post(
|
||||||
|
_api(SponsorBlockApi.voteOnSponsorTime),
|
||||||
|
data: {
|
||||||
|
'UUID': uuid,
|
||||||
|
'type': ?type,
|
||||||
|
'category': ?category?.name,
|
||||||
|
'userID': Pref.blockUserID,
|
||||||
|
},
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
return res.statusCode == 200 ? const Success(null) : getErrMsg(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<LoadingState<Null>> viewedVideoSponsorTime(String uuid) async {
|
||||||
|
final res = await Request().post(
|
||||||
|
_api(SponsorBlockApi.viewedVideoSponsorTime),
|
||||||
|
data: {'UUID': uuid},
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
return res.statusCode == 200 ? const Success(null) : getErrMsg(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<LoadingState<Null>> uptimeStatus() async {
|
||||||
|
final res = await Request().get(
|
||||||
|
_api(SponsorBlockApi.uptimeStatus),
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
if (res.statusCode == 200 &&
|
||||||
|
res.data is String &&
|
||||||
|
Utils.isStringNumeric(res.data)) {
|
||||||
|
return const Success(null);
|
||||||
|
}
|
||||||
|
return getErrMsg(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<LoadingState<UserInfo>> userInfo(
|
||||||
|
List<String> query, {
|
||||||
|
String? userId,
|
||||||
|
}) async {
|
||||||
|
final res = await Request().get(
|
||||||
|
_api(SponsorBlockApi.userInfo),
|
||||||
|
queryParameters: {
|
||||||
|
'userID': userId ?? Pref.blockUserID,
|
||||||
|
'values': jsonEncode(query),
|
||||||
|
},
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
return Success(UserInfo.fromJson(res.data));
|
||||||
|
}
|
||||||
|
return getErrMsg(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<LoadingState<List<SegmentItemModel>>> postSkipSegments({
|
||||||
|
required String bvid,
|
||||||
|
required int cid,
|
||||||
|
required double videoDuration,
|
||||||
|
required List<PostSegmentModel> segments,
|
||||||
|
}) async {
|
||||||
|
final res = await Request().post(
|
||||||
|
_api(SponsorBlockApi.skipSegments),
|
||||||
|
data: {
|
||||||
|
'videoID': bvid,
|
||||||
|
'cid': cid.toString(),
|
||||||
|
'userID': Pref.blockUserID,
|
||||||
|
'userAgent': '${Constants.appName}/${BuildConfig.versionName}',
|
||||||
|
'videoDuration': videoDuration,
|
||||||
|
'segments': segments
|
||||||
|
.map(
|
||||||
|
(item) => {
|
||||||
|
'segment': [item.segment.first, item.segment.second],
|
||||||
|
'category': item.category.name,
|
||||||
|
'actionType': item.actionType.name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
},
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
if (res.data case List list) {
|
||||||
|
return Success(list.map((i) => SegmentItemModel.fromJson(i)).toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getErrMsg(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {
|
||||||
|
/// "bvID": string, // B站视频BVID
|
||||||
|
/// "cid": string, // 视频CID
|
||||||
|
/// "ytbID": string, // YouTube视频ID
|
||||||
|
/// "UUID": string, // 绑定记录的UUID(不是视频中片段的UUID,是绑定记录本身的UUID)
|
||||||
|
/// "votes": int, // 绑定记录的投票数
|
||||||
|
/// "locked": int, // 绑定记录是否锁定
|
||||||
|
/// }
|
||||||
|
/// TODO: show port video info dialog
|
||||||
|
static Future<LoadingState<String>> getPortVideo({
|
||||||
|
required String bvid,
|
||||||
|
required int cid,
|
||||||
|
}) async {
|
||||||
|
final res = await Request().get(
|
||||||
|
_api(SponsorBlockApi.portVideo),
|
||||||
|
queryParameters: {
|
||||||
|
'videoID': bvid,
|
||||||
|
'cid': cid.toString(),
|
||||||
|
},
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
if (res.data case Map<String, dynamic> data) {
|
||||||
|
if (data['ytbID'] case String ytbId) {
|
||||||
|
return Success(ytbId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getErrMsg(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<LoadingState<String>> postPortVideo({
|
||||||
|
required String bvid,
|
||||||
|
required int cid,
|
||||||
|
required String ytbId,
|
||||||
|
required int videoDuration,
|
||||||
|
}) async {
|
||||||
|
final res = await Request().post(
|
||||||
|
_api(SponsorBlockApi.portVideo),
|
||||||
|
data: {
|
||||||
|
'bvID': bvid,
|
||||||
|
'cid': cid.toString(),
|
||||||
|
'ytbID': ytbId,
|
||||||
|
'userID': Pref.blockUserID,
|
||||||
|
'biliDuration': videoDuration,
|
||||||
|
},
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.statusCode == 200) {
|
||||||
|
if (res.data case Map<String, dynamic> data) {
|
||||||
|
if (data['UUID'] case String uuid) {
|
||||||
|
return Success(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getErrMsg(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib/http/sponsor_block_api.dart
Normal file
9
lib/http/sponsor_block_api.dart
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
abstract final class SponsorBlockApi {
|
||||||
|
static const String skipSegments = 'skipSegments';
|
||||||
|
static const String voteOnSponsorTime = 'voteOnSponsorTime';
|
||||||
|
static const String viewedVideoSponsorTime = 'viewedVideoSponsorTime';
|
||||||
|
static const String portVideo = 'portVideo';
|
||||||
|
|
||||||
|
static const String userInfo = 'userInfo';
|
||||||
|
static const String uptimeStatus = 'status/uptime';
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ class SegmentItemModel {
|
|||||||
List<int> segment;
|
List<int> segment;
|
||||||
String uuid;
|
String uuid;
|
||||||
num? videoDuration;
|
num? videoDuration;
|
||||||
|
int votes;
|
||||||
|
|
||||||
SegmentItemModel({
|
SegmentItemModel({
|
||||||
this.cid,
|
this.cid,
|
||||||
@@ -15,6 +16,7 @@ class SegmentItemModel {
|
|||||||
required this.segment,
|
required this.segment,
|
||||||
required this.uuid,
|
required this.uuid,
|
||||||
this.videoDuration,
|
this.videoDuration,
|
||||||
|
this.votes = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory SegmentItemModel.fromJson(Map<String, dynamic> json) =>
|
factory SegmentItemModel.fromJson(Map<String, dynamic> json) =>
|
||||||
@@ -29,6 +31,7 @@ class SegmentItemModel {
|
|||||||
videoDuration: json["videoDuration"] == null
|
videoDuration: json["videoDuration"] == null
|
||||||
? null
|
? null
|
||||||
: (json["videoDuration"] as num) * 1000,
|
: (json["videoDuration"] as num) * 1000,
|
||||||
|
votes: json["votes"],
|
||||||
);
|
);
|
||||||
|
|
||||||
factory SegmentItemModel.fromPgcJson(
|
factory SegmentItemModel.fromPgcJson(
|
||||||
|
|||||||
33
lib/models_new/sponsor_block/user_info.dart
Normal file
33
lib/models_new/sponsor_block/user_info.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:PiliPlus/utils/duration_utils.dart';
|
||||||
|
import 'package:PiliPlus/utils/num_utils.dart';
|
||||||
|
|
||||||
|
class UserInfo {
|
||||||
|
final int viewCount;
|
||||||
|
final double minutesSaved;
|
||||||
|
final int segmentCount;
|
||||||
|
|
||||||
|
const UserInfo({
|
||||||
|
required this.viewCount,
|
||||||
|
required this.minutesSaved,
|
||||||
|
required this.segmentCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory UserInfo.fromJson(Map<String, dynamic> json) => UserInfo(
|
||||||
|
viewCount: json['viewCount'],
|
||||||
|
minutesSaved: (json['minutesSaved'] as num).toDouble(),
|
||||||
|
segmentCount: json['segmentCount'],
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
String minutes = DurationUtils.formatTimeDuration(
|
||||||
|
Duration(minutes: minutesSaved.round()),
|
||||||
|
);
|
||||||
|
if (minutes.isEmpty) {
|
||||||
|
minutes = '0分钟';
|
||||||
|
}
|
||||||
|
return ('您提交了 ${NumUtils.formatPositiveDecimal(segmentCount)} 片段\n'
|
||||||
|
'您为大家节省了 ${NumUtils.formatPositiveDecimal(viewCount)} 片段\n'
|
||||||
|
'($minutes 的生命)');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,16 +4,15 @@ import 'package:PiliPlus/common/widgets/pair.dart';
|
|||||||
import 'package:PiliPlus/http/constants.dart';
|
import 'package:PiliPlus/http/constants.dart';
|
||||||
import 'package:PiliPlus/http/init.dart';
|
import 'package:PiliPlus/http/init.dart';
|
||||||
import 'package:PiliPlus/http/loading_state.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
|
import 'package:PiliPlus/http/sponsor_block.dart';
|
||||||
import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart';
|
import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart';
|
||||||
import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart';
|
import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart';
|
||||||
|
import 'package:PiliPlus/models_new/sponsor_block/user_info.dart';
|
||||||
import 'package:PiliPlus/pages/setting/slide_color_picker.dart';
|
import 'package:PiliPlus/pages/setting/slide_color_picker.dart';
|
||||||
import 'package:PiliPlus/utils/duration_utils.dart';
|
|
||||||
import 'package:PiliPlus/utils/num_utils.dart';
|
|
||||||
import 'package:PiliPlus/utils/page_utils.dart';
|
import 'package:PiliPlus/utils/page_utils.dart';
|
||||||
import 'package:PiliPlus/utils/storage.dart';
|
import 'package:PiliPlus/utils/storage.dart';
|
||||||
import 'package:PiliPlus/utils/storage_key.dart';
|
import 'package:PiliPlus/utils/storage_key.dart';
|
||||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||||
import 'package:PiliPlus/utils/utils.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart' show FilteringTextInputFormatter;
|
import 'package:flutter/services.dart' show FilteringTextInputFormatter;
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -39,7 +38,7 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
|||||||
String _blockServer = Pref.blockServer;
|
String _blockServer = Pref.blockServer;
|
||||||
bool _blockTrack = Pref.blockTrack;
|
bool _blockTrack = Pref.blockTrack;
|
||||||
final _serverStatus = Rxn<bool>();
|
final _serverStatus = Rxn<bool>();
|
||||||
final _userInfo = LoadingState<_UserInfo>.loading().obs;
|
final _userInfo = LoadingState<UserInfo>.loading().obs;
|
||||||
|
|
||||||
Box setting = GStorage.setting;
|
Box setting = GStorage.setting;
|
||||||
|
|
||||||
@@ -56,29 +55,16 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _checkServerStatus() {
|
Future<void> _checkServerStatus() async {
|
||||||
Request().get('$_blockServer/api/status/uptime').then((res) {
|
_serverStatus.value = (await SponsorBlock.uptimeStatus()).isSuccess;
|
||||||
_serverStatus.value =
|
|
||||||
res.statusCode == 200 &&
|
|
||||||
res.data is String &&
|
|
||||||
Utils.isStringNumeric(res.data);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getUserInfo() async {
|
Future<void> _getUserInfo() async {
|
||||||
final params = {
|
_userInfo.value = await SponsorBlock.userInfo(const [
|
||||||
'userID': _userId,
|
'viewCount',
|
||||||
'values': '["viewCount","minutesSaved","segmentCount"]',
|
'minutesSaved',
|
||||||
};
|
'segmentCount',
|
||||||
final res = await Request().get(
|
], userId: _userId);
|
||||||
'$_blockServer/api/userInfo',
|
|
||||||
queryParameters: params,
|
|
||||||
);
|
|
||||||
if (res.statusCode == 200) {
|
|
||||||
_userInfo.value = Success(_UserInfo.fromJson(res.data));
|
|
||||||
} else {
|
|
||||||
_userInfo.value = Error(res.data['message']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _blockLimitItem(
|
Widget _blockLimitItem(
|
||||||
@@ -309,7 +295,7 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
|||||||
),
|
),
|
||||||
subtitle: switch (_userInfo.value) {
|
subtitle: switch (_userInfo.value) {
|
||||||
Loading() => const SizedBox.shrink(),
|
Loading() => const SizedBox.shrink(),
|
||||||
Success<_UserInfo>(:final response) => Text(
|
Success<UserInfo>(:final response) => Text(
|
||||||
response.toString(),
|
response.toString(),
|
||||||
style: subTitleStyle,
|
style: subTitleStyle,
|
||||||
),
|
),
|
||||||
@@ -657,34 +643,3 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UserInfo {
|
|
||||||
final int viewCount;
|
|
||||||
final double minutesSaved;
|
|
||||||
final int segmentCount;
|
|
||||||
|
|
||||||
const _UserInfo({
|
|
||||||
required this.viewCount,
|
|
||||||
required this.minutesSaved,
|
|
||||||
required this.segmentCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory _UserInfo.fromJson(Map<String, dynamic> json) => _UserInfo(
|
|
||||||
viewCount: json['viewCount'],
|
|
||||||
minutesSaved: (json['minutesSaved'] as num).toDouble(),
|
|
||||||
segmentCount: json['segmentCount'],
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
String minutes = DurationUtils.formatTimeDuration(
|
|
||||||
Duration(minutes: minutesSaved.round()),
|
|
||||||
);
|
|
||||||
if (minutes.isEmpty) {
|
|
||||||
minutes = '0分钟';
|
|
||||||
}
|
|
||||||
return ('您提交了 ${NumUtils.formatPositiveDecimal(segmentCount)} 片段\n'
|
|
||||||
'您为大家节省了 ${NumUtils.formatPositiveDecimal(viewCount)} 片段\n'
|
|
||||||
'($minutes 的生命)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:PiliPlus/http/constants.dart';
|
|||||||
import 'package:PiliPlus/http/fav.dart';
|
import 'package:PiliPlus/http/fav.dart';
|
||||||
import 'package:PiliPlus/http/init.dart';
|
import 'package:PiliPlus/http/init.dart';
|
||||||
import 'package:PiliPlus/http/loading_state.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
|
import 'package:PiliPlus/http/sponsor_block.dart';
|
||||||
import 'package:PiliPlus/http/ua_type.dart';
|
import 'package:PiliPlus/http/ua_type.dart';
|
||||||
import 'package:PiliPlus/http/user.dart';
|
import 'package:PiliPlus/http/user.dart';
|
||||||
import 'package:PiliPlus/http/video.dart';
|
import 'package:PiliPlus/http/video.dart';
|
||||||
@@ -63,7 +64,6 @@ import 'package:PiliPlus/utils/storage.dart';
|
|||||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||||
import 'package:PiliPlus/utils/utils.dart';
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
import 'package:PiliPlus/utils/video_utils.dart';
|
import 'package:PiliPlus/utils/video_utils.dart';
|
||||||
import 'package:dio/dio.dart' show Options;
|
|
||||||
import 'package:easy_debounce/easy_throttle.dart';
|
import 'package:easy_debounce/easy_throttle.dart';
|
||||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||||
@@ -492,25 +492,15 @@ class VideoDetailController extends GetxController
|
|||||||
plPlayerController.blockColor[segment.index];
|
plPlayerController.blockColor[segment.index];
|
||||||
late RxString videoLabel = ''.obs;
|
late RxString videoLabel = ''.obs;
|
||||||
|
|
||||||
String get blockServer => plPlayerController.blockServer;
|
|
||||||
|
|
||||||
Timer? skipTimer;
|
Timer? skipTimer;
|
||||||
late final listKey = GlobalKey<AnimatedListState>();
|
late final listKey = GlobalKey<AnimatedListState>();
|
||||||
late final List listData = [];
|
late final List listData = [];
|
||||||
|
|
||||||
void _vote(String uuid, int type) {
|
void _vote(String uuid, int type) {
|
||||||
Request()
|
SponsorBlock.voteOnSponsorTime(
|
||||||
.post(
|
uuid: uuid,
|
||||||
'$blockServer/api/voteOnSponsorTime',
|
type: type,
|
||||||
queryParameters: {
|
).then((i) => SmartDialog.showToast(i.isSuccess ? '投票成功' : '投票失败: $i'));
|
||||||
'UUID': uuid,
|
|
||||||
'userID': Pref.blockUserID,
|
|
||||||
'type': type,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then((res) {
|
|
||||||
SmartDialog.showToast(res.statusCode == 200 ? '投票成功' : '投票失败');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showCategoryDialog(BuildContext context, SegmentModel segment) {
|
void _showCategoryDialog(BuildContext context, SegmentModel segment) {
|
||||||
@@ -528,20 +518,14 @@ class VideoDetailController extends GetxController
|
|||||||
dense: true,
|
dense: true,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Get.back();
|
Get.back();
|
||||||
Request()
|
SponsorBlock.voteOnSponsorTime(
|
||||||
.post(
|
uuid: segment.UUID,
|
||||||
'$blockServer/api/voteOnSponsorTime',
|
category: item,
|
||||||
queryParameters: {
|
).then((i) {
|
||||||
'UUID': segment.UUID,
|
SmartDialog.showToast(
|
||||||
'userID': Pref.blockUserID,
|
'类别更改${i.isSuccess ? '成功' : '失败: $i'}',
|
||||||
'category': item.name,
|
);
|
||||||
},
|
});
|
||||||
)
|
|
||||||
.then((res) {
|
|
||||||
SmartDialog.showToast(
|
|
||||||
'类别更改${res.statusCode == 200 ? '成功' : '失败'}',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
title: Text.rich(
|
title: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
@@ -736,18 +720,19 @@ class VideoDetailController extends GetxController
|
|||||||
videoLabel.value = '';
|
videoLabel.value = '';
|
||||||
segmentList.clear();
|
segmentList.clear();
|
||||||
segmentProgressList.clear();
|
segmentProgressList.clear();
|
||||||
final result = await Request().get(
|
|
||||||
'$blockServer/api/skipSegments',
|
final result = await SponsorBlock.getSkipSegments(
|
||||||
queryParameters: {
|
bvid: bvid,
|
||||||
'videoID': bvid,
|
cid: cid.value,
|
||||||
'cid': cid.value,
|
|
||||||
},
|
|
||||||
options: Options(validateStatus: (status) => true),
|
|
||||||
);
|
);
|
||||||
if (result.statusCode == 200) {
|
switch (result) {
|
||||||
if (result.data case List list) {
|
case Success<List<SegmentItemModel>>(:final response):
|
||||||
handleSBData(list.map((e) => SegmentItemModel.fromJson(e)).toList());
|
handleSBData(response);
|
||||||
}
|
case Error(:final code) when code != 404:
|
||||||
|
if (kDebugMode) {
|
||||||
|
result.toast();
|
||||||
|
}
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1013,10 +998,7 @@ class VideoDetailController extends GetxController
|
|||||||
_showBlockToast('已跳过${item.segmentType.shortTitle}片段');
|
_showBlockToast('已跳过${item.segmentType.shortTitle}片段');
|
||||||
}
|
}
|
||||||
if (_isBlock && Pref.blockTrack) {
|
if (_isBlock && Pref.blockTrack) {
|
||||||
Request().post(
|
SponsorBlock.viewedVideoSponsorTime(item.UUID);
|
||||||
'$blockServer/api/viewedVideoSponsorTime',
|
|
||||||
queryParameters: {'UUID': item.UUID},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_showBlockToast('已跳至${item.segmentType.shortTitle}');
|
_showBlockToast('已跳至${item.segmentType.shortTitle}');
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'package:PiliPlus/common/constants.dart';
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
|
||||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||||
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
||||||
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||||
import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
||||||
|
import 'package:PiliPlus/http/sponsor_block.dart';
|
||||||
import 'package:PiliPlus/models/common/image_type.dart';
|
import 'package:PiliPlus/models/common/image_type.dart';
|
||||||
import 'package:PiliPlus/models/common/stat_type.dart';
|
import 'package:PiliPlus/models/common/stat_type.dart';
|
||||||
import 'package:PiliPlus/models_new/video/video_detail/data.dart';
|
import 'package:PiliPlus/models_new/video/video_detail/data.dart';
|
||||||
@@ -603,6 +605,11 @@ class _UgcIntroPanelState extends State<UgcIntroPanel> {
|
|||||||
caseSensitive: false,
|
caseSensitive: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static final youtubeRegExp = RegExp(
|
||||||
|
r'(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-z0-9_\-]{11})',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
|
||||||
TextSpan buildContent(ThemeData theme, VideoDetailData content) {
|
TextSpan buildContent(ThemeData theme, VideoDetailData content) {
|
||||||
if (content.descV2.isNullOrEmpty) {
|
if (content.descV2.isNullOrEmpty) {
|
||||||
return const TextSpan();
|
return const TextSpan();
|
||||||
@@ -625,12 +632,57 @@ class _UgcIntroPanelState extends State<UgcIntroPanel> {
|
|||||||
text: matchStr,
|
text: matchStr,
|
||||||
style: TextStyle(color: theme.colorScheme.primary),
|
style: TextStyle(color: theme.colorScheme.primary),
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () {
|
..onTap = () async {
|
||||||
try {
|
if (videoDetailCtr
|
||||||
PageUtils.handleWebview(matchStr);
|
.plPlayerController
|
||||||
} catch (err) {
|
.enableSponsorBlock) {
|
||||||
SmartDialog.showToast(err.toString());
|
final duration =
|
||||||
|
videoDetailCtr.data.timeLength ??
|
||||||
|
videoDetailCtr
|
||||||
|
.plPlayerController
|
||||||
|
.duration
|
||||||
|
.value
|
||||||
|
.inMilliseconds;
|
||||||
|
if (duration > 0) {
|
||||||
|
final ytbId = youtubeRegExp
|
||||||
|
.firstMatch(matchStr)
|
||||||
|
?.group(1);
|
||||||
|
if (ytbId != null) {
|
||||||
|
final bvid = videoDetailCtr.bvid;
|
||||||
|
final cid = videoDetailCtr.cid.value;
|
||||||
|
|
||||||
|
SmartDialog.showLoading();
|
||||||
|
final hasPortVideo =
|
||||||
|
(await SponsorBlock.getPortVideo(
|
||||||
|
bvid: bvid,
|
||||||
|
cid: cid,
|
||||||
|
)).dataOrNull ==
|
||||||
|
ytbId;
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
final confirmed = await showConfirmDialog(
|
||||||
|
context: context,
|
||||||
|
title: '空降助手:搬运视频同步',
|
||||||
|
content:
|
||||||
|
'${hasPortVideo ? "" : "是否将"}该视频${hasPortVideo ? "已" : ""}绑定到此YouTube视频($ytbId)',
|
||||||
|
);
|
||||||
|
if (!hasPortVideo && confirmed) {
|
||||||
|
final res = await SponsorBlock.postPortVideo(
|
||||||
|
bvid: bvid,
|
||||||
|
cid: cid,
|
||||||
|
ytbId: ytbId,
|
||||||
|
videoDuration: (duration / 1000).round(),
|
||||||
|
);
|
||||||
|
SmartDialog.showToast(
|
||||||
|
'提交搬运视频${res.isSuccess ? "成功" : "失败: $res"}',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
PageUtils.handleWebview(matchStr);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:PiliPlus/common/constants.dart';
|
|
||||||
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
|
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
||||||
import 'package:PiliPlus/common/widgets/pair.dart';
|
import 'package:PiliPlus/common/widgets/pair.dart';
|
||||||
import 'package:PiliPlus/http/init.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
|
import 'package:PiliPlus/http/sponsor_block.dart';
|
||||||
import 'package:PiliPlus/models/common/sponsor_block/action_type.dart';
|
import 'package:PiliPlus/models/common/sponsor_block/action_type.dart';
|
||||||
import 'package:PiliPlus/models/common/sponsor_block/post_segment_model.dart';
|
import 'package:PiliPlus/models/common/sponsor_block/post_segment_model.dart';
|
||||||
import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart';
|
import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart';
|
||||||
import 'package:PiliPlus/models_new/sponsor_block/segment_item.dart';
|
|
||||||
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
|
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
|
||||||
import 'package:PiliPlus/pages/video/controller.dart';
|
import 'package:PiliPlus/pages/video/controller.dart';
|
||||||
import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart';
|
import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart';
|
||||||
import 'package:PiliPlus/plugin/pl_player/controller.dart';
|
import 'package:PiliPlus/plugin/pl_player/controller.dart';
|
||||||
import 'package:PiliPlus/utils/duration_utils.dart';
|
import 'package:PiliPlus/utils/duration_utils.dart';
|
||||||
import 'package:PiliPlus/utils/extension.dart';
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -310,69 +307,26 @@ class _PostPanelState extends State<PostPanel>
|
|||||||
|
|
||||||
Future<void> _onPost() async {
|
Future<void> _onPost() async {
|
||||||
Get.back();
|
Get.back();
|
||||||
final res = await Request().post(
|
final res = await SponsorBlock.postSkipSegments(
|
||||||
'${widget.videoDetailController.blockServer}/api/skipSegments',
|
bvid: videoDetailController.bvid,
|
||||||
data: {
|
cid: videoDetailController.cid.value,
|
||||||
'videoID': videoDetailController.bvid,
|
videoDuration: videoDuration,
|
||||||
'cid': videoDetailController.cid.value.toString(),
|
segments: list,
|
||||||
'userID': Pref.blockUserID.toString(),
|
|
||||||
'userAgent': Constants.userAgent,
|
|
||||||
'videoDuration': videoDuration,
|
|
||||||
'segments': list
|
|
||||||
.map(
|
|
||||||
(item) => {
|
|
||||||
'segment': [
|
|
||||||
item.segment.first,
|
|
||||||
item.segment.second,
|
|
||||||
],
|
|
||||||
'category': item.category.name,
|
|
||||||
'actionType': item.actionType.name,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
},
|
|
||||||
options: Options(
|
|
||||||
followRedirects: true, // Defaults to true.
|
|
||||||
validateStatus: (int? status) {
|
|
||||||
return (status! >= 200 && status < 300) ||
|
|
||||||
const [400, 403, 429, 409] // reduce extra toast
|
|
||||||
.contains(status);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res.statusCode == 200) {
|
if (res case Success(:final response)) {
|
||||||
Get.back();
|
Get.back();
|
||||||
SmartDialog.showToast('提交成功');
|
SmartDialog.showToast('提交成功');
|
||||||
list.clear();
|
list.clear();
|
||||||
if (res.data case List list) {
|
videoDetailController.handleSBData(response);
|
||||||
videoDetailController.handleSBData(
|
|
||||||
list.map((e) => SegmentItemModel.fromJson(e)).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (videoDetailController.positionSubscription == null) {
|
if (videoDetailController.positionSubscription == null) {
|
||||||
videoDetailController.initSkip();
|
videoDetailController.initSkip();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SmartDialog.showToast('提交失败: ${_errMsg(res)}');
|
SmartDialog.showToast('提交失败: $res');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _errMsg(Response res) {
|
|
||||||
if (res.data case String e) {
|
|
||||||
if (e.isNotEmpty) {
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return switch (res.statusCode) {
|
|
||||||
400 => '参数错误',
|
|
||||||
403 => '被自动审核机制拒绝',
|
|
||||||
429 => '重复提交太快',
|
|
||||||
409 => '重复提交',
|
|
||||||
_ => '${res.data}(${res.statusCode})',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildItem(ThemeData theme, int index, PostSegmentModel item) {
|
Widget _buildItem(ThemeData theme, int index, PostSegmentModel item) {
|
||||||
return Stack(
|
return Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
|
|||||||
@@ -407,7 +407,6 @@ class PlPlayerController {
|
|||||||
.where((item) => item.second != SkipType.disable)
|
.where((item) => item.second != SkipType.disable)
|
||||||
.map((item) => item.first.name)
|
.map((item) => item.first.name)
|
||||||
.toSet();
|
.toSet();
|
||||||
late final blockServer = Pref.blockServer;
|
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
late final showFSActionItem = Pref.showFSActionItem;
|
late final showFSActionItem = Pref.showFSActionItem;
|
||||||
|
|||||||
Reference in New Issue
Block a user