mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-20 09:20:13 +08:00
@@ -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(
|
||||
|
||||
@@ -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<Int64> subId;
|
||||
late int itemType;
|
||||
Int64? extraId;
|
||||
late final PlaylistSource from;
|
||||
late final isVideo = itemType == 1;
|
||||
@override
|
||||
late final bool isUgc = itemType == 1;
|
||||
|
||||
final Rx<DetailItem?> audioItem = Rx<DetailItem?>(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<void>? 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<bool> _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<void>? 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
|
||||
|
||||
@@ -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<AudioPage> {
|
||||
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<ListOrder>(
|
||||
@@ -107,14 +119,14 @@ class _AudioPageState extends State<AudioPage> {
|
||||
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<AudioPage> {
|
||||
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<AudioPage> {
|
||||
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<AudioPage> {
|
||||
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()) ...[
|
||||
|
||||
525
lib/pages/sponsor_block/block_mixin.dart
Normal file
525
lib/pages/sponsor_block/block_mixin.dart
Normal file
@@ -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<Color> blockColor = Pref.blockColor;
|
||||
late final Set<String> enableList = blockSettings
|
||||
.where((item) => item.second != SkipType.disable)
|
||||
.map((item) => item.first.name)
|
||||
.toSet();
|
||||
}
|
||||
|
||||
mixin BlockMixin on GetxController {
|
||||
int? _lastBlockPos;
|
||||
BlockConfigMixin get blockConfig;
|
||||
StreamSubscription<Duration>? _blockListener;
|
||||
StreamSubscription<Duration>? get blockListener => _blockListener;
|
||||
Color _getBlockColor(SegmentType segment) =>
|
||||
blockConfig.blockColor[segment.index];
|
||||
late final List<SegmentModel> _segmentList = <SegmentModel>[];
|
||||
late final RxList<Segment> segmentProgressList = <Segment>[].obs;
|
||||
|
||||
Timer? _skipTimer;
|
||||
late final listKey = GlobalKey<AnimatedListState>();
|
||||
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<void> querySponsorBlock({
|
||||
required String bvid,
|
||||
required int cid,
|
||||
}) async {
|
||||
resetBlock();
|
||||
|
||||
final result = await SponsorBlock.getSkipSegments(bvid: bvid, cid: cid);
|
||||
switch (result) {
|
||||
case Success<List<SegmentItemModel>>(: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<void> handleSBData(List<SegmentItemModel> 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<double> animation) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
void _stopSkipTimer() {
|
||||
if (_skipTimer != null) {
|
||||
_skipTimer!.cancel();
|
||||
_skipTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void>? seekTo(Duration duration, {required bool isSeek});
|
||||
|
||||
Future<void> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<ScaffoldState>();
|
||||
@@ -165,7 +161,6 @@ class VideoDetailController extends GetxController
|
||||
late final RxInt seasonIndex = 0.obs;
|
||||
|
||||
PlayerStatus? playerStatus;
|
||||
StreamSubscription<Duration>? positionSubscription;
|
||||
|
||||
late final scrollKey = GlobalKey<ExtendedNestedScrollViewState>();
|
||||
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<PostSegmentModel> postList = [];
|
||||
late final List<SegmentModel> segmentList = <SegmentModel>[];
|
||||
late final RxList<Segment> segmentProgressList = <Segment>[].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<void> 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<AnimatedListState>();
|
||||
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<void> _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<List<SegmentItemModel>>(:final response):
|
||||
handleSBData(response);
|
||||
case Error(:final code) when code != 404:
|
||||
if (kDebugMode) {
|
||||
result.toast();
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleSBData(List<SegmentItemModel> 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<double> animation) {
|
||||
final theme = Get.theme;
|
||||
return Align(
|
||||
@@ -995,36 +563,6 @@ class VideoDetailController extends GetxController
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> 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<void>? _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<PostSegmentModel> postList = <PostSegmentModel>[];
|
||||
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,
|
||||
|
||||
@@ -316,7 +316,7 @@ class _PostPanelState extends State<PostPanel>
|
||||
SmartDialog.showToast('提交成功');
|
||||
list.clear();
|
||||
videoDetailController.handleSBData(response);
|
||||
if (videoDetailController.positionSubscription == null) {
|
||||
if (videoDetailController.blockListener == null) {
|
||||
videoDetailController.initSkip();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -163,7 +163,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
// 获取视频资源,初始化播放器
|
||||
Future<void> 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<VideoDetailPageV>
|
||||
}
|
||||
}
|
||||
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<VideoDetailPageV>
|
||||
ScreenBrightnessPlatform.instance.resetApplicationScreenBrightness();
|
||||
}
|
||||
|
||||
videoDetailController.positionSubscription?.cancel();
|
||||
videoDetailController.cancelBlockListener();
|
||||
|
||||
introController.cancelTimer();
|
||||
|
||||
@@ -447,7 +447,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
}
|
||||
|
||||
() async {
|
||||
if (videoDetailController.autoPlay.value) {
|
||||
if (videoDetailController.autoPlay) {
|
||||
await videoDetailController.playerInit(
|
||||
autoplay: videoDetailController.playerStatus?.isPlaying ?? false,
|
||||
);
|
||||
@@ -547,7 +547,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
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<VideoDetailPageV>
|
||||
);
|
||||
|
||||
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<VideoDetailPageV>
|
||||
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<VideoDetailPageV>
|
||||
introController: introController,
|
||||
onSendDanmaku: videoDetailController.showShootDanmakuSheet,
|
||||
canPlay: () {
|
||||
if (videoDetailController.autoPlay.value) {
|
||||
if (videoDetailController.autoPlay) {
|
||||
return true;
|
||||
}
|
||||
handlePlay();
|
||||
@@ -1594,7 +1594,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
||||
if (isShowing) plPlayer(width: width, height: height),
|
||||
|
||||
Obx(() {
|
||||
if (!videoDetailController.autoPlay.value) {
|
||||
if (!videoDetailController.autoPlay) {
|
||||
return Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: handlePlay,
|
||||
|
||||
@@ -1944,8 +1944,7 @@ class HeaderControlState extends State<HeaderControl>
|
||||
child: IconButton(
|
||||
tooltip: '片段信息',
|
||||
style: btnStyle,
|
||||
onPressed: () =>
|
||||
videoDetailCtr.showSBDetail(context),
|
||||
onPressed: videoDetailCtr.showSBDetail,
|
||||
icon: const Icon(
|
||||
MdiIcons.advertisements,
|
||||
size: 19,
|
||||
|
||||
@@ -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<Color> blockColor = Pref.blockColor;
|
||||
late final Set<String> 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;
|
||||
|
||||
@@ -33,6 +33,7 @@ class ShutdownTimerService {
|
||||
ValueGetter<bool>? isPlaying;
|
||||
|
||||
Timer? _shutdownTimer;
|
||||
bool get isActive => _shutdownTimer?.isActive ?? false;
|
||||
int _durationInMinutes = 0;
|
||||
_ShutdownType _shutdownType = .pause;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user