notification_item.dart 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import 'package:appflowy/generated/flowy_svgs.g.dart';
  2. import 'package:appflowy/generated/locale_keys.g.dart';
  3. import 'package:appflowy_popover/appflowy_popover.dart';
  4. import 'package:easy_localization/easy_localization.dart';
  5. import 'package:fixnum/fixnum.dart';
  6. import 'package:flowy_infra/theme_extension.dart';
  7. import 'package:flowy_infra_ui/flowy_infra_ui.dart';
  8. import 'package:flutter/material.dart';
  9. DateFormat _dateFormat(BuildContext context) => DateFormat('MMM d, y');
  10. class NotificationItem extends StatefulWidget {
  11. const NotificationItem({
  12. super.key,
  13. required this.reminderId,
  14. required this.title,
  15. required this.scheduled,
  16. required this.body,
  17. required this.isRead,
  18. this.onAction,
  19. this.onDelete,
  20. this.onReadChanged,
  21. });
  22. final String reminderId;
  23. final String title;
  24. final Int64 scheduled;
  25. final String body;
  26. final bool isRead;
  27. final VoidCallback? onAction;
  28. final VoidCallback? onDelete;
  29. final void Function(bool isRead)? onReadChanged;
  30. @override
  31. State<NotificationItem> createState() => _NotificationItemState();
  32. }
  33. class _NotificationItemState extends State<NotificationItem> {
  34. final PopoverMutex mutex = PopoverMutex();
  35. bool _isHovering = false;
  36. @override
  37. Widget build(BuildContext context) {
  38. return MouseRegion(
  39. onEnter: (_) => _onHover(true),
  40. onExit: (_) => _onHover(false),
  41. cursor: widget.onAction != null
  42. ? SystemMouseCursors.click
  43. : MouseCursor.defer,
  44. child: Stack(
  45. children: [
  46. GestureDetector(
  47. onTap: widget.onAction,
  48. child: Opacity(
  49. opacity: widget.isRead ? 0.5 : 1,
  50. child: Container(
  51. padding: const EdgeInsets.all(10),
  52. decoration: BoxDecoration(
  53. borderRadius: const BorderRadius.all(Radius.circular(6)),
  54. color: _isHovering && widget.onAction != null
  55. ? AFThemeExtension.of(context).lightGreyHover
  56. : Colors.transparent,
  57. ),
  58. child: Row(
  59. crossAxisAlignment: CrossAxisAlignment.start,
  60. children: [
  61. Stack(
  62. children: [
  63. const FlowySvg(FlowySvgs.time_s, size: Size.square(20)),
  64. if (!widget.isRead)
  65. Positioned(
  66. bottom: 1,
  67. right: 1,
  68. child: DecoratedBox(
  69. decoration: BoxDecoration(
  70. shape: BoxShape.circle,
  71. color: AFThemeExtension.of(context).warning,
  72. ),
  73. child: const SizedBox(height: 8, width: 8),
  74. ),
  75. ),
  76. ],
  77. ),
  78. const HSpace(10),
  79. Expanded(
  80. child: Column(
  81. crossAxisAlignment: CrossAxisAlignment.start,
  82. mainAxisAlignment: MainAxisAlignment.center,
  83. children: [
  84. Row(
  85. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  86. children: [
  87. Flexible(
  88. child: FlowyText.semibold(widget.title),
  89. ),
  90. FlowyText.regular(
  91. _scheduledString(widget.scheduled),
  92. fontSize: 10,
  93. ),
  94. ],
  95. ),
  96. const VSpace(5),
  97. FlowyText.regular(widget.body, maxLines: 4),
  98. ],
  99. ),
  100. ),
  101. ],
  102. ),
  103. ),
  104. ),
  105. ),
  106. if (_isHovering)
  107. Positioned(
  108. right: 4,
  109. top: 4,
  110. child: NotificationItemActions(
  111. isRead: widget.isRead,
  112. onDelete: widget.onDelete,
  113. onReadChanged: widget.onReadChanged,
  114. ),
  115. ),
  116. ],
  117. ),
  118. );
  119. }
  120. String _scheduledString(Int64 secondsSinceEpoch) =>
  121. _dateFormat(context).format(
  122. DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000),
  123. );
  124. void _onHover(bool isHovering) => setState(() => _isHovering = isHovering);
  125. }
  126. class NotificationItemActions extends StatelessWidget {
  127. const NotificationItemActions({
  128. super.key,
  129. required this.isRead,
  130. this.onDelete,
  131. this.onReadChanged,
  132. });
  133. final bool isRead;
  134. final VoidCallback? onDelete;
  135. final void Function(bool isRead)? onReadChanged;
  136. @override
  137. Widget build(BuildContext context) {
  138. return Container(
  139. height: 30,
  140. decoration: BoxDecoration(
  141. color: Theme.of(context).cardColor,
  142. border: Border.all(color: Theme.of(context).dividerColor),
  143. borderRadius: BorderRadius.circular(6),
  144. ),
  145. child: IntrinsicHeight(
  146. child: Row(
  147. children: [
  148. if (isRead) ...[
  149. FlowyIconButton(
  150. height: 28,
  151. tooltipText:
  152. LocaleKeys.reminderNotification_tooltipMarkUnread.tr(),
  153. icon: const FlowySvg(FlowySvgs.restore_s),
  154. onPressed: () => onReadChanged?.call(false),
  155. ),
  156. ] else ...[
  157. FlowyIconButton(
  158. height: 28,
  159. tooltipText:
  160. LocaleKeys.reminderNotification_tooltipMarkRead.tr(),
  161. icon: const FlowySvg(FlowySvgs.messages_s),
  162. onPressed: () => onReadChanged?.call(true),
  163. ),
  164. ],
  165. VerticalDivider(
  166. width: 3,
  167. thickness: 1,
  168. indent: 2,
  169. endIndent: 2,
  170. color: Theme.of(context).dividerColor,
  171. ),
  172. FlowyIconButton(
  173. height: 28,
  174. tooltipText: LocaleKeys.reminderNotification_tooltipDelete.tr(),
  175. icon: const FlowySvg(FlowySvgs.delete_s),
  176. onPressed: onDelete,
  177. ),
  178. ],
  179. ),
  180. ),
  181. );
  182. }
  183. }