diff --git a/README.md b/README.md index ce55589c1..c43f1ba8e 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ ## feat +- [x] 评论楼中楼定位点击查看的评论 - [x] 评论楼中楼按热度/时间排序 - [x] 评论点踩 - [x] 显示ops专栏 diff --git a/lib/grpc/grpc_repo.dart b/lib/grpc/grpc_repo.dart index 3a71e3d53..e558be1d0 100644 --- a/lib/grpc/grpc_repo.dart +++ b/lib/grpc/grpc_repo.dart @@ -102,7 +102,6 @@ class GrpcRepo { try { return await request(); } catch (e) { - print('111111111111111111111111111111111111111 $e'); return {'status': false, 'msg': e.toString()}; } } diff --git a/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart b/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart index c1ae93f5d..bd12ed0c5 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart @@ -53,7 +53,7 @@ class ReplyItemGrpc extends StatelessWidget { onTap: () { feedBack(); if (replyReply != null) { - replyReply!(replyItem); + replyReply!(replyItem, null); } }, onLongPress: () { @@ -442,7 +442,8 @@ class ReplyItemRow extends StatelessWidget { for (int i = 0; i < replies.length; i++) ...[ InkWell( // 一楼点击评论展开评论详情 - onTap: () => replyReply!(replyItem), + onTap: () => + replyReply?.call(replyItem, replies[i].id.toInt()), onLongPress: () { feedBack(); showModalBottomSheet( @@ -543,7 +544,7 @@ class ReplyItemRow extends StatelessWidget { if (extraRow) InkWell( // 一楼点击【共xx条回复】展开评论详情 - onTap: () => replyReply!(replyItem), + onTap: () => replyReply!(replyItem, null), child: Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(8, 5, 8, 8), diff --git a/lib/pages/video/detail/reply_reply/controller.dart b/lib/pages/video/detail/reply_reply/controller.dart index 3cc19ded9..cd340c83c 100644 --- a/lib/pages/video/detail/reply_reply/controller.dart +++ b/lib/pages/video/detail/reply_reply/controller.dart @@ -4,9 +4,19 @@ import 'package:PiliPalaX/pages/common/common_controller.dart'; import 'package:get/get.dart'; import 'package:PiliPalaX/http/reply.dart'; import 'package:PiliPalaX/models/common/reply_type.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; class VideoReplyReplyController extends CommonController { - VideoReplyReplyController(this.aid, this.rpid, this.replyType); + VideoReplyReplyController( + this.hasRoot, + this.id, + this.aid, + this.rpid, + this.replyType, + ); + final itemScrollCtr = ItemScrollController(); + bool hasRoot = false; + dynamic id; // 视频aid 请求时使用的oid int? aid; // rpid 请求楼中楼回复 @@ -16,11 +26,11 @@ class VideoReplyReplyController extends CommonController { RxString noMore = ''.obs; // 当前回复的回复 ReplyInfo? currentReplyItem; - ReplyInfo? root; CursorReply? cursor; Rx mode = Mode.MAIN_LIST_HOT.obs; RxInt count = (-1).obs; + int? upMid; @override void onInit() { @@ -79,10 +89,26 @@ class VideoReplyReplyController extends CommonController { @override bool customHandleResponse(Success response) { DetailListReply replies = response.response; - root = replies.root; if (cursor == null) { count.value = replies.root.count.toInt(); + if (id != null) { + int index = replies.root.replies + .map((item) => item.id.toInt()) + .toList() + .indexOf(id); + if (index != -1) { + () async { + await Future.delayed(const Duration(milliseconds: 200)); + itemScrollCtr.scrollTo( + index: hasRoot ? index + 3 : index + 1, + duration: const Duration(milliseconds: 200), + ); + }(); + } + id = null; + } } + upMid ??= replies.subjectControl.upMid.toInt(); cursor = replies.cursor; if (replies.root.replies.isNotEmpty) { noMore.value = '加载中...'; diff --git a/lib/pages/video/detail/reply_reply/view.dart b/lib/pages/video/detail/reply_reply/view.dart index d5dfd349d..bb7fc1c73 100644 --- a/lib/pages/video/detail/reply_reply/view.dart +++ b/lib/pages/video/detail/reply_reply/view.dart @@ -1,16 +1,14 @@ import 'package:PiliPalaX/grpc/app/main/community/reply/v1/reply.pb.dart'; import 'package:PiliPalaX/http/loading_state.dart'; -import 'package:PiliPalaX/pages/video/detail/reply/view.dart' - show MySliverPersistentHeaderDelegate; import 'package:PiliPalaX/pages/video/detail/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPalaX/pages/video/detail/reply_new/reply_page.dart'; -import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPalaX/common/skeleton/video_reply.dart'; import 'package:PiliPalaX/common/widgets/http_error.dart'; import 'package:PiliPalaX/models/common/reply_type.dart'; import 'package:get/get_navigation/src/dialog/dialog_route.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import '../../../../utils/utils.dart'; import 'controller.dart'; @@ -18,6 +16,7 @@ import 'controller.dart'; class VideoReplyReplyPanel extends StatefulWidget { const VideoReplyReplyPanel({ // this.rcount, + this.id, this.oid, this.rpid, this.firstFloor, @@ -26,6 +25,7 @@ class VideoReplyReplyPanel extends StatefulWidget { super.key, }); // final dynamic rcount; + final dynamic id; final int? oid; final int? rpid; final ReplyInfo? firstFloor; @@ -45,29 +45,18 @@ class _VideoReplyReplyPanelState extends State { super.initState(); _videoReplyReplyController = Get.put( VideoReplyReplyController( - widget.oid, widget.rpid.toString(), widget.replyType!), + widget.firstFloor != null, + widget.id, + widget.oid, + widget.rpid.toString(), + widget.replyType!, + ), tag: widget.rpid.toString(), ); - - // 上拉加载更多 - _videoReplyReplyController.scrollController.addListener( - () { - if (_videoReplyReplyController.scrollController.position.pixels >= - _videoReplyReplyController - .scrollController.position.maxScrollExtent - - 300) { - EasyThrottle.throttle('replylist', const Duration(milliseconds: 200), - () { - _videoReplyReplyController.onLoadMore(); - }); - } - }, - ); } @override void dispose() { - _videoReplyReplyController.scrollController.removeListener(() {}); Get.delete(tag: widget.rpid.toString()); super.dispose(); } @@ -105,76 +94,52 @@ class _VideoReplyReplyPanelState extends State { onRefresh: () async { await _videoReplyReplyController.onRefresh(); }, - child: CustomScrollView( - controller: _videoReplyReplyController.scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - if (widget.firstFloor != null) ...[ - // const SliverToBoxAdapter(child: SizedBox(height: 10)), - SliverToBoxAdapter( - child: ReplyItemGrpc( - replyItem: widget.firstFloor!, - replyLevel: '2', - showReplyRow: false, - replyType: widget.replyType, - needDivider: false, - onReply: () { - _onReply(widget.firstFloor!); - }, - ), - ), - SliverToBoxAdapter( - child: Divider( - height: 20, - color: Theme.of(context).dividerColor.withOpacity(0.1), - thickness: 6, - ), - ), - ], - SliverPersistentHeader( - pinned: false, - floating: true, - delegate: MySliverPersistentHeaderDelegate( - child: Container( - height: 40, - padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), - color: Theme.of(context).colorScheme.surface, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Obx( - () => _videoReplyReplyController.count.value != -1 - ? Text( - '相关回复共${_videoReplyReplyController.count.value}条', - style: const TextStyle(fontSize: 13), - ) - : const SizedBox.shrink(), - ), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: () => - _videoReplyReplyController.queryBySort(), - icon: const Icon(Icons.sort, size: 16), - label: Obx( - () => Text( - _videoReplyReplyController.mode.value == - Mode.MAIN_LIST_HOT - ? '按热度' - : '按时间', - style: const TextStyle(fontSize: 13), - ), - ), - ), - ) - ], - ), - ), - ), - ), - Obx(() => _buildBody( - _videoReplyReplyController.loadingState.value)), - ], + child: Obx( + () => ScrollablePositionedList.builder( + itemCount: + _itemCount(_videoReplyReplyController.loadingState.value), + itemScrollController: + _videoReplyReplyController.itemScrollCtr, + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (_, index) { + if (widget.firstFloor != null) { + if (index == 0) { + return ReplyItemGrpc( + replyItem: widget.firstFloor!, + replyLevel: '2', + showReplyRow: false, + replyType: widget.replyType, + needDivider: false, + onReply: () { + _onReply(widget.firstFloor!); + }, + upMid: _videoReplyReplyController.upMid, + ); + } else if (index == 1) { + return Divider( + height: 20, + color: + Theme.of(context).dividerColor.withOpacity(0.1), + thickness: 6, + ); + } else if (index == 2) { + return _sortWidget; + } else { + return Obx(() => _buildBody( + _videoReplyReplyController.loadingState.value, + index - 3)); + } + } else { + if (index == 0) { + return _sortWidget; + } else { + return Obx(() => _buildBody( + _videoReplyReplyController.loadingState.value, + index - 1)); + } + } + }, + ), ), ), ), @@ -183,6 +148,40 @@ class _VideoReplyReplyPanelState extends State { ); } + Widget get _sortWidget => Container( + height: 40, + padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), + color: Theme.of(context).colorScheme.surface, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx( + () => _videoReplyReplyController.count.value != -1 + ? Text( + '相关回复共${_videoReplyReplyController.count.value}条', + style: const TextStyle(fontSize: 13), + ) + : const SizedBox.shrink(), + ), + SizedBox( + height: 35, + child: TextButton.icon( + onPressed: () => _videoReplyReplyController.queryBySort(), + icon: const Icon(Icons.sort, size: 16), + label: Obx( + () => Text( + _videoReplyReplyController.mode.value == Mode.MAIN_LIST_HOT + ? '按热度' + : '按时间', + style: const TextStyle(fontSize: 13), + ), + ), + ), + ) + ], + ), + ); + void _onReply(ReplyInfo? item) { dynamic oid = item?.oid.toInt(); dynamic root = item?.id.toInt(); @@ -236,89 +235,84 @@ class _VideoReplyReplyPanelState extends State { }); } - Widget _buildBody(LoadingState loadingState) { - return loadingState is Success - ? SliverMainAxisGroup( - slivers: [ - if (widget.firstFloor == null && - _videoReplyReplyController.root != null) ...[ - SliverToBoxAdapter( - child: ReplyItemGrpc( - replyItem: _videoReplyReplyController.root!, - replyLevel: '2', - showReplyRow: false, - replyType: widget.replyType, - needDivider: false, - onReply: () { - _onReply(_videoReplyReplyController.root); - }, - ), - ), - SliverToBoxAdapter( - child: Divider( - height: 20, - color: Theme.of(context).dividerColor.withOpacity(0.1), - thickness: 6, - ), - ), - ], - SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - if (index == loadingState.response.length) { - return Container( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom), - height: MediaQuery.of(context).padding.bottom + 100, - child: Center( - child: Obx( - () => Text( - _videoReplyReplyController.noMore.value, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.outline, - ), - ), - ), - ), - ); - } else { - return ReplyItemGrpc( - replyItem: loadingState.response[index], - replyLevel: '2', - showReplyRow: false, - replyType: widget.replyType, - onReply: () { - _onReply(loadingState.response[index]); - }, - onDelete: (rpid, frpid) { - List list = (_videoReplyReplyController - .loadingState.value as Success) - .response; - list = list.where((item) => item.id != rpid).toList(); - _videoReplyReplyController.loadingState.value = - LoadingState.success(list); - }, - ); - } - }, - childCount: loadingState.response.length + 1, + Widget _buildBody(LoadingState loadingState, int index) { + if (loadingState is Success) { + if (index == loadingState.response.length) { + _videoReplyReplyController.onLoadMore(); + return Container( + padding: + EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), + height: MediaQuery.of(context).padding.bottom + 100, + child: Center( + child: Obx( + () => Text( + _videoReplyReplyController.noMore.value, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.outline, ), ), - ], + ), + ), + ); + } else { + return ReplyItemGrpc( + replyItem: loadingState.response[index], + replyLevel: '2', + showReplyRow: false, + replyType: widget.replyType, + onReply: () { + _onReply(loadingState.response[index]); + }, + onDelete: (rpid, frpid) { + List list = + (_videoReplyReplyController.loadingState.value as Success) + .response; + list = list.where((item) => item.id != rpid).toList(); + _videoReplyReplyController.loadingState.value = + LoadingState.success(list); + }, + upMid: _videoReplyReplyController.upMid, + ); + } + } else if (loadingState is Error) { + return CustomScrollView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + slivers: [ + HttpError( + errMsg: loadingState.errMsg, + fn: _videoReplyReplyController.onReload, ) - : loadingState is Error - ? HttpError( - errMsg: loadingState.errMsg, - fn: _videoReplyReplyController.onReload, - ) - : SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return const VideoReplySkeleton(); - }, - childCount: 8, - ), - ); + ], + ); + } else { + return CustomScrollView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return const VideoReplySkeleton(); + }, + childCount: 8, + ), + ) + ], + ); + } + } + + int _itemCount(LoadingState loadingState) { + int itemCount = 0; + if (widget.firstFloor != null) { + itemCount = 2; + } + if (loadingState is Success) { + return loadingState.response.length + itemCount + 2; + } else { + return itemCount + 2; + } } } diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index b445ce41b..5bbea558d 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -1287,9 +1287,10 @@ class _VideoDetailPageState extends State ); // 展示二级回复 - void replyReply(replyItem) { + void replyReply(replyItem, id) { scaffoldKey.currentState?.showBottomSheet( (context) => VideoReplyReplyPanel( + id: id, // rcount: replyItem.rcount, oid: replyItem.oid.toInt(), rpid: replyItem.id.toInt(),