feat: show reply dialogue list

This commit is contained in:
bggRGjQaUbCoE
2024-10-18 17:32:54 +08:00
parent b57d1a0a62
commit a1c28569fb
6 changed files with 275 additions and 146 deletions

View File

@@ -47,6 +47,7 @@
## feat ## feat
- [x] 评论楼中楼查看对话
- [x] 评论楼中楼定位点击查看的评论 - [x] 评论楼中楼定位点击查看的评论
- [x] 评论楼中楼按热度/时间排序 - [x] 评论楼中楼按热度/时间排序
- [x] 评论点踩 - [x] 评论点踩

View File

@@ -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({ static Future detailList({
int type = 1, int type = 1,
required int oid, required int oid,

View File

@@ -99,6 +99,27 @@ class ReplyHttp {
} }
} }
static Future<LoadingState> 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<LoadingState> replyReplyListGrpc({ static Future<LoadingState> replyReplyListGrpc({
int type = 1, int type = 1,
required int oid, required int oid,

View File

@@ -35,6 +35,7 @@ class ReplyItemGrpc extends StatelessWidget {
this.onDelete, this.onDelete,
this.upMid, this.upMid,
this.isTop = false, this.isTop = false,
this.showDialogue,
}); });
final ReplyInfo replyItem; final ReplyInfo replyItem;
final String? replyLevel; final String? replyLevel;
@@ -46,6 +47,7 @@ class ReplyItemGrpc extends StatelessWidget {
final Function(dynamic rpid, dynamic frpid)? onDelete; final Function(dynamic rpid, dynamic frpid)? onDelete;
final dynamic upMid; final dynamic upMid;
final bool isTop; final bool isTop;
final VoidCallback? showDialogue;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -64,8 +66,7 @@ class ReplyItemGrpc extends StatelessWidget {
// showDialog( // showDialog(
// context: Get.context!, // context: Get.context!,
// builder: (_) => AlertDialog( // builder: (_) => AlertDialog(
// content: SelectableText( // content: SelectableText(jsonEncode(replyItem.toProto3Json())),
// jsonEncode(replyItem.replyControl.toProto3Json())),
// ), // ),
// ); // );
showModalBottomSheet( showModalBottomSheet(
@@ -392,6 +393,23 @@ class ReplyItemGrpc extends StatelessWidget {
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize), 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(), const Spacer(),
ZanButtonGrpc(replyItem: replyItem, replyType: replyType), ZanButtonGrpc(replyItem: replyItem, replyType: replyType),
const SizedBox(width: 5) const SizedBox(width: 5)

View File

@@ -9,20 +9,24 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class VideoReplyReplyController extends CommonController class VideoReplyReplyController extends CommonController
with GetTickerProviderStateMixin { with GetTickerProviderStateMixin {
VideoReplyReplyController( VideoReplyReplyController({
this.hasRoot, required this.hasRoot,
this.id, required this.id,
this.aid, required this.oid,
this.rpid, required this.rpid,
this.replyType, required this.dialog,
); required this.replyType,
required this.isDialogue,
});
final int? dialog;
final bool isDialogue;
final itemScrollCtr = ItemScrollController(); final itemScrollCtr = ItemScrollController();
bool hasRoot = false; bool hasRoot = false;
int? id; int? id;
// 视频aid 请求时使用的oid // 视频aid 请求时使用的oid
int? aid; int? oid;
// rpid 请求楼中楼回复 // rpid 请求楼中楼回复
String? rpid; int? rpid;
ReplyType replyType; // = ReplyType.video; ReplyType replyType; // = ReplyType.video;
// 当前页 // 当前页
RxString noMore = ''.obs; RxString noMore = ''.obs;
@@ -94,8 +98,8 @@ class VideoReplyReplyController extends CommonController
@override @override
bool customHandleResponse(Success response) { bool customHandleResponse(Success response) {
DetailListReply replies = response.response; dynamic replies = response.response;
if (cursor == null) { if (replies is DetailListReply && cursor == null) {
count.value = replies.root.count.toInt(); count.value = replies.root.count.toInt();
if (id != null) { if (id != null) {
index = replies.root.replies index = replies.root.replies
@@ -127,6 +131,17 @@ class VideoReplyReplyController extends CommonController
} }
upMid ??= replies.subjectControl.upMid.toInt(); upMid ??= replies.subjectControl.upMid.toInt();
cursor = replies.cursor; cursor = replies.cursor;
if (isDialogue) {
if (replies.replies.isNotEmpty) {
noMore.value = '加载中...';
if (replies.cursor.isEnd) {
noMore.value = '没有更多了';
}
} else {
// 未登录状态replies可能返回null
noMore.value = currentPage == 1 ? '还没有评论' : '没有更多了';
}
} else {
if (replies.root.replies.isNotEmpty) { if (replies.root.replies.isNotEmpty) {
noMore.value = '加载中...'; noMore.value = '加载中...';
if (replies.cursor.isEnd) { if (replies.cursor.isEnd) {
@@ -136,21 +151,41 @@ class VideoReplyReplyController extends CommonController
// 未登录状态replies可能返回null // 未登录状态replies可能返回null
noMore.value = currentPage == 1 ? '还没有评论' : '没有更多了'; noMore.value = currentPage == 1 ? '还没有评论' : '没有更多了';
} }
}
if (currentPage != 1) { if (currentPage != 1) {
List<ReplyInfo> list = loadingState.value is Success List<ReplyInfo> list = loadingState.value is Success
? (loadingState.value as Success).response ? (loadingState.value as Success).response
: <ReplyInfo>[]; : <ReplyInfo>[];
if (isDialogue) {
replies.replies.insertAll(0, list);
} else {
replies.root.replies.insertAll(0, list); 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; return true;
} }
@override @override
Future<LoadingState> customGetData() => ReplyHttp.replyReplyListGrpc( Future<LoadingState> customGetData() => isDialogue
? ReplyHttp.dialogListGrpc(
type: replyType.index, type: replyType.index,
oid: aid!, oid: oid!,
root: int.parse(rpid!), root: rpid!,
rpid: dialog!,
cursor: CursorReq(
next: cursor?.next,
mode: mode.value,
),
)
: ReplyHttp.replyReplyListGrpc(
type: replyType.index,
oid: oid!,
root: rpid!,
rpid: id ?? 0, rpid: id ?? 0,
cursor: CursorReq( cursor: CursorReq(
next: cursor?.next, next: cursor?.next,

View File

@@ -19,18 +19,22 @@ class VideoReplyReplyPanel extends StatefulWidget {
this.id, this.id,
this.oid, this.oid,
this.rpid, this.rpid,
this.dialog,
this.firstFloor, this.firstFloor,
this.source, this.source,
this.replyType, this.replyType,
this.isDialogue = false,
super.key, super.key,
}); });
// final dynamic rcount; // final dynamic rcount;
final dynamic id; final int? id;
final int? oid; final int? oid;
final int? rpid; final int? rpid;
final int? dialog;
final ReplyInfo? firstFloor; final ReplyInfo? firstFloor;
final String? source; final String? source;
final ReplyType? replyType; final ReplyType? replyType;
final bool isDialogue;
@override @override
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState(); State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
@@ -39,20 +43,23 @@ class VideoReplyReplyPanel extends StatefulWidget {
class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> { class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
late VideoReplyReplyController _videoReplyReplyController; late VideoReplyReplyController _videoReplyReplyController;
late final _savedReplies = {}; late final _savedReplies = {};
final itemPositionsListener = ItemPositionsListener.create(); late final itemPositionsListener = ItemPositionsListener.create();
late final _key = GlobalKey<ScaffoldState>();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_videoReplyReplyController = Get.put( _videoReplyReplyController = Get.put(
VideoReplyReplyController( VideoReplyReplyController(
widget.firstFloor != null, hasRoot: widget.firstFloor != null,
widget.id, id: widget.id,
widget.oid, oid: widget.oid,
widget.rpid.toString(), rpid: widget.rpid,
widget.replyType!, dialog: widget.dialog,
replyType: widget.replyType!,
isDialogue: widget.isDialogue,
), ),
tag: widget.rpid.toString(), tag: '${widget.rpid}${widget.dialog}${widget.isDialogue}',
); );
} }
@@ -61,36 +68,41 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
_videoReplyReplyController.controller?.stop(); _videoReplyReplyController.controller?.stop();
_videoReplyReplyController.controller?.dispose(); _videoReplyReplyController.controller?.dispose();
_videoReplyReplyController.controller = null; _videoReplyReplyController.controller = null;
Get.delete<VideoReplyReplyController>(tag: widget.rpid.toString()); Get.delete<VideoReplyReplyController>(
tag: '${widget.rpid}${widget.dialog}${widget.isDialogue}',
);
super.dispose(); super.dispose();
} }
Widget get _header => ValueListenableBuilder<Iterable<ItemPosition>>( Widget get _header => widget.firstFloor == null
? _sortWidget
: ValueListenableBuilder<Iterable<ItemPosition>>(
valueListenable: itemPositionsListener.itemPositions, valueListenable: itemPositionsListener.itemPositions,
builder: (context, positions, child) { builder: (context, positions, child) {
int min = -1; int min = -1;
if (positions.isNotEmpty) { if (positions.isNotEmpty) {
min = positions min = positions
.where((ItemPosition position) => position.itemTrailingEdge > 0) .where(
(ItemPosition position) => position.itemTrailingEdge > 0)
.reduce((ItemPosition min, ItemPosition position) => .reduce((ItemPosition min, ItemPosition position) =>
position.itemTrailingEdge < min.itemTrailingEdge position.itemTrailingEdge < min.itemTrailingEdge
? position ? position
: min) : min)
.index; .index;
} }
return widget.firstFloor == null return min >= 2 ? _sortWidget : const SizedBox.shrink();
? _sortWidget
: min >= 2
? _sortWidget
: const SizedBox.shrink();
}, },
); );
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Scaffold(
height: key: _key,
widget.source == 'videoDetail' ? Utils.getSheetHeight(context) : null, resizeToAvoidBottomInset: false,
body: Container(
height: widget.source == 'videoDetail'
? Utils.getSheetHeight(context)
: null,
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: Column( child: Column(
children: [ children: [
@@ -101,7 +113,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Text('评论详情'), Text(widget.isDialogue ? '对话列表' : '评论详情'),
IconButton( IconButton(
tooltip: '关闭', tooltip: '关闭',
icon: const Icon(Icons.close, size: 20), icon: const Icon(Icons.close, size: 20),
@@ -130,7 +142,11 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
_videoReplyReplyController.itemScrollCtr, _videoReplyReplyController.itemScrollCtr,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (_, index) { itemBuilder: (_, index) {
if (widget.firstFloor != null) { if (widget.isDialogue) {
return Obx(() => _buildBody(
_videoReplyReplyController.loadingState.value,
index));
} else if (widget.firstFloor != null) {
if (index == 0) { if (index == 0) {
return ReplyItemGrpc( return ReplyItemGrpc(
replyItem: widget.firstFloor!, replyItem: widget.firstFloor!,
@@ -169,7 +185,8 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
} }
}, },
), ),
if (_videoReplyReplyController.loadingState.value if (!widget.isDialogue &&
_videoReplyReplyController.loadingState.value
is Success) is Success)
_header, _header,
], ],
@@ -179,6 +196,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
), ),
], ],
), ),
),
); );
} }
@@ -337,7 +355,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
Widget _replyItem(replyItem) { Widget _replyItem(replyItem) {
return ReplyItemGrpc( return ReplyItemGrpc(
replyItem: replyItem, replyItem: replyItem,
replyLevel: '2', replyLevel: widget.isDialogue ? '3' : '2',
showReplyRow: false, showReplyRow: false,
replyType: widget.replyType, replyType: widget.replyType,
onReply: () { onReply: () {
@@ -351,10 +369,25 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
LoadingState.success(list); LoadingState.success(list);
}, },
upMid: _videoReplyReplyController.upMid, 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) { int _itemCount(LoadingState loadingState) {
if (widget.isDialogue) {
return (loadingState is Success ? loadingState.response.length : 0) + 1;
}
int itemCount = 0; int itemCount = 0;
if (widget.firstFloor != null) { if (widget.firstFloor != null) {
itemCount = 2; itemCount = 2;