mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: show reply dialogue list
This commit is contained in:
@@ -47,6 +47,7 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [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({
|
||||
int type = 1,
|
||||
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({
|
||||
int type = 1,
|
||||
required int oid,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user