diff --git a/lib/common/widgets/list_sheet.dart b/lib/common/widgets/list_sheet.dart index 50ac3274..1ff018bf 100644 --- a/lib/common/widgets/list_sheet.dart +++ b/lib/common/widgets/list_sheet.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/common/widgets/network_img_layer.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/models/bangumi/info.dart' as bangumi; import 'package:PiliPlus/models/video_detail_res.dart' as video; +import 'package:PiliPlus/pages/common/common_slide_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -16,7 +17,7 @@ import '../../utils/storage.dart'; import '../../utils/utils.dart'; import 'package:PiliPlus/common/widgets/spring_physics.dart'; -class ListSheetContent extends StatefulWidget { +class ListSheetContent extends CommonSlidePage { const ListSheetContent({ super.key, this.index, // tab index @@ -31,6 +32,7 @@ class ListSheetContent extends StatefulWidget { this.showTitle, this.isSupportReverse, this.isReversed, + super.enableSlide, }); final dynamic index; @@ -50,7 +52,7 @@ class ListSheetContent extends StatefulWidget { State createState() => _ListSheetContentState(); } -class _ListSheetContentState extends State +class _ListSheetContentState extends CommonSlidePageState with TickerProviderStateMixin { late List itemScrollController = []; late int currentIndex = _currentIndex; @@ -140,7 +142,7 @@ class _ListSheetContentState extends State }(); } WidgetsBinding.instance.addPostFrameCallback((_) { - if (GStorage.collapsibleVideoPage) { + if (enableSlide && GStorage.collapsibleVideoPage) { if (mounted) { setState(() { _isInit = false; @@ -293,176 +295,195 @@ class _ListSheetContentState extends State @override Widget build(BuildContext context) { - if (GStorage.collapsibleVideoPage && _isInit) { + if (enableSlide && GStorage.collapsibleVideoPage && _isInit) { return CustomScrollView( physics: const NeverScrollableScrollPhysics(), ); } - return Column( - children: [ - Container( - height: 45, - padding: EdgeInsets.symmetric( - horizontal: widget.showTitle != false ? 14 : 6), - child: Row( - children: [ - if (widget.showTitle != false) - Text( - '合集(${_isList ? widget.season.epCount : episodes?.length ?? ''})', - style: Theme.of(context).textTheme.titleMedium, - ), - StreamBuilder( - stream: _favStream?.stream, - builder: (context, snapshot) => snapshot.hasData - ? mediumButton( - tooltip: _seasonFav == 1 ? '取消订阅' : '订阅', - icon: _seasonFav == 1 - ? Icons.notifications_off_outlined - : Icons.notifications_active_outlined, - onPressed: () async { - dynamic result = await VideoHttp.seasonFav( - isFav: _seasonFav == 1, - seasonId: widget.season.id, - ); - if (result['status']) { - SmartDialog.showToast( - '${_seasonFav == 1 ? '取消' : ''}订阅成功'); - _seasonFav = _seasonFav == 1 ? 0 : 1; - _favStream?.add(_seasonFav); - } else { - SmartDialog.showToast(result['msg']); - } + return enableSlide + ? Padding( + padding: EdgeInsets.only(top: padding), + child: buildPage, + ) + : buildPage; + } + + @override + Widget get buildPage => Material( + color: widget.showTitle == false + ? Colors.transparent + : Theme.of(context).colorScheme.surface, + child: Column( + children: [ + Container( + height: 45, + padding: EdgeInsets.symmetric( + horizontal: widget.showTitle != false ? 14 : 6), + child: Row( + children: [ + if (widget.showTitle != false) + Text( + '合集(${_isList ? widget.season.epCount : episodes?.length ?? ''})', + style: Theme.of(context).textTheme.titleMedium, + ), + StreamBuilder( + stream: _favStream?.stream, + builder: (context, snapshot) => snapshot.hasData + ? mediumButton( + tooltip: _seasonFav == 1 ? '取消订阅' : '订阅', + icon: _seasonFav == 1 + ? Icons.notifications_off_outlined + : Icons.notifications_active_outlined, + onPressed: () async { + dynamic result = await VideoHttp.seasonFav( + isFav: _seasonFav == 1, + seasonId: widget.season.id, + ); + if (result['status']) { + SmartDialog.showToast( + '${_seasonFav == 1 ? '取消' : ''}订阅成功'); + _seasonFav = _seasonFav == 1 ? 0 : 1; + _favStream?.add(_seasonFav); + } else { + SmartDialog.showToast(result['msg']); + } + }, + ) + : const SizedBox.shrink(), + ), + mediumButton( + tooltip: '跳至顶部', + icon: Icons.vertical_align_top, + onPressed: () { + try { + itemScrollController[_ctr?.index ?? 0].scrollTo( + index: !reverse[_ctr?.index ?? 0] + ? 0 + : _isList + ? widget.season.sections[_ctr?.index].episodes + .length - + 1 + : episodes.length - 1, + duration: const Duration(milliseconds: 200), + ); + } catch (_) {} + }, + ), + mediumButton( + tooltip: '跳至底部', + icon: Icons.vertical_align_bottom, + onPressed: () { + try { + itemScrollController[_ctr?.index ?? 0].scrollTo( + index: !reverse[_ctr?.index ?? 0] + ? _isList + ? widget.season.sections[_ctr?.index].episodes + .length - + 1 + : episodes.length - 1 + : 0, + duration: const Duration(milliseconds: 200), + ); + } catch (_) {} + }, + ), + mediumButton( + tooltip: '跳至当前', + icon: Icons.my_location, + onPressed: () async { + if (_ctr != null && _ctr?.index != (_index)) { + _ctr?.animateTo(_index); + await Future.delayed(const Duration(milliseconds: 225)); + } + try { + itemScrollController[_ctr?.index ?? 0].scrollTo( + index: currentIndex, + duration: const Duration(milliseconds: 200), + ); + } catch (_) {} + }, + ), + if (widget.isSupportReverse == true) + if (!_isList) + _reverseButton + else + StreamBuilder( + stream: _indexStream?.stream, + initialData: _index, + builder: (context, snapshot) { + return snapshot.data == _index + ? _reverseButton + : const SizedBox.shrink(); }, - ) - : const SizedBox.shrink(), - ), - mediumButton( - tooltip: '跳至顶部', - icon: Icons.vertical_align_top, - onPressed: () { - try { - itemScrollController[_ctr?.index ?? 0].scrollTo( - index: !reverse[_ctr?.index ?? 0] - ? 0 - : _isList - ? widget.season.sections[_ctr?.index].episodes - .length - - 1 - : episodes.length - 1, - duration: const Duration(milliseconds: 200), - ); - } catch (_) {} - }, - ), - mediumButton( - tooltip: '跳至底部', - icon: Icons.vertical_align_bottom, - onPressed: () { - try { - itemScrollController[_ctr?.index ?? 0].scrollTo( - index: !reverse[_ctr?.index ?? 0] - ? _isList - ? widget.season.sections[_ctr?.index].episodes - .length - - 1 - : episodes.length - 1 - : 0, - duration: const Duration(milliseconds: 200), - ); - } catch (_) {} - }, - ), - mediumButton( - tooltip: '跳至当前', - icon: Icons.my_location, - onPressed: () async { - if (_ctr != null && _ctr?.index != (_index)) { - _ctr?.animateTo(_index); - await Future.delayed(const Duration(milliseconds: 225)); - } - try { - itemScrollController[_ctr?.index ?? 0].scrollTo( - index: currentIndex, - duration: const Duration(milliseconds: 200), - ); - } catch (_) {} - }, - ), - if (widget.isSupportReverse == true) - if (!_isList) - _reverseButton - else + ), + const Spacer(), StreamBuilder( stream: _indexStream?.stream, initialData: _index, - builder: (context, snapshot) { - return snapshot.data == _index - ? _reverseButton - : const SizedBox.shrink(); - }, - ), - const Spacer(), - StreamBuilder( - stream: _indexStream?.stream, - initialData: _index, - builder: (context, snapshot) => mediumButton( - tooltip: reverse[snapshot.data] ? '顺序' : '倒序', - icon: !reverse[snapshot.data] - ? MdiIcons.sortNumericAscending - : MdiIcons.sortNumericDescending, - onPressed: () { - setState(() { - reverse[_ctr?.index ?? 0] = !reverse[_ctr?.index ?? 0]; - }); - }, - ), - ), - if (widget.onClose != null) - mediumButton( - tooltip: '关闭', - icon: Icons.close, - onPressed: widget.onClose, - ), - ], - ), - ), - Divider( - height: 1, - color: Theme.of(context).dividerColor.withOpacity(0.1), - ), - if (_isList) - TabBar( - controller: _ctr, - padding: const EdgeInsets.only(right: 60), - isScrollable: true, - tabs: (widget.season.sections as List) - .map((item) => Tab(text: item.title)) - .toList(), - dividerHeight: 1, - dividerColor: Theme.of(context).dividerColor.withOpacity(0.1), - ), - Expanded( - child: _isList - ? Material( - color: Colors.transparent, - child: tabBarView( - controller: _ctr, - children: List.generate( - widget.season.sections.length, - (index) => _buildBody( - index, widget.season.sections[index].episodes), + builder: (context, snapshot) => mediumButton( + tooltip: reverse[snapshot.data] ? '顺序' : '倒序', + icon: !reverse[snapshot.data] + ? MdiIcons.sortNumericAscending + : MdiIcons.sortNumericDescending, + onPressed: () { + setState(() { + reverse[_ctr?.index ?? 0] = + !reverse[_ctr?.index ?? 0]; + }); + }, ), ), - ) - : Material( - color: Colors.transparent, - child: _buildBody(null, episodes), - ), + if (widget.onClose != null) + mediumButton( + tooltip: '关闭', + icon: Icons.close, + onPressed: widget.onClose, + ), + ], + ), + ), + Divider( + height: 1, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + if (_isList) + TabBar( + controller: _ctr, + padding: const EdgeInsets.only(right: 60), + isScrollable: true, + tabs: (widget.season.sections as List) + .map((item) => Tab(text: item.title)) + .toList(), + dividerHeight: 1, + dividerColor: Theme.of(context).dividerColor.withOpacity(0.1), + ), + Expanded( + child: _isList + ? Material( + color: Colors.transparent, + child: tabBarView( + controller: _ctr, + children: List.generate( + widget.season.sections.length, + (index) => _buildBody( + index, widget.season.sections[index].episodes), + ), + ), + ) + : enableSlide + ? slideList() + : buildList, + ), + ], ), - ], - ); - } + ); + + @override + Widget get buildList => Material( + color: Colors.transparent, + child: _buildBody(null, episodes), + ); Widget get _reverseButton => mediumButton( tooltip: widget.isReversed == true ? '正序播放' : '倒序播放', diff --git a/lib/models/common/sponsor_block/segment_type.dart b/lib/models/common/sponsor_block/segment_type.dart index 7dbd2152..36218d6b 100644 --- a/lib/models/common/sponsor_block/segment_type.dart +++ b/lib/models/common/sponsor_block/segment_type.dart @@ -1,5 +1,7 @@ import 'dart:ui'; +import 'package:PiliPlus/models/common/sponsor_block/action_type.dart'; + enum SegmentType { sponsor, selfpromo, @@ -13,6 +15,74 @@ enum SegmentType { exclusive_access } +// List _actionType2SegmentType(ActionType actionType) { +// return switch (actionType) { +// ActionType.skip => [ +// SegmentType.sponsor, +// SegmentType.selfpromo, +// SegmentType.interaction, +// SegmentType.intro, +// SegmentType.outro, +// SegmentType.preview, +// SegmentType.filler, +// ], +// ActionType.mute => [ +// SegmentType.sponsor, +// SegmentType.selfpromo, +// SegmentType.interaction, +// SegmentType.intro, +// SegmentType.outro, +// SegmentType.preview, +// SegmentType.music_offtopic, +// SegmentType.filler, +// ], +// ActionType.full => [ +// SegmentType.sponsor, +// SegmentType.selfpromo, +// SegmentType.exclusive_access, +// ], +// ActionType.poi => [ +// SegmentType.poi_highlight, +// ], +// }; +// } + +List segmentType2ActionType(SegmentType segmentType) { + return switch (segmentType) { + SegmentType.sponsor => [ActionType.skip, ActionType.mute, ActionType.full], + SegmentType.selfpromo => [ + ActionType.skip, + ActionType.mute, + ActionType.full + ], + SegmentType.interaction => [ + ActionType.skip, + ActionType.mute, + ], + SegmentType.intro => [ + ActionType.skip, + ActionType.mute, + ], + SegmentType.outro => [ + ActionType.skip, + ActionType.mute, + ], + SegmentType.preview => [ + ActionType.skip, + ActionType.mute, + ], + SegmentType.music_offtopic => [ + ActionType.skip, + ], + SegmentType.poi_highlight => [ActionType.poi], + SegmentType.filler => [ + ActionType.skip, + ActionType.mute, + ], + SegmentType.exclusive_access => [ActionType.full], + }; +} + extension SegmentTypeExt on SegmentType { /// from https://github.com/hanydd/BilibiliSponsorBlock/blob/master/public/_locales/zh_CN/messages.json String get title => [ diff --git a/lib/pages/bangumi/introduction/widgets/intro_detail.dart b/lib/pages/bangumi/introduction/widgets/intro_detail.dart index baa2f393..a4f21714 100644 --- a/lib/pages/bangumi/introduction/widgets/intro_detail.dart +++ b/lib/pages/bangumi/introduction/widgets/intro_detail.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/pages/common/common_slide_page.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:flutter/material.dart'; import 'package:PiliPlus/common/widgets/stat/danmu.dart'; @@ -6,7 +7,7 @@ import 'package:get/get.dart'; import '../../../../utils/utils.dart'; -class IntroDetail extends StatelessWidget { +class IntroDetail extends CommonSlidePage { final dynamic bangumiDetail; final dynamic videoTags; @@ -17,11 +18,17 @@ class IntroDetail extends StatelessWidget { }); @override - Widget build(BuildContext context) { - TextStyle smallTitle = TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurface, - ); + State createState() => _IntroDetailState(); +} + +class _IntroDetailState extends CommonSlidePageState { + late final TextStyle smallTitle = TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface, + ); + + @override + Widget get buildPage { return Padding( padding: const EdgeInsets.only(left: 14, right: 14), child: Column( @@ -46,99 +53,103 @@ class IntroDetail extends StatelessWidget { ), ), Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - bangumiDetail!.title, - style: const TextStyle( - fontSize: 16, - ), - ), - const SizedBox(height: 4), - Row( - children: [ - statView( - context: context, - theme: 'gray', - view: bangumiDetail!.stat!['views'], - size: 'medium', - ), - const SizedBox(width: 6), - statDanMu( - context: context, - theme: 'gray', - danmu: bangumiDetail!.stat!['danmakus'], - size: 'medium', - ), - ], - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - bangumiDetail!.areas!.first['name'], - style: smallTitle, - ), - const SizedBox(width: 6), - Text( - bangumiDetail!.publish!['pub_time_show'], - style: smallTitle, - ), - const SizedBox(width: 6), - Text( - bangumiDetail!.newEp!['desc'], - style: smallTitle, - ), - ], - ), - const SizedBox(height: 20), - Text( - '简介:', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 4), - SelectableText( - '${bangumiDetail!.evaluate!}', - style: smallTitle.copyWith(fontSize: 13), - ), - const SizedBox(height: 20), - Text( - '声优:', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 4), - SelectableText( - bangumiDetail.actors, - style: smallTitle.copyWith(fontSize: 13), - ), - if (videoTags is List && videoTags.isNotEmpty) ...[ - const SizedBox(height: 10), - Wrap( - spacing: 8, - runSpacing: 8, - children: (videoTags as List) - .map( - (item) => SearchText( - fontSize: 13, - text: item['tag_name'], - onTap: (_) => Get.toNamed('/searchResult', - parameters: {'keyword': item['tag_name']}), - onLongPress: (_) => - Utils.copyText(item['tag_name']), - ), - ) - .toList(), - ) - ], - SizedBox(height: MediaQuery.of(context).padding.bottom + 20) - ], - ), - ), + child: enableSlide ? slideList() : buildList, ) ], ), ); } + + @override + Widget get buildList => SingleChildScrollView( + controller: ScrollController(), + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + widget.bangumiDetail!.title, + style: const TextStyle( + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + statView( + context: context, + theme: 'gray', + view: widget.bangumiDetail!.stat!['views'], + size: 'medium', + ), + const SizedBox(width: 6), + statDanMu( + context: context, + theme: 'gray', + danmu: widget.bangumiDetail!.stat!['danmakus'], + size: 'medium', + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + widget.bangumiDetail!.areas!.first['name'], + style: smallTitle, + ), + const SizedBox(width: 6), + Text( + widget.bangumiDetail!.publish!['pub_time_show'], + style: smallTitle, + ), + const SizedBox(width: 6), + Text( + widget.bangumiDetail!.newEp!['desc'], + style: smallTitle, + ), + ], + ), + const SizedBox(height: 20), + Text( + '简介:', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 4), + SelectableText( + '${widget.bangumiDetail!.evaluate!}', + style: smallTitle.copyWith(fontSize: 13), + ), + const SizedBox(height: 20), + Text( + '声优:', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 4), + SelectableText( + widget.bangumiDetail.actors, + style: smallTitle.copyWith(fontSize: 13), + ), + if (widget.videoTags is List && widget.videoTags.isNotEmpty) ...[ + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: (widget.videoTags as List) + .map( + (item) => SearchText( + fontSize: 13, + text: item['tag_name'], + onTap: (_) => Get.toNamed('/searchResult', + parameters: {'keyword': item['tag_name']}), + onLongPress: (_) => Utils.copyText(item['tag_name']), + ), + ) + .toList(), + ) + ], + SizedBox(height: MediaQuery.of(context).padding.bottom + 20) + ], + ), + ); } diff --git a/lib/pages/common/common_publish_page.dart b/lib/pages/common/common_publish_page.dart index 6eba2eb7..975af0d8 100644 --- a/lib/pages/common/common_publish_page.dart +++ b/lib/pages/common/common_publish_page.dart @@ -33,9 +33,6 @@ abstract class CommonPublishPage extends StatefulWidget { final int? imageLengthLimit; final ValueChanged? onSave; final bool autofocus; - - @override - State createState(); } abstract class CommonPublishPageState diff --git a/lib/pages/common/common_slide_page.dart b/lib/pages/common/common_slide_page.dart new file mode 100644 index 00000000..9730eeb0 --- /dev/null +++ b/lib/pages/common/common_slide_page.dart @@ -0,0 +1,91 @@ +import 'package:PiliPlus/utils/storage.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +abstract class CommonSlidePage extends StatefulWidget { + const CommonSlidePage({super.key, this.enableSlide}); + + final bool? enableSlide; +} + +abstract class CommonSlidePageState + extends State { + Offset? downPos; + bool? isSliding; + late double padding = 0.0; + + late final enableSlide = + widget.enableSlide != false && GStorage.slideDismissReplyPage; + + @override + Widget build(BuildContext context) { + return enableSlide + ? Padding( + padding: EdgeInsets.only(top: padding), + child: buildPage, + ) + : buildPage; + } + + Widget get buildPage; + + Widget get buildList => throw UnimplementedError(); + + Widget slideList([Widget? buildList]) => GestureDetector( + onPanDown: (event) { + if (event.localPosition.dx > 30) { + isSliding = false; + } else { + downPos = event.localPosition; + } + }, + onPanUpdate: (event) { + if (isSliding == false) { + return; + } else if (isSliding == null) { + if (downPos != null) { + Offset cumulativeDelta = event.localPosition - downPos!; + if (cumulativeDelta.dx.abs() >= cumulativeDelta.dy.abs()) { + isSliding = true; + setState(() { + padding = event.localPosition.dx; + }); + } else { + isSliding = false; + } + } + } else if (isSliding == true) { + setState(() { + padding = event.localPosition.dx; + }); + } + }, + onPanCancel: () { + if (isSliding == true) { + if (padding >= 100) { + Get.back(); + } else { + setState(() { + padding = 0; + }); + } + } + downPos = null; + isSliding = null; + }, + onPanEnd: (event) { + if (isSliding == true) { + if (padding >= 100) { + Get.back(); + } else { + setState(() { + padding = 0; + }); + } + } + downPos = null; + isSliding = null; + }, + child: buildList ?? this.buildList, + ); +} diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 22f032c0..6da1c663 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -2,9 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'dart:ui'; -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'; @@ -21,9 +18,9 @@ import 'package:PiliPlus/models/video_detail_res.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:PiliPlus/pages/video/detail/introduction/controller.dart'; import 'package:PiliPlus/pages/video/detail/note/note_list_page.dart'; +import 'package:PiliPlus/pages/video/detail/post_panel/post_panel.dart'; import 'package:PiliPlus/pages/video/detail/related/controller.dart'; import 'package:PiliPlus/pages/video/detail/reply/controller.dart'; -import 'package:PiliPlus/pages/video/detail/view_v.dart' show ViewPointsPage; import 'package:PiliPlus/pages/video/detail/widgets/send_danmaku_panel.dart'; import 'package:PiliPlus/pages/video/detail/widgets/watch_later_list.dart'; import 'package:PiliPlus/utils/extension.dart'; @@ -33,7 +30,6 @@ import 'package:easy_debounce/easy_throttle.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:floating/floating.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:PiliPlus/http/constants.dart'; @@ -47,7 +43,6 @@ import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/video_utils.dart'; import 'package:get/get_navigation/src/dialog/dialog_route.dart'; import 'package:hive/hive.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import '../../../utils/id_utils.dart'; import 'widgets/header_control.dart'; @@ -400,6 +395,7 @@ class VideoDetailController extends GetxController showMediaListPanel(context) { if (mediaList.isNotEmpty) { childKey.currentState?.showBottomSheet( + shape: const RoundedRectangleBorder(), backgroundColor: Theme.of(context).colorScheme.surface, (context) => MediaListPanel( mediaList: mediaList, @@ -463,7 +459,7 @@ class VideoDetailController extends GetxController List? _blockColor; RxList segmentList = [].obs; List viewPointList = []; - List? _segmentProgressList; + List? segmentProgressList; Color _getColor(SegmentType segment) => _blockColor?[segment.index] ?? segment.color; late RxString videoLabel = ''.obs; @@ -481,7 +477,7 @@ class VideoDetailController extends GetxController 'userID': GStorage.blockUserID, 'type': type, }, - options: _options, + options: options, ) .then((res) { SmartDialog.showToast(res.statusCode == 200 ? '投票成功' : '投票失败'); @@ -510,7 +506,7 @@ class VideoDetailController extends GetxController 'userID': GStorage.blockUserID, 'category': item.name, }, - options: _options, + options: options, ) .then((res) { SmartDialog.showToast( @@ -705,25 +701,25 @@ class VideoDetailController extends GetxController ); } - Options get _options => Options(extra: {'clearCookie': true}); + Options get options => Options(extra: {'clearCookie': true}); Future _querySponsorBlock() async { positionSubscription?.cancel(); videoLabel.value = ''; segmentList.clear(); - _segmentProgressList = null; + segmentProgressList = null; dynamic result = await Request().get( '${GStorage.blockServer}/api/skipSegments', queryParameters: { 'videoID': bvid, 'cid': cid.value, }, - options: _options, + options: options, ); - _handleSBData(result); + handleSBData(result); } - void _handleSBData(result) { + void handleSBData(result) { if (result.data is List && result.data.isNotEmpty) { try { List list = @@ -767,8 +763,8 @@ class VideoDetailController extends GetxController ).toList()); // _segmentProgressList - _segmentProgressList ??= []; - _segmentProgressList!.addAll(segmentList.map((item) { + segmentProgressList ??= []; + segmentProgressList!.addAll(segmentList.map((item) { double start = (item.segment.first / ((data.timeLength ?? 0) / 1000)) .clamp(0.0, 1.0); double end = (item.segment.second / ((data.timeLength ?? 0) / 1000)) @@ -789,7 +785,7 @@ class VideoDetailController extends GetxController : -1; } - void _initSkip() { + void initSkip() { if (segmentList.isNotEmpty) { positionSubscription = plPlayerController .videoPlayerController?.stream.position @@ -920,7 +916,7 @@ class VideoDetailController extends GetxController Request().post( '${GStorage.blockServer}/api/viewedVideoSponsorTime', queryParameters: {'UUID': item.UUID}, - options: _options, + options: options, ); } } catch (e) { @@ -1073,7 +1069,7 @@ class VideoDetailController extends GetxController 'referer': HttpString.baseUrl }, ), - segmentList: _segmentProgressList, + segmentList: segmentProgressList, viewPointList: viewPointList, vttSubtitles: _vttSubtitles, vttSubtitlesIndex: vttSubtitlesIndex, @@ -1102,7 +1098,7 @@ class VideoDetailController extends GetxController }, ); - _initSkip(); + initSkip(); if (vttSubtitlesIndex == null) { _getSubtitle(); @@ -1337,601 +1333,24 @@ class VideoDetailController extends GetxController } if (plPlayerController.isFullScreen.value) { Utils.showFSSheet( - child: _postPanel(), - isFullScreen: plPlayerController.isFullScreen.value, + child: PostPanel( + enableSlide: false, + videoDetailController: this, + plPlayerController: plPlayerController, + ), + isFullScreen: () => plPlayerController.isFullScreen.value, ); } else { childKey.currentState?.showBottomSheet( - enableDrag: false, backgroundColor: Colors.transparent, - (context) => GStorage.collapsibleVideoPage - ? ViewPointsPage(child: _postPanel()) - : _postPanel(), + (context) => PostPanel( + videoDetailController: this, + plPlayerController: plPlayerController, + ), ); } } - Widget _postPanel() => StatefulBuilder( - builder: (context, setState) { - void updateSegment({ - required bool isFirst, - required int index, - required int 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({ - 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: plPlayerController.positionSeconds.value, - ); - }); - }, - ), - 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.of(context).colorScheme.outline), - ), - ), - TextButton( - onPressed: () => Get.back(result: initV), - child: Text('确定'), - ), - ], - ); - }, - ).then((res) { - if (res != null) { - try { - List split = (res as String) - .split(':') - .toList() - .reversed - .toList() - .map((e) => int.parse(e)) - .toList(); - int duration = 0; - for (int i = 0; i < split.length; i++) { - duration += split[i] * pow(60, i).toInt(); - } - if (duration <= - plPlayerController - .durationSeconds.value.inSeconds) { - setState(() { - updateSegment( - isFirst: isFirst, - index: index, - value: duration, - ); - }); - } - } catch (e) { - debugPrint(e.toString()); - } - } - }); - }, - ), - ]; - } - - 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: plPlayerController.positionSeconds.value, - ), - 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: list?.isNotEmpty == true - ? Stack( - children: [ - SingleChildScrollView( - controller: ScrollController(), - physics: const AlwaysScrollableScrollPhysics(), - child: Column( - children: [ - ...List.generate( - list!.length, - (index) => Stack( - children: [ - Container( - width: double.infinity, - margin: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 5, - ), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context) - .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( - isFirst: true, - index: index, - ), - ), - if (list![index].category != - SegmentType.poi_highlight) - Row( - mainAxisSize: - MainAxisSize.min, - children: segmentWidget( - 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< - SegmentType>( - 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.of(context) - .colorScheme - .secondary, - ), - strutStyle: StrutStyle( - height: 1, - leading: 0, - ), - ), - Icon( - MdiIcons - .unfoldMoreHorizontal, - size: MediaQuery - .textScalerOf( - context) - .scale(14), - color: Theme.of(context) - .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< - ActionType>( - 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.of(context) - .colorScheme - .secondary, - ), - strutStyle: StrutStyle( - height: 1, - leading: 0, - ), - ), - Icon( - MdiIcons - .unfoldMoreHorizontal, - size: MediaQuery - .textScalerOf( - context) - .scale(14), - color: Theme.of(context) - .colorScheme - .secondary, - ), - ], - ), - ), - ], - ), - ], - ), - ], - ), - ), - Positioned( - top: 10, - right: 21, - child: iconButton( - context: context, - size: 26, - tooltip: '移除', - icon: Icons.clear, - onPressed: () { - setState(() { - list!.removeAt(index); - }); - }, - ), - ), - ], - ), - ), - 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.of(context) - .colorScheme - .outline, - ), - ), - ), - TextButton( - onPressed: () { - Get.back(); - Request() - .post( - '${GStorage.blockServer}/api/skipSegments', - queryParameters: { - 'videoID': bvid, - 'cid': cid.value, - 'userID': GStorage.blockUserID, - 'userAgent': Constants.userAgent, - 'videoDuration': plPlayerController - .durationSeconds.value.inSeconds, - }, - data: { - 'segments': list! - .map( - (item) => { - 'segment': [ - item.segment.first, - item.segment.second, - ], - 'category': - item.category.name, - 'actionType': - item.actionType.name, - }, - ) - .toList(), - }, - options: _options, - ) - .then( - (res) { - if (res.statusCode == 200) { - Get.back(); - SmartDialog.showToast('提交成功'); - list?.clear(); - _handleSBData(res); - plPlayerController - .segmentList.value = - _segmentProgressList ?? - []; - if (positionSubscription == null) { - _initSkip(); - } - } else { - SmartDialog.showToast( - '提交失败: ${{ - 400: '参数错误', - 403: '被自动审核机制拒绝', - 429: '重复提交太快', - 409: '重复提交' - }[res.statusCode]}', - ); - } - }, - ); - }, - child: const Text('确定提交'), - ), - ], - ), - ); - }, - child: Icon(Icons.check), - ), - ) - ], - ) - : errorWidget(), - ); - }, - ); - - // List _actionType2SegmentType(ActionType actionType) { - // return switch (actionType) { - // ActionType.skip => [ - // SegmentType.sponsor, - // SegmentType.selfpromo, - // SegmentType.interaction, - // SegmentType.intro, - // SegmentType.outro, - // SegmentType.preview, - // SegmentType.filler, - // ], - // ActionType.mute => [ - // SegmentType.sponsor, - // SegmentType.selfpromo, - // SegmentType.interaction, - // SegmentType.intro, - // SegmentType.outro, - // SegmentType.preview, - // SegmentType.music_offtopic, - // SegmentType.filler, - // ], - // ActionType.full => [ - // SegmentType.sponsor, - // SegmentType.selfpromo, - // SegmentType.exclusive_access, - // ], - // ActionType.poi => [ - // SegmentType.poi_highlight, - // ], - // }; - // } - - List _segmentType2ActionType(SegmentType segmentType) { - return switch (segmentType) { - SegmentType.sponsor => [ - ActionType.skip, - ActionType.mute, - ActionType.full - ], - SegmentType.selfpromo => [ - ActionType.skip, - ActionType.mute, - ActionType.full - ], - SegmentType.interaction => [ - ActionType.skip, - ActionType.mute, - ], - SegmentType.intro => [ - ActionType.skip, - ActionType.mute, - ], - SegmentType.outro => [ - ActionType.skip, - ActionType.mute, - ], - SegmentType.preview => [ - ActionType.skip, - ActionType.mute, - ], - SegmentType.music_offtopic => [ - ActionType.skip, - ], - SegmentType.poi_highlight => [ActionType.poi], - SegmentType.filler => [ - ActionType.skip, - ActionType.mute, - ], - SegmentType.exclusive_access => [ActionType.full], - }; - } - late List> _vttSubtitles = >[]; int? vttSubtitlesIndex; late bool showVP = true; @@ -2139,7 +1558,7 @@ class VideoDetailController extends GetxController positionSubscription?.cancel(); videoLabel.value = ''; segmentList.clear(); - _segmentProgressList = null; + segmentProgressList = null; } // interactive video @@ -2177,11 +1596,14 @@ class VideoDetailController extends GetxController } } - void showNoteList() async { + void showNoteList(BuildContext context) async { if (plPlayerController.isFullScreen.value) { Utils.showFSSheet( - child: NoteListPage(oid: oid.value), - isFullScreen: plPlayerController.isFullScreen.value, + child: NoteListPage( + oid: oid.value, + enableSlide: false, + ), + isFullScreen: () => plPlayerController.isFullScreen.value, ); } else { childKey.currentState?.showBottomSheet( diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index bd72267d..a77c0b3e 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -35,13 +35,11 @@ class VideoIntroPanel extends StatefulWidget { super.key, required this.heroTag, required this.showAiBottomSheet, - required this.showIntroDetail, required this.showEpisodes, required this.onShowMemberPage, }); final String heroTag; final Function showAiBottomSheet; - final Function showIntroDetail; final Function showEpisodes; final ValueChanged onShowMemberPage; @@ -75,10 +73,6 @@ class _VideoIntroPanelState extends State videoIntroController: videoIntroController, heroTag: widget.heroTag, showAiBottomSheet: widget.showAiBottomSheet, - showIntroDetail: () => widget.showIntroDetail( - videoIntroController.videoDetail.value, - videoIntroController.videoTags, - ), showEpisodes: widget.showEpisodes, onShowMemberPage: widget.onShowMemberPage, ) @@ -88,10 +82,6 @@ class _VideoIntroPanelState extends State videoIntroController: videoIntroController, heroTag: widget.heroTag, showAiBottomSheet: widget.showAiBottomSheet, - showIntroDetail: () => widget.showIntroDetail( - videoIntroController.videoDetail.value, - videoIntroController.videoTags, - ), showEpisodes: widget.showEpisodes, onShowMemberPage: widget.onShowMemberPage, ), @@ -103,7 +93,6 @@ class VideoInfo extends StatefulWidget { final bool loadingStatus; final String heroTag; final Function showAiBottomSheet; - final Function showIntroDetail; final Function showEpisodes; final ValueChanged onShowMemberPage; final VideoIntroController videoIntroController; @@ -113,7 +102,6 @@ class VideoInfo extends StatefulWidget { this.loadingStatus = false, required this.heroTag, required this.showAiBottomSheet, - required this.showIntroDetail, required this.showEpisodes, required this.onShowMemberPage, required this.videoIntroController, diff --git a/lib/pages/video/detail/note/note_list_page.dart b/lib/pages/video/detail/note/note_list_page.dart index 1db51d68..55202fc7 100644 --- a/lib/pages/video/detail/note/note_list_page.dart +++ b/lib/pages/video/detail/note/note_list_page.dart @@ -4,14 +4,20 @@ import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/common/widgets/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/common/common_slide_page.dart'; import 'package:PiliPlus/pages/video/detail/note/note_list_page_ctr.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -class NoteListPage extends StatefulWidget { - const NoteListPage({super.key, this.oid, this.upperMid}); +class NoteListPage extends CommonSlidePage { + const NoteListPage({ + super.key, + super.enableSlide, + this.oid, + this.upperMid, + }); final dynamic oid; final dynamic upperMid; @@ -20,7 +26,7 @@ class NoteListPage extends StatefulWidget { State createState() => _NoteListPageState(); } -class _NoteListPageState extends State { +class _NoteListPageState extends CommonSlidePageState { late final _controller = Get.put( NoteListPageCtr(oid: widget.oid, upperMid: widget.upperMid), ); @@ -32,37 +38,37 @@ class _NoteListPageState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - titleSpacing: 16, - toolbarHeight: 45, - title: Obx( - () => Text( - '笔记${_controller.count.value == -1 ? '' : '(${_controller.count.value})'}'), - ), - bottom: PreferredSize( - preferredSize: Size.fromHeight(1), - child: Divider( - height: 1, - color: Theme.of(context).colorScheme.outline.withOpacity(0.1), + Widget get buildPage => Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + titleSpacing: 16, + toolbarHeight: 45, + title: Obx( + () => Text( + '笔记${_controller.count.value == -1 ? '' : '(${_controller.count.value})'}'), ), - ), - actions: [ - iconButton( - context: context, - tooltip: '关闭', - icon: Icons.clear, - onPressed: Get.back, - size: 32, + bottom: PreferredSize( + preferredSize: Size.fromHeight(1), + child: Divider( + height: 1, + color: Theme.of(context).colorScheme.outline.withOpacity(0.1), + ), ), - const SizedBox(width: 16), - ], - ), - body: Obx(() => _buildBody(_controller.loadingState.value)), - ); - } + actions: [ + iconButton( + context: context, + tooltip: '关闭', + icon: Icons.clear, + onPressed: Get.back, + size: 32, + ), + const SizedBox(width: 16), + ], + ), + body: enableSlide + ? slideList(Obx(() => _buildBody(_controller.loadingState.value))) + : Obx(() => _buildBody(_controller.loadingState.value)), + ); Widget _buildBody(LoadingState loadingState) { return switch (loadingState) { diff --git a/lib/pages/video/detail/post_panel/post_panel.dart b/lib/pages/video/detail/post_panel/post_panel.dart new file mode 100644 index 00000000..9d24b8de --- /dev/null +++ b/lib/pages/video/detail/post_panel/post_panel.dart @@ -0,0 +1,548 @@ +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_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: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 CommonSlidePage { + 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 CommonSlidePageState { + late bool _isInit = true; + VideoDetailController get videoDetailController => + widget.videoDetailController; + PlPlayerController get plPlayerController => widget.plPlayerController; + List? get list => videoDetailController.list; + + @override + void initState() { + super.initState(); + if (enableSlide && GStorage.collapsibleVideoPage) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _isInit = false; + }); + } + }); + } + } + + @override + Widget build(BuildContext context) { + if (enableSlide && GStorage.collapsibleVideoPage && _isInit) { + return CustomScrollView( + physics: const NeverScrollableScrollPhysics(), + ); + } + + return enableSlide + ? Padding( + padding: EdgeInsets.only(top: padding), + child: buildPage, + ) + : buildPage; + } + + @override + Widget get buildPage => 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: plPlayerController.positionSeconds.value, + ), + 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() : buildList, + ); + + @override + Widget get buildList => list?.isNotEmpty == true + ? Stack( + children: [ + SingleChildScrollView( + controller: ScrollController(), + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + ...List.generate( + list!.length, + (index) => Stack( + children: [ + Container( + width: double.infinity, + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 5, + ), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: + Theme.of(context).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( + isFirst: true, + index: index, + ), + ), + if (list![index].category != + SegmentType.poi_highlight) + Row( + mainAxisSize: MainAxisSize.min, + children: segmentWidget( + 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.of(context) + .colorScheme + .secondary, + ), + strutStyle: StrutStyle( + height: 1, + leading: 0, + ), + ), + Icon( + MdiIcons.unfoldMoreHorizontal, + size: MediaQuery.textScalerOf( + context) + .scale(14), + color: Theme.of(context) + .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.of(context) + .colorScheme + .secondary, + ), + strutStyle: StrutStyle( + height: 1, + leading: 0, + ), + ), + Icon( + MdiIcons.unfoldMoreHorizontal, + size: MediaQuery.textScalerOf( + context) + .scale(14), + color: Theme.of(context) + .colorScheme + .secondary, + ), + ], + ), + ), + ], + ), + ], + ), + ], + ), + ), + Positioned( + top: 10, + right: 21, + child: iconButton( + context: context, + size: 26, + tooltip: '移除', + icon: Icons.clear, + onPressed: () { + setState(() { + list!.removeAt(index); + }); + }, + ), + ), + ], + ), + ), + 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.of(context).colorScheme.outline, + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + Request() + .post( + '${GStorage.blockServer}/api/skipSegments', + queryParameters: { + 'videoID': videoDetailController.bvid, + 'cid': videoDetailController.cid.value, + 'userID': GStorage.blockUserID, + 'userAgent': Constants.userAgent, + 'videoDuration': plPlayerController + .durationSeconds.value.inSeconds, + }, + data: { + 'segments': list! + .map( + (item) => { + 'segment': [ + item.segment.first, + item.segment.second, + ], + 'category': item.category.name, + 'actionType': item.actionType.name, + }, + ) + .toList(), + }, + options: videoDetailController.options, + ) + .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( + '提交失败: ${{ + 400: '参数错误', + 403: '被自动审核机制拒绝', + 429: '重复提交太快', + 409: '重复提交' + }[res.statusCode] ?? res.statusCode}', + ); + } + }, + ); + }, + child: const Text('确定提交'), + ), + ], + ), + ); + }, + child: Icon(Icons.check), + ), + ) + ], + ) + : errorWidget(); + + void updateSegment({ + required bool isFirst, + required int index, + required int 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({ + 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: plPlayerController.positionSeconds.value, + ); + }); + }, + ), + 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.of(context).colorScheme.outline), + ), + ), + TextButton( + onPressed: () => Get.back(result: initV), + child: Text('确定'), + ), + ], + ); + }, + ).then((res) { + if (res != null) { + try { + List split = (res as String) + .split(':') + .toList() + .reversed + .toList() + .map((e) => int.parse(e)) + .toList(); + int duration = 0; + for (int i = 0; i < split.length; i++) { + duration += split[i] * pow(60, i).toInt(); + } + if (duration <= + plPlayerController.durationSeconds.value.inSeconds) { + setState(() { + updateSegment( + isFirst: isFirst, + index: index, + value: duration, + ); + }); + } + } catch (e) { + debugPrint(e.toString()); + } + } + }); + }, + ), + ]; + } +} diff --git a/lib/pages/video/detail/reply_reply/view.dart b/lib/pages/video/detail/reply_reply/view.dart index 0ec0dd07..8c48f432 100644 --- a/lib/pages/video/detail/reply_reply/view.dart +++ b/lib/pages/video/detail/reply_reply/view.dart @@ -3,12 +3,12 @@ import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/video/reply/item.dart'; +import 'package:PiliPlus/pages/common/common_slide_page.dart'; import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item.dart'; import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/detail/reply_new/reply_page.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/global_data.dart'; -import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -19,7 +19,7 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'controller.dart'; -class VideoReplyReplyPanel extends StatefulWidget { +class VideoReplyReplyPanel extends CommonSlidePage { const VideoReplyReplyPanel({ super.key, this.id, @@ -52,7 +52,8 @@ class VideoReplyReplyPanel extends StatefulWidget { State createState() => _VideoReplyReplyPanelState(); } -class _VideoReplyReplyPanelState extends State +class _VideoReplyReplyPanelState + extends CommonSlidePageState with TickerProviderStateMixin { late VideoReplyReplyController _videoReplyReplyController; late final _savedReplies = {}; @@ -116,21 +117,8 @@ class _VideoReplyReplyPanelState extends State }, ); - Offset? _downPos; - bool? _isSliding; - late final Rx padding = 0.0.obs; - @override - Widget build(BuildContext context) { - return GStorage.slideDismissReplyPage - ? Padding( - padding: EdgeInsets.only(top: padding.value), - child: _buildPage, - ) - : _buildPage; - } - - Widget get _buildPage => Scaffold( + Widget get buildPage => Scaffold( key: _key, resizeToAvoidBottomInset: false, body: Column( @@ -165,73 +153,14 @@ class _VideoReplyReplyPanelState extends State color: Theme.of(context).dividerColor.withOpacity(0.1), ), Expanded( - child: GStorage.slideDismissReplyPage - ? GestureDetector( - onPanDown: (event) { - if (event.localPosition.dx > 30) { - _isSliding = false; - } else { - _downPos = event.localPosition; - } - }, - onPanUpdate: (event) { - if (_isSliding == false) { - return; - } else if (_isSliding == null) { - if (_downPos != null) { - Offset cumulativeDelta = - event.localPosition - _downPos!; - if (cumulativeDelta.dx.abs() >= - cumulativeDelta.dy.abs()) { - _isSliding = true; - setState(() { - padding.value = event.localPosition.dx; - }); - } else { - _isSliding = false; - } - } - } else if (_isSliding == true) { - setState(() { - padding.value = event.localPosition.dx; - }); - } - }, - onPanCancel: () { - if (_isSliding == true) { - if (padding.value >= 100) { - Get.back(); - } else { - setState(() { - padding.value = 0; - }); - } - } - _downPos = null; - _isSliding = null; - }, - onPanEnd: (event) { - if (_isSliding == true) { - if (padding.value >= 100) { - Get.back(); - } else { - setState(() { - padding.value = 0; - }); - } - } - _downPos = null; - _isSliding = null; - }, - child: _buildList, - ) - : _buildList, + child: enableSlide ? slideList() : buildList, ), ], ), ); - Widget get _buildList => ClipRect( + @override + Widget get buildList => ClipRect( child: refreshIndicator( onRefresh: () async { await _videoReplyReplyController.onRefresh(); diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index b4fffed2..ba91b284 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -3,9 +3,7 @@ import 'dart:io'; import 'dart:math'; import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/widgets/icon_button.dart'; import 'package:PiliPlus/common/widgets/list_sheet.dart'; -import 'package:PiliPlus/common/widgets/segment_progress_bar.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/bangumi/info.dart'; import 'package:PiliPlus/models/common/reply_type.dart'; @@ -17,6 +15,7 @@ import 'package:PiliPlus/pages/video/detail/introduction/widgets/page.dart'; import 'package:PiliPlus/pages/video/detail/introduction/widgets/season.dart'; import 'package:PiliPlus/pages/video/detail/member/horizontal_member_page.dart'; import 'package:PiliPlus/pages/video/detail/reply_reply/view.dart'; +import 'package:PiliPlus/pages/video/detail/view_point/view_points_page.dart'; import 'package:PiliPlus/pages/video/detail/widgets/ai_detail.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/global_data.dart'; @@ -1105,7 +1104,7 @@ class _VideoDetailPageState extends State } break; case 'note': - videoDetailController.showNoteList(); + videoDetailController.showNoteList(context); break; } }, @@ -1546,7 +1545,6 @@ class _VideoDetailPageState extends State VideoIntroPanel( heroTag: heroTag, showAiBottomSheet: showAiBottomSheet, - showIntroDetail: showIntroDetail, showEpisodes: showEpisodes, onShowMemberPage: onShowMemberPage, ), @@ -1806,15 +1804,14 @@ class _VideoDetailPageState extends State // ai总结 showAiBottomSheet() { videoDetailController.childKey.currentState?.showBottomSheet( - enableDrag: true, - backgroundColor: Theme.of(context).colorScheme.surface, + backgroundColor: Colors.transparent, (context) => AiDetail(modelResult: videoIntroController.modelResult), ); } showIntroDetail(videoDetail, videoTags) { videoDetailController.childKey.currentState?.showBottomSheet( - enableDrag: true, + shape: const RoundedRectangleBorder(), backgroundColor: Theme.of(context).colorScheme.surface, (context) => videoDetail is BangumiInfoModel ? bangumi.IntroDetail( @@ -1829,7 +1826,8 @@ class _VideoDetailPageState extends State } showEpisodes(index, season, episodes, bvid, aid, cid) { - Widget listSheetContent() => ListSheetContent( + Widget listSheetContent([bool? enableSlide]) => ListSheetContent( + enableSlide: enableSlide, index: index, season: season, bvid: bvid, @@ -1860,15 +1858,12 @@ class _VideoDetailPageState extends State ); if (isFullScreen) { Utils.showFSSheet( - child: Material( - color: Theme.of(context).colorScheme.surface, - child: listSheetContent(), - ), - isFullScreen: isFullScreen, + child: listSheetContent(false), + isFullScreen: () => isFullScreen, ); } else { videoDetailController.childKey.currentState?.showBottomSheet( - backgroundColor: Theme.of(context).colorScheme.surface, + backgroundColor: Colors.transparent, (context) => listSheetContent(), ); } @@ -1945,150 +1940,22 @@ class _VideoDetailPageState extends State } void showViewPoints() { - Widget listSheetContent() { - int currentIndex = -1; - return StatefulBuilder( - builder: (context, setState) => Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - titleSpacing: 16, - title: const Text('分段信息'), - actions: [ - Text( - '分段进度条', - style: TextStyle(fontSize: 16), - ), - Obx( - () => Transform.scale( - alignment: Alignment.centerLeft, - scale: 0.8, - child: Switch( - thumbIcon: WidgetStateProperty.resolveWith((states) { - if (states.isNotEmpty && - states.first == WidgetState.selected) { - return const Icon(Icons.done); - } - return null; - }), - value: - videoDetailController.plPlayerController.showVP.value, - onChanged: (value) { - videoDetailController.plPlayerController.showVP.value = - value; - }, - ), - ), - ), - iconButton( - context: context, - size: 30, - icon: Icons.clear, - tooltip: '关闭', - onPressed: Get.back, - ), - const SizedBox(width: 16), - ], - ), - body: SingleChildScrollView( - child: Column( - children: [ - ...List.generate( - videoDetailController.viewPointList.length * 2 - 1, - (rawIndex) { - if (rawIndex % 2 == 1) { - return Divider( - height: 1, - color: Theme.of(context).dividerColor.withOpacity(0.1), - ); - } - int index = rawIndex ~/ 2; - Segment segment = videoDetailController.viewPointList[index]; - if (currentIndex == -1 && - segment.from != null && - segment.to != null) { - if (videoDetailController - .plPlayerController.positionSeconds.value >= - segment.from! && - videoDetailController - .plPlayerController.positionSeconds.value < - segment.to!) { - currentIndex = index; - } - } - return ListTile( - dense: true, - onTap: segment.from != null - ? () { - currentIndex = index; - plPlayerController?.danmakuController?.clear(); - plPlayerController?.videoPlayerController - ?.seek(Duration(seconds: segment.from!)); - Get.back(); - } - : null, - leading: segment.url?.isNotEmpty == true - ? Container( - margin: const EdgeInsets.symmetric(vertical: 6), - decoration: currentIndex == index - ? BoxDecoration( - borderRadius: BorderRadius.circular(6), - border: Border.all( - width: 1.8, - strokeAlign: - BorderSide.strokeAlignOutside, - color: - Theme.of(context).colorScheme.primary, - ), - ) - : null, - child: LayoutBuilder( - builder: (context, constraints) => - NetworkImgLayer( - radius: 6, - src: segment.url, - width: constraints.maxHeight * - StyleString.aspectRatio, - height: constraints.maxHeight, - ), - ), - ) - : null, - title: Text( - segment.title ?? '', - style: TextStyle( - fontSize: 14, - fontWeight: - currentIndex == index ? FontWeight.bold : null, - color: currentIndex == index - ? Theme.of(context).colorScheme.primary - : null, - ), - ), - subtitle: Text( - '${segment.from != null ? Utils.timeFormat(segment.from) : ''} - ${segment.to != null ? Utils.timeFormat(segment.to) : ''}', - style: TextStyle( - fontSize: 13, - color: currentIndex == index - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, - ), - ), - ); - }), - SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom), - ], - ), - ), - ), - ); - } - if (isFullScreen) { - Utils.showFSSheet(child: listSheetContent(), isFullScreen: isFullScreen); + Utils.showFSSheet( + child: ViewPointsPage( + enableSlide: false, + videoDetailController: videoDetailController, + plPlayerController: plPlayerController, + ), + isFullScreen: () => isFullScreen, + ); } else { videoDetailController.childKey.currentState?.showBottomSheet( backgroundColor: Colors.transparent, - (context) => listSheetContent(), + (context) => ViewPointsPage( + videoDetailController: videoDetailController, + plPlayerController: plPlayerController, + ), ); } } @@ -2109,6 +1976,7 @@ class _VideoDetailPageState extends State void onShowMemberPage(mid) { videoDetailController.childKey.currentState?.showBottomSheet( + shape: const RoundedRectangleBorder(), backgroundColor: Theme.of(context).colorScheme.surface, (context) { return HorizontalMemberPage( @@ -2117,7 +1985,6 @@ class _VideoDetailPageState extends State videoIntroController: videoIntroController, ); }, - enableDrag: true, ); } } diff --git a/lib/pages/video/detail/view_point/view_points_page.dart b/lib/pages/video/detail/view_point/view_points_page.dart new file mode 100644 index 00000000..436fc1a7 --- /dev/null +++ b/lib/pages/video/detail/view_point/view_points_page.dart @@ -0,0 +1,199 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/icon_button.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/segment_progress_bar.dart'; +import 'package:PiliPlus/pages/common/common_slide_page.dart'; +import 'package:PiliPlus/pages/video/detail/index.dart'; +import 'package:PiliPlus/plugin/pl_player/index.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class ViewPointsPage extends CommonSlidePage { + const ViewPointsPage({ + super.key, + super.enableSlide, + required this.videoDetailController, + required this.plPlayerController, + }); + + final VideoDetailController videoDetailController; + final PlPlayerController? plPlayerController; + + @override + State createState() => _ViewPointsPageState(); +} + +class _ViewPointsPageState extends CommonSlidePageState { + late bool _isInit = true; + VideoDetailController get videoDetailController => + widget.videoDetailController; + PlPlayerController? get plPlayerController => widget.plPlayerController; + + @override + void initState() { + super.initState(); + if (enableSlide && GStorage.collapsibleVideoPage) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _isInit = false; + }); + } + }); + } + } + + int currentIndex = -1; + + @override + Widget build(BuildContext context) { + if (enableSlide && GStorage.collapsibleVideoPage && _isInit) { + return CustomScrollView( + physics: const NeverScrollableScrollPhysics(), + ); + } + + return enableSlide + ? Padding( + padding: EdgeInsets.only(top: padding), + child: buildPage, + ) + : buildPage; + } + + @override + Widget get buildPage => Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + automaticallyImplyLeading: false, + titleSpacing: 16, + title: const Text('分段信息'), + actions: [ + Text( + '分段进度条', + style: TextStyle(fontSize: 16), + ), + Obx( + () => Transform.scale( + alignment: Alignment.centerLeft, + scale: 0.8, + child: Switch( + thumbIcon: WidgetStateProperty.resolveWith((states) { + if (states.isNotEmpty && + states.first == WidgetState.selected) { + return const Icon(Icons.done); + } + return null; + }), + value: videoDetailController.plPlayerController.showVP.value, + onChanged: (value) { + videoDetailController.plPlayerController.showVP.value = + value; + }, + ), + ), + ), + iconButton( + context: context, + size: 30, + icon: Icons.clear, + tooltip: '关闭', + onPressed: Get.back, + ), + const SizedBox(width: 16), + ], + ), + body: enableSlide ? slideList() : buildList, + ); + + @override + Widget get buildList => SingleChildScrollView( + controller: ScrollController(), + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + ...List.generate(videoDetailController.viewPointList.length * 2 - 1, + (rawIndex) { + if (rawIndex % 2 == 1) { + return Divider( + height: 1, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ); + } + int index = rawIndex ~/ 2; + Segment segment = videoDetailController.viewPointList[index]; + if (currentIndex == -1 && + segment.from != null && + segment.to != null) { + if (videoDetailController + .plPlayerController.positionSeconds.value >= + segment.from! && + videoDetailController + .plPlayerController.positionSeconds.value < + segment.to!) { + currentIndex = index; + } + } + return ListTile( + dense: true, + onTap: segment.from != null + ? () { + currentIndex = index; + plPlayerController?.danmakuController?.clear(); + plPlayerController?.videoPlayerController + ?.seek(Duration(seconds: segment.from!)); + Get.back(); + } + : null, + leading: segment.url?.isNotEmpty == true + ? Container( + margin: const EdgeInsets.symmetric(vertical: 6), + decoration: currentIndex == index + ? BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: Border.all( + width: 1.8, + strokeAlign: BorderSide.strokeAlignOutside, + color: Theme.of(context).colorScheme.primary, + ), + ) + : null, + child: LayoutBuilder( + builder: (context, constraints) => NetworkImgLayer( + radius: 6, + src: segment.url, + width: + constraints.maxHeight * StyleString.aspectRatio, + height: constraints.maxHeight, + ), + ), + ) + : null, + title: Text( + segment.title ?? '', + style: TextStyle( + fontSize: 14, + fontWeight: currentIndex == index ? FontWeight.bold : null, + color: currentIndex == index + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + subtitle: Text( + '${segment.from != null ? Utils.timeFormat(segment.from) : ''} - ${segment.to != null ? Utils.timeFormat(segment.to) : ''}', + style: TextStyle( + fontSize: 13, + color: currentIndex == index + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + ), + ), + ); + }), + SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom), + ], + ), + ); +} diff --git a/lib/pages/video/detail/view_v.dart b/lib/pages/video/detail/view_v.dart index 49d46b6e..3b117333 100644 --- a/lib/pages/video/detail/view_v.dart +++ b/lib/pages/video/detail/view_v.dart @@ -4,9 +4,7 @@ import 'dart:math'; import 'dart:ui'; import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/widgets/icon_button.dart'; import 'package:PiliPlus/common/widgets/list_sheet.dart'; -import 'package:PiliPlus/common/widgets/segment_progress_bar.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/bangumi/info.dart'; import 'package:PiliPlus/models/common/reply_type.dart'; @@ -18,6 +16,7 @@ import 'package:PiliPlus/pages/video/detail/introduction/widgets/page.dart'; import 'package:PiliPlus/pages/video/detail/introduction/widgets/season.dart'; import 'package:PiliPlus/pages/video/detail/member/horizontal_member_page.dart'; import 'package:PiliPlus/pages/video/detail/reply_reply/view.dart'; +import 'package:PiliPlus/pages/video/detail/view_point/view_points_page.dart'; import 'package:PiliPlus/pages/video/detail/widgets/ai_detail.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/global_data.dart'; @@ -35,7 +34,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; -import 'package:PiliPlus/common/widgets/network_img_layer.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/models/common/search_type.dart'; import 'package:PiliPlus/pages/bangumi/introduction/index.dart'; @@ -1398,7 +1396,7 @@ class _VideoDetailPageVState extends State } break; case 'note': - videoDetailController.showNoteList(); + videoDetailController.showNoteList(context); break; } }, @@ -1862,7 +1860,6 @@ class _VideoDetailPageVState extends State VideoIntroPanel( heroTag: heroTag, showAiBottomSheet: showAiBottomSheet, - showIntroDetail: showIntroDetail, showEpisodes: showEpisodes, onShowMemberPage: onShowMemberPage, ), @@ -2126,15 +2123,14 @@ class _VideoDetailPageVState extends State // ai总结 showAiBottomSheet() { videoDetailController.childKey.currentState?.showBottomSheet( - enableDrag: true, - backgroundColor: Theme.of(context).colorScheme.surface, + backgroundColor: Colors.transparent, (context) => AiDetail(modelResult: videoIntroController.modelResult), ); } showIntroDetail(videoDetail, videoTags) { videoDetailController.childKey.currentState?.showBottomSheet( - enableDrag: true, + shape: const RoundedRectangleBorder(), backgroundColor: Theme.of(context).colorScheme.surface, (context) => videoDetail is BangumiInfoModel ? bangumi.IntroDetail( @@ -2149,7 +2145,8 @@ class _VideoDetailPageVState extends State } showEpisodes(index, season, episodes, bvid, aid, cid) { - Widget listSheetContent() => ListSheetContent( + Widget listSheetContent([bool? enableSlide]) => ListSheetContent( + enableSlide: enableSlide, index: index, season: season, bvid: bvid, @@ -2180,15 +2177,12 @@ class _VideoDetailPageVState extends State ); if (isFullScreen) { Utils.showFSSheet( - child: Material( - color: Theme.of(context).colorScheme.surface, - child: listSheetContent(), - ), - isFullScreen: isFullScreen, + child: listSheetContent(false), + isFullScreen: () => isFullScreen, ); } else { videoDetailController.childKey.currentState?.showBottomSheet( - backgroundColor: Theme.of(context).colorScheme.surface, + backgroundColor: Colors.transparent, (context) => listSheetContent(), ); } @@ -2265,155 +2259,22 @@ class _VideoDetailPageVState extends State } void showViewPoints() { - Widget listSheetContent() { - int currentIndex = -1; - return StatefulBuilder( - builder: (context, setState) => Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - automaticallyImplyLeading: false, - titleSpacing: 16, - title: const Text('分段信息'), - actions: [ - Text( - '分段进度条', - style: TextStyle(fontSize: 16), - ), - Obx( - () => Transform.scale( - alignment: Alignment.centerLeft, - scale: 0.8, - child: Switch( - thumbIcon: WidgetStateProperty.resolveWith((states) { - if (states.isNotEmpty && - states.first == WidgetState.selected) { - return const Icon(Icons.done); - } - return null; - }), - value: - videoDetailController.plPlayerController.showVP.value, - onChanged: (value) { - videoDetailController.plPlayerController.showVP.value = - value; - }, - ), - ), - ), - iconButton( - context: context, - size: 30, - icon: Icons.clear, - tooltip: '关闭', - onPressed: Get.back, - ), - const SizedBox(width: 16), - ], - ), - body: SingleChildScrollView( - controller: ScrollController(), - physics: const AlwaysScrollableScrollPhysics(), - child: Column( - children: [ - ...List.generate( - videoDetailController.viewPointList.length * 2 - 1, - (rawIndex) { - if (rawIndex % 2 == 1) { - return Divider( - height: 1, - color: Theme.of(context).dividerColor.withOpacity(0.1), - ); - } - int index = rawIndex ~/ 2; - Segment segment = videoDetailController.viewPointList[index]; - if (currentIndex == -1 && - segment.from != null && - segment.to != null) { - if (videoDetailController - .plPlayerController.positionSeconds.value >= - segment.from! && - videoDetailController - .plPlayerController.positionSeconds.value < - segment.to!) { - currentIndex = index; - } - } - return ListTile( - dense: true, - onTap: segment.from != null - ? () { - currentIndex = index; - plPlayerController?.danmakuController?.clear(); - plPlayerController?.videoPlayerController - ?.seek(Duration(seconds: segment.from!)); - Get.back(); - } - : null, - leading: segment.url?.isNotEmpty == true - ? Container( - margin: const EdgeInsets.symmetric(vertical: 6), - decoration: currentIndex == index - ? BoxDecoration( - borderRadius: BorderRadius.circular(6), - border: Border.all( - width: 1.8, - strokeAlign: - BorderSide.strokeAlignOutside, - color: - Theme.of(context).colorScheme.primary, - ), - ) - : null, - child: LayoutBuilder( - builder: (context, constraints) => - NetworkImgLayer( - radius: 6, - src: segment.url, - width: constraints.maxHeight * - StyleString.aspectRatio, - height: constraints.maxHeight, - ), - ), - ) - : null, - title: Text( - segment.title ?? '', - style: TextStyle( - fontSize: 14, - fontWeight: - currentIndex == index ? FontWeight.bold : null, - color: currentIndex == index - ? Theme.of(context).colorScheme.primary - : null, - ), - ), - subtitle: Text( - '${segment.from != null ? Utils.timeFormat(segment.from) : ''} - ${segment.to != null ? Utils.timeFormat(segment.to) : ''}', - style: TextStyle( - fontSize: 13, - color: currentIndex == index - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, - ), - ), - ); - }), - SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom), - ], - ), - ), - ), - ); - } - if (isFullScreen) { - Utils.showFSSheet(child: listSheetContent(), isFullScreen: isFullScreen); + Utils.showFSSheet( + child: ViewPointsPage( + enableSlide: false, + videoDetailController: videoDetailController, + plPlayerController: plPlayerController, + ), + isFullScreen: () => isFullScreen, + ); } else { videoDetailController.childKey.currentState?.showBottomSheet( backgroundColor: Colors.transparent, - (context) => GStorage.collapsibleVideoPage - ? ViewPointsPage(child: listSheetContent()) - : listSheetContent(), + (context) => ViewPointsPage( + videoDetailController: videoDetailController, + plPlayerController: plPlayerController, + ), ); } } @@ -2434,6 +2295,7 @@ class _VideoDetailPageVState extends State void onShowMemberPage(mid) { videoDetailController.childKey.currentState?.showBottomSheet( + shape: const RoundedRectangleBorder(), backgroundColor: Theme.of(context).colorScheme.surface, (context) { return HorizontalMemberPage( @@ -2442,41 +2304,6 @@ class _VideoDetailPageVState extends State videoIntroController: videoIntroController, ); }, - enableDrag: true, ); } } - -class ViewPointsPage extends StatefulWidget { - const ViewPointsPage({super.key, required this.child}); - - final Widget child; - - @override - State createState() => _ViewPointsPageState(); -} - -class _ViewPointsPageState extends State { - bool _isInit = true; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _isInit = false; - }); - } - }); - } - - @override - Widget build(BuildContext context) { - return _isInit - ? CustomScrollView( - physics: const NeverScrollableScrollPhysics(), - ) - : widget.child; - } -} diff --git a/lib/pages/video/detail/widgets/ai_detail.dart b/lib/pages/video/detail/widgets/ai_detail.dart index d287a113..e0fdce22 100644 --- a/lib/pages/video/detail/widgets/ai_detail.dart +++ b/lib/pages/video/detail/widgets/ai_detail.dart @@ -1,13 +1,14 @@ import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/pages/common/common_slide_page.dart'; +import 'package:PiliPlus/pages/video/detail/controller.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/models/video/ai.dart'; -import 'package:PiliPlus/pages/video/detail/index.dart'; import 'package:PiliPlus/utils/utils.dart'; -class AiDetail extends StatelessWidget { +class AiDetail extends CommonSlidePage { final ModelResult modelResult; const AiDetail({ @@ -16,157 +17,10 @@ class AiDetail extends StatelessWidget { }); @override - Widget build(BuildContext context) { - return Container( - color: Theme.of(context).colorScheme.surface, - padding: const EdgeInsets.symmetric(horizontal: 14), - // height: Utils.getSheetHeight(context), - child: Column( - children: [ - InkWell( - onTap: Get.back, - child: Container( - height: 35, - padding: const EdgeInsets.only(bottom: 2), - child: Center( - child: Container( - width: 32, - height: 3, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: const BorderRadius.all(Radius.circular(3)), - ), - ), - ), - ), - ), - Expanded( - child: SingleChildScrollView( - controller: ScrollController(), - physics: const AlwaysScrollableScrollPhysics(), - child: Column( - children: [ - if (modelResult.summary?.isNotEmpty == true) ...[ - SelectableText( - '总结: ${modelResult.summary}', - style: const TextStyle( - fontSize: 15, - height: 1.5, - ), - ), - if (modelResult.outline?.isNotEmpty == true) - Divider( - height: 20, - color: Theme.of(context).dividerColor.withOpacity(0.1), - thickness: 6, - ) - ], - if (modelResult.outline?.isNotEmpty == true) - ListView.builder( - shrinkWrap: true, - itemCount: modelResult.outline!.length, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - return Column( - children: [ - SelectableText( - modelResult.outline![index].title!, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - height: 1.5, - ), - ), - const SizedBox(height: 6), - if (modelResult - .outline![index].partOutline?.isNotEmpty == - true) - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: modelResult - .outline![index].partOutline!.length, - itemBuilder: (context, i) { - return Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Wrap( - children: [ - SelectableText.rich( - TextSpan( - style: TextStyle( - fontSize: 13, - color: Theme.of(context) - .colorScheme - .onSurface, - height: 1.5, - ), - children: [ - TextSpan( - text: Utils.tampToSeektime( - modelResult - .outline![index] - .partOutline![i] - .timestamp!), - style: TextStyle( - color: Theme.of(context) - .colorScheme - .primary, - ), - recognizer: - TapGestureRecognizer() - ..onTap = () { - // 跳转到指定位置 - try { - Get.find( - tag: Get.arguments[ - 'heroTag']) - .plPlayerController - .seekTo( - Duration( - seconds: Utils - .duration( - Utils.tampToSeektime(modelResult - .outline![index] - .partOutline![i] - .timestamp!) - .toString(), - ), - ), - ); - } catch (_) {} - }, - ), - const TextSpan(text: ' '), - TextSpan( - text: modelResult - .outline![index] - .partOutline![i] - .content!), - ], - ), - ), - ], - ), - ], - ); - }, - ), - const SizedBox(height: 20), - ], - ); - }, - ) - ], - ), - ), - ), - ], - ), - ); - } + State createState() => _AiDetailState(); +} +class _AiDetailState extends CommonSlidePageState { InlineSpan buildContent(BuildContext context, content) { List descV2 = content.descV2; // type @@ -233,4 +87,158 @@ class AiDetail extends StatelessWidget { }); return TextSpan(children: spanChildren); } + + @override + Widget get buildPage => Container( + color: Theme.of(context).colorScheme.surface, + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Column( + children: [ + InkWell( + onTap: Get.back, + child: Container( + height: 35, + padding: const EdgeInsets.only(bottom: 2), + child: Center( + child: Container( + width: 32, + height: 3, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: const BorderRadius.all(Radius.circular(3)), + ), + ), + ), + ), + ), + Expanded( + child: enableSlide ? slideList() : buildList, + ), + ], + ), + ); + + @override + Widget get buildList => SingleChildScrollView( + controller: ScrollController(), + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + if (widget.modelResult.summary?.isNotEmpty == true) ...[ + SelectableText( + '总结: ${widget.modelResult.summary}', + style: const TextStyle( + fontSize: 15, + height: 1.5, + ), + ), + if (widget.modelResult.outline?.isNotEmpty == true) + Divider( + height: 20, + color: Theme.of(context).dividerColor.withOpacity(0.1), + thickness: 6, + ) + ], + if (widget.modelResult.outline?.isNotEmpty == true) + ListView.builder( + shrinkWrap: true, + itemCount: widget.modelResult.outline!.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Column( + children: [ + SelectableText( + widget.modelResult.outline![index].title!, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + height: 1.5, + ), + ), + const SizedBox(height: 6), + if (widget.modelResult.outline![index].partOutline + ?.isNotEmpty == + true) + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: widget + .modelResult.outline![index].partOutline!.length, + itemBuilder: (context, i) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + children: [ + SelectableText.rich( + TextSpan( + style: TextStyle( + fontSize: 13, + color: Theme.of(context) + .colorScheme + .onSurface, + height: 1.5, + ), + children: [ + TextSpan( + text: Utils.tampToSeektime(widget + .modelResult + .outline![index] + .partOutline![i] + .timestamp!), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + // 跳转到指定位置 + try { + Get.find( + tag: Get.arguments[ + 'heroTag']) + .plPlayerController + .seekTo( + Duration( + seconds: + Utils.duration( + Utils.tampToSeektime(widget + .modelResult + .outline![ + index] + .partOutline![ + i] + .timestamp!) + .toString(), + ), + ), + ); + } catch (_) {} + }, + ), + const TextSpan(text: ' '), + TextSpan( + text: widget + .modelResult + .outline![index] + .partOutline![i] + .content!), + ], + ), + ), + ], + ), + ], + ); + }, + ), + const SizedBox(height: 20), + ], + ); + }, + ) + ], + ), + ); } diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index 000b6b8a..375f3f73 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -100,7 +100,7 @@ class _HeaderControlState extends State { /// 设置面板 void showSettingSheet() { Utils.showFSSheet( - isFullScreen: isFullScreen, + isFullScreen: () => isFullScreen, child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( @@ -162,7 +162,7 @@ class _HeaderControlState extends State { dense: true, onTap: () { Get.back(); - widget.videoDetailCtr.showNoteList(); + widget.videoDetailCtr.showNoteList(context); }, leading: const Icon(Icons.note_alt_outlined, size: 20), title: const Text('查看笔记', style: titleStyle), @@ -570,7 +570,7 @@ class _HeaderControlState extends State { void scheduleExit() async { const List scheduleTimeChoices = [0, 15, 30, 45, 60]; Utils.showFSSheet( - isFullScreen: isFullScreen, + isFullScreen: () => isFullScreen, child: StatefulBuilder( builder: (context, setState) { return Container( @@ -778,7 +778,7 @@ class _HeaderControlState extends State { } Utils.showFSSheet( - isFullScreen: isFullScreen, + isFullScreen: () => isFullScreen, child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( @@ -882,7 +882,7 @@ class _HeaderControlState extends State { final AudioQuality currentAudioQa = widget.videoDetailCtr.currentAudioQa!; final List audio = videoInfo.dash!.audio!; Utils.showFSSheet( - isFullScreen: isFullScreen, + isFullScreen: () => isFullScreen, child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( @@ -973,7 +973,7 @@ class _HeaderControlState extends State { } Utils.showFSSheet( - isFullScreen: isFullScreen, + isFullScreen: () => isFullScreen, child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( @@ -1082,7 +1082,7 @@ class _HeaderControlState extends State { final DanmakuController? danmakuController = widget.controller.danmakuController; Utils.showFSSheet( - isFullScreen: isFullScreen, + isFullScreen: () => isFullScreen, padding: isFullScreen ? 70 : null, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { @@ -1702,7 +1702,7 @@ class _HeaderControlState extends State { /// 播放顺序 void showSetRepeat() async { Utils.showFSSheet( - isFullScreen: isFullScreen, + isFullScreen: () => isFullScreen, child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( diff --git a/lib/pages/video/detail/widgets/watch_later_list.dart b/lib/pages/video/detail/widgets/watch_later_list.dart index 5b4d1745..a850267d 100644 --- a/lib/pages/video/detail/widgets/watch_later_list.dart +++ b/lib/pages/video/detail/widgets/watch_later_list.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/widgets/icon_button.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/stat/danmu.dart'; import 'package:PiliPlus/common/widgets/stat/view.dart'; +import 'package:PiliPlus/pages/common/common_slide_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -14,7 +15,7 @@ import 'package:PiliPlus/utils/utils.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -class MediaListPanel extends StatefulWidget { +class MediaListPanel extends CommonSlidePage { const MediaListPanel({ super.key, required this.mediaList, @@ -42,7 +43,7 @@ class MediaListPanel extends StatefulWidget { State createState() => _MediaListPanelState(); } -class _MediaListPanelState extends State { +class _MediaListPanelState extends CommonSlidePageState { final _scrollController = ItemScrollController(); late RxBool desc; @@ -62,7 +63,7 @@ class _MediaListPanelState extends State { } @override - Widget build(BuildContext context) { + Widget get buildPage { return Column( children: [ AppBar( @@ -92,19 +93,22 @@ class _MediaListPanelState extends State { ], ), Expanded( - child: widget.loadPrevious != null - ? refreshIndicator( - onRefresh: () async { - await widget.loadPrevious!(); - }, - child: _buildList, - ) - : _buildList, + child: enableSlide ? slideList() : buildList, ), ], ); } + @override + Widget get buildList => widget.loadPrevious != null + ? refreshIndicator( + onRefresh: () async { + await widget.loadPrevious!(); + }, + child: _buildList, + ) + : _buildList; + Widget get _buildList => Obx( () => ScrollablePositionedList.builder( itemScrollController: _scrollController, diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 4ce3d63e..b04aa251 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -202,7 +202,7 @@ class Utils { static void showFSSheet({ required Widget child, - required bool isFullScreen, + required Function isFullScreen, double? padding, }) { Navigator.of(Get.context!).push( @@ -212,15 +212,28 @@ class Utils { ? Column( children: [ const Spacer(flex: 3), - Expanded(flex: 7, child: child), - if (isFullScreen && padding != null) + Expanded( + flex: 7, + child: MediaQuery.removePadding( + context: Get.context!, + removeTop: true, + child: child, + ), + ), + if (isFullScreen() && padding != null) SizedBox(height: padding), ], ) : Row( children: [ const Spacer(), - Expanded(child: child), + Expanded( + child: MediaQuery.removePadding( + context: Get.context!, + removeLeft: true, + child: child, + ), + ), ], ); },