mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
opt: sponsor block
This commit is contained in:
8
lib/common/widgets/pair.dart
Normal file
8
lib/common/widgets/pair.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
class Pair<T, R> {
|
||||||
|
Pair({
|
||||||
|
required this.first,
|
||||||
|
required this.second,
|
||||||
|
});
|
||||||
|
T first;
|
||||||
|
R second;
|
||||||
|
}
|
||||||
49
lib/common/widgets/segment_progress_bar.dart
Normal file
49
lib/common/widgets/segment_progress_bar.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class Segment {
|
||||||
|
final double start;
|
||||||
|
final double end;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
Segment(this.start, this.end, this.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SegmentProgressBar extends CustomPainter {
|
||||||
|
final double progress;
|
||||||
|
final List<Segment> segmentColors;
|
||||||
|
|
||||||
|
SegmentProgressBar({
|
||||||
|
required this.progress,
|
||||||
|
required this.segmentColors,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
for (var segment in segmentColors) {
|
||||||
|
paint.color = segment.color;
|
||||||
|
final segmentStart = segment.start * size.width;
|
||||||
|
final segmentEnd = segment.end * size.width;
|
||||||
|
final progressEnd = progress * size.width;
|
||||||
|
|
||||||
|
if (progressEnd < segmentStart) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final segmentWidth =
|
||||||
|
(progressEnd < segmentEnd ? progressEnd : segmentEnd) - segmentStart;
|
||||||
|
if (segmentWidth > 0) {
|
||||||
|
canvas.drawRect(
|
||||||
|
Rect.fromLTWH(segmentStart, 0, segmentWidth, size.height),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -139,12 +139,13 @@ class _ExtraSettingState extends State<ExtraSetting> {
|
|||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
const SetSwitchItem(
|
SetSwitchItem(
|
||||||
title: 'Sponsor Block',
|
title: 'Sponsor Block',
|
||||||
subTitle: '跳过赞助商广告',
|
subTitle: '点击配置',
|
||||||
leading: Icon(Icons.block),
|
leading: Icon(Icons.block),
|
||||||
setKey: SettingBoxKey.enableSponsorBlock,
|
setKey: SettingBoxKey.enableSponsorBlock,
|
||||||
defaultVal: false,
|
defaultVal: false,
|
||||||
|
onTap: () => Get.toNamed('/sponsorBlock'),
|
||||||
),
|
),
|
||||||
Obx(
|
Obx(
|
||||||
() => ListTile(
|
() => ListTile(
|
||||||
|
|||||||
160
lib/pages/setting/sponsor_block_page.dart
Normal file
160
lib/pages/setting/sponsor_block_page.dart
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:PiliPalaX/common/widgets/pair.dart';
|
||||||
|
import 'package:PiliPalaX/pages/video/detail/controller.dart'
|
||||||
|
show SegmentType, SegmentTypeExt, SkipType, SkipTypeExt;
|
||||||
|
import 'package:PiliPalaX/utils/storage.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class SponsorBlockPage extends StatefulWidget {
|
||||||
|
const SponsorBlockPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SponsorBlockPage> createState() => _SponsorBlockPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SponsorBlockPageState extends State<SponsorBlockPage> {
|
||||||
|
late double _blockLimit;
|
||||||
|
late List<Pair<SegmentType, SkipType>> _blockSettings;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_blockLimit = GStorage.blockLimit;
|
||||||
|
_blockSettings = GStorage.blockSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
centerTitle: false,
|
||||||
|
titleSpacing: 0,
|
||||||
|
title: Text(
|
||||||
|
'Sponsor Block',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: ListView.separated(
|
||||||
|
itemCount: _blockSettings.length + 1,
|
||||||
|
itemBuilder: (_, index) => 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
leading: Container(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Container(
|
||||||
|
height: 10,
|
||||||
|
width: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: _blockSettings[index - 1].first.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
separatorBuilder: (_, index) => Divider(height: 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ class SetSwitchItem extends StatefulWidget {
|
|||||||
final Function? callFn;
|
final Function? callFn;
|
||||||
final bool? needReboot;
|
final bool? needReboot;
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
|
final GestureTapCallback? onTap;
|
||||||
|
|
||||||
const SetSwitchItem({
|
const SetSwitchItem({
|
||||||
this.title,
|
this.title,
|
||||||
@@ -21,6 +22,7 @@ class SetSwitchItem extends StatefulWidget {
|
|||||||
this.callFn,
|
this.callFn,
|
||||||
this.needReboot,
|
this.needReboot,
|
||||||
this.leading,
|
this.leading,
|
||||||
|
this.onTap,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@@ -56,14 +58,19 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
|
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||||
|
color: widget.onTap != null && !val
|
||||||
|
? Theme.of(context).colorScheme.outline
|
||||||
|
: null);
|
||||||
TextStyle subTitleStyle = Theme.of(context)
|
TextStyle subTitleStyle = Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.labelMedium!
|
.labelMedium!
|
||||||
.copyWith(color: Theme.of(context).colorScheme.outline);
|
.copyWith(color: Theme.of(context).colorScheme.outline);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
|
enabled: widget.onTap != null ? val : true,
|
||||||
enableFeedback: true,
|
enableFeedback: true,
|
||||||
onTap: () => switchChange(null),
|
onTap: () =>
|
||||||
|
widget.onTap != null ? widget.onTap?.call() : switchChange(null),
|
||||||
title: Text(widget.title!, style: titleStyle),
|
title: Text(widget.title!, style: titleStyle),
|
||||||
subtitle: widget.subTitle != null
|
subtitle: widget.subTitle != null
|
||||||
? Text(widget.subTitle!, style: subTitleStyle)
|
? Text(widget.subTitle!, style: subTitleStyle)
|
||||||
@@ -73,9 +80,9 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
|
|||||||
alignment: Alignment.centerRight, // 缩放Switch的大小后保持右侧对齐, 避免右侧空隙过大
|
alignment: Alignment.centerRight, // 缩放Switch的大小后保持右侧对齐, 避免右侧空隙过大
|
||||||
scale: 0.8,
|
scale: 0.8,
|
||||||
child: Switch(
|
child: Switch(
|
||||||
thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
|
thumbIcon:
|
||||||
(Set<MaterialState> states) {
|
WidgetStateProperty.resolveWith<Icon?>((Set<WidgetState> states) {
|
||||||
if (states.isNotEmpty && states.first == MaterialState.selected) {
|
if (states.isNotEmpty && states.first == WidgetState.selected) {
|
||||||
return const Icon(Icons.done);
|
return const Icon(Icons.done);
|
||||||
}
|
}
|
||||||
return null; // All other states will use the default thumbIcon.
|
return null; // All other states will use the default thumbIcon.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:PiliPalaX/common/widgets/pair.dart';
|
||||||
|
import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart';
|
||||||
import 'package:PiliPalaX/http/danmaku.dart';
|
import 'package:PiliPalaX/http/danmaku.dart';
|
||||||
import 'package:PiliPalaX/http/init.dart';
|
import 'package:PiliPalaX/http/init.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -22,6 +24,55 @@ import 'package:ns_danmaku/models/danmaku_item.dart';
|
|||||||
import '../../../utils/id_utils.dart';
|
import '../../../utils/id_utils.dart';
|
||||||
import 'widgets/header_control.dart';
|
import 'widgets/header_control.dart';
|
||||||
|
|
||||||
|
enum SegmentType {
|
||||||
|
sponsor,
|
||||||
|
selfpromo,
|
||||||
|
interaction,
|
||||||
|
intro,
|
||||||
|
outro,
|
||||||
|
preview,
|
||||||
|
music_offtopic,
|
||||||
|
poi_highlight,
|
||||||
|
chapter,
|
||||||
|
filler,
|
||||||
|
exclusive_access
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SegmentTypeExt on SegmentType {
|
||||||
|
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
|
||||||
|
][index];
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SkipType { alwaysSkip, skipOnce, showOnly, disable }
|
||||||
|
|
||||||
|
extension SkipTypeExt on SkipType {
|
||||||
|
String get title => ['总是跳过', '跳过一次', '仅显示', '禁用'][index];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SegmentModel {
|
||||||
|
SegmentModel({
|
||||||
|
required this.segmentType,
|
||||||
|
required this.segment,
|
||||||
|
required this.skipType,
|
||||||
|
required this.hasSkipped,
|
||||||
|
});
|
||||||
|
SegmentType segmentType;
|
||||||
|
Pair<int, int> segment;
|
||||||
|
SkipType skipType;
|
||||||
|
bool hasSkipped;
|
||||||
|
}
|
||||||
|
|
||||||
class VideoDetailController extends GetxController
|
class VideoDetailController extends GetxController
|
||||||
with GetSingleTickerProviderStateMixin {
|
with GetSingleTickerProviderStateMixin {
|
||||||
/// 路由传参
|
/// 路由传参
|
||||||
@@ -90,8 +141,8 @@ class VideoDetailController extends GetxController
|
|||||||
late String cacheSecondDecode;
|
late String cacheSecondDecode;
|
||||||
late int cacheAudioQa;
|
late int cacheAudioQa;
|
||||||
|
|
||||||
|
late final bool _enableSponsorBlock;
|
||||||
PlayerStatus? playerStatus;
|
PlayerStatus? playerStatus;
|
||||||
|
|
||||||
StreamSubscription<Duration>? positionSubscription;
|
StreamSubscription<Duration>? positionSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -151,12 +202,19 @@ class VideoDetailController extends GetxController
|
|||||||
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
||||||
defaultValue: AudioQuality.hiRes.code);
|
defaultValue: AudioQuality.hiRes.code);
|
||||||
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
|
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
|
||||||
if (setting.get(SettingBoxKey.enableSponsorBlock, defaultValue: false)) {
|
_enableSponsorBlock =
|
||||||
_sponsorBlock();
|
setting.get(SettingBoxKey.enableSponsorBlock, defaultValue: false);
|
||||||
|
if (_enableSponsorBlock) {
|
||||||
|
_blockLimit = GStorage.blockLimit;
|
||||||
|
_blockSettings = GStorage.blockSettings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List? _segmentList;
|
int? _lastPos;
|
||||||
|
double? _blockLimit;
|
||||||
|
List<Pair<SegmentType, SkipType>>? _blockSettings;
|
||||||
|
List<SegmentModel>? _segmentList;
|
||||||
|
List<Segment>? _segmentProgressList;
|
||||||
|
|
||||||
Future _sponsorBlock() async {
|
Future _sponsorBlock() async {
|
||||||
dynamic result = await Request().get(
|
dynamic result = await Request().get(
|
||||||
@@ -175,30 +233,94 @@ class VideoDetailController extends GetxController
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (result.data is List && result.data.isNotEmpty) {
|
if (result.data is List && result.data.isNotEmpty) {
|
||||||
_segmentList = (result.data as List)
|
try {
|
||||||
.where((item) => item['category'] == 'sponsor')
|
List<String> list =
|
||||||
.toList()
|
SegmentType.values.map((item) => item.name).toList();
|
||||||
.map((item) => item['segment'])
|
List<String> enableList = _blockSettings!
|
||||||
.toList();
|
.where((item) => item.second != SkipType.disable)
|
||||||
|
.toList()
|
||||||
|
.map((item) => item.first.name)
|
||||||
|
.toList();
|
||||||
|
_segmentList = (result.data as List)
|
||||||
|
.where((item) =>
|
||||||
|
enableList.contains(item['category']) &&
|
||||||
|
item['segment'][1] > 0 &&
|
||||||
|
item['segment'][1] >= item['segment'][0])
|
||||||
|
.map(
|
||||||
|
(item) {
|
||||||
|
SegmentType segmentType =
|
||||||
|
SegmentType.values[list.indexOf(item['category'])];
|
||||||
|
SkipType skipType = _blockSettings![segmentType.index].second;
|
||||||
|
if (skipType != SkipType.showOnly) {
|
||||||
|
if (item['segment'][1] == item['segment'][0] ||
|
||||||
|
item['segment'][1] - item['segment'][0] < _blockLimit) {
|
||||||
|
skipType = SkipType.showOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SegmentModel(
|
||||||
|
segmentType: segmentType,
|
||||||
|
segment: Pair(
|
||||||
|
first: _convert(item['segment'][0]),
|
||||||
|
second: _convert(item['segment'][1]),
|
||||||
|
),
|
||||||
|
skipType: skipType,
|
||||||
|
hasSkipped: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList();
|
||||||
|
_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))
|
||||||
|
.clamp(0.0, 1.0);
|
||||||
|
return Segment(
|
||||||
|
start,
|
||||||
|
start == end ? (end + 0.01).clamp(0.0, 1.0) : end,
|
||||||
|
item.segmentType.color,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
} catch (e) {
|
||||||
|
print(e.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _convert(value) {
|
||||||
|
return value is double
|
||||||
|
? value.round()
|
||||||
|
: value is int
|
||||||
|
? value
|
||||||
|
: -1;
|
||||||
|
}
|
||||||
|
|
||||||
void _initSkip() {
|
void _initSkip() {
|
||||||
if (_segmentList != null && _segmentList!.isNotEmpty) {
|
if (_segmentList != null && _segmentList!.isNotEmpty) {
|
||||||
positionSubscription = plPlayerController
|
positionSubscription = plPlayerController
|
||||||
.videoPlayerController?.stream.position
|
.videoPlayerController?.stream.position
|
||||||
.listen((position) async {
|
.listen((position) async {
|
||||||
for (List item in _segmentList!) {
|
int currentPos = position.inSeconds;
|
||||||
// debugPrint(
|
if (currentPos != _lastPos) {
|
||||||
// '${position.inSeconds},,${(item.first as double).round()}');
|
_lastPos = currentPos;
|
||||||
if ((item.first as double).round() == position.inSeconds) {
|
for (SegmentModel item in _segmentList!) {
|
||||||
try {
|
// debugPrint(
|
||||||
await plPlayerController
|
// '${position.inSeconds},,${item.segment.first},,${item.segment.second},,${item.skipType.name},,${item.hasSkipped}');
|
||||||
.seekTo(Duration(seconds: (item[1] as double).round()));
|
if (item.segment.first == position.inSeconds) {
|
||||||
SmartDialog.showToast('已跳过赞助商广告');
|
if (item.skipType == SkipType.alwaysSkip ||
|
||||||
} catch (e) {
|
(item.skipType == SkipType.skipOnce && !item.hasSkipped)) {
|
||||||
debugPrint('failed to skip: $e');
|
try {
|
||||||
SmartDialog.showToast('广告跳过失败');
|
plPlayerController.danmakuController?.clear();
|
||||||
|
await plPlayerController.videoPlayerController
|
||||||
|
?.seek(Duration(seconds: item.segment.second));
|
||||||
|
// await plPlayerController
|
||||||
|
// .seekTo(Duration(seconds: item.segment.second));
|
||||||
|
SmartDialog.showToast('已跳过${item.segmentType.name}');
|
||||||
|
item.hasSkipped = true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('failed to skip: $e');
|
||||||
|
SmartDialog.showToast('${item.segmentType.name}跳过失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,6 +506,7 @@ class VideoDetailController extends GetxController
|
|||||||
'referer': HttpString.baseUrl
|
'referer': HttpString.baseUrl
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
segmentList: _segmentProgressList,
|
||||||
// 硬解
|
// 硬解
|
||||||
enableHA: enableHA.value,
|
enableHA: enableHA.value,
|
||||||
hwdec: hwdec.value,
|
hwdec: hwdec.value,
|
||||||
@@ -414,6 +537,9 @@ class VideoDetailController extends GetxController
|
|||||||
var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
|
var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
|
||||||
if (result['status']) {
|
if (result['status']) {
|
||||||
data = result['data'];
|
data = result['data'];
|
||||||
|
if (_enableSponsorBlock) {
|
||||||
|
await _sponsorBlock();
|
||||||
|
}
|
||||||
if (data.acceptDesc!.isNotEmpty && data.acceptDesc!.contains('试看')) {
|
if (data.acceptDesc!.isNotEmpty && data.acceptDesc!.contains('试看')) {
|
||||||
SmartDialog.showToast(
|
SmartDialog.showToast(
|
||||||
'该视频为专属视频,仅提供试看',
|
'该视频为专属视频,仅提供试看',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart';
|
||||||
import 'package:easy_debounce/easy_throttle.dart';
|
import 'package:easy_debounce/easy_throttle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
@@ -114,6 +115,8 @@ class PlPlayerController {
|
|||||||
Timer? _timerForGettingVolume;
|
Timer? _timerForGettingVolume;
|
||||||
Timer? timerForTrackingMouse;
|
Timer? timerForTrackingMouse;
|
||||||
|
|
||||||
|
final RxList<Segment> segmentList = <Segment>[].obs;
|
||||||
|
|
||||||
// final Durations durations;
|
// final Durations durations;
|
||||||
|
|
||||||
static List<Map<String, dynamic>> videoFitType = [
|
static List<Map<String, dynamic>> videoFitType = [
|
||||||
@@ -403,6 +406,7 @@ class PlPlayerController {
|
|||||||
// 初始化资源
|
// 初始化资源
|
||||||
Future<void> setDataSource(
|
Future<void> setDataSource(
|
||||||
DataSource dataSource, {
|
DataSource dataSource, {
|
||||||
|
List<Segment>? segmentList,
|
||||||
bool autoplay = true,
|
bool autoplay = true,
|
||||||
// 默认不循环
|
// 默认不循环
|
||||||
PlaylistMode looping = PlaylistMode.none,
|
PlaylistMode looping = PlaylistMode.none,
|
||||||
@@ -426,6 +430,7 @@ class PlPlayerController {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
|
this.segmentList.value = segmentList ?? <Segment>[];
|
||||||
_autoPlay = autoplay;
|
_autoPlay = autoplay;
|
||||||
_looping = looping;
|
_looping = looping;
|
||||||
// 初始化视频倍速
|
// 初始化视频倍速
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart';
|
||||||
import 'package:PiliPalaX/http/loading_state.dart';
|
import 'package:PiliPalaX/http/loading_state.dart';
|
||||||
import 'package:PiliPalaX/pages/video/detail/introduction/controller.dart';
|
import 'package:PiliPalaX/pages/video/detail/introduction/controller.dart';
|
||||||
import 'package:PiliPalaX/utils/id_utils.dart';
|
import 'package:PiliPalaX/utils/id_utils.dart';
|
||||||
@@ -970,6 +971,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
BottomControl(
|
BottomControl(
|
||||||
controller: widget.controller,
|
controller: widget.controller,
|
||||||
buildBottomControl: buildBottomControl(),
|
buildBottomControl: buildBottomControl(),
|
||||||
|
segmentList: _.segmentList,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1015,47 +1017,62 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
// label: '${(value / max * 100).round()}%',
|
// label: '${(value / max * 100).round()}%',
|
||||||
value: '${(value / max * 100).round()}%',
|
value: '${(value / max * 100).round()}%',
|
||||||
// enabled: false,
|
// enabled: false,
|
||||||
child: ProgressBar(
|
child: Stack(
|
||||||
progress: Duration(seconds: value),
|
alignment: Alignment.center,
|
||||||
buffered: Duration(seconds: buffer),
|
children: [
|
||||||
total: Duration(seconds: max),
|
ProgressBar(
|
||||||
progressBarColor: colorTheme,
|
progress: Duration(seconds: value),
|
||||||
baseBarColor: Colors.white.withOpacity(0.2),
|
buffered: Duration(seconds: buffer),
|
||||||
bufferedBarColor:
|
total: Duration(seconds: max),
|
||||||
Theme.of(context).colorScheme.primary.withOpacity(0.4),
|
progressBarColor: colorTheme,
|
||||||
timeLabelLocation: TimeLabelLocation.none,
|
baseBarColor: Colors.white.withOpacity(0.2),
|
||||||
thumbColor: colorTheme,
|
bufferedBarColor: Theme.of(context)
|
||||||
barHeight: 3.5,
|
.colorScheme
|
||||||
thumbRadius: draggingFixedProgressBar.value ? 7 : 2.5,
|
.primary
|
||||||
// onDragStart: (duration) {
|
.withOpacity(0.4),
|
||||||
// draggingFixedProgressBar.value = true;
|
timeLabelLocation: TimeLabelLocation.none,
|
||||||
// feedBack();
|
thumbColor: colorTheme,
|
||||||
// _.onChangedSliderStart();
|
barHeight: 3.5,
|
||||||
// },
|
thumbRadius: draggingFixedProgressBar.value ? 7 : 2.5,
|
||||||
// onDragUpdate: (duration) {
|
// onDragStart: (duration) {
|
||||||
// double newProgress = duration.timeStamp.inSeconds / max;
|
// draggingFixedProgressBar.value = true;
|
||||||
// if ((newProgress - _lastAnnouncedValue).abs() > 0.02) {
|
// feedBack();
|
||||||
// _accessibilityDebounce?.cancel();
|
// _.onChangedSliderStart();
|
||||||
// _accessibilityDebounce =
|
// },
|
||||||
// Timer(const Duration(milliseconds: 200), () {
|
// onDragUpdate: (duration) {
|
||||||
// SemanticsService.announce(
|
// double newProgress = duration.timeStamp.inSeconds / max;
|
||||||
// "${(newProgress * 100).round()}%",
|
// if ((newProgress - _lastAnnouncedValue).abs() > 0.02) {
|
||||||
// TextDirection.ltr);
|
// _accessibilityDebounce?.cancel();
|
||||||
// _lastAnnouncedValue = newProgress;
|
// _accessibilityDebounce =
|
||||||
// });
|
// Timer(const Duration(milliseconds: 200), () {
|
||||||
// }
|
// SemanticsService.announce(
|
||||||
// _.onUpdatedSliderProgress(duration.timeStamp);
|
// "${(newProgress * 100).round()}%",
|
||||||
// },
|
// TextDirection.ltr);
|
||||||
// onSeek: (duration) {
|
// _lastAnnouncedValue = newProgress;
|
||||||
// draggingFixedProgressBar.value = false;
|
// });
|
||||||
// _.onChangedSliderEnd();
|
// }
|
||||||
// _.onChangedSlider(duration.inSeconds.toDouble());
|
// _.onUpdatedSliderProgress(duration.timeStamp);
|
||||||
// _.seekTo(Duration(seconds: duration.inSeconds),
|
// },
|
||||||
// type: 'slider');
|
// onSeek: (duration) {
|
||||||
// SemanticsService.announce(
|
// draggingFixedProgressBar.value = false;
|
||||||
// "${(duration.inSeconds / max * 100).round()}%",
|
// _.onChangedSliderEnd();
|
||||||
// TextDirection.ltr);
|
// _.onChangedSlider(duration.inSeconds.toDouble());
|
||||||
// },
|
// _.seekTo(Duration(seconds: duration.inSeconds),
|
||||||
|
// type: 'slider');
|
||||||
|
// SemanticsService.announce(
|
||||||
|
// "${(duration.inSeconds / max * 100).round()}%",
|
||||||
|
// TextDirection.ltr);
|
||||||
|
// },
|
||||||
|
),
|
||||||
|
if (_.segmentList.isNotEmpty)
|
||||||
|
CustomPaint(
|
||||||
|
size: Size(double.infinity, 3.5),
|
||||||
|
painter: SegmentProgressBar(
|
||||||
|
progress: 1,
|
||||||
|
segmentColors: _.segmentList,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
// SlideTransition(
|
// SlideTransition(
|
||||||
// position: Tween<Offset>(
|
// position: Tween<Offset>(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -12,9 +13,11 @@ import '../../../common/widgets/audio_video_progress_bar.dart';
|
|||||||
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
||||||
final PlPlayerController? controller;
|
final PlPlayerController? controller;
|
||||||
final List<Widget>? buildBottomControl;
|
final List<Widget>? buildBottomControl;
|
||||||
|
final List<Segment>? segmentList;
|
||||||
const BottomControl({
|
const BottomControl({
|
||||||
this.controller,
|
this.controller,
|
||||||
this.buildBottomControl,
|
this.buildBottomControl,
|
||||||
|
this.segmentList,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@@ -49,44 +52,59 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
// label: '${(value / max * 100).round()}%',
|
// label: '${(value / max * 100).round()}%',
|
||||||
value: '${(value / max * 100).round()}%',
|
value: '${(value / max * 100).round()}%',
|
||||||
// enabled: false,
|
// enabled: false,
|
||||||
child: ProgressBar(
|
child: Stack(
|
||||||
progress: Duration(seconds: value),
|
alignment: Alignment.center,
|
||||||
buffered: Duration(seconds: buffer),
|
children: [
|
||||||
total: Duration(seconds: max),
|
ProgressBar(
|
||||||
progressBarColor: colorTheme,
|
progress: Duration(seconds: value),
|
||||||
baseBarColor: Colors.white.withOpacity(0.2),
|
buffered: Duration(seconds: buffer),
|
||||||
bufferedBarColor: colorTheme.withOpacity(0.4),
|
total: Duration(seconds: max),
|
||||||
timeLabelLocation: TimeLabelLocation.none,
|
progressBarColor: colorTheme,
|
||||||
thumbColor: colorTheme,
|
baseBarColor: Colors.white.withOpacity(0.2),
|
||||||
barHeight: 3.5,
|
bufferedBarColor: colorTheme.withOpacity(0.4),
|
||||||
thumbRadius: 7,
|
timeLabelLocation: TimeLabelLocation.none,
|
||||||
onDragStart: (duration) {
|
thumbColor: colorTheme,
|
||||||
feedBack();
|
barHeight: 3.5,
|
||||||
_.onChangedSliderStart();
|
thumbRadius: 7,
|
||||||
},
|
onDragStart: (duration) {
|
||||||
onDragUpdate: (duration) {
|
feedBack();
|
||||||
double newProgress = duration.timeStamp.inSeconds / max;
|
_.onChangedSliderStart();
|
||||||
if ((newProgress - _lastAnnouncedValue).abs() > 0.02) {
|
},
|
||||||
_accessibilityDebounce?.cancel();
|
onDragUpdate: (duration) {
|
||||||
_accessibilityDebounce =
|
double newProgress =
|
||||||
Timer(const Duration(milliseconds: 200), () {
|
duration.timeStamp.inSeconds / max;
|
||||||
|
if ((newProgress - _lastAnnouncedValue).abs() >
|
||||||
|
0.02) {
|
||||||
|
_accessibilityDebounce?.cancel();
|
||||||
|
_accessibilityDebounce =
|
||||||
|
Timer(const Duration(milliseconds: 200), () {
|
||||||
|
SemanticsService.announce(
|
||||||
|
"${(newProgress * 100).round()}%",
|
||||||
|
TextDirection.ltr);
|
||||||
|
_lastAnnouncedValue = newProgress;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_.onUpdatedSliderProgress(duration.timeStamp);
|
||||||
|
},
|
||||||
|
onSeek: (duration) {
|
||||||
|
_.onChangedSliderEnd();
|
||||||
|
_.onChangedSlider(duration.inSeconds.toDouble());
|
||||||
|
_.seekTo(Duration(seconds: duration.inSeconds),
|
||||||
|
type: 'slider');
|
||||||
SemanticsService.announce(
|
SemanticsService.announce(
|
||||||
"${(newProgress * 100).round()}%",
|
"${(duration.inSeconds / max * 100).round()}%",
|
||||||
TextDirection.ltr);
|
TextDirection.ltr);
|
||||||
_lastAnnouncedValue = newProgress;
|
},
|
||||||
});
|
),
|
||||||
}
|
if (segmentList?.isNotEmpty == true)
|
||||||
_.onUpdatedSliderProgress(duration.timeStamp);
|
CustomPaint(
|
||||||
},
|
size: Size(double.infinity, 3.5),
|
||||||
onSeek: (duration) {
|
painter: SegmentProgressBar(
|
||||||
_.onChangedSliderEnd();
|
progress: 1,
|
||||||
_.onChangedSlider(duration.inSeconds.toDouble());
|
segmentColors: segmentList!,
|
||||||
_.seekTo(Duration(seconds: duration.inSeconds),
|
),
|
||||||
type: 'slider');
|
),
|
||||||
SemanticsService.announce(
|
],
|
||||||
"${(duration.inSeconds / max * 100).round()}%",
|
|
||||||
TextDirection.ltr);
|
|
||||||
},
|
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// ignore_for_file: must_be_immutable
|
// ignore_for_file: must_be_immutable
|
||||||
|
|
||||||
import 'package:PiliPalaX/pages/member/new/member_page.dart';
|
import 'package:PiliPalaX/pages/member/new/member_page.dart';
|
||||||
|
import 'package:PiliPalaX/pages/setting/sponsor_block_page.dart';
|
||||||
import 'package:PiliPalaX/pages/webview/webview_page.dart';
|
import 'package:PiliPalaX/pages/webview/webview_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -184,6 +185,7 @@ class Routes {
|
|||||||
CustomGetPage(name: '/subDetail', page: () => const SubDetailPage()),
|
CustomGetPage(name: '/subDetail', page: () => const SubDetailPage()),
|
||||||
// 弹幕屏蔽管理
|
// 弹幕屏蔽管理
|
||||||
CustomGetPage(name: '/danmakuBlock', page: () => const DanmakuBlockPage()),
|
CustomGetPage(name: '/danmakuBlock', page: () => const DanmakuBlockPage()),
|
||||||
|
CustomGetPage(name: '/sponsorBlock', page: () => const SponsorBlockPage()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
import 'package:PiliPalaX/common/widgets/pair.dart';
|
||||||
import 'package:PiliPalaX/models/common/theme_type.dart';
|
import 'package:PiliPalaX/models/common/theme_type.dart';
|
||||||
|
import 'package:PiliPalaX/pages/video/detail/controller.dart'
|
||||||
|
show SegmentType, SkipType;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -17,6 +20,22 @@ class GStorage {
|
|||||||
static late final Box<dynamic> setting;
|
static late final Box<dynamic> setting;
|
||||||
static late final Box<dynamic> video;
|
static late final Box<dynamic> video;
|
||||||
|
|
||||||
|
static List<Pair<SegmentType, SkipType>> get blockSettings {
|
||||||
|
List<int> list = setting.get(
|
||||||
|
SettingBoxKey.blockSettings,
|
||||||
|
defaultValue: List.generate(SegmentType.values.length, (_) => 1),
|
||||||
|
);
|
||||||
|
return SegmentType.values
|
||||||
|
.map((item) => Pair<SegmentType, SkipType>(
|
||||||
|
first: item,
|
||||||
|
second: SkipType.values[list[item.index]],
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static double get blockLimit =>
|
||||||
|
setting.get(SettingBoxKey.blockLimit, defaultValue: 0.0);
|
||||||
|
|
||||||
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)) {
|
||||||
@@ -189,6 +208,8 @@ class SettingBoxKey {
|
|||||||
disableLikeMsg = 'disableLikeMsg',
|
disableLikeMsg = 'disableLikeMsg',
|
||||||
defaultHomePage = 'defaultHomePage',
|
defaultHomePage = 'defaultHomePage',
|
||||||
enableSponsorBlock = 'enableSponsorBlock',
|
enableSponsorBlock = 'enableSponsorBlock',
|
||||||
|
blockSettings = 'blockSettings',
|
||||||
|
blockLimit = 'blockLimit',
|
||||||
|
|
||||||
// 弹幕相关设置 权重(云屏蔽) 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 字体粗细
|
// 弹幕相关设置 权重(云屏蔽) 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 字体粗细
|
||||||
danmakuWeight = 'danmakuWeight',
|
danmakuWeight = 'danmakuWeight',
|
||||||
|
|||||||
Reference in New Issue
Block a user