diff --git a/README.md b/README.md index 564f770f4..4b1dfd1d3 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ ## feat +- [x] 评论楼中楼查看对话 - [x] 评论楼中楼定位点击查看的评论 - [x] 评论楼中楼按热度/时间排序 - [x] 评论点踩 diff --git a/lib/grpc/grpc_repo.dart b/lib/grpc/grpc_repo.dart index e62f90b81..157a5e328 100644 --- a/lib/grpc/grpc_repo.dart +++ b/lib/grpc/grpc_repo.dart @@ -137,6 +137,27 @@ class GrpcRepo { }); } + static Future dialogList({ + int type = 1, + required int oid, + required int root, + required int rpid, + required CursorReq cursor, + DetailListScene scene = DetailListScene.REPLY, + }) async { + return await _request(() async { + final request = DialogListReq() + ..oid = Int64(oid) + ..type = Int64(type) + ..root = Int64(root) + ..rpid = Int64(rpid) + ..cursor = cursor; + final response = await GrpcClient.instance.replyClient + .dialogList(request, options: options); + return {'status': true, 'data': response}; + }); + } + static Future detailList({ int type = 1, required int oid, diff --git a/lib/http/reply.dart b/lib/http/reply.dart index 6c811d7be..822bc39e8 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -99,6 +99,27 @@ class ReplyHttp { } } + static Future dialogListGrpc({ + int type = 1, + required int oid, + required int root, + required int rpid, + required CursorReq cursor, + }) async { + dynamic res = await GrpcRepo.dialogList( + type: type, + oid: oid, + root: root, + rpid: rpid, + cursor: cursor, + ); + if (res['status']) { + return LoadingState.success(res['data']); + } else { + return LoadingState.error(res['msg']); + } + } + static Future replyReplyListGrpc({ int type = 1, required int oid, 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 a3ddb5800..516ed7a91 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart @@ -35,6 +35,7 @@ class ReplyItemGrpc extends StatelessWidget { this.onDelete, this.upMid, this.isTop = false, + this.showDialogue, }); final ReplyInfo replyItem; final String? replyLevel; @@ -46,6 +47,7 @@ class ReplyItemGrpc extends StatelessWidget { final Function(dynamic rpid, dynamic frpid)? onDelete; final dynamic upMid; final bool isTop; + final VoidCallback? showDialogue; @override Widget build(BuildContext context) { @@ -64,8 +66,7 @@ class ReplyItemGrpc extends StatelessWidget { // showDialog( // context: Get.context!, // builder: (_) => AlertDialog( - // content: SelectableText( - // jsonEncode(replyItem.replyControl.toProto3Json())), + // content: SelectableText(jsonEncode(replyItem.toProto3Json())), // ), // ); showModalBottomSheet( @@ -392,6 +393,23 @@ class ReplyItemGrpc extends StatelessWidget { color: Theme.of(context).colorScheme.primary, fontSize: Theme.of(context).textTheme.labelMedium!.fontSize), ), + if (replyLevel == '2' && + needDivider && + replyItem.id != replyItem.dialog) + SizedBox( + height: 32, + child: TextButton( + onPressed: showDialogue, + child: Text( + '查看对话', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + fontWeight: FontWeight.normal, + ), + ), + ), + ), const Spacer(), ZanButtonGrpc(replyItem: replyItem, replyType: replyType), const SizedBox(width: 5) diff --git a/lib/pages/video/detail/reply_reply/controller.dart b/lib/pages/video/detail/reply_reply/controller.dart index 563fe1562..831090b83 100644 --- a/lib/pages/video/detail/reply_reply/controller.dart +++ b/lib/pages/video/detail/reply_reply/controller.dart @@ -9,20 +9,24 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; class VideoReplyReplyController extends CommonController with GetTickerProviderStateMixin { - VideoReplyReplyController( - this.hasRoot, - this.id, - this.aid, - this.rpid, - this.replyType, - ); + VideoReplyReplyController({ + required this.hasRoot, + required this.id, + required this.oid, + required this.rpid, + required this.dialog, + required this.replyType, + required this.isDialogue, + }); + final int? dialog; + final bool isDialogue; final itemScrollCtr = ItemScrollController(); bool hasRoot = false; int? id; // 视频aid 请求时使用的oid - int? aid; + int? oid; // rpid 请求楼中楼回复 - String? rpid; + int? rpid; ReplyType replyType; // = ReplyType.video; // 当前页 RxString noMore = ''.obs; @@ -94,8 +98,8 @@ class VideoReplyReplyController extends CommonController @override bool customHandleResponse(Success response) { - DetailListReply replies = response.response; - if (cursor == null) { + dynamic replies = response.response; + if (replies is DetailListReply && cursor == null) { count.value = replies.root.count.toInt(); if (id != null) { index = replies.root.replies @@ -127,36 +131,67 @@ class VideoReplyReplyController extends CommonController } upMid ??= replies.subjectControl.upMid.toInt(); cursor = replies.cursor; - if (replies.root.replies.isNotEmpty) { - noMore.value = '加载中...'; - if (replies.cursor.isEnd) { - noMore.value = '没有更多了'; + if (isDialogue) { + if (replies.replies.isNotEmpty) { + noMore.value = '加载中...'; + if (replies.cursor.isEnd) { + noMore.value = '没有更多了'; + } + } else { + // 未登录状态replies可能返回null + noMore.value = currentPage == 1 ? '还没有评论' : '没有更多了'; } } else { - // 未登录状态replies可能返回null - noMore.value = currentPage == 1 ? '还没有评论' : '没有更多了'; + if (replies.root.replies.isNotEmpty) { + noMore.value = '加载中...'; + if (replies.cursor.isEnd) { + noMore.value = '没有更多了'; + } + } else { + // 未登录状态replies可能返回null + noMore.value = currentPage == 1 ? '还没有评论' : '没有更多了'; + } } if (currentPage != 1) { List list = loadingState.value is Success ? (loadingState.value as Success).response : []; - replies.root.replies.insertAll(0, list); + if (isDialogue) { + replies.replies.insertAll(0, list); + } else { + replies.root.replies.insertAll(0, list); + } + } + if (isDialogue) { + loadingState.value = LoadingState.success(replies.replies); + } else { + loadingState.value = LoadingState.success(replies.root.replies); } - loadingState.value = LoadingState.success(replies.root.replies); return true; } @override - Future customGetData() => ReplyHttp.replyReplyListGrpc( - type: replyType.index, - oid: aid!, - root: int.parse(rpid!), - rpid: id ?? 0, - cursor: CursorReq( - next: cursor?.next, - mode: mode.value, - ), - ); + Future customGetData() => isDialogue + ? ReplyHttp.dialogListGrpc( + type: replyType.index, + oid: oid!, + root: rpid!, + rpid: dialog!, + cursor: CursorReq( + next: cursor?.next, + mode: mode.value, + ), + ) + : ReplyHttp.replyReplyListGrpc( + type: replyType.index, + oid: oid!, + root: rpid!, + rpid: id ?? 0, + cursor: CursorReq( + next: cursor?.next, + mode: mode.value, + ), + ); queryBySort() { noMore.value = ''; diff --git a/lib/pages/video/detail/reply_reply/view.dart b/lib/pages/video/detail/reply_reply/view.dart index 66fd64bfe..86a5d113c 100644 --- a/lib/pages/video/detail/reply_reply/view.dart +++ b/lib/pages/video/detail/reply_reply/view.dart @@ -19,18 +19,22 @@ class VideoReplyReplyPanel extends StatefulWidget { this.id, this.oid, this.rpid, + this.dialog, this.firstFloor, this.source, this.replyType, + this.isDialogue = false, super.key, }); // final dynamic rcount; - final dynamic id; + final int? id; final int? oid; final int? rpid; + final int? dialog; final ReplyInfo? firstFloor; final String? source; final ReplyType? replyType; + final bool isDialogue; @override State createState() => _VideoReplyReplyPanelState(); @@ -39,20 +43,23 @@ class VideoReplyReplyPanel extends StatefulWidget { class _VideoReplyReplyPanelState extends State { late VideoReplyReplyController _videoReplyReplyController; late final _savedReplies = {}; - final itemPositionsListener = ItemPositionsListener.create(); + late final itemPositionsListener = ItemPositionsListener.create(); + late final _key = GlobalKey(); @override void initState() { super.initState(); _videoReplyReplyController = Get.put( VideoReplyReplyController( - widget.firstFloor != null, - widget.id, - widget.oid, - widget.rpid.toString(), - widget.replyType!, + hasRoot: widget.firstFloor != null, + id: widget.id, + oid: widget.oid, + rpid: widget.rpid, + dialog: widget.dialog, + replyType: widget.replyType!, + isDialogue: widget.isDialogue, ), - tag: widget.rpid.toString(), + tag: '${widget.rpid}${widget.dialog}${widget.isDialogue}', ); } @@ -61,123 +68,134 @@ class _VideoReplyReplyPanelState extends State { _videoReplyReplyController.controller?.stop(); _videoReplyReplyController.controller?.dispose(); _videoReplyReplyController.controller = null; - Get.delete(tag: widget.rpid.toString()); + Get.delete( + tag: '${widget.rpid}${widget.dialog}${widget.isDialogue}', + ); super.dispose(); } - Widget get _header => 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 widget.firstFloor == null - ? _sortWidget - : min >= 2 - ? _sortWidget - : const SizedBox.shrink(); - }, - ); + Widget get _header => widget.firstFloor == null + ? _sortWidget + : 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 : const SizedBox.shrink(); + }, + ); @override Widget build(BuildContext context) { - return Container( - height: - widget.source == 'videoDetail' ? Utils.getSheetHeight(context) : null, - color: Theme.of(context).colorScheme.surface, - child: Column( - children: [ - if (widget.source == 'videoDetail') - Container( - height: 45, - padding: const EdgeInsets.only(left: 12, right: 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('评论详情'), - IconButton( - tooltip: '关闭', - icon: const Icon(Icons.close, size: 20), - onPressed: Get.back, - ), - ], - ), - ), - Divider( - height: 1, - color: Theme.of(context).dividerColor.withOpacity(0.1), - ), - Expanded( - child: RefreshIndicator( - onRefresh: () async { - await _videoReplyReplyController.onRefresh(); - }, - child: Obx( - () => Stack( - children: [ - ScrollablePositionedList.builder( - itemPositionsListener: itemPositionsListener, - 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)); - } - } - }, + return Scaffold( + key: _key, + resizeToAvoidBottomInset: false, + body: Container( + height: widget.source == 'videoDetail' + ? Utils.getSheetHeight(context) + : null, + color: Theme.of(context).colorScheme.surface, + child: Column( + children: [ + if (widget.source == 'videoDetail') + Container( + height: 45, + padding: const EdgeInsets.only(left: 12, right: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(widget.isDialogue ? '对话列表' : '评论详情'), + IconButton( + tooltip: '关闭', + icon: const Icon(Icons.close, size: 20), + onPressed: Get.back, ), - if (_videoReplyReplyController.loadingState.value - is Success) - _header, ], ), ), + Divider( + height: 1, + color: Theme.of(context).dividerColor.withOpacity(0.1), ), - ), - ], + Expanded( + child: RefreshIndicator( + onRefresh: () async { + await _videoReplyReplyController.onRefresh(); + }, + child: Obx( + () => Stack( + children: [ + ScrollablePositionedList.builder( + itemPositionsListener: itemPositionsListener, + itemCount: _itemCount( + _videoReplyReplyController.loadingState.value), + itemScrollController: + _videoReplyReplyController.itemScrollCtr, + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (_, index) { + if (widget.isDialogue) { + return Obx(() => _buildBody( + _videoReplyReplyController.loadingState.value, + index)); + } else 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)); + } + } + }, + ), + if (!widget.isDialogue && + _videoReplyReplyController.loadingState.value + is Success) + _header, + ], + ), + ), + ), + ), + ], + ), ), ); } @@ -337,7 +355,7 @@ class _VideoReplyReplyPanelState extends State { Widget _replyItem(replyItem) { return ReplyItemGrpc( replyItem: replyItem, - replyLevel: '2', + replyLevel: widget.isDialogue ? '3' : '2', showReplyRow: false, replyType: widget.replyType, onReply: () { @@ -351,10 +369,25 @@ class _VideoReplyReplyPanelState extends State { LoadingState.success(list); }, upMid: _videoReplyReplyController.upMid, + showDialogue: () { + _key.currentState?.showBottomSheet( + (context) => VideoReplyReplyPanel( + oid: replyItem.oid.toInt(), + rpid: replyItem.root.toInt(), + dialog: replyItem.dialog.toInt(), + replyType: ReplyType.video, + source: 'videoDetail', + isDialogue: true, + ), + ); + }, ); } int _itemCount(LoadingState loadingState) { + if (widget.isDialogue) { + return (loadingState is Success ? loadingState.response.length : 0) + 1; + } int itemCount = 0; if (widget.firstFloor != null) { itemCount = 2;