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
- [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({
int type = 1,
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({
int type = 1,
required int oid,

View File

@@ -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)

View File

@@ -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<ReplyInfo> list = loadingState.value is Success
? (loadingState.value as Success).response
: <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;
}
@override
Future<LoadingState> customGetData() => ReplyHttp.replyReplyListGrpc(
type: replyType.index,
oid: aid!,
root: int.parse(rpid!),
rpid: id ?? 0,
cursor: CursorReq(
next: cursor?.next,
mode: mode.value,
),
);
Future<LoadingState> 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 = '';

View File

@@ -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<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
@@ -39,20 +43,23 @@ class VideoReplyReplyPanel extends StatefulWidget {
class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
late VideoReplyReplyController _videoReplyReplyController;
late final _savedReplies = {};
final itemPositionsListener = ItemPositionsListener.create();
late final itemPositionsListener = ItemPositionsListener.create();
late final _key = GlobalKey<ScaffoldState>();
@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<VideoReplyReplyPanel> {
_videoReplyReplyController.controller?.stop();
_videoReplyReplyController.controller?.dispose();
_videoReplyReplyController.controller = null;
Get.delete<VideoReplyReplyController>(tag: widget.rpid.toString());
Get.delete<VideoReplyReplyController>(
tag: '${widget.rpid}${widget.dialog}${widget.isDialogue}',
);
super.dispose();
}
Widget get _header => ValueListenableBuilder<Iterable<ItemPosition>>(
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<Iterable<ItemPosition>>(
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: <Widget>[
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: <Widget>[
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<VideoReplyReplyPanel> {
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<VideoReplyReplyPanel> {
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;