From 7f58710c6fad530fdc15bb6298837d9b2278de28 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 20 Jan 2025 16:52:53 +0800 Subject: [PATCH] :sparkles: Notification indicator --- lib/main.dart | 1 + lib/providers/notification.dart | 21 +++++++++ lib/widgets/navigation/app_scaffold.dart | 18 +++++--- lib/widgets/notify_indicator.dart | 58 ++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 lib/widgets/notify_indicator.dart diff --git a/lib/main.dart b/lib/main.dart index 7f2c291..d89d8f8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -279,6 +279,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { await ws.tryConnect(); if (!mounted) return; final notify = context.read(); + notify.listen(); await notify.registerPushNotifications(); } catch (err) { if (!mounted) return; diff --git a/lib/providers/notification.dart b/lib/providers/notification.dart index 383ce92..48d1b93 100644 --- a/lib/providers/notification.dart +++ b/lib/providers/notification.dart @@ -8,14 +8,18 @@ import 'package:flutter_udid/flutter_udid.dart'; import 'package:provider/provider.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart'; +import 'package:surface/providers/websocket.dart'; +import 'package:surface/types/notification.dart'; class NotificationProvider extends ChangeNotifier { late final SnNetworkProvider _sn; late final UserProvider _ua; + late final WebSocketProvider _ws; NotificationProvider(BuildContext context) { _sn = context.read(); _ua = context.read(); + _ws = context.read(); } Future registerPushNotifications() async { @@ -62,4 +66,21 @@ class NotificationProvider extends ChangeNotifier { }, ); } + + List notifications = List.empty(growable: true); + + void listen() { + _ws.stream.stream.listen((event) { + if (event.method == 'notifications.new') { + final notification = SnNotification.fromJson(event.payload!); + notifications.add(notification); + notifyListeners(); + } + }); + } + + void clear() { + notifications.clear(); + notifyListeners(); + } } diff --git a/lib/widgets/navigation/app_scaffold.dart b/lib/widgets/navigation/app_scaffold.dart index 4e6ba12..9044e93 100644 --- a/lib/widgets/navigation/app_scaffold.dart +++ b/lib/widgets/navigation/app_scaffold.dart @@ -17,6 +17,7 @@ import 'package:surface/widgets/navigation/app_background.dart'; import 'package:surface/widgets/navigation/app_bottom_navigation.dart'; import 'package:surface/widgets/navigation/app_drawer_navigation.dart'; import 'package:surface/widgets/navigation/app_rail_navigation.dart'; +import 'package:surface/widgets/notify_indicator.dart'; final globalRootScaffoldKey = GlobalKey(); @@ -131,14 +132,18 @@ class AppRootScaffold extends StatelessWidget { ), ), if (!Platform.isMacOS) - MoveWindow( + Expanded( child: WindowTitleBarBox( child: Row( - mainAxisAlignment: MainAxisAlignment.end, children: [ - MinimizeWindowButton(colors: windowButtonColor), - MaximizeWindowButton(colors: windowButtonColor), - CloseWindowButton(colors: windowButtonColor), + Expanded(child: MoveWindow()), + Row( + children: [ + MinimizeWindowButton(colors: windowButtonColor), + MaximizeWindowButton(colors: windowButtonColor), + CloseWindowButton(colors: windowButtonColor), + ], + ), ], ), ), @@ -149,7 +154,8 @@ class AppRootScaffold extends StatelessWidget { Expanded(child: innerWidget), ], ), - Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: ConnectionIndicator()), + Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()), + Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()), ], ), drawer: !isExpandedDrawer ? AppNavigationDrawer() : null, diff --git a/lib/widgets/notify_indicator.dart b/lib/widgets/notify_indicator.dart new file mode 100644 index 0000000..cf40349 --- /dev/null +++ b/lib/widgets/notify_indicator.dart @@ -0,0 +1,58 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/providers/notification.dart'; +import 'package:surface/providers/userinfo.dart'; + +class NotifyIndicator extends StatelessWidget { + const NotifyIndicator({super.key}); + + @override + Widget build(BuildContext context) { + final ua = context.read(); + final nty = context.watch(); + + return ListenableBuilder( + listenable: nty, + builder: (context, _) { + return GestureDetector( + child: Material( + elevation: 2, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), + color: Theme.of(context).colorScheme.secondaryContainer, + child: ua.isAuthorized + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + nty.notifications.lastOrNull?.title ?? + 'notificationUnreadCount'.plural(nty.notifications.length), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (nty.notifications.lastOrNull?.body != null) + Text( + nty.notifications.lastOrNull!.body, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ).padding(left: 4), + const Gap(8), + const Icon(Symbols.notifications_unread, size: 18), + ], + ).padding(horizontal: 8, vertical: 4) + : const SizedBox.shrink(), + ).opacity(nty.notifications.isNotEmpty && ua.isAuthorized ? 1 : 0, animate: true).animate( + const Duration(milliseconds: 300), + Curves.easeInOut, + ), + onTap: () { + nty.clear(); + }, + ); + }); + } +}