feat: set top reply

Closes #589

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-04-03 14:50:42 +08:00
parent 7437d8c592
commit 978d634cb3
9 changed files with 139 additions and 23 deletions

View File

@@ -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';
}

View File

@@ -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']};
}
}
}

View File

@@ -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']);
}
}
}

View File

@@ -186,7 +186,6 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
source: 'dynamic',
replyType: ReplyType.values[replyType],
firstFloor: replyItem,
isTop: isTop ?? false,
onDispose: onDispose,
),
);
@@ -826,11 +825,18 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
);
},
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,
),
);
}
},

View File

@@ -183,7 +183,6 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
source: 'dynamic',
replyType: ReplyType.values[type],
firstFloor: replyItem,
isTop: isTop ?? false,
onDispose: onDispose,
),
);
@@ -799,11 +798,17 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
);
},
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,
),
);
}
},

View File

@@ -249,7 +249,6 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
);
},
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<VideoReplyPanel>
callback: widget.callback,
onCheckReply: (item) =>
_videoReplyController.onCheckReply(context, item),
onToggleTop: (isUpTop, rpid) =>
_videoReplyController.onToggleTop(
index,
_videoReplyController.aid,
ReplyType.video.index,
isUpTop,
rpid,
),
);
}
},

View File

@@ -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<int>? onDismissed;
final Function(List<String>, int)? callback;
final ValueChanged<ReplyInfo> 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<dynamic> 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: <Widget>[
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,

View File

@@ -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<int>? 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,
),
);
}

View File

@@ -2214,7 +2214,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
);
// 展示二级回复
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<VideoDetailPageV>
firstFloor: replyItem,
replyType: ReplyType.video,
source: 'videoDetail',
isTop: isTop ?? false,
onViewImage: videoDetailController.onViewImage,
onDismissed: videoDetailController.onDismissed,
),