mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-31 08:08:19 +08:00
feat: show reply dialogue list
This commit is contained in:
@@ -47,6 +47,7 @@
|
|||||||
|
|
||||||
## feat
|
## feat
|
||||||
|
|
||||||
|
- [x] 评论楼中楼查看对话
|
||||||
- [x] 评论楼中楼定位点击查看的评论
|
- [x] 评论楼中楼定位点击查看的评论
|
||||||
- [x] 评论楼中楼按热度/时间排序
|
- [x] 评论楼中楼按热度/时间排序
|
||||||
- [x] 评论点踩
|
- [x] 评论点踩
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user