feat(ui): Added thumbnailSize, thumbnailResizeType, and thumbnailCropType params to StreamMessageWidget and StreamAttachmentPicker to customize the appearance of image thumbnails.
Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
@@ -4,6 +4,45 @@
|
|||||||
|
|
||||||
- [[#882]](https://github.com/GetStream/stream-chat-flutter/issues/882) Lots of unhandled exceptions
|
- [[#882]](https://github.com/GetStream/stream-chat-flutter/issues/882) Lots of unhandled exceptions
|
||||||
when network is off or spotty.
|
when network is off or spotty.
|
||||||
|
- Fixes an error where Stream CDN images were not being resized in the message list view.
|
||||||
|
|
||||||
|
🚀 Improved
|
||||||
|
|
||||||
|
- Automatically resize images that are above a specific pixel count to ensure resizing works:
|
||||||
|
getstream.io/chat/docs/go-golang/file_uploads/#image-resizing
|
||||||
|
|
||||||
|
✅ Added
|
||||||
|
|
||||||
|
- Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params
|
||||||
|
to `StreamMessageWidget` and `StreamAttachmentPicker` to customize the appearance of image
|
||||||
|
thumbnails.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
StreamMessageInput(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
messageInputController: _messageInputController,
|
||||||
|
attachmentsPickerBuilder: (_, __, picker) {
|
||||||
|
return picker.copyWith(
|
||||||
|
attachmentThumbnailSize: ...,
|
||||||
|
attachmentThumbnailFormat: ...,
|
||||||
|
attachmentThumbnailQuality: ...,
|
||||||
|
attachmentThumbnailScale: ...,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
```dart
|
||||||
|
StreamMessageListView(
|
||||||
|
messageBuilder: (context, details, messages, defaultMessage) {
|
||||||
|
return defaultMessage.copyWith(
|
||||||
|
imageAttachmentThumbnailSize: ...,
|
||||||
|
imageAttachmentThumbnailCropType: ...,
|
||||||
|
imageAttachmentThumbnailResizeType: ...,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
## 4.4.1
|
## 4.4.1
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
import 'package:stream_chat_flutter/src/attachment/attachment_title.dart';
|
import 'package:stream_chat_flutter/src/attachment/attachment_title.dart';
|
||||||
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
/// {@macro image_attachment}
|
/// {@macro image_attachment}
|
||||||
@@ -24,6 +25,9 @@ class StreamImageAttachment extends StreamAttachmentWidget {
|
|||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
this.onReturnAction,
|
this.onReturnAction,
|
||||||
this.onAttachmentTap,
|
this.onAttachmentTap,
|
||||||
|
this.imageThumbnailSize = const Size(400, 400),
|
||||||
|
this.imageThumbnailResizeType = 'crop',
|
||||||
|
this.imageThumbnailCropType = 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
/// [StreamMessageThemeData] for showing image title
|
/// [StreamMessageThemeData] for showing image title
|
||||||
@@ -41,6 +45,19 @@ class StreamImageAttachment extends StreamAttachmentWidget {
|
|||||||
/// Callback when attachment is tapped
|
/// Callback when attachment is tapped
|
||||||
final VoidCallback? onAttachmentTap;
|
final VoidCallback? onAttachmentTap;
|
||||||
|
|
||||||
|
/// Size of the attachment image thumbnail.
|
||||||
|
final Size imageThumbnailSize;
|
||||||
|
|
||||||
|
/// Resize type of the image attachment thumbnail.
|
||||||
|
///
|
||||||
|
/// Defaults to [crop]
|
||||||
|
final String /*clip|crop|scale|fill*/ imageThumbnailResizeType;
|
||||||
|
|
||||||
|
/// Crop type of the image attachment thumbnail.
|
||||||
|
///
|
||||||
|
/// Defaults to [center]
|
||||||
|
final String /*center|top|bottom|left|right*/ imageThumbnailCropType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => source.when(
|
Widget build(BuildContext context) => source.when(
|
||||||
local: () {
|
local: () {
|
||||||
@@ -65,43 +82,22 @@ class StreamImageAttachment extends StreamAttachmentWidget {
|
|||||||
var imageUrl =
|
var imageUrl =
|
||||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
|
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
|
||||||
|
|
||||||
if (imageUrl == null) {
|
if (imageUrl == null) return AttachmentError(size: size);
|
||||||
return AttachmentError(size: size);
|
|
||||||
}
|
|
||||||
|
|
||||||
var imageUri = Uri.parse(imageUrl);
|
imageUrl = imageUrl.getResizedImageUrl(
|
||||||
if (imageUri.host.endsWith('stream-io-cdn.com') &&
|
width: imageThumbnailSize.width,
|
||||||
(imageUri.queryParameters['h'] == null ||
|
height: imageThumbnailSize.height,
|
||||||
imageUri.queryParameters['h'] == '*') &&
|
resize: imageThumbnailResizeType,
|
||||||
(imageUri.queryParameters['w'] == null ||
|
crop: imageThumbnailCropType,
|
||||||
imageUri.queryParameters['w'] == '*') &&
|
);
|
||||||
(imageUri.queryParameters['crop'] == null ||
|
|
||||||
imageUri.queryParameters['crop'] == '*') &&
|
|
||||||
(imageUri.queryParameters['resize'] == null ||
|
|
||||||
imageUri.queryParameters['resize'] == '*')) {
|
|
||||||
imageUri = imageUri.replace(queryParameters: {
|
|
||||||
...imageUri.queryParameters,
|
|
||||||
'h': '400', // TODO: Are these sizes optimal? Consider web/desktop
|
|
||||||
'w': '400',
|
|
||||||
'crop': 'center',
|
|
||||||
'resize': 'clip',
|
|
||||||
});
|
|
||||||
} else if (imageUri.host.endsWith('stream-cloud-uploads.imgix.net')) {
|
|
||||||
imageUri = imageUri.replace(queryParameters: {
|
|
||||||
...imageUri.queryParameters,
|
|
||||||
'height': '400',
|
|
||||||
'width': '400',
|
|
||||||
'fit': 'crop',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
imageUrl = imageUri.toString();
|
|
||||||
|
|
||||||
return _buildImageAttachment(
|
return _buildImageAttachment(
|
||||||
context,
|
context,
|
||||||
CachedNetworkImage(
|
CachedNetworkImage(
|
||||||
cacheKey: imageUri.replace(queryParameters: {}).toString(),
|
imageUrl: imageUrl,
|
||||||
height: size?.height,
|
height: size?.height,
|
||||||
width: size?.width,
|
width: size?.width,
|
||||||
|
fit: BoxFit.cover,
|
||||||
placeholder: (context, __) {
|
placeholder: (context, __) {
|
||||||
final image = Image.asset(
|
final image = Image.asset(
|
||||||
'images/placeholder.png',
|
'images/placeholder.png',
|
||||||
@@ -115,9 +111,7 @@ class StreamImageAttachment extends StreamAttachmentWidget {
|
|||||||
child: image,
|
child: image,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
imageUrl: imageUrl,
|
|
||||||
errorWidget: (context, url, error) => AttachmentError(size: size),
|
errorWidget: (context, url, error) => AttachmentError(size: size),
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,6 +31,46 @@ extension StringExtension on String {
|
|||||||
|
|
||||||
/// Levenshtein distance between this and [t].
|
/// Levenshtein distance between this and [t].
|
||||||
int levenshteinDistance(String t) => levenshtein(this, t);
|
int levenshteinDistance(String t) => levenshtein(this, t);
|
||||||
|
|
||||||
|
/// Returns a resized imageUrl with the given [width], [height], [resize]
|
||||||
|
/// and [crop] if it is from Stream CDN or Dashboard.
|
||||||
|
///
|
||||||
|
/// Read more at https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing
|
||||||
|
String getResizedImageUrl({
|
||||||
|
// TODO: Are these sizes optimal? Consider web/desktop
|
||||||
|
double width = 400,
|
||||||
|
double height = 400,
|
||||||
|
String /*clip|crop|scale|fill*/ resize = 'scale',
|
||||||
|
String /*center|top|bottom|left|right*/ crop = 'center',
|
||||||
|
}) {
|
||||||
|
final uri = Uri.parse(this);
|
||||||
|
final host = uri.host;
|
||||||
|
|
||||||
|
final fromStreamCDN = host.endsWith('stream-io-cdn.com');
|
||||||
|
final fromStreamDashboard = host.endsWith('stream-cloud-uploads.imgix.net');
|
||||||
|
|
||||||
|
if (!fromStreamCDN && !fromStreamDashboard) return this;
|
||||||
|
|
||||||
|
final queryParameters = {...uri.queryParameters};
|
||||||
|
|
||||||
|
if (fromStreamCDN) {
|
||||||
|
if (queryParameters['h'].isNullOrMatches('*') &&
|
||||||
|
queryParameters['w'].isNullOrMatches('*') &&
|
||||||
|
queryParameters['crop'].isNullOrMatches('*') &&
|
||||||
|
queryParameters['resize'].isNullOrMatches('*')) {
|
||||||
|
queryParameters['h'] = height.floor().toString();
|
||||||
|
queryParameters['w'] = width.floor().toString();
|
||||||
|
queryParameters['crop'] = crop;
|
||||||
|
queryParameters['resize'] = resize;
|
||||||
|
}
|
||||||
|
} else if (fromStreamDashboard) {
|
||||||
|
queryParameters['height'] = height.floor().toString();
|
||||||
|
queryParameters['width'] = width.floor().toString();
|
||||||
|
queryParameters['fit'] = crop;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uri.replace(queryParameters: queryParameters).toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List extension
|
/// List extension
|
||||||
@@ -281,3 +321,10 @@ extension UriX on Uri {
|
|||||||
return Uri.parse('http://${toString()}');
|
return Uri.parse('http://${toString()}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extensions on generic type [T]
|
||||||
|
extension TypeX<T> on T? {
|
||||||
|
/// Returns true if the value is null or matches the given [value]
|
||||||
|
/// otherwise returns false.
|
||||||
|
bool isNullOrMatches(T value) => this == null || this == value;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
import 'package:shimmer/shimmer.dart';
|
||||||
import 'package:stream_chat_flutter/src/extension.dart';
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
@@ -64,36 +65,21 @@ class StreamFullScreenMedia extends StatefulWidget {
|
|||||||
_StreamFullScreenMediaState createState() => _StreamFullScreenMediaState();
|
_StreamFullScreenMediaState createState() => _StreamFullScreenMediaState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
|
class _StreamFullScreenMediaState extends State<StreamFullScreenMedia> {
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late final AnimationController _animationController;
|
|
||||||
late final PageController _pageController;
|
late final PageController _pageController;
|
||||||
|
|
||||||
late final _curvedAnimation = CurvedAnimation(
|
late final _currentPage = ValueNotifier(widget.startIndex);
|
||||||
parent: _animationController,
|
late final _isDisplayingDetail = ValueNotifier<bool>(true);
|
||||||
curve: Curves.easeOut,
|
|
||||||
reverseCurve: Curves.easeIn,
|
|
||||||
);
|
|
||||||
|
|
||||||
final _opacityTween = Tween<double>(begin: 1, end: 0);
|
void switchDisplayingDetail() {
|
||||||
late final _opacityAnimation = _opacityTween.animate(
|
_isDisplayingDetail.value = !_isDisplayingDetail.value;
|
||||||
CurvedAnimation(
|
}
|
||||||
parent: _animationController,
|
|
||||||
curve: const Interval(0, 0.6, curve: Curves.easeOut),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
late final ValueNotifier<int> _currentPage = ValueNotifier(widget.startIndex);
|
|
||||||
|
|
||||||
final videoPackages = <String, VideoPackage>{};
|
final videoPackages = <String, VideoPackage>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_animationController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
);
|
|
||||||
_pageController = PageController(initialPage: widget.startIndex);
|
_pageController = PageController(initialPage: widget.startIndex);
|
||||||
for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) {
|
for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) {
|
||||||
final attachment = widget.mediaAttachmentPackages[i].attachment;
|
final attachment = widget.mediaAttachmentPackages[i].attachment;
|
||||||
@@ -127,26 +113,102 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(
|
Widget build(BuildContext context) => Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
body: Stack(
|
body: ValueListenableBuilder<int>(
|
||||||
children: [
|
valueListenable: _currentPage,
|
||||||
PageView.builder(
|
builder: (context, currentPage, child) {
|
||||||
|
final _currentAttachmentPackage =
|
||||||
|
widget.mediaAttachmentPackages[currentPage];
|
||||||
|
final _currentMessage = _currentAttachmentPackage.message;
|
||||||
|
final _currentAttachment = _currentAttachmentPackage.attachment;
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: child!),
|
||||||
|
ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: _isDisplayingDetail,
|
||||||
|
builder: (context, isDisplayingDetail, child) {
|
||||||
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final topPadding = mediaQuery.padding.top;
|
||||||
|
return AnimatedPositionedDirectional(
|
||||||
|
duration: kThemeAnimationDuration,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
top: isDisplayingDetail
|
||||||
|
? 0
|
||||||
|
: -(topPadding + kToolbarHeight),
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
height: topPadding + kToolbarHeight,
|
||||||
|
child: StreamGalleryHeader(
|
||||||
|
userName: widget.userName,
|
||||||
|
sentAt: context.translations.sentAtText(
|
||||||
|
date: _currentAttachmentPackage.message.createdAt,
|
||||||
|
time: _currentAttachmentPackage.message.createdAt,
|
||||||
|
),
|
||||||
|
onBackPressed: Navigator.of(context).pop,
|
||||||
|
message: _currentMessage,
|
||||||
|
attachment: _currentAttachment,
|
||||||
|
onShowMessage: () {
|
||||||
|
widget.onShowMessage?.call(
|
||||||
|
_currentMessage,
|
||||||
|
StreamChannel.of(context).channel,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
attachmentActionsModalBuilder:
|
||||||
|
widget.attachmentActionsModalBuilder,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!_currentMessage.isEphemeral)
|
||||||
|
ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: _isDisplayingDetail,
|
||||||
|
builder: (context, isDisplayingDetail, child) {
|
||||||
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final bottomPadding = mediaQuery.padding.bottom;
|
||||||
|
return AnimatedPositionedDirectional(
|
||||||
|
duration: kThemeAnimationDuration,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
bottom: isDisplayingDetail
|
||||||
|
? 0
|
||||||
|
: -(bottomPadding + kToolbarHeight),
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
height: bottomPadding + kToolbarHeight,
|
||||||
|
child: StreamGalleryFooter(
|
||||||
|
currentPage: currentPage,
|
||||||
|
totalPages: widget.mediaAttachmentPackages.length,
|
||||||
|
mediaAttachmentPackages:
|
||||||
|
widget.mediaAttachmentPackages,
|
||||||
|
mediaSelectedCallBack: (val) {
|
||||||
|
_currentPage.value = val;
|
||||||
|
_pageController.animateToPage(
|
||||||
|
val,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: InkWell(
|
||||||
|
onTap: switchDisplayingDetail,
|
||||||
|
child: PageView.builder(
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
|
itemCount: widget.mediaAttachmentPackages.length,
|
||||||
onPageChanged: (val) {
|
onPageChanged: (val) {
|
||||||
_currentPage.value = val;
|
_currentPage.value = val;
|
||||||
|
if (videoPackages.isEmpty) return;
|
||||||
if (videoPackages.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final currentAttachment =
|
final currentAttachment =
|
||||||
widget.mediaAttachmentPackages[val].attachment;
|
widget.mediaAttachmentPackages[val].attachment;
|
||||||
|
|
||||||
for (final e in videoPackages.values) {
|
for (final e in videoPackages.values) {
|
||||||
if (e._attachment != currentAttachment) {
|
if (e._attachment != currentAttachment) {
|
||||||
e._chewieController?.pause();
|
e._chewieController?.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.autoplayVideos &&
|
if (widget.autoplayVideos &&
|
||||||
currentAttachment.type == 'video') {
|
currentAttachment.type == 'video') {
|
||||||
final controller = videoPackages[currentAttachment.id]!;
|
final controller = videoPackages[currentAttachment.id]!;
|
||||||
@@ -161,33 +223,44 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
|
|||||||
final imageUrl = attachment.imageUrl ??
|
final imageUrl = attachment.imageUrl ??
|
||||||
attachment.assetUrl ??
|
attachment.assetUrl ??
|
||||||
attachment.thumbUrl;
|
attachment.thumbUrl;
|
||||||
return AnimatedBuilder(
|
return ValueListenableBuilder<bool>(
|
||||||
animation: _curvedAnimation,
|
valueListenable: _isDisplayingDetail,
|
||||||
builder: (context, child) => PhotoView(
|
builder: (context, isDisplayingDetail, _) =>
|
||||||
loadingBuilder: (context, image) => const Offstage(),
|
AnimatedContainer(
|
||||||
imageProvider: (imageUrl == null &&
|
color: isDisplayingDetail
|
||||||
attachment.localUri != null &&
|
? StreamChannelHeaderTheme.of(context).color
|
||||||
attachment.file?.bytes != null)
|
: Colors.black,
|
||||||
? Image.memory(attachment.file!.bytes!).image
|
duration: kThemeAnimationDuration,
|
||||||
: CachedNetworkImageProvider(imageUrl!),
|
child: PhotoView(
|
||||||
maxScale: PhotoViewComputedScale.covered,
|
imageProvider: (imageUrl == null &&
|
||||||
minScale: PhotoViewComputedScale.contained,
|
attachment.localUri != null &&
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
attachment.file?.bytes != null)
|
||||||
tag: widget.mediaAttachmentPackages,
|
? Image.memory(attachment.file!.bytes!).image
|
||||||
|
: CachedNetworkImageProvider(imageUrl!),
|
||||||
|
errorBuilder: (_, __, ___) => const AttachmentError(),
|
||||||
|
loadingBuilder: (context, _) {
|
||||||
|
final image = Image.asset(
|
||||||
|
'images/placeholder.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
package: 'stream_chat_flutter',
|
||||||
|
);
|
||||||
|
final colorTheme =
|
||||||
|
StreamChatTheme.of(context).colorTheme;
|
||||||
|
return Shimmer.fromColors(
|
||||||
|
baseColor: colorTheme.disabled,
|
||||||
|
highlightColor: colorTheme.inputBg,
|
||||||
|
child: image,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
maxScale: PhotoViewComputedScale.covered,
|
||||||
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
|
tag: widget.mediaAttachmentPackages,
|
||||||
|
),
|
||||||
|
backgroundDecoration: const BoxDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundDecoration: BoxDecoration(
|
|
||||||
color: ColorTween(
|
|
||||||
begin: StreamChannelHeaderTheme.of(context).color,
|
|
||||||
end: Colors.black,
|
|
||||||
).lerp(_curvedAnimation.value),
|
|
||||||
),
|
|
||||||
onTapUp: (a, b, c) {
|
|
||||||
if (_animationController.isCompleted) {
|
|
||||||
_animationController.reverse();
|
|
||||||
} else {
|
|
||||||
_animationController.forward();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (attachment.type == 'video') {
|
} else if (attachment.type == 'video') {
|
||||||
@@ -198,17 +271,9 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: switchDisplayingDetail,
|
||||||
if (_animationController.isCompleted) {
|
|
||||||
_animationController.reverse();
|
|
||||||
} else {
|
|
||||||
_animationController.forward();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(vertical: 50),
|
||||||
vertical: 50,
|
|
||||||
),
|
|
||||||
child: Chewie(
|
child: Chewie(
|
||||||
controller: controller.chewieController!,
|
controller: controller.chewieController!,
|
||||||
),
|
),
|
||||||
@@ -217,76 +282,16 @@ class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
|
|||||||
}
|
}
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
},
|
},
|
||||||
itemCount: widget.mediaAttachmentPackages.length,
|
|
||||||
),
|
),
|
||||||
FadeTransition(
|
),
|
||||||
opacity: _opacityAnimation,
|
|
||||||
child: ValueListenableBuilder<int>(
|
|
||||||
valueListenable: _currentPage,
|
|
||||||
builder: (context, value, child) {
|
|
||||||
final _currentAttachmentPackage =
|
|
||||||
widget.mediaAttachmentPackages[value];
|
|
||||||
final _currentMessage = _currentAttachmentPackage.message;
|
|
||||||
final _currentAttachment =
|
|
||||||
_currentAttachmentPackage.attachment;
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
StreamGalleryHeader(
|
|
||||||
userName: widget.userName,
|
|
||||||
sentAt: context.translations.sentAtText(
|
|
||||||
date: widget
|
|
||||||
.mediaAttachmentPackages[_currentPage.value]
|
|
||||||
.message
|
|
||||||
.createdAt,
|
|
||||||
time: widget
|
|
||||||
.mediaAttachmentPackages[_currentPage.value]
|
|
||||||
.message
|
|
||||||
.createdAt,
|
|
||||||
),
|
|
||||||
onBackPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
message: _currentMessage,
|
|
||||||
attachment: _currentAttachment,
|
|
||||||
onShowMessage: () {
|
|
||||||
widget.onShowMessage?.call(
|
|
||||||
_currentMessage,
|
|
||||||
StreamChannel.of(context).channel,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
attachmentActionsModalBuilder:
|
|
||||||
widget.attachmentActionsModalBuilder,
|
|
||||||
),
|
|
||||||
if (!_currentMessage.isEphemeral)
|
|
||||||
StreamGalleryFooter(
|
|
||||||
currentPage: value,
|
|
||||||
totalPages: widget.mediaAttachmentPackages.length,
|
|
||||||
mediaAttachmentPackages:
|
|
||||||
widget.mediaAttachmentPackages,
|
|
||||||
mediaSelectedCallBack: (val) {
|
|
||||||
_currentPage.value = val;
|
|
||||||
_pageController.animateToPage(
|
|
||||||
val,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
);
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_animationController.dispose();
|
_currentPage.dispose();
|
||||||
_pageController.dispose();
|
_pageController.dispose();
|
||||||
|
_isDisplayingDetail.dispose();
|
||||||
for (final package in videoPackages.values) {
|
for (final package in videoPackages.values) {
|
||||||
package.dispose();
|
package.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ class StreamImageGroup extends StatelessWidget {
|
|||||||
this.onReturnAction,
|
this.onReturnAction,
|
||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
this.onAttachmentTap,
|
this.onAttachmentTap,
|
||||||
|
this.imageThumbnailSize = const Size(400, 400),
|
||||||
|
this.imageThumbnailResizeType = 'crop',
|
||||||
|
this.imageThumbnailCropType = 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
/// List of attachments to show
|
/// List of attachments to show
|
||||||
@@ -36,12 +39,25 @@ class StreamImageGroup extends StatelessWidget {
|
|||||||
/// [StreamMessageThemeData] to apply to message
|
/// [StreamMessageThemeData] to apply to message
|
||||||
final StreamMessageThemeData messageTheme;
|
final StreamMessageThemeData messageTheme;
|
||||||
|
|
||||||
/// Size of iamges
|
/// Size of images
|
||||||
final Size size;
|
final Size size;
|
||||||
|
|
||||||
/// Callback for when show message is tapped
|
/// Callback for when show message is tapped
|
||||||
final ShowMessageCallback? onShowMessage;
|
final ShowMessageCallback? onShowMessage;
|
||||||
|
|
||||||
|
/// Size of the attachment image thumbnail.
|
||||||
|
final Size imageThumbnailSize;
|
||||||
|
|
||||||
|
/// Resize type of the image attachment thumbnail.
|
||||||
|
///
|
||||||
|
/// Defaults to [crop]
|
||||||
|
final String /*clip|crop|scale|fill*/ imageThumbnailResizeType;
|
||||||
|
|
||||||
|
/// Crop type of the image attachment thumbnail.
|
||||||
|
///
|
||||||
|
/// Defaults to [center]
|
||||||
|
final String /*center|top|bottom|left|right*/ imageThumbnailCropType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => ConstrainedBox(
|
Widget build(BuildContext context) => ConstrainedBox(
|
||||||
constraints: BoxConstraints.loose(size),
|
constraints: BoxConstraints.loose(size),
|
||||||
@@ -153,5 +169,8 @@ class StreamImageGroup extends StatelessWidget {
|
|||||||
message: message,
|
message: message,
|
||||||
messageTheme: messageTheme,
|
messageTheme: messageTheme,
|
||||||
onAttachmentTap: () => _onTap(context, index),
|
onAttachmentTap: () => _onTap(context, index),
|
||||||
|
imageThumbnailSize: imageThumbnailSize,
|
||||||
|
imageThumbnailResizeType: imageThumbnailResizeType,
|
||||||
|
imageThumbnailCropType: imageThumbnailCropType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ class StreamMediaListView extends StatefulWidget {
|
|||||||
this.selectedIds = const [],
|
this.selectedIds = const [],
|
||||||
this.onSelect,
|
this.onSelect,
|
||||||
this.controller,
|
this.controller,
|
||||||
|
this.thumbnailSize = const ThumbnailSize(400, 400),
|
||||||
|
this.thumbnailFormat = ThumbnailFormat.jpeg,
|
||||||
|
this.thumbnailQuality = 100,
|
||||||
|
this.thumbnailScale = 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Stores the media selected
|
/// Stores the media selected
|
||||||
@@ -33,6 +37,21 @@ class StreamMediaListView extends StatefulWidget {
|
|||||||
/// Controller that handles MediaListView
|
/// Controller that handles MediaListView
|
||||||
final MediaListViewController? controller;
|
final MediaListViewController? controller;
|
||||||
|
|
||||||
|
/// The thumbnail size.
|
||||||
|
final ThumbnailSize thumbnailSize;
|
||||||
|
|
||||||
|
/// {@macro photo_manager.ThumbnailFormat}
|
||||||
|
final ThumbnailFormat thumbnailFormat;
|
||||||
|
|
||||||
|
/// The quality value for the thumbnail.
|
||||||
|
///
|
||||||
|
/// Valid from 1 to 100.
|
||||||
|
/// Defaults to 100.
|
||||||
|
final int thumbnailQuality;
|
||||||
|
|
||||||
|
/// Scale of the image.
|
||||||
|
final double thumbnailScale;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_StreamMediaListViewState createState() => _StreamMediaListViewState();
|
_StreamMediaListViewState createState() => _StreamMediaListViewState();
|
||||||
}
|
}
|
||||||
@@ -75,14 +94,18 @@ class _StreamMediaListViewState extends State<StreamMediaListView> {
|
|||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: FadeInImage(
|
child: FadeInImage(
|
||||||
|
image: MediaThumbnailProvider(
|
||||||
|
media: media,
|
||||||
|
size: widget.thumbnailSize,
|
||||||
|
format: widget.thumbnailFormat,
|
||||||
|
quality: widget.thumbnailQuality,
|
||||||
|
scale: widget.thumbnailScale,
|
||||||
|
),
|
||||||
fadeInDuration: const Duration(milliseconds: 300),
|
fadeInDuration: const Duration(milliseconds: 300),
|
||||||
placeholder: const AssetImage(
|
placeholder: const AssetImage(
|
||||||
'images/placeholder.png',
|
'images/placeholder.png',
|
||||||
package: 'stream_chat_flutter',
|
package: 'stream_chat_flutter',
|
||||||
),
|
),
|
||||||
image: MediaThumbnailProvider(
|
|
||||||
media: media,
|
|
||||||
),
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -195,55 +218,95 @@ class _StreamMediaListViewState extends State<StreamMediaListView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ImageProvider implementation
|
/// ImageProvider implementation for [AssetEntity].
|
||||||
class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
|
class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
|
||||||
/// Constructor for creating a [MediaThumbnailProvider]
|
/// Constructor for creating a [MediaThumbnailProvider]
|
||||||
const MediaThumbnailProvider({
|
const MediaThumbnailProvider({
|
||||||
required this.media,
|
required this.media,
|
||||||
|
// TODO: Are these sizes optimal? Consider web/desktop
|
||||||
|
this.size = const ThumbnailSize(400, 400),
|
||||||
|
this.format = ThumbnailFormat.jpeg,
|
||||||
|
this.quality = 100,
|
||||||
|
this.scale = 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Media to load
|
/// Media to get the thumbnail from.
|
||||||
final AssetEntity media;
|
final AssetEntity media;
|
||||||
|
|
||||||
|
/// The thumbnail size.
|
||||||
|
final ThumbnailSize size;
|
||||||
|
|
||||||
|
/// {@macro photo_manager.ThumbnailFormat}
|
||||||
|
final ThumbnailFormat format;
|
||||||
|
|
||||||
|
/// The quality value for the thumbnail.
|
||||||
|
///
|
||||||
|
/// Valid from 1 to 100.
|
||||||
|
/// Defaults to 100.
|
||||||
|
final int quality;
|
||||||
|
|
||||||
|
/// Scale of the image.
|
||||||
|
final double scale;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<MediaThumbnailProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture<MediaThumbnailProvider>(this);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter load(
|
ImageStreamCompleter load(
|
||||||
MediaThumbnailProvider key,
|
MediaThumbnailProvider key,
|
||||||
DecoderCallback decode,
|
DecoderCallback decode,
|
||||||
) =>
|
) {
|
||||||
MultiFrameImageStreamCompleter(
|
return MultiFrameImageStreamCompleter(
|
||||||
codec: _loadAsync(key, decode),
|
codec: _loadAsync(key, decode),
|
||||||
scale: 1,
|
scale: key.scale,
|
||||||
informationCollector: () sync* {
|
informationCollector: () sync* {
|
||||||
yield ErrorDescription('Id: ${media.id}');
|
yield DiagnosticsProperty<ImageProvider>(
|
||||||
},
|
'Thumbnail provider: $this \n Thumbnail key: $key',
|
||||||
);
|
this,
|
||||||
|
style: DiagnosticsTreeStyle.errorProperty,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<ui.Codec> _loadAsync(
|
Future<ui.Codec> _loadAsync(
|
||||||
MediaThumbnailProvider key,
|
MediaThumbnailProvider key,
|
||||||
DecoderCallback decode,
|
DecoderCallback decode,
|
||||||
) async {
|
) async {
|
||||||
assert(key == this, 'Checks MediaThumbnailProvider');
|
assert(key == this, '$key is not $this');
|
||||||
final bytes = await media.thumbnailData;
|
final bytes = await media.thumbnailDataWithSize(
|
||||||
|
size,
|
||||||
|
format: format,
|
||||||
|
quality: quality,
|
||||||
|
);
|
||||||
return decode(bytes!);
|
return decode(bytes!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<MediaThumbnailProvider> obtainKey(ImageConfiguration configuration) =>
|
|
||||||
SynchronousFuture<MediaThumbnailProvider>(this);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
if (other.runtimeType != runtimeType) return false;
|
if (other is MediaThumbnailProvider) {
|
||||||
final MediaThumbnailProvider typedOther = other;
|
return media == other.media &&
|
||||||
return media.id == typedOther.media.id;
|
size == other.size &&
|
||||||
|
format == other.format &&
|
||||||
|
quality == other.quality &&
|
||||||
|
scale == other.scale;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => media.id.hashCode;
|
int get hashCode => hashValues(media, size, format, quality, scale);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType("${media.id}")';
|
String toString() => '$runtimeType('
|
||||||
|
'media: $media, '
|
||||||
|
'size: $size, '
|
||||||
|
'format: $format, '
|
||||||
|
'quality: $quality, '
|
||||||
|
'scale: $scale'
|
||||||
|
')';
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on Duration {
|
extension on Duration {
|
||||||
@@ -252,7 +315,6 @@ extension on Duration {
|
|||||||
if (s.startsWith('00:')) {
|
if (s.startsWith('00:')) {
|
||||||
return s.replaceFirst('00:', '');
|
return s.replaceFirst('00:', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
this.customActions = const [],
|
this.customActions = const [],
|
||||||
this.onAttachmentTap,
|
this.onAttachmentTap,
|
||||||
this.usernameBuilder,
|
this.usernameBuilder,
|
||||||
|
this.imageAttachmentThumbnailSize = const Size(400, 400),
|
||||||
|
this.imageAttachmentThumbnailResizeType = 'crop',
|
||||||
|
this.imageAttachmentThumbnailCropType = 'center',
|
||||||
}) : attachmentBuilders = {
|
}) : attachmentBuilders = {
|
||||||
'image': (context, message, attachments) {
|
'image': (context, message, attachments) {
|
||||||
final border = RoundedRectangleBorder(
|
final border = RoundedRectangleBorder(
|
||||||
@@ -130,6 +133,10 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
onShowMessage: onShowMessage,
|
onShowMessage: onShowMessage,
|
||||||
onReturnAction: onReturnAction,
|
onReturnAction: onReturnAction,
|
||||||
onAttachmentTap: onAttachmentTap,
|
onAttachmentTap: onAttachmentTap,
|
||||||
|
imageThumbnailSize: imageAttachmentThumbnailSize,
|
||||||
|
imageThumbnailResizeType:
|
||||||
|
imageAttachmentThumbnailResizeType,
|
||||||
|
imageThumbnailCropType: imageAttachmentThumbnailCropType,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
border,
|
border,
|
||||||
@@ -155,6 +162,9 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
onAttachmentTap.call(message, attachments[0]);
|
onAttachmentTap.call(message, attachments[0]);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
imageThumbnailSize: imageAttachmentThumbnailSize,
|
||||||
|
imageThumbnailResizeType: imageAttachmentThumbnailResizeType,
|
||||||
|
imageThumbnailCropType: imageAttachmentThumbnailCropType,
|
||||||
),
|
),
|
||||||
border,
|
border,
|
||||||
reverse,
|
reverse,
|
||||||
@@ -413,6 +423,19 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
/// Customize onTap on attachment
|
/// Customize onTap on attachment
|
||||||
final void Function(Message message, Attachment attachment)? onAttachmentTap;
|
final void Function(Message message, Attachment attachment)? onAttachmentTap;
|
||||||
|
|
||||||
|
/// Size of the image attachment thumbnail.
|
||||||
|
final Size imageAttachmentThumbnailSize;
|
||||||
|
|
||||||
|
/// Resize type of the image attachment thumbnail.
|
||||||
|
///
|
||||||
|
/// Defaults to [crop]
|
||||||
|
final String /*clip|crop|scale|fill*/ imageAttachmentThumbnailResizeType;
|
||||||
|
|
||||||
|
/// Crop type of the image attachment thumbnail.
|
||||||
|
///
|
||||||
|
/// Defaults to [center]
|
||||||
|
final String /*center|top|bottom|left|right*/ imageAttachmentThumbnailCropType;
|
||||||
|
|
||||||
/// Creates a copy of [StreamMessageWidget] with
|
/// Creates a copy of [StreamMessageWidget] with
|
||||||
/// specified attributes overridden.
|
/// specified attributes overridden.
|
||||||
StreamMessageWidget copyWith({
|
StreamMessageWidget copyWith({
|
||||||
@@ -468,6 +491,9 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
List<StreamMessageAction>? customActions,
|
List<StreamMessageAction>? customActions,
|
||||||
void Function(Message message, Attachment attachment)? onAttachmentTap,
|
void Function(Message message, Attachment attachment)? onAttachmentTap,
|
||||||
Widget Function(BuildContext, User)? userAvatarBuilder,
|
Widget Function(BuildContext, User)? userAvatarBuilder,
|
||||||
|
Size? imageAttachmentThumbnailSize,
|
||||||
|
String? imageAttachmentThumbnailResizeType,
|
||||||
|
String? imageAttachmentThumbnailCropType,
|
||||||
}) =>
|
}) =>
|
||||||
StreamMessageWidget(
|
StreamMessageWidget(
|
||||||
key: key ?? this.key,
|
key: key ?? this.key,
|
||||||
@@ -528,6 +554,13 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
customActions: customActions ?? this.customActions,
|
customActions: customActions ?? this.customActions,
|
||||||
onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap,
|
onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap,
|
||||||
userAvatarBuilder: userAvatarBuilder ?? this.userAvatarBuilder,
|
userAvatarBuilder: userAvatarBuilder ?? this.userAvatarBuilder,
|
||||||
|
imageAttachmentThumbnailSize:
|
||||||
|
imageAttachmentThumbnailSize ?? this.imageAttachmentThumbnailSize,
|
||||||
|
imageAttachmentThumbnailResizeType:
|
||||||
|
imageAttachmentThumbnailResizeType ??
|
||||||
|
this.imageAttachmentThumbnailResizeType,
|
||||||
|
imageAttachmentThumbnailCropType: imageAttachmentThumbnailCropType ??
|
||||||
|
this.imageAttachmentThumbnailCropType,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
+60
-5
@@ -43,6 +43,10 @@ class StreamAttachmentPicker extends StatefulWidget {
|
|||||||
DefaultAttachmentTypes.video,
|
DefaultAttachmentTypes.video,
|
||||||
],
|
],
|
||||||
this.customAttachmentTypes = const [],
|
this.customAttachmentTypes = const [],
|
||||||
|
this.attachmentThumbnailSize = const ThumbnailSize(400, 400),
|
||||||
|
this.attachmentThumbnailFormat = ThumbnailFormat.jpeg,
|
||||||
|
this.attachmentThumbnailQuality = 100,
|
||||||
|
this.attachmentThumbnailScale = 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// True if the picker is open.
|
/// True if the picker is open.
|
||||||
@@ -77,6 +81,25 @@ class StreamAttachmentPicker extends StatefulWidget {
|
|||||||
/// The list of custom attachment types that can be picked.
|
/// The list of custom attachment types that can be picked.
|
||||||
final List<CustomAttachmentType> customAttachmentTypes;
|
final List<CustomAttachmentType> customAttachmentTypes;
|
||||||
|
|
||||||
|
/// Size of the attachment thumbnails.
|
||||||
|
///
|
||||||
|
/// Defaults to (400, 400).
|
||||||
|
final ThumbnailSize attachmentThumbnailSize;
|
||||||
|
|
||||||
|
/// Format of the attachment thumbnails.
|
||||||
|
///
|
||||||
|
/// Defaults to [ThumbnailFormat.jpeg].
|
||||||
|
final ThumbnailFormat attachmentThumbnailFormat;
|
||||||
|
|
||||||
|
/// The quality value for the attachment thumbnails.
|
||||||
|
///
|
||||||
|
/// Valid from 1 to 100.
|
||||||
|
/// Defaults to 100.
|
||||||
|
final int attachmentThumbnailQuality;
|
||||||
|
|
||||||
|
/// The scale to apply on the [attachmentThumbnailSize].
|
||||||
|
final double attachmentThumbnailScale;
|
||||||
|
|
||||||
/// Used to create a new copy of [StreamAttachmentPicker] with modified
|
/// Used to create a new copy of [StreamAttachmentPicker] with modified
|
||||||
/// properties.
|
/// properties.
|
||||||
StreamAttachmentPicker copyWith({
|
StreamAttachmentPicker copyWith({
|
||||||
@@ -91,7 +114,11 @@ class StreamAttachmentPicker extends StatefulWidget {
|
|||||||
ValueChanged<bool>? onChangeInputState,
|
ValueChanged<bool>? onChangeInputState,
|
||||||
ValueChanged<String>? onError,
|
ValueChanged<String>? onError,
|
||||||
List<DefaultAttachmentTypes>? allowedAttachmentTypes,
|
List<DefaultAttachmentTypes>? allowedAttachmentTypes,
|
||||||
List<CustomAttachmentType>? customAttachmentTypes = const [],
|
List<CustomAttachmentType>? customAttachmentTypes,
|
||||||
|
ThumbnailSize? attachmentThumbnailSize,
|
||||||
|
ThumbnailFormat? attachmentThumbnailFormat,
|
||||||
|
int? attachmentThumbnailQuality,
|
||||||
|
double? attachmentThumbnailScale,
|
||||||
}) =>
|
}) =>
|
||||||
StreamAttachmentPicker(
|
StreamAttachmentPicker(
|
||||||
key: key ?? this.key,
|
key: key ?? this.key,
|
||||||
@@ -109,6 +136,14 @@ class StreamAttachmentPicker extends StatefulWidget {
|
|||||||
allowedAttachmentTypes ?? this.allowedAttachmentTypes,
|
allowedAttachmentTypes ?? this.allowedAttachmentTypes,
|
||||||
customAttachmentTypes:
|
customAttachmentTypes:
|
||||||
customAttachmentTypes ?? this.customAttachmentTypes,
|
customAttachmentTypes ?? this.customAttachmentTypes,
|
||||||
|
attachmentThumbnailSize:
|
||||||
|
attachmentThumbnailSize ?? this.attachmentThumbnailSize,
|
||||||
|
attachmentThumbnailFormat:
|
||||||
|
attachmentThumbnailFormat ?? this.attachmentThumbnailFormat,
|
||||||
|
attachmentThumbnailQuality:
|
||||||
|
attachmentThumbnailQuality ?? this.attachmentThumbnailQuality,
|
||||||
|
attachmentThumbnailScale:
|
||||||
|
attachmentThumbnailScale ?? this.attachmentThumbnailScale,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -360,6 +395,11 @@ class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
|
|||||||
},
|
},
|
||||||
allowedAttachmentTypes: widget.allowedAttachmentTypes,
|
allowedAttachmentTypes: widget.allowedAttachmentTypes,
|
||||||
customAttachmentTypes: widget.customAttachmentTypes,
|
customAttachmentTypes: widget.customAttachmentTypes,
|
||||||
|
mediaThumbnailSize: widget.attachmentThumbnailSize,
|
||||||
|
mediaThumbnailFormat: widget.attachmentThumbnailFormat,
|
||||||
|
mediaThumbnailQuality:
|
||||||
|
widget.attachmentThumbnailQuality,
|
||||||
|
mediaThumbnailScale: widget.attachmentThumbnailScale,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -374,9 +414,11 @@ class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
|
|||||||
void _addAssetAttachment(AssetEntity medium) async {
|
void _addAssetAttachment(AssetEntity medium) async {
|
||||||
final mediaFile = await medium.originFile;
|
final mediaFile = await medium.originFile;
|
||||||
if (mediaFile == null) return;
|
if (mediaFile == null) return;
|
||||||
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
|
||||||
// TODO: Confirm that this max resolution is final
|
// TODO: Confirm that this max resolution is final
|
||||||
|
// Taken from https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing
|
||||||
const maxCDNImageResolution = 16800000;
|
const maxCDNImageResolution = 16800000;
|
||||||
final imageResolution = medium.width * medium.height;
|
final imageResolution = medium.width * medium.height;
|
||||||
File? cachedFile;
|
File? cachedFile;
|
||||||
@@ -388,12 +430,13 @@ class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
|
|||||||
updatedSize.width.floor(),
|
updatedSize.width.floor(),
|
||||||
updatedSize.height.floor(),
|
updatedSize.height.floor(),
|
||||||
),
|
),
|
||||||
quality: 30, // TODO: investigate compressing all images
|
quality: 70, // TODO: investigate compressing all images
|
||||||
);
|
);
|
||||||
cachedFile =
|
final file =
|
||||||
await File('${tempDir.path}/${mediaFile.path.split('/').last}')
|
await File('${tempDir.path}/${mediaFile.path.split('/').last}')
|
||||||
.create()
|
.create();
|
||||||
..writeAsBytesSync(resizedImage!);
|
file.writeAsBytesSync(resizedImage!);
|
||||||
|
cachedFile = file;
|
||||||
} else {
|
} else {
|
||||||
cachedFile = await mediaFile
|
cachedFile = await mediaFile
|
||||||
.copy('${tempDir.path}/${mediaFile.path.split('/').last}');
|
.copy('${tempDir.path}/${mediaFile.path.split('/').last}');
|
||||||
@@ -457,6 +500,10 @@ class _PickerWidget extends StatefulWidget {
|
|||||||
required this.allowedAttachmentTypes,
|
required this.allowedAttachmentTypes,
|
||||||
required this.customAttachmentTypes,
|
required this.customAttachmentTypes,
|
||||||
required this.mediaListViewController,
|
required this.mediaListViewController,
|
||||||
|
this.mediaThumbnailSize = const ThumbnailSize(400, 400),
|
||||||
|
this.mediaThumbnailFormat = ThumbnailFormat.jpeg,
|
||||||
|
this.mediaThumbnailQuality = 100,
|
||||||
|
this.mediaThumbnailScale = 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
final int filePickerIndex;
|
final int filePickerIndex;
|
||||||
@@ -468,6 +515,10 @@ class _PickerWidget extends StatefulWidget {
|
|||||||
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
|
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
|
||||||
final List<CustomAttachmentType> customAttachmentTypes;
|
final List<CustomAttachmentType> customAttachmentTypes;
|
||||||
final MediaListViewController mediaListViewController;
|
final MediaListViewController mediaListViewController;
|
||||||
|
final ThumbnailSize mediaThumbnailSize;
|
||||||
|
final ThumbnailFormat mediaThumbnailFormat;
|
||||||
|
final int mediaThumbnailQuality;
|
||||||
|
final double mediaThumbnailScale;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_PickerWidgetState createState() => _PickerWidgetState();
|
_PickerWidgetState createState() => _PickerWidgetState();
|
||||||
@@ -522,6 +573,10 @@ class _PickerWidgetState extends State<_PickerWidget> {
|
|||||||
selectedIds: widget.selectedMedias,
|
selectedIds: widget.selectedMedias,
|
||||||
onSelect: widget.onMediaSelected,
|
onSelect: widget.onMediaSelected,
|
||||||
controller: widget.mediaListViewController,
|
controller: widget.mediaListViewController,
|
||||||
|
thumbnailSize: widget.mediaThumbnailSize,
|
||||||
|
thumbnailFormat: widget.mediaThumbnailFormat,
|
||||||
|
thumbnailQuality: widget.mediaThumbnailQuality,
|
||||||
|
thumbnailScale: widget.mediaThumbnailScale,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
import 'package:stream_chat_flutter/src/commands_overlay.dart';
|
import 'package:stream_chat_flutter/src/commands_overlay.dart';
|
||||||
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export 'package:jiffy/jiffy.dart';
|
export 'package:jiffy/jiffy.dart';
|
||||||
|
export 'package:photo_manager/photo_manager.dart'
|
||||||
|
show ThumbnailSize, ThumbnailFormat;
|
||||||
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
export 'src/attachment/attachment.dart';
|
export 'src/attachment/attachment.dart';
|
||||||
@@ -22,6 +24,7 @@ export 'src/info_tile.dart';
|
|||||||
export 'src/localization/stream_chat_localizations.dart';
|
export 'src/localization/stream_chat_localizations.dart';
|
||||||
export 'src/localization/translations.dart' show DefaultTranslations;
|
export 'src/localization/translations.dart' show DefaultTranslations;
|
||||||
export 'src/message_action.dart';
|
export 'src/message_action.dart';
|
||||||
|
|
||||||
// ignore: deprecated_member_use_from_same_package
|
// ignore: deprecated_member_use_from_same_package
|
||||||
export 'src/message_input.dart' show MessageInput, MessageInputState;
|
export 'src/message_input.dart' show MessageInput, MessageInputState;
|
||||||
export 'src/message_list_view.dart';
|
export 'src/message_list_view.dart';
|
||||||
@@ -49,6 +52,7 @@ export 'src/user_item.dart';
|
|||||||
export 'src/user_list_view.dart';
|
export 'src/user_list_view.dart';
|
||||||
export 'src/user_mention_tile.dart';
|
export 'src/user_mention_tile.dart';
|
||||||
export 'src/utils.dart';
|
export 'src/utils.dart';
|
||||||
|
|
||||||
// v4
|
// v4
|
||||||
export 'src/v4/message_input/countdown_button.dart';
|
export 'src/v4/message_input/countdown_button.dart';
|
||||||
export 'src/v4/message_input/stream_attachment_picker.dart';
|
export 'src/v4/message_input/stream_attachment_picker.dart';
|
||||||
|
|||||||
Reference in New Issue
Block a user