diff --git a/lib/http/api.dart b/lib/http/api.dart index 18519540a..2b11af601 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -747,4 +747,6 @@ class Api { static const String delFavArticle = '/x/article/favorites/del'; static const String addFavArticle = '/x/article/favorites/add'; + + static const String replyTop = '/x/v2/reply/top'; } diff --git a/lib/http/reply.dart b/lib/http/reply.dart index a58ce96fc..2d049d1fb 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -399,4 +399,30 @@ class ReplyHttp { return LoadingState.error(res.data['message']); } } + + static Future replyTop({ + required oid, + required type, + required rpid, + required bool isUpTop, + }) async { + var res = await Request().post( + Api.replyTop, + data: { + 'oid': oid, + 'type': type, + 'rpid': rpid, + 'action': isUpTop ? 0 : 1, + 'csrf': await Request.getCsrf(), + }, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } } diff --git a/lib/pages/common/reply_controller.dart b/lib/pages/common/reply_controller.dart index f2f753efd..4f5c860c7 100644 --- a/lib/pages/common/reply_controller.dart +++ b/lib/pages/common/reply_controller.dart @@ -480,4 +480,28 @@ https://api.bilibili.com/x/v2/reply/reply?oid=$oid&pn=1&ps=20&root=${rpid ?? rep } } } + + void onToggleTop(index, oid, int type, bool isUpTop, int rpid) async { + final res = await ReplyHttp.replyTop( + oid: oid, + type: type, + rpid: rpid, + isUpTop: isUpTop, + ); + if (res['status']) { + final data = (loadingState.value as Success).response; + if (data is MainListReply) { + data.replies[index].replyControl.isUpTop = !isUpTop; + if (!isUpTop && index != 0) { + data.replies[0].replyControl.isUpTop = false; + final item = data.replies.removeAt(index); + data.replies.insert(0, item); + } + loadingState.value = LoadingState.success(data); + } + SmartDialog.showToast('${isUpTop ? '取消' : ''}置顶成功'); + } else { + SmartDialog.showToast(res['msg']); + } + } } diff --git a/lib/pages/dynamics/detail/view.dart b/lib/pages/dynamics/detail/view.dart index 2a0a995cd..90f94114a 100644 --- a/lib/pages/dynamics/detail/view.dart +++ b/lib/pages/dynamics/detail/view.dart @@ -186,7 +186,6 @@ class _DynamicDetailPageState extends State source: 'dynamic', replyType: ReplyType.values[replyType], firstFloor: replyItem, - isTop: isTop ?? false, onDispose: onDispose, ), ); @@ -826,11 +825,18 @@ class _DynamicDetailPageState extends State ); }, onDelete: _dynamicDetailController.onMDelete, - isTop: _dynamicDetailController.hasUpTop && index == 0, upMid: loadingState.response.subjectControl.upMid, callback: _getImageCallback, onCheckReply: (item) => _dynamicDetailController.onCheckReply(context, item), + onToggleTop: (isUpTop, rpid) => + _dynamicDetailController.onToggleTop( + index, + _dynamicDetailController.oid, + _dynamicDetailController.type, + isUpTop, + rpid, + ), ); } }, diff --git a/lib/pages/html/view.dart b/lib/pages/html/view.dart index bb772d91c..1ca4d3a33 100644 --- a/lib/pages/html/view.dart +++ b/lib/pages/html/view.dart @@ -183,7 +183,6 @@ class _HtmlRenderPageState extends State source: 'dynamic', replyType: ReplyType.values[type], firstFloor: replyItem, - isTop: isTop ?? false, onDispose: onDispose, ), ); @@ -799,11 +798,17 @@ class _HtmlRenderPageState extends State ); }, onDelete: _htmlRenderCtr.onMDelete, - isTop: _htmlRenderCtr.hasUpTop && index == 0, upMid: loadingState.response.subjectControl.upMid, callback: _getImageCallback, onCheckReply: (item) => _htmlRenderCtr.onCheckReply(context, item), + onToggleTop: (isUpTop, rpid) => _htmlRenderCtr.onToggleTop( + index, + _htmlRenderCtr.oid, + _htmlRenderCtr.type, + isUpTop, + rpid, + ), ); } }, diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index eb9201014..35f64b669 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -249,7 +249,6 @@ class _VideoReplyPanelState extends State ); }, onDelete: _videoReplyController.onMDelete, - isTop: _videoReplyController.hasUpTop && index == 0, upMid: loadingState.response.subjectControl.upMid, getTag: () => heroTag, onViewImage: widget.onViewImage, @@ -257,6 +256,14 @@ class _VideoReplyPanelState extends State callback: widget.callback, onCheckReply: (item) => _videoReplyController.onCheckReply(context, item), + onToggleTop: (isUpTop, rpid) => + _videoReplyController.onToggleTop( + index, + _videoReplyController.aid, + ReplyType.video.index, + isUpTop, + rpid, + ), ); } }, diff --git a/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart b/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart index e16a192f3..87d22eca7 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart @@ -38,13 +38,13 @@ class ReplyItemGrpc extends StatelessWidget { this.onReply, this.onDelete, this.upMid, - this.isTop = false, this.showDialogue, this.getTag, this.onViewImage, this.onDismissed, this.callback, required this.onCheckReply, + required this.onToggleTop, }); final ReplyInfo replyItem; final String? replyLevel; @@ -55,13 +55,13 @@ class ReplyItemGrpc extends StatelessWidget { final Function()? onReply; final Function(dynamic rpid, dynamic frpid)? onDelete; final dynamic upMid; - final bool isTop; final VoidCallback? showDialogue; final Function? getTag; final VoidCallback? onViewImage; final ValueChanged? onDismissed; final Function(List, int)? callback; final ValueChanged onCheckReply; + final Function(bool isUpTop, int rpid) onToggleTop; @override Widget build(BuildContext context) { @@ -71,7 +71,7 @@ class ReplyItemGrpc extends StatelessWidget { // 点击整个评论区 评论详情/回复 onTap: () { feedBack(); - replyReply?.call(replyItem, null, isTop); + replyReply?.call(replyItem, null); }, onLongPress: () { feedBack(); @@ -92,6 +92,7 @@ class ReplyItemGrpc extends StatelessWidget { onDelete: (rpid) { onDelete?.call(rpid, null); }, + isSubReply: false, ); }, ); @@ -368,7 +369,7 @@ class ReplyItemGrpc extends StatelessWidget { style: style, TextSpan( children: [ - if (isTop) ...[ + if (replyItem.replyControl.isUpTop) ...[ const WidgetSpan( alignment: PlaceholderAlignment.top, child: PBadge( @@ -523,7 +524,7 @@ class ReplyItemGrpc extends StatelessWidget { InkWell( // 一楼点击评论展开评论详情 onTap: () => replyReply?.call( - replyItem, replyItem.replies[i].id.toInt(), isTop), + replyItem, replyItem.replies[i].id.toInt()), onLongPress: () { feedBack(); showModalBottomSheet( @@ -537,6 +538,7 @@ class ReplyItemGrpc extends StatelessWidget { onDelete: (rpid) { onDelete?.call(rpid, replyItem.id.toInt()); }, + isSubReply: true, ); }, ); @@ -625,7 +627,7 @@ class ReplyItemGrpc extends StatelessWidget { if (extraRow) InkWell( // 一楼点击【共xx条回复】展开评论详情 - onTap: () => replyReply?.call(replyItem, null, isTop), + onTap: () => replyReply?.call(replyItem, null), child: Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(8, 5, 8, 8), @@ -1073,7 +1075,9 @@ class ReplyItemGrpc extends StatelessWidget { required BuildContext context, required ReplyInfo item, required onDelete, + required bool isSubReply, }) { + int ownerMid = Accounts.main.mid; Future menuActionHandler(String type) async { late String message = item.content.message; switch (type) { @@ -1132,15 +1136,35 @@ class ReplyItemGrpc extends StatelessWidget { context: context, builder: (context) { return AlertDialog( - title: const Text('删除评论(测试)'), - content: Text( - '确定尝试删除这条评论吗?\n\n$message\n\n注:只能删除自己的评论,或自己管理的评论区下的评论'), + title: const Text('删除评论'), + content: Text.rich( + TextSpan( + children: [ + TextSpan(text: '确定删除这条评论吗?\n\n'), + if (ownerMid != item.member.mid.toInt()) ...[ + TextSpan( + text: '@${item.member.name}', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ), + TextSpan(text: ':\n'), + ], + TextSpan(text: message), + ], + ), + ), actions: [ TextButton( onPressed: () { Get.back(result: false); }, - child: const Text('取消'), + child: Text( + '取消', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), + ), ), TextButton( onPressed: () { @@ -1173,11 +1197,14 @@ class ReplyItemGrpc extends StatelessWidget { Get.back(); onCheckReply(item); break; + case 'top': + Get.back(); + onToggleTop(item.replyControl.isUpTop, item.id.toInt()); + break; default: } } - int ownerMid = Accounts.main.mid; Color errorColor = Theme.of(context).colorScheme.error; return Padding( @@ -1210,7 +1237,7 @@ class ReplyItemGrpc extends StatelessWidget { ), ), ), - if (ownerMid != 0) ...[ + if (ownerMid == upMid.toInt() || ownerMid == item.member.mid.toInt()) ListTile( onTap: () => menuActionHandler('delete'), minLeadingWidth: 0, @@ -1221,6 +1248,7 @@ class ReplyItemGrpc extends StatelessWidget { .titleSmall! .copyWith(color: errorColor)), ), + if (ownerMid != 0) ListTile( onTap: () => menuActionHandler('report'), minLeadingWidth: 0, @@ -1231,7 +1259,14 @@ class ReplyItemGrpc extends StatelessWidget { .titleSmall! .copyWith(color: errorColor)), ), - ], + if (replyLevel == '1' && isSubReply.not && ownerMid == upMid.toInt()) + ListTile( + onTap: () => menuActionHandler('top'), + minLeadingWidth: 0, + leading: Icon(Icons.vertical_align_top, size: 19), + title: Text('${replyItem.replyControl.isUpTop ? '取消' : ''}置顶', + style: Theme.of(context).textTheme.titleSmall!), + ), ListTile( onTap: () => menuActionHandler('copyAll'), minLeadingWidth: 0, diff --git a/lib/pages/video/detail/reply_reply/view.dart b/lib/pages/video/detail/reply_reply/view.dart index 7afa2270b..19673052d 100644 --- a/lib/pages/video/detail/reply_reply/view.dart +++ b/lib/pages/video/detail/reply_reply/view.dart @@ -27,7 +27,6 @@ class VideoReplyReplyPanel extends CommonSlidePage { this.source, required this.replyType, this.isDialogue = false, - this.isTop = false, this.onViewImage, this.onDismissed, this.onDispose, @@ -40,7 +39,6 @@ class VideoReplyReplyPanel extends CommonSlidePage { final String? source; final ReplyType replyType; final bool isDialogue; - final bool isTop; final VoidCallback? onViewImage; final ValueChanged? onDismissed; final VoidCallback? onDispose; @@ -188,12 +186,19 @@ class _VideoReplyReplyPanelState _onReply(firstFloor, -1); }, upMid: _videoReplyReplyController.upMid, - isTop: widget.isTop, onViewImage: widget.onViewImage, onDismissed: widget.onDismissed, callback: _getImageCallback, onCheckReply: (item) => _videoReplyReplyController .onCheckReply(context, item), + onToggleTop: (isUpTop, rpid) => + _videoReplyReplyController.onToggleTop( + index, + _videoReplyReplyController.oid, + _videoReplyReplyController.replyType.index, + isUpTop, + rpid, + ), ); } else if (index == 1) { return Divider( @@ -482,6 +487,13 @@ class _VideoReplyReplyPanelState callback: _getImageCallback, onCheckReply: (item) => _videoReplyReplyController.onCheckReply(context, item), + onToggleTop: (isUpTop, rpid) => _videoReplyReplyController.onToggleTop( + index, + _videoReplyReplyController.oid, + _videoReplyReplyController.replyType.index, + isUpTop, + rpid, + ), ); } diff --git a/lib/pages/video/detail/view_v.dart b/lib/pages/video/detail/view_v.dart index 28f5222ab..d015d9423 100644 --- a/lib/pages/video/detail/view_v.dart +++ b/lib/pages/video/detail/view_v.dart @@ -2214,7 +2214,7 @@ class _VideoDetailPageVState extends State ); // 展示二级回复 - void replyReply(replyItem, id, isTop) { + void replyReply(replyItem, id) { EasyThrottle.throttle('replyReply', const Duration(milliseconds: 500), () { int oid = replyItem.oid.toInt(); int rpid = replyItem.id.toInt(); @@ -2227,7 +2227,6 @@ class _VideoDetailPageVState extends State firstFloor: replyItem, replyType: ReplyType.video, source: 'videoDetail', - isTop: isTop ?? false, onViewImage: videoDetailController.onViewImage, onDismissed: videoDetailController.onDismissed, ),