feat: 回复我的

This commit is contained in:
guozhigq
2024-06-16 13:58:43 +08:00
parent 86023e46a6
commit 1ebbdfb6ca
9 changed files with 516 additions and 2 deletions

View File

@ -542,4 +542,7 @@ class Api {
/// 消息未读数
static const String unread = '${HttpString.tUrl}/x/im/web/msgfeed/unread';
/// 回复我的
static const String messageReplyAPi = '/x/msgfeed/reply';
}

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:math';
import 'package:dio/dio.dart';
import 'package:pilipala/models/msg/reply.dart';
import '../models/msg/account.dart';
import '../models/msg/session.dart';
import '../utils/wbi_sign.dart';
@ -237,4 +238,29 @@ class MsgHttp {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
// 回复我的
static Future messageReply({
int? id,
int? replyTime,
}) async {
var params = {
if (id != null) 'id': id,
if (replyTime != null) 'reply_time': replyTime,
};
var res = await Request().get(Api.messageReplyAPi, data: params);
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': MessageReplyModel.fromJson(res.data['data']),
};
} catch (err) {
print(err);
return {'status': false, 'date': [], 'msg': err.toString()};
}
} else {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
}

168
lib/models/msg/reply.dart Normal file
View File

@ -0,0 +1,168 @@
class MessageReplyModel {
MessageReplyModel({
this.cursor,
this.items,
});
Cursor? cursor;
List<MessageReplyItem>? items;
MessageReplyModel.fromJson(Map<String, dynamic> json) {
cursor = Cursor.fromJson(json['cursor']);
items = json["items"] != null
? json["items"].map<MessageReplyItem>((e) {
return MessageReplyItem.fromJson(e);
}).toList()
: [];
}
}
class Cursor {
Cursor({
this.id,
this.isEnd,
this.time,
});
int? id;
bool? isEnd;
int? time;
Cursor.fromJson(Map<String, dynamic> json) {
id = json['id'];
isEnd = json['is_end'];
time = json['time'];
}
}
class MessageReplyItem {
MessageReplyItem({
this.count,
this.id,
this.isMulti,
this.item,
this.replyTime,
this.user,
});
int? count;
int? id;
int? isMulti;
ReplyContentItem? item;
int? replyTime;
ReplyUser? user;
MessageReplyItem.fromJson(Map<String, dynamic> json) {
count = json['count'];
id = json['id'];
isMulti = json['is_multi'];
item = ReplyContentItem.fromJson(json["item"]);
replyTime = json['reply_time'];
user = ReplyUser.fromJson(json['user']);
}
}
class ReplyContentItem {
ReplyContentItem({
this.subjectId,
this.rootId,
this.sourceId,
this.targetId,
this.type,
this.businessId,
this.business,
this.title,
this.desc,
this.image,
this.uri,
this.nativeUri,
this.detailTitle,
this.rootReplyContent,
this.sourceContent,
this.targetReplyContent,
this.atDetails,
this.topicDetails,
this.hideReplyButton,
this.hideLikeButton,
this.likeState,
this.danmu,
this.message,
});
int? subjectId;
int? rootId;
int? sourceId;
int? targetId;
String? type;
int? businessId;
String? business;
String? title;
String? desc;
String? image;
String? uri;
String? nativeUri;
String? detailTitle;
String? rootReplyContent;
String? sourceContent;
String? targetReplyContent;
List? atDetails;
List? topicDetails;
bool? hideReplyButton;
bool? hideLikeButton;
int? likeState;
String? danmu;
String? message;
ReplyContentItem.fromJson(Map<String, dynamic> json) {
subjectId = json['subject_id'];
rootId = json['root_id'];
sourceId = json['source_id'];
targetId = json['target_id'];
type = json['type'];
businessId = json['business_id'];
business = json['business'];
title = json['title'];
desc = json['desc'];
image = json['image'];
uri = json['uri'];
nativeUri = json['native_uri'];
detailTitle = json['detail_title'];
rootReplyContent = json['root_reply_content'];
sourceContent = json['source_content'];
targetReplyContent = json['target_reply_content'];
atDetails = json['at_details'];
topicDetails = json['topic_details'];
hideReplyButton = json['hide_reply_button'];
hideLikeButton = json['hide_like_button'];
likeState = json['like_state'];
danmu = json['danmu'];
message = json['message'];
}
}
class ReplyUser {
ReplyUser({
this.mid,
this.fans,
this.nickname,
this.avatar,
this.midLink,
this.follow,
});
int? mid;
int? fans;
String? nickname;
String? avatar;
String? midLink;
bool? follow;
ReplyUser.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
fans = json['fans'];
nickname = json['nickname'];
avatar = json['avatar'];
midLink = json['mid_link'];
follow = json['follow'];
}
}

