mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: set top reply
Closes #589 Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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']};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user