Merge branch 'feature/new-ui' of github.com:GetStream/stream-chat-flutter into feat/two-way-pagination

This commit is contained in:
Sahil Kumar
2020-12-03 11:31:22 +05:30
21 changed files with 1476 additions and 169 deletions
@@ -6,8 +6,8 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:name=".Application"
android:label="ChatSample"
+12
View File
@@ -30,6 +30,8 @@ PODS:
- DKPhotoGallery/Resource (0.0.17):
- SDWebImage
- SwiftyGif
- esys_flutter_share (0.0.1):
- Flutter
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
@@ -106,6 +108,8 @@ PODS:
- GoogleUtilities/Logger
- GoogleUtilities/UserDefaults (6.7.2):
- GoogleUtilities/Logger
- image_gallery_saver (1.5.0):
- Flutter
- image_picker (0.0.1):
- Flutter
- nanopb (1.30906.0):
@@ -159,6 +163,7 @@ PODS:
- Flutter
DEPENDENCIES:
- esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
@@ -168,6 +173,7 @@ DEPENDENCIES:
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
- image_picker (from `.symlinks/plugins/image_picker/ios`)
- path_provider (from `.symlinks/plugins/path_provider/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
@@ -203,6 +209,8 @@ SPEC REPOS:
- SwiftyGif
EXTERNAL SOURCES:
esys_flutter_share:
:path: ".symlinks/plugins/esys_flutter_share/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_core:
@@ -221,6 +229,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
image_gallery_saver:
:path: ".symlinks/plugins/image_gallery_saver/ios"
image_picker:
:path: ".symlinks/plugins/image_picker/ios"
path_provider:
@@ -245,6 +255,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5
firebase_core: 7423d688a1c6f2f2d859d64ae26991be39989781
@@ -263,6 +274,7 @@ SPEC CHECKSUMS:
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833
GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3
image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2
image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09
nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
@@ -320,12 +320,14 @@
"${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework",
"${BUILT_PRODUCTS_DIR}/esys_flutter_share/esys_flutter_share.framework",
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
"${BUILT_PRODUCTS_DIR}/flutter_apns/flutter_apns.framework",
"${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework",
"${BUILT_PRODUCTS_DIR}/flutter_keyboard_visibility/flutter_keyboard_visibility.framework",
"${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework",
"${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework",
"${BUILT_PRODUCTS_DIR}/image_gallery_saver/image_gallery_saver.framework",
"${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework",
"${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
"${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
@@ -352,12 +354,14 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/esys_flutter_share.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_apns.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_keyboard_visibility.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_gallery_saver.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
+5
View File
@@ -481,6 +481,11 @@ class _ChannelListViewState extends State<ChannelListView>
onTap: () {
onTap(channel, widget.channelWidget);
},
onLongPress: widget.onChannelLongPress != null
? () {
widget.onChannelLongPress(channel);
}
: null,
),
),
),
-35
View File
@@ -1,35 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
/// A full screen image widget
class FullScreenImage extends StatelessWidget {
/// The url of the image
final String url;
/// Instantiate a new FullScreenImage
const FullScreenImage({
Key key,
@required this.url,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.black,
iconTheme: IconThemeData(
color: Colors.white,
),
),
body: PhotoView(
imageProvider: CachedNetworkImageProvider(url),
maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(
tag: url,
),
),
);
}
}
+259
View File
@@ -0,0 +1,259 @@
import 'package:cached_network_image/cached_network_image.dart';
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:video_player/video_player.dart';
import '../stream_chat_flutter.dart';
/// A full screen image widget
class FullScreenMedia extends StatefulWidget {
/// The url of the image
final List<Attachment> mediaAttachments;
final Message message;
final int startIndex;
final String userName;
final DateTime sentAt;
/// Instantiate a new FullScreenImage
const FullScreenMedia({
Key key,
@required this.mediaAttachments,
this.message,
this.startIndex = 0,
this.userName = '',
this.sentAt,
}) : super(key: key);
@override
_FullScreenMediaState createState() => _FullScreenMediaState();
}
class _FullScreenMediaState extends State<FullScreenMedia>
with SingleTickerProviderStateMixin {
bool _optionsShown = true;
AnimationController _controller;
PageController _pageController;
int _currentPage;
List<VideoPackage> videoPackages = [];
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
_pageController = PageController(initialPage: widget.startIndex);
_currentPage = widget.startIndex;
widget.mediaAttachments
.where((element) => element.type == 'video')
.toList()
.forEach((element) {
videoPackages.add(VideoPackage(context, element, () {
setState(() {});
}));
});
}
@override
Widget build(BuildContext context) {
var videoAttachments = widget.mediaAttachments
.where((element) => element.type == 'video')
.toList();
return Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, snapshot) {
return PageView.builder(
controller: _pageController,
onPageChanged: (val) {
setState(() {
_currentPage = val;
});
},
itemBuilder: (context, position) {
if (widget.mediaAttachments[position].type == 'image' ||
widget.mediaAttachments[position].type == 'giphy') {
return PhotoView(
imageProvider: CachedNetworkImageProvider(
widget.mediaAttachments[position].imageUrl ??
widget.mediaAttachments[position].assetUrl ??
widget.mediaAttachments[position].thumbUrl),
maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(
tag: widget.mediaAttachments,
),
backgroundDecoration: BoxDecoration(
color: ColorTween(
begin: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.color,
end: Colors.black)
.lerp(_controller.value),
),
onTapUp: (a, b, c) {
setState(() {
_optionsShown = !_optionsShown;
});
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
);
} else if (widget.mediaAttachments[position].type ==
'video') {
var controllerPackage = videoPackages[videoAttachments
.indexOf(widget.mediaAttachments[position])];
if (!controllerPackage.initialised) {
return Center(
child: CircularProgressIndicator(),
);
}
return InkWell(
onTap: () {
setState(() {
_optionsShown = !_optionsShown;
});
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 50.0,
),
child: Chewie(
controller: controllerPackage.chewieController,
),
),
);
}
return Container();
},
itemCount: widget.mediaAttachments.length,
);
}),
AnimatedOpacity(
opacity: _optionsShown ? 1.0 : 0.0,
duration: Duration(milliseconds: 300),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ImageHeader(
userName: widget.userName,
sentAt: widget.message.createdAt == null
? ''
: 'Sent ${getDay(widget.message.createdAt)} at ${Jiffy(widget.sentAt.toLocal()).format('HH:mm')}',
onBackPressed: () {
Navigator.of(context).pop();
},
message: widget.message,
urls: widget.mediaAttachments,
currentIndex: _currentPage,
),
ImageFooter(
currentPage: _currentPage,
totalPages: widget.mediaAttachments.length,
mediaAttachments: widget.mediaAttachments,
message: widget.message,
videoPackages: videoPackages,
mediaSelectedCallBack: (val) {
setState(() {
_currentPage = val;
_pageController.animateToPage(val,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut);
Navigator.pop(context);
});
},
),
],
),
),
],
),
);
}
String getDay(DateTime dateTime) {
var now = DateTime.now();
if (DateTime(dateTime.year, dateTime.month, dateTime.day) ==
DateTime(now.year, now.month, now.day)) {
return 'today';
} else if (DateTime(now.year, now.month, now.day)
.difference(dateTime)
.inHours <
24) {
return 'yesterday';
} else {
return 'on ${Jiffy(dateTime).format("MMM do")}';
}
}
@override
void dispose() {
videoPackages.forEach((element) {
element.dispose();
});
super.dispose();
}
}
class VideoPackage {
VideoPlayerController _videoPlayerController;
ChewieController _chewieController;
bool initialised = false;
VoidCallback onInit;
BuildContext context;
///
VideoPackage(this.context, Attachment attachment, this.onInit) {
_videoPlayerController = VideoPlayerController.network(attachment.assetUrl);
_videoPlayerController.initialize().whenComplete(() {
initialised = true;
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoInitialize: false,
aspectRatio: _videoPlayerController.value.aspectRatio,
);
onInit();
});
VoidCallback errorListener;
errorListener = () {
if (_videoPlayerController.value.hasError) {
Navigator.pop(context);
launchURL(context, attachment.titleLink);
}
_videoPlayerController.removeListener(errorListener);
};
_videoPlayerController.addListener(errorListener);
}
get videoPlayer => _videoPlayerController;
get chewieController => _chewieController;
void dispose() {
_videoPlayerController.dispose();
_chewieController.dispose();
}
}
-84
View File
@@ -1,84 +0,0 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
import 'utils.dart';
class FullScreenVideo extends StatefulWidget {
final Attachment attachment;
FullScreenVideo({
Key key,
@required this.attachment,
}) : super(key: key);
@override
_FullScreenVideoState createState() => _FullScreenVideoState();
}
class _FullScreenVideoState extends State<FullScreenVideo> {
ChewieController _chewieController;
VideoPlayerController _videoPlayerController;
bool initialized = false;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.black,
iconTheme: IconThemeData(
color: Colors.white,
),
),
body: Builder(
key: _scaffoldKey,
builder: (context) {
if (!initialized) {
return Center(
child: CircularProgressIndicator(),
);
}
return Chewie(
controller: _chewieController,
);
},
),
);
}
@override
void initState() {
super.initState();
_videoPlayerController =
VideoPlayerController.network(widget.attachment.assetUrl);
_videoPlayerController.initialize().whenComplete(() {
setState(() {
initialized = true;
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoInitialize: false,
aspectRatio: _videoPlayerController.value.aspectRatio,
);
});
});
VoidCallback errorListener;
errorListener = () {
if (_videoPlayerController.value.hasError) {
Navigator.pop(context);
launchURL(_scaffoldKey.currentContext, widget.attachment.titleLink);
}
_videoPlayerController.removeListener(errorListener);
};
_videoPlayerController.addListener(errorListener);
}
@override
void dispose() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
}
+25 -9
View File
@@ -5,7 +5,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import '../stream_chat_flutter.dart';
import 'attachment_error.dart';
import 'full_screen_image.dart';
import 'full_screen_media.dart';
class GiphyAttachment extends StatelessWidget {
final Attachment attachment;
@@ -64,10 +64,18 @@ class GiphyAttachment extends StatelessWidget {
child: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return FullScreenImage(
url: attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl,
final channel = StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: [
attachment,
],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
),
);
}));
},
@@ -278,10 +286,18 @@ class GiphyAttachment extends StatelessWidget {
child: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return FullScreenImage(
url: attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl,
var channel = StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: [
attachment,
],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
),
);
}));
},
+222
View File
@@ -0,0 +1,222 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import '../stream_chat_flutter.dart';
import 'package:path_provider/path_provider.dart';
class ImageActionsModal extends StatelessWidget {
final Message message;
final String userName;
final String sentAt;
final List<Attachment> urls;
final currentIndex;
ImageActionsModal(
{this.message, this.userName, this.sentAt, this.urls, this.currentIndex});
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
Navigator.pop(context);
},
child: Stack(
children: [
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.8),
Colors.transparent,
],
stops: [0.0, 0.4],
)),
)),
_buildPage(context),
],
),
);
}
Widget _buildPage(context) {
return Material(
color: Colors.transparent,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: 40.0,
),
),
Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
userName,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
Text(
sentAt,
style: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle
.copyWith(color: Colors.white.withOpacity(0.5)),
),
],
),
IconButton(
icon: StreamSvgIcon.close(
size: 24.0,
color: Colors.white,
),
onPressed: () {
Navigator.pop(context);
},
),
],
),
Align(
alignment: Alignment.centerRight,
child: Container(
width: MediaQuery.of(context).size.width / 1.8,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Material(
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: Color(0xffe5e5e5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: ListTile.divideTiles(
context: context,
tiles: [
_buildButton(
context,
'Reply',
StreamSvgIcon.Icon_curve_line_left_up(
size: 24.0,
color: Colors.black.withOpacity(0.5),
),
() {}),
_buildButton(
context,
'Show in Chat',
StreamSvgIcon.eye(
size: 24.0,
color: Colors.black,
), () {
Navigator.pop(context);
Navigator.pop(context);
}),
_buildButton(
context,
'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
StreamSvgIcon.Icon_save(
size: 24.0,
color: Colors.black.withOpacity(0.5),
), () async {
var url = urls[currentIndex].imageUrl ??
urls[currentIndex].assetUrl ??
urls[currentIndex].thumbUrl;
if (urls[currentIndex].type == 'video') {
await _saveVideo(url);
Navigator.pop(context);
} else {
await _saveImage(url);
Navigator.pop(context);
}
}),
if (StreamChat.of(context).user.id == message.user.id)
_buildButton(
context,
'Delete',
StreamSvgIcon.delete(
size: 24.0,
color: Color(0xffFF3742),
),
() {
Navigator.pop(context);
Navigator.pop(context);
StreamChat.of(context).client.deleteMessage(
message,
StreamChannel.of(context).channel.cid,
);
},
color: Color(0xffFF3742),
),
],
).toList(),
),
),
),
),
),
],
),
);
}
Widget _buildButton(
context, String title, StreamSvgIcon icon, VoidCallback onTap,
{Color color}) {
var titleStyle = TextStyle(
fontSize: 14.5,
color: Colors.black,
);
return Material(
child: InkWell(
onTap: onTap,
child: ListTile(
dense: true,
title: Text(
title,
style:
color == null ? titleStyle : titleStyle.copyWith(color: color),
),
leading: icon,
),
),
);
}
Future<void> _saveImage(String url) async {
var response = await Dio()
.get(url, options: Options(responseType: ResponseType.bytes));
final result = await ImageGallerySaver.saveImage(
Uint8List.fromList(response.data),
quality: 60,
name: "${DateTime.now().millisecondsSinceEpoch}");
return result;
}
Future<void> _saveVideo(String url) async {
var appDocDir = await getTemporaryDirectory();
var savePath =
appDocDir.path + "/${DateTime.now().millisecondsSinceEpoch}.mp4";
await Dio().download(url, savePath);
final result = await ImageGallerySaver.saveFile(savePath);
print(result);
}
}
+13 -5
View File
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import '../stream_chat_flutter.dart';
import 'attachment_error.dart';
import 'attachment_title.dart';
import 'full_screen_image.dart';
import 'full_screen_media.dart';
import 'utils.dart';
class ImageAttachment extends StatelessWidget {
@@ -43,10 +43,18 @@ class ImageAttachment extends StatelessWidget {
context,
MaterialPageRoute(
builder: (_) {
return FullScreenImage(
url: attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl,
final channel = StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: [
attachment,
],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
),
);
},
),
+682
View File
@@ -0,0 +1,682 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:esys_flutter_share/esys_flutter_share.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'stream_channel.dart';
class ImageFooter extends StatefulWidget {
/// 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;
final int currentPage;
final int totalPages;
final List<Attachment> mediaAttachments;
final Message message;
final List<VideoPackage> videoPackages;
final ValueChanged<int> mediaSelectedCallBack;
/// Creates a channel header
ImageFooter({
Key key,
this.onBackPressed,
this.onTitleTap,
this.onImageTap,
this.currentPage = 0,
this.totalPages = 0,
this.mediaAttachments,
this.message,
this.videoPackages,
this.mediaSelectedCallBack,
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override
_ImageFooterState createState() => _ImageFooterState();
@override
final Size preferredSize;
}
class _ImageFooterState extends State<ImageFooter> {
bool _userSearchMode = false;
TextEditingController _searchController;
TextEditingController _messageController = TextEditingController();
String _userNameQuery;
bool _isSearchActive = false;
Set<User> _selectedUsers = {};
bool _loading = false;
Timer _debounce;
Function modalSetStateCallback;
void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted && modalSetStateCallback != null) {
modalSetStateCallback(() {
_userNameQuery = _searchController.text;
_isSearchActive = _userNameQuery.isNotEmpty;
});
}
});
}
@override
void initState() {
super.initState();
_searchController = TextEditingController()..addListener(_userNameListener);
}
@override
void dispose() {
_searchController?.clear();
_searchController?.removeListener(_userNameListener);
_searchController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
color:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: StreamSvgIcon.icon_SHARE(
size: 24.0,
color: Colors.black,
),
onPressed: () {
_buildShareModal(context);
},
),
InkWell(
onTap: widget.onTitleTap,
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'${widget.currentPage + 1} of ${widget.totalPages}',
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
],
),
),
),
IconButton(
icon: StreamSvgIcon.Icon_grid(
color: Colors.black,
),
onPressed: () {
_buildPhotosModal(context);
},
),
],
),
),
);
}
Widget _buildPhotosModal(context) {
var videoAttachments = widget.mediaAttachments
.where((element) => element.type == 'video')
.toList();
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0),
topLeft: Radius.circular(16.0),
)),
child: Stack(
children: [
Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Photos',
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
),
),
Align(
alignment: Alignment.centerRight,
child: IconButton(
icon: StreamSvgIcon.close(
color: Colors.black,
),
onPressed: () {
Navigator.pop(context);
},
),
),
],
),
),
Container(
color: Colors.white,
child: GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, position) {
if (widget.mediaAttachments[position].type == 'video') {
var controllerPackage = widget.videoPackages[
videoAttachments
.indexOf(widget.mediaAttachments[position])];
return InkWell(
onTap: () {
widget.mediaSelectedCallBack(position);
},
child: FittedBox(
child: Chewie(
controller: controllerPackage.chewieController,
),
),
);
} else {
return InkWell(
onTap: () {
widget.mediaSelectedCallBack(position);
},
child: Padding(
padding: const EdgeInsets.all(1.0),
child: AspectRatio(
child: CachedNetworkImage(
imageUrl: widget
.mediaAttachments[position].imageUrl ??
widget.mediaAttachments[position].assetUrl ??
widget.mediaAttachments[position].thumbUrl,
fit: BoxFit.cover,
),
aspectRatio: 1.0,
),
),
);
}
},
itemCount: widget.mediaAttachments.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3),
),
),
],
);
},
);
}
Widget _buildShareModal(context) {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(builder: (context, modalSetState) {
modalSetStateCallback = modalSetState;
return Padding(
padding: EdgeInsets.only(
top: _userSearchMode
? 16.0
: MediaQuery.of(context).size.height / 2,
left: 8.0,
right: 8.0),
child: Material(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
clipBehavior: Clip.antiAlias,
child: Scaffold(
body: UsersBloc(
child: Column(
children: [
_buildTextInputSection(modalSetState),
Expanded(
child: UserListView(
selectedUsers: _selectedUsers,
onUserTap: (user, _) {
_searchController.clear();
if (!_selectedUsers.contains(user)) {
modalSetState(() {
_selectedUsers.add(user);
});
} else {
modalSetState(() {
_selectedUsers.remove(user);
});
}
},
crossAxisCount: 4,
pagination: PaginationParams(
limit: 25,
),
filter: {
if (_searchController.text.isNotEmpty)
'name': {
r'$autocomplete': _userNameQuery,
},
'id': {
r'$ne': StreamChat.of(context).user.id,
},
},
sort: [
SortOption(
'name',
direction: 1,
),
],
emptyBuilder: (_) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(24),
child: StreamSvgIcon.search(
size: 96,
color: Colors.grey,
),
),
Text(
'No user matches these keywords...'),
],
),
),
),
);
},
);
},
),
),
if (_selectedUsers.isNotEmpty)
_buildShareTextInputSection(modalSetState),
if (!_userSearchMode && _selectedUsers.isEmpty)
Align(
alignment: Alignment.bottomCenter,
child: Container(
color: Colors.white,
height: 48.0,
child: Material(
child: InkWell(
onTap: () async {
var url = widget
.mediaAttachments[widget.currentPage]
.imageUrl ??
widget
.mediaAttachments[widget.currentPage]
.assetUrl ??
widget
.mediaAttachments[widget.currentPage]
.thumbUrl;
if (widget
.mediaAttachments[widget.currentPage]
.type ==
'video') {
await _saveVideo(url);
Navigator.pop(context);
} else {
await _saveImage(url);
Navigator.pop(context);
}
},
child: SizedBox.expand(
child: Center(
child: Text(
'Save to Photos',
style: TextStyle(
color: StreamChatTheme.of(context)
.accentColor,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
),
],
),
),
),
),
);
});
},
);
}
Widget _buildTextInputSection(modalSetState) {
if (_userSearchMode) {
return Column(
children: [
SizedBox(
height: 16.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: 16.0,
),
Expanded(
child: TextField(
controller: _searchController,
cursorColor: Colors.black,
autofocus: true,
decoration: InputDecoration(
isDense: true,
prefixIconConstraints:
BoxConstraints.tight(Size(36.0, 44.0)),
prefixIcon: Padding(
padding: const EdgeInsets.symmetric(
vertical: 2.0, horizontal: 6.0),
child: StreamSvgIcon.search(
color: Colors.black,
),
),
hintText: 'Search',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
borderSide:
BorderSide(color: Colors.black.withOpacity(0.08)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
borderSide:
BorderSide(color: Colors.black.withOpacity(0.08)),
),
contentPadding: EdgeInsets.zero,
),
),
),
SizedBox(
width: 8.0,
),
IconButton(
icon: StreamSvgIcon.close_small(
color: Colors.black.withOpacity(0.5),
),
onPressed: () {
modalSetState(() {
_userSearchMode = false;
});
setState(() {});
},
)
],
),
],
);
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: IconButton(
icon: StreamSvgIcon.search(
color: Colors.black,
),
iconSize: 24.0,
onPressed: () {
modalSetState(() {
_userSearchMode = true;
});
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Select a Chat to Share',
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: IconButton(
icon: StreamSvgIcon.share_arrow(
color: Colors.black,
),
onPressed: () async {
var url =
widget.mediaAttachments[widget.currentPage].imageUrl ??
widget.mediaAttachments[widget.currentPage].assetUrl ??
widget.mediaAttachments[widget.currentPage].thumbUrl;
var type =
widget.mediaAttachments[widget.currentPage].type == 'image'
? 'jpg'
: url?.split('?')?.first?.split('.')?.last ?? 'jpg';
var request = await HttpClient().getUrl(Uri.parse(url));
var response = await request.close();
var bytes = await consolidateHttpClientResponseBytes(response);
await Share.file('File', 'image.$type', bytes, 'image/$type');
},
),
),
],
);
}
}
Widget _buildShareTextInputSection(modalSetState) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
color: Colors.white,
height: 56.0,
child: _loading
? Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
)
: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: TextField(
controller: _messageController,
onChanged: (val) {
modalSetState(() {});
},
onTap: () {
modalSetState(() {});
setState(() {});
},
decoration: InputDecoration(
isDense: true,
prefixText: ' ',
hintText: 'Add a comment',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
borderSide: BorderSide(
color: Colors.black.withOpacity(0.16),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
borderSide: BorderSide(
color: Colors.black.withOpacity(0.16),
)),
contentPadding: EdgeInsets.symmetric(vertical: 12.0),
),
),
),
),
IconTheme(
data: StreamChatTheme.of(context)
.channelTheme
.messageInputButtonIconTheme,
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
modalSetState(() {
_loading = true;
});
await sendMessage();
modalSetState(() {
_loading = false;
});
},
child: Transform.rotate(
angle: -pi / 2,
child: StreamSvgIcon.Icon_send_message(
color: StreamChatTheme.of(context).accentColor,
),
),
),
),
),
),
],
),
),
);
}
/// Sends the current message
Future sendMessage() async {
var text = _messageController.text.trim();
final attachments = widget.message.attachments;
_messageController.clear();
final client = StreamChat.of(context).client;
for (var user in _selectedUsers) {
var c = client.channel('messaging', extraData: {
'members': [
user.id,
StreamChat.of(context).user.id,
],
});
await c.watch();
final message = Message(
text: text,
attachments: [attachments[widget.currentPage]],
);
await c.sendMessage(message);
}
_selectedUsers.clear();
Navigator.pop(context);
}
Future<void> _saveImage(String url) async {
var response = await Dio()
.get(url, options: Options(responseType: ResponseType.bytes));
final result = await ImageGallerySaver.saveImage(
Uint8List.fromList(response.data),
quality: 60,
name: "${DateTime.now().millisecondsSinceEpoch}");
return result;
}
Future<void> _saveVideo(String url) async {
var appDocDir = await getTemporaryDirectory();
var savePath =
appDocDir.path + "/${DateTime.now().millisecondsSinceEpoch}.mp4";
await Dio().download(url, savePath);
final result = await ImageGallerySaver.saveFile(savePath);
print(result);
}
}
/// Used for clipping textfield prefix icon
class IconClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
var leftX = size.width / 5;
var rightX = 4 * size.width / 5;
var topY = size.height / 5;
var bottomY = 4 * size.height / 5;
final path = Path();
path.moveTo(leftX, topY);
path.lineTo(leftX, bottomY);
path.lineTo(rightX, bottomY);
path.lineTo(rightX, topY);
path.lineTo(leftX, topY);
path.lineTo(0.0, 0.0);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper oldClipper) {
return false;
}
}
+14 -29
View File
@@ -3,7 +3,8 @@ import 'package:carousel_slider/carousel_options.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/full_screen_image.dart';
import 'package:stream_chat_flutter/src/full_screen_media.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ImageGroup extends StatelessWidget {
const ImageGroup({
@@ -107,37 +108,21 @@ class ImageGroup extends StatelessWidget {
BuildContext context, [
int index,
]) {
final channel = StreamChannel.of(context).channel;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
iconTheme: IconThemeData(
color: Colors.white,
),
),
backgroundColor: Colors.black,
body: SizedBox.expand(
child: CarouselSlider(
items: images
.map((image) => FullScreenImage(
url: image.imageUrl ??
image.thumbUrl ??
image.assetUrl,
))
.toList(),
options: CarouselOptions(
initialPage: index ?? 0,
enableInfiniteScroll: false,
viewportFraction: 0.95,
height: MediaQuery.of(context).size.height,
),
),
),
);
},
builder: (context) => StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: images,
startIndex: index,
userName: message.user.name,
sentAt: message.createdAt,
message: message,
),
),
),
);
}
+122
View File
@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'image_actions_modal.dart';
import 'stream_channel.dart';
class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
/// True if this header shows the leading back button
final bool showBackButton;
/// 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;
final Message message;
final String userName;
final String sentAt;
final List<Attachment> urls;
final currentIndex;
/// Creates a channel header
ImageHeader({
Key key,
this.message,
this.urls,
this.currentIndex,
this.showBackButton = true,
this.onBackPressed,
this.onTitleTap,
this.onImageTap,
this.userName = '',
this.sentAt = '',
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override
Widget build(BuildContext context) {
return AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
leading: showBackButton
? IconButton(
icon: StreamSvgIcon.close(
color: Colors.black,
size: 24.0,
),
onPressed: onBackPressed,
)
: SizedBox(),
backgroundColor:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
actions: <Widget>[
IconButton(
icon: StreamSvgIcon.Icon_menu_point_v(
color: Colors.black,
),
onPressed: () {
_showMessageActionModalBottomSheet(context);
},
),
],
centerTitle: true,
title: InkWell(
onTap: onTitleTap,
child: Container(
height: preferredSize.height,
width: preferredSize.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
userName,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
Text(
sentAt,
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
),
],
),
),
),
);
}
@override
final Size preferredSize;
void _showMessageActionModalBottomSheet(BuildContext context) {
final channel = StreamChannel.of(context).channel;
showDialog(
context: context,
builder: (context) {
return StreamChannel(
channel: channel,
child: ImageActionsModal(
userName: userName,
sentAt: sentAt,
message: message,
urls: urls,
currentIndex: currentIndex,
),
);
});
}
}
+1
View File
@@ -180,6 +180,7 @@ class MessageWidget extends StatefulWidget {
MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3,
),
message: message,
);
},
'giphy': (context, message, attachment) {
+84
View File
@@ -397,4 +397,88 @@ class StreamSvgIcon extends StatelessWidget {
height: size,
);
}
factory StreamSvgIcon.Icon_curve_line_left_up({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_curve_line_left_up.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.icon_SHARE({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'icon_SHARE.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_grid({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_grid.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_send_message({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_send_message.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_menu_point_v({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_menu_point_v.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_save({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_save.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.share_arrow({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'share_arrow.svg',
color: color,
width: size,
height: size,
);
}
}
+14 -4
View File
@@ -1,7 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/full_screen_video.dart';
import 'package:stream_chat_flutter/src/full_screen_media.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
@@ -13,11 +13,13 @@ class VideoAttachment extends StatefulWidget {
final Attachment attachment;
final MessageTheme messageTheme;
final Size size;
final Message message;
VideoAttachment({
Key key,
@required this.attachment,
@required this.messageTheme,
this.message,
this.size,
}) : super(key: key);
@@ -81,11 +83,19 @@ class _VideoAttachmentState extends State<VideoAttachment> {
return GestureDetector(
onTap: () {
final channel = StreamChannel.of(context).channel;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => FullScreenVideo(
attachment: widget.attachment,
builder: (_) => StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: [widget.attachment],
userName: widget.message.user.name,
sentAt: widget.message.createdAt,
message: widget.message,
),
),
),
);
@@ -100,7 +110,7 @@ class _VideoAttachmentState extends State<VideoAttachment> {
children: <Widget>[
Expanded(
child: FittedBox(
fit: BoxFit.cover,
fit: BoxFit.none,
child: Stack(
children: <Widget>[
Chewie(
+3 -1
View File
@@ -11,7 +11,9 @@ export 'src/channels_bloc.dart';
export 'src/date_divider.dart';
export 'src/deleted_message.dart';
export 'src/file_attachment.dart';
export 'src/full_screen_video.dart';
export 'src/full_screen_media.dart';
export 'src/image_header.dart';
export 'src/image_footer.dart';
export 'src/giphy_attachment.dart';
export 'src/image_attachment.dart';
export 'src/message_input.dart';
+5
View File
@@ -0,0 +1,5 @@
<svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 4C0.8954 4 0 3.10457 0 2C0 0.89543 0.8954 0 2 0C3.1046 0 4 0.89543 4 2C4 3.10457 3.1046 4 2 4Z" fill="black"/>
<path d="M2 10C0.8954 10 0 9.1046 0 8C0 6.8954 0.8954 6 2 6C3.1046 6 4 6.8954 4 8C4 9.1046 3.1046 10 2 10Z" fill="black"/>
<path d="M2 16C0.8954 16 0 15.1046 0 14C0 12.8954 0.8954 12 2 12C3.1046 12 4 12.8954 4 14C4 15.1046 3.1046 16 2 16Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 479 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C11.4477 4 11 4.44771 11 5V14.529L6.78165 10.306C6.37408 9.89799 5.71326 9.89799 5.30568 10.306C4.8981 10.714 4.89811 11.3756 5.30568 11.7836L11.2096 17.694C11.4267 17.9114 11.7157 18.0129 12 17.9987C12.2843 18.0129 12.5733 17.9113 12.7905 17.694L18.6944 11.7836C19.1019 11.3756 19.1019 10.714 18.6944 10.306C18.2868 9.89799 17.626 9.89799 17.2184 10.306L13 14.529V5C13 4.44771 12.5523 4 12 4Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 21C5 20.4477 5.44771 20 6 20H18C18.5523 20 19 20.4477 19 21C19 21.5523 18.5523 22 18 22H6C5.44772 22 5 21.5523 5 21Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 752 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M22 12C22 6.48 17.52 2 12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12ZM8 11H12V8L16 12L12 16V13H8V11Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 293 B

+2
View File
@@ -41,6 +41,8 @@ dependencies:
flutter_slidable: ^0.5.4
carousel_slider: ^2.2.1
clipboard: ^0.1.2+8
image_gallery_saver: ^1.6.6
esys_flutter_share: ^1.0.2
photo_manager: ^0.5.8
transparent_image: ^1.0.0
ezanimation: ^0.4.1