feat: home: show unread badge

Closes #107

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-01-07 17:00:58 +08:00
parent 30a5889215
commit c1ce704e4e
10 changed files with 484 additions and 367 deletions

View File

@@ -1,10 +1,13 @@
import 'dart:async';
import 'package:PiliPlus/grpc/grpc_repo.dart';
import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/http/common.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/pages/dynamics/view.dart';
import 'package:PiliPlus/pages/home/view.dart';
import 'package:PiliPlus/pages/media/view.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
@@ -21,40 +24,135 @@ class MainController extends GetxController {
late bool hideTabBar;
late PageController pageController;
int selectedIndex = 0;
RxBool userLogin = false.obs;
late DynamicBadgeMode dynamicBadgeType;
late bool checkDynamic;
late int dynamicPeriod;
int? _lastCheckAt;
int? dynIndex;
RxBool isLogin = false.obs;
late DynamicBadgeMode dynamicBadgeMode;
late bool checkDynamic = GStorage.checkDynamic;
late int dynamicPeriod = GStorage.dynamicPeriod;
late int _lastCheckDynamicAt = 0;
late int dynIndex = -1;
late int homeIndex = -1;
late DynamicBadgeMode msgBadgeMode = GStorage.msgBadgeMode;
late MsgUnReadType msgUnReadType = GStorage.msgUnReadType;
late final RxString msgUnReadCount = ''.obs;
late int lastCheckUnreadAt = 0;
@override
void onInit() {
super.onInit();
checkDynamic = GStorage.checkDynamic;
dynamicPeriod = GStorage.dynamicPeriod;
hideTabBar =
GStorage.setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
dynamic userInfo = GStorage.userInfo.get('userInfoCache');
userLogin.value = userInfo != null;
dynamicBadgeType = DynamicBadgeMode.values[GStorage.setting.get(
isLogin.value = GStorage.isLogin;
dynamicBadgeMode = DynamicBadgeMode.values[GStorage.setting.get(
SettingBoxKey.dynamicBadgeMode,
defaultValue: DynamicBadgeMode.number.code)];
defaultValue: DynamicBadgeMode.number.index)];
setNavBarConfig();
if (dynamicBadgeType != DynamicBadgeMode.hidden) {
dynIndex = navigationBars.indexWhere((e) => e['id'] == 1);
dynIndex = navigationBars.indexWhere((e) => e['id'] == 1);
if (dynamicBadgeMode != DynamicBadgeMode.hidden) {
if (dynIndex != -1) {
if (checkDynamic) {
_lastCheckAt = DateTime.now().millisecondsSinceEpoch;
_lastCheckDynamicAt = DateTime.now().millisecondsSinceEpoch;
}
getUnreadDynamic();
}
}
homeIndex = navigationBars.indexWhere((e) => e['id'] == 0);
if (msgBadgeMode != DynamicBadgeMode.hidden) {
if (homeIndex != -1) {
lastCheckUnreadAt = DateTime.now().millisecondsSinceEpoch;
queryUnreadMsg();
}
}
}
Future queryUnreadMsg() async {
if (isLogin.value.not || homeIndex == -1) {
return;
}
try {
bool shouldCheckPM = msgUnReadType == MsgUnReadType.pm ||
msgUnReadType == MsgUnReadType.all;
bool shouldCheckFeed = msgUnReadType != MsgUnReadType.pm ||
msgUnReadType == MsgUnReadType.all;
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;
count += (switch (msgUnReadType) {
MsgUnReadType.pm => 0,
MsgUnReadType.reply => res[index]['data']['reply'],
MsgUnReadType.at => res[index]['data']['at'],
MsgUnReadType.like => res[index]['data']['like'],
MsgUnReadType.sysMsg => res[index]['data']['sys_msg'],
MsgUnReadType.all => res[index]['data']['reply'] +
res[index]['data']['at'] +
res[index]['data']['like'] +
res[index]['data']['sys_msg'],
} as int?) ??
0;
}
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 {
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'],
};
}
}
Future _queryMsgFeedUnread() async {
if (isLogin.value.not) {
return;
}
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'],
};
}
}
void getUnreadDynamic() async {
if (!userLogin.value || dynIndex == -1) {
if (!isLogin.value || dynIndex == -1) {
return;
}
if (GlobalData().grpcReply) {
@@ -73,22 +171,21 @@ class MainController extends GetxController {
}
void setCount([int count = 0]) async {
dynIndex ??= navigationBars.indexWhere((e) => e['id'] == 1);
if (dynIndex == -1 || navigationBars[dynIndex!]['count'] == count) return;
navigationBars[dynIndex!]['count'] = count; // 修改 count 属性为新的值
if (dynIndex == -1 || navigationBars[dynIndex]['count'] == count) return;
navigationBars[dynIndex]['count'] = count; // 修改 count 属性为新的值
navigationBars.refresh();
}
void checkUnreadDynamic() {
if (dynIndex == -1 ||
!userLogin.value ||
dynamicBadgeType == DynamicBadgeMode.hidden ||
!isLogin.value ||
dynamicBadgeMode == DynamicBadgeMode.hidden ||
!checkDynamic) {
return;
}
int now = DateTime.now().millisecondsSinceEpoch;
if (now - (_lastCheckAt ?? 0) >= dynamicPeriod * 60 * 1000) {
_lastCheckAt = now;
if (now - _lastCheckDynamicAt >= dynamicPeriod * 60 * 1000) {
_lastCheckDynamicAt = now;
getUnreadDynamic();
}
}

View File

@@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/grpc/grpc_client.dart';
import 'package:PiliPlus/pages/mine/controller.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -12,6 +14,7 @@ import 'package:PiliPlus/pages/home/index.dart';
import 'package:PiliPlus/utils/event_bus.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import './controller.dart';
class MainApp extends StatefulWidget {
@@ -27,8 +30,8 @@ class MainApp extends StatefulWidget {
class _MainAppState extends State<MainApp>
with SingleTickerProviderStateMixin, RouteAware, WidgetsBindingObserver {
final MainController _mainController = Get.put(MainController());
final HomeController _homeController = Get.put(HomeController());
final DynamicsController _dynamicController = Get.put(DynamicsController());
late final _homeController = Get.put(HomeController());
late final _dynamicController = Get.put(DynamicsController());
int? _lastSelectTime; //上次点击时间
late bool enableMYBar;
@@ -57,6 +60,7 @@ class _MainAppState extends State<MainApp>
void didPopNext() {
_mainController.checkUnreadDynamic();
_checkDefaultSearch(true);
_checkUnread(true);
super.didPopNext();
}
@@ -65,23 +69,40 @@ class _MainAppState extends State<MainApp>
if (state == AppLifecycleState.resumed) {
_mainController.checkUnreadDynamic();
_checkDefaultSearch(true);
_checkUnread(true);
}
}
void _checkDefaultSearch([bool shouldCheck = false]) {
if (_homeController.enableSearchWord) {
if (_mainController.homeIndex != -1 && _homeController.enableSearchWord) {
if (shouldCheck &&
_mainController.pages[_mainController.selectedIndex] is! HomePage) {
return;
}
int now = DateTime.now().millisecondsSinceEpoch;
if (now - _homeController.lateCheckAt >= 5 * 60 * 1000) {
_homeController.lateCheckAt = now;
if (now - _homeController.lateCheckSearchAt >= 5 * 60 * 1000) {
_homeController.lateCheckSearchAt = now;
_homeController.querySearchDefault();
}
}
}
void _checkUnread([bool shouldCheck = false]) {
if (_mainController.isLogin.value &&
_mainController.homeIndex != -1 &&
_mainController.msgBadgeMode != DynamicBadgeMode.hidden) {
if (shouldCheck &&
_mainController.pages[_mainController.selectedIndex] is! HomePage) {
return;
}
int now = DateTime.now().millisecondsSinceEpoch;
if (now - _mainController.lastCheckUnreadAt >= 5 * 60 * 1000) {
_mainController.lastCheckUnreadAt = now;
_mainController.queryUnreadMsg();
}
}
}
void setIndex(int value) async {
feedBack();
_mainController.pageController.jumpToPage(value);
@@ -98,25 +119,11 @@ class _MainAppState extends State<MainApp>
}
_homeController.flag = true;
_checkDefaultSearch();
_checkUnread();
} else {
_homeController.flag = false;
}
// if (currentPage is RankPage) {
// if (_rankController.flag) {
// // 单击返回顶部 双击并刷新
// if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {
// _rankController.onRefresh();
// } else {
// _rankController.animateToTop();
// }
// _lastSelectTime = DateTime.now().millisecondsSinceEpoch;
// }
// _rankController.flag = true;
// } else {
// _rankController.flag = false;
// }
if (currentPage is DynamicsPage) {
if (_dynamicController.flag) {
// 单击返回顶部 双击并刷新
@@ -172,8 +179,8 @@ class _MainAppState extends State<MainApp>
children: [
if (useSideBar) ...[
SizedBox(
width: context.width * 0.0387 +
36.801 +
width: context.width * 0.04 +
40 +
MediaQuery.of(context).padding.left,
child: Obx(
() => _mainController.navigationBars.length > 1
@@ -184,8 +191,7 @@ class _MainAppState extends State<MainApp>
selectedIndex: _mainController.selectedIndex,
onDestinationSelected: setIndex,
labelType: NavigationRailLabelType.none,
leading:
UserAndSearchVertical(ctr: _homeController),
leading: userAndSearchVertical,
destinations: _mainController.navigationBars
.map((e) => NavigationRailDestination(
icon: _buildIcon(
@@ -211,8 +217,7 @@ class _MainAppState extends State<MainApp>
constraints: BoxConstraints(
minWidth: context.width * 0.0286 + 28.56,
),
child:
UserAndSearchVertical(ctr: _homeController),
child: userAndSearchVertical,
),
),
),
@@ -352,10 +357,10 @@ class _MainAppState extends State<MainApp>
required Widget icon,
}) =>
id == 1 &&
_mainController.dynamicBadgeType != DynamicBadgeMode.hidden &&
_mainController.dynamicBadgeMode != DynamicBadgeMode.hidden &&
count > 0
? Badge(
label: _mainController.dynamicBadgeType == DynamicBadgeMode.number
label: _mainController.dynamicBadgeMode == DynamicBadgeMode.number
? Text(count.toString())
: null,
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
@@ -367,4 +372,119 @@ class _MainAppState extends State<MainApp>
child: icon,
)
: icon;
Widget get userAndSearchVertical {
return Column(
children: [
Semantics(
label: "我的",
child: Obx(
() => _homeController.userLogin.value
? Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
type: 'avatar',
width: 34,
height: 34,
src: _homeController.userFace.value,
),
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () =>
_homeController.showUserInfoDialog(context),
splashColor: Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.3),
borderRadius: const BorderRadius.all(
Radius.circular(50),
),
),
),
),
Positioned(
right: -6,
bottom: -6,
child: Obx(() => MineController.anonymity.value
? IgnorePointer(
child: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.secondaryContainer,
shape: BoxShape.circle,
),
child: Icon(
size: 16,
MdiIcons.incognito,
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
),
),
)
: const SizedBox.shrink()),
),
],
)
: DefaultUser(
onPressed: () =>
_homeController.showUserInfoDialog(context)),
),
),
const SizedBox(height: 8),
Obx(
() => _homeController.userLogin.value
? Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
IconButton(
tooltip: '消息',
onPressed: () {
Get.toNamed('/whisper');
_mainController.msgUnReadCount.value = '';
},
icon: const Icon(
Icons.notifications_none,
),
),
if (_mainController.msgBadgeMode !=
DynamicBadgeMode.hidden &&
_mainController.msgUnReadCount.value.isNotEmpty)
Positioned(
top: _mainController.msgBadgeMode ==
DynamicBadgeMode.number
? 8
: 12,
left: _mainController.msgBadgeMode ==
DynamicBadgeMode.number
? 24
: 32,
child: Badge(
label: _mainController.msgBadgeMode ==
DynamicBadgeMode.number
? Text(_mainController.msgUnReadCount.value
.toString())
: null,
),
),
],
)
: const SizedBox.shrink(),
),
IconButton(
icon: const Icon(
Icons.search_outlined,
semanticLabel: '搜索',
),
onPressed: () => Get.toNamed('/search'),
),
],
);
}
}