diff --git a/lib/http/msg.dart b/lib/http/msg.dart index 9c8fb7bff..010ec8512 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -6,6 +6,100 @@ import 'init.dart'; class MsgHttp { + static Future msgFeedReplyMe({int cursor = -1, int cursorTime = -1}) async { + var res = await Request().get(Api.msgFeedReply, data: { + 'id': cursor == -1 ? null : cursor, + 'reply_time': cursorTime == -1 ? null : cursorTime, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'date': [], + 'msg': res.data['message'], + }; + } + } + static Future msgFeedAtMe({int cursor = -1, int cursorTime = -1}) async { + var res = await Request().get(Api.msgFeedAt,data: { + 'id': cursor == -1 ? null : cursor, + 'at_time': cursorTime == -1 ? null : cursorTime, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'date': [], + 'msg': res.data['message'], + }; + } + } + static Future msgFeedLikeMe({int cursor = -1, int cursorTime = -1}) async { + var res = await Request().get(Api.msgFeedLike,data: { + 'id': cursor == -1 ? null : cursor, + 'like_time': cursorTime == -1 ? null : cursorTime, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'date': [], + 'msg': res.data['message'], + }; + } + } + static Future msgFeedSysUserNotify() async { + String csrf = await Request.getCsrf(); + var res = await Request().get(Api.msgSysUserNotify, data: { + 'csrf': csrf, + 'csrf': csrf, + 'page_size': 20, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'date': [], + 'msg': res.data['message'], + }; + } + } + static Future msgFeedSysUnifiedNotify() async { + String csrf = await Request.getCsrf(); + var res = await Request().get(Api.msgSysUnifiedNotify, data: { + 'csrf': csrf, + 'csrf': csrf, + 'page_size': 10, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'date': [], + 'msg': res.data['message'], + }; + } + } static Future msgFeedUnread() async { var res = await Request().get(Api.msgFeedUnread); if (res.data['code'] == 0) { diff --git a/lib/models/msg/msgfeed_at_me.dart b/lib/models/msg/msgfeed_at_me.dart new file mode 100644 index 000000000..a01fe9579 --- /dev/null +++ b/lib/models/msg/msgfeed_at_me.dart @@ -0,0 +1,222 @@ +class MsgFeedAtMe { + Cursor? cursor; + List? items; + + MsgFeedAtMe({cursor, items}); + + MsgFeedAtMe.fromJson(Map json) { + cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null; + if (json['items'] != null) { + items = []; + json['items'].forEach((v) { + items!.add(AtMeItems.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['cursor'] = cursor?.toJson(); + data['items'] = items?.map((v) => v.toJson()).toList(); + return data; + } +} + +class Cursor { + bool? isEnd; + int? id; + int? time; + + Cursor({isEnd, id, time}); + + Cursor.fromJson(Map json) { + isEnd = json['is_end']; + id = json['id']; + time = json['time']; + } + + Map toJson() { + final Map data = {}; + data['is_end'] = isEnd; + data['id'] = id; + data['time'] = time; + return data; + } +} + +class AtMeItems { + int? id; + User? user; + Item? item; + int? atTime; + + AtMeItems({id, user, item, atTime}); + + AtMeItems.fromJson(Map json) { + id = json['id']; + user = json['user'] != null ? User.fromJson(json['user']) : null; + item = json['item'] != null ? Item.fromJson(json['item']) : null; + atTime = json['at_time']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['user'] = user?.toJson(); + data['item'] = item?.toJson(); + data['at_time'] = atTime; + return data; + } +} + +class User { + int? mid; + int? fans; + String? nickname; + String? avatar; + String? midLink; + bool? follow; + + User( + {this.mid, + this.fans, + this.nickname, + this.avatar, + this.midLink, + this.follow}); + + User.fromJson(Map json) { + mid = json['mid']; + fans = json['fans']; + nickname = json['nickname']; + avatar = json['avatar']; + midLink = json['mid_link']; + follow = json['follow']; + } + + Map toJson() { + final Map data = {}; + data['mid'] = mid; + data['fans'] = fans; + data['nickname'] = nickname; + data['avatar'] = avatar; + data['mid_link'] = midLink; + data['follow'] = follow; + return data; + } +} + +class Item { + String? type; + String? business; + int? businessId; + String? title; + String? image; + String? uri; + int? subjectId; + int? rootId; + int? targetId; + int? sourceId; + String? sourceContent; + String? nativeUri; + List? atDetails; + List? topicDetails; + bool? hideReplyButton; + + Item( + {this.type, + this.business, + this.businessId, + this.title, + this.image, + this.uri, + this.subjectId, + this.rootId, + this.targetId, + this.sourceId, + this.sourceContent, + this.nativeUri, + this.atDetails, + this.topicDetails, + this.hideReplyButton}); + + Item.fromJson(Map json) { + type = json['type']; + business = json['business']; + businessId = json['business_id']; + title = json['title']; + image = json['image']; + uri = json['uri']; + subjectId = json['subject_id']; + rootId = json['root_id']; + targetId = json['target_id']; + sourceId = json['source_id']; + sourceContent = json['source_content']; + nativeUri = json['native_uri']; + if (json['at_details'] != null) { + atDetails = []; + json['at_details'].forEach((v) { + atDetails!.add(AtDetails.fromJson(v)); + }); + } + topicDetails = json['topic_details']; + hideReplyButton = json['hide_reply_button']; + } + + Map toJson() { + final Map data = {}; + data['type'] = type; + data['business'] = business; + data['business_id'] = businessId; + data['title'] = title; + data['image'] = image; + data['uri'] = uri; + data['subject_id'] = subjectId; + data['root_id'] = rootId; + data['target_id'] = targetId; + data['source_id'] = sourceId; + data['source_content'] = sourceContent; + data['native_uri'] = nativeUri; + data['at_details'] = atDetails?.map((v) => v.toJson()).toList(); + data['topic_details'] = topicDetails?.map((v) => v.toJson()).toList(); + data['hide_reply_button'] = hideReplyButton; + return data; + } +} + +class AtDetails { + int? mid; + int? fans; + String? nickname; + String? avatar; + String? midLink; + bool? follow; + + AtDetails( + {this.mid, + this.fans, + this.nickname, + this.avatar, + this.midLink, + this.follow}); + + AtDetails.fromJson(Map json) { + mid = json['mid']; + fans = json['fans']; + nickname = json['nickname']; + avatar = json['avatar']; + midLink = json['mid_link']; + follow = json['follow']; + } + + Map toJson() { + final Map data = {}; + data['mid'] = mid; + data['fans'] = fans; + data['nickname'] = nickname; + data['avatar'] = avatar; + data['mid_link'] = midLink; + data['follow'] = follow; + return data; + } +} diff --git a/lib/models/msg/msgfeed_like_me.dart b/lib/models/msg/msgfeed_like_me.dart new file mode 100644 index 000000000..a3e5d6b17 --- /dev/null +++ b/lib/models/msg/msgfeed_like_me.dart @@ -0,0 +1,238 @@ +class MsgFeedLikeMe { + Latest? latest; + Total? total; + + MsgFeedLikeMe({latest, total}); + + MsgFeedLikeMe.fromJson(Map json) { + latest = + json['latest'] != null ? Latest.fromJson(json['latest']) : null; + total = json['total'] != null ? Total.fromJson(json['total']) : null; + } + + Map toJson() { + final Map data = {}; + data['latest'] = latest?.toJson(); + data['total'] = total?.toJson(); + return data; + } +} + +class Latest { + List? items; + int? lastViewAt; + + Latest({items, lastViewAt}); + + Latest.fromJson(Map json) { + if (json['items'] != null) { + items = []; + json['items'].forEach((v) { + items!.add(LikeMeItems.fromJson(v)); + }); + } + lastViewAt = json['last_view_at']; + } + + Map toJson() { + final Map data = {}; + data['items'] = items?.map((v) => v.toJson()).toList(); + data['last_view_at'] = lastViewAt; + return data; + } +} + +class LikeMeItems { + int? id; + List? users; + Item? item; + int? counts; + int? likeTime; + int? noticeState; + + LikeMeItems( + {id, + users, + item, + counts, + likeTime, + noticeState}); + + LikeMeItems.fromJson(Map json) { + id = json['id']; + if (json['users'] != null) { + users = []; + json['users'].forEach((v) { + users!.add(Users.fromJson(v)); + }); + } + item = json['item'] != null ? Item.fromJson(json['item']) : null; + counts = json['counts']; + likeTime = json['like_time']; + noticeState = json['notice_state']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['users'] = users?.map((v) => v.toJson()).toList(); + data['item'] = item?.toJson(); + data['counts'] = counts; + data['like_time'] = likeTime; + data['notice_state'] = noticeState; + return data; + } +} + +class Users { + int? mid; + int? fans; + String? nickname; + String? avatar; + String? midLink; + bool? follow; + + Users( + {mid, + fans, + nickname, + avatar, + midLink, + follow}); + + Users.fromJson(Map json) { + mid = json['mid']; + fans = json['fans']; + nickname = json['nickname']; + avatar = json['avatar']; + midLink = json['mid_link']; + follow = json['follow']; + } + + Map toJson() { + final Map data = {}; + data['mid'] = mid; + data['fans'] = fans; + data['nickname'] = nickname; + data['avatar'] = avatar; + data['mid_link'] = midLink; + data['follow'] = follow; + return data; + } +} + +class Item { + 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; + + Item( + {itemId, + pid, + type, + business, + businessId, + replyBusinessId, + likeBusinessId, + title, + desc, + image, + uri, + detailName, + nativeUri, + ctime}); + + Item.fromJson(Map json) { + 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']; + } + + Map toJson() { + final Map data = {}; + data['item_id'] = itemId; + data['pid'] = pid; + data['type'] = type; + data['business'] = business; + data['business_id'] = businessId; + data['reply_business_id'] = replyBusinessId; + data['like_business_id'] = likeBusinessId; + data['title'] = title; + data['desc'] = desc; + data['image'] = image; + data['uri'] = uri; + data['detail_name'] = detailName; + data['native_uri'] = nativeUri; + data['ctime'] = ctime; + return data; + } +} + +class Total { + Cursor? cursor; + List? items; + + Total({cursor, items}); + + Total.fromJson(Map json) { + cursor = + json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null; + if (json['items'] != null) { + items = []; + json['items'].forEach((v) { + items!.add(LikeMeItems.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['cursor'] = cursor?.toJson(); + data['items'] = items?.map((v) => v.toJson()).toList(); + return data; + } +} + +class Cursor { + bool? isEnd; + int? id; + int? time; + + Cursor({isEnd, id, time}); + + Cursor.fromJson(Map json) { + isEnd = json['is_end']; + id = json['id']; + time = json['time']; + } + + Map toJson() { + final Map data = {}; + data['is_end'] = isEnd; + data['id'] = id; + data['time'] = time; + return data; + } +} \ No newline at end of file diff --git a/lib/models/msg/msgfeed_reply_me.dart b/lib/models/msg/msgfeed_reply_me.dart new file mode 100644 index 000000000..4d824a612 --- /dev/null +++ b/lib/models/msg/msgfeed_reply_me.dart @@ -0,0 +1,274 @@ +class MsgFeedReplyMe { + Cursor? cursor; + List? items; + int? lastViewAt; + + MsgFeedReplyMe({this.cursor, this.items, this.lastViewAt}); + + MsgFeedReplyMe.fromJson(Map json) { + cursor = + json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null; + if (json['items'] != null) { + items = []; + json['items'].forEach((v) { + items!.add(ReplyMeItems.fromJson(v)); + }); + } + lastViewAt = json['last_view_at']; + } + + Map toJson() { + final Map data = {}; + data['cursor'] = cursor?.toJson(); + data['items'] = items?.map((v) => v.toJson()).toList(); + data['last_view_at'] = lastViewAt; + return data; + } +} + +class Cursor { + bool? isEnd; + int? id; + int? time; + + Cursor({this.isEnd, this.id, this.time}); + + Cursor.fromJson(Map json) { + isEnd = json['is_end']; + id = json['id']; + time = json['time']; + } + + Map toJson() { + final Map data = {}; + data['is_end'] = isEnd; + data['id'] = id; + data['time'] = time; + return data; + } +} + +class ReplyMeItems { + int? id; + User? user; + Item? item; + int? counts; + int? isMulti; + int? replyTime; + + ReplyMeItems( + {this.id, + this.user, + this.item, + this.counts, + this.isMulti, + this.replyTime}); + + ReplyMeItems.fromJson(Map json) { + id = json['id']; + user = json['user'] != null ? User.fromJson(json['user']) : null; + item = json['item'] != null ? Item.fromJson(json['item']) : null; + counts = json['counts']; + isMulti = json['is_multi']; + replyTime = json['reply_time']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + if (user != null) { + data['user'] = user!.toJson(); + } + if (item != null) { + data['item'] = item!.toJson(); + } + data['counts'] = counts; + data['is_multi'] = isMulti; + data['reply_time'] = replyTime; + return data; + } +} + +class User { + int? mid; + int? fans; + String? nickname; + String? avatar; + String? midLink; + bool? follow; + + User( + {this.mid, + this.fans, + this.nickname, + this.avatar, + this.midLink, + this.follow}); + + User.fromJson(Map json) { + mid = json['mid']; + fans = json['fans']; + nickname = json['nickname']; + avatar = json['avatar']; + midLink = json['mid_link']; + follow = json['follow']; + } + + Map toJson() { + final Map data = {}; + data['mid'] = mid; + data['fans'] = fans; + data['nickname'] = nickname; + data['avatar'] = avatar; + data['mid_link'] = midLink; + data['follow'] = follow; + return data; + } +} + +class Item { + 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; + dynamic danmu; + String? message; + + Item( + {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}); + + Item.fromJson(Map 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']; + if (json['at_details'] != null) { + atDetails = []; + json['at_details'].forEach((v) { + atDetails!.add(AtDetails.fromJson(v)); + }); + } + topicDetails = json['topic_details']; + hideReplyButton = json['hide_reply_button']; + hideLikeButton = json['hide_like_button']; + likeState = json['like_state']; + danmu = json['danmu']; + message = json['message']; + } + + Map toJson() { + final Map data = {}; + data['subject_id'] = subjectId; + data['root_id'] = rootId; + data['source_id'] = sourceId; + data['target_id'] = targetId; + data['type'] = type; + data['business_id'] = businessId; + data['business'] = business; + data['title'] = title; + data['desc'] = desc; + data['image'] = image; + data['uri'] = uri; + data['native_uri'] = nativeUri; + data['detail_title'] = detailTitle; + data['root_reply_content'] = rootReplyContent; + data['source_content'] = sourceContent; + data['target_reply_content'] = targetReplyContent; + data['at_details'] = atDetails?.map((v) => v.toJson()).toList(); + data['topic_details'] = topicDetails?.map((v) => v.toJson()).toList(); + data['hide_reply_button'] = hideReplyButton; + data['hide_like_button'] = hideLikeButton; + data['like_state'] = likeState; + data['danmu'] = danmu; + data['message'] = message; + return data; + } +} + +class AtDetails { + int? mid; + int? fans; + String? nickname; + String? avatar; + String? midLink; + bool? follow; + + AtDetails( + {this.mid, + this.fans, + this.nickname, + this.avatar, + this.midLink, + this.follow}); + + AtDetails.fromJson(Map json) { + mid = json['mid']; + fans = json['fans']; + nickname = json['nickname']; + avatar = json['avatar']; + midLink = json['mid_link']; + follow = json['follow']; + } + + Map toJson() { + final Map data = {}; + data['mid'] = mid; + data['fans'] = fans; + data['nickname'] = nickname; + data['avatar'] = avatar; + data['mid_link'] = midLink; + data['follow'] = follow; + return data; + } +} diff --git a/lib/pages/msg_feed_top/at_me/controller.dart b/lib/pages/msg_feed_top/at_me/controller.dart new file mode 100644 index 000000000..ceb211d15 --- /dev/null +++ b/lib/pages/msg_feed_top/at_me/controller.dart @@ -0,0 +1,43 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/msg.dart'; +import 'package:pilipala/models/msg/msgfeed_at_me.dart'; + +class AtMeController extends GetxController { + RxList msgFeedAtMeList = [].obs; + bool isLoading = false; + int cursor = -1; + int cursorTime = -1; + bool isEnd = false; + + Future queryMsgFeedAtMe() async { + if (isLoading) return; + isLoading = true; + var res = await MsgHttp.msgFeedAtMe(cursor: cursor, cursorTime: cursorTime); + isLoading = false; + if (res['status']) { + MsgFeedAtMe data = MsgFeedAtMe.fromJson(res['data']); + isEnd = data.cursor?.isEnd ?? false; + if (cursor == -1) { + msgFeedAtMeList.assignAll(data.items!); + } else { + msgFeedAtMeList.addAll(data.items!); + } + cursor = data.cursor?.id ?? -1; + cursorTime = data.cursor?.time ?? -1; + } else { + SmartDialog.showToast(res['msg']); + } + } + + Future onLoad() async { + if (isEnd) return; + queryMsgFeedAtMe(); + } + + Future onRefresh() async { + cursor = -1; + queryMsgFeedAtMe(); + } + +} diff --git a/lib/pages/msg_feed_top/at_me/index.dart b/lib/pages/msg_feed_top/at_me/index.dart new file mode 100644 index 000000000..6d2ce5339 --- /dev/null +++ b/lib/pages/msg_feed_top/at_me/index.dart @@ -0,0 +1,4 @@ +library whisper; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/msg_feed_top/at_me/view.dart b/lib/pages/msg_feed_top/at_me/view.dart new file mode 100644 index 000000000..ab15b2df3 --- /dev/null +++ b/lib/pages/msg_feed_top/at_me/view.dart @@ -0,0 +1,124 @@ +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/network_img_layer.dart'; + +import 'controller.dart'; + +class AtMePage extends StatefulWidget { + const AtMePage({super.key}); + + @override + State createState() => _AtMePageState(); +} + +class _AtMePageState extends State { + late final AtMeController _atMeController = Get.put(AtMeController()); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + _atMeController.queryMsgFeedAtMe(); + super.initState(); + _scrollController.addListener(_scrollListener); + } + + Future _scrollListener() async { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle('my-throttler', const Duration(milliseconds: 800), + () async { + await _atMeController.onLoad(); + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('@我的'), + ), + body: RefreshIndicator( + onRefresh: () async { + await _atMeController.onRefresh(); + }, + child: SingleChildScrollView( + controller: _scrollController, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Obx( + () { + if (_atMeController.msgFeedAtMeList.isEmpty) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return ListView.separated( + itemCount: _atMeController.msgFeedAtMeList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (_, int i) { + return ListTile( + onTap: () { + String nativeUri = _atMeController + .msgFeedAtMeList[i].item?.nativeUri ?? + ""; + SmartDialog.showToast("跳转至:$nativeUri(暂未实现)"); + }, + leading: NetworkImgLayer( + width: 45, + height: 45, + type: 'avatar', + src: _atMeController.msgFeedAtMeList[i].user?.avatar, + ), + title: Text( + "${_atMeController.msgFeedAtMeList[i].user?.nickname} " + "在${_atMeController.msgFeedAtMeList[i].item?.business}中@了我", + style: Theme.of(context).textTheme.bodyMedium!, + ), + subtitle: Text( + _atMeController + .msgFeedAtMeList[i].item?.sourceContent ?? + "", + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith( + color: + Theme.of(context).colorScheme.outline)), + trailing: _atMeController + .msgFeedAtMeList[i].item?.image != + null && + _atMeController.msgFeedAtMeList[i].item?.image != + "" + ? NetworkImgLayer( + width: 45, + height: 45, + type: 'cover', + src: _atMeController + .msgFeedAtMeList[i].item?.image, + ) + : null, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider( + indent: 72, + endIndent: 20, + height: 6, + color: Colors.grey.withOpacity(0.1), + ); + }, + ); + }, + ); + }), + ), + ), + ); + } +} diff --git a/lib/pages/msg_feed_top/like_me/controller.dart b/lib/pages/msg_feed_top/like_me/controller.dart new file mode 100644 index 000000000..aa2e3a5c5 --- /dev/null +++ b/lib/pages/msg_feed_top/like_me/controller.dart @@ -0,0 +1,43 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/msg.dart'; +import '../../../models/msg/msgfeed_like_me.dart'; + +class LikeMeController extends GetxController { + RxList msgFeedLikeMeList = [].obs; + bool isLoading = false; + int cursor = -1; + int cursorTime = -1; + bool isEnd = false; + + Future queryMsgFeedLikeMe() async { + if (isLoading) return; + isLoading = true; + var res = await MsgHttp.msgFeedLikeMe(cursor: cursor, cursorTime: cursorTime); + isLoading = false; + if (res['status']) { + MsgFeedLikeMe data = MsgFeedLikeMe.fromJson(res['data']); + isEnd = data.total?.cursor?.isEnd ?? false; + if (cursor == -1) { + msgFeedLikeMeList.assignAll(data.total!.items!); + } else { + msgFeedLikeMeList.addAll(data.total!.items!); + } + cursor = data.total?.cursor?.id ?? -1; + cursorTime = data.total?.cursor?.time ?? -1; + } else { + SmartDialog.showToast(res['msg']); + } + } + + Future onLoad() async { + if (isEnd) return; + queryMsgFeedLikeMe(); + } + + Future onRefresh() async { + cursor = -1; + queryMsgFeedLikeMe(); + } + +} diff --git a/lib/pages/msg_feed_top/like_me/index.dart b/lib/pages/msg_feed_top/like_me/index.dart new file mode 100644 index 000000000..6d2ce5339 --- /dev/null +++ b/lib/pages/msg_feed_top/like_me/index.dart @@ -0,0 +1,4 @@ +library whisper; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/msg_feed_top/like_me/view.dart b/lib/pages/msg_feed_top/like_me/view.dart new file mode 100644 index 000000000..2fc3ba2d1 --- /dev/null +++ b/lib/pages/msg_feed_top/like_me/view.dart @@ -0,0 +1,167 @@ +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/network_img_layer.dart'; + +import 'controller.dart'; + +class LikeMePage extends StatefulWidget { + const LikeMePage({super.key}); + + @override + State createState() => _LikeMePageState(); +} + +class _LikeMePageState extends State { + late final LikeMeController _likeMeController = Get.put(LikeMeController()); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + _likeMeController.queryMsgFeedLikeMe(); + super.initState(); + _scrollController.addListener(_scrollListener); + } + + Future _scrollListener() async { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle('my-throttler', const Duration(milliseconds: 800), + () async { + await _likeMeController.onLoad(); + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('收到的赞'), + ), + body: RefreshIndicator( + onRefresh: () async { + await _likeMeController.onRefresh(); + }, + child: SingleChildScrollView( + controller: _scrollController, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Obx( + () { + if (_likeMeController.msgFeedLikeMeList.isEmpty) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return ListView.separated( + itemCount: _likeMeController.msgFeedLikeMeList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (_, int i) { + return ListTile( + onTap: () { + String nativeUri = _likeMeController + .msgFeedLikeMeList[i].item?.nativeUri ?? + ""; + SmartDialog.showToast("跳转至:$nativeUri(暂未实现)"); + }, + leading: SizedBox( + width: 50, + height: 50, + child: Stack( + children: [ + for (var j = 0; + j < + _likeMeController.msgFeedLikeMeList[i] + .users!.length && + j < 4; + j++) ...[ + Positioned( + left: 15 * (j % 2).toDouble(), + top: 15 * (j ~/ 2).toDouble(), + child: NetworkImgLayer( + width: _likeMeController + .msgFeedLikeMeList[i] + .users! + .length > + 1 + ? 30 + : 45, + height: _likeMeController + .msgFeedLikeMeList[i] + .users! + .length > + 1 + ? 30 + : 45, + type: 'avatar', + src: _likeMeController + .msgFeedLikeMeList[i] + .users![j] + .avatar, + )), + ] + ], + )), + title: Text( + "${_likeMeController.msgFeedLikeMeList[i].users!.map((e) => e.nickname).join("、")} " + "赞了我的${_likeMeController.msgFeedLikeMeList[i].item?.business}", + style: Theme.of(context).textTheme.bodyMedium!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + subtitle: + _likeMeController.msgFeedLikeMeList[i].item?.title != + null && + _likeMeController + .msgFeedLikeMeList[i].item?.title != + "" + ? Text( + _likeMeController + .msgFeedLikeMeList[i].item?.title ?? + "", + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .outline)) + : null, + trailing: + _likeMeController.msgFeedLikeMeList[i].item?.image != + null && + _likeMeController + .msgFeedLikeMeList[i].item?.image != + "" + ? NetworkImgLayer( + width: 45, + height: 45, + type: 'cover', + src: _likeMeController + .msgFeedLikeMeList[i].item?.image, + ) + : null, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider( + indent: 72, + endIndent: 20, + height: 6, + color: Colors.grey.withOpacity(0.1), + ); + }, + ); + }, + ); + }), + ), + ), + ); + } +} diff --git a/lib/pages/msg_feed_top/reply_me/controller.dart b/lib/pages/msg_feed_top/reply_me/controller.dart new file mode 100644 index 000000000..a1b7f2e32 --- /dev/null +++ b/lib/pages/msg_feed_top/reply_me/controller.dart @@ -0,0 +1,44 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/msg.dart'; + +import '../../../models/msg/msgfeed_reply_me.dart'; + +class ReplyMeController extends GetxController { + RxList msgFeedReplyMeList = [].obs; + bool isLoading = false; + int cursor = -1; + int cursorTime = -1; + bool isEnd = false; + + Future queryMsgFeedReplyMe() async { + if (isLoading) return; + isLoading = true; + var res = await MsgHttp.msgFeedReplyMe(cursor: cursor, cursorTime: cursorTime); + isLoading = false; + if (res['status']) { + MsgFeedReplyMe data = MsgFeedReplyMe.fromJson(res['data']); + isEnd = data.cursor?.isEnd ?? false; + if (cursor == -1) { + msgFeedReplyMeList.assignAll(data.items!); + } else { + msgFeedReplyMeList.addAll(data.items!); + } + cursor = data.cursor?.id ?? -1; + cursorTime = data.cursor?.time ?? -1; + } else { + SmartDialog.showToast(res['msg']); + } + } + + Future onLoad() async { + if (isEnd) return; + queryMsgFeedReplyMe(); + } + + Future onRefresh() async { + cursor = -1; + queryMsgFeedReplyMe(); + } + +} diff --git a/lib/pages/msg_feed_top/reply_me/index.dart b/lib/pages/msg_feed_top/reply_me/index.dart new file mode 100644 index 000000000..6d2ce5339 --- /dev/null +++ b/lib/pages/msg_feed_top/reply_me/index.dart @@ -0,0 +1,4 @@ +library whisper; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/msg_feed_top/reply_me/view.dart b/lib/pages/msg_feed_top/reply_me/view.dart new file mode 100644 index 000000000..616029a70 --- /dev/null +++ b/lib/pages/msg_feed_top/reply_me/view.dart @@ -0,0 +1,149 @@ +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/network_img_layer.dart'; + +import 'controller.dart'; + +class ReplyMePage extends StatefulWidget { + const ReplyMePage({super.key}); + + @override + State createState() => _ReplyMePageState(); +} + +class _ReplyMePageState extends State { + late final ReplyMeController _replyMeController = + Get.put(ReplyMeController()); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + _replyMeController.queryMsgFeedReplyMe(); + super.initState(); + _scrollController.addListener(_scrollListener); + } + + Future _scrollListener() async { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle('my-throttler', const Duration(milliseconds: 800), + () async { + await _replyMeController.onLoad(); + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('回复我的'), + ), + body: RefreshIndicator( + onRefresh: () async { + await _replyMeController.onRefresh(); + }, + child: SingleChildScrollView( + controller: _scrollController, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Obx( + () { + if (_replyMeController.msgFeedReplyMeList.isEmpty) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return ListView.separated( + itemCount: _replyMeController.msgFeedReplyMeList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (_, int i) { + return ListTile( + onTap: () { + String nativeUri = _replyMeController + .msgFeedReplyMeList[i].item?.nativeUri ?? + ""; + SmartDialog.showToast("跳转至:$nativeUri(暂未实现)"); + }, + leading: NetworkImgLayer( + width: 45, + height: 45, + type: 'avatar', + src: _replyMeController + .msgFeedReplyMeList[i].user?.avatar, + ), + title: Text( + "${_replyMeController.msgFeedReplyMeList[i].user?.nickname} " + "回复了我的${_replyMeController.msgFeedReplyMeList[i].item?.business}", + style: Theme.of(context).textTheme.bodyMedium!, + ), + subtitle: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + _replyMeController.msgFeedReplyMeList[i].item + ?.sourceContent ?? + "", + style: Theme.of(context).textTheme.bodyMedium), + const SizedBox(height: 4), + if (_replyMeController.msgFeedReplyMeList[i].item + ?.targetReplyContent != + null && + _replyMeController.msgFeedReplyMeList[i].item + ?.targetReplyContent != + "") + Text( + "| ${_replyMeController.msgFeedReplyMeList[i].item?.targetReplyContent}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .outline, + height: 1.5)), + if (_replyMeController.msgFeedReplyMeList[i].item + ?.rootReplyContent != + null && + _replyMeController.msgFeedReplyMeList[i].item + ?.rootReplyContent != + "") + Text( + " | ${_replyMeController.msgFeedReplyMeList[i].item?.rootReplyContent}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .outline, + height: 1.5)), + ]), + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider( + indent: 72, + endIndent: 20, + height: 6, + color: Colors.grey.withOpacity(0.1), + ); + }, + ); + }, + ); + }), + ), + ), + ); + } +} diff --git a/lib/pages/whisper/controller.dart b/lib/pages/whisper/controller.dart index 6b2444fd3..d04641223 100644 --- a/lib/pages/whisper/controller.dart +++ b/lib/pages/whisper/controller.dart @@ -16,25 +16,25 @@ class WhisperController extends GetxController { { "name":"回复我的", "icon":Icons.message_outlined, - "route": "/", + "route": "/replyMe", "value": 0 }, { "name":"@我", "icon":Icons.alternate_email_outlined, - "route": "/", + "route": "/atMe", "value": 0 }, { "name":"收到的赞", "icon":Icons.favorite_border_outlined, - "route": "/", + "route": "/likeMe", "value": 0 }, { "name":"系统通知", "icon":Icons.notifications_none_outlined, - "route": "/", + "route": "/sysMsg", "value": 0 }, ].obs; diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index e9615347d..d20e4c889 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -58,20 +58,20 @@ class _WhisperPageState extends State { return Padding( padding: const EdgeInsets.only(left: 20, right: 20), child: SizedBox( - height: constraints.maxWidth / 5, + height: constraints.maxWidth / 4, child: Obx( () => GridView.count( primary: false, crossAxisCount: 4, padding: const EdgeInsets.all(0), childAspectRatio: 1.25, - children: - _whisperController.msgFeedTop.map((item) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Badge( + children: _whisperController.msgFeedTop.map((item) { + return GestureDetector( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Badge( isLabelVisible: item['value'] > 0, backgroundColor: Theme.of(context).colorScheme.primary, @@ -80,38 +80,25 @@ class _WhisperPageState extends State { .onInverseSurface, label: Text(" ${item['value']} "), alignment: Alignment.topRight, - child: 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: () => Get.toNamed( - item['route'], - ), - icon: Icon( - item['icon'], - size: 18, - color: Theme.of(context) - .colorScheme - .primary, - ), + child: CircleAvatar( + radius: 22, + backgroundColor: Theme.of(context) + .colorScheme + .onInverseSurface, + child: Icon( + item['icon'], + size: 20, + color: + Theme.of(context).colorScheme.primary, ), - )), - const SizedBox(height: 6), - Text(item['name'], - style: const TextStyle(fontSize: 13)) - ], + ), + ), + const SizedBox(height: 6), + Text(item['name'], + style: const TextStyle(fontSize: 13)) + ], + ), + onTap: () => Get.toNamed(item['route']), ); }).toList(), ), diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index 88ce09468..87b3987c9 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -133,6 +133,7 @@ class ChatItem extends StatelessWidget { jsonDecode(content['content']) .map((m) => m['text'] as String) .join("\n"), + textAlign: TextAlign.center, style: TextStyle( letterSpacing: 0.6, height: 5, diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index ff5066301..38d4622d2 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -3,6 +3,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/pages/msg_feed_top/at_me/view.dart'; +import 'package:pilipala/pages/msg_feed_top/reply_me/view.dart'; +import 'package:pilipala/pages/msg_feed_top/like_me/view.dart'; import 'package:pilipala/pages/setting/pages/logs.dart'; import '../pages/about/index.dart'; @@ -139,6 +142,14 @@ class Routes { // 私信详情 CustomGetPage( name: '/whisperDetail', page: () => const WhisperDetailPage()), + // 回复我的 + CustomGetPage(name: '/replyMe', page: () => const ReplyMePage()), + // @我的 + CustomGetPage(name: '/atMe', page: () => const AtMePage()), + // 收到的赞 + CustomGetPage(name: '/likeMe', page: () => const LikeMePage()), + // 系统消息 + CustomGetPage(name: '/sysMsg', page: () => const WhisperPage()), // 登录页面 CustomGetPage(name: '/loginPage', page: () => const LoginPage()), // 用户动态