From d3f4ba4b4aaed382ff1a03001174dd0304f92722 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Sat, 13 Sep 2025 14:26:08 +0800 Subject: [PATCH] refa: reply2reply panel tweaks Signed-off-by: bggRGjQaUbCoE --- ...tom_sliver_persistent_header_delegate.dart | 14 +- .../interactive_viewer.dart | 4 +- lib/pages/fav_create/view.dart | 14 +- lib/pages/pgc_review/child/view.dart | 12 +- lib/pages/save_panel/view.dart | 5 +- lib/pages/search/controller.dart | 9 + .../pgc/widgets/intro_detail.dart | 184 +++++----- .../video/introduction/ugc/controller.dart | 6 +- lib/pages/video/introduction/ugc/view.dart | 123 +++---- lib/pages/video/note/view.dart | 12 +- lib/pages/video/pay_coins/view.dart | 17 +- lib/pages/video/reply_reply/controller.dart | 28 +- lib/pages/video/reply_reply/view.dart | 315 ++++++++---------- lib/plugin/pl_player/view.dart | 7 + pubspec.lock | 9 + pubspec.yaml | 4 + 16 files changed, 387 insertions(+), 376 deletions(-) diff --git a/lib/common/widgets/custom_sliver_persistent_header_delegate.dart b/lib/common/widgets/custom_sliver_persistent_header_delegate.dart index c49ec8c3..c67bbb00 100644 --- a/lib/common/widgets/custom_sliver_persistent_header_delegate.dart +++ b/lib/common/widgets/custom_sliver_persistent_header_delegate.dart @@ -1,3 +1,5 @@ +import 'dart:io' show Platform; + import 'package:flutter/material.dart'; class CustomSliverPersistentHeaderDelegate @@ -26,7 +28,17 @@ class CustomSliverPersistentHeaderDelegate //overlapsContent:SliverPersistentHeader覆盖其他子组件返回true,否则返回false return bgColor != null ? DecoratedBox( - decoration: BoxDecoration(color: bgColor), + decoration: BoxDecoration( + color: bgColor, + boxShadow: Platform.isIOS + ? null + : [ + BoxShadow( + color: bgColor!, + offset: const Offset(0, -1), + ), + ], + ), child: child, ) : child; diff --git a/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart index 4b0606d6..7a60ae2d 100644 --- a/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart +++ b/lib/common/widgets/interactiveviewer_gallery/interactive_viewer.dart @@ -846,7 +846,9 @@ class _InteractiveViewerState extends State _currentRotation = desiredRotation; case _GestureType.pan: - assert(_referenceFocalPoint != null); + if (_referenceFocalPoint == null) { + return; + } // details may have a change in scale here when scaleEnabled is false. // In an effort to keep the behavior similar whether or not scaleEnabled // is true, these gestures are thrown away. diff --git a/lib/pages/fav_create/view.dart b/lib/pages/fav_create/view.dart index a2d854d7..a2d886f3 100644 --- a/lib/pages/fav_create/view.dart +++ b/lib/pages/fav_create/view.dart @@ -1,4 +1,4 @@ -import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/msg.dart'; import 'package:PiliPlus/models_new/fav/fav_folder/list.dart'; @@ -101,17 +101,7 @@ class _CreateFavPageState extends State { ? _titleController.text.isNotEmpty ? _buildBody(theme) : _errMsg?.isNotEmpty == true - ? Center( - child: CustomScrollView( - shrinkWrap: true, - slivers: [ - HttpError( - errMsg: _errMsg, - onReload: _getFolderInfo, - ), - ], - ), - ) + ? scrollErrorWidget(errMsg: _errMsg, onReload: _getFolderInfo) : const Center(child: CircularProgressIndicator()) : _buildBody(theme), ); diff --git a/lib/pages/pgc_review/child/view.dart b/lib/pages/pgc_review/child/view.dart index d4ea42a8..4fac3b43 100644 --- a/lib/pages/pgc_review/child/view.dart +++ b/lib/pages/pgc_review/child/view.dart @@ -83,11 +83,13 @@ class _PgcReviewChildPageState extends State ); return switch (loadingState) { Loading() => SliverToBoxAdapter( - child: ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => const VideoReplySkeleton(), - itemCount: 8, + child: IgnorePointer( + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => const VideoReplySkeleton(), + itemCount: 8, + ), ), ), Success(:var response) => diff --git a/lib/pages/save_panel/view.dart b/lib/pages/save_panel/view.dart index 4b39d4fe..05bd8139 100644 --- a/lib/pages/save_panel/view.dart +++ b/lib/pages/save_panel/view.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'dart:ui'; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' @@ -271,7 +272,7 @@ class _SavePanelState extends State { } Future _onSaveOrSharePic([bool isShare = false]) async { - if (!isShare) { + if (!isShare && Utils.isMobile) { if (mounted && !await ImageUtils.checkPermissionDependOnSdkInt(context)) { return; } @@ -285,7 +286,7 @@ class _SavePanelState extends State { ByteData? byteData = await image.toByteData(format: ImageByteFormat.png); Uint8List pngBytes = byteData!.buffer.asUint8List(); String picName = - "plpl_reply_${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}"; + "${Constants.appName}_${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}"; if (isShare) { Get.back(); SmartDialog.dismiss(); diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart index ec6f6256..f223be39 100644 --- a/lib/pages/search/controller.dart +++ b/lib/pages/search/controller.dart @@ -10,7 +10,9 @@ import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:get/get.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -165,6 +167,13 @@ class SSearchController extends GetxController }, ); searchFocusNode.requestFocus(); + if (Utils.isDesktop) { + SchedulerBinding.instance.addPostFrameCallback((_) { + controller.selection = TextSelection.collapsed( + offset: controller.text.length, + ); + }); + } } // 获取热搜关键词 diff --git a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart index 294c69a2..568d06e0 100644 --- a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart +++ b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart @@ -97,100 +97,98 @@ class _IntroDetailState extends CommonCollapseSlidePageState { final TextStyle textStyle = TextStyle( color: theme.colorScheme.onSurfaceVariant, ); - return SelectionArea( - child: ListView( - controller: _controller, - physics: const AlwaysScrollableScrollPhysics(), - padding: EdgeInsets.only( - left: 14, - right: 14, - top: 14, - bottom: MediaQuery.viewPaddingOf(context).bottom + 100, - ), - children: [ - Text( - widget.item.title!, - style: const TextStyle(fontSize: 16), - ), - const SizedBox(height: 4), - Row( - spacing: 6, - children: [ - StatWidget( - type: StatType.play, - value: widget.item.stat!.view, - ), - StatWidget( - type: StatType.danmaku, - value: widget.item.stat!.danmaku, - ), - ], - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - widget.item.areas!.first.name!, - style: smallTitle, - ), - const SizedBox(width: 6), - Text( - widget.item.publish!.pubTimeShow!, - style: smallTitle, - ), - const SizedBox(width: 6), - Text( - widget.item.newEp!.desc!, - style: smallTitle, - ), - ], - ), - if (widget.item.evaluate?.isNotEmpty == true) ...[ - const SizedBox(height: 20), - Text( - '简介:', - style: theme.textTheme.titleMedium, - ), - const SizedBox(height: 4), - Text( - widget.item.evaluate!, - style: textStyle, - ), - ], - if (widget.item.actors?.isNotEmpty == true) ...[ - const SizedBox(height: 20), - Text( - '演职人员:', - style: theme.textTheme.titleMedium, - ), - const SizedBox(height: 4), - Text( - widget.item.actors!, - style: textStyle, - ), - ], - if (widget.videoTags?.isNotEmpty == true) ...[ - const SizedBox(height: 10), - Wrap( - spacing: 8, - runSpacing: 8, - children: widget.videoTags! - .map( - (item) => SearchText( - fontSize: 13, - text: item.tagName!, - onTap: (tagName) => Get.toNamed( - '/searchResult', - parameters: {'keyword': tagName}, - ), - onLongPress: Utils.copyText, - ), - ) - .toList(), - ), - ], - ], + return ListView( + controller: _controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.only( + left: 14, + right: 14, + top: 14, + bottom: MediaQuery.viewPaddingOf(context).bottom + 100, ), + children: [ + SelectableText( + widget.item.title!, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 4), + Row( + spacing: 6, + children: [ + StatWidget( + type: StatType.play, + value: widget.item.stat!.view, + ), + StatWidget( + type: StatType.danmaku, + value: widget.item.stat!.danmaku, + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + widget.item.areas!.first.name!, + style: smallTitle, + ), + const SizedBox(width: 6), + Text( + widget.item.publish!.pubTimeShow!, + style: smallTitle, + ), + const SizedBox(width: 6), + Text( + widget.item.newEp!.desc!, + style: smallTitle, + ), + ], + ), + if (widget.item.evaluate?.isNotEmpty == true) ...[ + const SizedBox(height: 20), + Text( + '简介:', + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 4), + SelectableText( + widget.item.evaluate!, + style: textStyle, + ), + ], + if (widget.item.actors?.isNotEmpty == true) ...[ + const SizedBox(height: 20), + Text( + '演职人员:', + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 4), + Text( + widget.item.actors!, + style: textStyle, + ), + ], + if (widget.videoTags?.isNotEmpty == true) ...[ + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: widget.videoTags! + .map( + (item) => SearchText( + fontSize: 13, + text: item.tagName!, + onTap: (tagName) => Get.toNamed( + '/searchResult', + parameters: {'keyword': tagName}, + ), + onLongPress: Utils.copyText, + ), + ) + .toList(), + ), + ], + ], ); } } diff --git a/lib/pages/video/introduction/ugc/controller.dart b/lib/pages/video/introduction/ugc/controller.dart index a29514ef..5c303a10 100644 --- a/lib/pages/video/introduction/ugc/controller.dart +++ b/lib/pages/video/introduction/ugc/controller.dart @@ -431,7 +431,11 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { SmartDialog.showToast('账号未登录'); return; } - int? mid = videoDetail.value.owner?.mid; + final videoDetail = this.videoDetail.value; + if (videoDetail.staff?.isNotEmpty == true) { + return; + } + int? mid = videoDetail.owner?.mid; if (mid == null) { return; } diff --git a/lib/pages/video/introduction/ugc/view.dart b/lib/pages/video/introduction/ugc/view.dart index d972bcf7..812133db 100644 --- a/lib/pages/video/introduction/ugc/view.dart +++ b/lib/pages/video/introduction/ugc/view.dart @@ -171,27 +171,13 @@ class _UgcIntroPanelState extends TripleState const SizedBox(height: 8), if (isLoading) _buildVideoTitle(theme, videoDetail) + else if (isHorizontal && Utils.isDesktop) + _buildTitle(theme, videoDetail, isExpand: true) else ExpandablePanel( controller: introController.expandableCtr, - collapsed: GestureDetector( - onLongPress: () { - Feedback.forLongPress(context); - Utils.copyText(videoDetail.title ?? ''); - }, - child: _buildVideoTitle(theme, videoDetail), - ), - expanded: GestureDetector( - onLongPress: () { - Feedback.forLongPress(context); - Utils.copyText(videoDetail.title ?? ''); - }, - child: _buildVideoTitle( - theme, - videoDetail, - isExpand: true, - ), - ), + collapsed: _buildTitle(theme, videoDetail), + expanded: _buildTitle(theme, videoDetail, isExpand: true), theme: expandTheme, ), const SizedBox(height: 8), @@ -228,48 +214,19 @@ class _UgcIntroPanelState extends TripleState ), ), ], - ExpandablePanel( - controller: introController.expandableCtr, - collapsed: const SizedBox.shrink(), - expanded: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - GestureDetector( - onTap: () => Utils.copyText('${videoDetail.bvid}'), - child: Text( - videoDetail.bvid ?? '', - style: TextStyle( - fontSize: 14, - color: theme.colorScheme.secondary, - ), - ), - ), - if (videoDetail.descV2?.isNotEmpty == true) ...[ - const SizedBox(height: 8), - SelectableText.rich( - style: const TextStyle( - height: 1.4, - ), - TextSpan( - children: [ - buildContent(theme, videoDetail), - ], - ), - ), - ], - Obx(() { - final videoTags = introController.videoTags.value; - if (videoTags.isNullOrEmpty) { - return const SizedBox.shrink(); - } - return _buildTags(videoTags!); - }), - ], + if (isHorizontal && Utils.isDesktop) + ..._infos(theme, videoDetail) + else + ExpandablePanel( + controller: introController.expandableCtr, + collapsed: const SizedBox.shrink(), + expanded: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _infos(theme, videoDetail), + ), + theme: expandTheme, ), - theme: expandTheme, - ), Obx( () => introController.status.value ? const SizedBox.shrink() @@ -339,6 +296,54 @@ class _UgcIntroPanelState extends TripleState ); } + Widget _buildTitle( + ThemeData theme, + VideoDetailData videoDetail, { + bool isExpand = false, + }) => GestureDetector( + onLongPress: () { + Feedback.forLongPress(context); + Utils.copyText(videoDetail.title ?? ''); + }, + child: _buildVideoTitle( + theme, + videoDetail, + isExpand: isExpand, + ), + ); + + List _infos(ThemeData theme, VideoDetailData videoDetail) => [ + const SizedBox(height: 8), + GestureDetector( + onTap: () => Utils.copyText('${videoDetail.bvid}'), + child: Text( + videoDetail.bvid ?? '', + style: TextStyle( + fontSize: 14, + color: theme.colorScheme.secondary, + ), + ), + ), + if (videoDetail.descV2?.isNotEmpty == true) ...[ + const SizedBox(height: 8), + SelectableText.rich( + style: const TextStyle(height: 1.4), + TextSpan( + children: [ + buildContent(theme, videoDetail), + ], + ), + ), + ], + Obx(() { + final videoTags = introController.videoTags.value; + if (videoTags.isNullOrEmpty) { + return const SizedBox.shrink(); + } + return _buildTags(videoTags!); + }), + ]; + WidgetSpan _labelWidget(String text, Color bgColor, Color textColor) { return WidgetSpan( alignment: PlaceholderAlignment.middle, diff --git a/lib/pages/video/note/view.dart b/lib/pages/video/note/view.dart index 8051aa27..71206a84 100644 --- a/lib/pages/video/note/view.dart +++ b/lib/pages/video/note/view.dart @@ -170,11 +170,13 @@ class _NoteListPageState extends CommonSlidePageState { ); return switch (loadingState) { Loading() => SliverToBoxAdapter( - child: ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => const VideoReplySkeleton(), - itemCount: 8, + child: IgnorePointer( + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => const VideoReplySkeleton(), + itemCount: 8, + ), ), ), Success(:var response) => diff --git a/lib/pages/video/pay_coins/view.dart b/lib/pages/video/pay_coins/view.dart index b6a0ffa1..98ddfb18 100644 --- a/lib/pages/video/pay_coins/view.dart +++ b/lib/pages/video/pay_coins/view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math' as math; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/global_data.dart'; @@ -221,15 +222,17 @@ class _PayCoinsPageState extends State @override Widget build(BuildContext context) { - bool isPortrait = MediaQuery.sizeOf(context).isPortrait; + final size = MediaQuery.sizeOf(context); + final isPortrait = size.isPortrait; return isPortrait ? _buildBody(isPortrait) - : Row( - children: [ - const Spacer(), - Expanded(flex: 3, child: _buildBody(isPortrait)), - const Spacer(), - ], + : Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: math.min(525, size.width * 0.6), + ), + child: _buildBody(isPortrait), + ), ); } diff --git a/lib/pages/video/reply_reply/controller.dart b/lib/pages/video/reply_reply/controller.dart index 074c8888..4ff3cec0 100644 --- a/lib/pages/video/reply_reply/controller.dart +++ b/lib/pages/video/reply_reply/controller.dart @@ -11,7 +11,7 @@ import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get/get_navigation/src/dialog/dialog_route.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:super_sliver_list/super_sliver_list.dart'; class VideoReplyReplyController extends ReplyController with GetSingleTickerProviderStateMixin { @@ -26,19 +26,19 @@ class VideoReplyReplyController extends ReplyController }); final int? dialog; final bool isDialogue; - final itemScrollCtr = ItemScrollController(); - bool hasRoot = false; int? id; // 视频aid 请求时使用的oid int oid; // rpid 请求楼中楼回复 int rpid; - int replyType; // = ReplyType.video; + int replyType; - ReplyInfo? firstFloor; + bool hasRoot = false; + late final Rx firstFloor = Rx(null); int? index; - AnimationController? controller; + AnimationController? animController; + final listController = ListController(); late final horizontalPreview = Pref.horizontalPreview; @@ -69,27 +69,28 @@ class VideoReplyReplyController extends ReplyController // reply2Reply // isDialogue.not if (data is DetailListReply) { count.value = data.root.count.toInt(); - if (isRefresh && firstFloor == null) { - firstFloor = data.root; + if (isRefresh && !hasRoot) { + firstFloor.value ??= data.root; } if (id != null) { final id64 = Int64(id!); final index = data.root.replies.indexWhere((item) => item.id == id64); if (index != -1) { this.index = index; - controller = AnimationController( + animController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); WidgetsBinding.instance.addPostFrameCallback((_) async { try { - itemScrollCtr.jumpTo( - index: hasRoot ? index + 3 : index + 1, + listController.jumpToItem( + index: index, + scrollController: scrollController, alignment: 0.25, ); await Future.delayed( const Duration(milliseconds: 800), - controller?.forward, + animController?.forward, ); this.index = null; } catch (_) {} @@ -200,7 +201,8 @@ class VideoReplyReplyController extends ReplyController @override void onClose() { - controller?.dispose(); + animController?.dispose(); + listController.dispose(); super.dispose(); } } diff --git a/lib/pages/video/reply_reply/view.dart b/lib/pages/video/reply_reply/view.dart index a35c9fff..6a6dd76f 100644 --- a/lib/pages/video/reply_reply/view.dart +++ b/lib/pages/video/reply_reply/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/skeleton/video_reply.dart'; -import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo, Mode; @@ -13,7 +14,7 @@ import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart' hide ContextExtensionss; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:super_sliver_list/super_sliver_list.dart'; class VideoReplyReplyPanel extends CommonSlidePage { const VideoReplyReplyPanel({ @@ -48,15 +49,11 @@ class VideoReplyReplyPanel extends CommonSlidePage { class _VideoReplyReplyPanelState extends CommonSlidePageState { late VideoReplyReplyController _controller; - late final itemPositionsListener = ItemPositionsListener.create(); late final _key = GlobalKey(); - late final _listKey = GlobalKey(); late final _tag = Utils.makeHeroTag( '${widget.rpid}${widget.dialog}${widget.isDialogue}', ); - ReplyInfo? get firstFloor => widget.firstFloor ?? _controller.firstFloor; - bool get _horizontalPreview => _controller.horizontalPreview && context.isLandscape; Function(List imgList, int index)? _imageCallback; @@ -138,163 +135,138 @@ class _VideoReplyReplyPanelState ); } + ReplyInfo? get firstFloor => + widget.firstFloor ?? _controller.firstFloor.value; + @override Widget buildList(ThemeData theme) { return refreshIndicator( onRefresh: _controller.onRefresh, - child: Obx( - () => Stack( - clipBehavior: Clip.none, - children: [ - ScrollablePositionedList.builder( - key: _listKey, - itemPositionsListener: itemPositionsListener, - itemCount: _itemCount(_controller.loadingState.value), - itemScrollController: _controller.itemScrollCtr, - physics: const AlwaysScrollableScrollPhysics(), - itemBuilder: (context, index) { - if (widget.isDialogue) { - return _buildBody( - theme, - _controller.loadingState.value, - index, - ); - } else if (firstFloor != null) { - if (index == 0) { - return ReplyItemGrpc( - replyItem: firstFloor!, - replyLevel: 2, - needDivider: false, - onReply: (replyItem) => _controller.onReply( - context, - replyItem: replyItem, - index: -1, - ), - upMid: _controller.upMid, - onViewImage: widget.onViewImage, - onDismissed: widget.onDismissed, - callback: _imageCallback, - onCheckReply: (item) => - _controller.onCheckReply(item, isManual: true), - ); - } else if (index == 1) { - return Divider( - height: 20, - color: theme.dividerColor.withValues(alpha: 0.1), - thickness: 6, - ); - } else if (index == 2) { - return _sortWidget(theme); - } else { - return _buildBody( - theme, - _controller.loadingState.value, - index - 3, - ); - } - } else { - if (index == 0) { - return _sortWidget(theme); - } else { - return _buildBody( - theme, - _controller.loadingState.value, - index - 1, - ); - } + child: CustomScrollView( + controller: _controller.scrollController, + slivers: [ + if (!widget.isDialogue) ...[ + if (widget.firstFloor case final firstFloor?) + _header(theme, firstFloor) + else + Obx(() { + final firstFloor = _controller.firstFloor.value; + if (firstFloor == null) { + return const SliverToBoxAdapter(); } - }, - ), - if (!widget.isDialogue && _controller.loadingState.value.isSuccess) - _header(theme), + return _header(theme, firstFloor); + }), + _sortWidget(theme), ], + Obx(() => _buildBody(theme, _controller.loadingState.value)), + ], + ), + ); + } + + Widget _header(ThemeData theme, ReplyInfo firstFloor) { + return SliverMainAxisGroup( + slivers: [ + SliverToBoxAdapter( + child: ReplyItemGrpc( + replyItem: firstFloor, + replyLevel: 2, + needDivider: false, + onReply: (replyItem) => _controller.onReply( + context, + replyItem: replyItem, + index: -1, + ), + upMid: _controller.upMid, + onViewImage: widget.onViewImage, + onDismissed: widget.onDismissed, + callback: _imageCallback, + onCheckReply: (item) => + _controller.onCheckReply(item, isManual: true), + ), + ), + SliverToBoxAdapter( + child: Divider( + height: 20, + color: theme.dividerColor.withValues(alpha: 0.1), + thickness: 6, + ), + ), + ], + ); + } + + Widget _sortWidget(ThemeData theme) { + return SliverPersistentHeader( + pinned: true, + delegate: CustomSliverPersistentHeaderDelegate( + extent: 40, + bgColor: theme.colorScheme.surface, + child: Container( + height: 40, + padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx( + () { + final count = _controller.count.value; + return count != -1 + ? Text( + '相关回复共${NumUtils.numFormat(count)}条', + style: const TextStyle(fontSize: 13), + ) + : const SizedBox.shrink(); + }, + ), + SizedBox( + height: 35, + child: TextButton.icon( + onPressed: _controller.queryBySort, + icon: Icon( + Icons.sort, + size: 16, + color: theme.colorScheme.secondary, + ), + label: Obx( + () => Text( + _controller.mode.value == Mode.MAIN_LIST_HOT + ? '按热度' + : '按时间', + style: TextStyle( + fontSize: 13, + color: theme.colorScheme.secondary, + ), + ), + ), + ), + ), + ], + ), ), ), ); } - Widget _header(ThemeData theme) => firstFloor == null - ? _sortWidget(theme) - : ValueListenableBuilder>( - valueListenable: itemPositionsListener.itemPositions, - builder: (context, positions, child) { - int min = -1; - if (positions.isNotEmpty) { - min = positions - .where( - (ItemPosition position) => position.itemTrailingEdge > 0, - ) - .reduce( - (ItemPosition min, ItemPosition position) => - position.itemTrailingEdge < min.itemTrailingEdge - ? position - : min, - ) - .index; - } - return min >= 2 ? _sortWidget(theme) : const SizedBox.shrink(); - }, - ); - - Widget _sortWidget(ThemeData theme) => Container( - height: 40, - color: theme.colorScheme.surface, - padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Obx( - () { - final count = _controller.count.value; - return count != -1 - ? Text( - '相关回复共${NumUtils.numFormat(count)}条', - style: const TextStyle(fontSize: 13), - ) - : const SizedBox.shrink(); - }, - ), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: _controller.queryBySort, - icon: Icon( - Icons.sort, - size: 16, - color: theme.colorScheme.secondary, - ), - label: Obx( - () => Text( - _controller.mode.value == Mode.MAIN_LIST_HOT ? '按热度' : '按时间', - style: TextStyle( - fontSize: 13, - color: theme.colorScheme.secondary, - ), - ), - ), - ), - ), - ], - ), - ); - Widget _buildBody( ThemeData theme, LoadingState?> loadingState, - int index, ) { return switch (loadingState) { - Loading() => IgnorePointer( - child: ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => const VideoReplySkeleton(), - itemCount: 8, + Loading() => SliverToBoxAdapter( + child: IgnorePointer( + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => const VideoReplySkeleton(), + itemCount: 8, + ), ), ), - Success(:var response) => Builder( - builder: (context) { - if (index == response!.length) { + Success(:var response) => SuperSliverList.builder( + listController: _controller.listController, + itemBuilder: (context, index) { + if (index == response.length) { _controller.onLoadMore(); return Container( height: 125, @@ -311,30 +283,30 @@ class _VideoReplyReplyPanelState ), ), ); - } else { - final child = _replyItem(response[index], index); - if (_controller.index != null && _controller.index == index) { - colorAnimation ??= ColorTween( - begin: theme.colorScheme.onInverseSurface, - end: theme.colorScheme.surface, - ).animate(_controller.controller!); - return AnimatedBuilder( - animation: colorAnimation!, - builder: (context, _) { - return ColoredBox( - color: - colorAnimation!.value ?? - theme.colorScheme.onInverseSurface, - child: child, - ); - }, - ); - } - return child; } + final child = _replyItem(response[index], index); + if (_controller.index == index) { + colorAnimation ??= ColorTween( + begin: theme.colorScheme.onInverseSurface, + end: theme.colorScheme.surface, + ).animate(_controller.animController!); + return AnimatedBuilder( + animation: colorAnimation!, + builder: (context, _) { + return ColoredBox( + color: + colorAnimation!.value ?? + theme.colorScheme.onInverseSurface, + child: child, + ); + }, + ); + } + return child; }, + itemCount: response!.length + 1, ), - Error(:var errMsg) => errorWidget( + Error(:var errMsg) => HttpError( errMsg: errMsg, onReload: _controller.onReload, ), @@ -367,15 +339,4 @@ class _VideoReplyReplyPanelState onCheckReply: (item) => _controller.onCheckReply(item, isManual: true), ); } - - int _itemCount(LoadingState?> loadingState) { - if (widget.isDialogue) { - return (loadingState.dataOrNull?.length ?? 0) + 1; - } - int itemCount = 0; - if (firstFloor != null) { - itemCount = 2; - } - return (loadingState.dataOrNull?.length ?? 0) + itemCount + 2; - } } diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 6497d883..bf2aee37 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -23,6 +23,7 @@ import 'package:PiliPlus/models_new/video/video_shot/data.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart'; +import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart'; import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart'; import 'package:PiliPlus/pages/video/post_panel/view.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; @@ -1203,6 +1204,12 @@ class _PLVideoPlayerState extends State introController.viewLater(); break; + case LogicalKeyboardKey.keyG when (!plPlayerController.isLive): + if (introController case UgcIntroController ugcCtr) { + ugcCtr.actionRelationMod(context); + } + break; + case LogicalKeyboardKey.bracketLeft when (!plPlayerController.isLive): if (!introController.prevPlay()) { SmartDialog.showToast('已经是第一集了'); diff --git a/pubspec.lock b/pubspec.lock index a8988637..3083941f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1756,6 +1756,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + super_sliver_list: + dependency: "direct main" + description: + path: "." + ref: mod + resolved-ref: "71f34fa179e2d05818004e22e632dd59f2a223be" + url: "https://github.com/bggRGjQaUbCoE/super_sliver_list.git" + source: git + version: "0.4.1" synchronized: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index daef74ce..ee920f82 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -213,6 +213,10 @@ dependencies: window_manager: ^0.5.1 tray_manager: ^0.5.1 file_picker: ^10.3.3 + super_sliver_list: + git: + url: https://github.com/bggRGjQaUbCoE/super_sliver_list.git + ref: mod vector_math: any fixnum: any