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,36 +131,67 @@ class VideoReplyReplyController extends CommonController
} }
upMid ??= replies.subjectControl.upMid.toInt(); upMid ??= replies.subjectControl.upMid.toInt();
cursor = replies.cursor; cursor = replies.cursor;
if (replies.root.replies.isNotEmpty) { if (isDialogue) {
noMore.value = '加载中...'; if (replies.replies.isNotEmpty) {
if (replies.cursor.isEnd) { noMore.value = '加载中...';
noMore.value = '没有更多了'; if (replies.cursor.isEnd) {
noMore.value = '没有更多了';
}
} else {
// 未登录状态replies可能返回null
noMore.value = currentPage == 1 ? '还没有评论' : '没有更多了';
} }
} else { } else {
// 未登录状态replies可能返回null if (replies.root.replies.isNotEmpty) {
noMore.value = currentPage == 1 ? '还没有评论' : '没有更多了'; noMore.value = '加载中...';
if (replies.cursor.isEnd) {
noMore.value = '没有更多了';
}
} else {
// 未登录状态replies可能返回null
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>[];
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; return true;
} }
@override @override
Future<LoadingState> customGetData() => ReplyHttp.replyReplyListGrpc( Future<LoadingState> customGetData() => isDialogue
type: replyType.index, ? ReplyHttp.dialogListGrpc(
oid: aid!, type: replyType.index,
root: int.parse(rpid!), oid: oid!,
rpid: id ?? 0, root: rpid!,
cursor: CursorReq( rpid: dialog!,
next: cursor?.next, cursor: CursorReq(
mode: mode.value, 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() { queryBySort() {
noMore.value = ''; noMore.value = '';

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,123 +68,134 @@ 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
valueListenable: itemPositionsListener.itemPositions, ? _sortWidget
builder: (context, positions, child) { : ValueListenableBuilder<Iterable<ItemPosition>>(
int min = -1; valueListenable: itemPositionsListener.itemPositions,
if (positions.isNotEmpty) { builder: (context, positions, child) {
min = positions int min = -1;
.where((ItemPosition position) => position.itemTrailingEdge > 0) if (positions.isNotEmpty) {
.reduce((ItemPosition min, ItemPosition position) => min = positions
position.itemTrailingEdge < min.itemTrailingEdge .where(
? position (ItemPosition position) => position.itemTrailingEdge > 0)
: min) .reduce((ItemPosition min, ItemPosition position) =>
.index; position.itemTrailingEdge < min.itemTrailingEdge
} ? position
return widget.firstFloor == null : min)
? _sortWidget .index;
: min >= 2 }
? _sortWidget return min >= 2 ? _sortWidget : const SizedBox.shrink();
: 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,
color: Theme.of(context).colorScheme.surface, body: Container(
child: Column( height: widget.source == 'videoDetail'
children: [ ? Utils.getSheetHeight(context)
if (widget.source == 'videoDetail') : null,
Container( color: Theme.of(context).colorScheme.surface,
height: 45, child: Column(
padding: const EdgeInsets.only(left: 12, right: 2), children: [
child: Row( if (widget.source == 'videoDetail')
mainAxisAlignment: MainAxisAlignment.spaceBetween, Container(
children: <Widget>[ height: 45,
Text('评论详情'), padding: const EdgeInsets.only(left: 12, right: 2),
IconButton( child: Row(
tooltip: '关闭', mainAxisAlignment: MainAxisAlignment.spaceBetween,
icon: const Icon(Icons.close, size: 20), children: <Widget>[
onPressed: Get.back, Text(widget.isDialogue ? '对话列表' : '评论详情'),
), 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));
}
}
},
), ),
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<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;