feat: at msg

This commit is contained in:
guozhigq
2024-10-31 00:13:11 +08:00
parent c8bd564cf0
commit f71ab166c8
9 changed files with 388 additions and 12 deletions

View File

@ -606,4 +606,7 @@ class Api {
/// 番剧点赞投币收藏状态
static const String bangumiActionStatus = '/pgc/season/episode/community';
/// @我的
static const String messageAtAPi = '/x/msgfeed/at?';
}

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:pilipala/models/msg/at.dart';
import 'package:pilipala/models/msg/like.dart';
import 'package:pilipala/models/msg/reply.dart';
import 'package:pilipala/models/msg/system.dart';
@ -350,4 +351,24 @@ class MsgHttp {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
// @我的
static Future messageAt() async {
var res = await Request().get(Api.messageAtAPi, data: {
'build': 0,
'mobi_app': 'web',
});
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': MessageAtModel.fromJson(res.data['data']),
};
} catch (err) {
return {'status': false, 'data': [], 'msg': err.toString()};
}
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
}

140
lib/models/msg/at.dart Normal file
View File

@ -0,0 +1,140 @@
class MessageAtModel {
Cursor? cursor;
List<MessageAtItems>? items;
MessageAtModel({this.cursor, this.items});
MessageAtModel.fromJson(Map<String, dynamic> json) {
cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
if (json['items'] != null) {
items = <MessageAtItems>[];
json['items'].forEach((v) {
items!.add(MessageAtItems.fromJson(v));
});
}
}
}
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['isEnd'];
time = json['time'];
}
}
class MessageAtItems {
int? id;
int? atTime;
User? user;
MessageAtItem? item;
MessageAtItems({this.id, this.atTime, this.user, this.item});
MessageAtItems.fromJson(Map<String, dynamic> json) {
id = json['id'];
atTime = json['at_time'];
user = json['user'] != null ? User.fromJson(json['user']) : null;
item = json['item'] != null ? MessageAtItem.fromJson(json['item']) : null;
}
}
class MessageAtItem {
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<User>? atDetails;
List<dynamic>? topicDetails;
bool? hideReplyButton;
MessageAtItem({
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,
});
MessageAtItem.fromJson(Map<String, dynamic> 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 = <User>[];
json['at_details'].forEach((v) {
atDetails!.add(User.fromJson(v));
});
}
if (json['topic_details'] != null) {
topicDetails = <dynamic>[];
json['topic_details'].forEach((v) {
topicDetails!.add(v);
});
}
hideReplyButton = json['hide_reply_button'];
}
}
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<String, dynamic> json) {
mid = json['mid'];
fans = json['fans'];
nickname = json['nickname'];
avatar = json['avatar'];
midLink = json['mid_link'];
follow = json['follow'];
}
}

View File

@ -1,3 +1,27 @@
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/at.dart';
class MessageAtController extends GetxController {}
class MessageAtController extends GetxController {
Cursor? cursor;
RxList<MessageAtItems> atItems = <MessageAtItems>[].obs;
Future queryMessageAt({String type = 'init'}) async {
if (cursor != null && cursor!.isEnd == true) {
return {};
}
var res = await MsgHttp.messageAt();
if (res['status']) {
cursor = res['data'].cursor;
if (type == 'init') {
atItems.value = res['data'].items;
} else {
atItems.addAll(res['data'].items);
}
} else {
SmartDialog.showToast(res['msg']);
}
return res;
}
}

View File

