feat: rework flutter_notification_center design

This commit is contained in:
mike doornenbal 2024-06-06 13:30:18 +02:00
parent b57bc7ce46
commit 169228152e
21 changed files with 496 additions and 471 deletions

View file

@ -1,3 +1,8 @@
## [2.0.0] - 6 June 2024
* Rework design for notification center
* Added iconica linter
## [1.4.1] - 4 June 2024 ## [1.4.1] - 4 June 2024
* Fix notification amount number to properly size and show plus icon when above certain amount * Fix notification amount number to properly size and show plus icon when above certain amount

View file

@ -1,28 +1,9 @@
# This file configures the analyzer, which statically analyzes Dart code to include: package:flutter_iconica_analysis/analysis_options.yaml
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps, # Possible to overwrite the rules from the package
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml analyzer:
exclude:
linter: linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -23,6 +23,5 @@ linter:
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View file

@ -82,7 +82,6 @@ class _NotificationCenterDemoState extends State<NotificationCenterDemo> {
notificationTranslations: const NotificationTranslations.empty(), notificationTranslations: const NotificationTranslations.empty(),
context: context, context: context,
), ),
seperateNotificationsWithDivider: true,
); );
popupHandler = PopupHandler(context: context, config: config); popupHandler = PopupHandler(context: context, config: config);
} }

View file

@ -2,12 +2,12 @@ name: example
description: "A new Flutter project." description: "A new Flutter project."
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.0.0 version: 1.0.0
environment: environment:
sdk: '>=3.3.2 <4.0.0' sdk: ">=3.3.2 <4.0.0"
dependencies: dependencies:
flutter: flutter:
@ -17,16 +17,14 @@ dependencies:
flutter_notification_center: flutter_notification_center:
git: git:
url: https://github.com/Iconica-Development/flutter_notification_center url: https://github.com/Iconica-Development/flutter_notification_center
ref: 1.4.0
path: packages/flutter_notification_center path: packages/flutter_notification_center
ref: 2.0.0
flutter_notification_center_firebase: flutter_notification_center_firebase:
git: git:
url: https://github.com/Iconica-Development/flutter_notification_center url: https://github.com/Iconica-Development/flutter_notification_center
ref: 1.4.0
path: packages/flutter_notification_center_firebase path: packages/flutter_notification_center_firebase
ref: 2.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -39,4 +37,3 @@ dev_dependencies:
# The following section is specific to Flutter packages. # The following section is specific to Flutter packages.
flutter: flutter:
uses-material-design: true uses-material-design: true

View file

