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

@@ -444,6 +444,11 @@ class Api {
// 获取指定分组下的up // 获取指定分组下的up
static const String followUpGroup = '/x/relation/tag'; 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'; static const String msgFeedUnread = '/x/msgfeed/unread';
//https://api.bilibili.com/x/msgfeed/reply?platform=web&build=0&mobi_app=web //https://api.bilibili.com/x/msgfeed/reply?platform=web&build=0&mobi_app=web

View File

@@ -4,6 +4,8 @@ extension DynamicBadgeModeDesc on DynamicBadgeMode {
String get description => ['隐藏', '红点', '数字'][index]; String get description => ['隐藏', '红点', '数字'][index];
} }
extension DynamicBadgeModeCode on DynamicBadgeMode { enum MsgUnReadType { pm, reply, at, like, sysMsg, all }
int get code => [0, 1, 2][index];
extension MsgUnReadTypeExt on MsgUnReadType {
String get title => ['私信', '回复我的', '@我', '收到的赞', '系统通知', '全部'][index];
} }

View File

@@ -27,7 +27,7 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
late bool enableSearchWord; late bool enableSearchWord;
late RxString defaultSearch = ''.obs; late RxString defaultSearch = ''.obs;
late int lateCheckAt = 0; late int lateCheckSearchAt = 0;
@override @override
void onInit() { void onInit() {
@@ -40,7 +40,7 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
enableSearchWord = GStorage.setting enableSearchWord = GStorage.setting
.get(SettingBoxKey.enableSearchWord, defaultValue: true); .get(SettingBoxKey.enableSearchWord, defaultValue: true);
if (enableSearchWord) { if (enableSearchWord) {
lateCheckAt = DateTime.now().millisecondsSinceEpoch; lateCheckSearchAt = DateTime.now().millisecondsSinceEpoch;
querySearchDefault(); querySearchDefault();
} }
useSideBar = useSideBar =

View File

@@ -1,5 +1,7 @@
import 'dart:async'; 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/pages/mine/controller.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -20,6 +22,7 @@ class _HomePageState extends State<HomePage>
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
final HomeController _homeController = Get.put(HomeController()); final HomeController _homeController = Get.put(HomeController());
late Stream<bool> stream; late Stream<bool> stream;
final MainController _mainController = Get.put(MainController());
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@@ -37,13 +40,7 @@ class _HomePageState extends State<HomePage>
appBar: AppBar(toolbarHeight: 0), appBar: AppBar(toolbarHeight: 0),
body: Column( body: Column(
children: [ children: [
if (!_homeController.useSideBar) if (!_homeController.useSideBar) customAppBar,
CustomAppBar(
stream: _homeController.hideSearchBar
? stream
: StreamController<bool>.broadcast().stream,
homeController: _homeController,
),
if (_homeController.tabs.length > 1) ...[ if (_homeController.tabs.length > 1) ...[
...[ ...[
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -85,77 +82,57 @@ class _HomePageState extends State<HomePage>
), ),
); );
} }
}
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { Widget get searchBarAndUser {
final double height;
final Stream<bool>? 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) {
return Row( return Row(
children: [ children: [
SearchBar(homeController: homeController), searchBar,
const SizedBox(width: 4), const SizedBox(width: 4),
Obx(() => homeController.userLogin.value Obx(
? ClipRect( () => _homeController.userLogin.value
child: IconButton( ? Stack(
tooltip: '消息', clipBehavior: Clip.none,
onPressed: () => Get.toNamed('/whisper'), alignment: Alignment.center,
icon: const Icon( children: [
Icons.notifications_none, IconButton(
), tooltip: '消息',
), onPressed: () {
) Get.toNamed('/whisper');
: const SizedBox.shrink()), _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), const SizedBox(width: 8),
Semantics( Semantics(
label: "我的", label: "我的",
child: Obx( child: Obx(
() => homeController.userLogin.value () => _homeController.userLogin.value
? Stack( ? Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
@@ -163,14 +140,14 @@ class SearchBarAndUser extends StatelessWidget {
type: 'avatar', type: 'avatar',
width: 34, width: 34,
height: 34, height: 34,
src: homeController.userFace.value, src: _homeController.userFace.value,
), ),
Positioned.fill( Positioned.fill(
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: () => onTap: () =>
homeController.showUserInfoDialog(context), _homeController.showUserInfoDialog(context),
splashColor: Theme.of(context) splashColor: Theme.of(context)
.colorScheme .colorScheme
.primaryContainer .primaryContainer
@@ -208,100 +185,89 @@ class SearchBarAndUser extends StatelessWidget {
], ],
) )
: DefaultUser( : DefaultUser(
onPressed: () => homeController.showUserInfoDialog(context), onPressed: () =>
_homeController.showUserInfoDialog(context),
), ),
), ),
), ),
], ],
); );
} }
}
class UserAndSearchVertical extends StatelessWidget { Widget get customAppBar {
const UserAndSearchVertical({ return StreamBuilder(
super.key, stream: _homeController.hideSearchBar
required this.ctr, ? stream
}); : StreamController<bool>.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; Widget get searchBar {
return Expanded(
@override child: Container(
Widget build(BuildContext context) { width: 250,
return Column( height: 44,
children: [ clipBehavior: Clip.hardEdge,
Semantics( decoration: BoxDecoration(
label: "我的", borderRadius: BorderRadius.circular(25),
child: Obx( ),
() => ctr.userLogin.value child: Material(
? Stack( color: Theme.of(context)
clipBehavior: Clip.none, .colorScheme
children: [ .onSecondaryContainer
NetworkImgLayer( .withOpacity(0.05),
type: 'avatar', child: InkWell(
width: 34, splashColor:
height: 34, Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
src: ctr.userFace.value, 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, const SizedBox(width: 2),
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(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<CustomTabs> createState() => _CustomTabsState();
// }
// class _CustomTabsState extends State<CustomTabs> {
// 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<Color>((Set<WidgetState> 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),
],
],
),
),
),
),
);
}
}

View File

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

View File

@@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/grpc/grpc_client.dart'; import 'package:PiliPlus/grpc/grpc_client.dart';
import 'package:PiliPlus/pages/mine/controller.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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/event_bus.dart';
import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import './controller.dart'; import './controller.dart';
class MainApp extends StatefulWidget { class MainApp extends StatefulWidget {
@@ -27,8 +30,8 @@ class MainApp extends StatefulWidget {
class _MainAppState extends State<MainApp> class _MainAppState extends State<MainApp>
with SingleTickerProviderStateMixin, RouteAware, WidgetsBindingObserver { with SingleTickerProviderStateMixin, RouteAware, WidgetsBindingObserver {
final MainController _mainController = Get.put(MainController()); final MainController _mainController = Get.put(MainController());
final HomeController _homeController = Get.put(HomeController()); late final _homeController = Get.put(HomeController());
final DynamicsController _dynamicController = Get.put(DynamicsController()); late final _dynamicController = Get.put(DynamicsController());
int? _lastSelectTime; //上次点击时间 int? _lastSelectTime; //上次点击时间
late bool enableMYBar; late bool enableMYBar;
@@ -57,6 +60,7 @@ class _MainAppState extends State<MainApp>
void didPopNext() { void didPopNext() {
_mainController.checkUnreadDynamic(); _mainController.checkUnreadDynamic();
_checkDefaultSearch(true); _checkDefaultSearch(true);
_checkUnread(true);
super.didPopNext(); super.didPopNext();
} }
@@ -65,23 +69,40 @@ class _MainAppState extends State<MainApp>
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed) {
_mainController.checkUnreadDynamic(); _mainController.checkUnreadDynamic();
_checkDefaultSearch(true); _checkDefaultSearch(true);
_checkUnread(true);
} }
} }
void _checkDefaultSearch([bool shouldCheck = false]) { void _checkDefaultSearch([bool shouldCheck = false]) {
if (_homeController.enableSearchWord) { if (_mainController.homeIndex != -1 && _homeController.enableSearchWord) {
if (shouldCheck && if (shouldCheck &&
_mainController.pages[_mainController.selectedIndex] is! HomePage) { _mainController.pages[_mainController.selectedIndex] is! HomePage) {
return; return;
} }
int now = DateTime.now().millisecondsSinceEpoch; int now = DateTime.now().millisecondsSinceEpoch;
if (now - _homeController.lateCheckAt >= 5 * 60 * 1000) { if (now - _homeController.lateCheckSearchAt >= 5 * 60 * 1000) {
_homeController.lateCheckAt = now; _homeController.lateCheckSearchAt = now;
_homeController.querySearchDefault(); _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 { void setIndex(int value) async {
feedBack(); feedBack();
_mainController.pageController.jumpToPage(value); _mainController.pageController.jumpToPage(value);
@@ -98,25 +119,11 @@ class _MainAppState extends State<MainApp>
} }
_homeController.flag = true; _homeController.flag = true;
_checkDefaultSearch(); _checkDefaultSearch();
_checkUnread();
} else { } else {
_homeController.flag = false; _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 (currentPage is DynamicsPage) {
if (_dynamicController.flag) { if (_dynamicController.flag) {
// 单击返回顶部 双击并刷新 // 单击返回顶部 双击并刷新
@@ -172,8 +179,8 @@ class _MainAppState extends State<MainApp>
children: [ children: [
if (useSideBar) ...[ if (useSideBar) ...[
SizedBox( SizedBox(
width: context.width * 0.0387 + width: context.width * 0.04 +
36.801 + 40 +
MediaQuery.of(context).padding.left, MediaQuery.of(context).padding.left,
child: Obx( child: Obx(
() => _mainController.navigationBars.length > 1 () => _mainController.navigationBars.length > 1
@@ -184,8 +191,7 @@ class _MainAppState extends State<MainApp>
selectedIndex: _mainController.selectedIndex, selectedIndex: _mainController.selectedIndex,
onDestinationSelected: setIndex, onDestinationSelected: setIndex,
labelType: NavigationRailLabelType.none, labelType: NavigationRailLabelType.none,
leading: leading: userAndSearchVertical,
UserAndSearchVertical(ctr: _homeController),
destinations: _mainController.navigationBars destinations: _mainController.navigationBars
.map((e) => NavigationRailDestination( .map((e) => NavigationRailDestination(
icon: _buildIcon( icon: _buildIcon(
@@ -211,8 +217,7 @@ class _MainAppState extends State<MainApp>
constraints: BoxConstraints( constraints: BoxConstraints(
minWidth: context.width * 0.0286 + 28.56, minWidth: context.width * 0.0286 + 28.56,
), ),
child: child: userAndSearchVertical,
UserAndSearchVertical(ctr: _homeController),
), ),
), ),
), ),
@@ -352,10 +357,10 @@ class _MainAppState extends State<MainApp>
required Widget icon, required Widget icon,
}) => }) =>
id == 1 && id == 1 &&
_mainController.dynamicBadgeType != DynamicBadgeMode.hidden && _mainController.dynamicBadgeMode != DynamicBadgeMode.hidden &&
count > 0 count > 0
? Badge( ? Badge(
label: _mainController.dynamicBadgeType == DynamicBadgeMode.number label: _mainController.dynamicBadgeMode == DynamicBadgeMode.number
? Text(count.toString()) ? Text(count.toString())
: null, : null,
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0), padding: const EdgeInsets.fromLTRB(6, 0, 6, 0),
@@ -367,4 +372,119 @@ class _MainAppState extends State<MainApp>
child: icon, child: icon,
) )
: 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'),
),
],
);
}
} }

View File

@@ -125,7 +125,7 @@ class SettingPage extends StatelessWidget {
if (Get.isRegistered<MainController>()) { if (Get.isRegistered<MainController>()) {
MainController mainController = MainController mainController =
Get.find<MainController>(); Get.find<MainController>();
mainController.userLogin.value = false; mainController.isLogin.value = false;
} }
await LoginUtils.refreshLoginStatus(false); await LoginUtils.refreshLoginStatus(false);
Get.back(); Get.back();

View File

@@ -235,11 +235,11 @@ List<SettingsModel> get styleSettings => [
}, },
); );
if (result != null) { if (result != null) {
GStorage.setting.put(SettingBoxKey.dynamicBadgeMode, result.code); GStorage.setting.put(SettingBoxKey.dynamicBadgeMode, result.index);
MainController mainController = Get.put(MainController()); MainController mainController = Get.put(MainController());
mainController.dynamicBadgeType = mainController.dynamicBadgeMode =
DynamicBadgeMode.values[result.code]; DynamicBadgeMode.values[result.index];
if (mainController.dynamicBadgeType != DynamicBadgeMode.hidden) { if (mainController.dynamicBadgeMode != DynamicBadgeMode.hidden) {
mainController.getUnreadDynamic(); mainController.getUnreadDynamic();
} }
SmartDialog.showToast('设置成功'); SmartDialog.showToast('设置成功');
@@ -250,6 +250,68 @@ List<SettingsModel> get styleSettings => [
leading: const Icon(Icons.motion_photos_on_outlined), leading: const Icon(Icons.motion_photos_on_outlined),
getSubtitle: () => '当前标记样式:${GStorage.dynamicBadgeType.description}', getSubtitle: () => '当前标记样式:${GStorage.dynamicBadgeType.description}',
), ),
SettingsModel(
settingsType: SettingsType.normal,
onTap: (setState) async {
DynamicBadgeMode? result = await showDialog(
context: Get.context!,
builder: (context) {
return SelectDialog<DynamicBadgeMode>(
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<MsgUnReadType>(
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( SettingsModel(
settingsType: SettingsType.sw1tch, settingsType: SettingsType.sw1tch,
title: '首页顶栏收起', title: '首页顶栏收起',

View File

@@ -28,6 +28,9 @@ extension ListExt<T> on List<T>? {
if (isNullOrEmpty) { if (isNullOrEmpty) {
return null; return null;
} }
if (index < 0 || index >= this!.length) {
return null;
}
return this![index]; return this![index];
} }

View File

@@ -90,7 +90,18 @@ class GStorage {
static DynamicBadgeMode get dynamicBadgeType => static DynamicBadgeMode get dynamicBadgeType =>
DynamicBadgeMode.values[setting.get( DynamicBadgeMode.values[setting.get(
SettingBoxKey.dynamicBadgeMode, 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 => static int get defaultHomePage =>
@@ -551,6 +562,8 @@ class SettingBoxKey {
hideTabBar = 'hideTabBar', // 收起底栏 hideTabBar = 'hideTabBar', // 收起底栏
tabbarSort = 'tabbarSort', // 首页tabbar tabbarSort = 'tabbarSort', // 首页tabbar
dynamicBadgeMode = 'dynamicBadgeMode', dynamicBadgeMode = 'dynamicBadgeMode',
msgBadgeMode = 'msgBadgeMode',
msgUnReadType = 'msgUnReadType',
hiddenSettingUnlocked = 'hiddenSettingUnlocked', hiddenSettingUnlocked = 'hiddenSettingUnlocked',
enableGradientBg = 'enableGradientBg', enableGradientBg = 'enableGradientBg',
navBarSort = 'navBarSort'; navBarSort = 'navBarSort';