Files
stream-chat-flutter/packages/stream_chat_flutter/lib/src/image_footer.dart
T
2021-05-21 09:40:03 +02:00

313 lines
11 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
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 {
/// Creates a channel header
const ImageFooter({
Key? key,
required this.message,
this.onBackPressed,
this.onTitleTap,
this.onImageTap,
this.currentPage = 0,
this.totalPages = 0,
this.mediaAttachments = const [],
this.mediaSelectedCallBack,
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
super(key: key);
/// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop]
final VoidCallback? onBackPressed;
/// Callback to call when the header is tapped.
final VoidCallback? onTitleTap;
/// Callback to call when the image is tapped.
final VoidCallback? onImageTap;
/// Stores the current index of media shown
final int currentPage;
/// Total number of pages of media
final int totalPages;
/// All attachments to show
final List<Attachment> mediaAttachments;
/// Message which attachments are attached to
final Message message;
/// Callback when media is selected
final ValueChanged<int>? mediaSelectedCallBack;
@override
_ImageFooterState createState() => _ImageFooterState();
@override
final Size preferredSize;
}
class _ImageFooterState extends State<ImageFooter> {
final TextEditingController _messageController = TextEditingController();
final FocusNode _messageFocusNode = FocusNode();
final List<Channel> _selectedChannels = [];
@override
void initState() {
super.initState();
_messageFocusNode.addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
const showShareButton = !kIsWeb;
final mediaQueryData = MediaQuery.of(context);
final chatThemeData = StreamChatTheme.of(context);
return SizedBox.fromSize(
size: Size(
mediaQueryData.size.width,
mediaQueryData.padding.bottom + widget.preferredSize.height,
),
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: BottomAppBar(
color: chatThemeData.colorTheme.white,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (!showShareButton)
Container(
width: 48,
)
else
IconButton(
icon: StreamSvgIcon.iconShare(
size: 24,
color: chatThemeData.colorTheme.black,
),
onPressed: () async {
final attachment =
widget.mediaAttachments[widget.currentPage];
final url = attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl!;
final type = attachment.type == 'image'
? 'jpg'
: url.split('?').first.split('.').last;
final request = await HttpClient().getUrl(Uri.parse(url));
final response = await request.close();
final bytes =
await consolidateHttpClientResponseBytes(response);
final tmpPath = await getTemporaryDirectory();
final filePath = '${tmpPath.path}/${attachment.id}.$type';
final file = File(filePath);
await file.writeAsBytes(bytes);
await Share.shareFiles(
[filePath],
mimeTypes: [
'image/$type',
],
);
},
),
InkWell(
onTap: widget.onTitleTap,
child: SizedBox(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'${widget.currentPage + 1} of ${widget.totalPages}',
style: chatThemeData.textTheme.headlineBold,
),
],
),
),
),
IconButton(
icon: StreamSvgIcon.iconGrid(
color: chatThemeData.colorTheme.black,
),
onPressed: () => _showPhotosModal(context),
),
],
),
),
),
);
}
void _showPhotosModal(context) {
final chatThemeData = StreamChatTheme.of(context);
showModalBottomSheet(
context: context,
barrierColor: chatThemeData.colorTheme.overlay,
backgroundColor: chatThemeData.colorTheme.white,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
builder: (context) {
const crossAxisCount = 3;
final noOfRowToShowInitially =
widget.mediaAttachments.length > crossAxisCount ? 2 : 1;
final size = MediaQuery.of(context).size;
final initialChildSize =
48 + (size.width * noOfRowToShowInitially) / crossAxisCount;
return DraggableScrollableSheet(
expand: false,
initialChildSize: initialChildSize / size.height,
minChildSize: initialChildSize / size.height,
builder: (context, scrollController) => SingleChildScrollView(
controller: scrollController,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
children: [
Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Photos',
style: chatThemeData.textTheme.headlineBold,
),
),
),
Align(
alignment: Alignment.centerRight,
child: IconButton(
icon: StreamSvgIcon.close(
color: chatThemeData.colorTheme.black,
),
onPressed: () => Navigator.maybePop(context),
),
),
],
),
Flexible(
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: widget.mediaAttachments.length,
padding: const EdgeInsets.all(1),
// ignore: lines_longer_than_80_chars
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
),
itemBuilder: (context, index) {
Widget media;
final attachment = widget.mediaAttachments[index];
if (attachment.type == 'video') {
media = InkWell(
onTap: () => widget.mediaSelectedCallBack!(index),
child: FittedBox(
fit: BoxFit.cover,
child: VideoThumbnailImage(
video: (attachment.file?.path ??
attachment.assetUrl)!,
),
),
);
} else {
media = InkWell(
onTap: () => widget.mediaSelectedCallBack!(index),
child: AspectRatio(
aspectRatio: 1,
child: CachedNetworkImage(
imageUrl: attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl!,
fit: BoxFit.cover,
),
),
);
}
return Stack(
children: [
media,
if (widget.message.user != null)
Padding(
padding: const EdgeInsets.all(8),
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.6),
boxShadow: [
BoxShadow(
blurRadius: 8,
color: chatThemeData.colorTheme.black
.withOpacity(0.3),
),
],
),
padding: const EdgeInsets.all(2),
child: UserAvatar(
user: widget.message.user!,
constraints:
BoxConstraints.tight(const Size(24, 24)),
showOnlineStatus: false,
),
),
),
],
);
},
),
),
],
),
),
);
},
);
}
/// Sends the current message
Future sendMessage() async {
final text = _messageController.text.trim();
final attachments = widget.message.attachments;
_messageController.clear();
for (final channel in _selectedChannels) {
final message = Message(
text: text,
attachments: [attachments[widget.currentPage]],
);
await channel.sendMessage(message);
}
_selectedChannels.clear();
Navigator.pop(context);
}
}