diff --git a/lib/http/api.dart b/lib/http/api.dart index d0a807919..8d99b5f5d 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -444,6 +444,11 @@ class Api { // 获取指定分组下的up static const String followUpGroup = '/x/relation/tag'; + // 获取未读私信数 + // 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 msgFeedUnread = '/x/msgfeed/unread'; //https://api.bilibili.com/x/msgfeed/reply?platform=web&build=0&mobi_app=web diff --git a/lib/models/common/dynamic_badge_mode.dart b/lib/models/common/dynamic_badge_mode.dart index 2609c5e24..a49e90861 100644 --- a/lib/models/common/dynamic_badge_mode.dart +++ b/lib/models/common/dynamic_badge_mode.dart @@ -4,6 +4,8 @@ extension DynamicBadgeModeDesc on DynamicBadgeMode { String get description => ['隐藏', '红点', '数字'][index]; } -extension DynamicBadgeModeCode on DynamicBadgeMode { - int get code => [0, 1, 2][index]; +enum MsgUnReadType { pm, reply, at, like, sysMsg, all } + +extension MsgUnReadTypeExt on MsgUnReadType { + String get title => ['私信', '回复我的', '@我', '收到的赞', '系统通知', '全部'][index]; } diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index 1d1291baf..d261a0192 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -27,7 +27,7 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { late bool enableSearchWord; late RxString defaultSearch = ''.obs; - late int lateCheckAt = 0; + late int lateCheckSearchAt = 0; @override void onInit() { @@ -40,7 +40,7 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { enableSearchWord = GStorage.setting .get(SettingBoxKey.enableSearchWord, defaultValue: true); if (enableSearchWord) { - lateCheckAt = DateTime.now().millisecondsSinceEpoch; + lateCheckSearchAt = DateTime.now().millisecondsSinceEpoch; querySearchDefault(); } useSideBar = diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index e2c3c70a7..643ca9f43 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:PiliPlus/models/common/dynamic_badge_mode.dart'; +import 'package:PiliPlus/pages/main/index.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:flutter/material.dart'; @@ -20,6 +22,7 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { final HomeController _homeController = Get.put(HomeController()); late Stream stream; + final MainController _mainController = Get.put(MainController()); @override bool get wantKeepAlive => true; @@ -37,13 +40,7 @@ class _HomePageState extends State appBar: AppBar(toolbarHeight: 0), body: Column( children: [ - if (!_homeController.useSideBar) - CustomAppBar( - stream: _homeController.hideSearchBar - ? stream - : StreamController.broadcast().stream, - homeController: _homeController, - ), + if (!_homeController.useSideBar) customAppBar, if (_homeController.tabs.length > 1) ...[ ...[ const SizedBox(height: 4), @@ -85,77 +82,57 @@ class _HomePageState extends State ), ); } -} -class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { - final double height; - final Stream? stream; - final HomeController homeController; - - const CustomAppBar({ - super.key, - this.height = kToolbarHeight, - this.stream, - required this.homeController, - }); - - @override - Size get preferredSize => Size.fromHeight(height); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: stream, - initialData: true, - builder: (BuildContext context, AsyncSnapshot snapshot) { - return AnimatedOpacity( - opacity: snapshot.data ? 1 : 0, - duration: const Duration(milliseconds: 300), - child: AnimatedContainer( - curve: Curves.easeInOutCubicEmphasized, - duration: const Duration(milliseconds: 500), - height: snapshot.data ? 52 : 0, - padding: const EdgeInsets.fromLTRB(14, 6, 14, 0), - child: SearchBarAndUser( - homeController: homeController, - ), - ), - ); - }, - ); - } -} - -class SearchBarAndUser extends StatelessWidget { - const SearchBarAndUser({ - super.key, - required this.homeController, - }); - - final HomeController homeController; - - @override - Widget build(BuildContext context) { + Widget get searchBarAndUser { return Row( children: [ - SearchBar(homeController: homeController), + searchBar, const SizedBox(width: 4), - Obx(() => homeController.userLogin.value - ? ClipRect( - child: IconButton( - tooltip: '消息', - onPressed: () => Get.toNamed('/whisper'), - icon: const Icon( - Icons.notifications_none, - ), - ), - ) - : const SizedBox.shrink()), + 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(), + ), const SizedBox(width: 8), Semantics( label: "我的", child: Obx( - () => homeController.userLogin.value + () => _homeController.userLogin.value ? Stack( clipBehavior: Clip.none, children: [ @@ -163,14 +140,14 @@ class SearchBarAndUser extends StatelessWidget { type: 'avatar', width: 34, height: 34, - src: homeController.userFace.value, + src: _homeController.userFace.value, ), Positioned.fill( child: Material( color: Colors.transparent, child: InkWell( onTap: () => - homeController.showUserInfoDialog(context), + _homeController.showUserInfoDialog(context), splashColor: Theme.of(context) .colorScheme .primaryContainer @@ -208,100 +185,89 @@ class SearchBarAndUser extends StatelessWidget { ], ) : DefaultUser( - onPressed: () => homeController.showUserInfoDialog(context), + onPressed: () => + _homeController.showUserInfoDialog(context), ), ), ), ], ); } -} -class UserAndSearchVertical extends StatelessWidget { - const UserAndSearchVertical({ - super.key, - required this.ctr, - }); + Widget get customAppBar { + return StreamBuilder( + stream: _homeController.hideSearchBar + ? stream + : StreamController.broadcast().stream, + initialData: true, + builder: (BuildContext context, AsyncSnapshot snapshot) { + return AnimatedOpacity( + opacity: snapshot.data ? 1 : 0, + duration: const Duration(milliseconds: 300), + child: AnimatedContainer( + curve: Curves.easeInOutCubicEmphasized, + duration: const Duration(milliseconds: 500), + height: snapshot.data ? 52 : 0, + padding: const EdgeInsets.fromLTRB(14, 6, 14, 0), + child: searchBarAndUser, + ), + ); + }, + ); + } - final HomeController ctr; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Semantics( - label: "我的", - child: Obx( - () => ctr.userLogin.value - ? Stack( - clipBehavior: Clip.none, - children: [ - NetworkImgLayer( - type: 'avatar', - width: 34, - height: 34, - src: ctr.userFace.value, + Widget get searchBar { + return Expanded( + child: Container( + width: 250, + height: 44, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + ), + child: Material( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer + .withOpacity(0.05), + child: InkWell( + splashColor: + Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), + onTap: () => Get.toNamed( + '/search', + parameters: { + if (_homeController.enableSearchWord) + 'hintText': _homeController.defaultSearch.value, + }, + ), + child: Row( + children: [ + const SizedBox(width: 14), + Icon( + Icons.search_outlined, + color: Theme.of(context).colorScheme.onSecondaryContainer, + semanticLabel: '搜索', + ), + const SizedBox(width: 10), + if (_homeController.enableSearchWord) ...[ + Expanded( + child: Obx( + () => Text( + _homeController.defaultSearch.value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).colorScheme.outline), ), - Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => ctr.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: () => ctr.showUserInfoDialog(context)), + ), + ), + const SizedBox(width: 2), + ], + ], + ), ), ), - const SizedBox(height: 8), - Obx(() => ctr.userLogin.value - ? IconButton( - tooltip: '消息', - onPressed: () => Get.toNamed('/whisper'), - icon: const Icon(Icons.notifications_none), - ) - : const SizedBox.shrink()), - IconButton( - icon: const Icon( - Icons.search_outlined, - semanticLabel: '搜索', - ), - onPressed: () => Get.toNamed('/search'), - ), - ], + ), ); } } @@ -333,154 +299,3 @@ class DefaultUser extends StatelessWidget { ); } } - -// class CustomTabs extends StatefulWidget { -// const CustomTabs({super.key}); - -// @override -// State createState() => _CustomTabsState(); -// } - -// class _CustomTabsState extends State { -// final HomeController _homeController = Get.put(HomeController()); - -// void onTap(int index) { -// feedBack(); -// if (_homeController.initialIndex.value == index) { -// _homeController.tabsCtrList[index]().animateToTop(); -// } -// _homeController.initialIndex.value = index; -// _homeController.tabController.index = index; -// } - -// @override -// Widget build(BuildContext context) { -// return Container( -// height: 44, -// margin: const EdgeInsets.only(top: 4), -// child: Obx( -// () => ListView.separated( -// padding: const EdgeInsets.symmetric(horizontal: 14.0), -// scrollDirection: Axis.horizontal, -// itemCount: _homeController.tabs.length, -// separatorBuilder: (BuildContext context, int index) { -// return const SizedBox(width: 10); -// }, -// itemBuilder: (BuildContext context, int index) { -// String label = _homeController.tabs[index]['label']; -// return Obx( -// () => CustomChip( -// onTap: () => onTap(index), -// label: label, -// selected: index == _homeController.initialIndex.value, -// ), -// ); -// }, -// ), -// ), -// ); -// } -// } - -class CustomChip extends StatelessWidget { - final VoidCallback onTap; - final String label; - final bool selected; - const CustomChip({ - super.key, - required this.onTap, - required this.label, - required this.selected, - }); - - @override - Widget build(BuildContext context) { - final ColorScheme colorTheme = Theme.of(context).colorScheme; - final TextStyle chipTextStyle = selected - ? const TextStyle(fontWeight: FontWeight.bold, fontSize: 13) - : const TextStyle(fontSize: 13); - final ColorScheme colorScheme = Theme.of(context).colorScheme; - const VisualDensity visualDensity = - VisualDensity(horizontal: -4.0, vertical: -2.0); - return InputChip( - side: selected - ? BorderSide( - color: colorScheme.secondary.withOpacity(0.2), - width: 2, - ) - : BorderSide.none, - // backgroundColor: colorTheme.primaryContainer.withOpacity(0.1), - // selectedColor: colorTheme.secondaryContainer.withOpacity(0.8), - color: WidgetStateProperty.resolveWith((Set states) { - return colorTheme.secondaryContainer.withOpacity(0.6); - }), - padding: const EdgeInsets.fromLTRB(6, 1, 6, 1), - label: Text(label, style: chipTextStyle), - onPressed: onTap, - selected: selected, - showCheckmark: false, - visualDensity: visualDensity, - ); - } -} - -class SearchBar extends StatelessWidget { - const SearchBar({ - super.key, - required this.homeController, - }); - - final HomeController homeController; - - @override - Widget build(BuildContext context) { - final ColorScheme colorScheme = Theme.of(context).colorScheme; - return Expanded( - child: Container( - width: 250, - height: 44, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(25), - ), - child: Material( - color: colorScheme.onSecondaryContainer.withOpacity(0.05), - child: InkWell( - splashColor: colorScheme.primaryContainer.withOpacity(0.3), - onTap: () => Get.toNamed( - '/search', - parameters: { - if (homeController.enableSearchWord) - 'hintText': homeController.defaultSearch.value, - }, - ), - child: Row( - children: [ - const SizedBox(width: 14), - Icon( - Icons.search_outlined, - color: colorScheme.onSecondaryContainer, - semanticLabel: '搜索', - ), - const SizedBox(width: 10), - if (homeController.enableSearchWord) ...[ - Expanded( - child: Obx( - () => Text( - homeController.defaultSearch.value, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: colorScheme.outline), - ), - ), - ), - const SizedBox(width: 2), - ], - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index c4084819a..1a87fbaf5 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -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(); } } diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index c7b1bdcdf..4243c47a7 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -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 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 void didPopNext() { _mainController.checkUnreadDynamic(); _checkDefaultSearch(true); + _checkUnread(true); super.didPopNext(); } @@ -65,23 +69,40 @@ class _MainAppState extends State 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 } _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 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 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 constraints: BoxConstraints( minWidth: context.width * 0.0286 + 28.56, ), - child: - UserAndSearchVertical(ctr: _homeController), + child: userAndSearchVertical, ), ), ), @@ -352,10 +357,10 @@ class _MainAppState extends State 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 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'), + ), + ], + ); + } } diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart index 4e8923273..3b0ca6c51 100644 --- a/lib/pages/setting/view.dart +++ b/lib/pages/setting/view.dart @@ -125,7 +125,7 @@ class SettingPage extends StatelessWidget { if (Get.isRegistered()) { MainController mainController = Get.find(); - mainController.userLogin.value = false; + mainController.isLogin.value = false; } await LoginUtils.refreshLoginStatus(false); Get.back(); diff --git a/lib/pages/setting/widgets/model.dart b/lib/pages/setting/widgets/model.dart index fb529c844..07ff751b0 100644 --- a/lib/pages/setting/widgets/model.dart +++ b/lib/pages/setting/widgets/model.dart @@ -235,11 +235,11 @@ List get styleSettings => [ }, ); if (result != null) { - GStorage.setting.put(SettingBoxKey.dynamicBadgeMode, result.code); + GStorage.setting.put(SettingBoxKey.dynamicBadgeMode, result.index); MainController mainController = Get.put(MainController()); - mainController.dynamicBadgeType = - DynamicBadgeMode.values[result.code]; - if (mainController.dynamicBadgeType != DynamicBadgeMode.hidden) { + mainController.dynamicBadgeMode = + DynamicBadgeMode.values[result.index]; + if (mainController.dynamicBadgeMode != DynamicBadgeMode.hidden) { mainController.getUnreadDynamic(); } SmartDialog.showToast('设置成功'); @@ -250,6 +250,68 @@ List get styleSettings => [ leading: const Icon(Icons.motion_photos_on_outlined), getSubtitle: () => '当前标记样式:${GStorage.dynamicBadgeType.description}', ), + SettingsModel( + settingsType: SettingsType.normal, + onTap: (setState) async { + DynamicBadgeMode? result = await showDialog( + context: Get.context!, + builder: (context) { + return SelectDialog( + title: '消息未读标记', + value: GStorage.msgBadgeMode, + values: DynamicBadgeMode.values.map((e) { + return {'title': e.description, 'value': e}; + }).toList(), + ); + }, + ); + if (result != null) { + GStorage.setting.put(SettingBoxKey.msgBadgeMode, result.index); + MainController mainController = Get.put(MainController()); + mainController.msgBadgeMode = DynamicBadgeMode.values[result.index]; + if (mainController.msgBadgeMode != DynamicBadgeMode.hidden) { + mainController.queryUnreadMsg(); + } else { + mainController.msgUnReadCount.value = ''; + } + SmartDialog.showToast('设置成功'); + setState(); + } + }, + title: '消息未读标记', + leading: const Icon(Icons.notifications_active_outlined), + getSubtitle: () => '当前标记样式:${GStorage.msgBadgeMode.description}', + ), + SettingsModel( + settingsType: SettingsType.normal, + onTap: (setState) async { + MsgUnReadType? result = await showDialog( + context: Get.context!, + builder: (context) { + return SelectDialog( + title: '消息未读类型', + value: GStorage.msgUnReadType, + values: MsgUnReadType.values.map((e) { + return {'title': e.title, 'value': e}; + }).toList(), + ); + }, + ); + if (result != null) { + GStorage.setting.put(SettingBoxKey.msgUnReadType, result.index); + MainController mainController = Get.put(MainController()); + mainController.msgUnReadType = MsgUnReadType.values[result.index]; + if (mainController.msgBadgeMode != DynamicBadgeMode.hidden) { + mainController.queryUnreadMsg(); + } + SmartDialog.showToast('设置成功'); + setState(); + } + }, + title: '消息未读类型', + leading: const Icon(Icons.notifications_active_outlined), + getSubtitle: () => '当前消息类型:${GStorage.msgUnReadType.title}', + ), SettingsModel( settingsType: SettingsType.sw1tch, title: '首页顶栏收起', diff --git a/lib/utils/extension.dart b/lib/utils/extension.dart index 5e4c499a1..78e4d77f6 100644 --- a/lib/utils/extension.dart +++ b/lib/utils/extension.dart @@ -28,6 +28,9 @@ extension ListExt on List? { if (isNullOrEmpty) { return null; } + if (index < 0 || index >= this!.length) { + return null; + } return this![index]; } diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index d015e8cd5..6644b70cf 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -90,7 +90,18 @@ class GStorage { static DynamicBadgeMode get dynamicBadgeType => DynamicBadgeMode.values[setting.get( SettingBoxKey.dynamicBadgeMode, - defaultValue: DynamicBadgeMode.number.code, + defaultValue: DynamicBadgeMode.number.index, + )]; + + static DynamicBadgeMode get msgBadgeMode => + DynamicBadgeMode.values[setting.get( + SettingBoxKey.msgBadgeMode, + defaultValue: DynamicBadgeMode.number.index, + )]; + + static MsgUnReadType get msgUnReadType => MsgUnReadType.values[setting.get( + SettingBoxKey.msgUnReadType, + defaultValue: MsgUnReadType.pm.index, )]; static int get defaultHomePage => @@ -551,6 +562,8 @@ class SettingBoxKey { hideTabBar = 'hideTabBar', // 收起底栏 tabbarSort = 'tabbarSort', // 首页tabbar dynamicBadgeMode = 'dynamicBadgeMode', + msgBadgeMode = 'msgBadgeMode', + msgUnReadType = 'msgUnReadType', hiddenSettingUnlocked = 'hiddenSettingUnlocked', enableGradientBg = 'enableGradientBg', navBarSort = 'navBarSort';