mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
opt: sponsor block
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -8,6 +8,7 @@ class HttpString {
|
||||
static const String messageBaseUrl = 'https://message.bilibili.com';
|
||||
static const String dynamicShareBaseUrl = 'https://t.bilibili.com';
|
||||
static const String spaceBaseUrl = 'https://space.bilibili.com';
|
||||
static const String sponsorBlockBaseUrl = 'https://www.bsbsb.top';
|
||||
static const List<int> validateStatusCodes = [
|
||||
302,
|
||||
304,
|
||||
|
||||
@@ -224,7 +224,8 @@ class Request {
|
||||
/*
|
||||
* post请求
|
||||
*/
|
||||
post(url, {data, queryParameters, options, cancelToken, extra}) async {
|
||||
Future<Response> post(url,
|
||||
{data, queryParameters, options, cancelToken, extra}) async {
|
||||
// debugPrint('post-data: $data');
|
||||
Response response;
|
||||
try {
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:PiliPalaX/utils/utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class SponsorBlockPage extends StatefulWidget {
|
||||
const SponsorBlockPage({super.key});
|
||||
@@ -17,10 +18,11 @@ class SponsorBlockPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
||||
final _url = 'https://github.com/hanydd/BilibiliSponsorBlock';
|
||||
late double _blockLimit;
|
||||
late List<Pair<SegmentType, SkipType>> _blockSettings;
|
||||
late List<Color> _blockColor;
|
||||
final _url = 'https://github.com/hanydd/BilibiliSponsorBlock';
|
||||
late String _userId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -28,8 +30,139 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
||||
_blockLimit = GStorage.blockLimit;
|
||||
_blockSettings = GStorage.blockSettings;
|
||||
_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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -41,198 +174,155 @@ class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
body: ListView.separated(
|
||||
itemCount: _blockSettings.length + 2,
|
||||
itemBuilder: (_, index) => index == _blockSettings.length + 1
|
||||
? ListTile(
|
||||
leading: Icon(Icons.code),
|
||||
title: const Text('About'),
|
||||
subtitle: Text(_url),
|
||||
onTap: () => Utils.launchURL(_url),
|
||||
)
|
||||
: index == 0
|
||||
? ListTile(
|
||||
onTap: () {
|
||||
final textController =
|
||||
TextEditingController(text: _blockLimit.toString());
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Block Limit'),
|
||||
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,
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: _blockLimitItem),
|
||||
SliverToBoxAdapter(child: Divider(height: 1)),
|
||||
SliverToBoxAdapter(child: _userIdItem),
|
||||
SliverToBoxAdapter(child: Divider(height: 1)),
|
||||
SliverList.separated(
|
||||
itemCount: _blockSettings.length,
|
||||
itemBuilder: (_, index) => ListTile(
|
||||
enabled: _blockSettings[index].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],
|
||||
),
|
||||
),
|
||||
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(
|
||||
color: _blockColor[index - 1],
|
||||
callback: (Color? color) async {
|
||||
_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(() {});
|
||||
},
|
||||
TextSpan(
|
||||
text: ' ${_blockSettings[index].first.title}',
|
||||
style: TextStyle(fontSize: 16, height: 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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,
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,24 +33,49 @@ enum SegmentType {
|
||||
preview,
|
||||
music_offtopic,
|
||||
poi_highlight,
|
||||
chapter,
|
||||
filler,
|
||||
exclusive_access
|
||||
}
|
||||
|
||||
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 => [
|
||||
Colors.amber,
|
||||
Colors.blue,
|
||||
Colors.red,
|
||||
Colors.indigo,
|
||||
Colors.pink,
|
||||
Colors.purple,
|
||||
Colors.lightGreen,
|
||||
Colors.teal,
|
||||
Colors.cyan,
|
||||
Colors.yellow,
|
||||
Colors.orange
|
||||
Color(0xFF00d400), //sponsor
|
||||
Color(0xFFffff00), //selfpromo
|
||||
Color(0xFFcc00ff), //interaction
|
||||
Color(0xFF00ffff), //intro
|
||||
Color(0xFF0202ed), //outro
|
||||
Color(0xFF008fd6), //preview
|
||||
Color(0xFFff9900), //music_offtopic
|
||||
Color(0xFFff1684), //poi_highlight
|
||||
Color(0xFF7300FF), //filler
|
||||
Color(0xFF008a5c), //exclusive_access
|
||||
][index];
|
||||
}
|
||||
|
||||
@@ -62,11 +87,15 @@ extension SkipTypeExt on SkipType {
|
||||
|
||||
class SegmentModel {
|
||||
SegmentModel({
|
||||
// ignore: non_constant_identifier_names
|
||||
required this.UUID,
|
||||
required this.segmentType,
|
||||
required this.segment,
|
||||
required this.skipType,
|
||||
required this.hasSkipped,
|
||||
});
|
||||
// ignore: non_constant_identifier_names
|
||||
String UUID;
|
||||
SegmentType segmentType;
|
||||
Pair<int, int> segment;
|
||||
SkipType skipType;
|
||||
@@ -215,13 +244,256 @@ class VideoDetailController extends GetxController
|
||||
double? _blockLimit;
|
||||
List<Pair<SegmentType, SkipType>>? _blockSettings;
|
||||
List<Color>? _blockColor;
|
||||
List<SegmentModel>? _segmentList;
|
||||
RxList<SegmentModel> segmentList = <SegmentModel>[].obs;
|
||||
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 {
|
||||
dynamic result = await Request().get(
|
||||
'https://www.bsbsb.top/api/skipSegments',
|
||||
data: {'videoID': bvid},
|
||||
'${HttpString.sponsorBlockBaseUrl}/api/skipSegments',
|
||||
data: {
|
||||
'videoID': bvid,
|
||||
'cid': cid.value,
|
||||
},
|
||||
options: Options(
|
||||
headers: {
|
||||
'env': '',
|
||||
@@ -243,10 +515,10 @@ class VideoDetailController extends GetxController
|
||||
.toList()
|
||||
.map((item) => item.first.name)
|
||||
.toList();
|
||||
_segmentList = (result.data as List)
|
||||
segmentList.value = (result.data as List)
|
||||
.where((item) =>
|
||||
enableList.contains(item['category']) &&
|
||||
item['segment'][1] > 0 &&
|
||||
// item['segment'][1] > 0 &&
|
||||
item['segment'][1] >= item['segment'][0])
|
||||
.map(
|
||||
(item) {
|
||||
@@ -260,6 +532,7 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
}
|
||||
return SegmentModel(
|
||||
UUID: item['UUID'],
|
||||
segmentType: segmentType,
|
||||
segment: Pair(
|
||||
first: _convert(item['segment'][0]),
|
||||
@@ -270,7 +543,7 @@ class VideoDetailController extends GetxController
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
_segmentProgressList = _segmentList?.map((item) {
|
||||
_segmentProgressList = segmentList.map((item) {
|
||||
double start = (item.segment.first / ((data.timeLength ?? 0) / 1000))
|
||||
.clamp(0.0, 1.0);
|
||||
double end = (item.segment.second / ((data.timeLength ?? 0) / 1000))
|
||||
@@ -278,11 +551,11 @@ class VideoDetailController extends GetxController
|
||||
return Segment(
|
||||
start,
|
||||
start == end ? (end + 0.01).clamp(0.0, 1.0) : end,
|
||||
_blockColor?[item.segmentType.index] ?? item.segmentType.color,
|
||||
_getColor(item.segmentType),
|
||||
);
|
||||
}).toList();
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
debugPrint('filed to parse sponsorblock: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,14 +569,14 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
|
||||
void _initSkip() {
|
||||
if (_segmentList != null && _segmentList!.isNotEmpty) {
|
||||
if (segmentList.isNotEmpty) {
|
||||
positionSubscription = plPlayerController
|
||||
.videoPlayerController?.stream.position
|
||||
.listen((position) async {
|
||||
int currentPos = position.inSeconds;
|
||||
if (currentPos != _lastPos) {
|
||||
_lastPos = currentPos;
|
||||
for (SegmentModel item in _segmentList!) {
|
||||
for (SegmentModel item in segmentList) {
|
||||
// debugPrint(
|
||||
// '${position.inSeconds},,${item.segment.first},,${item.segment.second},,${item.skipType.name},,${item.hasSkipped}');
|
||||
if (item.segment.first == position.inSeconds) {
|
||||
@@ -315,11 +588,17 @@ class VideoDetailController extends GetxController
|
||||
?.seek(Duration(seconds: item.segment.second));
|
||||
// await plPlayerController
|
||||
// .seekTo(Duration(seconds: item.segment.second));
|
||||
SmartDialog.showToast('已跳过${item.segmentType.name}');
|
||||
SmartDialog.showToast(
|
||||
'已跳过${item.segmentType.title}片段',
|
||||
displayType: SmartToastType.normal,
|
||||
);
|
||||
item.hasSkipped = true;
|
||||
} catch (e) {
|
||||
debugPrint('failed to skip: $e');
|
||||
SmartDialog.showToast('${item.segmentType.name}跳过失败');
|
||||
SmartDialog.showToast(
|
||||
'${item.segmentType.title}片段跳过失败',
|
||||
displayType: SmartToastType.normal,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1454,6 +1454,27 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
// ),
|
||||
// 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(
|
||||
width: 42,
|
||||
height: 34,
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:PiliPalaX/models/model_owner.dart';
|
||||
import 'package:PiliPalaX/models/search/hot.dart';
|
||||
import 'package:PiliPalaX/models/user/info.dart';
|
||||
import 'global_data.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class GStorage {
|
||||
static late final Box<dynamic> userInfo;
|
||||
@@ -49,6 +50,16 @@ class GStorage {
|
||||
static double get blockLimit =>
|
||||
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 {
|
||||
switch (setting.get(SettingBoxKey.themeMode,
|
||||
defaultValue: ThemeType.system.code)) {
|
||||
@@ -225,6 +236,7 @@ class SettingBoxKey {
|
||||
blockSettings = 'blockSettings',
|
||||
blockLimit = 'blockLimit',
|
||||
blockColor = 'blockColor',
|
||||
blockUserID = 'blockUserID',
|
||||
|
||||
// 弹幕相关设置 权重(云屏蔽) 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 字体粗细
|
||||
danmakuWeight = 'danmakuWeight',
|
||||
|
||||
@@ -602,6 +602,22 @@ class Utils {
|
||||
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) {
|
||||
List timeList = duration.split(':');
|
||||
int len = timeList.length;
|
||||
|
||||
Reference in New Issue
Block a user