@ -2,16 +2,17 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
export "package:flutter_animated_widgets/flutter_animated_widgets.dart";
export "src/models/notification.dart"; export "src/models/notification.dart";
export "src/models/notification_config.dart"; export "src/models/notification_config.dart";
export "src/models/notification_theme.dart"; export "src/models/notification_theme.dart";
export "src/models/notification_translation.dart"; export "src/models/notification_translation.dart";
export "src/notification_bell.dart"; export "src/notification_bell.dart";
export "src/notification_dialog.dart";
export "src/popup_handler.dart";
export "src/notification_snackbar.dart";
export "src/notification_detail.dart";
export "src/notification_bell_story.dart"; export "src/notification_bell_story.dart";
export "src/notification_center.dart"; export "src/notification_center.dart";
export "src/notification_detail.dart";
export "src/notification_dialog.dart";
export "src/notification_snackbar.dart";
export "src/popup_handler.dart";
export "src/services/notification_service.dart"; export "src/services/notification_service.dart";
export "package:flutter_animated_widgets/flutter_animated_widgets.dart";

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import '../../flutter_notification_center.dart'; import "package:flutter_notification_center/flutter_notification_center.dart";
/// Configuration class for notifications. /// Configuration class for notifications.
class NotificationConfig { class NotificationConfig {
@ -12,7 +12,6 @@ class NotificationConfig {
/// translations for notification messages. /// translations for notification messages.
const NotificationConfig({ const NotificationConfig({
required this.service, required this.service,
this.seperateNotificationsWithDivider = true,
this.translations = const NotificationTranslations.empty(), this.translations = const NotificationTranslations.empty(),
this.notificationWidgetBuilder, this.notificationWidgetBuilder,
this.showAsSnackBar = true, this.showAsSnackBar = true,
@ -23,9 +22,6 @@ class NotificationConfig {
/// The notification service to use for delivering notifications. /// The notification service to use for delivering notifications.
final NotificationService service; final NotificationService service;
/// Whether to seperate notifications with a divider.
final bool seperateNotificationsWithDivider;
/// Translations for notification messages. /// Translations for notification messages.
final NotificationTranslations translations; final NotificationTranslations translations;
@ -33,7 +29,8 @@ class NotificationConfig {
final Widget Function(NotificationModel, BuildContext)? final Widget Function(NotificationModel, BuildContext)?
notificationWidgetBuilder; notificationWidgetBuilder;
/// Whether to show notifications as snackbars. If false show notifications as a dialog. /// Whether to show notifications as snackbars.
/// If false show notifications as a dialog.
final bool showAsSnackBar; final bool showAsSnackBar;
/// Whether to show notification popups. /// Whether to show notification popups.

View file

@ -14,7 +14,7 @@ class NotificationTranslations {
}); });
const NotificationTranslations.empty({ const NotificationTranslations.empty({
this.appBarTitle = "Notification Center", this.appBarTitle = "Notifications",
this.noNotifications = "No unread notifications available.", this.noNotifications = "No unread notifications available.",
this.notificationDismissed = "Notification dismissed.", this.notificationDismissed = "Notification dismissed.",
this.notificationPinned = "Notification pinned.", this.notificationPinned = "Notification pinned.",
@ -63,8 +63,8 @@ class NotificationTranslations {
String? datePrefix, String? datePrefix,
String? notAvailable, String? notAvailable,
String? dissmissDialog, String? dissmissDialog,
}) { }) =>
return NotificationTranslations( NotificationTranslations(
appBarTitle: appBarTitle ?? this.appBarTitle, appBarTitle: appBarTitle ?? this.appBarTitle,
noNotifications: noNotifications ?? this.noNotifications, noNotifications: noNotifications ?? this.noNotifications,
notificationDismissed: notificationDismissed:
@ -77,4 +77,3 @@ class NotificationTranslations {
dissmissDialog: dissmissDialog ?? this.dissmissDialog, dissmissDialog: dissmissDialog ?? this.dissmissDialog,
); );
} }
}

View file

@ -1,5 +1,5 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "../flutter_notification_center.dart"; import "package:flutter_notification_center/flutter_notification_center.dart";
/// A bell icon widget that displays the number of active notifications. /// A bell icon widget that displays the number of active notifications.
/// ///

View file

@ -1,5 +1,5 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "../flutter_notification_center.dart"; import "package:flutter_notification_center/flutter_notification_center.dart";
/// A widget representing a notification bell. /// A widget representing a notification bell.
class NotificationBellWidgetStory extends StatelessWidget { class NotificationBellWidgetStory extends StatelessWidget {

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import '../flutter_notification_center.dart'; import "package:flutter_notification_center/flutter_notification_center.dart";
import "package:intl/intl.dart";
/// Widget for displaying the notification center. /// Widget for displaying the notification center.
class NotificationCenter extends StatefulWidget { class NotificationCenter extends StatefulWidget {
@ -27,7 +28,7 @@ class NotificationCenterState extends State<NotificationCenter> {
super.initState(); super.initState();
// ignore: discarded_futures // ignore: discarded_futures
_notificationsFuture = widget.config.service.getActiveNotifications(); _notificationsFuture = widget.config.service.getActiveNotifications();
widget.config.service.getActiveAmountStream().listen((amount) { widget.config.service.getActiveAmountStream().listen((amount) async {
_notificationsFuture = widget.config.service.getActiveNotifications(); _notificationsFuture = widget.config.service.getActiveNotifications();
}); });
widget.config.service.addListener(_listener); widget.config.service.addListener(_listener);
@ -44,17 +45,26 @@ class NotificationCenterState extends State<NotificationCenter> {
} }
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) {
var theme = Theme.of(context);
return Scaffold(
backgroundColor: theme.colorScheme.surface,
appBar: AppBar( appBar: AppBar(
backgroundColor: theme.appBarTheme.backgroundColor,
title: Text( title: Text(
widget.config.translations.appBarTitle, widget.config.translations.appBarTitle,
style: theme.appBarTheme.titleTextStyle,
), ),
centerTitle: true, centerTitle: true,
leading: IconButton( iconTheme: theme.appBarTheme.iconTheme ??
icon: const Icon(Icons.arrow_back), const IconThemeData(color: Colors.white),
onPressed: () { leading: GestureDetector(
onTap: () {
Navigator.pop(context); Navigator.pop(context);
}, },
child: const Icon(
Icons.arrow_back_ios,
),
), ),
), ),
body: FutureBuilder<List<NotificationModel>>( body: FutureBuilder<List<NotificationModel>>(
@ -64,47 +74,57 @@ class NotificationCenterState extends State<NotificationCenter> {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
debugPrint("Error: ${snapshot.error}"); debugPrint("Error: ${snapshot.error}");
return Center( return Center(child: Text(widget.config.translations.errorMessage));
child: Text(widget.config.translations.errorMessage));
} else if (snapshot.data == null || snapshot.data!.isEmpty) { } else if (snapshot.data == null || snapshot.data!.isEmpty) {
return Center( return Center(
child: Text(widget.config.translations.noNotifications), child: Text(widget.config.translations.noNotifications),
); );
} else { } else {
return ListView.builder( return ListView.builder(
itemCount: snapshot.data!.length * 2 - 1, padding: const EdgeInsets.symmetric(
vertical: 20,
horizontal: 20,
),
itemCount: snapshot.data!.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index.isOdd) { var notification = snapshot.data![index];
return Padding( return notification.isPinned
padding: const EdgeInsets.symmetric(horizontal: 24.0), ? GestureDetector(
child: widget.config.seperateNotificationsWithDivider onTap: () async => _navigateToNotificationDetail(
? const Divider( context,
color: Colors.grey, notification,
thickness: 1.0, widget.config.service,
) widget.config.translations,
: Container(), const NotificationStyle(),
); ),
} child: Dismissible(
var notification = snapshot.data![index ~/ 2]; key: Key("${notification.id}_pinned"),
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: widget.config.notificationWidgetBuilder != null
? widget.config.notificationWidgetBuilder!(
notification, context)
: notification.isPinned
//Pinned notification
? Dismissible(
key: Key('${notification.id}_pinned'),
onDismissed: (direction) async { onDismissed: (direction) async {
if (direction == DismissDirection.endToStart) {
await unPinNotification( await unPinNotification(
widget.config.service, widget.config.service,
notification, notification,
widget.config.translations, widget.config.translations,
context); context,
);
} else if (direction ==
DismissDirection.startToEnd) {
await unPinNotification(
widget.config.service,
notification,
widget.config.translations,
context,
);
}
}, },
background: Container( background: Container(
color: decoration: const BoxDecoration(
const Color.fromRGBO(59, 213, 111, 1), color: Color.fromRGBO(59, 213, 111, 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(6),
bottomLeft: Radius.circular(6),
),
),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: const Padding( child: const Padding(
padding: EdgeInsets.only(left: 16.0), padding: EdgeInsets.only(left: 16.0),
@ -115,85 +135,64 @@ class NotificationCenterState extends State<NotificationCenter> {
), ),
), ),
secondaryBackground: Container( secondaryBackground: Container(
color: decoration: const BoxDecoration(
const Color.fromRGBO(59, 213, 111, 1), color: Color.fromRGBO(59, 213, 111, 1),
alignment: Alignment.centerLeft, borderRadius: BorderRadius.only(
topRight: Radius.circular(6),
bottomRight: Radius.circular(6),
),
),
alignment: Alignment.centerRight,
child: const Padding( child: const Padding(
padding: EdgeInsets.only(left: 16.0), padding: EdgeInsets.only(right: 16.0),
child: Icon( child: Icon(
Icons.push_pin, Icons.push_pin,
color: Colors.white, color: Colors.white,
), ),
), ),
), ),
child: GestureDetector( child: _notificationItem(
onTap: () async =>
_navigateToNotificationDetail(
context, context,
notification, notification,
widget.config.service,
widget.config.translations,
const NotificationStyle()),
child: ListTile(
leading: Icon(
notification.icon,
color: Colors.grey,
),
title: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
notification.title,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 16,
),
),
),
],
),
trailing: IconButton(
icon: const Icon(Icons.push_pin),
color: Colors.grey,
onPressed: () async =>
_navigateToNotificationDetail(
context,
notification,
widget.config.service,
widget.config.translations,
const NotificationStyle()),
padding:
const EdgeInsets.only(left: 60.0),
),
), ),
), ),
) )
//Dismissable notification : GestureDetector(
: Dismissible( onTap: () async => _navigateToNotificationDetail(
context,
notification,
widget.config.service,
widget.config.translations,
const NotificationStyle(),
),
child: Dismissible(
key: Key(notification.id), key: Key(notification.id),
onDismissed: (direction) async { onDismissed: (direction) async {
if (direction == if (direction == DismissDirection.endToStart) {
DismissDirection.endToStart) {
await dismissNotification( await dismissNotification(
widget.config.service, widget.config.service,
notification, notification,
widget.config.translations, widget.config.translations,
context); context,
);
} else if (direction == } else if (direction ==
DismissDirection.startToEnd) { DismissDirection.startToEnd) {
await pinNotification( await pinNotification(
widget.config.service, widget.config.service,
notification, notification,
widget.config.translations, widget.config.translations,
context); context,
);
} }
}, },
background: Container( background: Container(
color: decoration: const BoxDecoration(
const Color.fromRGBO(59, 213, 111, 1), color: Color.fromRGBO(59, 213, 111, 1),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(6),
bottomLeft: Radius.circular(6),
),
),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: const Padding( child: const Padding(
padding: EdgeInsets.only(left: 16.0), padding: EdgeInsets.only(left: 16.0),
@ -203,9 +202,16 @@ class NotificationCenterState extends State<NotificationCenter> {
), ),
), ),
), ),
secondaryBackground: Container( secondaryBackground: Padding(
color: padding: const EdgeInsets.only(bottom: 8),
const Color.fromRGBO(255, 131, 131, 1), child: Container(
decoration: const BoxDecoration(
color: Color.fromRGBO(255, 131, 131, 1),
borderRadius: BorderRadius.only(
topRight: Radius.circular(6),
bottomRight: Radius.circular(6),
),
),
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: const Padding( child: const Padding(
padding: EdgeInsets.only(right: 16.0), padding: EdgeInsets.only(right: 16.0),
@ -215,50 +221,13 @@ class NotificationCenterState extends State<NotificationCenter> {
), ),
), ),
), ),
child: GestureDetector( ),
onTap: () async => child: _notificationItem(
_navigateToNotificationDetail(
context, context,
notification, notification,
widget.config.service,
widget.config.translations,
const NotificationStyle()),
child: ListTile(
leading: Icon(
notification.icon,
color: Colors.grey,
),
title: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
notification.title,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
fontSize: 16,
), ),
), ),
), );
],
),
trailing: !notification.isRead
? Container(
margin: const EdgeInsets.only(
right: 8.0),
width: 12.0,
height: 12.0,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.red,
),
)
: null,
),
),
));
}, },
); );
} }
@ -266,6 +235,80 @@ class NotificationCenterState extends State<NotificationCenter> {
), ),
); );
} }
}
Widget _notificationItem(
BuildContext context,
NotificationModel notification,
) {
var theme = Theme.of(context);
var dateTimePushed =
DateFormat("dd-MM-yyyy HH:mm").format(notification.dateTimePushed!);
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
),
width: double.infinity,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (!notification.isPinned) ...[
if (!notification.isRead) ...[
const SizedBox(
width: 8,
),
const Icon(
Icons.circle_rounded,
color: Colors.black,
size: 10,
),
const SizedBox(
width: 8,
),
],
] else ...[
const SizedBox(
width: 8,
),
Transform.rotate(
angle: 0.5,
child: Icon(
notification.isRead
? Icons.push_pin_outlined
: Icons.push_pin,
color: Colors.black,
size: 30,
),
),
const SizedBox(
width: 8,
),
],
Text(
notification.title,
style: notification.isRead
? theme.textTheme.bodyMedium
: theme.textTheme.bodyLarge,
),
],
),
Text(
dateTimePushed,
style: theme.textTheme.bodyMedium,
),
],
),
),
),
);
}
Future<void> _navigateToNotificationDetail( Future<void> _navigateToNotificationDetail(
BuildContext context, BuildContext context,
@ -274,7 +317,6 @@ Future<void> _navigateToNotificationDetail(
NotificationTranslations notificationTranslations, NotificationTranslations notificationTranslations,
NotificationStyle style, NotificationStyle style,
) async { ) async {
await markNotificationAsRead(notificationService, notification);
if (context.mounted) { if (context.mounted) {
await Navigator.push( await Navigator.push(
context, context,
@ -287,6 +329,7 @@ Future<void> _navigateToNotificationDetail(
), ),
); );
} }
await markNotificationAsRead(notificationService, notification);
} }
Future<void> dismissNotification( Future<void> dismissNotification(

View file

@ -1,5 +1,5 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "../flutter_notification_center.dart"; import "package:flutter_notification_center/flutter_notification_center.dart";
import "package:intl/intl.dart"; import "package:intl/intl.dart";
/// A page displaying the details of a notification. /// A page displaying the details of a notification.
@ -27,13 +27,27 @@ class NotificationDetailPage extends StatelessWidget {
final NotificationTranslations translations; final NotificationTranslations translations;
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) {
var theme = Theme.of(context);
return Scaffold(
backgroundColor: theme.colorScheme.surface,
appBar: AppBar( appBar: AppBar(
backgroundColor: theme.appBarTheme.backgroundColor,
title: Text( title: Text(
translations.appBarTitle, translations.appBarTitle,
style: notificationStyle.appTitleTextStyle, style: theme.appBarTheme.titleTextStyle,
), ),
iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white),
centerTitle: true, centerTitle: true,
leading: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: const Icon(
Icons.arrow_back_ios,
),
),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Padding(
@ -48,12 +62,12 @@ class NotificationDetailPage extends StatelessWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
notification.body, notification.body,
style: style: notificationStyle.subtitleTextStyle ?? const TextStyle(),
notificationStyle.subtitleTextStyle ?? const TextStyle(),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
'${translations.datePrefix} ${DateFormat('yyyy-MM-dd HH:mm').format( "${translations.datePrefix}"
' ${DateFormat('yyyy-MM-dd HH:mm').format(
notification.dateTimePushed ?? DateTime.now(), notification.dateTimePushed ?? DateTime.now(),
)}', )}',
style: const TextStyle( style: const TextStyle(
@ -67,3 +81,4 @@ class NotificationDetailPage extends StatelessWidget {
), ),
); );
} }
}