@ -1,4 +1,14 @@
import 'package:easy_debounce/easy_throttle.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/common/widgets/no_data.dart';
import 'package:pilipala/models/msg/at.dart';
import 'package:pilipala/pages/message/utils/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'controller.dart';
class MessageAtPage extends StatefulWidget {
const MessageAtPage({super.key});
@ -8,12 +18,179 @@ class MessageAtPage extends StatefulWidget {
}
class _MessageAtPageState extends State<MessageAtPage> {
final MessageAtController _messageAtCtr = Get.put(MessageAtController());
late Future _futureBuilderFuture;
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_futureBuilderFuture = _messageAtCtr.queryMessageAt();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
_messageAtCtr.queryMessageAt(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 _messageAtCtr.queryMessageAt(type: 'init');
},
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
final Map<String, dynamic>? data = snapshot.data;
if (data != null && data['status']) {
final RxList<MessageAtItems> atItems = _messageAtCtr.atItems;
return Obx(
() => atItems.isEmpty
? const CustomScrollView(slivers: [NoData()])
: ListView.separated(
controller: scrollController,
itemBuilder: (context, index) => AtItem(
item: atItems[index],
index: index,
messageAtCtr: _messageAtCtr,
),
itemCount: atItems.length,
separatorBuilder: (BuildContext context, int index) {
return Divider(
indent: 66,
endIndent: 14,
height: 1,
color: Colors.grey.withOpacity(0.1),
);
},
),
);
} else {
// 请求错误
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
fn: () {
setState(() {
_futureBuilderFuture = _messageAtCtr.queryMessageAt();
});
},
isInSliver: false,
);
}
} else {
return const SizedBox();
}
},
),
),
);
}
}
class AtItem extends StatelessWidget {
final MessageAtItems item;
final int index;
final MessageAtController messageAtCtr;
const AtItem({
super.key,
required this.item,
required this.index,
required this.messageAtCtr,
});
@override
Widget build(BuildContext context) {
Color outline = Theme.of(context).colorScheme.outline;
final User user = item.user!;
final String heroTag = Utils.makeHeroTag(user.mid);
final Uri uri = Uri.parse(item.item!.uri!);
/// bilibili://
final Uri nativeUri = Uri.parse(item.item!.nativeUri!);
final String type = item.item!.type!;
return InkWell(
onTap: () async {
MessageUtils.onClickMessage(context, uri, nativeUri, type);
},
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! == 'reply')
TextSpan(
text: '在评论中@了我',
style: TextStyle(color: outline),
),
])),
const SizedBox(height: 6),
Text(item.item!.sourceContent!),
const SizedBox(height: 4),
Text(
Utils.dateFormat(item.atTime!, formatType: 'detail'),
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!.title!,
maxLines: 3,
style: const TextStyle(fontSize: 12, letterSpacing: 0.3),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
}

View File

@ -18,7 +18,11 @@ class MessageLikeController extends GetxController {
id: params['id'], likeTime: params['likeTime']);
if (res['status']) {
cursor = res['data'].total.cursor;
likeItems.addAll(res['data'].total.items);
if (type == 'init') {
likeItems.value = res['data'].total.items;
} else {
likeItems.addAll(res['data'].total.items);
}
}
return res;
}

View File

@ -13,13 +13,20 @@ class MessageUtils {
BuildContext context, Uri uri, Uri nativeUri, String type) async {
final String path = uri.path;
final String bvid = path.split('/').last;
String? sourceType;
final String nativePath = nativeUri.path;
final String oid = nativePath.split('/').last;
String oid = nativePath.split('/').last;
final Map<String, String> queryParameters = nativeUri.queryParameters;
final String? argCid = queryParameters['cid'];
// final String? page = queryParameters['page'];
final String? commentRootId = queryParameters['comment_root_id'];
String? commentRootId = queryParameters['comment_root_id'];
// final String? commentSecondaryId = queryParameters['comment_secondary_id'];
if (nativePath.contains('detail')) {
// 动态详情
sourceType = 'opus';
oid = nativePath.split('/')[3];
commentRootId = nativePath.split('/')[4];
}
switch (type) {
case 'video':
case 'danmu':
@ -42,7 +49,12 @@ class MessageUtils {
case 'reply':
debugPrint('commentRootId: $oid, $commentRootId');
navigateToComment(
context, oid, commentRootId!, ReplyType.video, nativeUri);
context,
oid,
commentRootId!,
sourceType == 'opus' ? ReplyType.dynamics : ReplyType.video,
nativeUri,
);
break;
default:
break;

View File

@ -11,7 +11,7 @@ class VideoReplyReplyController extends GetxController {
int? aid;
// rpid 请求楼中楼回复
String? rpid;
ReplyType replyType = ReplyType.video;
ReplyType? replyType;
bool showRoot = false;
ReplyItemModel? rootReply;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
@ -40,7 +40,7 @@ class VideoReplyReplyController extends GetxController {
oid: aid!,
root: rpid!,
pageNum: currentPage + 1,
type: replyType.index,
type: (replyType ?? ReplyType.video).index,
);
if (res['status']) {
final List<ReplyItemModel> replies = res['data'].replies;

View File

@ -73,11 +73,6 @@ class _WhisperPageState extends State<WhisperPage> {
..._whisperController.noticesList.map((element) {
return InkWell(
onTap: () {
if (['/messageAt']
.contains(element['path'])) {
SmartDialog.showToast('功能开发中');
return;
}
Get.toNamed(element['path']);
if (element['count'] > 0) {