Skip to content

Commit

Permalink
feat: add snackbars (#659)
Browse files Browse the repository at this point in the history
* feat: add CatalystSnackBar

* feat: update voices_snackbar

* feat: add example

* wip

* feat: update voices theme

* Update voices_snackbar.dart

* chore: clean up

* chore: add behavior and width

* Update voices_snackbar.dart

* chore: fix colors

* chore: rename showSnackBar to show

* Update voices_snackbar_type.dart

* chore: address pr comments
  • Loading branch information
minikin authored Aug 1, 2024
1 parent 64e5a80 commit 5130966
Show file tree
Hide file tree
Showing 14 changed files with 738 additions and 213 deletions.
165 changes: 165 additions & 0 deletions catalyst_voices/lib/widgets/snackbar/voices_snackbar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import 'package:catalyst_voices/widgets/snackbar/voices_snackbar_type.dart';
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
import 'package:catalyst_voices_brands/catalyst_voices_brands.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:flutter/material.dart';

/// [VoicesSnackBar] is a custom [SnackBar] widget that displays messages with
/// different types and actions.
///
/// [VoicesSnackBar] comes with different types (info, success, warning, error)
/// and optional actions such as primary, secondary, and close buttons.
class VoicesSnackBar extends StatelessWidget {
/// The type of the [VoicesSnackBar],
/// which determines its appearance and behavior.
final VoicesSnackBarType type;

/// Function to be executed when the primary action button is pressed.
final VoidCallback? onPrimaryPressed;

/// Callback function to be executed when the secondary action button is
/// pressed.
final VoidCallback? onSecondaryPressed;

/// Callback function to be executed when the close button is pressed.
final VoidCallback? onClosePressed;

/// The behavior of the [VoicesSnackBar], which can be fixed or floating.
final SnackBarBehavior? behavior;

/// The padding around the content of the [VoicesSnackBar].
final EdgeInsetsGeometry? padding;

/// The width of the [VoicesSnackBar].
final double? width;

const VoicesSnackBar({
super.key,
required this.type,
this.onPrimaryPressed,
this.onSecondaryPressed,
this.onClosePressed,
this.width,
this.behavior = SnackBarBehavior.fixed,
this.padding = const EdgeInsets.all(16),
});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final l10n = context.l10n;

return DecoratedBox(
decoration: BoxDecoration(
color: type.backgroundColor(context),
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
Positioned(
top: 12,
right: 12,
child: IconButton(
icon: Icon(
size: 24,
CatalystVoicesIcons.x,
color: theme.colors.iconsForeground,
),
onPressed: onClosePressed,
),
),
Column(
children: [
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Icon(
size: 20,
type.icon(context),
color: type.iconColor(context),
),
const SizedBox(width: 16),
Text(
type.title(context),
style: TextStyle(
color: type.titleColor(context),
fontSize: textTheme.titleMedium?.fontSize,
fontWeight: textTheme.titleMedium?.fontWeight,
fontFamily: textTheme.titleMedium?.fontFamily,
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(
left: 48,
),
child: Row(
children: [
Text(
type.message(context),
style: textTheme.bodyMedium,
),
],
),
),
const SizedBox(height: 18),
Padding(
padding: const EdgeInsets.only(
left: 36,
),
child: Row(
children: [
TextButton(
onPressed: onPrimaryPressed,
child: Text(
type == VoicesSnackBarType.success
? l10n.snackbarOkButtonText
: l10n.snackbarRefreshButtonText,
style: TextStyle(
color: theme.colors.textPrimary,
),
),
),
const SizedBox(width: 8),
TextButton(
onPressed: onSecondaryPressed,
child: Text(
l10n.snackbarMoreButtonText,
style: TextStyle(
color: theme.colors.textPrimary,
decoration: TextDecoration.underline,
),
),
),
],
),
),
const SizedBox(height: 18),
],
),
],
),
);
}

void show(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: this,
behavior: behavior,
width: behavior == SnackBarBehavior.floating ? width : null,
padding: padding,
elevation: 0,
backgroundColor: Colors.transparent,
),
);
}

static void hideCurrent(BuildContext context) =>
ScaffoldMessenger.of(context).hideCurrentSnackBar();
}
83 changes: 83 additions & 0 deletions catalyst_voices/lib/widgets/snackbar/voices_snackbar_type.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'package:catalyst_voices/widgets/snackbar/voices_snackbar.dart';
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
import 'package:catalyst_voices_brands/catalyst_voices_brands.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:flutter/material.dart';

/// Enum representing the different types of SnackBars available in the
/// [VoicesSnackBar] widget.
///
/// Each type determines the appearance and behavior of the [VoicesSnackBar].
enum VoicesSnackBarType { info, success, warning, error }

class _SnackBarData {
final IconData icon;
final ColorResolver iconColor;
final ColorResolver titleColor;
final ColorResolver backgroundColor;
final L10nResolver message;
final L10nResolver title;

const _SnackBarData({
required this.icon,
required this.iconColor,
required this.titleColor,
required this.backgroundColor,
required this.message,
required this.title,
});
}