View File

@ -0,0 +1,25 @@
import 'package:get/get.dart';
import 'package:pilipala/http/msg.dart';
import 'package:pilipala/models/msg/reply.dart';
class MessageReplyController extends GetxController {
Cursor? cursor;
RxList<MessageReplyItem> replyItems = <MessageReplyItem>[].obs;
Future queryMessageReply({String type = 'init'}) async {
if (cursor != null && cursor!.isEnd == true) {
return {};
}
var params = {
if (type == 'onLoad') 'id': cursor!.id,
if (type == 'onLoad') 'replyTime': cursor!.time,
};
var res = await MsgHttp.messageReply(
id: params['id'], replyTime: params['replyTime']);
if (res['status']) {
cursor = res['data'].cursor;
replyItems.addAll(res['data'].items);
}
return res;
}
}

View File

@ -0,0 +1,4 @@
library message_reply;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,285 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/msg/reply.dart';
import 'package:pilipala/utils/utils.dart';
import 'controller.dart';
class MessageReplyPage extends StatefulWidget {
const MessageReplyPage({super.key});
@override
State<MessageReplyPage> createState() => _MessageReplyPageState();
}
class _MessageReplyPageState extends State<MessageReplyPage> {
final MessageReplyController _messageReplyCtr =
Get.put(MessageReplyController());
late Future _futureBuilderFuture;
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_futureBuilderFuture = _messageReplyCtr.queryMessageReply();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
_messageReplyCtr.queryMessageReply(type: 'onLoad');
});
}
},
);
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('回复我的'),
),
body: RefreshIndicator(
onRefresh: () async {
await _messageReplyCtr.queryMessageReply(type: 'init');
},
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
final replyItems = _messageReplyCtr.replyItems;
return Obx(
() => ListView.separated(
controller: scrollController,
itemBuilder: (context, index) =>
ReplyItem(item: replyItems[index]),
itemCount: replyItems.length,
separatorBuilder: (BuildContext context, int index) {
return Divider(
indent: 66,
endIndent: 14,
height: 1,
color: Colors.grey.withOpacity(0.1),
);
},
),
);
} else {
// 请求错误
return CustomScrollView(
slivers: [
HttpError(
errMsg: snapshot.data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_messageReplyCtr.queryMessageReply();
});
},
)
],
);
}
} else {
return const SizedBox();
}
},
),
),
);
}
}
class ReplyItem extends StatelessWidget {
final MessageReplyItem item;
const ReplyItem({super.key, required this.item});
@override
Widget build(BuildContext context) {
final String heroTag = Utils.makeHeroTag(item.user!.mid);
final String bvid = item.item!.uri!.split('/').last;
// 页码
final String page =
item.item!.nativeUri!.split('page=').last.split('&').first;
// 根评论id
final String commentRootId =
item.item!.nativeUri!.split('comment_root_id=').last.split('&').first;
// 二级评论id
final String commentSecondaryId =
item.item!.nativeUri!.split('comment_secondary_id=').last;
return InkWell(
onTap: () async {
final int cid = await SearchHttp.ab2c(bvid: bvid);
final String heroTag = Utils.makeHeroTag(bvid);
Get.toNamed<dynamic>(
'/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'pic': '',
'heroTag': heroTag,
},
);
},
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
Get.toNamed('/member?mid=${item.user!.mid}',
arguments: {'face': item.user!.avatar, 'heroTag': heroTag});
},
child: Hero(
tag: heroTag,
child: NetworkImgLayer(
width: 42,
height: 42,
type: 'avatar',
src: item.user!.avatar,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(TextSpan(children: [
TextSpan(text: item.user!.nickname!),
const TextSpan(text: ' '),
if (item.item!.type! == 'video')
TextSpan(
text: '对我的视频发表了评论',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
),
if (item.item!.type! == 'reply')
TextSpan(
text: '回复了我的评论',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
),
])),
const SizedBox(height: 6),
Text.rich(
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(letterSpacing: 0.3),
buildContent(context, item.item)),
if (item.item!.targetReplyContent != '') ...[
const SizedBox(height: 2),
Text(
item.item!.targetReplyContent!,
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
),
],
const SizedBox(height: 4),
Row(
children: [
Text(
Utils.dateFormat(item.replyTime!, formatType: 'detail'),
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
),
const SizedBox(width: 16),
Text(
'回复',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
),
],
)
],
),
),
// Spacer(),
const SizedBox(width: 25),
if (item.item!.type! == 'reply')
Container(
width: 60,
height: 80,
padding: const EdgeInsets.all(4),
child: Text(
item.item!.rootReplyContent!,
maxLines: 4,
style: const TextStyle(
fontSize: 12,
letterSpacing: 0.3,
),
overflow: TextOverflow.ellipsis,
),
),
if (item.item!.type! == 'video')
NetworkImgLayer(
width: 60,
height: 60,
src: item.item!.image,
),
],
),
),
);
}
}
InlineSpan buildContent(BuildContext context, item) {
List? atDetails = item!.atDetails;
final List<InlineSpan> spanChilds = <InlineSpan>[];
if (atDetails!.isNotEmpty) {
final String patternStr =
atDetails.map<String>((e) => '@${e['nickname']}').toList().join('|');
final RegExp regExp = RegExp(patternStr);
item.sourceContent!.splitMapJoin(
regExp,
onMatch: (Match match) {
spanChilds.add(
TextSpan(
text: match.group(0),
recognizer: TapGestureRecognizer()
..onTap = () {
var currentUser = atDetails
.where((e) => e['nickname'] == match.group(0)!.substring(1))
.first;
Get.toNamed('/member?mid=${currentUser['mid']}', arguments: {
'face': currentUser['avatar'],
});
},
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
);
return '';
},
onNonMatch: (String nonMatch) {
spanChilds.add(
TextSpan(text: nonMatch),
);
return '';
},
);
} else {
spanChilds.add(
TextSpan(text: item.sourceContent),
);
}
return TextSpan(children: spanChilds);
}

View File

@ -12,7 +12,7 @@ class WhisperController extends GetxController {
{
'icon': Icons.message_outlined,
'title': '回复我的',
'path': '',
'path': '/messageReply',
'count': 0,
},
{

View File

@ -69,7 +69,7 @@ class _WhisperPageState extends State<WhisperPage> {
children: [
..._whisperController.noticesList.map((element) {
return InkWell(
onTap: () => {},
onTap: () => Get.toNamed(element['path']),
onLongPress: () {},
borderRadius: StyleString.mdRadius,
child: Column(

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/pages/follow_search/view.dart';
import 'package:pilipala/pages/message/reply/index.dart';
import 'package:pilipala/pages/setting/pages/logs.dart';
import '../pages/about/index.dart';
@ -178,6 +179,8 @@ class Routes {
// 操作菜单
CustomGetPage(
name: '/actionMenuSet', page: () => const ActionMenuSetPage()),
// 回复我的
CustomGetPage(name: '/messageReply', page: () => const MessageReplyPage()),
];
}