View file

@ -1,25 +1,24 @@
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_notification_center/flutter_notification_center.dart'; import "package:flutter_notification_center/flutter_notification_center.dart";
import 'package:intl/intl.dart'; import "package:intl/intl.dart";
class NotificationDialog extends StatelessWidget { class NotificationDialog extends StatelessWidget {
const NotificationDialog({
required this.title,
required this.body,
required this.translations,
super.key,
this.datetimePublished,
});
final String title; final String title;
final String body; final String body;
final DateTime? datetimePublished; final DateTime? datetimePublished;
final NotificationTranslations translations; final NotificationTranslations translations;
const NotificationDialog({
super.key,
required this.title,
required this.body,
required this.translations,
this.datetimePublished,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String formattedDateTime = datetimePublished != null var formattedDateTime = datetimePublished != null
? DateFormat('dd MMM HH:mm').format(datetimePublished!) ? DateFormat("dd MMM HH:mm").format(datetimePublished!)
: translations.notAvailable; : translations.notAvailable;
return AlertDialog( return AlertDialog(

View file

@ -1,14 +1,14 @@
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_notification_center/flutter_notification_center.dart'; import "package:flutter_notification_center/flutter_notification_center.dart";
import 'package:intl/intl.dart'; import "package:intl/intl.dart";
class NotificationSnackbar extends SnackBar { class NotificationSnackbar extends SnackBar {
NotificationSnackbar({ NotificationSnackbar({
super.key,
required String title, required String title,
required String body, required String body,
required NotificationTranslations translations, required NotificationTranslations translations,
required VoidCallback onDismiss, required VoidCallback onDismiss,
super.key,
DateTime? datetimePublished, DateTime? datetimePublished,
}) : super( }) : super(
content: Column( content: Column(
@ -33,7 +33,7 @@ class NotificationSnackbar extends SnackBar {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
datetimePublished != null datetimePublished != null
? DateFormat('dd MMM HH:mm').format(datetimePublished) ? DateFormat("dd-MM-yyyy HH:mm").format(datetimePublished)
: translations.notAvailable, : translations.notAvailable,
style: const TextStyle( style: const TextStyle(
fontSize: 12.0, fontSize: 12.0,

View file

@ -1,18 +1,17 @@
// Define a PopupHandler class to handle notification popups // Define a PopupHandler class to handle notification popups
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_notification_center/flutter_notification_center.dart'; import "package:flutter_notification_center/flutter_notification_center.dart";
class PopupHandler { class PopupHandler {
final BuildContext context;
final NotificationConfig config;
PopupHandler({ PopupHandler({
required this.context, required this.context,
required this.config, required this.config,
}); });
final BuildContext context;
final NotificationConfig config;
void handleNotificationPopup(NotificationModel notification) { Future<void> handleNotificationPopup(NotificationModel notification) async {
if (!config.enableNotificationPopups) return; if (!config.enableNotificationPopups) return;
if (config.showAsSnackBar) { if (config.showAsSnackBar) {
@ -31,7 +30,7 @@ class PopupHandler {
} else { } else {
if (ModalRoute.of(context)?.isCurrent != true) return; if (ModalRoute.of(context)?.isCurrent != true) return;
showDialog( await showDialog(
context: context, context: context,
builder: (context) => NotificationDialog( builder: (context) => NotificationDialog(
translations: config.translations, translations: config.translations,

View file

@ -1,7 +1,7 @@
import "dart:async"; import "dart:async";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "../models/notification.dart"; import "package:flutter_notification_center/src/models/notification.dart";
/// An abstract class representing a service for managing notifications. /// An abstract class representing a service for managing notifications.
abstract class NotificationService with ChangeNotifier { abstract class NotificationService with ChangeNotifier {
@ -26,8 +26,10 @@ abstract class NotificationService with ChangeNotifier {
List<NotificationModel> listOfPlannedNotifications; List<NotificationModel> listOfPlannedNotifications;
/// Pushes a notification to the service. /// Pushes a notification to the service.
Future pushNotification(NotificationModel notification, Future pushNotification(
[Function(NotificationModel model)? onNewNotification]); NotificationModel notification, [
Function(NotificationModel model)? onNewNotification,
]);
/// Retrieves the list of active notifications. /// Retrieves the list of active notifications.
Future<List<NotificationModel>> getActiveNotifications(); Future<List<NotificationModel>> getActiveNotifications();

View file

@ -1,11 +1,10 @@
name: flutter_notification_center name: flutter_notification_center
description: "A Flutter package for displaying notifications in a notification center." description: "A Flutter package for displaying notifications in a notification center."
publish_to: 'none' publish_to: "none"
version: 2.0.0
version: 1.4.1
environment: environment:
sdk: '>=3.3.2 <4.0.0' sdk: ">=3.3.2 <4.0.0"
dependencies: dependencies:
flutter: flutter:
@ -15,7 +14,7 @@ dependencies:
flutter_animated_widgets: flutter_animated_widgets:
git: git:
url: https://github.com/Iconica-Development/flutter_animated_widgets url: https://github.com/Iconica-Development/flutter_animated_widgets
ref: 0.2.0 ref: 0.3.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -3,10 +3,10 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import 'package:flutter_test/flutter_test.dart'; import "package:flutter_test/flutter_test.dart";
void main() { void main() {
test('test', () { test("test", () {
expect(true, true); expect(true, true);
}); });
} }

View file

@ -1,28 +1,9 @@
# This file configures the analyzer, which statically analyzes Dart code to include: package:flutter_iconica_analysis/analysis_options.yaml
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps, # Possible to overwrite the rules from the package
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml analyzer:
exclude:
linter: linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -1,14 +1,25 @@
import 'dart:async'; import "dart:async";
import 'package:cloud_firestore/cloud_firestore.dart'; import "package:cloud_firestore/cloud_firestore.dart";
import 'package:firebase_auth/firebase_auth.dart'; import "package:firebase_auth/firebase_auth.dart";
import 'package:firebase_core/firebase_core.dart'; import "package:firebase_core/firebase_core.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_notification_center/flutter_notification_center.dart'; import "package:flutter_notification_center/flutter_notification_center.dart";
class FirebaseNotificationService class FirebaseNotificationService
with ChangeNotifier with ChangeNotifier
implements NotificationService { implements NotificationService {
FirebaseNotificationService({
required this.newNotificationCallback,
this.firebaseApp,
this.activeNotificationsCollection = "active_notifications",
this.plannedNotificationsCollection = "planned_notifications",
this.listOfActiveNotifications = const [],
this.listOfPlannedNotifications = const [],
}) {
_firebaseApp = firebaseApp ?? Firebase.app();
unawaited(_startTimer());
}
final Function(NotificationModel) newNotificationCallback; final Function(NotificationModel) newNotificationCallback;
final FirebaseApp? firebaseApp; final FirebaseApp? firebaseApp;
final String activeNotificationsCollection; final String activeNotificationsCollection;
@ -23,33 +34,23 @@ class FirebaseNotificationService
// ignore: unused_field // ignore: unused_field
late Timer _timer; late Timer _timer;
FirebaseNotificationService({ Future<void> _startTimer() async {
required this.newNotificationCallback, _timer = Timer.periodic(const Duration(seconds: 15), (timer) async {
this.firebaseApp, debugPrint("Checking for scheduled notifications");
this.activeNotificationsCollection = 'active_notifications', await checkForScheduledNotifications();
this.plannedNotificationsCollection = 'planned_notifications',
this.listOfActiveNotifications = const [],
this.listOfPlannedNotifications = const [],
}) {
_firebaseApp = firebaseApp ?? Firebase.app();
_startTimer();
}
void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 15), (timer) {
debugPrint('Checking for scheduled notifications');
checkForScheduledNotifications();
}); });
} }
@override @override
Future<void> pushNotification(NotificationModel notification, Future<void> pushNotification(
[Function(NotificationModel model)? onNewNotification]) async { NotificationModel notification, [
Function(NotificationModel model)? onNewNotification,
]) async {
try { try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) { if (userId == null) {
debugPrint('User is not authenticated'); debugPrint("User is not authenticated");
return; return;
} }
@ -59,9 +60,9 @@ class FirebaseNotificationService
.doc(userId) .doc(userId)
.collection(activeNotificationsCollection); .collection(activeNotificationsCollection);
DateTime currentDateTime = DateTime.now(); var currentDateTime = DateTime.now();
notification.dateTimePushed = currentDateTime; notification.dateTimePushed = currentDateTime;
Map<String, dynamic> notificationMap = notification.toMap(); var notificationMap = notification.toMap();
await notifications.doc(notification.id).set(notificationMap); await notifications.doc(notification.id).set(notificationMap);
listOfActiveNotifications = [...listOfActiveNotifications, notification]; listOfActiveNotifications = [...listOfActiveNotifications, notification];
@ -74,8 +75,8 @@ class FirebaseNotificationService
} }
notifyListeners(); notifyListeners();
} catch (e) { } on Exception catch (e) {
debugPrint('Error creating document: $e'); debugPrint("Error creating document: $e");
} }
} }
@ -85,7 +86,7 @@ class FirebaseNotificationService
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) { if (userId == null) {
debugPrint('User is not authenticated'); debugPrint("User is not authenticated");
return []; return [];
} }
@ -95,12 +96,11 @@ class FirebaseNotificationService
.doc(userId) .doc(userId)
.collection(activeNotificationsCollection); .collection(activeNotificationsCollection);
QuerySnapshot querySnapshot = await activeNotificationsResult.get(); var querySnapshot = await activeNotificationsResult.get();
List<NotificationModel> activeNotifications = var activeNotifications = querySnapshot.docs.map((doc) {
querySnapshot.docs.map((doc) { var data = doc.data()! as Map<String, dynamic>;
Map<String, dynamic> data = doc.data() as Map<String, dynamic>; data["id"] = doc.id;
data['id'] = doc.id;
return NotificationModel.fromJson(data); return NotificationModel.fromJson(data);
}).toList(); }).toList();
@ -114,19 +114,22 @@ class FirebaseNotificationService
.sort((a, b) => b.dateTimePushed!.compareTo(a.dateTimePushed!)); .sort((a, b) => b.dateTimePushed!.compareTo(a.dateTimePushed!));
listOfActiveNotifications.insertAll( listOfActiveNotifications.insertAll(
0, activeNotifications.where((element) => element.isPinned)); 0,
activeNotifications.where((element) => element.isPinned),
);
notifyListeners(); notifyListeners();
return listOfActiveNotifications; return listOfActiveNotifications;
} catch (e) { } on Exception catch (e) {
debugPrint('Error getting active notifications: $e'); debugPrint("Error getting active notifications: $e");
return []; return [];
} }
} }
@override @override
Future<void> createRecurringNotification( Future<void> createRecurringNotification(
NotificationModel notification) async { NotificationModel notification,
) async {
if (notification.recurring) { if (notification.recurring) {
switch (notification.occuringInterval) { switch (notification.occuringInterval) {
case OcurringInterval.daily: case OcurringInterval.daily:
@ -147,18 +150,19 @@ class FirebaseNotificationService
break; break;
case null: case null:
} }
createScheduledNotification(notification); await createScheduledNotification(notification);
} }
} }
@override @override
Future<void> createScheduledNotification( Future<void> createScheduledNotification(
NotificationModel notification) async { NotificationModel notification,
) async {
try { try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) { if (userId == null) {
debugPrint('User is not authenticated'); debugPrint("User is not authenticated");
return; return;
} }
@ -168,21 +172,22 @@ class FirebaseNotificationService
.doc(userId) .doc(userId)
.collection(plannedNotificationsCollection); .collection(plannedNotificationsCollection);
Map<String, dynamic> notificationMap = notification.toMap(); var notificationMap = notification.toMap();
await plannedNotifications.doc(notification.id).set(notificationMap); await plannedNotifications.doc(notification.id).set(notificationMap);
} catch (e) { } on Exception catch (e) {
debugPrint('Error creating document: $e'); debugPrint("Error creating document: $e");
} }
} }
@override @override
Future<void> deletePlannedNotification( Future<void> deletePlannedNotification(
NotificationModel notificationModel) async { NotificationModel notificationModel,
) async {
try { try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) { if (userId == null) {
debugPrint('User is not authenticated'); debugPrint("User is not authenticated");
return; return;
} }
@ -202,24 +207,26 @@ class FirebaseNotificationService
.get(); .get();
if (querySnapshot.docs.isEmpty) { if (querySnapshot.docs.isEmpty) {
debugPrint('The collection is now empty'); debugPrint("The collection is now empty");
} else { } else {
debugPrint( debugPrint(
'Deleted planned notification with title: ${notificationModel.title}'); "Deleted planned notification with title: ${notificationModel.title}",
);
} }
} catch (e) { } on Exception catch (e) {
debugPrint('Error deleting document: $e'); debugPrint("Error deleting document: $e");
} }
} }
@override @override
Future<void> dismissActiveNotification( Future<void> dismissActiveNotification(
NotificationModel notificationModel) async { NotificationModel notificationModel,
) async {
try { try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) { if (userId == null) {
debugPrint('User is not authenticated'); debugPrint("User is not authenticated");
return; return;
} }
@ -233,19 +240,20 @@ class FirebaseNotificationService
listOfActiveNotifications listOfActiveNotifications
.removeAt(listOfActiveNotifications.indexOf(notificationModel)); .removeAt(listOfActiveNotifications.indexOf(notificationModel));
notifyListeners(); notifyListeners();
} catch (e) { } on Exception catch (e) {
debugPrint('Error deleting document: $e'); debugPrint("Error deleting document: $e");
} }
} }
@override @override
Future<void> pinActiveNotification( Future<void> pinActiveNotification(
NotificationModel notificationModel) async { NotificationModel notificationModel,
) async {
try { try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) { if (userId == null) {
debugPrint('User is not authenticated'); debugPrint("User is not authenticated");
return; return;
} }
@ -255,7 +263,7 @@ class FirebaseNotificationService
.doc(userId) .doc(userId)
.collection(activeNotificationsCollection) .collection(activeNotificationsCollection)
.doc(notificationModel.id); .doc(notificationModel.id);
await documentReference.update({'isPinned': true}); await documentReference.update({"isPinned": true});
notificationModel.isPinned = true; notificationModel.isPinned = true;
listOfActiveNotifications listOfActiveNotifications
@ -263,19 +271,20 @@ class FirebaseNotificationService
listOfActiveNotifications.insert(0, notificationModel); listOfActiveNotifications.insert(0, notificationModel);
notifyListeners(); notifyListeners();
} catch (e) { } on Exception catch (e) {
debugPrint('Error updating document: $e'); debugPrint("Error updating document: $e");
} }
} }
@override @override
Future<void> unPinActiveNotification( Future<void> unPinActiveNotification(
NotificationModel notificationModel) async { NotificationModel notificationModel,
) async {
try { try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) { if (userId == null) {
debugPrint('User is not authenticated'); debugPrint("User is not authenticated");
return; return;
} }
@ -285,7 +294,7 @@ class FirebaseNotificationService
.doc(userId) .doc(userId)
.collection(activeNotificationsCollection) .collection(activeNotificationsCollection)
.doc(notificationModel.id); .doc(notificationModel.id);
await documentReference.update({'isPinned': false}); await documentReference.update({"isPinned": false});
notificationModel.isPinned = false; notificationModel.isPinned = false;
listOfActiveNotifications listOfActiveNotifications
@ -294,19 +303,20 @@ class FirebaseNotificationService
listOfActiveNotifications.add(notificationModel); listOfActiveNotifications.add(notificationModel);
notifyListeners(); notifyListeners();
} catch (e) { } on Exception catch (e) {
debugPrint('Error updating document: $e'); debugPrint("Error updating document: $e");
} }
} }
@override @override
Future<void> markNotificationAsRead( Future<void> markNotificationAsRead(
NotificationModel notificationModel) async { NotificationModel notificationModel,
) async {
try { try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) { if (userId == null) {
debugPrint('User is not authenticated'); debugPrint("User is not authenticated");
return; return;
} }
@ -316,22 +326,22 @@ class FirebaseNotificationService
.doc(userId) .doc(userId)
.collection(activeNotificationsCollection) .collection(activeNotificationsCollection)
.doc(notificationModel.id); .doc(notificationModel.id);
await documentReference.update({'isRead': true}); await documentReference.update({"isRead": true});
notificationModel.isRead = true; notificationModel.isRead = true;
notifyListeners(); notifyListeners();
} catch (e) { } on Exception catch (e) {
debugPrint('Error updating document: $e'); debugPrint("Error updating document: $e");
} }
} }
@override @override
Future<void> checkForScheduledNotifications() async { Future<void> checkForScheduledNotifications() async {
DateTime currentTime = DateTime.now(); var currentTime = DateTime.now();
try { try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) { if (userId == null) {
debugPrint('User is not authenticated'); debugPrint("User is not authenticated");
return; return;
} }
@ -341,20 +351,19 @@ class FirebaseNotificationService
.doc(userId) .doc(userId)
.collection(plannedNotificationsCollection); .collection(plannedNotificationsCollection);
QuerySnapshot querySnapshot = await plannedNotificationsResult.get(); var querySnapshot = await plannedNotificationsResult.get();
if (querySnapshot.docs.isEmpty) { if (querySnapshot.docs.isEmpty) {
debugPrint('No scheduled notifications to be pushed'); debugPrint("No scheduled notifications to be pushed");
return; return;
} }
List<NotificationModel> plannedNotifications = var plannedNotifications = querySnapshot.docs.map((doc) {
querySnapshot.docs.map((doc) { var data = doc.data()! as Map<String, dynamic>;
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
return NotificationModel.fromJson(data); return NotificationModel.fromJson(data);
}).toList(); }).toList();
for (NotificationModel notification in plannedNotifications) { for (var notification in plannedNotifications) {
if (notification.scheduledFor!.isBefore(currentTime) || if (notification.scheduledFor!.isBefore(currentTime) ||
notification.scheduledFor!.isAtSameMomentAs(currentTime)) { notification.scheduledFor!.isAtSameMomentAs(currentTime)) {
await pushNotification(notification, newNotificationCallback); await pushNotification(notification, newNotificationCallback);
@ -363,7 +372,7 @@ class FirebaseNotificationService
//Plan new recurring notification instance //Plan new recurring notification instance
if (notification.recurring) { if (notification.recurring) {
NotificationModel newNotification = NotificationModel( var newNotification = NotificationModel(
id: UniqueKey().toString(), id: UniqueKey().toString(),
title: notification.title, title: notification.title,
body: notification.body, body: notification.body,
@ -375,8 +384,8 @@ class FirebaseNotificationService
} }
} }
} }
} catch (e) { } on Exception catch (e) {
debugPrint('Error getting planned notifications: $e'); debugPrint("Error getting planned notifications: $e");
return; return;
} }
} }
@ -386,7 +395,7 @@ class FirebaseNotificationService
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid; var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) { if (userId == null) {
debugPrint('User is not authenticated'); debugPrint("User is not authenticated");
yield 0; yield 0;
} }

View file

@ -1,8 +1,7 @@
name: flutter_notification_center_firebase name: flutter_notification_center_firebase
description: "A new Flutter project." description: "A new Flutter project."
publish_to: "none" publish_to: "none"
version: 2.0.0
version: 1.4.1
environment: environment:
sdk: ">=2.18.0 <3.0.0" sdk: ">=2.18.0 <3.0.0"
@ -22,15 +21,16 @@ dependencies:
flutter_notification_center: flutter_notification_center:
git: git:
url: https://github.com/Iconica-Development/flutter_notification_center url: https://github.com/Iconica-Development/flutter_notification_center
ref: 1.4.0 ref: 2.0.0
path: packages/flutter_notification_center path: packages/flutter_notification_center
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_iconica_analysis:
flutter_lints: ^2.0.0 git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
flutter: flutter:
uses-material-design: true uses-material-design: true