feat: add GalleryHeader/GalleryFooter theme classes (#546)

* chore: Add ImageHeaderTheme and ImageHeaderThemeData to stream_chat_theme.dart

This commit adds the following classes to `stream_chat_theme.dart`:
1. `ImageHeaderTheme`, an `InheritedTheme` widget that can wrap other widgets and provide that sub-tree with an `ImageHeaderThemeData`
2. `ImageHeaderThemeData`, a class that allows for the customization of `ImageHeader` widgets

* Make ImageHeaderThemeData constructor const

* test: initial ImageHeaderThemeData tests

Tests the copyWith, ==, and hashcode functions

* test: add tests for default ImageHeaderThemeData values

Tests the default values of ImageHeaderThemeData that gets built in a StreamChatThemeData against control values.

* Update test name and add comments describing control

* fix: use ImageHeaderTheme.of(context) instead of StreamChatTheme.of(context)

This ensures that ImageHeader will always look for the closest ImageHeaderThemeData in the widget tree.

* test: add dark theme default values test

* run dartfmt on test file

* test: lerping between ImageHeaderThemeData

Added tests that lerp from one ImageHeaderThemeData to another (light to dark and dark to light)

* test: merging ImageHeaderThemeData

Added tests that merge light and dark ImageHeaderThemeData together (light + dark = dark, dark + light = light)

* test: add a half-lerp test

Added a test that lerps halway between light and dark ImageHeaderThemeData

* chore: Add ImageFooterTheme and ImageFooterThemeData to stream_chat_theme.dart

This commit adds the following classes to `stream_chat_theme.dart`:
1. `ImageFooterTheme`, an `InheritedTheme` widget that can wrap other widgets and provide that sub-tree with an `ImageFooterThemeData`
2. `ImageFooterThemeData`, a class that allows for the customization of `ImageFooter` widget

* chore: apply ImageFooterThemeData to image_footer.dart

* fix: make ImageFooterThemeData constructor const

* test: add tests for ImageFooterThemeData

* add a missing trailing comma

* chore: rename ImageHeader & ImageFooter to GalleryHeader and GalleryFooter

The respective theme classes and tests have also been renamed

* Run dartfmt on test

* test: fix failing tests due to renaming
This commit is contained in:
Reuben Turner
2021-07-15 15:44:12 +02:00
committed by GitHub
parent 6399e8f2db
commit 485e077596
8 changed files with 874 additions and 63 deletions
@@ -6,8 +6,8 @@ import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:photo_view/photo_view.dart';
import 'package:stream_chat_flutter/src/image_footer.dart';
import 'package:stream_chat_flutter/src/image_header.dart';
import 'package:stream_chat_flutter/src/gallery_footer.dart';
import 'package:stream_chat_flutter/src/gallery_header.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
@@ -180,7 +180,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ImageHeader(
GalleryHeader(
userName: widget.userName,
sentAt:
// ignore: lines_longer_than_80_chars
@@ -198,7 +198,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
},
),
if (widget.message.type != 'ephemeral')
ImageFooter(
GalleryFooter(
currentPage: _currentPage,
totalPages: widget.mediaAttachments.length,
mediaAttachments: widget.mediaAttachments,
@@ -13,9 +13,9 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Footer widget for media display
class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
/// Creates a channel header
const ImageFooter({
const GalleryFooter({
Key? key,
required this.message,
this.onBackPressed,
@@ -54,13 +54,13 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
final ValueChanged<int>? mediaSelectedCallBack;
@override
_ImageFooterState createState() => _ImageFooterState();
_GalleryFooterState createState() => _GalleryFooterState();
@override
final Size preferredSize;
}
class _ImageFooterState extends State<ImageFooter> {
class _GalleryFooterState extends State<GalleryFooter> {
final TextEditingController _messageController = TextEditingController();
final FocusNode _messageFocusNode = FocusNode();
@@ -78,7 +78,7 @@ class _ImageFooterState extends State<ImageFooter> {
Widget build(BuildContext context) {
const showShareButton = !kIsWeb;
final mediaQueryData = MediaQuery.of(context);
final chatThemeData = StreamChatTheme.of(context);
final galleryFooterThemeData = GalleryFooterTheme.of(context);
return SizedBox.fromSize(
size: Size(
mediaQueryData.size.width,
@@ -88,7 +88,7 @@ class _ImageFooterState extends State<ImageFooter> {
context: context,
removeTop: true,
child: BottomAppBar(
color: chatThemeData.colorTheme.barsBg,
color: galleryFooterThemeData.backgroundColor,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -100,7 +100,7 @@ class _ImageFooterState extends State<ImageFooter> {
IconButton(
icon: StreamSvgIcon.iconShare(
size: 24,
color: chatThemeData.colorTheme.textHighEmphasis,
color: galleryFooterThemeData.shareIconColor,
),
onPressed: () async {
final attachment =
@@ -136,7 +136,7 @@ class _ImageFooterState extends State<ImageFooter> {
children: <Widget>[
Text(
'${widget.currentPage + 1} of ${widget.totalPages}',
style: chatThemeData.textTheme.headlineBold,
style: galleryFooterThemeData.titleTextStyle,
),
],
),
@@ -144,7 +144,7 @@ class _ImageFooterState extends State<ImageFooter> {
),
IconButton(
icon: StreamSvgIcon.iconGrid(
color: chatThemeData.colorTheme.textHighEmphasis,
color: galleryFooterThemeData.gridIconButtonColor,
),
onPressed: () => _showPhotosModal(context),
),
@@ -157,10 +157,11 @@ class _ImageFooterState extends State<ImageFooter> {
void _showPhotosModal(context) {
final chatThemeData = StreamChatTheme.of(context);
final galleryFooterThemeData = GalleryFooterTheme.of(context);
showModalBottomSheet(
context: context,
barrierColor: chatThemeData.colorTheme.overlay,
backgroundColor: chatThemeData.colorTheme.barsBg,
barrierColor: galleryFooterThemeData.bottomSheetBarrierColor,
backgroundColor: galleryFooterThemeData.bottomSheetBackgroundColor,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
@@ -191,7 +192,8 @@ class _ImageFooterState extends State<ImageFooter> {
padding: const EdgeInsets.all(16),
child: Text(
'Photos',
style: chatThemeData.textTheme.headlineBold,
style:
galleryFooterThemeData.bottomSheetPhotosTextStyle,
),
),
),
@@ -199,7 +201,8 @@ class _ImageFooterState extends State<ImageFooter> {
alignment: Alignment.centerRight,
child: IconButton(
icon: StreamSvgIcon.close(
color: chatThemeData.colorTheme.textHighEmphasis,
color:
galleryFooterThemeData.bottomSheetCloseIconColor,
),
onPressed: () => Navigator.maybePop(context),
),
@@ -5,9 +5,9 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Header/AppBar widget for media display screen
class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
/// Creates a channel header
const ImageHeader({
const GalleryHeader({
Key? key,
required this.message,
this.currentIndex = 0,
@@ -51,7 +51,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
return AppBar(
textTheme: Theme.of(context).textTheme,
brightness: Theme.of(context).brightness,
@@ -59,18 +59,18 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
leading: showBackButton
? IconButton(
icon: StreamSvgIcon.close(
color: chatThemeData.colorTheme.textHighEmphasis,
color: galleryHeaderThemeData.closeButtonColor,
size: 24,
),
onPressed: onBackPressed,
)
: const SizedBox(),
backgroundColor: chatThemeData.channelTheme.channelHeaderTheme.color,
backgroundColor: galleryHeaderThemeData.backgroundColor,
actions: <Widget>[
if (message.type != 'ephemeral')
IconButton(
icon: StreamSvgIcon.iconMenuPoint(
color: chatThemeData.colorTheme.textHighEmphasis,
color: galleryHeaderThemeData.iconMenuPointColor,
),
onPressed: () {
_showMessageActionModalBottomSheet(context);
@@ -90,11 +90,11 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
children: <Widget>[
Text(
userName,
style: chatThemeData.textTheme.headlineBold,
style: galleryHeaderThemeData.titleTextStyle,
),
Text(
sentAt,
style: chatThemeData.channelPreviewTheme.subtitle,
style: galleryHeaderThemeData.subtitleTextStyle,
),
],
),
@@ -109,11 +109,13 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
void _showMessageActionModalBottomSheet(BuildContext context) async {
final channel = StreamChannel.of(context).channel;
final galleryHeaderThemeData =
StreamChatTheme.of(context).galleryHeaderTheme;
final result = await showDialog(
useRootNavigator: false,
context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor,
builder: (context) => StreamChannel(
channel: channel,
child: AttachmentActionsModal(
@@ -1,4 +1,5 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/channel_header.dart';
import 'package:stream_chat_flutter/src/channel_preview.dart';
@@ -58,6 +59,8 @@ class StreamChatThemeData {
Widget Function(BuildContext, User)? defaultUserImage,
IconThemeData? primaryIconTheme,
List<ReactionIcon>? reactionIcons,
GalleryHeaderThemeData? imageHeaderTheme,
GalleryFooterThemeData? imageFooterTheme,
}) {
brightness ??= colorTheme?.brightness ?? Brightness.light;
final isDark = brightness == Brightness.dark;
@@ -80,6 +83,8 @@ class StreamChatThemeData {
defaultUserImage: defaultUserImage,
primaryIconTheme: primaryIconTheme,
reactionIcons: reactionIcons,
galleryHeaderTheme: imageHeaderTheme,
galleryFooterTheme: imageFooterTheme,
);
return defaultData.merge(customizedData);
@@ -107,6 +112,8 @@ class StreamChatThemeData {
required this.defaultUserImage,
required this.primaryIconTheme,
required this.reactionIcons,
required this.galleryHeaderTheme,
required this.galleryFooterTheme,
});
/// Create a theme from a Material [Theme]
@@ -136,6 +143,14 @@ class StreamChatThemeData {
/// Theme of the chat widgets dedicated to a channel
final ChannelTheme channelTheme;
/// The default style for [GalleryHeader]s below the overall
/// [StreamChatTheme].
final GalleryHeaderThemeData galleryHeaderTheme;
/// The default style for [GalleryFooter]s below the overall
/// [StreamChatTheme].
final GalleryFooterThemeData galleryFooterTheme;
/// Theme of the current user messages
final MessageTheme ownMessageTheme;
@@ -172,6 +187,8 @@ class StreamChatThemeData {
IconThemeData? primaryIconTheme,
ChannelListHeaderTheme? channelListHeaderTheme,
List<ReactionIcon>? reactionIcons,
GalleryHeaderThemeData? galleryHeaderTheme,
GalleryFooterThemeData? galleryFooterTheme,
}) =>
StreamChatThemeData.raw(
channelListHeaderTheme:
@@ -188,6 +205,8 @@ class StreamChatThemeData {
otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme),
messageInputTheme: this.messageInputTheme.merge(messageInputTheme),
reactionIcons: reactionIcons ?? this.reactionIcons,
galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme,
galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme,
);
/// Merge themes
@@ -207,6 +226,8 @@ class StreamChatThemeData {
otherMessageTheme: otherMessageTheme.merge(other.otherMessageTheme),
messageInputTheme: messageInputTheme.merge(other.messageInputTheme),
reactionIcons: other.reactionIcons,
galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme),
galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme),
);
}
@@ -219,6 +240,40 @@ class StreamChatThemeData {
final accentColor = colorTheme.accentPrimary;
final iconTheme =
IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5));
final channelTheme = ChannelTheme(
channelHeaderTheme: ChannelHeaderTheme(
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
color: colorTheme.barsBg,
title: textTheme.headlineBold,
subtitle: textTheme.footnote.copyWith(
color: const Color(0xff7A7A7A),
),
),
);
final channelPreviewTheme = ChannelPreviewTheme(
unreadCounterColor: colorTheme.accentError,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
title: textTheme.bodyBold,
subtitle: textTheme.footnote.copyWith(
color: const Color(0xff7A7A7A),
),
lastMessageAt: textTheme.footnote.copyWith(
color: colorTheme.textHighEmphasis.withOpacity(.5),
),
indicatorIconSize: 16,
);
return StreamChatThemeData.raw(
textTheme: textTheme,
colorTheme: colorTheme,
@@ -231,24 +286,7 @@ class StreamChatThemeData {
fit: BoxFit.cover,
),
),
channelPreviewTheme: ChannelPreviewTheme(
unreadCounterColor: colorTheme.accentError,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
title: textTheme.bodyBold,
subtitle: textTheme.footnote.copyWith(
color: const Color(0xff7A7A7A),
),
lastMessageAt: textTheme.footnote.copyWith(
color: colorTheme.textHighEmphasis.withOpacity(.5),
),
indicatorIconSize: 16,
),
channelPreviewTheme: channelPreviewTheme,
channelListHeaderTheme: ChannelListHeaderTheme(
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
@@ -260,22 +298,7 @@ class StreamChatThemeData {
color: colorTheme.barsBg,
title: textTheme.headlineBold,
),
channelTheme: ChannelTheme(
channelHeaderTheme: ChannelHeaderTheme(
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
color: colorTheme.barsBg,
title: textTheme.headlineBold,
subtitle: textTheme.footnote.copyWith(
color: const Color(0xff7A7A7A),
),
),
),
channelTheme: channelTheme,
ownMessageTheme: MessageTheme(
messageAuthor:
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
@@ -407,6 +430,24 @@ class StreamChatThemeData {
},
),
],
galleryHeaderTheme: GalleryHeaderThemeData(
closeButtonColor: colorTheme.textHighEmphasis,
backgroundColor: channelTheme.channelHeaderTheme.color,
iconMenuPointColor: colorTheme.textHighEmphasis,
titleTextStyle: textTheme.headlineBold,
subtitleTextStyle: channelPreviewTheme.subtitle,
bottomSheetBarrierColor: colorTheme.overlay,
),
galleryFooterTheme: GalleryFooterThemeData(
backgroundColor: colorTheme.barsBg,
shareIconColor: colorTheme.textHighEmphasis,
titleTextStyle: textTheme.headlineBold,
gridIconButtonColor: colorTheme.textHighEmphasis,
bottomSheetBarrierColor: colorTheme.overlay,
bottomSheetBackgroundColor: colorTheme.barsBg,
bottomSheetPhotosTextStyle: textTheme.headlineBold,
bottomSheetCloseIconColor: colorTheme.textHighEmphasis,
),
);
}
}
@@ -1287,3 +1328,393 @@ class Effect {
blur: blur ?? this.blur,
);
}
/// Overrides the default style of [GalleryHeader] descendants.
///
/// See also:
///
/// * [GalleryHeaderThemeData], which is used to configure this theme.
class GalleryHeaderTheme extends InheritedTheme {
/// Creates an [GalleryHeaderTheme].
///
/// The [data] parameter must not be null.
const GalleryHeaderTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final GalleryHeaderThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [GalleryHeaderTheme] widget, then
/// [StreamChatThemeData.galleryHeaderTheme] is used.
///
/// Typical usage is as follows:
///
/// ```dart
/// ImageHeaderTheme theme = ImageHeaderTheme.of(context);
/// ```
static GalleryHeaderThemeData of(BuildContext context) {
final galleryHeaderTheme =
context.dependOnInheritedWidgetOfExactType<GalleryHeaderTheme>();
return galleryHeaderTheme?.data ??
StreamChatTheme.of(context).galleryHeaderTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
GalleryHeaderTheme(data: data, child: child);
@override
bool updateShouldNotify(GalleryHeaderTheme oldWidget) =>
data != oldWidget.data;
}
/// A style that overrides the default appearance of [GalleryHeader]s when used
/// with [GalleryHeaderTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.galleryHeaderTheme].
///
/// See also:
///
/// * [GalleryHeaderTheme], the theme which is configured with this class.
/// * [StreamChatThemeData.galleryHeaderTheme], which can be used to override
/// the default style for [GalleryHeader]s below the overall [StreamChatTheme].
class GalleryHeaderThemeData with Diagnosticable {
/// Creates an [GalleryHeaderThemeData].
const GalleryHeaderThemeData({
this.closeButtonColor,
this.backgroundColor,
this.iconMenuPointColor,
this.titleTextStyle,
this.subtitleTextStyle,
this.bottomSheetBarrierColor,
});
/// The color of the "close" button.
///
/// Defaults to [ColorTheme.textHighEmphasis].
final Color? closeButtonColor;
/// The background color of the [GalleryHeader] widget.
///
/// Defaults to [ChannelHeaderTheme.color].
final Color? backgroundColor;
/// Defaults to [ColorTheme.textHighEmphasis].
final Color? iconMenuPointColor;
/// The [TextStyle] to use for the [GalleryHeader] title text.
///
/// Defaults to [TextTheme.headlineBold].
final TextStyle? titleTextStyle;
/// The [TextStyle] to use for the [GalleryHeader] subtitle text.
///
/// Defaults to [ChannelPreviewTheme.subtitle].
final TextStyle? subtitleTextStyle;
///
final Color? bottomSheetBarrierColor;
/// Copies this [GalleryHeaderThemeData] to another.
GalleryHeaderThemeData copyWith({
Color? closeButtonColor,
Color? backgroundColor,
Color? iconMenuPointColor,
TextStyle? titleTextStyle,
TextStyle? subtitleTextStyle,
Color? bottomSheetBarrierColor,
}) =>
GalleryHeaderThemeData(
closeButtonColor: closeButtonColor ?? this.closeButtonColor,
backgroundColor: backgroundColor ?? this.backgroundColor,
iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor,
titleTextStyle: titleTextStyle ?? this.titleTextStyle,
subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle,
bottomSheetBarrierColor:
bottomSheetBarrierColor ?? this.bottomSheetBarrierColor,
);
/// Linearly interpolate between two [GalleryHeader] themes.
///
/// All the properties must be non-null.
GalleryHeaderThemeData lerp(
GalleryHeaderThemeData a,
GalleryHeaderThemeData b,
double t,
) =>
GalleryHeaderThemeData(
closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t),
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
iconMenuPointColor:
Color.lerp(a.iconMenuPointColor, b.iconMenuPointColor, t),
titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t),
subtitleTextStyle:
TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t),
bottomSheetBarrierColor:
Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t),
);
/// Merges one [GalleryHeaderThemeData] with the another
GalleryHeaderThemeData merge(GalleryHeaderThemeData? other) {
if (other == null) return this;
return copyWith(
closeButtonColor: other.closeButtonColor,
backgroundColor: other.backgroundColor,
iconMenuPointColor: other.iconMenuPointColor,
titleTextStyle: other.titleTextStyle,
subtitleTextStyle: other.subtitleTextStyle,
bottomSheetBarrierColor: other.bottomSheetBarrierColor,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GalleryHeaderThemeData &&
runtimeType == other.runtimeType &&
closeButtonColor == other.closeButtonColor &&
backgroundColor == other.backgroundColor &&
iconMenuPointColor == other.iconMenuPointColor &&
titleTextStyle == other.titleTextStyle &&
subtitleTextStyle == other.subtitleTextStyle &&
bottomSheetBarrierColor == other.bottomSheetBarrierColor;
@override
int get hashCode =>
closeButtonColor.hashCode ^
backgroundColor.hashCode ^
iconMenuPointColor.hashCode ^
titleTextStyle.hashCode ^
subtitleTextStyle.hashCode ^
bottomSheetBarrierColor.hashCode;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(ColorProperty('closeButtonColor', closeButtonColor))
..add(ColorProperty('backgroundColor', backgroundColor))
..add(ColorProperty('iconMenuPointColor', iconMenuPointColor))
..add(DiagnosticsProperty('titleTextStyle', titleTextStyle))
..add(DiagnosticsProperty('subtitleTextStyle', subtitleTextStyle))
..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor));
}
}
/// Overrides the default style of [GalleryFooter] descendants.
///
/// See also:
///
/// * [GalleryFooterThemeData], which is used to configure this theme.
class GalleryFooterTheme extends InheritedTheme {
/// Creates an [GalleryFooterTheme].
///
/// The [data] parameter must not be null.
const GalleryFooterTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final GalleryFooterThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [GalleryFooterTheme] widget, then
/// [StreamChatThemeData.galleryFooterTheme] is used.
///
/// Typical usage is as follows:
///
/// ```dart
/// ImageFooterTheme theme = ImageFooterTheme.of(context);
/// ```
static GalleryFooterThemeData of(BuildContext context) {
final imageFooterTheme =
context.dependOnInheritedWidgetOfExactType<GalleryFooterTheme>();
return imageFooterTheme?.data ??
StreamChatTheme.of(context).galleryFooterTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
GalleryFooterTheme(data: data, child: child);
@override
bool updateShouldNotify(GalleryFooterTheme oldWidget) =>
data != oldWidget.data;
}
/// A style that overrides the default appearance of [GalleryFooter]s when used
/// with [GalleryFooterTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.galleryFooterTheme].
///
/// See also:
///
/// * [GalleryFooterTheme], the theme which is configured with this class.
/// * [StreamChatThemeData.galleryFooterTheme], which can be used to override
/// the default style for [GalleryFooter]s below the overall [StreamChatTheme].
class GalleryFooterThemeData with Diagnosticable {
/// Creates an [GalleryFooterThemeData].
const GalleryFooterThemeData({
this.backgroundColor,
this.shareIconColor,
this.titleTextStyle,
this.gridIconButtonColor,
this.bottomSheetBarrierColor,
this.bottomSheetBackgroundColor,
this.bottomSheetPhotosTextStyle,
this.bottomSheetCloseIconColor,
});
/// The background color for the [GalleryFooter] widget.
///
/// Defaults to [ColorTheme.barsBg].
final Color? backgroundColor;
/// The color for the "share" icon.
///
/// Defaults to [ColorTheme.textHighEmphasis].
final Color? shareIconColor;
/// The [TextStyle] to use for the [GalleryFooter] title text.
///
/// Defaults to [TextTheme.headlineBold].
final TextStyle? titleTextStyle;
/// The color to use for the "grid" icon.
///
/// Defaults to [ColorTheme.textHighEmphasis].
final Color? gridIconButtonColor;
/// The color to use behind the bottom sheet.
///
/// Defaults to [ColorTheme.overlay].
final Color? bottomSheetBarrierColor;
/// The background color to use for the bottom sheet.
///
/// Defaults to [ColorTheme.barsBg].
final Color? bottomSheetBackgroundColor;
/// The [TextStyle] to use for the "photos" text in the bottom sheet.
///
/// Defaults to [TextTheme.headlineBold].
final TextStyle? bottomSheetPhotosTextStyle;
/// The color to use for the "close" icon.
///
/// Defaults to [ColorTheme.textHighEmphasis].
final Color? bottomSheetCloseIconColor;
/// Copies this [GalleryFooterThemeData] to another.
GalleryFooterThemeData copyWith({
Color? backgroundColor,
Color? shareIconColor,
TextStyle? titleTextStyle,
Color? gridIconButtonColor,
Color? bottomSheetBarrierColor,
Color? bottomSheetBackgroundColor,
TextStyle? bottomSheetPhotosTextStyle,
Color? bottomSheetCloseIconColor,
}) =>
GalleryFooterThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
shareIconColor: shareIconColor ?? this.shareIconColor,
titleTextStyle: titleTextStyle ?? this.titleTextStyle,
gridIconButtonColor: gridIconButtonColor ?? this.gridIconButtonColor,
bottomSheetBarrierColor:
bottomSheetBarrierColor ?? this.bottomSheetBarrierColor,
bottomSheetBackgroundColor:
bottomSheetBackgroundColor ?? this.bottomSheetBackgroundColor,
bottomSheetPhotosTextStyle:
bottomSheetPhotosTextStyle ?? this.bottomSheetPhotosTextStyle,
bottomSheetCloseIconColor:
bottomSheetCloseIconColor ?? this.bottomSheetCloseIconColor,
);
/// Linearly interpolate between two [GalleryFooter] themes.
///
/// All the properties must be non-null.
GalleryFooterThemeData lerp(
GalleryFooterThemeData a,
GalleryFooterThemeData b,
double t,
) =>
GalleryFooterThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t),
titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t),
gridIconButtonColor:
Color.lerp(a.gridIconButtonColor, b.gridIconButtonColor, t),
bottomSheetBarrierColor:
Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t),
bottomSheetBackgroundColor: Color.lerp(
a.bottomSheetBackgroundColor, b.bottomSheetBackgroundColor, t),
bottomSheetPhotosTextStyle: TextStyle.lerp(
a.bottomSheetPhotosTextStyle, b.bottomSheetPhotosTextStyle, t),
bottomSheetCloseIconColor: Color.lerp(
a.bottomSheetCloseIconColor, b.bottomSheetCloseIconColor, t),
);
/// Merges one [GalleryFooterThemeData] with the another
GalleryFooterThemeData merge(GalleryFooterThemeData? other) {
if (other == null) return this;
return copyWith(
backgroundColor: other.backgroundColor,
bottomSheetBarrierColor: other.bottomSheetBarrierColor,
bottomSheetBackgroundColor: other.bottomSheetBackgroundColor,
bottomSheetCloseIconColor: other.bottomSheetCloseIconColor,
bottomSheetPhotosTextStyle: other.bottomSheetPhotosTextStyle,
gridIconButtonColor: other.gridIconButtonColor,
titleTextStyle: other.titleTextStyle,
shareIconColor: other.shareIconColor,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GalleryFooterThemeData &&
runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor &&
shareIconColor == other.shareIconColor &&
titleTextStyle == other.titleTextStyle &&
gridIconButtonColor == other.gridIconButtonColor &&
bottomSheetBarrierColor == other.bottomSheetBarrierColor &&
bottomSheetBackgroundColor == other.bottomSheetBackgroundColor &&
bottomSheetPhotosTextStyle == other.bottomSheetPhotosTextStyle &&
bottomSheetCloseIconColor == other.bottomSheetCloseIconColor;
@override
int get hashCode =>
backgroundColor.hashCode ^
shareIconColor.hashCode ^
titleTextStyle.hashCode ^
gridIconButtonColor.hashCode ^
bottomSheetBarrierColor.hashCode ^
bottomSheetBackgroundColor.hashCode ^
bottomSheetPhotosTextStyle.hashCode ^
bottomSheetCloseIconColor.hashCode;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(ColorProperty('backgroundColor', backgroundColor))
..add(ColorProperty('shareIconColor', shareIconColor))
..add(DiagnosticsProperty('titleTextStyle', titleTextStyle))
..add(ColorProperty('gridIconButtonColor', gridIconButtonColor))
..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor))
..add(ColorProperty(
'bottomSheetBackgroundColor', bottomSheetBackgroundColor))
..add(DiagnosticsProperty(
'bottomSheetPhotosTextStyle', bottomSheetPhotosTextStyle))
..add(ColorProperty(
'bottomSheetCloseIconColor', bottomSheetCloseIconColor));
}
}
@@ -12,8 +12,8 @@ export 'src/connection_status_builder.dart';
export 'src/date_divider.dart';
export 'src/deleted_message.dart';
export 'src/full_screen_media.dart';
export 'src/image_footer.dart';
export 'src/image_header.dart';
export 'src/gallery_footer.dart';
export 'src/gallery_header.dart';
export 'src/info_tile.dart';
export 'src/mention_tile.dart';
export 'src/message_action.dart';
@@ -0,0 +1,185 @@
import 'package:flutter/material.dart' hide TextTheme;
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:mocktail/mocktail.dart';
class MockStreamChatClient extends Mock implements StreamChatClient {}
void main() {
test('GalleryFooterThemeData copyWith, ==, hashCode basics', () {
expect(const GalleryFooterThemeData(),
const GalleryFooterThemeData().copyWith());
expect(const GalleryFooterThemeData().hashCode,
const GalleryFooterThemeData().copyWith().hashCode);
});
test(
'Light GalleryFooterThemeData lerps completely to dark GalleryFooterThemeData',
() {
expect(
const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl,
_galleryFooterThemeDataControlDark, 1),
_galleryFooterThemeDataControlDark);
});
test(
'Light GalleryFooterThemeData lerps halfway to dark GalleryFooterThemeData',
() {
expect(
const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl,
_galleryFooterThemeDataControlDark, 0.5),
_galleryFooterThemeDataControlMidLerp);
});
test(
'Dark GalleryFooterThemeData lerps completely to light GalleryFooterThemeData',
() {
expect(
const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControlDark,
_galleryFooterThemeDataControl, 1),
_galleryFooterThemeDataControl);
});
test('Merging dark and light themes results in a dark theme', () {
expect(
_galleryFooterThemeDataControl
.merge(_galleryFooterThemeDataControlDark),
_galleryFooterThemeDataControlDark);
});
test('Merging dark and light themes results in a dark theme', () {
expect(
_galleryFooterThemeDataControlDark
.merge(_galleryFooterThemeDataControl),
_galleryFooterThemeDataControl);
});
testWidgets(
'Passing no GalleryFooterThemeData returns default light theme values',
(WidgetTester tester) async {
late BuildContext _context;
await tester.pumpWidget(
MaterialApp(
builder: (context, child) => StreamChat(
client: MockStreamChatClient(),
child: child,
),
home: Builder(
builder: (context) {
_context = context;
return Scaffold(
appBar: GalleryFooter(
message: Message(),
),
);
},
),
),
);
final imageFooterTheme = GalleryFooterTheme.of(_context);
expect(imageFooterTheme.backgroundColor,
_galleryFooterThemeDataControl.backgroundColor);
expect(imageFooterTheme.shareIconColor,
_galleryFooterThemeDataControl.shareIconColor);
expect(imageFooterTheme.titleTextStyle,
_galleryFooterThemeDataControl.titleTextStyle);
expect(imageFooterTheme.gridIconButtonColor,
_galleryFooterThemeDataControl.gridIconButtonColor);
expect(imageFooterTheme.bottomSheetBarrierColor,
_galleryFooterThemeDataControl.bottomSheetBarrierColor);
expect(imageFooterTheme.bottomSheetBackgroundColor,
_galleryFooterThemeDataControl.bottomSheetBackgroundColor);
expect(imageFooterTheme.bottomSheetCloseIconColor,
_galleryFooterThemeDataControl.bottomSheetCloseIconColor);
expect(imageFooterTheme.bottomSheetPhotosTextStyle,
_galleryFooterThemeDataControl.bottomSheetPhotosTextStyle);
});
testWidgets(
'Passing no GalleryFooterThemeData returns default dark theme values',
(WidgetTester tester) async {
late BuildContext _context;
await tester.pumpWidget(
MaterialApp(
builder: (context, child) => StreamChat(
client: MockStreamChatClient(),
streamChatThemeData: StreamChatThemeData.dark(),
child: child,
),
home: Builder(
builder: (context) {
_context = context;
return Scaffold(
appBar: GalleryFooter(
message: Message(),
),
);
},
),
),
);
final imageFooterTheme = GalleryFooterTheme.of(_context);
expect(imageFooterTheme.backgroundColor,
_galleryFooterThemeDataControlDark.backgroundColor);
expect(imageFooterTheme.shareIconColor,
_galleryFooterThemeDataControlDark.shareIconColor);
expect(imageFooterTheme.titleTextStyle,
_galleryFooterThemeDataControlDark.titleTextStyle);
expect(imageFooterTheme.gridIconButtonColor,
_galleryFooterThemeDataControlDark.gridIconButtonColor);
expect(imageFooterTheme.bottomSheetBarrierColor,
_galleryFooterThemeDataControlDark.bottomSheetBarrierColor);
expect(imageFooterTheme.bottomSheetBackgroundColor,
_galleryFooterThemeDataControlDark.bottomSheetBackgroundColor);
expect(imageFooterTheme.bottomSheetCloseIconColor,
_galleryFooterThemeDataControlDark.bottomSheetCloseIconColor);
expect(imageFooterTheme.bottomSheetPhotosTextStyle,
_galleryFooterThemeDataControlDark.bottomSheetPhotosTextStyle);
});
}
// Light theme control
final _galleryFooterThemeDataControl = GalleryFooterThemeData(
backgroundColor: ColorTheme.light().barsBg,
shareIconColor: ColorTheme.light().textHighEmphasis,
titleTextStyle: TextTheme.light().headlineBold,
gridIconButtonColor: ColorTheme.light().textHighEmphasis,
bottomSheetBackgroundColor: ColorTheme.light().barsBg,
bottomSheetBarrierColor: ColorTheme.light().overlay,
bottomSheetCloseIconColor: ColorTheme.light().textHighEmphasis,
bottomSheetPhotosTextStyle: TextTheme.light().headlineBold,
);
// Mid-lerp theme control
const _galleryFooterThemeDataControlMidLerp = GalleryFooterThemeData(
backgroundColor: Color(0xff87898b),
shareIconColor: Color(0xff7f7f7f),
titleTextStyle: TextStyle(
color: Color(0xff7f7f7f),
fontSize: 16,
fontWeight: FontWeight.bold,
),
gridIconButtonColor: Color(0xff7f7f7f),
bottomSheetBarrierColor: Color(0x4c000000),
bottomSheetBackgroundColor: Color(0xff87898b),
bottomSheetPhotosTextStyle: TextStyle(
color: Color(0xff7f7f7f),
fontSize: 16,
fontWeight: FontWeight.bold,
),
bottomSheetCloseIconColor: Color(0xff7f7f7f),
);
// Dark theme control
final _galleryFooterThemeDataControlDark = GalleryFooterThemeData(
backgroundColor: ColorTheme.dark().barsBg,
shareIconColor: ColorTheme.dark().textHighEmphasis,
titleTextStyle: TextTheme.dark().headlineBold,
gridIconButtonColor: ColorTheme.dark().textHighEmphasis,
bottomSheetBackgroundColor: ColorTheme.dark().barsBg,
bottomSheetBarrierColor: ColorTheme.dark().overlay,
bottomSheetCloseIconColor: ColorTheme.dark().textHighEmphasis,
bottomSheetPhotosTextStyle: TextTheme.dark().headlineBold,
);
@@ -0,0 +1,190 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:mocktail/mocktail.dart';
class MockStreamChatClient extends Mock implements StreamChatClient {}
void main() {
test('GalleryHeaderThemeData copyWith, ==, hashCode basics', () {
expect(const GalleryHeaderThemeData(),
const GalleryHeaderThemeData().copyWith());
expect(const GalleryHeaderThemeData().hashCode,
const GalleryHeaderThemeData().copyWith().hashCode);
});
test(
'Light GalleryHeaderThemeData lerps completely to dark GalleryHeaderThemeData',
() {
expect(
const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl,
_galleryHeaderThemeDataDarkControl, 1),
_galleryHeaderThemeDataDarkControl);
});
test(
'Light GalleryHeaderThemeData lerps halfway to dark GalleryHeaderThemeData',
() {
expect(
const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl,
_galleryHeaderThemeDataDarkControl, 0.5),
_galleryHeaderThemeDataHalfLerpControl);
});
test(
'Dark GalleryHeaderThemeData lerps completely to light GalleryHeaderThemeData',
() {
expect(
const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataDarkControl,
_galleryHeaderThemeDataControl, 1),
_galleryHeaderThemeDataControl);
});
test('Merging dark and light themes results in a dark theme', () {
expect(
_galleryHeaderThemeDataControl
.merge(_galleryHeaderThemeDataDarkControl),
_galleryHeaderThemeDataDarkControl);
});
test('Merging dark and light themes results in a dark theme', () {
expect(
_galleryHeaderThemeDataDarkControl
.merge(_galleryHeaderThemeDataControl),
_galleryHeaderThemeDataControl);
});
testWidgets(
'Passing no GalleryHeaderThemeData returns default light theme values',
(WidgetTester tester) async {
late BuildContext _context;
await tester.pumpWidget(
MaterialApp(
builder: (context, child) => StreamChat(
client: MockStreamChatClient(),
child: child,
),
home: Builder(
builder: (context) {
_context = context;
return Scaffold(
appBar: GalleryHeader(
message: Message(),
),
);
},
),
),
);
final imageHeaderTheme = GalleryHeaderTheme.of(_context);
expect(imageHeaderTheme.closeButtonColor,
_galleryHeaderThemeDataControl.closeButtonColor);
expect(imageHeaderTheme.backgroundColor,
_galleryHeaderThemeDataControl.backgroundColor);
expect(imageHeaderTheme.iconMenuPointColor,
_galleryHeaderThemeDataControl.iconMenuPointColor);
expect(imageHeaderTheme.titleTextStyle,
_galleryHeaderThemeDataControl.titleTextStyle);
expect(imageHeaderTheme.subtitleTextStyle,
_galleryHeaderThemeDataControl.subtitleTextStyle);
expect(imageHeaderTheme.bottomSheetBarrierColor,
_galleryHeaderThemeDataControl.bottomSheetBarrierColor);
});
testWidgets(
'Passing no GalleryHeaderThemeData returns default dark theme values',
(WidgetTester tester) async {
late BuildContext _context;
await tester.pumpWidget(
MaterialApp(
builder: (context, child) => StreamChat(
client: MockStreamChatClient(),
streamChatThemeData: StreamChatThemeData.dark(),
child: child,
),
home: Builder(
builder: (context) {
_context = context;
return Scaffold(
appBar: GalleryHeader(
message: Message(),
),
);
},
),
),
);
final imageHeaderTheme = GalleryHeaderTheme.of(_context);
expect(imageHeaderTheme.closeButtonColor,
_galleryHeaderThemeDataDarkControl.closeButtonColor);
expect(imageHeaderTheme.backgroundColor,
_galleryHeaderThemeDataDarkControl.backgroundColor);
expect(imageHeaderTheme.iconMenuPointColor,
_galleryHeaderThemeDataDarkControl.iconMenuPointColor);
expect(imageHeaderTheme.titleTextStyle,
_galleryHeaderThemeDataDarkControl.titleTextStyle);
expect(imageHeaderTheme.subtitleTextStyle,
_galleryHeaderThemeDataDarkControl.subtitleTextStyle);
expect(imageHeaderTheme.bottomSheetBarrierColor,
_galleryHeaderThemeDataDarkControl.bottomSheetBarrierColor);
});
}
// Light theme test control.
final _galleryHeaderThemeDataControl = GalleryHeaderThemeData(
closeButtonColor: const Color(0xff000000),
backgroundColor: const Color(0xffffffff),
iconMenuPointColor: const Color(0xff000000),
titleTextStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
subtitleTextStyle: const TextStyle(
fontSize: 12,
color: Colors.black,
).copyWith(
color: const Color(0xff7A7A7A),
),
bottomSheetBarrierColor: const Color.fromRGBO(0, 0, 0, 0.2),
);
// Light theme test control.
final _galleryHeaderThemeDataHalfLerpControl = GalleryHeaderThemeData(
closeButtonColor: const Color(0xff7f7f7f),
backgroundColor: const Color(0xff87898b),
iconMenuPointColor: const Color(0xff7f7f7f),
titleTextStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xff7f7f7f),
),
subtitleTextStyle: const TextStyle(
fontSize: 12,
color: Color(0xff7a7a7a),
).copyWith(
color: const Color(0xff7A7A7A),
),
bottomSheetBarrierColor: const Color(0x4c000000),
);
// Dark theme test control.
final _galleryHeaderThemeDataDarkControl = GalleryHeaderThemeData(
closeButtonColor: const Color(0xffffffff),
backgroundColor: const Color(0xff101418),
iconMenuPointColor: const Color(0xffffffff),
titleTextStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
subtitleTextStyle: const TextStyle(
fontSize: 12,
color: Colors.white,
).copyWith(
color: const Color(0xff7A7A7A),
),
bottomSheetBarrierColor: const Color.fromRGBO(0, 0, 0, 0.4),
);
@@ -37,7 +37,7 @@ void main() {
child: WillPopScope(
onWillPop: () async => false,
child: Scaffold(
body: ImageFooter(
body: GalleryFooter(
message: Message(),
),
),