import 'dart:math'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/icon_button.dart'; import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/segment_progress_bar.dart'; import 'package:PiliPlus/http/init.dart'; 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_type.dart'; import 'package:PiliPlus/pages/common/common_collapse_slide_page.dart'; import 'package:PiliPlus/pages/video/detail/index.dart'; import 'package:PiliPlus/plugin/pl_player/index.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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'; class PostPanel extends CommonCollapseSlidePage { const PostPanel({ super.key, super.enableSlide, required this.videoDetailController, required this.plPlayerController, }); final VideoDetailController videoDetailController; final PlPlayerController plPlayerController; @override State createState() => _PostPanelState(); } class _PostPanelState extends CommonCollapseSlidePageState { VideoDetailController get videoDetailController => widget.videoDetailController; PlPlayerController get plPlayerController => widget.plPlayerController; List? get list => videoDetailController.list; late final double videoDuration = plPlayerController.durationSeconds.value.inMilliseconds / 1000; double get currentPos => plPlayerController.position.value.inMilliseconds / 1000; @override Widget buildPage(ThemeData theme) { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( automaticallyImplyLeading: false, titleSpacing: 16, title: const Text('提交片段'), actions: [ iconButton( context: context, tooltip: '添加片段', onPressed: () { setState(() { list?.insert( 0, PostSegmentModel( segment: Pair( first: 0, second: currentPos, ), category: SegmentType.sponsor, actionType: ActionType.skip, ), ); }); }, icon: Icons.add, ), const SizedBox(width: 10), iconButton( context: context, tooltip: '关闭', onPressed: Get.back, icon: Icons.close, ), const SizedBox(width: 16), ], ), body: enableSlide ? slideList(theme) : buildList(theme), ); } @override Widget buildList(ThemeData theme) { return list?.isNotEmpty == true ? Stack( clipBehavior: Clip.none, children: [ SingleChildScrollView( controller: ScrollController(), physics: const AlwaysScrollableScrollPhysics(), child: Column( children: [ ...List.generate( list!.length, (index) => Stack( clipBehavior: Clip.none, children: [ Container( width: double.infinity, margin: const EdgeInsets.symmetric( horizontal: 16, vertical: 5, ), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.colorScheme.onInverseSurface, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (list![index].actionType != ActionType.full) ...[ Wrap( runSpacing: 8, spacing: 16, children: [ Row( mainAxisSize: MainAxisSize.min, children: segmentWidget( theme, isFirst: true, index: index, ), ), if (list![index].category != SegmentType.poi_highlight) Row( mainAxisSize: MainAxisSize.min, children: segmentWidget( theme, isFirst: false, index: index, ), ), ], ), const SizedBox(height: 8), ], Wrap( runSpacing: 8, spacing: 16, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ const Text('分类: '), PopupMenuButton( initialValue: list![index].category, onSelected: (item) async { list![index].category = item; List constraintList = segmentType2ActionType(item); if (constraintList .contains( list![index].actionType) .not) { list![index].actionType = constraintList.first; } switch (item) { case SegmentType.poi_highlight: updateSegment( isFirst: false, index: index, value: list![index] .segment .first, ); break; case SegmentType.exclusive_access: updateSegment( isFirst: true, index: index, value: 0, ); break; case _: } setState(() {}); }, itemBuilder: (context) => SegmentType .values .map((item) => PopupMenuItem( value: item, child: Text(item.title), )) .toList(), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( list![index].category.title, style: TextStyle( height: 1, fontSize: 14, color: theme .colorScheme.secondary, ), strutStyle: StrutStyle( height: 1, leading: 0, ), ), Icon( MdiIcons.unfoldMoreHorizontal, size: MediaQuery.textScalerOf( context) .scale(14), color: theme.colorScheme.secondary, ), ], ), ), ], ), Row( mainAxisSize: MainAxisSize.min, children: [ const Text('行为类别: '), PopupMenuButton( initialValue: list![index].actionType, onSelected: (item) async { list![index].actionType = item; if (item == ActionType.full) { updateSegment( isFirst: true, index: index, value: 0, ); } setState(() {}); }, itemBuilder: (context) => ActionType .values .map( (item) => PopupMenuItem( enabled: segmentType2ActionType( list![index] .category) .contains(item), value: item, child: Text(item.title), ), ) .toList(), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( list![index].actionType.title, style: TextStyle( height: 1, fontSize: 14, color: theme .colorScheme.secondary, ), strutStyle: StrutStyle( height: 1, leading: 0, ), ), Icon( MdiIcons.unfoldMoreHorizontal, size: MediaQuery.textScalerOf( context) .scale(14), color: theme.colorScheme.secondary, ), ], ), ), ], ), ], ), ], ), ), Positioned( top: 0, right: 4, child: iconButton( context: context, size: 26, tooltip: '移除', icon: Icons.clear, onPressed: () { setState(() { list!.removeAt(index); }); }, ), ), Positioned( top: 0, left: 4, child: iconButton( context: context, size: 26, tooltip: '预览', icon: Icons.preview_outlined, onPressed: () async { if (widget.plPlayerController .videoPlayerController != null) { int start = max( 0, (list![index].segment.first * 1000) .toInt() - 1, ); await widget .plPlayerController.videoPlayerController! .seek( Duration(milliseconds: start), ); if (widget .plPlayerController .videoPlayerController! .state .playing .not) { await widget.plPlayerController .videoPlayerController! .play(); } if (start != 0) { await Future.delayed( const Duration(seconds: 1)); } widget .plPlayerController.videoPlayerController! .seek( Duration( milliseconds: (list![index].segment.second * 1000) .toInt(), ), ); } }, ), ), ], ), ), SizedBox( height: 88 + MediaQuery.paddingOf(context).bottom, ), ], ), ), Positioned( right: 16, bottom: 16 + MediaQuery.paddingOf(context).bottom, child: FloatingActionButton( tooltip: '提交', onPressed: () { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('确定无误再提交'), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle( color: theme.colorScheme.outline, ), ), ), TextButton( onPressed: () { Get.back(); _onPost(); }, child: const Text('确定提交'), ), ], ), ); }, child: Icon(Icons.check), ), ) ], ) : errorWidget(); } void updateSegment({ required bool isFirst, required int index, required double value, }) { if (isFirst) { list![index].segment.first = value; } else { list![index].segment.second = value; } if (list![index].category == SegmentType.poi_highlight || list![index].actionType == ActionType.full) { list![index].segment.second = value; } } List segmentWidget( ThemeData theme, { required int index, required bool isFirst, }) { String value = Utils.timeFormat( isFirst ? list![index].segment.first : list![index].segment.second); return [ Text( '${isFirst ? '开始' : '结束'}: $value', ), const SizedBox(width: 5), iconButton( context: context, size: 26, tooltip: '设为当前', icon: Icons.my_location, onPressed: () { setState(() { updateSegment( isFirst: isFirst, index: index, value: currentPos, ); }); }, ), const SizedBox(width: 5), iconButton( context: context, size: 26, tooltip: isFirst ? '视频开头' : '视频结尾', icon: isFirst ? Icons.first_page : Icons.last_page, onPressed: () { setState(() { updateSegment( isFirst: isFirst, index: index, value: isFirst ? 0 : videoDuration, ); }); }, ), const SizedBox(width: 5), iconButton( context: context, size: 26, tooltip: '编辑', icon: Icons.edit, onPressed: () { showDialog( context: context, builder: (context) { String initV = value; return AlertDialog( content: TextFormField( initialValue: value, autofocus: true, onChanged: (value) { initV = value; }, inputFormatters: [ FilteringTextInputFormatter.allow( RegExp(r'[\d:.]+'), ), ], ), actions: [ TextButton( onPressed: Get.back, child: Text( '取消', style: TextStyle(color: theme.colorScheme.outline), ), ), TextButton( onPressed: () => Get.back(result: initV), child: Text('确定'), ), ], ); }, ).then((res) { if (res != null) { try { List split = res.split(':').reversed.map((e) => num.parse(e)).toList(); double duration = 0; for (int i = 0; i < split.length; i++) { duration += split[i] * pow(60, i); } if (duration <= videoDuration) { setState(() { updateSegment( isFirst: isFirst, index: index, value: duration, ); }); } } catch (e) { debugPrint(e.toString()); } } }); }, ), ]; } void _onPost({String? url}) async { Request().post( url ?? '${GStorage.blockServer}/api/skipSegments', data: { 'videoID': videoDetailController.bvid, 'cid': videoDetailController.cid.value.toString(), 'userID': GStorage.blockUserID.toString(), 'userAgent': Constants.userAgent, 'videoDuration': videoDuration, 'segments': list! .map( (item) => { 'segment': [ item.segment.first, item.segment.second, ], 'category': item.category.name, 'actionType': item.actionType.name, }, ) .toList(), }, options: Options( validateStatus: (int? status) { return status != null && ((status >= 200 && status < 300) || const [400, 403, 429, 409].contains(status)); }, ), ).then( (res) { if (res.statusCode == 200) { Get.back(); SmartDialog.showToast('提交成功'); list?.clear(); videoDetailController.handleSBData(res); plPlayerController.segmentList.value = videoDetailController.segmentProgressList ?? []; if (videoDetailController.positionSubscription == null) { videoDetailController.initSkip(); } } else { SmartDialog.showToast( '提交失败: ${const { 400: '参数错误', 403: '被自动审核机制拒绝', 429: '重复提交太快', 409: '重复提交' }[res.statusCode] ?? res.statusCode}', ); } }, ); } }