opt: sponsor block

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2024-11-11 12:36:36 +08:00
parent 3d67c5939c
commit 44f5484aed
7 changed files with 629 additions and 209 deletions

View File

@@ -8,6 +8,7 @@ class HttpString {
static const String messageBaseUrl = 'https://message.bilibili.com'; static const String messageBaseUrl = 'https://message.bilibili.com';
static const String dynamicShareBaseUrl = 'https://t.bilibili.com'; static const String dynamicShareBaseUrl = 'https://t.bilibili.com';
static const String spaceBaseUrl = 'https://space.bilibili.com'; static const String spaceBaseUrl = 'https://space.bilibili.com';
static const String sponsorBlockBaseUrl = 'https://www.bsbsb.top';
static const List<int> validateStatusCodes = [ static const List<int> validateStatusCodes = [
302, 302,
304, 304,

View File

@@ -224,7 +224,8 @@ class Request {
/* /*
* post请求 * post请求
*/ */
post(url, {data, queryParameters, options, cancelToken, extra}) async { Future<Response> post(url,
{data, queryParameters, options, cancelToken, extra}) async {
// debugPrint('post-data: $data'); // debugPrint('post-data: $data');
Response response; Response response;
try { try {

View File

@@ -8,6 +8,7 @@ import 'package:PiliPalaX/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:uuid/uuid.dart';
class SponsorBlockPage extends StatefulWidget { class SponsorBlockPage extends StatefulWidget {
const SponsorBlockPage({super.key}); const SponsorBlockPage({super.key});
@@ -17,10 +18,11 @@ class SponsorBlockPage extends StatefulWidget {
} }
class _SponsorBlockPageState extends State<SponsorBlockPage> { class _SponsorBlockPageState extends State<SponsorBlockPage> {
final _url = 'https://github.com/hanydd/BilibiliSponsorBlock';
late double _blockLimit; late double _blockLimit;
late List<Pair<SegmentType, SkipType>> _blockSettings; late List<Pair<SegmentType, SkipType>> _blockSettings;
late List<Color> _blockColor; late List<Color> _blockColor;
final _url = 'https://github.com/hanydd/BilibiliSponsorBlock'; late String _userId;
@override @override
void initState() { void initState() {
@@ -28,8 +30,139 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
_blockLimit = GStorage.blockLimit; _blockLimit = GStorage.blockLimit;
_blockSettings = GStorage.blockSettings; _blockSettings = GStorage.blockSettings;
_blockColor = GStorage.blockColor; _blockColor = GStorage.blockColor;
_userId = GStorage.blockUserID;
} }
TextStyle get _titleStyle => TextStyle(fontSize: 15);
TextStyle get _subTitleStyle =>
TextStyle(color: Theme.of(context).colorScheme.outline);
Widget get _blockLimitItem => ListTile(
onTap: () {
final textController =
TextEditingController(text: _blockLimit.toString());
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('最短片段时长'),
content: TextFormField(
keyboardType: TextInputType.numberWithOptions(decimal: true),
controller: textController,
autofocus: true,
decoration: InputDecoration(suffixText: 's'),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () async {
Get.back();
_blockLimit =
max(0.0, double.tryParse(textController.text) ?? 0.0);
await GStorage.setting
.put(SettingBoxKey.blockLimit, _blockLimit);
setState(() {});
},
child: Text('确定'),
)
],
);
},
);
},
title: Text('最短片段时长', style: _titleStyle),
subtitle: Text(
'忽略短于此时长的片段',
style: _subTitleStyle,
),
trailing: Text(
'${_blockLimit}s',
style: TextStyle(fontSize: 13),
),
);
Widget get _aboudItem => ListTile(
title: Text('关于 SponsorBlock', style: _titleStyle),
subtitle: Text(_url, style: _subTitleStyle),
onTap: () => Utils.launchURL(_url),
);
Widget get _userIdItem => ListTile(
title: Text('用户ID', style: _titleStyle),
subtitle: Text(_userId, style: _subTitleStyle),
onTap: () {
final key = GlobalKey<FormState>();
final textController = TextEditingController(text: _userId);
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('用户ID'),
content: Form(
key: key,
child: TextFormField(
minLines: 1,
maxLines: 4,
autofocus: true,
controller: textController,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\d]+')),
],
validator: (value) {
if ((value?.length ?? -1) < 30) {
return '用户ID要求至少为30个字符长度的纯字符串';
}
return null;
},
),
),
actions: [
TextButton(
onPressed: () async {
Get.back();
_userId = Uuid().v4().replaceAll('-', '');
await GStorage.setting
.put(SettingBoxKey.blockUserID, _userId);
setState(() {});
},
child: Text('随机'),
),
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () async {
if (key.currentState?.validate() == true) {
Get.back();
_userId = textController.text;
await GStorage.setting
.put(SettingBoxKey.blockUserID, _userId);
setState(() {});
}
},
child: Text('确定'),
)
],
);
},
);
},
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -41,198 +174,155 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
body: ListView.separated( body: CustomScrollView(
itemCount: _blockSettings.length + 2, slivers: [
itemBuilder: (_, index) => index == _blockSettings.length + 1 SliverToBoxAdapter(child: _blockLimitItem),
? ListTile( SliverToBoxAdapter(child: Divider(height: 1)),
leading: Icon(Icons.code), SliverToBoxAdapter(child: _userIdItem),
title: const Text('About'), SliverToBoxAdapter(child: Divider(height: 1)),
subtitle: Text(_url), SliverList.separated(
onTap: () => Utils.launchURL(_url), itemCount: _blockSettings.length,
) itemBuilder: (_, index) => ListTile(
: index == 0 enabled: _blockSettings[index].second != SkipType.disable,
? ListTile( onTap: () {
onTap: () { showDialog(
final textController = context: context,
TextEditingController(text: _blockLimit.toString()); builder: (_) => AlertDialog(
showDialog( clipBehavior: Clip.hardEdge,
context: context, contentPadding: const EdgeInsets.symmetric(vertical: 16),
builder: (BuildContext context) { title: Text.rich(
return AlertDialog( style: TextStyle(height: 1),
title: const Text('Block Limit'), strutStyle: StrutStyle(height: 1, leading: 0),
content: TextFormField( TextSpan(
keyboardType: TextInputType.numberWithOptions( children: [
decimal: true), TextSpan(
controller: textController, text: 'Color Picker\n',
autofocus: true, style: TextStyle(fontSize: 18, height: 1.5),
decoration: InputDecoration(suffixText: 's'), ),
), WidgetSpan(
actions: [ alignment: PlaceholderAlignment.middle,
TextButton( child: Container(
onPressed: Get.back, height:
child: Text( MediaQuery.textScalerOf(context).scale(16),
'取消', width: MediaQuery.textScalerOf(context).scale(16),
style: TextStyle( alignment: Alignment.center,
color: child: Container(
Theme.of(context).colorScheme.outline, height: 10,
), width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _blockColor[index],
), ),
), ),
TextButton(
onPressed: () async {
Get.back();
_blockLimit = max(
0.0,
double.tryParse(textController.text) ??
0.0);
await GStorage.setting.put(
SettingBoxKey.blockLimit, _blockLimit);
setState(() {});
},
child: Text('确定'),
)
],
);
},
);
},
leading: Icon(Icons.av_timer),
title: const Text('Block Limit'),
trailing: Text(
'${_blockLimit}s',
style: TextStyle(fontSize: 13),
),
)
: ListTile(
enabled:
_blockSettings[index - 1].second != SkipType.disable,
onTap: () {
showDialog(
context: context,
builder: (_) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding:
const EdgeInsets.symmetric(vertical: 16),
title: Text.rich(
style: TextStyle(height: 1),
strutStyle: StrutStyle(height: 1, leading: 0),
TextSpan(
children: [
TextSpan(
text: 'Color Picker\n',
style: TextStyle(fontSize: 18, height: 1.5),
),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Container(
height: MediaQuery.textScalerOf(context)
.scale(16),
width: MediaQuery.textScalerOf(context)
.scale(16),
alignment: Alignment.center,
child: Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _blockColor[index - 1],
),
),
),
style: TextStyle(fontSize: 16, height: 1),
),
TextSpan(
text:
' ${_blockSettings[index - 1].first.name}',
style: TextStyle(fontSize: 16, height: 1),
),
],
), ),
style: TextStyle(fontSize: 16, height: 1),
), ),
content: SlideColorPicker( TextSpan(
color: _blockColor[index - 1], text: ' ${_blockSettings[index].first.title}',
callback: (Color? color) async { style: TextStyle(fontSize: 16, height: 1),
_blockColor[index - 1] = color ??
_blockSettings[index - 1].first.color;
await GStorage.setting.put(
SettingBoxKey.blockColor,
_blockColor
.map((item) => item.value
.toRadixString(16)
.substring(2))
.toList());
setState(() {});
},
), ),
),
);
},
leading: Container(
height: 24,
width: 24,
alignment: Alignment.center,
child: Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _blockColor[index - 1],
),
),
),
title: Text(
_blockSettings[index - 1].first.name,
style:
_blockSettings[index - 1].second == SkipType.disable
? TextStyle(
color: Theme.of(context).colorScheme.outline,
)
: null,
),
trailing: PopupMenuButton(
initialValue: _blockSettings[index - 1].second,
onSelected: (item) async {
_blockSettings[index - 1].second = item;
await GStorage.setting.put(
SettingBoxKey.blockSettings,
_blockSettings
.map((item) => item.second.index)
.toList());
setState(() {});
},
itemBuilder: (context) => SkipType.values
.map((item) => PopupMenuItem<SkipType>(
value: item,
child: Text(item.title),
))
.toList(),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_blockSettings[index - 1].second.title,
style: TextStyle(
fontSize: 13,
color: _blockSettings[index - 1].second ==
SkipType.disable
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
),
),
Icon(
size: 20,
Icons.keyboard_arrow_right,
color: _blockSettings[index - 1].second ==
SkipType.disable
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
)
], ],
), ),
), ),
content: SlideColorPicker(
color: _blockColor[index],
callback: (Color? color) async {
_blockColor[index] =
color ?? _blockSettings[index].first.color;
await GStorage.setting.put(
SettingBoxKey.blockColor,
_blockColor
.map((item) =>
item.value.toRadixString(16).substring(2))
.toList());
setState(() {});
},
),
), ),
separatorBuilder: (_, index) => Divider(height: 1), );
},
title: Text.rich(
TextSpan(
children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Container(
height: MediaQuery.textScalerOf(context).scale(15),
width: 10,
alignment: Alignment.center,
child: Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _blockColor[index],
),
),
),
style: TextStyle(fontSize: 15),
),
TextSpan(
text: ' ${_blockSettings[index].first.title}',
style: TextStyle(fontSize: 15),
),
],
),
),
subtitle: Text(
_blockSettings[index].first.description,
style: _blockSettings[index].second == SkipType.disable
? null
: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
trailing: PopupMenuButton(
initialValue: _blockSettings[index].second,
onSelected: (item) async {
_blockSettings[index].second = item;
await GStorage.setting.put(SettingBoxKey.blockSettings,
_blockSettings.map((item) => item.second.index).toList());
setState(() {});
},
itemBuilder: (context) => SkipType.values
.map((item) => PopupMenuItem<SkipType>(
value: item,
child: Text(item.title),
))
.toList(),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_blockSettings[index].second.title,
style: TextStyle(
fontSize: 13,
color: _blockSettings[index].second == SkipType.disable
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
),
),
Icon(
size: 20,
Icons.keyboard_arrow_right,
color: _blockSettings[index].second == SkipType.disable
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
)
],
),
),
),
separatorBuilder: (_, index) => Divider(height: 1),
),
SliverToBoxAdapter(child: Divider(height: 1)),
SliverToBoxAdapter(child: _aboudItem),
SliverToBoxAdapter(
child: SizedBox(
height: 25 + MediaQuery.paddingOf(context).bottom,
)),
],
), ),
); );
} }

