diff --git a/lib/grpc/grpc_repo.dart b/lib/grpc/grpc_repo.dart index 801d7f059..827b2c8a9 100644 --- a/lib/grpc/grpc_repo.dart +++ b/lib/grpc/grpc_repo.dart @@ -62,6 +62,7 @@ class GrpcUrl { static const keywordBlockingAdd = '$im2/KeywordBlockingAdd'; static const keywordBlockingDelete = '$im2/KeywordBlockingDelete'; static const syncFetchSessionMsgs = '$im/SyncFetchSessionMsgs'; + static const getTotalUnread = '$im/GetTotalUnread'; } class GrpcRepo { diff --git a/lib/grpc/im.dart b/lib/grpc/im.dart index 19769e44f..2e6ebe196 100644 --- a/lib/grpc/im.dart +++ b/lib/grpc/im.dart @@ -199,4 +199,13 @@ class ImGrpc { KeywordBlockingDeleteReply.fromBuffer, ); } + + static Future> getTotalUnread( + {int? unreadType}) { + return GrpcRepo.request( + GrpcUrl.getTotalUnread, + ReqTotalUnread(unreadType: unreadType, showUnfollowList: 1), + RspTotalUnread.fromBuffer, + ); + } } diff --git a/lib/http/api.dart b/lib/http/api.dart index 32ed44a92..18581e8e1 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -476,11 +476,11 @@ class Api { // 获取未读私信数 // https://api.vc.bilibili.com/session_svr/v1/session_svr/single_unread - static const String msgUnread = - '${HttpString.tUrl}/session_svr/v1/session_svr/single_unread'; + // static const String msgUnread = + // '${HttpString.tUrl}/session_svr/v1/session_svr/single_unread'; // 获取消息中心未读信息 - static const String msgFeedUnread = '/x/msgfeed/unread'; + // static const String msgFeedUnread = '/x/msgfeed/unread'; //https://api.bilibili.com/x/msgfeed/reply?platform=web&build=0&mobi_app=web static const String msgFeedReply = '/x/msgfeed/reply'; //https://api.bilibili.com/x/msgfeed/at?platform=web&build=0&mobi_app=web diff --git a/lib/http/msg.dart b/lib/http/msg.dart index f4cc2fe41..19b6c6c73 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -103,18 +103,6 @@ class MsgHttp { } } - static Future msgFeedUnread() async { - var res = await Request().get(Api.msgFeedUnread); - if (res.data['code'] == 0) { - return { - 'status': true, - 'data': res.data['data'], - }; - } else { - return {'status': false, 'msg': res.data['message']}; - } - } - static Future createDynamic({ dynamic mid, dynamic dynIdStr, // repost dyn diff --git a/lib/models/common/msg/msg_unread_type.dart b/lib/models/common/msg/msg_unread_type.dart index 2e9e0d337..37da73979 100644 --- a/lib/models/common/msg/msg_unread_type.dart +++ b/lib/models/common/msg/msg_unread_type.dart @@ -1,5 +1,10 @@ -enum MsgUnReadType { pm, reply, at, like, sysMsg } +enum MsgUnReadType { + pm('私信'), + reply('回复我的'), + at('@我'), + like('收到的赞'), + sysMsg('系统通知'); -extension MsgUnReadTypeExt on MsgUnReadType { - String get title => const ['私信', '回复我的', '@我', '收到的赞', '系统通知'][index]; + final String title; + const MsgUnReadType(this.title); } diff --git a/lib/models/msg/msgfeed_unread.dart b/lib/models/msg/msgfeed_unread.dart index f044e065c..671c70e95 100644 --- a/lib/models/msg/msgfeed_unread.dart +++ b/lib/models/msg/msgfeed_unread.dart @@ -1,26 +1,15 @@ -class MsgFeedUnread { - MsgFeedUnread({ - this.at = 0, - this.chat = 0, - this.like = 0, - this.reply = 0, - this.sysMsg = 0, - this.up = 0, - }); +import 'package:fixnum/fixnum.dart'; +class MsgFeedUnread { int at = 0; - int chat = 0; int like = 0; int reply = 0; int sysMsg = 0; - int up = 0; - MsgFeedUnread.fromJson(Map json) { - at = json['at'] ?? 0; - chat = json['chat'] ?? 0; - like = json['like'] ?? 0; - reply = json['reply'] ?? 0; - sysMsg = json['sys_msg'] ?? 0; - up = json['up'] ?? 0; + MsgFeedUnread.fromJson(Map json) { + at = json['at']?.toInt() ?? 0; + like = json['like']?.toInt() ?? 0; + reply = json['reply']?.toInt() ?? 0; + sysMsg = json['sys_msg']?.toInt() ?? 0; } } diff --git a/lib/pages/article_list/view.dart b/lib/pages/article_list/view.dart index 37505fc54..f6d5a36b4 100644 --- a/lib/pages/article_list/view.dart +++ b/lib/pages/article_list/view.dart @@ -3,6 +3,7 @@ import 'package:PiliPlus/common/skeleton/video_card_h.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models/dynamics/article_list/list.dart'; @@ -10,6 +11,7 @@ import 'package:PiliPlus/models/space_article/item.dart'; import 'package:PiliPlus/pages/article_list/controller.dart'; import 'package:PiliPlus/pages/article_list/widgets/item.dart'; import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -188,6 +190,16 @@ class _ArticleListPageState extends State { ), ), ), + actions: [ + IconButton( + tooltip: '浏览器打开', + onPressed: () { + PageUtils.inAppWebview( + '${HttpString.baseUrl}/read/readlist/rl${_controller.id}'); + }, + icon: const Icon(Icons.open_in_browser_outlined, size: 19), + ) + ], ); } } diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 013e40e7a..c5777a0eb 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -1,8 +1,7 @@ import 'dart:async'; import 'package:PiliPlus/grpc/dyn.dart'; -import 'package:PiliPlus/http/api.dart'; -import 'package:PiliPlus/http/init.dart'; +import 'package:PiliPlus/grpc/im.dart'; import 'package:PiliPlus/models/common/dynamic/dynamic_badge_mode.dart'; import 'package:PiliPlus/models/common/msg/msg_unread_type.dart'; import 'package:PiliPlus/models/common/nav_bar_config.dart'; @@ -83,92 +82,51 @@ class MainController extends GetxController { msgUnReadCount.value = ''; return; } - try { - bool shouldCheckPM = msgUnReadTypes.contains(MsgUnReadType.pm); - bool shouldCheckFeed = - shouldCheckPM ? msgUnReadTypes.length > 1 : msgUnReadTypes.isNotEmpty; - List res = await Future.wait([ - if (shouldCheckPM) _queryPMUnread(), - if (shouldCheckFeed) _queryMsgFeedUnread(), - ]); - dynamic count = 0; - if (shouldCheckPM && res.firstOrNull?['status'] == true) { - count = (res.first['data'] as int?) ?? 0; - } - if ((shouldCheckPM.not && res.firstOrNull?['status'] == true) || - (shouldCheckPM && res.getOrNull(1)?['status'] == true)) { - int index = shouldCheckPM.not ? 0 : 1; - dynamic data = res[index]['data']; + + int count = 0; + final res = await ImGrpc.getTotalUnread(); + if (res.isSuccess) { + final data = res.data; + if (msgUnReadTypes.length == MsgUnReadType.values.length) { + count = data.hasTotalUnread() ? data.totalUnread : 0; + } else { + final msgUnread = data.msgFeedUnread.unread; for (final item in msgUnReadTypes) { switch (item) { case MsgUnReadType.pm: + final pmUnread = data.sessionSingleUnread; + count += (pmUnread.followUnread + + pmUnread.unfollowUnread + + pmUnread.dustbinUnread) + .toInt(); break; case MsgUnReadType.reply: - count += (data['reply'] as int?) ?? 0; + count += msgUnread['reply']?.toInt() ?? 0; break; case MsgUnReadType.at: - count += (data['at'] as int?) ?? 0; + count += msgUnread['at']?.toInt() ?? 0; break; case MsgUnReadType.like: - count += (data['like'] as int?) ?? 0; + count += msgUnread['like']?.toInt() ?? 0; break; case MsgUnReadType.sysMsg: - count += (data['sys_msg'] as int?) ?? 0; + count += msgUnread['sys_msg']?.toInt() ?? 0; break; } } } - count = count == 0 - ? '' - : count > 99 - ? '99+' - : count.toString(); - if (msgUnReadCount.value == count) { - msgUnReadCount.refresh(); - } else { - msgUnReadCount.value = count; - } - } catch (e) { - debugPrint('failed to get unread count: $e'); } - } - Future _queryPMUnread() async { - try { - dynamic res = await Request().get(Api.msgUnread); - if (res.data['code'] == 0) { - return { - 'status': true, - 'data': ((res.data['data']?['unfollow_unread'] as int?) ?? 0) + - ((res.data['data']?['follow_unread'] as int?) ?? 0), - }; - } else { - return { - 'status': false, - 'msg': res.data['message'], - }; - } - } catch (_) {} - } - - Future _queryMsgFeedUnread() async { - if (isLogin.value.not) { - return; + final countStr = count == 0 + ? '' + : count > 99 + ? '99+' + : count.toString(); + if (msgUnReadCount.value == countStr) { + msgUnReadCount.refresh(); + } else { + msgUnReadCount.value = countStr; } - try { - dynamic res = await Request().get(Api.msgFeedUnread); - if (res.data['code'] == 0) { - return { - 'status': true, - 'data': res.data['data'], - }; - } else { - return { - 'status': false, - 'msg': res.data['message'], - }; - } - } catch (_) {} } Future getUnreadDynamic() async { diff --git a/lib/pages/video/reply/widgets/zan_grpc.dart b/lib/pages/video/reply/widgets/zan_grpc.dart index 0d4611c04..e41a2031a 100644 --- a/lib/pages/video/reply/widgets/zan_grpc.dart +++ b/lib/pages/video/reply/widgets/zan_grpc.dart @@ -22,13 +22,16 @@ class ZanButtonGrpc extends StatefulWidget { } class _ZanButtonGrpcState extends State { + bool get isLike => widget.replyItem.replyControl.action == $fixnum.Int64.ONE; + bool get isDislike => + widget.replyItem.replyControl.action == $fixnum.Int64.TWO; + Future onHateReply() async { feedBack(); final int oid = widget.replyItem.oid.toInt(); final int rpid = widget.replyItem.id.toInt(); // 1 已点赞 2 不喜欢 0 未操作 - final int action = - widget.replyItem.replyControl.action.toInt() != 2 ? 2 : 0; + final int action = isDislike ? 0 : 2; final res = await ReplyHttp.hateReply( type: widget.replyItem.type.toInt(), action: action == 2 ? 1 : 0, @@ -37,13 +40,9 @@ class _ZanButtonGrpcState extends State { ); // SmartDialog.dismiss(); if (res['status']) { - SmartDialog.showToast( - widget.replyItem.replyControl.action.toInt() != 2 ? '点踩成功' : '取消踩'); + SmartDialog.showToast(isDislike ? '取消踩' : '点踩成功'); if (action == 2) { - if (widget.replyItem.replyControl.action.toInt() == 1) { - widget.replyItem.like = - $fixnum.Int64(widget.replyItem.like.toInt() - 1); - } + if (isLike) widget.replyItem.like -= $fixnum.Int64.ONE; widget.replyItem.replyControl.action = $fixnum.Int64.TWO; } else { widget.replyItem.replyControl.action = $fixnum.Int64.ZERO; @@ -60,8 +59,7 @@ class _ZanButtonGrpcState extends State { final int oid = widget.replyItem.oid.toInt(); final int rpid = widget.replyItem.id.toInt(); // 1 已点赞 2 不喜欢 0 未操作 - final int action = - widget.replyItem.replyControl.action.toInt() != 1 ? 1 : 0; + final int action = isLike ? 0 : 1; final res = await ReplyHttp.likeReply( type: widget.replyItem.type.toInt(), oid: oid, @@ -69,15 +67,12 @@ class _ZanButtonGrpcState extends State { action: action, ); if (res['status']) { - SmartDialog.showToast( - widget.replyItem.replyControl.action.toInt() != 1 ? '点赞成功' : '取消赞'); + SmartDialog.showToast(isLike ? '取消赞' : '点赞成功'); if (action == 1) { - widget.replyItem.like = - $fixnum.Int64(widget.replyItem.like.toInt() + 1); + widget.replyItem.like += $fixnum.Int64.ONE; widget.replyItem.replyControl.action = $fixnum.Int64.ONE; } else { - widget.replyItem.like = - $fixnum.Int64(widget.replyItem.like.toInt() - 1); + widget.replyItem.like -= $fixnum.Int64.ONE; widget.replyItem.replyControl.action = $fixnum.Int64.ZERO; } setState(() {}); @@ -115,16 +110,12 @@ class _ZanButtonGrpcState extends State { style: _style, onPressed: () => handleState(onHateReply), child: Icon( - widget.replyItem.replyControl.action.toInt() == 2 + isDislike ? FontAwesomeIcons.solidThumbsDown : FontAwesomeIcons.thumbsDown, size: 16, - color: widget.replyItem.replyControl.action.toInt() == 2 - ? primary - : color, - semanticLabel: widget.replyItem.replyControl.action.toInt() == 2 - ? '已踩' - : '点踩', + color: isDislike ? primary : color, + semanticLabel: isDislike ? '已踩' : '点踩', ), ), ), @@ -136,17 +127,12 @@ class _ZanButtonGrpcState extends State { child: Row( children: [ Icon( - widget.replyItem.replyControl.action.toInt() == 1 + isLike ? FontAwesomeIcons.solidThumbsUp : FontAwesomeIcons.thumbsUp, size: 16, - color: widget.replyItem.replyControl.action.toInt() == 1 - ? primary - : color, - semanticLabel: - widget.replyItem.replyControl.action.toInt() == 1 - ? '已赞' - : '点赞', + color: isLike ? primary : color, + semanticLabel: isLike ? '已赞' : '点赞', ), const SizedBox(width: 4), AnimatedSwitcher( @@ -158,9 +144,7 @@ class _ZanButtonGrpcState extends State { child: Text( Utils.numFormat(widget.replyItem.like.toInt()), style: TextStyle( - color: widget.replyItem.replyControl.action.toInt() == 1 - ? primary - : color, + color: isLike ? primary : color, fontSize: theme.textTheme.labelSmall!.fontSize, ), ), diff --git a/lib/pages/whisper/controller.dart b/lib/pages/whisper/controller.dart index 34f661805..84e8fd164 100644 --- a/lib/pages/whisper/controller.dart +++ b/lib/pages/whisper/controller.dart @@ -2,12 +2,11 @@ import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' show Offset, Session, SessionMainReply, SessionPageType, ThreeDotItem; import 'package:PiliPlus/grpc/im.dart'; import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/http/msg.dart'; import 'package:PiliPlus/models/msg/msgfeed_unread.dart'; import 'package:PiliPlus/pages/common/common_whisper_controller.dart'; import 'package:PiliPlus/utils/storage.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:protobuf/protobuf.dart' show PbMap; @@ -15,7 +14,8 @@ class WhisperController extends CommonWhisperController { @override SessionPageType sessionPageType = SessionPageType.SESSION_PAGE_TYPE_HOME; - late final List msgFeedTopItems; + late final List<({bool enabled, IconData icon, String name, String route})> + msgFeedTopItems; late final RxList unreadCounts; PbMap? offset; @@ -29,44 +29,46 @@ class WhisperController extends CommonWhisperController { final disableLikeMsg = GStorage.setting.get(SettingBoxKey.disableLikeMsg, defaultValue: false); msgFeedTopItems = [ - { - "name": "回复我的", - "icon": Icons.message_outlined, - "route": "/replyMe", - "enabled": true, - }, - { - "name": "@我", - "icon": Icons.alternate_email_outlined, - "route": "/atMe", - "enabled": true, - }, - { - "name": "收到的赞", - "icon": Icons.favorite_border_outlined, - "route": "/likeMe", - "enabled": !disableLikeMsg, - }, - { - "name": "系统通知", - "icon": Icons.notifications_none_outlined, - "route": "/sysMsg", - "enabled": true, - }, + const ( + name: "回复我的", + icon: Icons.message_outlined, + route: "/replyMe", + enabled: true, + ), + const ( + name: "@我", + icon: Icons.alternate_email_outlined, + route: "/atMe", + enabled: true, + ), + ( + name: "收到的赞", + icon: Icons.favorite_border_outlined, + route: "/likeMe", + enabled: !disableLikeMsg, + ), + const ( + name: "系统通知", + icon: Icons.notifications_none_outlined, + route: "/sysMsg", + enabled: true, + ), ]; - unreadCounts = - List.generate(msgFeedTopItems.length, (index) => 0).toList().obs; + unreadCounts = List.filled(msgFeedTopItems.length, 0).obs; queryMsgFeedUnread(); queryData(); } Future queryMsgFeedUnread() async { - var res = await MsgHttp.msgFeedUnread(); - if (res['status']) { - final data = MsgFeedUnread.fromJson(res['data']); - unreadCounts.value = [data.reply, data.at, data.like, data.sysMsg]; + var res = await ImGrpc.getTotalUnread(unreadType: 2); + if (res.isSuccess) { + final data = MsgFeedUnread.fromJson(res.data.msgFeedUnread.unread); + final unreadCounts = [data.reply, data.at, data.like, data.sysMsg]; + if (!listEquals(this.unreadCounts, unreadCounts)) { + this.unreadCounts.value = unreadCounts; + } } else { - SmartDialog.showToast(res['msg']); + res.toast(); } } diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index 9938c66b4..9f6949150 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -155,7 +155,7 @@ class _WhisperPageState extends State { radius: 22, backgroundColor: theme.colorScheme.onInverseSurface, child: Icon( - _controller.msgFeedTopItems[index]['icon'], + _controller.msgFeedTopItems[index].icon, size: 20, color: theme.colorScheme.primary, ), @@ -164,20 +164,20 @@ class _WhisperPageState extends State { ), const SizedBox(height: 6), Text( - _controller.msgFeedTopItems[index]['name'], + _controller.msgFeedTopItems[index].name, style: const TextStyle(fontSize: 13), ), ], ), ), onTap: () { - if (!_controller.msgFeedTopItems[index]['enabled']) { + if (!_controller.msgFeedTopItems[index].enabled) { SmartDialog.showToast('已禁用'); return; } _controller.unreadCounts[index] = 0; Get.toNamed( - _controller.msgFeedTopItems[index]['route'], + _controller.msgFeedTopItems[index].route, ); }, ); diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index b92307e00..a284dd3b4 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -20,8 +20,7 @@ import 'package:get/get.dart'; class ChatItem extends StatelessWidget { static MsgType msgTypeFromValue(int value) { - return MsgType.values.firstWhere((e) => e.value == value, - orElse: () => MsgType.EN_INVALID_MSG_TYPE); + return MsgType.valueOf(value) ?? MsgType.EN_INVALID_MSG_TYPE; } const ChatItem({ diff --git a/lib/utils/extension.dart b/lib/utils/extension.dart index 854b120ba..8bfb9061e 100644 --- a/lib/utils/extension.dart +++ b/lib/utils/extension.dart @@ -57,23 +57,6 @@ extension ListExt on List? { T getOrElse(int index, {required T Function() orElse}) { return getOrNull(index) ?? orElse(); } - - bool eq(List? other) { - if (this == null) { - return other == null; - } - if (other == null || this!.length != other.length) { - return false; - } - for (int index = 0; index < this!.length; index += 1) { - if (this![index] != other[index]) { - return false; - } - } - return true; - } - - bool ne(List? other) => !eq(other); } final _regExp = RegExp("^(http:)?//", caseSensitive: false); @@ -128,17 +111,6 @@ extension ColorExtension on Color { assert(amount >= 0 && amount <= 1, 'Amount must be between 0 and 1'); return Color.lerp(this, Colors.black, amount)!; } - - Color blend(Color color, [double fraction = 0.5]) { - assert(fraction >= 0 && fraction <= 1, 'Fraction must be between 0 and 1'); - final blendedRed = (red * (1 - fraction) + color.red * fraction).toInt(); - final blendedGreen = - (green * (1 - fraction) + color.green * fraction).toInt(); - final blendedBlue = (blue * (1 - fraction) + color.blue * fraction).toInt(); - final blendedAlpha = - (alpha * (1 - fraction) + color.alpha * fraction).toInt(); - return Color.fromARGB(blendedAlpha, blendedRed, blendedGreen, blendedBlue); - } } extension BrightnessExt on Brightness {