Merge branch 'feature-sendMsg'
This commit is contained in:
@ -535,4 +535,17 @@ class Api {
|
||||
|
||||
/// 搜索结果计数
|
||||
static const String searchCount = '/x/web-interface/wbi/search/all/v2';
|
||||
|
||||
/// 关闭会话
|
||||
static const String removeSession =
|
||||
'${HttpString.tUrl}/session_svr/v1/session_svr/remove_session';
|
||||
|
||||
/// 消息未读数
|
||||
static const String unread = '${HttpString.tUrl}/x/im/web/msgfeed/unread';
|
||||
|
||||
/// 回复我的
|
||||
static const String messageReplyAPi = '/x/msgfeed/reply';
|
||||
|
||||
/// 收到的赞
|
||||
static const String messageLikeAPi = '/x/msgfeed/like';
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:pilipala/models/msg/like.dart';
|
||||
import 'package:pilipala/models/msg/reply.dart';
|
||||
import '../models/msg/account.dart';
|
||||
import '../models/msg/session.dart';
|
||||
import '../utils/wbi_sign.dart';
|
||||
@ -122,68 +126,48 @@ class MsgHttp {
|
||||
'data': res.data['data'],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'date': [],
|
||||
'msg': "message: ${res.data['message']},"
|
||||
" msg: ${res.data['msg']},"
|
||||
" code: ${res.data['code']}",
|
||||
};
|
||||
return {'status': false, 'date': [], 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
|
||||
// 发送私信
|
||||
static Future sendMsg({
|
||||
int? senderUid,
|
||||
int? receiverId,
|
||||
required int senderUid,
|
||||
required int receiverId,
|
||||
int? receiverType,
|
||||
int? msgType,
|
||||
dynamic content,
|
||||
}) async {
|
||||
String csrf = await Request.getCsrf();
|
||||
Map<String, dynamic> params = await WbiSign().makSign({
|
||||
'msg[sender_uid]': senderUid,
|
||||
'msg[receiver_id]': receiverId,
|
||||
'msg[receiver_type]': receiverType ?? 1,
|
||||
'msg[msg_type]': msgType ?? 1,
|
||||
'msg[msg_status]': 0,
|
||||
'msg[dev_id]': getDevId(),
|
||||
'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
'msg[new_face_version]': 0,
|
||||
'msg[content]': content,
|
||||
'from_firework': 0,
|
||||
'build': 0,
|
||||
'mobi_app': 'web',
|
||||
'csrf_token': csrf,
|
||||
'csrf': csrf,
|
||||
});
|
||||
var res =
|
||||
await Request().post(Api.sendMsg, queryParameters: <String, dynamic>{
|
||||
...params,
|
||||
'csrf_token': csrf,
|
||||
'csrf': csrf,
|
||||
}, data: {
|
||||
'w_sender_uid': params['msg[sender_uid]'],
|
||||
'w_receiver_id': params['msg[receiver_id]'],
|
||||
'w_dev_id': params['msg[dev_id]'],
|
||||
'w_rid': params['w_rid'],
|
||||
'wts': params['wts'],
|
||||
'csrf_token': csrf,
|
||||
'csrf': csrf,
|
||||
});
|
||||
var res = await Request().post(
|
||||
Api.sendMsg,
|
||||
data: {
|
||||
'msg[sender_uid]': senderUid,
|
||||
'msg[receiver_id]': receiverId,
|
||||
'msg[receiver_type]': 1,
|
||||
'msg[msg_type]': 1,
|
||||
'msg[msg_status]': 0,
|
||||
'msg[content]': jsonEncode(content),
|
||||
'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
'msg[new_face_version]': 0,
|
||||
'msg[dev_id]': getDevId(),
|
||||
'from_firework': 0,
|
||||
'build': 0,
|
||||
'mobi_app': 'web',
|
||||
'csrf_token': csrf,
|
||||
'csrf': csrf,
|
||||
},
|
||||
options: Options(
|
||||
contentType: Headers.formUrlEncodedContentType,
|
||||
),
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': res.data['data'],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'date': [],
|
||||
'msg': "message: ${res.data['message']},"
|
||||
" msg: ${res.data['msg']},"
|
||||
" code: ${res.data['code']}",
|
||||
};
|
||||
return {'status': false, 'date': [], 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,4 +204,87 @@ class MsgHttp {
|
||||
}
|
||||
return s.join();
|
||||
}
|
||||
|
||||
static Future removeSession({
|
||||
int? talkerId,
|
||||
}) async {
|
||||
String csrf = await Request.getCsrf();
|
||||
Map params = await WbiSign().makSign({
|
||||
'talker_id': talkerId,
|
||||
'session_type': 1,
|
||||
'build': 0,
|
||||
'mobi_app': 'web',
|
||||
'csrf_token': csrf,
|
||||
'csrf': csrf
|
||||
});
|
||||
var res = await Request().get(Api.removeSession, data: params);
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': res.data['data'],
|
||||
};
|
||||
} else {
|
||||
return {'status': false, 'date': [], 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
|
||||
static Future unread() async {
|
||||
var res = await Request().get(Api.unread);
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': res.data['data'],
|
||||
};
|
||||
} else {
|
||||
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) {
|
||||
return {'status': false, 'date': [], 'msg': err.toString()};
|
||||
}
|
||||
} else {
|
||||
return {'status': false, 'date': [], 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
|
||||
// 收到的赞
|
||||
static Future messageLike({
|
||||
int? id,
|
||||
int? likeTime,
|
||||
}) async {
|
||||
var params = {
|
||||
if (id != null) 'id': id,
|
||||
if (likeTime != null) 'like_time': likeTime,
|
||||
};
|
||||
var res = await Request().get(Api.messageLikeAPi, data: params);
|
||||
if (res.data['code'] == 0) {
|
||||
try {
|
||||
return {
|
||||
'status': true,
|
||||
'data': MessageLikeModel.fromJson(res.data['data']),
|
||||
};
|
||||
} catch (err) {
|
||||
return {'status': false, 'date': [], 'msg': err.toString()};
|
||||
}
|
||||
} else {
|
||||
return {'status': false, 'date': [], 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
183
lib/models/msg/like.dart
Normal file
183
lib/models/msg/like.dart
Normal file
@ -0,0 +1,183 @@
|
||||
class MessageLikeModel {
|
||||
MessageLikeModel({
|
||||
this.latest,
|
||||
this.total,
|
||||
});
|
||||
|
||||
Latest? latest;
|
||||
Total? total;
|
||||
|
||||
factory MessageLikeModel.fromJson(Map<String, dynamic> json) =>
|
||||
MessageLikeModel(
|
||||
latest: json["latest"] == null ? null : Latest.fromJson(json["latest"]),
|
||||
total: json["total"] == null ? null : Total.fromJson(json["total"]),
|
||||
);
|
||||
}
|
||||
|
||||
class Latest {
|
||||
Latest({
|
||||
this.items,
|
||||
this.lastViewAt,
|
||||
});
|
||||
|
||||
List? items;
|
||||
int? lastViewAt;
|
||||
|
||||
factory Latest.fromJson(Map<String, dynamic> json) => Latest(
|
||||
items: json["items"],
|
||||
lastViewAt: json["last_view_at"],
|
||||
);
|
||||
}
|
||||
|
||||
class Total {
|
||||
Total({
|
||||
this.cursor,
|
||||
this.items,
|
||||
});
|
||||
|
||||
Cursor? cursor;
|
||||
List<MessageLikeItem>? items;
|
||||
|
||||
factory Total.fromJson(Map<String, dynamic> json) => Total(
|
||||
cursor: Cursor.fromJson(json['cursor']),
|
||||
items: json["items"] == null
|
||||
? []
|
||||
: json["items"].map<MessageLikeItem>((e) {
|
||||
return MessageLikeItem.fromJson(e);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
class Cursor {
|
||||
Cursor({
|
||||
this.id,
|
||||
this.isEnd,
|
||||
this.time,
|
||||
});
|
||||
|
||||
int? id;
|
||||
bool? isEnd;
|
||||
int? time;
|
||||
|
||||
factory Cursor.fromJson(Map<String, dynamic> json) => Cursor(
|
||||
id: json['id'],
|
||||
isEnd: json['is_end'],
|
||||
time: json['time'],
|
||||
);
|
||||
}
|
||||
|
||||
class MessageLikeItem {
|
||||
MessageLikeItem({
|
||||
this.id,
|
||||
this.users,
|
||||
this.item,
|
||||
this.counts,
|
||||
this.likeTime,
|
||||
this.noticeState,
|
||||
this.isExpand = false,
|
||||
});
|
||||
|
||||
int? id;
|
||||
List<MessageLikeUser>? users;
|
||||
MessageLikeItemItem? item;
|
||||
int? counts;
|
||||
int? likeTime;
|
||||
int? noticeState;
|
||||
bool isExpand;
|
||||
|
||||
factory MessageLikeItem.fromJson(Map<String, dynamic> json) =>
|
||||
MessageLikeItem(
|
||||
id: json["id"],
|
||||
users: json["users"] == null
|
||||
? []
|
||||
: json["users"].map<MessageLikeUser>((e) {
|
||||
return MessageLikeUser.fromJson(e);
|
||||
}).toList(),
|
||||
item: json["item"] == null
|
||||
? null
|
||||
: MessageLikeItemItem.fromJson(json["item"]),
|
||||
counts: json["counts"],
|
||||
likeTime: json["like_time"],
|
||||
noticeState: json["notice_state"],
|
||||
);
|
||||
}
|
||||
|
||||
class MessageLikeUser {
|
||||
MessageLikeUser({
|
||||
this.mid,
|
||||
this.fans,
|
||||
this.nickname,
|
||||
this.avatar,
|
||||
this.midLink,
|
||||
this.follow,
|
||||
});
|
||||
|
||||
int? mid;
|
||||
int? fans;
|
||||
String? nickname;
|
||||
String? avatar;
|
||||
String? midLink;
|
||||
bool? follow;
|
||||
|
||||
factory MessageLikeUser.fromJson(Map<String, dynamic> json) =>
|
||||
MessageLikeUser(
|
||||
mid: json["mid"],
|
||||
fans: json["fans"],
|
||||
nickname: json["nickname"],
|
||||
avatar: json["avatar"],
|
||||
midLink: json["mid_link"],
|
||||
follow: json["follow"],
|
||||
);
|
||||
}
|
||||
|
||||
class MessageLikeItemItem {
|
||||
MessageLikeItemItem({
|
||||
this.itemId,
|
||||
this.pid,
|
||||
this.type,
|
||||
this.business,
|
||||
this.businessId,
|
||||
this.replyBusinessId,
|
||||
this.likeBusinessId,
|
||||
this.title,
|
||||
this.desc,
|
||||
this.image,
|
||||
this.uri,
|
||||
this.detailName,
|
||||
this.nativeUri,
|
||||
this.ctime,
|
||||
});
|
||||
|
||||
int? itemId;
|
||||
int? pid;
|
||||
String? type;
|
||||
String? business;
|
||||
int? businessId;
|
||||
int? replyBusinessId;
|
||||
int? likeBusinessId;
|
||||
String? title;
|
||||
String? desc;
|
||||
String? image;
|
||||
String? uri;
|
||||
String? detailName;
|
||||
String? nativeUri;
|
||||
int? ctime;
|
||||
|
||||
factory MessageLikeItemItem.fromJson(Map<String, dynamic> json) =>
|
||||
MessageLikeItemItem(
|
||||
itemId: json["item_id"],
|
||||
pid: json["pid"],
|
||||
type: json["type"],
|
||||
business: json["business"],
|
||||
businessId: json["business_id"],
|
||||
replyBusinessId: json["reply_business_id"],
|
||||
likeBusinessId: json["like_business_id"],
|
||||
title: json["title"],
|
||||
desc: json["desc"],
|
||||
image: json["image"],
|
||||
uri: json["uri"],
|
||||
detailName: json["detail_name"],
|
||||
nativeUri: json["native_uri"],
|
||||
ctime: json["ctime"],
|
||||
);
|
||||
}
|
168
lib/models/msg/reply.dart
Normal file
168
lib/models/msg/reply.dart
Normal 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'];
|
||||
}
|
||||
}
|
@ -208,7 +208,17 @@ class ProfilePanel extends StatelessWidget {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
Get.toNamed(
|
||||
'/whisperDetail',
|
||||
parameters: {
|
||||
'name': memberInfo.name!,
|
||||
'face': memberInfo.face!,
|
||||
'mid': memberInfo.mid.toString(),
|
||||
'heroTag': ctr.heroTag!,
|
||||
},
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
|
3
lib/pages/message/at/controller.dart
Normal file
3
lib/pages/message/at/controller.dart
Normal file
@ -0,0 +1,3 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MessageAtController extends GetxController {}
|
4
lib/pages/message/at/index.dart
Normal file
4
lib/pages/message/at/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library message_at;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
19
lib/pages/message/at/view.dart
Normal file
19
lib/pages/message/at/view.dart
Normal file
@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageAtPage extends StatefulWidget {
|
||||
const MessageAtPage({super.key});
|
||||
|
||||
@override
|
||||
State<MessageAtPage> createState() => _MessageAtPageState();
|
||||
}
|
||||
|
||||
class _MessageAtPageState extends State<MessageAtPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('@我的'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
30
lib/pages/message/like/controller.dart
Normal file
30
lib/pages/message/like/controller.dart
Normal file
@ -0,0 +1,30 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/msg.dart';
|
||||
import 'package:pilipala/models/msg/like.dart';
|
||||
|
||||
class MessageLikeController extends GetxController {
|
||||
Cursor? cursor;
|
||||
RxList<MessageLikeItem> likeItems = <MessageLikeItem>[].obs;
|
||||
|
||||
Future queryMessageLike({String type = 'init'}) async {
|
||||
if (cursor != null && cursor!.isEnd == true) {
|
||||
return {};
|
||||
}
|
||||
var params = {
|
||||
if (type == 'onLoad') 'id': cursor!.id,
|
||||
if (type == 'onLoad') 'likeTime': cursor!.time,
|
||||
};
|
||||
var res = await MsgHttp.messageLike(
|
||||
id: params['id'], likeTime: params['likeTime']);
|
||||
if (res['status']) {
|
||||
cursor = res['data'].total.cursor;
|
||||
likeItems.addAll(res['data'].total.items);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Future expandedUsersAvatar(i) async {
|
||||
likeItems[i].isExpand = !likeItems[i].isExpand;
|
||||
likeItems.refresh();
|
||||
}
|
||||
}
|
4
lib/pages/message/like/index.dart
Normal file
4
lib/pages/message/like/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library message_like;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
319
lib/pages/message/like/view.dart
Normal file
319
lib/pages/message/like/view.dart
Normal file
@ -0,0 +1,319 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.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/like.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
class MessageLikePage extends StatefulWidget {
|
||||
const MessageLikePage({super.key});
|
||||
|
||||
@override
|
||||
State<MessageLikePage> createState() => _MessageLikePageState();
|
||||
}
|
||||
|
||||
class _MessageLikePageState extends State<MessageLikePage> {
|
||||
final MessageLikeController _messageLikeCtr =
|
||||
Get.put(MessageLikeController());
|
||||
late Future _futureBuilderFuture;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureBuilderFuture = _messageLikeCtr.queryMessageLike();
|
||||
scrollController.addListener(
|
||||
() async {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
|
||||
_messageLikeCtr.queryMessageLike(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 _messageLikeCtr.queryMessageLike(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 likeItems = _messageLikeCtr.likeItems;
|
||||
return Obx(
|
||||
() => ListView.separated(
|
||||
controller: scrollController,
|
||||
itemBuilder: (context, index) => LikeItem(
|
||||
item: likeItems[index],
|
||||
index: index,
|
||||
messageLikeCtr: _messageLikeCtr,
|
||||
),
|
||||
itemCount: likeItems.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 =
|
||||
_messageLikeCtr.queryMessageLike();
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LikeItem extends StatelessWidget {
|
||||
final MessageLikeItem item;
|
||||
final int index;
|
||||
final MessageLikeController messageLikeCtr;
|
||||
|
||||
const LikeItem(
|
||||
{super.key,
|
||||
required this.item,
|
||||
required this.index,
|
||||
required this.messageLikeCtr});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color outline = Theme.of(context).colorScheme.outline;
|
||||
final nickNameList = item.users!.map((e) => e.nickname).take(2).toList();
|
||||
int usersLen = item.users!.length > 3 ? 3 : item.users!.length;
|
||||
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 {
|
||||
try {
|
||||
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,
|
||||
},
|
||||
);
|
||||
} catch (_) {
|
||||
SmartDialog.showToast('视频可能失效了');
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
if (usersLen == 1) {
|
||||
final String heroTag =
|
||||
Utils.makeHeroTag(item.users!.first.mid);
|
||||
Get.toNamed('/member?mid=${item.users!.first.mid}',
|
||||
arguments: {
|
||||
'face': item.users!.first.avatar,
|
||||
'heroTag': heroTag
|
||||
});
|
||||
} else {
|
||||
messageLikeCtr.expandedUsersAvatar(index);
|
||||
}
|
||||
},
|
||||
// 多个头像层叠
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: Stack(
|
||||
children: [
|
||||
for (var i = 0; i < usersLen; i++)
|
||||
Positioned(
|
||||
top: i % 2 * (50 / (usersLen >= 2 ? 2 : 1)),
|
||||
left: i / 2 * (50 / (usersLen >= 2 ? 2 : 1)),
|
||||
child: NetworkImgLayer(
|
||||
width: 50 / (usersLen >= 2 ? 2 : 1),
|
||||
height: 50 / (usersLen >= 2 ? 2 : 1),
|
||||
type: 'avatar',
|
||||
src: item.users![i].avatar,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(TextSpan(children: [
|
||||
TextSpan(text: nickNameList.join('、')),
|
||||
const TextSpan(text: ' '),
|
||||
if (item.users!.length > 1)
|
||||
TextSpan(
|
||||
text: '等总计${item.users!.length}人',
|
||||
style: TextStyle(color: outline),
|
||||
),
|
||||
TextSpan(
|
||||
text: '赞了我的评论',
|
||||
style: TextStyle(color: outline),
|
||||
),
|
||||
])),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
Utils.dateFormat(item.likeTime!, formatType: 'detail'),
|
||||
style: TextStyle(color: outline),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 25),
|
||||
if (item.item!.type! == 'reply')
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Text(
|
||||
item.item!.title!,
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: item.isExpand ? Get.size.width - 74 : 0,
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ListView.builder(
|
||||
itemCount: item.users!.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(i == 0 ? 12 : 4, 8, 4, 0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final String heroTag =
|
||||
Utils.makeHeroTag(item.users![i].mid);
|
||||
Get.toNamed(
|
||||
'/member?mid=${item.users![i].mid}',
|
||||
arguments: {
|
||||
'face': item.users![i].avatar,
|
||||
'heroTag': heroTag
|
||||
},
|
||||
);
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
width: 42,
|
||||
height: 42,
|
||||
type: 'avatar',
|
||||
src: item.users![i].avatar,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
SizedBox(
|
||||
width: 68,
|
||||
child: Text(
|
||||
textAlign: TextAlign.center,
|
||||
item.users![i].nickname!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(color: outline),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
messageLikeCtr.expandedUsersAvatar(index);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: item.isExpand ? 74 : 0,
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
25
lib/pages/message/reply/controller.dart
Normal file
25
lib/pages/message/reply/controller.dart
Normal 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;
|
||||
}
|
||||
}
|
4
lib/pages/message/reply/index.dart
Normal file
4
lib/pages/message/reply/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library message_reply;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
272
lib/pages/message/reply/view.dart
Normal file
272
lib/pages/message/reply/view.dart
Normal file
@ -0,0 +1,272 @@
|
||||
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) {
|
||||
Color outline = Theme.of(context).colorScheme.outline;
|
||||
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: outline)),
|
||||
if (item.item!.type! == 'reply')
|
||||
TextSpan(
|
||||
text: '回复了我的评论',
|
||||
style: TextStyle(color: 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: outline),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
Utils.dateFormat(item.replyTime!, formatType: 'detail'),
|
||||
style: TextStyle(color: outline),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text('回复', style: TextStyle(color: outline)),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
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);
|
||||
}
|
3
lib/pages/message/system/controller.dart
Normal file
3
lib/pages/message/system/controller.dart
Normal file
@ -0,0 +1,3 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MessageSystemController extends GetxController {}
|
4
lib/pages/message/system/index.dart
Normal file
4
lib/pages/message/system/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library message_system;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
19
lib/pages/message/system/view.dart
Normal file
19
lib/pages/message/system/view.dart
Normal file
@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageSystemPage extends StatefulWidget {
|
||||
const MessageSystemPage({super.key});
|
||||
|
||||
@override
|
||||
State<MessageSystemPage> createState() => _MessageSystemPageState();
|
||||
}
|
||||
|
||||
class _MessageSystemPageState extends State<MessageSystemPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('系统通知'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/msg.dart';
|
||||
import 'package:pilipala/models/msg/account.dart';
|
||||
@ -7,6 +8,38 @@ class WhisperController extends GetxController {
|
||||
RxList<SessionList> sessionList = <SessionList>[].obs;
|
||||
RxList<AccountListModel> accountList = <AccountListModel>[].obs;
|
||||
bool isLoading = false;
|
||||
RxList noticesList = [
|
||||
{
|
||||
'icon': Icons.message_outlined,
|
||||
'title': '回复我的',
|
||||
'path': '/messageReply',
|
||||
'count': 0,
|
||||
},
|
||||
{
|
||||
'icon': Icons.alternate_email,
|
||||
'title': '@ 我的',
|
||||
'path': '/messageAt',
|
||||
'count': 0,
|
||||
},
|
||||
{
|
||||
'icon': Icons.thumb_up_outlined,
|
||||
'title': '收到的赞',
|
||||
'path': '/messageLike',
|
||||
'count': 0,
|
||||
},
|
||||
{
|
||||
'icon': Icons.notifications_none_outlined,
|
||||
'title': '系统通知',
|
||||
'path': '/messageSystem',
|
||||
'count': 0,
|
||||
}
|
||||
].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
unread();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
Future querySessionList(String? type) async {
|
||||
if (isLoading) return;
|
||||
@ -62,4 +95,31 @@ class WhisperController extends GetxController {
|
||||
Future onRefresh() async {
|
||||
querySessionList('onRefresh');
|
||||
}
|
||||
|
||||
void refreshLastMsg(int talkerId, String content) {
|
||||
final SessionList currentItem =
|
||||
sessionList.where((p0) => p0.talkerId == talkerId).first;
|
||||
currentItem.lastMsg!.content['content'] = content;
|
||||
sessionList.removeWhere((p0) => p0.talkerId == talkerId);
|
||||
sessionList.insert(0, currentItem);
|
||||
sessionList.refresh();
|
||||
}
|
||||
|
||||
// 移除会话
|
||||
void removeSessionMsg(int talkerId) {
|
||||
sessionList.removeWhere((p0) => p0.talkerId == talkerId);
|
||||
sessionList.refresh();
|
||||
}
|
||||
|
||||
// 消息未读数
|
||||
void unread() async {
|
||||
var res = await MsgHttp.unread();
|
||||
if (res['status']) {
|
||||
noticesList[0]['count'] = res['data']['reply'];
|
||||
noticesList[1]['count'] = res['data']['at'];
|
||||
noticesList[2]['count'] = res['data']['like'];
|
||||
noticesList[3]['count'] = res['data']['sys_msg'];
|
||||
noticesList.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/skeleton/skeleton.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
@ -44,147 +45,145 @@ class _WhisperPageState extends State<WhisperPage> {
|
||||
appBar: AppBar(
|
||||
title: const Text('消息'),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// LayoutBuilder(
|
||||
// builder: (BuildContext context, BoxConstraints constraints) {
|
||||
// // 在这里根据父级容器的约束条件构建小部件树
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.only(left: 20, right: 20),
|
||||
// child: SizedBox(
|
||||
// height: constraints.maxWidth / 5,
|
||||
// child: GridView.count(
|
||||
// primary: false,
|
||||
// crossAxisCount: 4,
|
||||
// padding: const EdgeInsets.all(0),
|
||||
// childAspectRatio: 1.25,
|
||||
// children: [
|
||||
// Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// width: 36,
|
||||
// height: 36,
|
||||
// child: IconButton(
|
||||
// style: ButtonStyle(
|
||||
// padding:
|
||||
// MaterialStateProperty.all(EdgeInsets.zero),
|
||||
// backgroundColor:
|
||||
// MaterialStateProperty.resolveWith((states) {
|
||||
// return Theme.of(context)
|
||||
// .colorScheme
|
||||
// .primary
|
||||
// .withOpacity(0.1);
|
||||
// }),
|
||||
// ),
|
||||
// onPressed: () {},
|
||||
// icon: Icon(
|
||||
// Icons.message_outlined,
|
||||
// size: 18,
|
||||
// color: Theme.of(context).colorScheme.primary,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 6),
|
||||
// const Text('回复我的', style: TextStyle(fontSize: 13))
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await _whisperController.onRefresh();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map? data = snapshot.data;
|
||||
if (data != null && data['status']) {
|
||||
RxList sessionList = _whisperController.sessionList;
|
||||
return Obx(
|
||||
() => sessionList.isEmpty
|
||||
? const SizedBox()
|
||||
: ListView.separated(
|
||||
itemCount: sessionList.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (_, int i) {
|
||||
return SessionItem(
|
||||
sessionItem: sessionList[i],
|
||||
changeFucCall: () =>
|
||||
sessionList.refresh(),
|
||||
);
|
||||
},
|
||||
separatorBuilder:
|
||||
(BuildContext context, int index) {
|
||||
return Divider(
|
||||
indent: 72,
|
||||
endIndent: 20,
|
||||
height: 6,
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
);
|
||||
},
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
_whisperController.unread();
|
||||
await _whisperController.onRefresh();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
child: Column(
|
||||
children: [
|
||||
LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
// 在这里根据父级容器的约束条件构建小部件树
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 20, right: 20),
|
||||
child: SizedBox(
|
||||
height: constraints.maxWidth / 4,
|
||||
child: Obx(
|
||||
() => GridView.count(
|
||||
primary: false,
|
||||
crossAxisCount: 4,
|
||||
padding: const EdgeInsets.all(0),
|
||||
children: [
|
||||
..._whisperController.noticesList.map((element) {
|
||||
return InkWell(
|
||||
onTap: () => Get.toNamed(element['path']),
|
||||
onLongPress: () {},
|
||||
borderRadius: StyleString.mdRadius,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Badge(
|
||||
isLabelVisible: element['count'] > 0,
|
||||
label: Text(element['count'] > 99
|
||||
? '99+'
|
||||
: element['count'].toString()),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Icon(
|
||||
element['icon'],
|
||||
size: 21,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(element['title'])
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return Center(
|
||||
child: Text(data?['msg'] ?? '请求异常'),
|
||||
);
|
||||
}
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map? data = snapshot.data;
|
||||
if (data != null && data['status']) {
|
||||
RxList sessionList = _whisperController.sessionList;
|
||||
return Obx(
|
||||
() => sessionList.isEmpty
|
||||
? const SizedBox()
|
||||
: ListView.separated(
|
||||
itemCount: sessionList.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (_, int i) {
|
||||
return SessionItem(
|
||||
sessionItem: sessionList[i],
|
||||
changeFucCall: () => sessionList.refresh(),
|
||||
);
|
||||
},
|
||||
separatorBuilder:
|
||||
(BuildContext context, int index) {
|
||||
return Divider(
|
||||
indent: 72,
|
||||
endIndent: 20,
|
||||
height: 6,
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 骨架屏
|
||||
return ListView.builder(
|
||||
itemCount: 15,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, int i) {
|
||||
return Skeleton(
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
),
|
||||
title: Container(
|
||||
width: 100,
|
||||
height: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
),
|
||||
subtitle: Container(
|
||||
width: 80,
|
||||
height: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
// 请求错误
|
||||
return Center(
|
||||
child: Text(data?['msg'] ?? '请求异常'),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
} else {
|
||||
// 骨架屏
|
||||
return ListView.builder(
|
||||
itemCount: 15,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, int i) {
|
||||
return Skeleton(
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
),
|
||||
title: Container(
|
||||
width: 100,
|
||||
height: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
),
|
||||
subtitle: Container(
|
||||
width: 80,
|
||||
height: 14,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -202,7 +201,10 @@ class SessionItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String heroTag = Utils.makeHeroTag(sessionItem.accountInfo.mid);
|
||||
final content = sessionItem.lastMsg.content;
|
||||
final msgStatus = sessionItem.lastMsg.msgStatus;
|
||||
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
sessionItem.unreadCount = 0;
|
||||
@ -214,6 +216,7 @@ class SessionItem extends StatelessWidget {
|
||||
'name': sessionItem.accountInfo.name,
|
||||
'face': sessionItem.accountInfo.face,
|
||||
'mid': sessionItem.accountInfo.mid.toString(),
|
||||
'heroTag': heroTag,
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -221,22 +224,27 @@ class SessionItem extends StatelessWidget {
|
||||
isLabelVisible: sessionItem.unreadCount > 0,
|
||||
label: Text(sessionItem.unreadCount.toString()),
|
||||
alignment: Alignment.topRight,
|
||||
child: NetworkImgLayer(
|
||||
width: 45,
|
||||
height: 45,
|
||||
type: 'avatar',
|
||||
src: sessionItem.accountInfo.face,
|
||||
child: Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
width: 45,
|
||||
height: 45,
|
||||
type: 'avatar',
|
||||
src: sessionItem.accountInfo.face,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(sessionItem.accountInfo.name),
|
||||
subtitle: Text(
|
||||
content != null && content != ''
|
||||
? (content['text'] ??
|
||||
content['content'] ??
|
||||
content['title'] ??
|
||||
content['reply_content'] ??
|
||||
'不支持的消息类型')
|
||||
: '不支持的消息类型',
|
||||
msgStatus == 1
|
||||
? '你撤回了一条消息'
|
||||
: content != null && content != ''
|
||||
? (content['text'] ??
|
||||
content['content'] ??
|
||||
content['title'] ??
|
||||
content['reply_content'] ??
|
||||
'不支持的消息类型')
|
||||
: '不支持的消息类型',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
@ -245,10 +253,10 @@ class SessionItem extends StatelessWidget {
|
||||
.copyWith(color: Theme.of(context).colorScheme.outline)),
|
||||
trailing: Text(
|
||||
Utils.dateFormat(sessionItem.lastMsg.timestamp),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.copyWith(color: Theme.of(context).colorScheme.outline),
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,30 +1,40 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/msg.dart';
|
||||
import 'package:pilipala/models/msg/session.dart';
|
||||
import 'package:pilipala/pages/whisper/index.dart';
|
||||
import '../../utils/feed_back.dart';
|
||||
import '../../utils/storage.dart';
|
||||
|
||||
class WhisperDetailController extends GetxController {
|
||||
late int talkerId;
|
||||
int? talkerId;
|
||||
late String name;
|
||||
late String face;
|
||||
late String mid;
|
||||
late String heroTag;
|
||||
RxList<MessageItem> messageList = <MessageItem>[].obs;
|
||||
//表情转换图片规则
|
||||
List<dynamic>? eInfos;
|
||||
RxList<dynamic> eInfos = [].obs;
|
||||
final TextEditingController replyContentController = TextEditingController();
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
List emoteList = [];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
talkerId = int.parse(Get.parameters['talkerId']!);
|
||||
if (Get.parameters.containsKey('talkerId')) {
|
||||
talkerId = int.parse(Get.parameters['talkerId']!);
|
||||
} else {
|
||||
talkerId = int.parse(Get.parameters['mid']!);
|
||||
}
|
||||
name = Get.parameters['name']!;
|
||||
face = Get.parameters['face']!;
|
||||
mid = Get.parameters['mid']!;
|
||||
heroTag = Get.parameters['heroTag']!;
|
||||
}
|
||||
|
||||
Future querySessionMsg() async {
|
||||
@ -34,7 +44,7 @@ class WhisperDetailController extends GetxController {
|
||||
if (messageList.isNotEmpty) {
|
||||
ackSessionMsg();
|
||||
if (res['data'].eInfos != null) {
|
||||
eInfos = res['data'].eInfos;
|
||||
eInfos.value = res['data'].eInfos;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -73,9 +83,64 @@ class WhisperDetailController extends GetxController {
|
||||
msgType: 1,
|
||||
);
|
||||
if (result['status']) {
|
||||
SmartDialog.showToast('发送成功');
|
||||
String content = jsonDecode(result['data']['msg_content'])['content'];
|
||||
messageList.insert(
|
||||
0,
|
||||
MessageItem(
|
||||
msgSeqno: result['data']['msg_key'],
|
||||
senderUid: userInfo.mid,
|
||||
receiverId: int.parse(mid),
|
||||
content: {'content': content},
|
||||
msgType: 1,
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
eInfos.addAll(emoteList);
|
||||
replyContentController.clear();
|
||||
try {
|
||||
late final WhisperController whisperController =
|
||||
Get.find<WhisperController>();
|
||||
whisperController.refreshLastMsg(talkerId!, message);
|
||||
} catch (_) {}
|
||||
} else {
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
void removeSession(context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
title: const Text('提示'),
|
||||
content: const Text('确认清空会话内容并移除会话?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Get.back,
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
var res = await MsgHttp.removeSession(talkerId: talkerId);
|
||||
if (res['status']) {
|
||||
SmartDialog.showToast('操作成功');
|
||||
try {
|
||||
late final WhisperController whisperController =
|
||||
Get.find<WhisperController>();
|
||||
whisperController.removeSessionMsg(talkerId!);
|
||||
Get.back();
|
||||
} catch (_) {}
|
||||
}
|
||||
},
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/video/reply/emote.dart';
|
||||
import 'package:pilipala/pages/emote/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply_new/toolbar_icon_button.dart';
|
||||
import 'package:pilipala/pages/whisper_detail/controller.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import '../../utils/storage.dart';
|
||||
@ -24,9 +27,9 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
|
||||
late TextEditingController _replyContentController;
|
||||
final FocusNode replyContentFocusNode = FocusNode();
|
||||
final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间
|
||||
late double emoteHeight = 0.0;
|
||||
late double emoteHeight = 230.0;
|
||||
double keyboardHeight = 0.0; // 键盘高度
|
||||
String toolbarType = 'input';
|
||||
RxString toolbarType = ''.obs;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
|
||||
@override
|
||||
@ -41,9 +44,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
|
||||
_focuslistener() {
|
||||
replyContentFocusNode.addListener(() {
|
||||
if (replyContentFocusNode.hasFocus) {
|
||||
setState(() {
|
||||
toolbarType = 'input';
|
||||
});
|
||||
toolbarType.value = 'input';
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -52,7 +53,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
|
||||
void didChangeMetrics() {
|
||||
super.didChangeMetrics();
|
||||
final String routePath = Get.currentRoute;
|
||||
if (mounted && routePath.startsWith('/whisper_detail')) {
|
||||
if (mounted && routePath.startsWith('/whisperDetail')) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// 键盘高度
|
||||
final viewInsets = EdgeInsets.fromViewPadding(
|
||||
@ -61,8 +62,11 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
|
||||
if (mounted) {
|
||||
if (keyboardHeight == 0) {
|
||||
setState(() {
|
||||
emoteHeight = keyboardHeight =
|
||||
keyboardHeight =
|
||||
keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight;
|
||||
if (keyboardHeight != 0) {
|
||||
emoteHeight = keyboardHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -79,6 +83,23 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onChooseEmote(PackageItem package, Emote emote) {
|
||||
_whisperDetailController.emoteList.add(
|
||||
{'text': emote.text, 'url': emote.url},
|
||||
);
|
||||
final int cursorPosition =
|
||||
max(_replyContentController.selection.baseOffset, 0);
|
||||
final String currentText = _replyContentController.text;
|
||||
final String newText = currentText.substring(0, cursorPosition) +
|
||||
emote.text! +
|
||||
currentText.substring(cursorPosition);
|
||||
_replyContentController.value = TextEditingValue(
|
||||
text: newText,
|
||||
selection:
|
||||
TextSelection.collapsed(offset: cursorPosition + emote.text!.length),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -88,30 +109,20 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
backgroundColor: MaterialStateProperty.resolveWith(
|
||||
(Set<MaterialState> states) {
|
||||
return Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withOpacity(0.6);
|
||||
}),
|
||||
),
|
||||
onPressed: () => Get.back(),
|
||||
icon: Icon(
|
||||
Icons.arrow_back_outlined,
|
||||
Icons.arrow_back_ios,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
feedBack();
|
||||
@ -125,13 +136,16 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
|
||||
},
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
NetworkImgLayer(
|
||||
width: 34,
|
||||
height: 34,
|
||||
type: 'avatar',
|
||||
src: _whisperDetailController.face,
|
||||
Hero(
|
||||
tag: _whisperDetailController.heroTag,
|
||||
child: NetworkImgLayer(
|
||||
width: 34,
|
||||
height: 34,
|
||||
type: 'avatar',
|
||||
src: _whisperDetailController.face,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
_whisperDetailController.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
@ -143,155 +157,171 @@ class _WhisperDetailPageState extends State<WhisperDetailPage>
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.more_vert_outlined, size: 20),
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
PopupMenuItem(
|
||||
onTap: () => _whisperDetailController.removeSession(context),
|
||||
child: const Text('关闭会话'),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 14)
|
||||
],
|
||||
),
|
||||
body: GestureDetector(
|
||||
onTap: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
setState(() {
|
||||
keyboardHeight = 0;
|
||||
});
|
||||
},
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
List messageList = _whisperDetailController.messageList;
|
||||
return Obx(
|
||||
() => messageList.isEmpty
|
||||
? const SizedBox()
|
||||
: ListView.builder(
|
||||
itemCount: messageList.length,
|
||||
shrinkWrap: true,
|
||||
reverse: true,
|
||||
itemBuilder: (_, int i) {
|
||||
if (i == 0) {
|
||||
return Column(
|
||||
children: [
|
||||
ChatItem(
|
||||
item: messageList[i],
|
||||
e_infos: _whisperDetailController.eInfos),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return ChatItem(
|
||||
item: messageList[i],
|
||||
e_infos: _whisperDetailController.eInfos);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return const SizedBox();
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
// resizeToAvoidBottomInset: true,
|
||||
bottomNavigationBar: Container(
|
||||
width: double.infinity,
|
||||
height: MediaQuery.of(context).padding.bottom + 70 + keyboardHeight,
|
||||
padding: EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 12,
|
||||
top: 12,
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 4,
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
toolbarType.value = '';
|
||||
},
|
||||
child: FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
final Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
List messageList = _whisperDetailController.messageList;
|
||||
return Obx(
|
||||
() => messageList.isEmpty
|
||||
? const SizedBox()
|
||||
: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ListView.builder(
|
||||
itemCount: messageList.length,
|
||||
shrinkWrap: true,
|
||||
reverse: true,
|
||||
itemBuilder: (_, int i) {
|
||||
if (i == 0) {
|
||||
return Column(
|
||||
children: [
|
||||
ChatItem(
|
||||
item: messageList[i],
|
||||
e_infos: _whisperDetailController
|
||||
.eInfos),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return ChatItem(
|
||||
item: messageList[i],
|
||||
e_infos:
|
||||
_whisperDetailController.eInfos);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return const SizedBox();
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: Icon(
|
||||
// Icons.add_circle_outline,
|
||||
// color: Theme.of(context).colorScheme.outline,
|
||||
// ),
|
||||
// ),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// if (toolbarType == 'input') {
|
||||
// setState(() {
|
||||
// toolbarType = 'emote';
|
||||
// });
|
||||
// }
|
||||
// FocusScope.of(context).unfocus();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.emoji_emotions_outlined,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
Obx(
|
||||
() => Container(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
8,
|
||||
12,
|
||||
12,
|
||||
toolbarType.value == ''
|
||||
? MediaQuery.of(context).padding.bottom + 6
|
||||
: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 1,
|
||||
color: Colors.grey.withOpacity(0.15),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(40.0),
|
||||
),
|
||||
child: TextField(
|
||||
readOnly: true,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
controller: _replyContentController,
|
||||
autofocus: false,
|
||||
focusNode: replyContentFocusNode,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none, // 移除默认边框
|
||||
hintText: '开发中 ...', // 提示文本
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 16.0, vertical: 12.0), // 内边距
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ToolbarIconButton(
|
||||
onPressed: () {
|
||||
if (toolbarType.value == '') {
|
||||
toolbarType.value = 'emote';
|
||||
} else if (toolbarType.value == 'input') {
|
||||
FocusScope.of(context).unfocus();
|
||||
toolbarType.value = 'emote';
|
||||
} else if (toolbarType.value == 'emote') {
|
||||
FocusScope.of(context).requestFocus();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.emoji_emotions_outlined, size: 22),
|
||||
toolbarType: toolbarType.value,
|
||||
selected: false,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline
|
||||
.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(40.0),
|
||||
),
|
||||
child: TextField(
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
controller: _replyContentController,
|
||||
autofocus: false,
|
||||
focusNode: replyContentFocusNode,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none, // 移除默认边框
|
||||
hintText: '文明发言 ~', // 提示文本
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 16.0, vertical: 12.0), // 内边距
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
// onPressed: _whisperDetailController.sendMsg,
|
||||
onPressed: null,
|
||||
icon: Icon(
|
||||
Icons.send,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
IconButton(
|
||||
onPressed: _whisperDetailController.sendMsg,
|
||||
icon: Icon(
|
||||
Icons.send,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
// const SizedBox(width: 16),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
curve: Curves.easeInOut,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
),
|
||||
Obx(
|
||||
() => AnimatedSize(
|
||||
curve: Curves.linear,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: toolbarType == 'input' ? keyboardHeight : emoteHeight,
|
||||
height: toolbarType.value == 'input'
|
||||
? keyboardHeight
|
||||
: toolbarType.value == 'emote'
|
||||
? emoteHeight
|
||||
: 0,
|
||||
child: EmotePanel(
|
||||
onChoose: (package, emote) => {},
|
||||
onChoose: (package, emote) => onChooseEmote(package, emote),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// ignore_for_file: must_be_immutable
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -69,9 +68,13 @@ class ChatItem extends StatelessWidget {
|
||||
Color textColor(BuildContext context) {
|
||||
return isOwner
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSecondaryContainer;
|
||||
: Theme.of(context).colorScheme.onBackground;
|
||||
}
|
||||
|
||||
const double safeDistanceval = 6;
|
||||
const double borderRadiusVal = 12;
|
||||
const double paddingVal = 10;
|
||||
|
||||
Widget richTextMessage(BuildContext context) {
|
||||
var text = content['content'];
|
||||
if (e_infos != null) {
|
||||
@ -386,73 +389,109 @@ class ChatItem extends StatelessWidget {
|
||||
? messageContent(context)
|
||||
: isRevoke
|
||||
? const SizedBox()
|
||||
: Row(
|
||||
children: [
|
||||
if (!isOwner) const SizedBox(width: 12),
|
||||
if (isOwner) const Spacer(),
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 300.0, // 设置最大宽度为200.0
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isOwner
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(16),
|
||||
topRight: const Radius.circular(16),
|
||||
bottomLeft: Radius.circular(isOwner ? 16 : 6),
|
||||
bottomRight: Radius.circular(isOwner ? 6 : 16),
|
||||
: Container(
|
||||
padding: const EdgeInsets.only(top: 6, bottom: 6),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
left: item.msgStatus == 1 && !isOwner
|
||||
? BorderSide(
|
||||
width: 4, color: Theme.of(context).dividerColor)
|
||||
: BorderSide.none,
|
||||
right: item.msgStatus == 1 && isOwner
|
||||
? BorderSide(
|
||||
width: 4, color: Theme.of(context).primaryColor)
|
||||
: BorderSide.none,
|
||||
)),
|
||||
child: Row(
|
||||
mainAxisAlignment: !isOwner
|
||||
? MainAxisAlignment.start
|
||||
: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(width: safeDistanceval),
|
||||
if (isOwner)
|
||||
Text(
|
||||
Utils.dateFormat(item.timestamp),
|
||||
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 300.0, // 设置最大宽度为200.0
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isOwner
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withAlpha(180)
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.outlineVariant
|
||||
.withOpacity(0.6)
|
||||
.withAlpha(125),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(borderRadiusVal),
|
||||
topRight: const Radius.circular(borderRadiusVal),
|
||||
bottomLeft:
|
||||
Radius.circular(isOwner ? borderRadiusVal : 2),
|
||||
bottomRight:
|
||||
Radius.circular(isOwner ? 2 : borderRadiusVal),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
),
|
||||
padding: const EdgeInsets.all(paddingVal),
|
||||
child: messageContent(context),
|
||||
// child: Column(
|
||||
// crossAxisAlignment: isOwner
|
||||
// ? CrossAxisAlignment.end
|
||||
// : CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// messageContent(context),
|
||||
// SizedBox(height: isPic ? 7 : 2),
|
||||
// Row(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// Text(
|
||||
// Utils.dateFormat(item.timestamp),
|
||||
// style: Theme.of(context)
|
||||
// .textTheme
|
||||
// .labelSmall!
|
||||
// .copyWith(
|
||||
// color: isOwner
|
||||
// ? Theme.of(context)
|
||||
// .colorScheme
|
||||
// .onPrimary
|
||||
// .withOpacity(0.8)
|
||||
// : Theme.of(context)
|
||||
// .colorScheme
|
||||
// .onSecondaryContainer
|
||||
// .withOpacity(0.8)),
|
||||
// ),
|
||||
// item.msgStatus == 1
|
||||
// ? Text(
|
||||
// ' 已撤回',
|
||||
// style:
|
||||
// Theme.of(context).textTheme.labelSmall!,
|
||||
// )
|
||||
// : const SizedBox()
|
||||
// ],
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
),
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
padding: EdgeInsets.only(
|
||||
top: 8,
|
||||
bottom: 6,
|
||||
left: isPic ? 8 : 12,
|
||||
right: isPic ? 8 : 12,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: isOwner
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
messageContent(context),
|
||||
SizedBox(height: isPic ? 7 : 2),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
Utils.dateFormat(item.timestamp),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.copyWith(
|
||||
color: isOwner
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary
|
||||
.withOpacity(0.8)
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer
|
||||
.withOpacity(0.8)),
|
||||
),
|
||||
item.msgStatus == 1
|
||||
? Text(
|
||||
' 已撤回',
|
||||
style:
|
||||
Theme.of(context).textTheme.labelSmall!,
|
||||
)
|
||||
: const SizedBox()
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isOwner) const Spacer(),
|
||||
if (isOwner) const SizedBox(width: 12),
|
||||
],
|
||||
if (!isOwner)
|
||||
Text(
|
||||
Utils.dateFormat(item.timestamp),
|
||||
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
const SizedBox(width: safeDistanceval),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,10 @@ 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/at/index.dart';
|
||||
import 'package:pilipala/pages/message/like/index.dart';
|
||||
import 'package:pilipala/pages/message/reply/index.dart';
|
||||
import 'package:pilipala/pages/message/system/index.dart';
|
||||
import 'package:pilipala/pages/setting/pages/logs.dart';
|
||||
|
||||
import '../pages/about/index.dart';
|
||||
@ -178,6 +182,15 @@ class Routes {
|
||||
// 操作菜单
|
||||
CustomGetPage(
|
||||
name: '/actionMenuSet', page: () => const ActionMenuSetPage()),
|
||||
// 回复我的
|
||||
CustomGetPage(name: '/messageReply', page: () => const MessageReplyPage()),
|
||||
// @我的
|
||||
CustomGetPage(name: '/messageAt', page: () => const MessageAtPage()),
|
||||
// 收到的赞
|
||||
CustomGetPage(name: '/messageLike', page: () => const MessageLikePage()),
|
||||
// 系统通知
|
||||
CustomGetPage(
|
||||
name: '/messageSystem', page: () => const MessageSystemPage()),
|
||||
];
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user