diff --git a/CHANGELOG.md b/CHANGELOG.md index 28243be7..db965550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.32 + +* [DSToast] Fixed toast overlap + ## 0.0.31 * [DSText] Add selectable text feature diff --git a/assets/images/blip_ balloon.svg b/assets/images/blip_balloon.svg similarity index 100% rename from assets/images/blip_ balloon.svg rename to assets/images/blip_balloon.svg diff --git a/lib/blip_ds.dart b/lib/blip_ds.dart index e7544495..906bbf91 100644 --- a/lib/blip_ds.dart +++ b/lib/blip_ds.dart @@ -1,31 +1,60 @@ library blip_ds; +/// Extensions +export 'package:blip_ds/src/extensions/ds_border_radius.extension.dart' + show DSBorderRadiusExtension, DSBorderRadiusListExtension; +export 'package:blip_ds/src/extensions/ds_delivery_report_status.extension.dart' + show DSDeliveryReportStatusExtension; + +export 'src/enums/ds_align.enum.dart' show DSAlign; +/// Enums +export 'src/enums/ds_border_radius.enum.dart' show DSBorderRadius; +export 'src/enums/ds_delivery_report_status.enum.dart' + show DSDeliveryReportStatus; +export 'src/enums/ds_ticket_message_type.enum.dart' show DSTicketMessageType; +export 'src/enums/ds_toast_action_type.enum.dart' show DSToastActionType; +export 'src/models/ds_message_bubble_avatar_config.model.dart' + show DSMessageBubbleAvatarConfig; +export 'src/models/ds_message_bubble_style.model.dart' + show DSMessageBubbleStyle; +/// Models +export 'src/models/ds_message_item.model.dart' show DSMessageItemModel; +export 'src/models/ds_toast_props.model.dart' show DSToastProps; +export 'src/services/ds_bottom_sheet.service.dart' show DSBottomSheetService; +/// Services +export 'src/services/ds_dialog.service.dart' show DSDialogService; +export 'src/services/ds_toast.service.dart' show DSToastService; +/// Themes / Colors +export 'src/themes/colors/ds_colors.theme.dart' show DSColors; +export 'src/themes/colors/ds_linear_gradient.theme.dart' show DSLinearGradient; +/// Icons +export 'src/themes/icons/ds_icons.dart' show DSIcons; +export 'src/themes/texts/ds_cupertino_theme_data.theme.dart' + show DSCupertinoThemeData; +export 'src/themes/texts/ds_text_selection_theme.theme.dart' + show DSTextSelectionThemeData; +/// Themes / Texts +export 'src/themes/texts/ds_text_theme.theme.dart' show DSTextTheme; +export 'src/themes/texts/styles/ds_body_text_style.theme.dart' + show DSBodyTextStyle; +export 'src/themes/texts/styles/ds_button_text_style.theme.dart' + show DSButtonTextStyle; +export 'src/themes/texts/styles/ds_caption_small_text_style.theme.dart' + show DSCaptionSmallTextStyle; +export 'src/themes/texts/styles/ds_caption_text_style.theme.dart' + show DSCaptionTextStyle; +/// Themes / Texts / Styles +export 'src/themes/texts/styles/ds_headline_large_text_style.theme.dart' + show DSHeadlineLargeTextStyle; +export 'src/themes/texts/styles/ds_headline_small_text_style.theme.dart' + show DSHeadlineSmallTextStyle; +/// Themes / Texts / Utils +export 'src/themes/texts/utils/ds_font_families.theme.dart' show DSFontFamilies; +export 'src/themes/texts/utils/ds_font_weights.theme.dart' show DSFontWeights; +export 'src/utils/ds_animate.util.dart' show DSAnimate; +export 'src/utils/ds_linkify.util.dart' show DSLinkify; /// Utils export 'src/utils/ds_utils.util.dart' show DSUtils; -export 'src/utils/ds_linkify.util.dart' show DSLinkify; -export 'src/utils/ds_animate.util.dart' show DSAnimate; - -/// Widgets / Chat -export 'src/widgets/chat/ds_message_bubble.widget.dart' show DSMessageBubble; -export 'src/widgets/chat/ds_text_message_bubble.widget.dart' - show DSTextMessageBubble; -export 'src/widgets/chat/ds_file_message_bubble.widget.dart' - show DSFileMessageBubble; -export 'src/widgets/chat/ds_image_message_bubble.widget.dart' - show DSImageMessageBubble; -export 'src/widgets/chat/ds_unsupported_content_message_bubble.widget.dart' - show DSUnsupportedContentMessageBubble; -export 'src/widgets/chat/ds_url_preview.widget.dart' show DSUrlPreview; -export 'src/widgets/chat/ds_delivery_report_icon.widget.dart' - show DSDeliveryReportIcon; - -/// Widgets / Chat / Audio -export 'src/widgets/chat/audio/ds_audio_message_bubble.widget.dart' - show DSAudioMessageBubble; - -/// Widgets / Chat / Form Fields -export 'src/widgets/fields/ds_text_form_field.widget.dart' show DSTextFormField; - /// Widgets / Animations export 'src/widgets/animations/ds_animated_size.widget.dart' show DSAnimatedSize; @@ -34,126 +63,76 @@ export 'src/widgets/animations/ds_fading_circle_loading.widget.dart' export 'src/widgets/animations/ds_ring_loading.widget.dart' show DSRingLoading; export 'src/widgets/animations/ds_spinner_loading.widget.dart' show DSSpinnerLoading; - +export 'src/widgets/buttons/ds_attachment_button.widget.dart' + show DSAttachmentButton; /// Widgets / Buttons export 'src/widgets/buttons/ds_button.widget.dart' show DSButton; +export 'src/widgets/buttons/ds_icon_button.widget.dart' show DSIconButton; export 'src/widgets/buttons/ds_primary_button.widget.dart' show DSPrimaryButton; export 'src/widgets/buttons/ds_secondary_button.widget.dart' show DSSecondaryButton; +export 'src/widgets/buttons/ds_send_button.widget.dart' show DSSendButton; export 'src/widgets/buttons/ds_tertiary_button.widget.dart' show DSTertiaryButton; -export 'src/widgets/buttons/ds_send_button.widget.dart' show DSSendButton; -export 'src/widgets/buttons/ds_icon_button.widget.dart' show DSIconButton; -export 'src/widgets/buttons/ds_attachment_button.widget.dart' - show DSAttachmentButton; - -/// Widgets / Texts -export 'src/widgets/texts/ds_text.widget.dart' show DSText; +/// Widgets / Chat / Audio +export 'src/widgets/chat/audio/ds_audio_message_bubble.widget.dart' + show DSAudioMessageBubble; +/// Widgets / Carrousel +export 'src/widgets/chat/ds_carrousel.widget.dart' show DSCarrousel; +export 'src/widgets/chat/ds_delivery_report_icon.widget.dart' + show DSDeliveryReportIcon; +export 'src/widgets/chat/ds_file_message_bubble.widget.dart' + show DSFileMessageBubble; +export 'src/widgets/chat/ds_image_message_bubble.widget.dart' + show DSImageMessageBubble; +/// Widgets / Chat +export 'src/widgets/chat/ds_message_bubble.widget.dart' show DSMessageBubble; +export 'src/widgets/chat/ds_text_message_bubble.widget.dart' + show DSTextMessageBubble; +export 'src/widgets/chat/ds_unsupported_content_message_bubble.widget.dart' + show DSUnsupportedContentMessageBubble; +export 'src/widgets/chat/ds_url_preview.widget.dart' show DSUrlPreview; +/// Widgets / Weblink +export 'src/widgets/chat/ds_weblink.widget.dart' show DSWeblink; +export 'src/widgets/chat/typing/ds_typing_dot_animation.widget.dart' + show DSTypingDotAnimation; +/// Widgets / Typing +export 'src/widgets/chat/typing/ds_typing_message_bubble.widget.dart' + show DSTypingAnimationMessageBubble; +/// Widgets / Video +export 'src/widgets/chat/video/ds_video_message_bubble.widget.dart' + show DSVideoMessageBubble; +export 'src/widgets/chat/video/ds_video_player.widget.dart' show DSVideoPlayer; +/// Widgets / Chat / Form Fields +export 'src/widgets/fields/ds_text_form_field.widget.dart' show DSTextFormField; +/// Widgets / Radio +export 'src/widgets/radio/ds_radio.widget.dart' show DSRadio; +export 'src/widgets/radio/ds_radio_tile.widget.dart' show DSRadioTile; +export 'src/widgets/switch/ds_switch.widget.dart' show DSSwitch; +/// Widgets / SwitchTile +export 'src/widgets/switch/ds_switch_tile.widget.dart' show DSSwitchTile; +export 'src/widgets/tags/ds_input_chip.widget.dart' show DSInputChip; export 'src/widgets/texts/ds_body_text.widget.dart' show DSBodyText; +export 'src/widgets/texts/ds_button_text.widget.dart' show DSButtonText; +export 'src/widgets/texts/ds_caption_small_text.widget.dart' + show DSCaptionSmallText; +export 'src/widgets/texts/ds_caption_text.widget.dart' show DSCaptionText; export 'src/widgets/texts/ds_headline_large_text.widget.dart' show DSHeadlineLargeText; export 'src/widgets/texts/ds_headline_small_text.widget.dart' show DSHeadlineSmallText; -export 'src/widgets/texts/ds_caption_text.widget.dart' show DSCaptionText; -export 'src/widgets/texts/ds_caption_small_text.widget.dart' - show DSCaptionSmallText; -export 'src/widgets/texts/ds_button_text.widget.dart' show DSButtonText; - +/// Widgets / Texts +export 'src/widgets/texts/ds_text.widget.dart' show DSText; +/// Widgets / Information message +export 'src/widgets/ticket_message/ds_ticket_message.widget.dart' + show DSTicketMessage; /// Widgets / Utils export 'src/widgets/utils/ds_cached_network_image_view.widget.dart' show DSCachedNetworkImageView; -export 'src/widgets/utils/ds_user_avatar.widget.dart' show DSUserAvatar; -export 'src/widgets/utils/ds_group_card.widget.dart' show DSGroupCard; -export 'src/widgets/utils/ds_header.widget.dart' show DSHeader; export 'src/widgets/utils/ds_chip.widget.dart' show DSChip; export 'src/widgets/utils/ds_divider.widget.dart' show DSDivider; -export 'src/widgets/tags/ds_input_chip.widget.dart' show DSInputChip; export 'src/widgets/utils/ds_file_extension_icon.util.dart' show DSFileExtensionIcon; - -/// Enums -export 'src/enums/ds_border_radius.enum.dart' show DSBorderRadius; -export 'src/enums/ds_align.enum.dart' show DSAlign; -export 'src/enums/ds_delivery_report_status.enum.dart' - show DSDeliveryReportStatus; -export 'src/enums/ds_toast_action_type.enum.dart' show DSToastActionType; -export 'src/enums/ds_ticket_message_type.enum.dart' show DSTicketMessageType; - -/// Extensions -export 'package:blip_ds/src/extensions/ds_border_radius.extension.dart' - show DSBorderRadiusExtension, DSBorderRadiusListExtension; -export 'package:blip_ds/src/extensions/ds_delivery_report_status.extension.dart' - show DSDeliveryReportStatusExtension; - -/// Themes / Colors -export 'src/themes/colors/ds_colors.theme.dart' show DSColors; -export 'src/themes/colors/ds_linear_gradient.theme.dart' show DSLinearGradient; - -/// Themes / Texts -export 'src/themes/texts/ds_text_theme.theme.dart' show DSTextTheme; -export 'src/themes/texts/ds_text_selection_theme.theme.dart' - show DSTextSelectionThemeData; -export 'src/themes/texts/ds_cupertino_theme_data.theme.dart' - show DSCupertinoThemeData; - -/// Themes / Texts / Utils -export 'src/themes/texts/utils/ds_font_families.theme.dart' show DSFontFamilies; -export 'src/themes/texts/utils/ds_font_weights.theme.dart' show DSFontWeights; - -/// Themes / Texts / Styles -export 'src/themes/texts/styles/ds_headline_large_text_style.theme.dart' - show DSHeadlineLargeTextStyle; -export 'src/themes/texts/styles/ds_headline_small_text_style.theme.dart' - show DSHeadlineSmallTextStyle; -export 'src/themes/texts/styles/ds_caption_small_text_style.theme.dart' - show DSCaptionSmallTextStyle; -export 'src/themes/texts/styles/ds_body_text_style.theme.dart' - show DSBodyTextStyle; -export 'src/themes/texts/styles/ds_button_text_style.theme.dart' - show DSButtonTextStyle; -export 'src/themes/texts/styles/ds_caption_text_style.theme.dart' - show DSCaptionTextStyle; - -/// Services -export 'src/services/ds_dialog.service.dart' show DSDialogService; -export 'src/services/ds_toast.service.dart' show DSToastService; -export 'src/services/ds_bottom_sheet.service.dart' show DSBottomSheetService; - -/// Models -export 'src/models/ds_message_item.model.dart' show DSMessageItemModel; -export 'src/models/ds_message_bubble_style.model.dart' - show DSMessageBubbleStyle; -export 'src/models/ds_message_bubble_avatar_config.model.dart' - show DSMessageBubbleAvatarConfig; - -/// Widgets / Typing -export 'src/widgets/chat/typing/ds_typing_message_bubble.widget.dart' - show DSTypingAnimationMessageBubble; -export 'src/widgets/chat/typing/ds_typing_dot_animation.widget.dart' - show DSTypingDotAnimation; - -/// Widgets / SwitchTile -export 'src/widgets/switch/ds_switch_tile.widget.dart' show DSSwitchTile; -export 'src/widgets/switch/ds_switch.widget.dart' show DSSwitch; - -/// Widgets / Radio -export 'src/widgets/radio/ds_radio.widget.dart' show DSRadio; -export 'src/widgets/radio/ds_radio_tile.widget.dart' show DSRadioTile; - -/// Widgets / Video -export 'src/widgets/chat/video/ds_video_message_bubble.widget.dart' - show DSVideoMessageBubble; -export 'src/widgets/chat/video/ds_video_player.widget.dart' show DSVideoPlayer; - -/// Widgets / Carrousel -export 'src/widgets/chat/ds_carrousel.widget.dart' show DSCarrousel; - -/// Widgets / Information message -export 'src/widgets/ticket_message/ds_ticket_message.widget.dart' - show DSTicketMessage; - -/// Icons -export 'src/themes/icons/ds_icons.dart' show DSIcons; - -/// Widgets / Weblink -export 'src/widgets/chat/ds_weblink.widget.dart' show DSWeblink; +export 'src/widgets/utils/ds_group_card.widget.dart' show DSGroupCard; +export 'src/widgets/utils/ds_header.widget.dart' show DSHeader; +export 'src/widgets/utils/ds_user_avatar.widget.dart' show DSUserAvatar; diff --git a/lib/src/models/ds_toast_props.model.dart b/lib/src/models/ds_toast_props.model.dart new file mode 100644 index 00000000..c2e5c3f0 --- /dev/null +++ b/lib/src/models/ds_toast_props.model.dart @@ -0,0 +1,59 @@ +import 'package:flutter/foundation.dart'; + +import '../enums/ds_toast_action_type.enum.dart'; +import '../enums/ds_toast_type.enum.dart'; + +class DSToastProps { + /// Creates a new Design System's [DSToastProps] + DSToastProps({ + this.title, + this.message, + this.actionType = DSToastActionType.icon, + this.buttonText, + this.onPressedButton, + this.toastDuration, + this.positionOffset = 60.0, + }) : assert( + (actionType == DSToastActionType.button && + buttonText != null && + onPressedButton != null) || + actionType != DSToastActionType.button, + ), + assert( + (title?.isNotEmpty ?? false) || (message?.isNotEmpty ?? false), + ); + + /// Use [title] to show title in toast. + /// + /// The [title] parameter is optional. If not defined, it will not be shown. + final String? title; + + /// Use [message] to show the message below the title in the toast + final String? message; + + /// Use [actionType] to set the action type of the toast output resource + /// [DSActionType.icon] or [DSActionType.button]. + final DSToastActionType actionType; + + /// If you want to replace the close icon with a custom one, use the [buttonText] + /// parameter to define the name + final String? buttonText; + + /// When using a custom button, it is possible to define a callback + /// function to perform some action. Use the [onPressedButton] parameter. + final Function? onPressedButton; + + /// Use [positionOffset] to position the toast, moving up relative to the bottom of the screen. + final double positionOffset; + + /// Set a time value in milliseconds using the [toastDuration] parameter to + /// keep the toast on the screen without closing. If you set the value to 0, the toast + /// will not close automatically, depending on a manual action. + final int? toastDuration; + + /// [DSToast] type that will be automatically set + DSToastType type = DSToastType.system; + + /// [DSToast] unique key that will be automatically set + UniqueKey? key; +} diff --git a/lib/src/services/ds_toast.service.dart b/lib/src/services/ds_toast.service.dart index 7d23add1..4638b015 100644 --- a/lib/src/services/ds_toast.service.dart +++ b/lib/src/services/ds_toast.service.dart @@ -1,341 +1,145 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; -import 'package:simple_animations/simple_animations.dart'; -import 'package:blip_ds/blip_ds.dart'; import '../enums/ds_toast_type.enum.dart'; +import '../models/ds_toast_props.model.dart'; +import '../utils/ds_utils.util.dart'; +import '../widgets/toast/ds_toast.widget.dart'; /// A Design System's [DSToastService] used to display a toast. -class DSToastService { - /// Creates a new Design System's [DSToastService] - DSToastService({ - this.title, - this.message, - this.actionType = DSToastActionType.icon, - this.buttonText, - this.onPressedButton, - this.toastDuration, - this.positionOffset = 16.0, - }) : assert( - (actionType == DSToastActionType.button && - buttonText != null && - onPressedButton != null) || - actionType != DSToastActionType.button, - ), - assert( - (title?.isNotEmpty ?? false) || (message?.isNotEmpty ?? false), - ); - - /// Use [title] to show title in toast. - /// - /// The [title] parameter is optional. If not defined, it will not be shown. - final String? title; - - /// Use [message] to show the message below the title in the toast - final String? message; - - /// Use [actionType] to set the action type of the toast output resource - /// [DSActionType.icon] or [DSActionType.button]. - final DSToastActionType actionType; - - /// If you want to replace the close icon with a custom one, use the [buttonText] - /// parameter to define the name - final String? buttonText; - - /// When using a custom button, it is possible to define a callback - /// function to perform some action. Use the [onPressedButton] parameter. - final Function? onPressedButton; - - /// Use [positionOffset] to position the toast, moving up relative to the bottom of the screen. - final double? positionOffset; - - /// Set a time value in milliseconds using the [toastDuration] parameter to - /// keep the toast on the screen without closing. If you set the value to 0, the toast - /// will not close automatically, depending on a manual action. - final int? toastDuration; // miliseconds - - /// Button widget to show - Widget? mainButton; - - /// Icon widget to show - Widget? icon; - - /// Overlay, where the toast will be overlayed - OverlayEntry? _overlayEntry; - - /// Color of elements in toast - Color? backgroundColor; - final Color titleColor = DSColors.neutralDarkCity; - final Color textColor = DSColors.neutralDarkCity; - - /// Toast opening and closing animation duration - final int animationDuration = 300; // miliseconds - - /// Animation scene controller - Control? _controlAnimation = Control.stop; - - /// Toast state manager to be recreated when closing - StateSetter? state; - - /// Timer to keep toast on screen - Timer? _timeToastDuration; - - /// Overlay content while displaying toast - Widget? _content; - - void _show(final DSToastType type) { - _prepareToast(type); +abstract class DSToastService { + static final _visibleToasts = RxList(); + static OverlayEntry? _overlayEntry; + static ScrollController? _controller; + + static void _show(final DSToastProps props) { + final toast = DSToast( + key: UniqueKey(), + props: props, + onClose: _onClose, + ); - _overlayEntry = createOverlayEntry(); + _visibleToasts.insert(0, toast); + _controller?.animateTo( + 0, + duration: DSUtils.defaultAnimationDuration, + curve: Curves.easeInOut, + ); - Overlay.of(Get.overlayContext!).insert(_overlayEntry!); + if (_overlayEntry == null) { + _controller = ScrollController(); + _overlayEntry = _createOverlayEntry(); - _controlAnimation = Control.playFromStart; + Overlay.of(Get.overlayContext!).insert(_overlayEntry!); + } } - OverlayEntry createOverlayEntry() { + static OverlayEntry _createOverlayEntry() { return OverlayEntry( builder: (context) { final mediaQuery = MediaQuery.of(context); - - _content ??= Positioned( - bottom: mediaQuery.viewPadding.bottom + 70 + positionOffset!, - width: mediaQuery.size.width - 32.0, - left: 16.0, - child: Dismissible( - key: const Key('ds-toast-key'), - onDismissed: (direction) { - if (_overlayEntry != null) { - _controlAnimation = Control.stop; - _close(); - } - }, - child: _animeCard(), - ), - ); - - return _content!; - }, - ); - } - - /// Create the toast card - Material _cardToast() { - return Material( - elevation: 10.0, - borderRadius: BorderRadius.circular(8.0), - clipBehavior: Clip.hardEdge, - child: Stack( - children: [ - Positioned( - left: -15, - top: -2, - child: SvgPicture.asset( - 'assets/images/blip_ balloon.svg', - package: DSUtils.packageName, - ), - ), - Container( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(8.0), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (icon != null) icon!, - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (title != null) - DSHeadlineSmallText( - title, - overflow: TextOverflow.visible, - ), - if (message != null) - DSBodyText( - message, - overflow: TextOverflow.visible, + final paddingBottom = mediaQuery.padding.bottom; + final viewInsetsBottom = mediaQuery.viewInsets.bottom; + + final offset = _visibleToasts.isNotEmpty + ? _visibleToasts[_visibleToasts.length - 1].props.positionOffset + : 0; + + final content = Align( + alignment: Alignment.bottomCenter, + child: SafeArea( + bottom: false, + child: Stack( + children: [ + ShaderMask( + shaderCallback: (Rect rect) => const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white, + Colors.transparent, + ], + stops: [-1, .04], + ).createShader(rect), + blendMode: BlendMode.dstOut, + child: Container( + margin: EdgeInsets.only( + bottom: paddingBottom + viewInsetsBottom + offset, + top: 0, + ), + constraints: BoxConstraints( + maxHeight: (mediaQuery.size.height - + mediaQuery.viewInsets.bottom) * + .50, + ), + child: Obx( + () { + return ListView( + controller: _controller, + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.only( + top: 20, ), - ], + shrinkWrap: true, + reverse: true, + children: _visibleToasts, + ); + }, ), ), ), - _setMainButton(), ], ), ), - ], - ), - ); - } - - /// Prepares the presentation of toast elements according to the type - void _prepareToast(final DSToastType type) { - switch (type) { - case DSToastType.warning: - backgroundColor = DSColors.primaryYellowsCorn; - icon = _setIcon(DSIcons.warning_outline); - break; - case DSToastType.error: - backgroundColor = DSColors.extendRedsFlower; - icon = _setIcon(DSIcons.error_outline); - break; - case DSToastType.system: - backgroundColor = DSColors.illustrationBlueGenie; - icon = _setIcon(DSIcons.message_ballon_outline); - break; - case DSToastType.notification: - backgroundColor = Colors.transparent; - icon = _setIcon(DSIcons.bell_outline); - break; - default: - backgroundColor = DSColors.primaryGreensMint; - icon = _setIcon(DSIcons.like_outline); - } - } - - /// Switches between exit button types - Widget _setMainButton() { - return actionType == DSToastActionType.icon - ? DSIconButton( - size: 40.0, - icon: const Icon(DSIcons.close_outline), - onPressed: () { - state!(() { - _stopTimer(); - _controlAnimation = Control.playReverse; - }); - }, - ) - : actionType == DSToastActionType.button - ? DSTertiaryButton( - label: buttonText, - onPressed: () { - onPressedButton!(); - state!( - () { - _stopTimer(); - _controlAnimation = Control.playReverse; - }, - ); - }, - ) - : const SizedBox.shrink(); - } - - /// Create and manage the toast animation - StatefulBuilder _animeCard() { - double start = (MediaQuery.of(Get.context!).size.width) * -1.0; - double end = 0.0; - - final duration = toastDuration ?? - ((message?.length ?? 0) * 100 + (title?.length ?? 0) * 100); - - return StatefulBuilder( - builder: (BuildContext context, StateSetter mystate) { - state = mystate; - return CustomAnimationBuilder( - duration: Duration(milliseconds: animationDuration), - control: _controlAnimation!, - tween: Tween(begin: start, end: end), - builder: (_, value, child) { - return Transform.translate( - offset: Offset(value, 0.0), - child: child, - ); - }, - child: _cardToast(), - onCompleted: () async { - if (toastDuration == 0) { - if (_controlAnimation == Control.playReverse) { - _close(); - } - } else { - if (_controlAnimation == Control.playFromStart) { - _timeToastDuration = Timer( - Duration(milliseconds: duration), - () { - state!( - () { - _controlAnimation = Control.playReverse; - }, - ); - }, - ); - } else { - _close(); - } - } - }, ); + + return content; }, ); } - Widget _setIcon(final IconData icon) { - return Padding( - padding: const EdgeInsets.only(top: 8), - child: Icon( - icon, - color: DSColors.neutralDarkCity, - size: 24.0, - ), - ); - } + static void _onClose(DSToast toast) { + _visibleToasts.remove(toast); - void _close() { - if (_overlayEntry != null) { - _stopTimer(); + if (_visibleToasts.isEmpty && _overlayEntry != null) { _overlayEntry!.remove(); _overlayEntry = null; - } - } - - void _stopTimer() { - if (_timeToastDuration != null) { - _content = null; - _timeToastDuration!.cancel(); - _timeToastDuration = null; + _controller = null; } } /// Shows a [DSToastType.warning] toast type - void warning() { - _close(); - _show(DSToastType.warning); + static void warning(final DSToastProps props) { + props.type = DSToastType.warning; + + _show(props); } /// Shows a [DSToastType.system] toast type - void system() { - _close(); - _show(DSToastType.system); + static void system(final DSToastProps props) { + props.type = DSToastType.system; + + _show(props); } /// Shows a [DSToastType.error] toast type - void error() { - _close(); - _show(DSToastType.error); + static void error(final DSToastProps props) { + props.type = DSToastType.error; + + _show(props); } /// Shows a [DSToastType.success] toast type - void success() { - _close(); - _show(DSToastType.success); + static void success(final DSToastProps props) { + props.type = DSToastType.success; + + _show(props); } /// Shows a [DSToastType.notification] toast type - void notification() { - _close(); - _show(DSToastType.notification); + static void notification(final DSToastProps props) { + props.type = DSToastType.notification; + + _show(props); } } diff --git a/lib/src/widgets/toast/ds_toast.widget.dart b/lib/src/widgets/toast/ds_toast.widget.dart new file mode 100644 index 00000000..88c9ed1f --- /dev/null +++ b/lib/src/widgets/toast/ds_toast.widget.dart @@ -0,0 +1,278 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:simple_animations/simple_animations.dart'; + +import '../../enums/ds_toast_action_type.enum.dart'; +import '../../enums/ds_toast_type.enum.dart'; +import '../../models/ds_toast_props.model.dart'; +import '../../themes/colors/ds_colors.theme.dart'; +import '../../themes/icons/ds_icons.dart'; +import '../../utils/ds_utils.util.dart'; +import '../buttons/ds_icon_button.widget.dart'; +import '../buttons/ds_tertiary_button.widget.dart'; +import '../texts/ds_body_text.widget.dart'; +import '../texts/ds_headline_small_text.widget.dart'; + +class DSToast extends StatefulWidget { + /// A Design System's [DSToast] widget. + const DSToast({ + super.key, + required this.props, + required this.onClose, + }); + + /// A property of type [DSToastProps] used to configure the toast. + final DSToastProps props; + + /// A function called when the toast is closed + final void Function(DSToast) onClose; + + @override + State createState() => _DSToastState(); +} + +class _DSToastState extends State with AutomaticKeepAliveClientMixin { + /// Icon widget to show + Widget? icon; + + /// Color of elements in toast + Color? backgroundColor; + + /// Animation scene controller + Control? _controlAnimation = Control.stop; + + /// Toast state manager to be recreated when closing + StateSetter? state; + + /// Timer to keep toast on screen + Timer? _timeToastDuration; + + DSToastProps get props => widget.props; + + @override + void initState() { + super.initState(); + + _prepareToast(); + start(); + } + + void close() { + widget.onClose(widget); + _stopTimer(); + } + + void _stopTimer() { + if (_timeToastDuration != null) { + _timeToastDuration!.cancel(); + _timeToastDuration = null; + } + } + + void start() { + _controlAnimation = Control.playFromStart; + } + + void stop() { + _controlAnimation = Control.stop; + } + + /// Prepares the presentation of toast elements according to the type + void _prepareToast() { + switch (props.type) { + case DSToastType.warning: + backgroundColor = DSColors.primaryYellowsCorn; + icon = _setIcon(DSIcons.warning_outline); + break; + case DSToastType.error: + backgroundColor = DSColors.extendRedsFlower; + icon = _setIcon(DSIcons.error_outline); + break; + case DSToastType.system: + backgroundColor = DSColors.illustrationBlueGenie; + icon = _setIcon(DSIcons.message_ballon_outline); + break; + case DSToastType.notification: + backgroundColor = Colors.transparent; + icon = _setIcon(DSIcons.bell_outline); + break; + default: + backgroundColor = DSColors.primaryGreensMint; + icon = _setIcon(DSIcons.like_outline); + } + } + + Widget _setIcon(final IconData icon) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Icon( + icon, + color: DSColors.neutralDarkCity, + size: 24.0, + ), + ); + } + + /// Create the toast card + Material _cardToast() { + return Material( + elevation: 10.0, + borderRadius: BorderRadius.circular(8.0), + clipBehavior: Clip.hardEdge, + child: Stack( + children: [ + Positioned( + left: -15, + top: -2, + child: SvgPicture.asset( + 'assets/images/blip_balloon.svg', + package: DSUtils.packageName, + ), + ), + Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (icon != null) icon!, + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (props.title != null) + DSHeadlineSmallText( + props.title, + overflow: TextOverflow.visible, + ), + if (props.message != null) + DSBodyText( + props.message, + overflow: TextOverflow.visible, + ), + ], + ), + ), + ), + _setMainButton(), + ], + ), + ), + ], + ), + ); + } + + /// Switches between exit button types + Widget _setMainButton() { + return props.actionType == DSToastActionType.icon + ? DSIconButton( + size: 40.0, + icon: const Icon(DSIcons.close_outline), + onPressed: () { + state!(() { + _stopTimer(); + _controlAnimation = Control.playReverse; + }); + }, + ) + : props.actionType == DSToastActionType.button + ? DSTertiaryButton( + label: props.buttonText, + onPressed: () { + props.onPressedButton!(); + state!( + () { + _stopTimer(); + _controlAnimation = Control.playReverse; + }, + ); + }, + ) + : const SizedBox.shrink(); + } + + /// Create and manage the toast animation + @override + Widget build(BuildContext context) { + super.build(context); + + double start = (MediaQuery.of(Get.context!).size.width) * -1.0; + double end = 0.0; + + final duration = props.toastDuration ?? + ((props.message?.length ?? 0) * 100 + (props.title?.length ?? 0) * 100); + + return Dismissible( + key: widget.key!, + onDismissed: (direction) { + stop(); + close(); + }, + child: StatefulBuilder( + builder: (BuildContext context, StateSetter mystate) { + state = mystate; + const defaultPadding = 16.0; + + return CustomAnimationBuilder( + duration: DSUtils.defaultAnimationDuration, + control: _controlAnimation!, + tween: Tween(begin: start, end: end), + builder: (_, value, child) { + return Transform.translate( + offset: Offset(value, 0.0), + child: child, + ); + }, + child: Container( + padding: const EdgeInsets.fromLTRB( + defaultPadding, + 0, + defaultPadding, + defaultPadding, + ), + child: _cardToast(), + ), + onCompleted: () async { + if (props.toastDuration == 0) { + if (_controlAnimation == Control.playReverse) { + close(); + } + } else { + if (_controlAnimation == Control.playFromStart) { + _timeToastDuration = Timer( + Duration(milliseconds: duration), + () { + state!( + () { + _controlAnimation = Control.playReverse; + }, + ); + }, + ); + } else { + close(); + } + } + }, + ); + }, + ), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/src/widgets/utils/ds_card.widget.dart b/lib/src/widgets/utils/ds_card.widget.dart index d919cfff..4ea76c73 100644 --- a/lib/src/widgets/utils/ds_card.widget.dart +++ b/lib/src/widgets/utils/ds_card.widget.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import '../../enums/ds_ticket_message_type.enum.dart'; + import '../../enums/ds_align.enum.dart'; import '../../enums/ds_border_radius.enum.dart'; +import '../../enums/ds_ticket_message_type.enum.dart'; import '../../models/ds_document_select.model.dart'; import '../../models/ds_message_bubble_avatar_config.model.dart'; import '../../models/ds_message_bubble_style.model.dart'; @@ -12,15 +13,15 @@ import '../chat/ds_file_message_bubble.widget.dart'; import '../chat/ds_image_message_bubble.widget.dart'; import '../chat/ds_text_message_bubble.widget.dart'; import '../chat/ds_unsupported_content_message_bubble.widget.dart'; -import '../chat/video/ds_video_message_bubble.widget.dart'; import '../chat/ds_weblink.widget.dart'; +import '../chat/video/ds_video_message_bubble.widget.dart'; import '../ticket_message/ds_ticket_message.widget.dart'; /// A Design System widget used to display a Design System's widget based in LIME protocol content types class DSCard extends StatelessWidget { /// Creates a new [DSCard] widget DSCard({ - Key? key, + super.key, required this.type, required this.content, required this.align, @@ -30,8 +31,7 @@ class DSCard extends StatelessWidget { this.avatarConfig = const DSMessageBubbleAvatarConfig(), DSMessageBubbleStyle? style, this.messageId, - }) : style = style ?? DSMessageBubbleStyle(), - super(key: key); + }) : style = style ?? DSMessageBubbleStyle(); final String type; final dynamic content; diff --git a/lib/src/widgets/utils/ds_group_card.widget.dart b/lib/src/widgets/utils/ds_group_card.widget.dart index 1fca3f49..31c45f7e 100644 --- a/lib/src/widgets/utils/ds_group_card.widget.dart +++ b/lib/src/widgets/utils/ds_group_card.widget.dart @@ -10,9 +10,9 @@ import '../../models/ds_message_bubble_style.model.dart'; import '../../models/ds_message_item.model.dart'; import '../../themes/colors/ds_colors.theme.dart'; import '../../themes/icons/ds_icons.dart'; +import '../../utils/ds_animate.util.dart'; import '../../utils/ds_message_content_type.util.dart'; import '../../utils/ds_utils.util.dart'; -import '../../utils/ds_animate.util.dart'; import '../buttons/ds_button.widget.dart'; import '../chat/ds_message_bubble_detail.widget.dart'; import '../chat/ds_quick_reply.widget.dart'; @@ -332,7 +332,8 @@ class _DSGroupCardState extends State { 0, Padding( key: ValueKey( - message.id ?? DateTime.now().toIso8601String()), + message.id ?? DateTime.now().toIso8601String(), + ), padding: const EdgeInsets.symmetric(horizontal: 16), child: Table( columnWidths: diff --git a/lib/src/widgets/utils/ds_user_avatar.widget.dart b/lib/src/widgets/utils/ds_user_avatar.widget.dart index 6a56cb74..8671bde2 100644 --- a/lib/src/widgets/utils/ds_user_avatar.widget.dart +++ b/lib/src/widgets/utils/ds_user_avatar.widget.dart @@ -10,16 +10,40 @@ class DSUserAvatar extends StatelessWidget { final Color textColor; const DSUserAvatar({ - Key? key, + super.key, this.text, this.uri, this.radius = 25.0, this.backgroundColor = DSColors.primaryGreensTrue, this.textColor = Colors.black, - }) : super(key: key); + }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context) => uri != null + ? CachedNetworkImage( + imageUrl: uri.toString(), + imageBuilder: (_, image) => CircleAvatar( + radius: radius, + backgroundColor: backgroundColor, + backgroundImage: image, + ), + progressIndicatorBuilder: (_, __, downloadProgress) { + final size = Size.fromRadius(radius); + + return SizedBox( + height: size.height, + width: size.width, + child: CircularProgressIndicator( + value: downloadProgress.progress, + strokeWidth: 1, + ), + ); + }, + errorWidget: (_, __, ___) => _defaultUserIcon, + ) + : _defaultUserIcon; + + String get _initials { String initials = ''; if ((text?.isNotEmpty ?? false) && (int.tryParse(text!) == null)) { @@ -33,54 +57,36 @@ class DSUserAvatar extends StatelessWidget { initials = initials.substring(0, initials.length >= 2 ? 2 : 1); } - return uri != null - ? CachedNetworkImage( - imageUrl: uri.toString(), - imageBuilder: (context, image) => CircleAvatar( - radius: radius, - backgroundColor: backgroundColor, - backgroundImage: image, - ), - progressIndicatorBuilder: (context, url, downloadProgress) { - final size = radius * 2; - - return SizedBox( - height: size, - width: size, - child: CircularProgressIndicator( - value: downloadProgress.progress, strokeWidth: 1), - ); - }, - errorWidget: (context, url, error) => const Icon(Icons.error), - ) - : (text?.isNotEmpty ?? false) - ? CircleAvatar( - radius: radius, - backgroundColor: backgroundColor, - child: Padding( - padding: const EdgeInsets.all(2), - child: int.tryParse(text!) == null - ? DSBodyText( - initials, - color: textColor, - overflow: TextOverflow.clip, - maxLines: 1, - textAlign: TextAlign.center, - ) - : const Icon( - DSIcons.user_defaut_outline, - color: DSColors.neutralLightSnow, - size: 20.0, - ), - ), - ) - : CircleAvatar( - radius: radius, - backgroundColor: backgroundColor, - backgroundImage: const AssetImage( - 'assets/images/avatar-default.png', - package: DSUtils.packageName, - ), - ); + return initials; } + + CircleAvatar get _defaultUserIcon => (text?.isNotEmpty ?? false) + ? CircleAvatar( + radius: radius, + backgroundColor: backgroundColor, + child: Padding( + padding: const EdgeInsets.all(2.0), + child: int.tryParse(text!) == null + ? DSBodyText( + _initials, + color: textColor, + overflow: TextOverflow.clip, + maxLines: 1, + textAlign: TextAlign.center, + ) + : const Icon( + DSIcons.user_defaut_outline, + color: DSColors.neutralLightSnow, + size: 20.0, + ), + ), + ) + : CircleAvatar( + radius: radius, + backgroundColor: backgroundColor, + backgroundImage: const AssetImage( + 'assets/images/avatar-default.png', + package: DSUtils.packageName, + ), + ); } diff --git a/pubspec.yaml b/pubspec.yaml index 0fb84d5d..e5974658 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: blip_ds description: Blip Design System for Flutter. -version: 0.0.31 +version: 0.0.32 homepage: https://github.com/takenet/blip-ds-flutter#readme repository: https://github.com/takenet/blip-ds-flutter diff --git a/sample/lib/widgets/showcase/sample_toast.showcase.dart b/sample/lib/widgets/showcase/sample_toast.showcase.dart index a434933b..9838f80e 100644 --- a/sample/lib/widgets/showcase/sample_toast.showcase.dart +++ b/sample/lib/widgets/showcase/sample_toast.showcase.dart @@ -1,40 +1,36 @@ -import 'package:flutter/material.dart'; - import 'package:blip_ds/blip_ds.dart'; +import 'package:flutter/material.dart'; class SampleToastShowcase extends StatelessWidget { const SampleToastShowcase({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - final toast = DSToastService( + final toastProps = DSToastProps( title: 'Título do toast', message: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sed blandit ex. ', - // toastDuration: 3000, ); - final toastAction = DSToastService( + final toastActionProps = DSToastProps( title: 'Título do toast com action', message: 'Lorem ipsum dolor sit amet, consectetur adipisci elit.', actionType: DSToastActionType.button, buttonText: 'Action', onPressedButton: () => debugPrint('FECHOU'), - // toastDuration: 2000, ); - final toastPersistent = DSToastService( - title: 'Título do toast', + final toastPersistentProps = DSToastProps( + title: 'Persistent Toast', message: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sed blandit ex. Vivamus eget molestie urna. ', toastDuration: 0, ); - final toastNoTitle = DSToastService( + final toastNoTitleProps = DSToastProps( message: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sed blandit ex. Vivamus eget molestie urna. ', positionOffset: 100.0, - // toastDuration: 3000, ); return Column( @@ -45,11 +41,11 @@ class SampleToastShowcase extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ DSPrimaryButton( - onPressed: () => toastAction.success(), + onPressed: () => DSToastService.success(toastActionProps), label: 'Success', ), DSPrimaryButton( - onPressed: () => toast.notification(), + onPressed: () => DSToastService.notification(toastProps), label: 'Notification', ), ], @@ -59,15 +55,15 @@ class SampleToastShowcase extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ DSPrimaryButton( - onPressed: () => toast.system(), + onPressed: () => DSToastService.system(toastProps), label: 'System', ), DSPrimaryButton( - onPressed: () => toast.error(), + onPressed: () => DSToastService.error(toastProps), label: 'Error', ), DSPrimaryButton( - onPressed: () => toast.warning(), + onPressed: () => DSToastService.warning(toastProps), label: 'Warning', ), ], @@ -77,11 +73,11 @@ class SampleToastShowcase extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ DSPrimaryButton( - onPressed: () => toastPersistent.system(), + onPressed: () => DSToastService.system(toastPersistentProps), label: 'System persistent toast', ), DSPrimaryButton( - onPressed: () => toastNoTitle.system(), + onPressed: () => DSToastService.system(toastNoTitleProps), label: 'No title', ), ], diff --git a/sample/pubspec.lock b/sample/pubspec.lock index 22323a00..34426f20 100644 --- a/sample/pubspec.lock +++ b/sample/pubspec.lock @@ -23,7 +23,7 @@ packages: path: ".." relative: true source: path - version: "0.0.31" + version: "0.0.32" boolean_selector: dependency: transitive description: