opt: unread & zan grpc & readlist open with browser (#852)

* opt: unread

* opt: zan grpc

* feat: readlist open with browser
This commit is contained in:
My-Responsitories
2025-05-11 18:58:00 +08:00
committed by GitHub
parent 8d34e6f340
commit 72734d4b4e
13 changed files with 127 additions and 208 deletions

View File

@@ -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<ArticleListPage> {
),
),
),
actions: [
IconButton(
tooltip: '浏览器打开',
onPressed: () {
PageUtils.inAppWebview(
'${HttpString.baseUrl}/read/readlist/rl${_controller.id}');
},
icon: const Icon(Icons.open_in_browser_outlined, size: 19),
)
],
);
}
}

View File

@@ -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<void> getUnreadDynamic() async {

View File

@@ -22,13 +22,16 @@ class ZanButtonGrpc extends StatefulWidget {
}
class _ZanButtonGrpcState extends State<ZanButtonGrpc> {
bool get isLike => widget.replyItem.replyControl.action == $fixnum.Int64.ONE;
bool get isDislike =>
widget.replyItem.replyControl.action == $fixnum.Int64.TWO;
Future<void> 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<ZanButtonGrpc> {
);
// 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<ZanButtonGrpc> {
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<ZanButtonGrpc> {
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<ZanButtonGrpc> {
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<ZanButtonGrpc> {
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<ZanButtonGrpc> {
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,
),
),

View File

@@ -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<SessionMainReply> {
@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<int> unreadCounts;
PbMap<int, Offset>? offset;
@@ -29,44 +29,46 @@ class WhisperController extends CommonWhisperController<SessionMainReply> {
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<void> 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();
}
}

View File

@@ -155,7 +155,7 @@ class _WhisperPageState extends State<WhisperPage> {
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<WhisperPage> {
),
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,
);
},
);

View File

@@ -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({