View File

@@ -33,24 +33,49 @@ enum SegmentType {
preview, preview,
music_offtopic, music_offtopic,
poi_highlight, poi_highlight,
chapter,
filler, filler,
exclusive_access exclusive_access
} }
extension SegmentTypeExt on SegmentType { extension SegmentTypeExt on SegmentType {
/// from https://github.com/hanydd/BilibiliSponsorBlock/*/public/_locales/zh_CN/messages.json
String get title => [
'赞助广告', //sponsor
'无偿/自我推广', //selfpromo
'三连/订阅提醒', //interaction
'过场/开场动画', //intro
'鸣谢/结束画面', //outro
'回顾/概要', //preview
'音乐:非音乐部分', //music_offtopic
'精彩时刻/重点', //poi_highlight
'离题闲聊/玩笑', //filler
'品牌合作', //exclusive_access
][index];
String get description => [
'付费推广、付费推荐和直接广告。不是自我推广或免费提及他们喜欢的商品/创作者/网站/产品。', //sponsor
'类似于 “赞助广告” ,但无报酬或是自我推广。包括有关商品、捐赠的部分或合作者的信息。', //selfpromo
'视频中间简短提醒观众来一键三连或关注。 如果片段较长,或是有具体内容,则应分类为自我推广。', //interaction
'没有实际内容的间隔片段。可以是暂停、静态帧或重复动画。不适用于包含内容的过场。', //intro
'致谢画面或片尾画面。不包含内容的结尾。', //outro
'展示此视频或同系列视频将出现的画面集锦,片段中所有内容都将在之后的正片中再次出现。', //preview
'仅用于音乐视频。此分类只能用于音乐视频中未包括于其他分类的部分。', //music_offtopic
'大部分人都在寻找的空降时间。类似于“封面在12:34”的评论。', //poi_highlight
"仅作为填充内容或增添趣味而添加的离题片段,这些内容对理解视频的主要内容并非必需。这不包括提供背景信息或上下文的片段。这是一个非常激进的分类,适用于当你不想看'娱乐性'内容的时候。", //filler
'仅用于对整个视频进行标记。适用于展示UP主免费或获得补贴后使用的产品、服务或场地的视频。', //exclusive_access
][index];
Color get color => [ Color get color => [
Colors.amber, Color(0xFF00d400), //sponsor
Colors.blue, Color(0xFFffff00), //selfpromo
Colors.red, Color(0xFFcc00ff), //interaction
Colors.indigo, Color(0xFF00ffff), //intro
Colors.pink, Color(0xFF0202ed), //outro
Colors.purple, Color(0xFF008fd6), //preview
Colors.lightGreen, Color(0xFFff9900), //music_offtopic
Colors.teal, Color(0xFFff1684), //poi_highlight
Colors.cyan, Color(0xFF7300FF), //filler
Colors.yellow, Color(0xFF008a5c), //exclusive_access
Colors.orange
][index]; ][index];
} }
@@ -62,11 +87,15 @@ extension SkipTypeExt on SkipType {
class SegmentModel { class SegmentModel {
SegmentModel({ SegmentModel({
// ignore: non_constant_identifier_names
required this.UUID,
required this.segmentType, required this.segmentType,
required this.segment, required this.segment,
required this.skipType, required this.skipType,
required this.hasSkipped, required this.hasSkipped,
}); });
// ignore: non_constant_identifier_names
String UUID;
SegmentType segmentType; SegmentType segmentType;
Pair<int, int> segment; Pair<int, int> segment;
SkipType skipType; SkipType skipType;
@@ -215,13 +244,256 @@ class VideoDetailController extends GetxController
double? _blockLimit; double? _blockLimit;
List<Pair<SegmentType, SkipType>>? _blockSettings; List<Pair<SegmentType, SkipType>>? _blockSettings;
List<Color>? _blockColor; List<Color>? _blockColor;
List<SegmentModel>? _segmentList; RxList<SegmentModel> segmentList = <SegmentModel>[].obs;
List<Segment>? _segmentProgressList; List<Segment>? _segmentProgressList;
Color _getColor(SegmentType segment) =>
_blockColor?[segment.index] ?? segment.color;
Future _vote(String uuid, int type) async {
Request()
.post(
'${HttpString.sponsorBlockBaseUrl}/api/voteOnSponsorTime',
queryParameters: {
'UUID': uuid,
'userID': GStorage.blockUserID,
'type': 1,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
)
.then((res) {
SmartDialog.showToast(res.statusCode == 200 ? '投票成功' : '投票失败');
});
}
void _showCategoryDialog(BuildContext context, SegmentModel segment) {
showDialog(
context: context,
builder: (_) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: SegmentType.values
.map((item) => ListTile(
dense: true,
onTap: () {
Get.back();
Request().post(
'${HttpString.sponsorBlockBaseUrl}/api/voteOnSponsorTime',
queryParameters: {
'UUID': segment.UUID,
'userID': GStorage.blockUserID,
'category': item.name,
},
).then((res) {
SmartDialog.showToast(
'类别更改${res.statusCode == 200 ? '成功' : '失败'}');
});
},
title: Text.rich(
TextSpan(
children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Container(
height:
MediaQuery.textScalerOf(context).scale(14),
width: 10,
alignment: Alignment.center,
child: Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getColor(item),
),
),
),
style: TextStyle(fontSize: 14),
),
TextSpan(
text: ' ${item.title}',
style: TextStyle(fontSize: 14),
),
],
),
),
))
.toList(),
),
),
),
);
}
void _showVoteDialog(BuildContext context, SegmentModel segment) {
showDialog(
context: context,
builder: (_) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
dense: true,
title: Text(
'赞成票',
style: TextStyle(fontSize: 14),
),
onTap: () {
Get.back();
_vote(segment.UUID, 1);
},
),
ListTile(
dense: true,
title: Text(
'反对票',
style: TextStyle(fontSize: 14),
),
onTap: () {
Get.back();
_vote(segment.UUID, 0);
},
),
ListTile(
dense: true,
title: Text(
'更改类别',
style: TextStyle(fontSize: 14),
),
onTap: () {
Get.back();
_showCategoryDialog(context, segment);
},
),
],
),
),
),
);
}
void showSponsorBlock(BuildContext context) {
showDialog(
context: context,
builder: (_) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: segmentList
.map(
(item) => ListTile(
onTap: () {
Get.back();
_showVoteDialog(context, item);
},
dense: true,
title: Text.rich(
TextSpan(
children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Container(
height:
MediaQuery.textScalerOf(context).scale(14),
width: 10,
alignment: Alignment.center,
child: Container(
height: 10,
width: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getColor(item.segmentType),
),
),
),
style: TextStyle(fontSize: 14),
),
TextSpan(
text: ' ${item.segmentType.title}',
style: TextStyle(fontSize: 14),
),
],
),
),
contentPadding: EdgeInsets.only(left: 16, right: 8),
subtitle: Text(
'${Utils.formatDuration(item.segment.first)}${Utils.formatDuration(item.segment.second)}',
style: TextStyle(fontSize: 13),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.skipType.title,
style: TextStyle(fontSize: 13),
),
if (item.skipType == SkipType.showOnly)
SizedBox(
width: 36,
height: 36,
child: IconButton(
tooltip: '跳转至此片段',
onPressed: () async {
Get.back();
try {
plPlayerController.danmakuController?.clear();
await plPlayerController.videoPlayerController
?.seek(Duration(
seconds: item.segment.first));
SmartDialog.showToast(
'已跳至${Utils.formatDuration(item.segment.first)}',
displayType: SmartToastType.normal,
);
} catch (e) {
SmartDialog.showToast(
'跳转失败: $e',
displayType: SmartToastType.normal,
);
}
},
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
icon: Icon(
Icons.my_location,
size: 18,
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.7),
),
),
)
else
const SizedBox(width: 10),
],
),
),
)
.toList(),
),
),
),
);
}
Future _sponsorBlock() async { Future _sponsorBlock() async {
dynamic result = await Request().get( dynamic result = await Request().get(
'https://www.bsbsb.top/api/skipSegments', '${HttpString.sponsorBlockBaseUrl}/api/skipSegments',
data: {'videoID': bvid}, data: {
'videoID': bvid,
'cid': cid.value,
},
options: Options( options: Options(
headers: { headers: {
'env': '', 'env': '',
@@ -243,10 +515,10 @@ class VideoDetailController extends GetxController
.toList() .toList()
.map((item) => item.first.name) .map((item) => item.first.name)
.toList(); .toList();
_segmentList = (result.data as List) segmentList.value = (result.data as List)
.where((item) => .where((item) =>
enableList.contains(item['category']) && enableList.contains(item['category']) &&
item['segment'][1] > 0 && // item['segment'][1] > 0 &&
item['segment'][1] >= item['segment'][0]) item['segment'][1] >= item['segment'][0])
.map( .map(
(item) { (item) {
@@ -260,6 +532,7 @@ class VideoDetailController extends GetxController
} }
} }
return SegmentModel( return SegmentModel(
UUID: item['UUID'],
segmentType: segmentType, segmentType: segmentType,
segment: Pair( segment: Pair(
first: _convert(item['segment'][0]), first: _convert(item['segment'][0]),
@@ -270,7 +543,7 @@ class VideoDetailController extends GetxController
); );
}, },
).toList(); ).toList();
_segmentProgressList = _segmentList?.map((item) { _segmentProgressList = segmentList.map((item) {
double start = (item.segment.first / ((data.timeLength ?? 0) / 1000)) double start = (item.segment.first / ((data.timeLength ?? 0) / 1000))
.clamp(0.0, 1.0); .clamp(0.0, 1.0);
double end = (item.segment.second / ((data.timeLength ?? 0) / 1000)) double end = (item.segment.second / ((data.timeLength ?? 0) / 1000))
@@ -278,11 +551,11 @@ class VideoDetailController extends GetxController
return Segment( return Segment(
start, start,
start == end ? (end + 0.01).clamp(0.0, 1.0) : end, start == end ? (end + 0.01).clamp(0.0, 1.0) : end,
_blockColor?[item.segmentType.index] ?? item.segmentType.color, _getColor(item.segmentType),
); );
}).toList(); }).toList();
} catch (e) { } catch (e) {
debugPrint(e.toString()); debugPrint('filed to parse sponsorblock: $e');
} }
} }
} }
@@ -296,14 +569,14 @@ class VideoDetailController extends GetxController
} }
void _initSkip() { void _initSkip() {
if (_segmentList != null && _segmentList!.isNotEmpty) { if (segmentList.isNotEmpty) {
positionSubscription = plPlayerController positionSubscription = plPlayerController
.videoPlayerController?.stream.position .videoPlayerController?.stream.position
.listen((position) async { .listen((position) async {
int currentPos = position.inSeconds; int currentPos = position.inSeconds;
if (currentPos != _lastPos) { if (currentPos != _lastPos) {
_lastPos = currentPos; _lastPos = currentPos;
for (SegmentModel item in _segmentList!) { for (SegmentModel item in segmentList) {
// debugPrint( // debugPrint(
// '${position.inSeconds},,${item.segment.first},,${item.segment.second},,${item.skipType.name},,${item.hasSkipped}'); // '${position.inSeconds},,${item.segment.first},,${item.segment.second},,${item.skipType.name},,${item.hasSkipped}');
if (item.segment.first == position.inSeconds) { if (item.segment.first == position.inSeconds) {
@@ -315,11 +588,17 @@ class VideoDetailController extends GetxController
?.seek(Duration(seconds: item.segment.second)); ?.seek(Duration(seconds: item.segment.second));
// await plPlayerController // await plPlayerController
// .seekTo(Duration(seconds: item.segment.second)); // .seekTo(Duration(seconds: item.segment.second));
SmartDialog.showToast('已跳过${item.segmentType.name}'); SmartDialog.showToast(
'已跳过${item.segmentType.title}片段',
displayType: SmartToastType.normal,
);
item.hasSkipped = true; item.hasSkipped = true;
} catch (e) { } catch (e) {
debugPrint('failed to skip: $e'); debugPrint('failed to skip: $e');
SmartDialog.showToast('${item.segmentType.name}跳过失败'); SmartDialog.showToast(
'${item.segmentType.title}片段跳过失败',
displayType: SmartToastType.normal,
);
} }
} }
break; break;

View File

@@ -1454,6 +1454,27 @@ class _HeaderControlState extends State<HeaderControl> {
// ), // ),
// fuc: () => _.screenshot(), // fuc: () => _.screenshot(),
// ), // ),
Obx(
() => widget.videoDetailCtr?.segmentList.isNotEmpty == true
? SizedBox(
width: 42,
height: 34,
child: IconButton(
tooltip: 'SponsorBlock',
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero),
),
onPressed: () =>
widget.videoDetailCtr?.showSponsorBlock(context),
icon: const Icon(
Icons.block,
size: 19,
color: Colors.white,
),
),
)
: const SizedBox.shrink(),
),
SizedBox( SizedBox(
width: 42, width: 42,
height: 34, height: 34,

View File

@@ -12,6 +12,7 @@ import 'package:PiliPalaX/models/model_owner.dart';
import 'package:PiliPalaX/models/search/hot.dart'; import 'package:PiliPalaX/models/search/hot.dart';
import 'package:PiliPalaX/models/user/info.dart'; import 'package:PiliPalaX/models/user/info.dart';
import 'global_data.dart'; import 'global_data.dart';
import 'package:uuid/uuid.dart';
class GStorage { class GStorage {
static late final Box<dynamic> userInfo; static late final Box<dynamic> userInfo;
@@ -49,6 +50,16 @@ class GStorage {
static double get blockLimit => static double get blockLimit =>
setting.get(SettingBoxKey.blockLimit, defaultValue: 0.0); setting.get(SettingBoxKey.blockLimit, defaultValue: 0.0);
static String get blockUserID {
String blockUserID =
setting.get(SettingBoxKey.blockUserID, defaultValue: '');
if (blockUserID.isEmpty) {
blockUserID = Uuid().v4().replaceAll('-', '');
setting.put(SettingBoxKey.blockUserID, blockUserID);
}
return blockUserID;
}
static ThemeMode get themeMode { static ThemeMode get themeMode {
switch (setting.get(SettingBoxKey.themeMode, switch (setting.get(SettingBoxKey.themeMode,
defaultValue: ThemeType.system.code)) { defaultValue: ThemeType.system.code)) {
@@ -225,6 +236,7 @@ class SettingBoxKey {
blockSettings = 'blockSettings', blockSettings = 'blockSettings',
blockLimit = 'blockLimit', blockLimit = 'blockLimit',
blockColor = 'blockColor', blockColor = 'blockColor',
blockUserID = 'blockUserID',
// 弹幕相关设置 权重(云屏蔽) 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 字体粗细 // 弹幕相关设置 权重(云屏蔽) 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 字体粗细
danmakuWeight = 'danmakuWeight', danmakuWeight = 'danmakuWeight',

View File

@@ -602,6 +602,22 @@ class Utils {
return v.toString() + random.nextInt(9999).toString(); return v.toString() + random.nextInt(9999).toString();
} }
static String formatDuration(int seconds) {
int hours = seconds ~/ 3600;
int minutes = (seconds % 3600) ~/ 60;
int remainingSeconds = seconds % 60;
String minutesStr = minutes.toString().padLeft(2, '0');
String secondsStr = remainingSeconds.toString().padLeft(2, '0');
if (hours > 0) {
String hoursStr = hours.toString().padLeft(2, '0');
return "$hoursStr:$minutesStr:$secondsStr";
} else {
return "$minutesStr:$secondsStr";
}
}
static int duration(String duration) { static int duration(String duration) {
List timeList = duration.split(':'); List timeList = duration.split(':');
int len = timeList.length; int len = timeList.length;