From be42ce97f84ec82695f1c9e3b3fed634f4b1adb0 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Sat, 4 Jan 2025 15:35:13 +0800 Subject: [PATCH] feat: sponsorblock: manual skip Signed-off-by: bggRGjQaUbCoE --- lib/pages/search/widgets/search_text.dart | 41 ++++---- lib/pages/video/detail/controller.dart | 118 +++++++++++++++++----- lib/pages/video/detail/view.dart | 26 ++++- 3 files changed, 139 insertions(+), 46 deletions(-) diff --git a/lib/pages/search/widgets/search_text.dart b/lib/pages/search/widgets/search_text.dart index 5eb04b1a..d46cce25 100644 --- a/lib/pages/search/widgets/search_text.dart +++ b/lib/pages/search/widgets/search_text.dart @@ -8,6 +8,7 @@ class SearchText extends StatelessWidget { final Color? bgColor; final Color? textColor; final TextAlign? textAlign; + final EdgeInsetsGeometry? padding; const SearchText({ super.key, @@ -18,6 +19,7 @@ class SearchText extends StatelessWidget { this.bgColor, this.textColor, this.textAlign, + this.padding, }); @override @@ -25,27 +27,24 @@ class SearchText extends StatelessWidget { return Material( color: bgColor ?? Theme.of(context).colorScheme.onInverseSurface, borderRadius: BorderRadius.circular(6), - child: Padding( - padding: EdgeInsets.zero, - child: InkWell( - onTap: () { - onTap?.call(text); - }, - onLongPress: () { - onLongPress?.call(text); - }, - borderRadius: BorderRadius.circular(6), - child: Padding( - padding: - const EdgeInsets.only(top: 5, bottom: 5, left: 11, right: 11), - child: Text( - text, - textAlign: textAlign, - style: TextStyle( - fontSize: fontSize, - color: - textColor ?? Theme.of(context).colorScheme.onSurfaceVariant, - ), + child: InkWell( + onTap: () { + onTap?.call(text); + }, + onLongPress: () { + onLongPress?.call(text); + }, + borderRadius: BorderRadius.circular(6), + child: Padding( + padding: padding ?? + const EdgeInsets.symmetric(horizontal: 11, vertical: 5), + child: Text( + text, + textAlign: textAlign, + style: TextStyle( + fontSize: fontSize, + color: + textColor ?? Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 170be27d..ee0236cd 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -13,6 +13,7 @@ import 'package:PiliPalaX/http/user.dart'; import 'package:PiliPalaX/models/video/later.dart'; import 'package:PiliPalaX/models/video/play/subtitle.dart'; import 'package:PiliPalaX/models/video_detail_res.dart'; +import 'package:PiliPalaX/pages/search/widgets/search_text.dart'; import 'package:PiliPalaX/pages/video/detail/introduction/controller.dart'; import 'package:PiliPalaX/pages/video/detail/related/controller.dart'; import 'package:PiliPalaX/pages/video/detail/reply/controller.dart'; @@ -21,6 +22,7 @@ import 'package:PiliPalaX/utils/extension.dart'; import 'package:canvas_danmaku/models/danmaku_content_item.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; +import 'package:easy_debounce/easy_throttle.dart'; import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -109,10 +111,16 @@ extension SegmentTypeExt on SegmentType { ][index]; } -enum SkipType { alwaysSkip, skipOnce, showOnly, disable } +enum SkipType { alwaysSkip, skipOnce, skipManually, showOnly, disable } extension SkipTypeExt on SkipType { - String get title => ['总是跳过', '跳过一次', '仅显示', '禁用'][index]; + String get title => [ + '总是跳过', + '跳过一次', + '手动跳过', + '仅显示', + '禁用', + ][index]; } class SegmentModel { @@ -122,14 +130,14 @@ class SegmentModel { required this.segmentType, required this.segment, required this.skipType, - required this.hasSkipped, + this.hasSkipped, }); // ignore: non_constant_identifier_names String UUID; SegmentType segmentType; Pair segment; SkipType skipType; - bool hasSkipped; + bool? hasSkipped; } class PostSegmentModel { @@ -471,6 +479,10 @@ class VideoDetailController extends GetxController Color _getColor(SegmentType segment) => _blockColor?[segment.index] ?? segment.color; + Timer? skipTimer; + late final listKey = GlobalKey(); + late final listData = []; + Future _vote(String uuid, int type) async { Request() .post( @@ -817,27 +829,24 @@ class VideoDetailController extends GetxController // 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)); - item.hasSkipped = true; - if (GStorage.blockToast) { - _showBlockToast('已跳过${item.segmentType.shortTitle}片段'); + if (item.skipType == SkipType.alwaysSkip) { + onSkip(item); + } else if (item.skipType == SkipType.skipOnce && + item.hasSkipped != true) { + item.hasSkipped = true; + onSkip(item); + } else if (item.skipType == SkipType.skipManually) { + listData.insert(0, item); + listKey.currentState?.insertItem(0); + skipTimer ??= + Timer.periodic(const Duration(milliseconds: 2500), (_) { + if (listData.isNotEmpty) { + onRemoveItem(listData.length - 1, listData.last); + } else { + skipTimer?.cancel(); + skipTimer = null; } - if (GStorage.blockTrack) { - Request().post( - '${GStorage.blockServer}/api/viewedVideoSponsorTime', - queryParameters: {'UUID': item.UUID}, - options: _options, - ); - } - } catch (e) { - debugPrint('failed to skip: $e'); - _showBlockToast('${item.segmentType.shortTitle}片段跳过失败'); - } + }); } break; } @@ -847,6 +856,67 @@ class VideoDetailController extends GetxController } } + void onRemoveItem(int index, SegmentModel item) { + EasyThrottle.throttle('onRemoveItem', const Duration(seconds: 1), () { + try { + listData.removeAt(index); + listKey.currentState?.removeItem( + index, + (context, animation) => buildItem(item, animation), + ); + } catch (_) {} + }); + } + + Widget buildItem(SegmentModel item, Animation animation) { + return Align( + alignment: Alignment.centerLeft, + child: SlideTransition( + position: Tween( + begin: Offset(-1, 0), + end: Offset(0, 0), + ).animate(animation), + child: Padding( + padding: const EdgeInsets.only(top: 5), + child: SearchText( + bgColor: Theme.of(Get.context!) + .colorScheme + .onInverseSurface + .withOpacity(0.7), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + fontSize: 13, + text: '跳过: ${item.segmentType.shortTitle}', + onTap: (_) { + onSkip(item); + onRemoveItem(listData.indexOf(item), item); + }, + ), + ), + ), + ); + } + + void onSkip(SegmentModel item) async { + try { + plPlayerController.danmakuController?.clear(); + await plPlayerController.videoPlayerController + ?.seek(Duration(seconds: item.segment.second)); + if (GStorage.blockToast) { + _showBlockToast('已跳过${item.segmentType.shortTitle}片段'); + } + if (GStorage.blockTrack) { + Request().post( + '${GStorage.blockServer}/api/viewedVideoSponsorTime', + queryParameters: {'UUID': item.UUID}, + options: _options, + ); + } + } catch (e) { + debugPrint('failed to skip: $e'); + _showBlockToast('${item.segmentType.shortTitle}片段跳过失败'); + } + } + /// 发送弹幕 void showShootDanmakuSheet() { final TextEditingController textController = TextEditingController(); diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index d1c5931b..2afe4a60 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -346,6 +346,10 @@ class _VideoDetailPageState extends State _listenerDetail?.cancel(); _listenerLoadingState?.cancel(); _listenerCid?.cancel(); + + videoDetailController.skipTimer?.cancel(); + videoDetailController.skipTimer = null; + WidgetsBinding.instance.removeObserver(this); if (!Get.previousRoute.startsWith('/video')) { ScreenBrightness().resetApplicationScreenBrightness(); @@ -1358,7 +1362,27 @@ class _VideoDetailPageState extends State ), ), manualPlayerWidget, - ] + ], + + if (videoDetailController.enableSponsorBlock) + Align( + alignment: Alignment(-0.9, 0.5), + child: SizedBox( + width: MediaQuery.textScalerOf(context).scale(120), + child: AnimatedList( + key: videoDetailController.listKey, + reverse: true, + shrinkWrap: true, + initialItemCount: videoDetailController.listData.length, + itemBuilder: (context, index, animation) { + return videoDetailController.buildItem( + videoDetailController.listData[index], + animation, + ); + }, + ), + ), + ), ], );