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

@@ -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 =

View File

@@ -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<HomePage>
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
final HomeController _homeController = Get.put(HomeController());
late Stream<bool> stream;
final MainController _mainController = Get.put(MainController());
@override
bool get wantKeepAlive => true;
@@ -37,13 +40,7 @@ class _HomePageState extends State<HomePage>
appBar: AppBar(toolbarHeight: 0),
body: Column(
children: [
if (!_homeController.useSideBar)
CustomAppBar(
stream: _homeController.hideSearchBar
? stream
: StreamController<bool>.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<HomePage>
),
);
}
}
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
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) {
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<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;
@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<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),
],
],
),
),
),
),
);
}
}