From ca28dd374a3b82372786de9e48b1b4969ef5a31a Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Mon, 21 Oct 2024 21:12:50 +0800 Subject: [PATCH] opt: sponsor block --- lib/common/widgets/pair.dart | 8 + lib/common/widgets/segment_progress_bar.dart | 49 ++++++ lib/pages/setting/extra_setting.dart | 5 +- lib/pages/setting/sponsor_block_page.dart | 160 +++++++++++++++++ lib/pages/setting/widgets/switch_item.dart | 17 +- lib/pages/video/detail/controller.dart | 166 +++++++++++++++--- lib/plugin/pl_player/controller.dart | 5 + lib/plugin/pl_player/view.dart | 99 ++++++----- .../pl_player/widgets/bottom_control.dart | 90 ++++++---- lib/router/app_pages.dart | 2 + lib/utils/storage.dart | 21 +++ 11 files changed, 518 insertions(+), 104 deletions(-) create mode 100644 lib/common/widgets/pair.dart create mode 100644 lib/common/widgets/segment_progress_bar.dart create mode 100644 lib/pages/setting/sponsor_block_page.dart diff --git a/lib/common/widgets/pair.dart b/lib/common/widgets/pair.dart new file mode 100644 index 00000000..0abf2ad9 --- /dev/null +++ b/lib/common/widgets/pair.dart @@ -0,0 +1,8 @@ +class Pair { + Pair({ + required this.first, + required this.second, + }); + T first; + R second; +} diff --git a/lib/common/widgets/segment_progress_bar.dart b/lib/common/widgets/segment_progress_bar.dart new file mode 100644 index 00000000..fcc1fcc0 --- /dev/null +++ b/lib/common/widgets/segment_progress_bar.dart @@ -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 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; + } +} diff --git a/lib/pages/setting/extra_setting.dart b/lib/pages/setting/extra_setting.dart index c9d13623..5301bc70 100644 --- a/lib/pages/setting/extra_setting.dart +++ b/lib/pages/setting/extra_setting.dart @@ -139,12 +139,13 @@ class _ExtraSettingState extends State { ), body: ListView( children: [ - const SetSwitchItem( + SetSwitchItem( title: 'Sponsor Block', - subTitle: '跳过赞助商广告', + subTitle: '点击配置', leading: Icon(Icons.block), setKey: SettingBoxKey.enableSponsorBlock, defaultVal: false, + onTap: () => Get.toNamed('/sponsorBlock'), ), Obx( () => ListTile( diff --git a/lib/pages/setting/sponsor_block_page.dart b/lib/pages/setting/sponsor_block_page.dart new file mode 100644 index 00000000..f2e0defb --- /dev/null +++ b/lib/pages/setting/sponsor_block_page.dart @@ -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 createState() => _SponsorBlockPageState(); +} + +class _SponsorBlockPageState extends State { + late double _blockLimit; + late List> _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( + 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), + ), + ); + } +} diff --git a/lib/pages/setting/widgets/switch_item.dart b/lib/pages/setting/widgets/switch_item.dart index 618de131..0247c4fd 100644 --- a/lib/pages/setting/widgets/switch_item.dart +++ b/lib/pages/setting/widgets/switch_item.dart @@ -12,6 +12,7 @@ class SetSwitchItem extends StatefulWidget { final Function? callFn; final bool? needReboot; final Widget? leading; + final GestureTapCallback? onTap; const SetSwitchItem({ this.title, @@ -21,6 +22,7 @@ class SetSwitchItem extends StatefulWidget { this.callFn, this.needReboot, this.leading, + this.onTap, Key? key, }) : super(key: key); @@ -56,14 +58,19 @@ class _SetSwitchItemState extends State { @override 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) .textTheme .labelMedium! .copyWith(color: Theme.of(context).colorScheme.outline); return ListTile( + enabled: widget.onTap != null ? val : true, enableFeedback: true, - onTap: () => switchChange(null), + onTap: () => + widget.onTap != null ? widget.onTap?.call() : switchChange(null), title: Text(widget.title!, style: titleStyle), subtitle: widget.subTitle != null ? Text(widget.subTitle!, style: subTitleStyle) @@ -73,9 +80,9 @@ class _SetSwitchItemState extends State { alignment: Alignment.centerRight, // 缩放Switch的大小后保持右侧对齐, 避免右侧空隙过大 scale: 0.8, child: Switch( - thumbIcon: MaterialStateProperty.resolveWith( - (Set states) { - if (states.isNotEmpty && states.first == MaterialState.selected) { + thumbIcon: + WidgetStateProperty.resolveWith((Set states) { + if (states.isNotEmpty && states.first == WidgetState.selected) { return const Icon(Icons.done); } return null; // All other states will use the default thumbIcon. diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 954da393..97273095 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -1,5 +1,7 @@ import 'dart:async'; 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/init.dart'; import 'package:dio/dio.dart'; @@ -22,6 +24,55 @@ import 'package:ns_danmaku/models/danmaku_item.dart'; import '../../../utils/id_utils.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 segment; + SkipType skipType; + bool hasSkipped; +} + class VideoDetailController extends GetxController with GetSingleTickerProviderStateMixin { /// 路由传参 @@ -90,8 +141,8 @@ class VideoDetailController extends GetxController late String cacheSecondDecode; late int cacheAudioQa; + late final bool _enableSponsorBlock; PlayerStatus? playerStatus; - StreamSubscription? positionSubscription; @override @@ -151,12 +202,19 @@ class VideoDetailController extends GetxController cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa, defaultValue: AudioQuality.hiRes.code); oid.value = IdUtils.bv2av(Get.parameters['bvid']!); - if (setting.get(SettingBoxKey.enableSponsorBlock, defaultValue: false)) { - _sponsorBlock(); + _enableSponsorBlock = + setting.get(SettingBoxKey.enableSponsorBlock, defaultValue: false); + if (_enableSponsorBlock) { + _blockLimit = GStorage.blockLimit; + _blockSettings = GStorage.blockSettings; } } - List? _segmentList; + int? _lastPos; + double? _blockLimit; + List>? _blockSettings; + List? _segmentList; + List? _segmentProgressList; Future _sponsorBlock() async { dynamic result = await Request().get( @@ -175,30 +233,94 @@ class VideoDetailController extends GetxController ), ); if (result.data is List && result.data.isNotEmpty) { - _segmentList = (result.data as List) - .where((item) => item['category'] == 'sponsor') - .toList() - .map((item) => item['segment']) - .toList(); + try { + List list = + SegmentType.values.map((item) => item.name).toList(); + List enableList = _blockSettings! + .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() { if (_segmentList != null && _segmentList!.isNotEmpty) { positionSubscription = plPlayerController .videoPlayerController?.stream.position .listen((position) async { - for (List item in _segmentList!) { - // debugPrint( - // '${position.inSeconds},,${(item.first as double).round()}'); - if ((item.first as double).round() == position.inSeconds) { - try { - await plPlayerController - .seekTo(Duration(seconds: (item[1] as double).round())); - SmartDialog.showToast('已跳过赞助商广告'); - } catch (e) { - debugPrint('failed to skip: $e'); - SmartDialog.showToast('广告跳过失败'); + int currentPos = position.inSeconds; + if (currentPos != _lastPos) { + _lastPos = currentPos; + 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) { + if (item.skipType == SkipType.alwaysSkip || + (item.skipType == SkipType.skipOnce && !item.hasSkipped)) { + try { + 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 }, ), + segmentList: _segmentProgressList, // 硬解 enableHA: enableHA.value, hwdec: hwdec.value, @@ -414,6 +537,9 @@ class VideoDetailController extends GetxController var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid); if (result['status']) { data = result['data']; + if (_enableSponsorBlock) { + await _sponsorBlock(); + } if (data.acceptDesc!.isNotEmpty && data.acceptDesc!.contains('试看')) { SmartDialog.showToast( '该视频为专属视频,仅提供试看', diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index e80b2b90..5462ac33 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; +import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -114,6 +115,8 @@ class PlPlayerController { Timer? _timerForGettingVolume; Timer? timerForTrackingMouse; + final RxList segmentList = [].obs; + // final Durations durations; static List> videoFitType = [ @@ -403,6 +406,7 @@ class PlPlayerController { // 初始化资源 Future setDataSource( DataSource dataSource, { + List? segmentList, bool autoplay = true, // 默认不循环 PlaylistMode looping = PlaylistMode.none, @@ -426,6 +430,7 @@ class PlPlayerController { }) async { try { this.dataSource = dataSource; + this.segmentList.value = segmentList ?? []; _autoPlay = autoplay; _looping = looping; // 初始化视频倍速 diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index a2808afe..3d8b5bcd 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:ui'; +import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart'; import 'package:PiliPalaX/http/loading_state.dart'; import 'package:PiliPalaX/pages/video/detail/introduction/controller.dart'; import 'package:PiliPalaX/utils/id_utils.dart'; @@ -970,6 +971,7 @@ class _PLVideoPlayerState extends State BottomControl( controller: widget.controller, buildBottomControl: buildBottomControl(), + segmentList: _.segmentList, ), ), ), @@ -1015,47 +1017,62 @@ class _PLVideoPlayerState extends State // label: '${(value / max * 100).round()}%', value: '${(value / max * 100).round()}%', // enabled: false, - child: ProgressBar( - progress: Duration(seconds: value), - buffered: Duration(seconds: buffer), - total: Duration(seconds: max), - progressBarColor: colorTheme, - baseBarColor: Colors.white.withOpacity(0.2), - bufferedBarColor: - Theme.of(context).colorScheme.primary.withOpacity(0.4), - timeLabelLocation: TimeLabelLocation.none, - thumbColor: colorTheme, - barHeight: 3.5, - thumbRadius: draggingFixedProgressBar.value ? 7 : 2.5, - // onDragStart: (duration) { - // draggingFixedProgressBar.value = true; - // feedBack(); - // _.onChangedSliderStart(); - // }, - // onDragUpdate: (duration) { - // double newProgress = 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) { - // draggingFixedProgressBar.value = false; - // _.onChangedSliderEnd(); - // _.onChangedSlider(duration.inSeconds.toDouble()); - // _.seekTo(Duration(seconds: duration.inSeconds), - // type: 'slider'); - // SemanticsService.announce( - // "${(duration.inSeconds / max * 100).round()}%", - // TextDirection.ltr); - // }, + child: Stack( + alignment: Alignment.center, + children: [ + ProgressBar( + progress: Duration(seconds: value), + buffered: Duration(seconds: buffer), + total: Duration(seconds: max), + progressBarColor: colorTheme, + baseBarColor: Colors.white.withOpacity(0.2), + bufferedBarColor: Theme.of(context) + .colorScheme + .primary + .withOpacity(0.4), + timeLabelLocation: TimeLabelLocation.none, + thumbColor: colorTheme, + barHeight: 3.5, + thumbRadius: draggingFixedProgressBar.value ? 7 : 2.5, + // onDragStart: (duration) { + // draggingFixedProgressBar.value = true; + // feedBack(); + // _.onChangedSliderStart(); + // }, + // onDragUpdate: (duration) { + // double newProgress = 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) { + // draggingFixedProgressBar.value = false; + // _.onChangedSliderEnd(); + // _.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( // position: Tween( diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index 39016d5a..51289b41 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; @@ -12,9 +13,11 @@ import '../../../common/widgets/audio_video_progress_bar.dart'; class BottomControl extends StatelessWidget implements PreferredSizeWidget { final PlPlayerController? controller; final List? buildBottomControl; + final List? segmentList; const BottomControl({ this.controller, this.buildBottomControl, + this.segmentList, Key? key, }) : super(key: key); @@ -49,44 +52,59 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { // label: '${(value / max * 100).round()}%', value: '${(value / max * 100).round()}%', // enabled: false, - child: ProgressBar( - progress: Duration(seconds: value), - buffered: Duration(seconds: buffer), - total: Duration(seconds: max), - progressBarColor: colorTheme, - baseBarColor: Colors.white.withOpacity(0.2), - bufferedBarColor: colorTheme.withOpacity(0.4), - timeLabelLocation: TimeLabelLocation.none, - thumbColor: colorTheme, - barHeight: 3.5, - thumbRadius: 7, - onDragStart: (duration) { - feedBack(); - _.onChangedSliderStart(); - }, - onDragUpdate: (duration) { - double newProgress = duration.timeStamp.inSeconds / max; - if ((newProgress - _lastAnnouncedValue).abs() > 0.02) { - _accessibilityDebounce?.cancel(); - _accessibilityDebounce = - Timer(const Duration(milliseconds: 200), () { + child: Stack( + alignment: Alignment.center, + children: [ + ProgressBar( + progress: Duration(seconds: value), + buffered: Duration(seconds: buffer), + total: Duration(seconds: max), + progressBarColor: colorTheme, + baseBarColor: Colors.white.withOpacity(0.2), + bufferedBarColor: colorTheme.withOpacity(0.4), + timeLabelLocation: TimeLabelLocation.none, + thumbColor: colorTheme, + barHeight: 3.5, + thumbRadius: 7, + onDragStart: (duration) { + feedBack(); + _.onChangedSliderStart(); + }, + onDragUpdate: (duration) { + double newProgress = + 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( - "${(newProgress * 100).round()}%", + "${(duration.inSeconds / max * 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( - "${(duration.inSeconds / max * 100).round()}%", - TextDirection.ltr); - }, + }, + ), + if (segmentList?.isNotEmpty == true) + CustomPaint( + size: Size(double.infinity, 3.5), + painter: SegmentProgressBar( + progress: 1, + segmentColors: segmentList!, + ), + ), + ], )), ); }, diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index b76fbaa5..1a303407 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,6 +1,7 @@ // ignore_for_file: must_be_immutable 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:flutter/material.dart'; import 'package:get/get.dart'; @@ -184,6 +185,7 @@ class Routes { CustomGetPage(name: '/subDetail', page: () => const SubDetailPage()), // 弹幕屏蔽管理 CustomGetPage(name: '/danmakuBlock', page: () => const DanmakuBlockPage()), + CustomGetPage(name: '/sponsorBlock', page: () => const SponsorBlockPage()), ]; } diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 47f16c22..78bd4881 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -1,7 +1,10 @@ import 'dart:convert'; import 'dart:io'; import 'dart:ui'; +import 'package:PiliPalaX/common/widgets/pair.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:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; @@ -17,6 +20,22 @@ class GStorage { static late final Box setting; static late final Box video; + static List> get blockSettings { + List list = setting.get( + SettingBoxKey.blockSettings, + defaultValue: List.generate(SegmentType.values.length, (_) => 1), + ); + return SegmentType.values + .map((item) => Pair( + first: item, + second: SkipType.values[list[item.index]], + )) + .toList(); + } + + static double get blockLimit => + setting.get(SettingBoxKey.blockLimit, defaultValue: 0.0); + static ThemeMode get themeMode { switch (setting.get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)) { @@ -189,6 +208,8 @@ class SettingBoxKey { disableLikeMsg = 'disableLikeMsg', defaultHomePage = 'defaultHomePage', enableSponsorBlock = 'enableSponsorBlock', + blockSettings = 'blockSettings', + blockLimit = 'blockLimit', // 弹幕相关设置 权重(云屏蔽) 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 字体粗细 danmakuWeight = 'danmakuWeight',