From 0cb07aef1ca941079caac0fe8dde2cdf95522f45 Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 8 Feb 2026 21:01:38 +0800 Subject: [PATCH] audio block Signed-off-by: dom --- lib/http/sponsor_block.dart | 15 +- lib/pages/audio/controller.dart | 64 ++- lib/pages/audio/view.dart | 48 +- lib/pages/sponsor_block/block_mixin.dart | 525 +++++++++++++++++++ lib/pages/video/controller.dart | 550 ++------------------ lib/pages/video/post_panel/view.dart | 2 +- lib/pages/video/view.dart | 18 +- lib/pages/video/widgets/header_control.dart | 3 +- lib/plugin/pl_player/controller.dart | 17 +- lib/services/shutdown_timer_service.dart | 1 + 10 files changed, 687 insertions(+), 556 deletions(-) create mode 100644 lib/pages/sponsor_block/block_mixin.dart diff --git a/lib/http/sponsor_block.dart b/lib/http/sponsor_block.dart index bb143f485..a73760f38 100644 --- a/lib/http/sponsor_block.dart +++ b/lib/http/sponsor_block.dart @@ -12,6 +12,7 @@ import 'package:PiliPlus/models_new/sponsor_block/user_info.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart' show kDebugMode; /// https://github.com/hanydd/BilibiliSponsorBlock/wiki/API abstract final class SponsorBlock { @@ -19,10 +20,12 @@ abstract final class SponsorBlock { static final options = Options( followRedirects: true, // https://github.com/hanydd/BilibiliSponsorBlock/wiki/API#1-%E5%85%AC%E7%94%A8%E5%8F%82%E6%95%B0 - headers: { - 'origin': Constants.appName, - 'x-ext-version': BuildConfig.versionName, - }, + headers: kDebugMode + ? null + : { + 'origin': Constants.appName, + 'x-ext-version': BuildConfig.versionName, + }, validateStatus: (status) => true, ); @@ -142,7 +145,9 @@ abstract final class SponsorBlock { 'videoID': bvid, 'cid': cid.toString(), 'userID': Pref.blockUserID, - 'userAgent': '${Constants.appName}/${BuildConfig.versionName}', + 'userAgent': kDebugMode + ? Constants.userAgent + : '${Constants.appName}/${BuildConfig.versionName}', 'videoDuration': videoDuration, 'segments': segments .map( diff --git a/lib/pages/audio/controller.dart b/lib/pages/audio/controller.dart index df0c01727..6e669bb3a 100644 --- a/lib/pages/audio/controller.dart +++ b/lib/pages/audio/controller.dart @@ -19,6 +19,7 @@ import 'package:PiliPlus/pages/common/common_intro_controller.dart' show FavMixin; import 'package:PiliPlus/pages/dynamics_repost/view.dart'; import 'package:PiliPlus/pages/main_reply/view.dart'; +import 'package:PiliPlus/pages/sponsor_block/block_mixin.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/widgets/triple_mixin.dart'; import 'package:PiliPlus/pages/video/pay_coins/view.dart'; @@ -43,17 +44,24 @@ import 'package:get/get.dart'; import 'package:media_kit/media_kit.dart'; class AudioController extends GetxController - with GetTickerProviderStateMixin, TripleMixin, FavMixin { + with + GetTickerProviderStateMixin, + TripleMixin, + FavMixin, + BlockConfigMixin, + BlockMixin { late Int64 id; late Int64 oid; late List subId; late int itemType; Int64? extraId; late final PlaylistSource from; - late final isVideo = itemType == 1; + @override + late final bool isUgc = itemType == 1; final Rx audioItem = Rx(null); + @override Player? player; late int cacheAudioQa; @@ -109,6 +117,7 @@ class AudioController extends GetxController final String? audioUrl = args['audioUrl']; final hasAudioUrl = audioUrl != null; if (hasAudioUrl) { + _querySponsorBlock(); _onOpenMedia( audioUrl, ua: UaType.pc.ua, @@ -130,6 +139,16 @@ class AudioController extends GetxController vsync: this, duration: const Duration(milliseconds: 200), ); + + if (shutdownTimerService.isActive) { + shutdownTimerService + ..onPause = onPause + ..isPlaying = isPlaying; + } + } + + bool isPlaying() { + return player?.state.playing ?? false; } Future? onPlay() { @@ -203,7 +222,19 @@ class AudioController extends GetxController } } + @pragma('vm:notify-debugger-on-exception') + void _querySponsorBlock() { + if (isUgc && enableSponsorBlock) { + try { + final bvid = IdUtils.av2bv(oid.toInt()); + final cid = subId.first.toInt(); + querySponsorBlock(bvid: bvid, cid: cid); + } catch (_) {} + } + } + Future _queryPlayUrl() async { + _querySponsorBlock(); final res = await AudioGrpc.audioPlayUrl( itemType: itemType, oid: oid, @@ -475,12 +506,12 @@ class AudioController extends GetxController void showReply() { MainReplyPage.toMainReplyPage( oid: oid.toInt(), - replyType: isVideo ? 1 : 14, + replyType: isUgc ? 1 : 14, ); } void actionShareVideo(BuildContext context) { - final audioUrl = isVideo + final audioUrl = isUgc ? '${HttpString.baseUrl}/video/${IdUtils.av2bv(oid.toInt())}' : '${HttpString.baseUrl}/audio/au$oid'; showDialog( @@ -552,7 +583,7 @@ class AudioController extends GetxController useSafeArea: true, builder: (context) => RepostPanel( rid: oid.toInt(), - dynType: isVideo ? 8 : 256, + dynType: isUgc ? 8 : 256, pic: arc.cover, title: arc.title, uname: owner.name, @@ -561,7 +592,7 @@ class AudioController extends GetxController } }, ), - if (isVideo) + if (isUgc) ListTile( dense: true, title: const Text( @@ -679,7 +710,7 @@ class AudioController extends GetxController } @override - (Object, int) get getFavRidType => (oid, isVideo ? 2 : 12); + (Object, int) get getFavRidType => (oid, isUgc ? 2 : 12); @override void updateFavCount(int count) { @@ -716,6 +747,25 @@ class AudioController extends GetxController } } + @override + BlockConfigMixin get blockConfig => this; + + @override + int get currPosInMilliseconds => position.value.inMilliseconds; + + @override + Future? seekTo(Duration duration, {required bool isSeek}) => + onSeek(duration); + + @override + int? get timeLength => duration.value.inMilliseconds; + + @override + bool get autoPlay => true; + + @override + bool get preInitPlayer => true; + @override void onClose() { shutdownTimerService diff --git a/lib/pages/audio/view.dart b/lib/pages/audio/view.dart index dafe6bbea..7c5054ac4 100644 --- a/lib/pages/audio/view.dart +++ b/lib/pages/audio/view.dart @@ -6,6 +6,7 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/progress_bar/audio_video_progress_bar.dart'; +import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart'; import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/common/image_type.dart'; @@ -29,6 +30,7 @@ import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart' hide DraggableScrollableSheet; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class AudioPage extends StatefulWidget { const AudioPage({super.key}); @@ -87,6 +89,16 @@ class _AudioPageState extends State { resizeToAvoidBottomInset: false, appBar: AppBar( actions: [ + if (_controller.isUgc && _controller.enableSponsorBlock) + Obx(() { + if (_controller.segmentProgressList.isNotEmpty) { + return IconButton( + onPressed: _controller.showSBDetail, + icon: const Icon(MdiIcons.advertisements, size: 22), + ); + } + return const SizedBox.shrink(); + }), Builder( builder: (context) { return PopupMenuButton( @@ -107,14 +119,14 @@ class _AudioPageState extends State { tooltip: '定时关闭', onPressed: () => shutdownTimerService ..onPause ??= _controller.onPause - ..isPlaying ??= (() => _controller.player?.state.playing ?? false) + ..isPlaying ??= _controller.isPlaying ..showScheduleExitDialog( context, isFullScreen: false, ), icon: const Icon(Icons.schedule, size: 22), ), - if (_controller.isVideo) + if (_controller.isUgc) IconButton( tooltip: '更多', onPressed: _showMore, @@ -754,7 +766,7 @@ class _AudioPageState extends State { final baseBarColor = colorScheme.brightness.isDark ? const Color(0x33FFFFFF) : const Color(0x33999999); - return Obx( + Widget child = Obx( () => ProgressBar( progress: _controller.position.value, total: _controller.duration.value, @@ -770,6 +782,30 @@ class _AudioPageState extends State { onSeek: _onSeek, ), ); + if (_controller.isUgc && _controller.enableSponsorBlock) { + child = Stack( + children: [ + child, + Positioned( + left: 0, + right: 0, + bottom: 3.5, + child: Obx( + () { + if (_controller.segmentProgressList.isNotEmpty) { + return SegmentProgressBar( + height: 5, + segments: _controller.segmentProgressList, + ); + } + return const SizedBox(); + }, + ), + ), + ], + ); + } + return child; } Widget _buildDuration(ColorScheme colorScheme) { @@ -883,10 +919,8 @@ class _AudioPageState extends State { const SizedBox(height: 12), SelectableText( audioItem.arc.title, - style: const TextStyle( - height: 1.7, - fontSize: 16, - ), + style: const TextStyle(height: 1.7, fontSize: 16), + scrollPhysics: const NeverScrollableScrollPhysics(), ), const SizedBox(height: 12), if (audioItem.owner.hasName()) ...[ diff --git a/lib/pages/sponsor_block/block_mixin.dart b/lib/pages/sponsor_block/block_mixin.dart new file mode 100644 index 000000000..b4337b974 --- /dev/null +++ b/lib/pages/sponsor_block/block_mixin.dart @@ -0,0 +1,525 @@ +import 'dart:async' show StreamSubscription, Timer; + +import 'package:PiliPlus/common/widgets/pair.dart'; +import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/sponsor_block.dart'; +import 'package:PiliPlus/models/common/sponsor_block/segment_model.dart'; +import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; +import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart'; +import 'package:PiliPlus/models_new/sponsor_block/segment_item.dart'; +import 'package:PiliPlus/utils/duration_utils.dart'; +import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:media_kit/media_kit.dart'; + +mixin BlockConfigMixin { + late final pgcSkipType = Pref.pgcSkipType; + late final enablePgcSkip = pgcSkipType != SkipType.disable; + late final bool enableSponsorBlock = Pref.enableSponsorBlock; + late final bool enableBlock = enableSponsorBlock || enablePgcSkip; + late final double blockLimit = Pref.blockLimit; + late final blockSettings = Pref.blockSettings; + late final List blockColor = Pref.blockColor; + late final Set enableList = blockSettings + .where((item) => item.second != SkipType.disable) + .map((item) => item.first.name) + .toSet(); +} + +mixin BlockMixin on GetxController { + int? _lastBlockPos; + BlockConfigMixin get blockConfig; + StreamSubscription? _blockListener; + StreamSubscription? get blockListener => _blockListener; + Color _getBlockColor(SegmentType segment) => + blockConfig.blockColor[segment.index]; + late final List _segmentList = []; + late final RxList segmentProgressList = [].obs; + + Timer? _skipTimer; + late final listKey = GlobalKey(); + late final List listData = []; + + RxString? get videoLabel => null; + Player? get player; + bool get autoPlay; + int? get timeLength; + bool get preInitPlayer; + int get currPosInMilliseconds; + bool get isFullScreen => false; + + bool get isUgc; + late final isBlock = isUgc || !blockConfig.enablePgcSkip; + + Future querySponsorBlock({ + required String bvid, + required int cid, + }) async { + resetBlock(); + + final result = await SponsorBlock.getSkipSegments(bvid: bvid, cid: cid); + switch (result) { + case Success>(:final response): + handleSBData(response); + case Error(:final code) when code != 404: + if (kDebugMode) { + result.toast(); + } + default: + } + } + + void initSkip() { + if (isClosed) return; + if (_segmentList.isNotEmpty) { + _blockListener?.cancel(); + _blockListener = player?.stream.position.listen((position) { + int currentPos = position.inSeconds; + if (currentPos != _lastBlockPos) { + _lastBlockPos = currentPos; + final msPos = currentPos * 1000; + for (SegmentModel item in _segmentList) { + // if (kDebugMode) { + // debugPrint( + // '${position.inSeconds},,${item.segment.first},,${item.segment.second},,${item.skipType.name},,${item.hasSkipped}'); + // } + if (msPos <= item.segment.first && + item.segment.first <= msPos + 1000) { + switch (item.skipType) { + case SkipType.alwaysSkip: + onSkip(item, isSeek: false); + break; + case SkipType.skipOnce: + if (!item.hasSkipped) { + item.hasSkipped = true; + onSkip(item, isSeek: false); + } + break; + case SkipType.skipManually: + onAddItem(item); + break; + default: + break; + } + break; + } + } + } + }); + } + } + + Future handleSBData(List list) async { + if (list.isNotEmpty) { + try { + Future? future; + final duration = list.first.videoDuration ?? timeLength!; + // segmentList + _segmentList.addAll( + list + .where( + (item) => + blockConfig.enableList.contains(item.category) && + item.segment[1] >= item.segment[0], + ) + .map( + (item) { + final segmentType = SegmentType.values.byName(item.category); + if (item.segment[0] == 0 && item.segment[1] == 0) { + videoLabel?.value += + '${videoLabel!.value.isNotEmpty ? '/' : ''}${segmentType.title}'; + } + SkipType skipType; + if (isBlock) { + skipType = + blockConfig.blockSettings[segmentType.index].second; + if (skipType != SkipType.showOnly) { + if (item.segment[1] == item.segment[0] || + item.segment[1] - item.segment[0] < + blockConfig.blockLimit) { + skipType = SkipType.showOnly; + } + } + } else { + skipType = blockConfig.pgcSkipType; + } + + final segmentModel = SegmentModel( + UUID: item.uuid, + segmentType: segmentType, + segment: Pair( + first: item.segment[0], + second: item.segment[1], + ), + skipType: skipType, + ); + + if (_blockListener == null && autoPlay && player != null) { + final currPos = currPosInMilliseconds; + + if (currPos >= segmentModel.segment.first && + currPos < segmentModel.segment.second) { + _lastBlockPos = currPos; + + switch (segmentModel.skipType) { + case SkipType.alwaysSkip: + case SkipType.skipOnce: + segmentModel.hasSkipped = true; + if (player!.state.playing) { + future = onSkip( + segmentModel, + ); + } else { + player!.stream.playing.firstWhere((e) { + if (e) { + future = onSkip(segmentModel); + return true; + } + return false; + }); + } + break; + case SkipType.skipManually: + onAddItem(segmentModel); + break; + default: + break; + } + } + } + + return segmentModel; + }, + ), + ); + + // _segmentProgressList + segmentProgressList.addAll( + _segmentList.map((e) { + double start = (e.segment.first / duration).clamp(0.0, 1.0); + double end = (e.segment.second / duration).clamp(0.0, 1.0); + return Segment( + start: start, + end: end, + color: _getBlockColor(e.segmentType), + ); + }), + ); + + if (_blockListener == null && (autoPlay || preInitPlayer)) { + await future; + initSkip(); + } + } catch (e) { + if (kDebugMode) debugPrint('failed to parse sponsorblock: $e'); + } + } + } + + void onAddItem(dynamic item) { + if (listData.contains(item)) return; + listData.insert(0, item); + listKey.currentState?.insertItem(0); + _skipTimer ??= Timer.periodic(const Duration(seconds: 4), (_) { + if (listData.isNotEmpty) { + onRemoveItem(listData.length - 1, listData.last); + } + }); + } + + void onRemoveItem(int index, item) { + EasyThrottle.throttle( + 'onRemoveItem', + const Duration(milliseconds: 500), + () { + try { + listData.removeAt(index); + if (listData.isEmpty) { + _stopSkipTimer(); + } + listKey.currentState?.removeItem( + index, + (context, animation) => buildItem(item, animation), + ); + } catch (_) {} + }, + ); + } + + Widget buildItem(dynamic item, Animation animation) => + throw UnimplementedError(); + + void _stopSkipTimer() { + if (_skipTimer != null) { + _skipTimer!.cancel(); + _skipTimer = null; + } + } + + Future? seekTo(Duration duration, {required bool isSeek}); + + Future onSkip( + SegmentModel item, { + bool isSkip = true, + bool isSeek = true, + }) async { + try { + await seekTo( + Duration(milliseconds: item.segment.second), + isSeek: isSeek, + ); + if (isSkip) { + if (autoPlay && Pref.blockToast) { + _showBlockToast('已跳过${item.segmentType.shortTitle}片段'); + } + if (isBlock && Pref.blockTrack) { + SponsorBlock.viewedVideoSponsorTime(item.UUID); + } + } else { + _showBlockToast('已跳至${item.segmentType.shortTitle}'); + } + } catch (e) { + if (kDebugMode) debugPrint('failed to skip: $e'); + if (isSkip) { + _showBlockToast('${item.segmentType.shortTitle}片段跳过失败'); + } else { + _showBlockToast('跳转失败'); + } + } + } + + void _showBlockToast(String msg) { + SmartDialog.showToast( + msg, + alignment: isFullScreen ? const Alignment(0, 0.7) : null, + ); + } + + void _showVoteDialog(SegmentModel segment) { + showDialog( + context: Get.context!, + builder: (context) => AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + dense: true, + title: const Text('赞成票', style: TextStyle(fontSize: 14)), + onTap: () { + Get.back(); + _doVote(segment.UUID, 1); + }, + ), + ListTile( + dense: true, + title: const Text('反对票', style: TextStyle(fontSize: 14)), + onTap: () { + Get.back(); + _doVote(segment.UUID, 0); + }, + ), + ListTile( + dense: true, + title: const Text('更改类别', style: TextStyle(fontSize: 14)), + onTap: () { + Get.back(); + _showCategoryDialog(segment); + }, + ), + ], + ), + ), + ), + ); + } + + void _doVote(String uuid, int type) => SponsorBlock.voteOnSponsorTime( + uuid: uuid, + type: type, + ).then((i) => SmartDialog.showToast(i.isSuccess ? '投票成功' : '投票失败: $i')); + + void _showCategoryDialog(SegmentModel segment) { + showDialog( + context: Get.context!, + builder: (context) => 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(); + SponsorBlock.voteOnSponsorTime( + uuid: segment.UUID, + category: item, + ).then((i) { + SmartDialog.showToast( + '类别更改${i.isSuccess ? '成功' : '失败: $i'}', + ); + }); + }, + title: Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _getBlockColor(item), + ), + ), + style: const TextStyle(fontSize: 14, height: 1), + ), + TextSpan( + text: ' ${item.title}', + style: const TextStyle(fontSize: 14, height: 1), + ), + ], + ), + ), + ), + ) + .toList(), + ), + ), + ), + ); + } + + void showSBDetail() { + showDialog( + context: Get.context!, + builder: (context) => 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(); + if (isBlock) { + _showVoteDialog(item); + } + }, + dense: true, + title: Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _getBlockColor(item.segmentType), + ), + ), + style: const TextStyle(fontSize: 14, height: 1), + ), + TextSpan( + text: ' ${item.segmentType.title}', + style: const TextStyle(fontSize: 14, height: 1), + ), + ], + ), + ), + contentPadding: const EdgeInsets.only(left: 16, right: 8), + subtitle: Text( + '${DurationUtils.formatDuration(item.segment.first / 1000)} 至 ${DurationUtils.formatDuration(item.segment.second / 1000)}', + style: const TextStyle(fontSize: 13), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + item.skipType.label, + style: const TextStyle(fontSize: 13), + ), + if (item.segment.second != 0) + SizedBox( + width: 36, + height: 36, + child: IconButton( + tooltip: item.skipType == SkipType.showOnly + ? '跳至此片段' + : '跳过此片段', + onPressed: () { + Get.back(); + onSkip( + item, + isSkip: item.skipType != SkipType.showOnly, + isSeek: false, + ); + }, + style: IconButton.styleFrom( + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + icon: Icon( + item.skipType == SkipType.showOnly + ? Icons.my_location + : MdiIcons.debugStepOver, + size: 18, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + ) + else + const SizedBox(width: 10), + ], + ), + ), + ) + .toList(), + ), + ), + ), + ); + } + + void cancelBlockListener() { + if (_blockListener != null) { + _blockListener!.cancel(); + _blockListener = null; + } + } + + void resetBlock() { + cancelBlockListener(); + _lastBlockPos = null; + videoLabel?.value = ''; + _segmentList.clear(); + segmentProgressList.clear(); + } + + @override + void onClose() { + _stopSkipTimer(); + if (blockConfig.enableBlock) { + resetBlock(); + } + super.onClose(); + } +} diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index cb26cdf24..7c220fec7 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -11,7 +11,6 @@ import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/http/sponsor_block.dart'; import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/video.dart'; @@ -21,7 +20,6 @@ import 'package:PiliPlus/models/common/sponsor_block/action_type.dart'; import 'package:PiliPlus/models/common/sponsor_block/post_segment_model.dart'; import 'package:PiliPlus/models/common/sponsor_block/segment_model.dart'; import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; -import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart'; import 'package:PiliPlus/models/common/video/audio_quality.dart'; import 'package:PiliPlus/models/common/video/source_type.dart'; import 'package:PiliPlus/models/common/video/subtitle_pref_type.dart'; @@ -32,7 +30,6 @@ import 'package:PiliPlus/models/video/play/url.dart'; import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart'; import 'package:PiliPlus/models_new/media_list/media_list.dart'; import 'package:PiliPlus/models_new/pgc/pgc_info_model/result.dart'; -import 'package:PiliPlus/models_new/sponsor_block/segment_item.dart'; import 'package:PiliPlus/models_new/video/video_detail/data.dart'; import 'package:PiliPlus/models_new/video/video_detail/episode.dart' as ugc; import 'package:PiliPlus/models_new/video/video_detail/page.dart'; @@ -42,6 +39,7 @@ import 'package:PiliPlus/models_new/video/video_stein_edgeinfo/data.dart'; import 'package:PiliPlus/pages/audio/view.dart'; import 'package:PiliPlus/pages/common/publish/publish_route.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; +import 'package:PiliPlus/pages/sponsor_block/block_mixin.dart'; import 'package:PiliPlus/pages/video/download_panel/view.dart'; import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart'; @@ -56,7 +54,6 @@ import 'package:PiliPlus/plugin/pl_player/models/heart_beat_type.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; import 'package:PiliPlus/services/download/download_service.dart'; import 'package:PiliPlus/utils/accounts.dart'; -import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; @@ -67,7 +64,6 @@ import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/video_utils.dart'; -import 'package:easy_debounce/easy_throttle.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; @@ -75,11 +71,10 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:media_kit/media_kit.dart'; class VideoDetailController extends GetxController - with GetTickerProviderStateMixin { + with GetTickerProviderStateMixin, BlockMixin { /// 路由传参 late final Map args; late String bvid; @@ -93,6 +88,7 @@ class VideoDetailController extends GetxController // 视频类型 默认投稿视频 late final VideoType videoType; + @override late final isUgc = videoType == VideoType.ugc; VideoType? _actualVideoType; @@ -118,7 +114,7 @@ class VideoDetailController extends GetxController late VideoDecodeFormatType currentDecodeFormats; // 是否开始自动播放 存在多p的情况下,第二p需要为true - final RxBool autoPlay = Pref.autoPlayEnable.obs; + final RxBool _autoPlay = Pref.autoPlayEnable.obs; final videoPlayerKey = GlobalKey(); final childKey = GlobalKey(); @@ -165,7 +161,6 @@ class VideoDetailController extends GetxController late final RxInt seasonIndex = 0.obs; PlayerStatus? playerStatus; - StreamSubscription? positionSubscription; late final scrollKey = GlobalKey(); late final RxBool isVertical = false.obs; @@ -487,459 +482,32 @@ class VideoDetailController extends GetxController bool get showVideoSheet => (!horizontalScreen && !isPortrait) || plPlayerController.isDesktopPip; - late final _isBlock = isUgc || !plPlayerController.enablePgcSkip; - int? _lastPos; - late final List postList = []; - late final List segmentList = []; - late final RxList segmentProgressList = [].obs; + @override + late final RxString videoLabel = ''.obs; + @override + int? get timeLength => data.timeLength; + @override + BlockConfigMixin get blockConfig => plPlayerController; + @override + Player? get player => plPlayerController.videoPlayerController; + @override + bool get isFullScreen => plPlayerController.isFullScreen.value; + @override + bool get autoPlay => _autoPlay.value; + set autoPlay(bool value) => _autoPlay.value = value; + @override + bool get preInitPlayer => plPlayerController.preInitPlayer; + @override + int get currPosInMilliseconds => + defaultST?.inMilliseconds ?? + plPlayerController.position.value.inMilliseconds; + @override + Future seekTo(Duration duration, {required bool isSeek}) => + plPlayerController.seekTo(duration, isSeek: isSeek); - Color _getColor(SegmentType segment) => - plPlayerController.blockColor[segment.index]; - late RxString videoLabel = ''.obs; - Timer? skipTimer; - late final listKey = GlobalKey(); - late final List listData = []; - - void _vote(String uuid, int type) { - SponsorBlock.voteOnSponsorTime( - uuid: uuid, - type: type, - ).then((i) => SmartDialog.showToast(i.isSuccess ? '投票成功' : '投票失败: $i')); - } - - void _showCategoryDialog(BuildContext context, SegmentModel segment) { - showDialog( - context: context, - builder: (context) => 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(); - SponsorBlock.voteOnSponsorTime( - uuid: segment.UUID, - category: item, - ).then((i) { - SmartDialog.showToast( - '类别更改${i.isSuccess ? '成功' : '失败: $i'}', - ); - }); - }, - title: Text.rich( - TextSpan( - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Container( - height: 10, - width: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: _getColor(item), - ), - ), - style: const TextStyle(fontSize: 14, height: 1), - ), - TextSpan( - text: ' ${item.title}', - style: const TextStyle(fontSize: 14, height: 1), - ), - ], - ), - ), - ), - ) - .toList(), - ), - ), - ), - ); - } - - void _showVoteDialog(BuildContext context, SegmentModel segment) { - showDialog( - context: context, - builder: (context) => AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - title: const Text( - '赞成票', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - _vote(segment.UUID, 1); - }, - ), - ListTile( - dense: true, - title: const Text( - '反对票', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - _vote(segment.UUID, 0); - }, - ), - ListTile( - dense: true, - title: const Text( - '更改类别', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - _showCategoryDialog(context, segment); - }, - ), - ], - ), - ), - ), - ); - } - - void showSBDetail(BuildContext context) { - showDialog( - context: context, - builder: (context) => 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(); - if (_isBlock) { - _showVoteDialog(context, item); - } - }, - dense: true, - title: Text.rich( - TextSpan( - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Container( - height: 10, - width: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: _getColor(item.segmentType), - ), - ), - style: const TextStyle(fontSize: 14, height: 1), - ), - TextSpan( - text: ' ${item.segmentType.title}', - style: const TextStyle(fontSize: 14, height: 1), - ), - ], - ), - ), - contentPadding: const EdgeInsets.only(left: 16, right: 8), - subtitle: Text( - '${DurationUtils.formatDuration(item.segment.first / 1000)} 至 ${DurationUtils.formatDuration(item.segment.second / 1000)}', - style: const TextStyle(fontSize: 13), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - item.skipType.label, - style: const TextStyle(fontSize: 13), - ), - if (item.segment.second != 0) - SizedBox( - width: 36, - height: 36, - child: IconButton( - tooltip: item.skipType == SkipType.showOnly - ? '跳至此片段' - : '跳过此片段', - onPressed: () { - Get.back(); - onSkip( - item, - isSkip: item.skipType != SkipType.showOnly, - isSeek: false, - ); - }, - style: IconButton.styleFrom( - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - icon: Icon( - item.skipType == SkipType.showOnly - ? Icons.my_location - : MdiIcons.debugStepOver, - size: 18, - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - ) - else - const SizedBox(width: 10), - ], - ), - ), - ) - .toList(), - ), - ), - ), - ); - } - - void _showBlockToast(String msg) { - SmartDialog.showToast( - msg, - alignment: plPlayerController.isFullScreen.value - ? const Alignment(0, 0.7) - : null, - ); - } - - Future _querySponsorBlock() async { - positionSubscription?.cancel(); - positionSubscription = null; - videoLabel.value = ''; - segmentList.clear(); - segmentProgressList.clear(); - - final result = await SponsorBlock.getSkipSegments( - bvid: bvid, - cid: cid.value, - ); - switch (result) { - case Success>(:final response): - handleSBData(response); - case Error(:final code) when code != 404: - if (kDebugMode) { - result.toast(); - } - default: - } - } - - Future handleSBData(List list) async { - if (list.isNotEmpty) { - try { - Future? future; - final duration = list.first.videoDuration ?? data.timeLength!; - // segmentList - segmentList.addAll( - list - .where( - (item) => - plPlayerController.enableList.contains(item.category) && - item.segment[1] >= item.segment[0], - ) - .map( - (item) { - final segmentType = SegmentType.values.byName(item.category); - if (item.segment[0] == 0 && item.segment[1] == 0) { - videoLabel.value += - '${videoLabel.value.isNotEmpty ? '/' : ''}${segmentType.title}'; - } - SkipType skipType; - if (_isBlock) { - skipType = plPlayerController - .blockSettings[segmentType.index] - .second; - if (skipType != SkipType.showOnly) { - if (item.segment[1] == item.segment[0] || - item.segment[1] - item.segment[0] < - plPlayerController.blockLimit) { - skipType = SkipType.showOnly; - } - } - } else { - skipType = Pref.pgcSkipType; - } - - final segmentModel = SegmentModel( - UUID: item.uuid, - segmentType: segmentType, - segment: Pair( - first: item.segment[0], - second: item.segment[1], - ), - skipType: skipType, - ); - - if (positionSubscription == null && - autoPlay.value && - plPlayerController.videoPlayerController != null) { - final currPost = - defaultST?.inMilliseconds ?? - plPlayerController.position.value.inMilliseconds; - - if (currPost >= segmentModel.segment.first && - currPost < segmentModel.segment.second) { - _lastPos = currPost; - - switch (segmentModel.skipType) { - case SkipType.alwaysSkip: - case SkipType.skipOnce: - segmentModel.hasSkipped = true; - final videoPlayerController = - plPlayerController.videoPlayerController!; - if (videoPlayerController.state.playing) { - future = onSkip( - segmentModel, - ); - } else { - videoPlayerController.stream.playing.firstWhere(( - e, - ) { - if (e) { - future = onSkip( - segmentModel, - ); - return true; - } - return false; - }); - } - - break; - case SkipType.skipManually: - onAddItem(segmentModel); - break; - default: - break; - } - } - } - - return segmentModel; - }, - ), - ); - - // _segmentProgressList - segmentProgressList.addAll( - segmentList.map((e) { - double start = (e.segment.first / duration).clamp(0.0, 1.0); - double end = (e.segment.second / duration).clamp(0.0, 1.0); - return Segment( - start: start, - end: end, - color: _getColor(e.segmentType), - ); - }), - ); - - if (positionSubscription == null && - (autoPlay.value || plPlayerController.preInitPlayer)) { - await future; - initSkip(); - } - } catch (e) { - if (kDebugMode) debugPrint('failed to parse sponsorblock: $e'); - } - } - } - - void initSkip() { - if (isClosed) return; - if (segmentList.isNotEmpty) { - positionSubscription?.cancel(); - positionSubscription = plPlayerController - .videoPlayerController - ?.stream - .position - .listen((position) { - int currentPos = position.inSeconds; - if (currentPos != _lastPos) { - _lastPos = currentPos; - final msPos = currentPos * 1000; - for (SegmentModel item in segmentList) { - // if (kDebugMode) { - // debugPrint( - // '${position.inSeconds},,${item.segment.first},,${item.segment.second},,${item.skipType.name},,${item.hasSkipped}'); - // } - if (msPos <= item.segment.first && - item.segment.first <= msPos + 1000) { - switch (item.skipType) { - case SkipType.alwaysSkip: - onSkip(item, isSeek: false); - break; - case SkipType.skipOnce: - if (!item.hasSkipped) { - item.hasSkipped = true; - onSkip(item, isSeek: false); - } - break; - case SkipType.skipManually: - onAddItem(item); - break; - default: - break; - } - break; - } - } - } - }); - } - } - - void onAddItem(dynamic item) { - if (listData.contains(item)) return; - listData.insert(0, item); - listKey.currentState?.insertItem(0); - skipTimer ??= Timer.periodic(const Duration(seconds: 4), (_) { - if (listData.isNotEmpty) { - onRemoveItem(listData.length - 1, listData.last); - } - }); - } - - void cancelSkipTimer() { - skipTimer?.cancel(); - skipTimer = null; - } - - void onRemoveItem(int index, item) { - EasyThrottle.throttle( - 'onRemoveItem', - const Duration(milliseconds: 500), - () { - try { - listData.removeAt(index); - if (listData.isEmpty) { - cancelSkipTimer(); - } - listKey.currentState?.removeItem( - index, - (context, animation) => buildItem(item, animation), - ); - } catch (_) {} - }, - ); - } + @override Widget buildItem(dynamic item, Animation animation) { final theme = Get.theme; return Align( @@ -995,36 +563,6 @@ class VideoDetailController extends GetxController ); } - Future onSkip( - SegmentModel item, { - bool isSkip = true, - bool isSeek = true, - }) async { - try { - await plPlayerController.seekTo( - Duration(milliseconds: item.segment.second), - isSeek: isSeek, - ); - if (isSkip) { - if (autoPlay.value && Pref.blockToast) { - _showBlockToast('已跳过${item.segmentType.shortTitle}片段'); - } - if (_isBlock && Pref.blockTrack) { - SponsorBlock.viewedVideoSponsorTime(item.UUID); - } - } else { - _showBlockToast('已跳至${item.segmentType.shortTitle}'); - } - } catch (e) { - if (kDebugMode) debugPrint('failed to skip: $e'); - if (isSkip) { - _showBlockToast('${item.segmentType.shortTitle}片段跳过失败'); - } else { - _showBlockToast('跳转失败'); - } - } - } - ({int mode, int fontSize, Color color})? dmConfig; String? savedDanmaku; @@ -1035,7 +573,7 @@ class VideoDetailController extends GetxController return; } final isPlaying = - autoPlay.value && plPlayerController.playerStatus.isPlaying; + _autoPlay.value && plPlayerController.playerStatus.isPlaying; if (isPlaying) { await plPlayerController.pause(); } @@ -1095,7 +633,7 @@ class VideoDetailController extends GetxController void updatePlayer() { final currentVideoQa = this.currentVideoQa.value; if (currentVideoQa == null) return; - autoPlay.value = true; + _autoPlay.value = true; playedTime = plPlayerController.position.value; plPlayerController ..removeListeners() @@ -1122,7 +660,7 @@ class VideoDetailController extends GetxController } Future? _initPlayerIfNeeded() { - if (autoPlay.value || + if (_autoPlay.value || (plPlayerController.preInitPlayer && !plPlayerController.processing) && (isFileSource ? true @@ -1167,7 +705,7 @@ class VideoDetailController extends GetxController aid: aid, bvid: bvid, cid: cid.value, - autoplay: autoplay ?? autoPlay.value, + autoplay: autoplay ?? _autoPlay.value, epid: isUgc ? null : epId, seasonId: isUgc ? null : seasonId, pgcType: isUgc ? null : pgcType, @@ -1233,8 +771,8 @@ class VideoDetailController extends GetxController return; } isQuerying = true; - if (plPlayerController.enableSponsorBlock && _isBlock && !fromReset) { - _querySponsorBlock(); + if (plPlayerController.enableSponsorBlock && isBlock && !fromReset) { + querySponsorBlock(bvid: bvid, cid: cid.value); } if (plPlayerController.cacheVideoQa == null) { final isWiFi = await Utils.isWiFi; @@ -1279,8 +817,7 @@ class VideoDetailController extends GetxController if (!isUgc && !fromReset && plPlayerController.enablePgcSkip) { if (data.clipInfoList case final clipInfoList?) { - positionSubscription?.cancel(); - positionSubscription = null; + resetBlock(); handleSBData(clipInfoList); } } @@ -1313,7 +850,7 @@ class VideoDetailController extends GetxController } if (data.dash == null) { SmartDialog.showToast('视频资源不存在'); - autoPlay.value = false; + _autoPlay.value = false; videoState.value = const Error('视频资源不存在'); if (plPlayerController.isFullScreen.value) { plPlayerController.toggleFullScreen(false); @@ -1408,7 +945,7 @@ class VideoDetailController extends GetxController } await _initPlayerIfNeeded(); } else { - autoPlay.value = false; + _autoPlay.value = false; videoState.value = result..toast(); if (plPlayerController.isFullScreen.value) { plPlayerController.toggleFullScreen(false); @@ -1417,6 +954,7 @@ class VideoDetailController extends GetxController isQuerying = false; } + late final List postList = []; void onBlock(BuildContext context) { if (postList.isEmpty) { postList.add( @@ -1656,9 +1194,6 @@ class VideoDetailController extends GetxController @override void onClose() { - cancelSkipTimer(); - positionSubscription?.cancel(); - positionSubscription = null; cid.close(); if (isFileSource) { cacheLocalProgress(); @@ -1713,13 +1248,8 @@ class VideoDetailController extends GetxController } // sponsor block - if (plPlayerController.enableBlock) { - _lastPos = null; - positionSubscription?.cancel(); - positionSubscription = null; - videoLabel.value = ''; - segmentList.clear(); - segmentProgressList.clear(); + if (blockConfig.enableBlock) { + resetBlock(); } // interactive video @@ -1842,7 +1372,7 @@ class VideoDetailController extends GetxController oid: aid, subId: [cid.value], from: from, - heroTag: autoPlay.value ? heroTag : null, + heroTag: _autoPlay.value ? heroTag : null, start: playedTime, audioUrl: audioUrl, extraId: extraId, diff --git a/lib/pages/video/post_panel/view.dart b/lib/pages/video/post_panel/view.dart index 605124005..9e0b6838b 100644 --- a/lib/pages/video/post_panel/view.dart +++ b/lib/pages/video/post_panel/view.dart @@ -316,7 +316,7 @@ class _PostPanelState extends State SmartDialog.showToast('提交成功'); list.clear(); videoDetailController.handleSBData(response); - if (videoDetailController.positionSubscription == null) { + if (videoDetailController.blockListener == null) { videoDetailController.initSkip(); } } else { diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index e6bed0d72..bfee2d660 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -163,7 +163,7 @@ class _VideoDetailPageVState extends State // 获取视频资源,初始化播放器 Future videoSourceInit() async { videoDetailController.queryVideoUrl(); - if (videoDetailController.autoPlay.value) { + if (videoDetailController.autoPlay) { plPlayerController = videoDetailController.plPlayerController; plPlayerController! ..addStatusLister(playerListener) @@ -310,7 +310,7 @@ class _VideoDetailPageVState extends State } } plPlayerController = videoDetailController.plPlayerController; - videoDetailController.autoPlay.value = true; + videoDetailController.autoPlay = true; if (videoDetailController.plPlayerController.preInitPlayer) { await plPlayerController!.play(); } else { @@ -385,7 +385,7 @@ class _VideoDetailPageVState extends State ScreenBrightnessPlatform.instance.resetApplicationScreenBrightness(); } - videoDetailController.positionSubscription?.cancel(); + videoDetailController.cancelBlockListener(); introController.cancelTimer(); @@ -447,7 +447,7 @@ class _VideoDetailPageVState extends State } () async { - if (videoDetailController.autoPlay.value) { + if (videoDetailController.autoPlay) { await videoDetailController.playerInit( autoplay: videoDetailController.playerStatus?.isPlaying ?? false, ); @@ -547,7 +547,7 @@ class _VideoDetailPageVState extends State if (!isPortrait && !isFullScreen && plPlayerController != null && - videoDetailController.autoPlay.value) { + videoDetailController.autoPlay) { WidgetsBinding.instance.addPostFrameCallback((_) { plPlayerController!.triggerFullScreen( status: true, @@ -1199,7 +1199,7 @@ class _VideoDetailPageVState extends State ); Widget get manualPlayerWidget => Obx(() { - if (!videoDetailController.autoPlay.value) { + if (!videoDetailController.autoPlay) { return Stack( clipBehavior: Clip.none, children: [ @@ -1354,7 +1354,7 @@ class _VideoDetailPageVState extends State child: Obx( () => videoDetailController.videoState.value is! Success || - !videoDetailController.autoPlay.value || + !videoDetailController.autoPlay || plPlayerController?.videoController == null ? const SizedBox.shrink() : PLVideoPlayer( @@ -1415,7 +1415,7 @@ class _VideoDetailPageVState extends State introController: introController, onSendDanmaku: videoDetailController.showShootDanmakuSheet, canPlay: () { - if (videoDetailController.autoPlay.value) { + if (videoDetailController.autoPlay) { return true; } handlePlay(); @@ -1594,7 +1594,7 @@ class _VideoDetailPageVState extends State if (isShowing) plPlayer(width: width, height: height), Obx(() { - if (!videoDetailController.autoPlay.value) { + if (!videoDetailController.autoPlay) { return Positioned.fill( child: GestureDetector( onTap: handlePlay, diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index 7f26516ed..95b9970a7 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -1944,8 +1944,7 @@ class HeaderControlState extends State child: IconButton( tooltip: '片段信息', style: btnStyle, - onPressed: () => - videoDetailCtr.showSBDetail(context), + onPressed: videoDetailCtr.showSBDetail, icon: const Icon( MdiIcons.advertisements, size: 19, diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index a87cc9e18..e1e0d1a1a 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -11,7 +11,6 @@ import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/models/common/audio_normalization.dart'; -import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart'; import 'package:PiliPlus/models/common/super_resolution_type.dart'; import 'package:PiliPlus/models/common/video/video_type.dart'; import 'package:PiliPlus/models/user/danmaku_rule.dart'; @@ -19,6 +18,7 @@ import 'package:PiliPlus/models/video/play/url.dart'; import 'package:PiliPlus/models_new/video/video_shot/data.dart'; import 'package:PiliPlus/pages/danmaku/danmaku_model.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; +import 'package:PiliPlus/pages/sponsor_block/block_mixin.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_source.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_status.dart'; import 'package:PiliPlus/plugin/pl_player/models/double_tap_type.dart'; @@ -62,7 +62,7 @@ import 'package:path/path.dart' as path; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:window_manager/window_manager.dart'; -class PlPlayerController { +class PlPlayerController with BlockConfigMixin { Player? _videoPlayerController; VideoController? _videoController; @@ -358,19 +358,6 @@ class PlPlayerController { late double subtitleStrokeWidth = Pref.subtitleStrokeWidth; late int subtitleFontWeight = Pref.subtitleFontWeight; - late final pgcSkipType = Pref.pgcSkipType; - late final enablePgcSkip = Pref.pgcSkipType != SkipType.disable; - // sponsor block - late final bool enableSponsorBlock = Pref.enableSponsorBlock; - late final bool enableBlock = enableSponsorBlock || enablePgcSkip; - late final double blockLimit = Pref.blockLimit; - late final blockSettings = Pref.blockSettings; - late final List blockColor = Pref.blockColor; - late final Set enableList = blockSettings - .where((item) => item.second != SkipType.disable) - .map((item) => item.first.name) - .toSet(); - // settings late final showFSActionItem = Pref.showFSActionItem; late final enableShrinkVideoSize = Pref.enableShrinkVideoSize; diff --git a/lib/services/shutdown_timer_service.dart b/lib/services/shutdown_timer_service.dart index cc9b97106..9903dd4a8 100644 --- a/lib/services/shutdown_timer_service.dart +++ b/lib/services/shutdown_timer_service.dart @@ -33,6 +33,7 @@ class ShutdownTimerService { ValueGetter? isPlaying; Timer? _shutdownTimer; + bool get isActive => _shutdownTimer?.isActive ?? false; int _durationInMinutes = 0; _ShutdownType _shutdownType = .pause;