diff --git a/lib/http/api.dart b/lib/http/api.dart index 2bd83d69..078e615e 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -606,4 +606,7 @@ class Api { /// 番剧点赞投币收藏状态 static const String bangumiActionStatus = '/pgc/season/episode/community'; + + /// @我的 + static const String messageAtAPi = '/x/msgfeed/at?'; } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index ebcb0895..65156e03 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -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']}; + } + } } diff --git a/lib/http/reply.dart b/lib/http/reply.dart index c37a92cf..846ef45b 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -136,13 +136,13 @@ class ReplyHttp { } // 图片上传 - static Future uploadImage({required XFile xFile, String type = 'im'}) async { + static Future uploadImage( + {required XFile xFile, String type = 'new_dyn'}) async { var formData = FormData.fromMap({ 'file_up': await xFileToMultipartFile(xFile), 'biz': type, 'csrf': await Request.getCsrf(), - 'build': 0, - 'mobi_app': 'web', + 'category': 'daily', }); var res = await Request().post( Api.uploadImage, diff --git a/lib/models/msg/at.dart b/lib/models/msg/at.dart new file mode 100644 index 00000000..b8373989 --- /dev/null +++ b/lib/models/msg/at.dart @@ -0,0 +1,140 @@ +class MessageAtModel { + Cursor? cursor; + List? items; + + MessageAtModel({this.cursor, this.items}); + + MessageAtModel.fromJson(Map json) { + cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null; + if (json['items'] != null) { + items = []; + 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 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 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? atDetails; + List? 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 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(User.fromJson(v)); + }); + } + if (json['topic_details'] != null) { + topicDetails = []; + 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 json) { + mid = json['mid']; + fans = json['fans']; + nickname = json['nickname']; + avatar = json['avatar']; + midLink = json['mid_link']; + follow = json['follow']; + } +} diff --git a/lib/pages/member_archive/controller.dart b/lib/pages/member_archive/controller.dart index 20cf38d3..92f95da6 100644 --- a/lib/pages/member_archive/controller.dart +++ b/lib/pages/member_archive/controller.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/member.dart'; import 'package:pilipala/models/member/archive.dart'; +import 'package:pilipala/utils/global_data_cache.dart'; class MemberArchiveController extends GetxController { final ScrollController scrollController = ScrollController(); @@ -17,12 +18,18 @@ class MemberArchiveController extends GetxController { ].obs; RxList archivesList = [].obs; RxBool isLoading = false.obs; + late int ownerMid; + RxBool isOwner = false.obs; @override void onInit() { super.onInit(); mid = int.parse(Get.parameters['mid']!); currentOrder.value = orderList.first; + ownerMid = GlobalDataCache().userInfo != null + ? GlobalDataCache().userInfo!.mid! + : -1; + isOwner.value = mid == -1 || mid == ownerMid; } // 获取用户投稿 diff --git a/lib/pages/member_archive/view.dart b/lib/pages/member_archive/view.dart index 898aa915..86ff9940 100644 --- a/lib/pages/member_archive/view.dart +++ b/lib/pages/member_archive/view.dart @@ -51,8 +51,9 @@ class _MemberArchivePageState extends State { centerTitle: false, title: Obx( () => Text( - '他的投稿 - ${_memberArchivesController.currentOrder['label']}', - style: Theme.of(context).textTheme.titleMedium), + '${_memberArchivesController.isOwner.value ? '我' : 'Ta'}的投稿 - ${_memberArchivesController.currentOrder['label']}', + style: Theme.of(context).textTheme.titleMedium, + ), ), actions: [ // Obx( diff --git a/lib/pages/member_article/controller.dart b/lib/pages/member_article/controller.dart index 936dd9da..9c67f679 100644 --- a/lib/pages/member_article/controller.dart +++ b/lib/pages/member_article/controller.dart @@ -3,6 +3,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/member.dart'; import 'package:pilipala/models/member/article.dart'; +import 'package:pilipala/utils/global_data_cache.dart'; class MemberArticleController extends GetxController { final ScrollController scrollController = ScrollController(); @@ -12,11 +13,17 @@ class MemberArticleController extends GetxController { bool hasMore = true; RxBool isLoading = false.obs; RxList articleList = [].obs; + late int ownerMid; + RxBool isOwner = false.obs; @override void onInit() { super.onInit(); mid = int.parse(Get.parameters['mid']!); + ownerMid = GlobalDataCache().userInfo != null + ? GlobalDataCache().userInfo!.mid! + : -1; + isOwner.value = mid == -1 || mid == ownerMid; } Future getMemberArticle(type) async { diff --git a/lib/pages/member_article/view.dart b/lib/pages/member_article/view.dart index bc7d4087..2557beba 100644 --- a/lib/pages/member_article/view.dart +++ b/lib/pages/member_article/view.dart @@ -50,7 +50,12 @@ class _MemberArticlePageState extends State { appBar: AppBar( titleSpacing: 0, centerTitle: false, - title: const Text('Ta的图文', style: TextStyle(fontSize: 16)), + title: Obx( + () => Text( + '${_memberArticleController.isOwner.value ? '我' : 'Ta'}的图文', + style: Theme.of(context).textTheme.titleMedium, + ), + ), ), body: FutureBuilder( future: _futureBuilderFuture, diff --git a/lib/pages/member_dynamics/controller.dart b/lib/pages/member_dynamics/controller.dart index efc6104c..8fdf55b7 100644 --- a/lib/pages/member_dynamics/controller.dart +++ b/lib/pages/member_dynamics/controller.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/member.dart'; import 'package:pilipala/models/dynamics/result.dart'; +import 'package:pilipala/utils/global_data_cache.dart'; class MemberDynamicsController extends GetxController { final ScrollController scrollController = ScrollController(); @@ -10,11 +11,17 @@ class MemberDynamicsController extends GetxController { int count = 0; bool hasMore = true; RxList dynamicsList = [].obs; + late int ownerMid; + RxBool isOwner = false.obs; @override void onInit() { super.onInit(); mid = int.parse(Get.parameters['mid']!); + ownerMid = GlobalDataCache().userInfo != null + ? GlobalDataCache().userInfo!.mid! + : -1; + isOwner.value = mid == -1 || mid == ownerMid; } Future getMemberDynamic(type) async { diff --git a/lib/pages/member_dynamics/view.dart b/lib/pages/member_dynamics/view.dart index 2e093bcc..e6153b7b 100644 --- a/lib/pages/member_dynamics/view.dart +++ b/lib/pages/member_dynamics/view.dart @@ -56,7 +56,12 @@ class _MemberDynamicsPageState extends State { appBar: AppBar( titleSpacing: 0, centerTitle: false, - title: Text('他的动态', style: Theme.of(context).textTheme.titleMedium), + title: Obx( + () => Text( + '${_memberDynamicController.isOwner.value ? '我' : 'Ta'}的动态', + style: Theme.of(context).textTheme.titleMedium, + ), + ), ), body: CustomScrollView( controller: _memberDynamicController.scrollController, diff --git a/lib/pages/message/at/controller.dart b/lib/pages/message/at/controller.dart index af08987f..4f6117a8 100644 --- a/lib/pages/message/at/controller.dart +++ b/lib/pages/message/at/controller.dart @@ -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 atItems = [].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; + } +} diff --git a/lib/pages/message/at/view.dart b/lib/pages/message/at/view.dart index 9c48ec99..542b894c 100644 --- a/lib/pages/message/at/view.dart +++ b/lib/pages/message/at/view.dart @@ -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 { + 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? data = snapshot.data; + if (data != null && data['status']) { + final RxList 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, + ), + ), + ], + ), + ), ); } } diff --git a/lib/pages/message/like/controller.dart b/lib/pages/message/like/controller.dart index 9b09f89a..8afe4d35 100644 --- a/lib/pages/message/like/controller.dart +++ b/lib/pages/message/like/controller.dart @@ -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; } diff --git a/lib/pages/message/utils/index.dart b/lib/pages/message/utils/index.dart index 01c3612c..c14e4d0a 100644 --- a/lib/pages/message/utils/index.dart +++ b/lib/pages/message/utils/index.dart @@ -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 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; diff --git a/lib/pages/video/detail/reply_reply/controller.dart b/lib/pages/video/detail/reply_reply/controller.dart index 506e530e..f57baa79 100644 --- a/lib/pages/video/detail/reply_reply/controller.dart +++ b/lib/pages/video/detail/reply_reply/controller.dart @@ -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 replyList = [].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 replies = res['data'].replies; diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index 4780b908..721ffbe2 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -73,11 +73,6 @@ class _WhisperPageState extends State { ..._whisperController.noticesList.map((element) { return InkWell( onTap: () { - if (['/messageAt'] - .contains(element['path'])) { - SmartDialog.showToast('功能开发中'); - return; - } Get.toNamed(element['path']); if (element['count'] > 0) {