extension VoicesSnackBarTypeExtension on VoicesSnackBarType {
static final Map<VoicesSnackBarType, _SnackBarData> _data = {
VoicesSnackBarType.info: _SnackBarData(
icon: CatalystVoicesIcons.information_circle,
iconColor: (colors) => colors.iconsPrimary,
titleColor: (colors) => colors.onPrimaryContainer,
backgroundColor: (colors) => colors.primaryContainer,
message: (l10n) => l10n.snackbarInfoMessageText,
title: (l10n) => l10n.snackbarInfoLabelText,
),
VoicesSnackBarType.success: _SnackBarData(
icon: CatalystVoicesIcons.check_circle,
iconColor: (colors) => colors.iconsSuccess,
titleColor: (colors) => colors.onSuccessContainer,
backgroundColor: (colors) => colors.successContainer,
message: (l10n) => l10n.snackbarSuccessMessageText,
title: (l10n) => l10n.snackbarSuccessLabelText,
),
VoicesSnackBarType.warning: _SnackBarData(
icon: CatalystVoicesIcons.exclamation,
iconColor: (colors) => colors.iconsWarning,
titleColor: (colors) => colors.onWarningContainer,
backgroundColor: (colors) => colors.warningContainer,
message: (l10n) => l10n.snackbarWarningMessageText,
title: (l10n) => l10n.snackbarWarningLabelText,
),
VoicesSnackBarType.error: _SnackBarData(
icon: CatalystVoicesIcons.exclamation_circle,
iconColor: (colors) => colors.iconsError,
titleColor: (colors) => colors.onErrorContainer,
backgroundColor: (colors) => colors.errorContainer,
message: (l10n) => l10n.snackbarErrorMessageText,
title: (l10n) => l10n.snackbarErrorLabelText,
),
};

_SnackBarData get _snackBarData => _data[this]!;

Color? backgroundColor(BuildContext context) =>
_snackBarData.backgroundColor(Theme.of(context).colors);

IconData icon(BuildContext context) => _snackBarData.icon;

Color? iconColor(BuildContext context) =>
_snackBarData.iconColor(Theme.of(context).colors);

String message(BuildContext context) => _snackBarData.message(context.l10n);

String title(BuildContext context) => _snackBarData.title(context.l10n);

Color? titleColor(BuildContext context) =>
_snackBarData.titleColor(Theme.of(context).colors);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'brands/brands.dart';
export 'theme_builder/theme_builder.dart';
export 'theme_extensions/theme_extensions.dart';
export 'utils/typedefs.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ class VoicesColorScheme extends ThemeExtension<VoicesColorScheme> {
final Color? elevationsOnSurfaceNeutralLv0;
final Color? outlineBorder;
final Color? outlineBorderVariant;
final Color? primaryContainer;
final Color? onPrimaryContainer;
final Color? errorContainer;
final Color? onErrorContainer;

const VoicesColorScheme({
required this.textPrimary,
Expand Down Expand Up @@ -97,6 +101,10 @@ class VoicesColorScheme extends ThemeExtension<VoicesColorScheme> {
required this.elevationsOnSurfaceNeutralLv0,
required this.outlineBorder,
required this.outlineBorderVariant,
required this.primaryContainer,
required this.onPrimaryContainer,
required this.errorContainer,
required this.onErrorContainer,
});

@override
Expand Down Expand Up @@ -145,6 +153,10 @@ class VoicesColorScheme extends ThemeExtension<VoicesColorScheme> {
Color? elevationsOnSurfaceNeutralLv0,
Color? outlineBorder,
Color? outlineBorderVariant,
Color? primaryContainer,
Color? onPrimaryContainer,
Color? errorContainer,
Color? onErrorContainer,
}) {
return VoicesColorScheme(
textPrimary: textPrimary ?? this.textPrimary,
Expand Down Expand Up @@ -199,6 +211,10 @@ class VoicesColorScheme extends ThemeExtension<VoicesColorScheme> {
elevationsOnSurfaceNeutralLv0 ?? this.elevationsOnSurfaceNeutralLv0,
outlineBorder: outlineBorder ?? this.outlineBorder,
outlineBorderVariant: outlineBorderVariant ?? this.outlineBorderVariant,
primaryContainer: primaryContainer ?? this.primaryContainer,
onPrimaryContainer: onPrimaryContainer ?? this.onPrimaryContainer,
errorContainer: errorContainer ?? this.errorContainer,
onErrorContainer: onErrorContainer ?? this.onErrorContainer,
);
}

Expand Down Expand Up @@ -290,6 +306,11 @@ class VoicesColorScheme extends ThemeExtension<VoicesColorScheme> {
outlineBorder: Color.lerp(outlineBorder, other.outlineBorder, t),
outlineBorderVariant:
Color.lerp(outlineBorderVariant, other.outlineBorderVariant, t),
primaryContainer: Color.lerp(primaryContainer, other.primaryContainer, t),
onPrimaryContainer:
Color.lerp(onPrimaryContainer, other.onPrimaryContainer, t),
errorContainer: Color.lerp(errorContainer, other.errorContainer, t),
onErrorContainer: Color.lerp(onErrorContainer, other.onErrorContainer, t),
);
}
}
Expand Down
Loading

0 comments on commit 5130966

Please sign in to comment.