diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 81791a3..c981d0f 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -35,7 +35,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 5214f13..6af34f6 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2,8 +2,6 @@ PODS: - audio_session (0.0.1): - Flutter - Flutter (1.0.0) - - flutter_keyboard_visibility (0.0.1): - - Flutter - image_picker_ios (0.0.1): - Flutter - just_audio (0.0.1): @@ -18,7 +16,6 @@ PODS: DEPENDENCIES: - audio_session (from `.symlinks/plugins/audio_session/ios`) - Flutter (from `Flutter`) - - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - just_audio (from `.symlinks/plugins/just_audio/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) @@ -30,8 +27,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/audio_session/ios" Flutter: :path: Flutter - flutter_keyboard_visibility: - :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" just_audio: @@ -46,7 +41,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: audio_session: 4f3e461722055d21515cf3261b64c973c062f345 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 diff --git a/example/pubspec.lock b/example/pubspec.lock index 7e9123c..674d39f 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -22,6 +22,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + camera: + dependency: transitive + description: + name: camera + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.1" + camera_android: + dependency: transitive + description: + name: camera_android + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.2" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.10" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.2" + camera_web: + dependency: transitive + description: + name: camera_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1" characters: dependency: transitive description: @@ -97,48 +132,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_keyboard_visibility: - dependency: transitive - description: - name: flutter_keyboard_visibility - url: "https://pub.dartlang.org" - source: hosted - version: "5.4.0" - flutter_keyboard_visibility_linux: - dependency: transitive - description: - name: flutter_keyboard_visibility_linux - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_macos: - dependency: transitive - description: - name: flutter_keyboard_visibility_macos - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_platform_interface: - dependency: transitive - description: - name: flutter_keyboard_visibility_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_web: - dependency: transitive - description: - name: flutter_keyboard_visibility_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_windows: - dependency: transitive - description: - name: flutter_keyboard_visibility_windows - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -408,6 +401,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.0.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" record: dependency: transitive description: @@ -490,6 +490,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" string_scanner: dependency: transitive description: diff --git a/lib/chat_package.dart b/lib/chat_package.dart index 2614cc2..551fa50 100644 --- a/lib/chat_package.dart +++ b/lib/chat_package.dart @@ -1,15 +1,13 @@ library chat_package; -import 'dart:developer'; - import 'package:chat_package/components/message/message_widget.dart'; +import 'package:chat_package/components/new_chat_input_field/new_chat_input_field.dart'; import 'package:chat_package/models/chat_message.dart'; import 'package:chat_package/models/media/chat_media.dart'; import 'package:chat_package/models/media/media_type.dart'; import 'package:chat_package/utils/constants.dart'; -import 'package:chat_package/components/chat_input_field/chat_input_field.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +// import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:image_picker/image_picker.dart'; class ChatScreen extends StatefulWidget { @@ -99,6 +97,27 @@ class ChatScreen extends StatefulWidget { /// this is an optional parameter to override the default attachment bottom sheet final Function(BuildContext context)? attachmentClick; + //TODO check for dublickates + //send button + final Color? sendButtonColor; + final bool? disableRecording; + final IconData? sendButtonTextIcon; + final IconData? sendButtonRecordIcon; + final Color? sendButtonIconColor; + //text field + //text field + final String? textFieldHintText; + final TextStyle? textFieldHintTextStyle; + final bool? disableCamera; + final bool? disableAttachment; + + final IconData? cameraIcon; + final IconData? attachmentIcon; + + final String? capturedMediaHintText; + + final Function(String path, String caption)? handleMediaSubmitted; + ChatScreen({ Key? key, this.senderColor, @@ -127,6 +146,23 @@ class ChatScreen extends StatefulWidget { this.messageContainerTextStyle, this.sendDateTextStyle, this.attachmentClick, + + /// + this.sendButtonColor, + this.disableRecording, + this.sendButtonRecordIcon, + this.sendButtonTextIcon, + this.sendButtonIconColor, + + ///text field + this.textFieldHintTextStyle, + this.textFieldHintText, + this.disableCamera, + this.disableAttachment, + this.cameraIcon, + this.attachmentIcon, + this.capturedMediaHintText, + this.handleMediaSubmitted, }) : super(key: key); @override @@ -138,105 +174,65 @@ class _ChatScreenState extends State { @override Widget build(BuildContext context) { - return Column( + return Stack( children: [ - Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: kDefaultPadding), - controller: widget.scrollController ?? _controller, - itemCount: widget.messages.length, - itemBuilder: (context, index) => MessageWidget( - message: widget.messages[index], - activeAudioSliderColor: - widget.activeAudioSliderColor ?? kSecondaryColor, - inActiveAudioSliderColor: - widget.inActiveAudioSliderColor ?? kLightColor, - senderColor: widget.senderColor ?? kPrimaryColor, - messageContainerTextStyle: widget.messageContainerTextStyle, - sendDateTextStyle: widget.sendDateTextStyle, - ), + ListView.builder( + padding: const EdgeInsets.only( + left: kDefaultPadding, right: kDefaultPadding, bottom: 100), + controller: widget.scrollController ?? _controller, + itemCount: widget.messages.length, + itemBuilder: (context, index) => MessageWidget( + message: widget.messages[index], + activeAudioSliderColor: + widget.activeAudioSliderColor ?? kSecondaryColor, + inActiveAudioSliderColor: + widget.inActiveAudioSliderColor ?? kLightColor, + senderColor: widget.senderColor ?? kPrimaryColor, + messageContainerTextStyle: widget.messageContainerTextStyle, + sendDateTextStyle: widget.sendDateTextStyle, ), ), - KeyboardVisibilityProvider( - child: ChatInputField( - imageAttachmentCancelText: widget.imageAttachmentCancelText, - imageAttachmentFromCameraText: widget.imageAttachmentFromCameraText, - imageAttachmentFromGalleryText: - widget.imageAttachmentFromGalleryText, - chatInputFieldColor: widget.chatInputFieldColor, - recordingNoteHintText: widget.recordingNoteHintText, - sendMessageHintText: widget.sendMessageHintText, - disableInput: widget.disableInput, - chatInputFieldDecoration: widget.chatInputFieldDecoration, - chatInputFieldPadding: widget.chatInputFieldPadding, - imageAttachmentTextStyle: widget.imageAttachmentTextStyle, - imageAttachmentFromGalleryIcon: - widget.imageAttachmentFromGalleryIcon, - imageAttachmentFromCameraIcon: widget.imageAttachmentFromCameraIcon, - imageAttachmentCancelIcon: widget.imageAttachmentCancelIcon, - attachmentClick: widget.attachmentClick, - handleRecord: widget.handleRecord ?? - (source, canceled) { - if (!canceled && source != null) { + Positioned( + bottom: 20, + left: 5, + right: 5, + child: NewChatInputField( + sendButtonColor: widget.sendButtonColor, + disableRecording: widget.disableRecording, + sendButtonRecordIcon: widget.sendButtonRecordIcon, + sendButtonTextIcon: widget.sendButtonTextIcon, + sendButtonIconColor: widget.sendButtonIconColor, + textFieldHintText: widget.textFieldHintText, + textFieldHintTextStyle: widget.textFieldHintTextStyle, + disableCamera: widget.disableCamera, + disableAttachment: widget.disableAttachment, + cameraIcon: widget.cameraIcon, + attachmentIcon: widget.attachmentIcon, + capturedMediaHintText: widget.capturedMediaHintText, + handleMediaSubmitted: widget.handleMediaSubmitted ?? + (path, caption) { setState(() { widget.messages.add( ChatMessage( isSender: true, + text: caption, chatMedia: ChatMedia( - url: source, - mediaType: MediaType.audioMediaType(), + url: path, + mediaType: MediaType.imageMediaType(), ), ), ); - widget.scrollController?.jumpTo( - widget.scrollController!.position.maxScrollExtent + - 90); }); - } - }, - handleImageSelect: widget.handleImageSelect ?? - (file) async { - // final bytes = await file.readAsBytes(); - // final image = await decodeImageFromList(bytes); - // final name = file.path.split('/').last; - setState(() { - widget.messages.add( - ChatMessage( - isSender: true, - chatMedia: ChatMedia( - url: file.path, - mediaType: MediaType.imageMediaType(), - ), - ), - ); - }); - - setState(() { - widget.scrollController?.jumpTo( - widget.scrollController!.position.maxScrollExtent + - 300); - }); - }, - onSlideToCancelRecord: widget.onSlideToCancelRecord ?? - () { - log('slide to cancel'); - }, - onTextSubmit: (text) { - if (widget.onSubmit != null) { - widget.onSubmit!(text); - } else { - if (text != null) { - setState(() { - widget.messages - .add(ChatMessage(isSender: true, text: text)); - - widget.scrollController?.jumpTo( - widget.scrollController!.position.maxScrollExtent + 50); - }); - } - } - }, - ), + + setState(() { + (widget.scrollController ?? _controller).animateTo( + (widget.scrollController ?? _controller) + .position + .maxScrollExtent, + duration: Duration(milliseconds: 300), + curve: Curves.easeOut); + }); + }), ), ], ); diff --git a/lib/components/chat_input_field/chat_input_field.dart b/lib/components/chat_input_field/chat_input_field.dart index e5758e3..c4a16f0 100644 --- a/lib/components/chat_input_field/chat_input_field.dart +++ b/lib/components/chat_input_field/chat_input_field.dart @@ -3,7 +3,6 @@ import 'package:chat_package/components/chat_input_field/widgets/chat_animated_b import 'package:chat_package/components/chat_input_field/widgets/chat_attachment_bottom_sheet.dart'; import 'package:chat_package/components/chat_input_field/widgets/chat_input_field_container_widget.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; import 'widgets/chat_drag_trail.dart'; @@ -121,8 +120,6 @@ class ChatInputField extends StatelessWidget { ), child: Consumer( builder: (context, provider, child) { - provider.isText = - KeyboardVisibilityProvider.isKeyboardVisible(context); return Padding( padding: chatInputFieldPadding ?? EdgeInsets.only(bottom: 3), child: IgnorePointer( diff --git a/lib/components/message/image_message/image_message_widget.dart b/lib/components/message/image_message/image_message_widget.dart index f9a5269..e56c196 100644 --- a/lib/components/message/image_message/image_message_widget.dart +++ b/lib/components/message/image_message/image_message_widget.dart @@ -64,6 +64,7 @@ class ImageMessageWidget extends StatelessWidget { ) : Image.file( File(message.chatMedia!.url), + fit: BoxFit.cover, ), ), ), diff --git a/lib/components/new_chat_input_field/chat_input_field_provider.dart b/lib/components/new_chat_input_field/chat_input_field_provider.dart new file mode 100644 index 0000000..9c8cb77 --- /dev/null +++ b/lib/components/new_chat_input_field/chat_input_field_provider.dart @@ -0,0 +1,66 @@ +import 'dart:developer'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; + +class NewChatInputFieldProvider extends ChangeNotifier { + final TextEditingController textFieldController; + final Function(String path, String caption) handleMediaSubmitted; + + NewChatInputFieldProvider( + {required this.textFieldController, required this.handleMediaSubmitted}); + + double scale = 0; + bool isText = false; + + void onTextFieldValueChanged(String value) { + if (value.length > 0) { + isText = true; + notifyListeners(); + } else { + isText = false; + notifyListeners(); + } + } + + void onSendButtonClicked() { + if (isText) { + // _scrollController.animateTo( + // _scrollController.position.maxScrollExtent, + // duration: Duration(milliseconds: 300), + // curve: Curves.easeOut); + + textFieldController.clear(); + + isText = false; + notifyListeners(); + } + } + + void onLongPressDragChange(double value) { + double x = -value / 200; + scale = x; + notifyListeners(); + } + + void onLongPress() { + if (!isText) { + log('start recording'); + } + } + + Future> getCameras() async { + final cameras = await availableCameras(); + return cameras; + } + + Future takePhoto(CameraController cameraController) async { + XFile file = await cameraController.takePicture(); + print(file.toString()); + return file; + } + + onSubmitMediaFromCamera(String path, String? caption) { + handleMediaSubmitted(path, caption ?? ''); + } +} diff --git a/lib/components/new_chat_input_field/new_chat_input_field.dart b/lib/components/new_chat_input_field/new_chat_input_field.dart new file mode 100644 index 0000000..73a0be1 --- /dev/null +++ b/lib/components/new_chat_input_field/new_chat_input_field.dart @@ -0,0 +1,140 @@ +import 'package:chat_package/components/new_chat_input_field/widgets/camera/camera_capture_screen.dart'; +import 'package:chat_package/components/new_chat_input_field/widgets/camera/camptured_media_view.dart'; +import 'package:chat_package/components/new_chat_input_field/widgets/send_button.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; +import 'chat_input_field_provider.dart'; +import 'widgets/text_field.dart'; + +import 'widgets/attachment_bottom_sheet.dart'; + +class NewChatInputField extends StatelessWidget { + //send button + final Color? sendButtonColor; + final bool? disableRecording; + final IconData? sendButtonTextIcon; + final IconData? sendButtonRecordIcon; + final Color? sendButtonIconColor; + //text field + final String? textFieldHintText; + final TextStyle? textFieldHintTextStyle; + final bool? disableCamera; + final bool? disableAttachment; + + final IconData? cameraIcon; + final IconData? attachmentIcon; + + final String? capturedMediaHintText; + + final Function(String path, String caption) handleMediaSubmitted; + + NewChatInputField({ + Key? key, + this.sendButtonColor, + this.disableRecording, + this.sendButtonRecordIcon, + this.sendButtonTextIcon, + this.sendButtonIconColor, + this.textFieldHintText, + this.textFieldHintTextStyle, + this.disableCamera, + this.disableAttachment, + this.cameraIcon, + this.attachmentIcon, + this.capturedMediaHintText, + required this.handleMediaSubmitted, + }) : super(key: key); + + final FocusNode focusNode = FocusNode(); + + final TextEditingController textFieldController = TextEditingController(); + // ScrollController _scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => NewChatInputFieldProvider( + textFieldController: textFieldController, + handleMediaSubmitted: handleMediaSubmitted), + child: Consumer( + builder: (context, provider, child) { + double width = + MediaQuery.of(context).size.width * (1 - provider.scale); + return AnimatedContainer( + duration: Duration(milliseconds: 100), + width: width, + child: WillPopScope( + child: Row( + children: [ + CustomTextField( + width: width * (0.85 - provider.scale), + focusNode: focusNode, + controller: textFieldController, + onChanged: provider.onTextFieldValueChanged, + onAttachmentClicked: () { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + builder: (builder) => AttachmentBottomSheet()); + }, + textFieldHintText: textFieldHintText, + textFieldHintTextStyle: textFieldHintTextStyle, + disableCamera: disableCamera, + disableAttachment: disableAttachment, + cameraIcon: cameraIcon, + attachmentIcon: attachmentIcon, + onCameraIconPressed: () async { + final cameras = await provider.getCameras(); + Navigator.push( + context, + MaterialPageRoute( + builder: (builder) => CameraCaptureScreen( + cameras: cameras, + takeImage: (controller) async { + final file = await provider.takePhoto(controller); + Navigator.push( + context, + MaterialPageRoute( + builder: (builder) => CapturedMediaView( + path: file.path, + capturedMediaHintText: + capturedMediaHintText, + onSubmitMediaFromCamera: (path, caption) { + provider.onSubmitMediaFromCamera( + path, caption); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }), + ), + ); + }, + ), + ), + ); + }, + ), + SendButton( + sendButtonColor: sendButtonColor, + onDragChange: provider.onLongPressDragChange, + onPressed: provider.onSendButtonClicked, + onLongPress: provider.onLongPress, + isText: provider.isText, + disableRecording: disableRecording ?? false, + sendButtonRecordIcon: sendButtonRecordIcon, + sendButtonTextIcon: sendButtonTextIcon, + sendButtonIconColor: sendButtonIconColor, + ), + ], + ), + onWillPop: () { + Navigator.pop(context); + return Future.value(false); + }, + ), + ); + }, + ), + ); + } +} diff --git a/lib/components/new_chat_input_field/widgets/attachment_bottom_sheet.dart b/lib/components/new_chat_input_field/widgets/attachment_bottom_sheet.dart new file mode 100644 index 0000000..c7fec5c --- /dev/null +++ b/lib/components/new_chat_input_field/widgets/attachment_bottom_sheet.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +class AttachmentBottomSheet extends StatelessWidget { + const AttachmentBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: 278, + width: MediaQuery.of(context).size.width, + child: Card( + margin: const EdgeInsets.all(18.0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + iconCreation( + Icons.insert_drive_file, Colors.indigo, "Document"), + SizedBox( + width: 40, + ), + iconCreation(Icons.camera_alt, Colors.pink, "Camera"), + SizedBox( + width: 40, + ), + iconCreation(Icons.insert_photo, Colors.purple, "Gallery"), + ], + ), + SizedBox( + height: 30, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + iconCreation(Icons.headset, Colors.orange, "Audio"), + SizedBox( + width: 40, + ), + iconCreation(Icons.location_pin, Colors.teal, "Location"), + SizedBox( + width: 40, + ), + iconCreation(Icons.person, Colors.blue, "Contact"), + ], + ), + ], + ), + ), + ), + ); + } + + Widget iconCreation(IconData icons, Color color, String text) { + return InkWell( + onTap: () {}, + child: Column( + children: [ + CircleAvatar( + radius: 30, + backgroundColor: color, + child: Icon( + icons, + // semanticLabel: "Help", + size: 29, + color: Colors.white, + ), + ), + SizedBox( + height: 5, + ), + Text( + text, + style: TextStyle( + fontSize: 12, + // fontWeight: FontWeight.w100, + ), + ) + ], + ), + ); + } +} diff --git a/lib/components/new_chat_input_field/widgets/camera/camera_capture_screen.dart b/lib/components/new_chat_input_field/widgets/camera/camera_capture_screen.dart new file mode 100644 index 0000000..d2f1201 --- /dev/null +++ b/lib/components/new_chat_input_field/widgets/camera/camera_capture_screen.dart @@ -0,0 +1,162 @@ +import 'dart:math'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; + +class CameraCaptureScreen extends StatefulWidget { + final List cameras; + final Function(CameraController cameraController) takeImage; + CameraCaptureScreen({ + Key? key, + required this.cameras, + required this.takeImage, + }) : super(key: key); + + @override + _CameraCaptureScreenState createState() => _CameraCaptureScreenState(); +} + +class _CameraCaptureScreenState extends State { + late CameraController _cameraController; + late Future cameraValue; + late bool isRecording = false; + bool flash = false; + bool isFrontCamera = true; + double transform = 0; + + @override + void initState() { + super.initState(); + _cameraController = + CameraController(widget.cameras[0], ResolutionPreset.high); + cameraValue = _cameraController.initialize(); + } + + @override + void dispose() { + super.dispose(); + _cameraController.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + FutureBuilder( + future: cameraValue, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: CameraPreview(_cameraController)); + } else { + return Center( + child: CircularProgressIndicator(), + ); + } + }), + Positioned( + bottom: 0.0, + child: Container( + color: Colors.red, + padding: EdgeInsets.only(top: 5, bottom: 5), + width: MediaQuery.of(context).size.width, + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: Icon( + flash ? Icons.flash_on : Icons.flash_off, + color: Colors.white, + size: 28, + ), + onPressed: () { + setState(() { + flash = !flash; + }); + flash + ? _cameraController + .setFlashMode(FlashMode.torch) + : _cameraController.setFlashMode(FlashMode.off); + }), + GestureDetector( + onLongPress: () async { + await _cameraController.startVideoRecording(); + setState(() { + isRecording = true; + }); + }, + onLongPressUp: () async { + // XFile videopath = + // await _cameraController.stopVideoRecording(); + // setState(() { + // isRecording = false; + // }); + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (builder) => VideoViewPage( + // path: videopath.path, + // ))); + }, + onTap: () { + if (!isRecording) widget.takeImage(_cameraController); + }, + child: isRecording + ? Icon( + Icons.radio_button_on, + color: Colors.red, + size: 80, + ) + : Icon( + Icons.panorama_fish_eye, + color: Colors.white, + size: 70, + ), + ), + IconButton( + icon: Transform.rotate( + angle: transform, + child: Icon( + Icons.flip_camera_ios, + color: Colors.white, + size: 28, + ), + ), + onPressed: () async { + setState(() { + isFrontCamera = !isFrontCamera; + transform = transform + pi; + }); + int cameraPos = isFrontCamera ? 0 : 1; + _cameraController = CameraController( + widget.cameras[cameraPos], + ResolutionPreset.high); + cameraValue = _cameraController.initialize(); + }), + ], + ), + SizedBox( + height: 4, + ), + Text( + "Hold for Video, tap for photo", + style: TextStyle( + color: Colors.white, + ), + textAlign: TextAlign.center, + ) + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/components/new_chat_input_field/widgets/camera/camptured_media_view.dart b/lib/components/new_chat_input_field/widgets/camera/camptured_media_view.dart new file mode 100644 index 0000000..d44e6b1 --- /dev/null +++ b/lib/components/new_chat_input_field/widgets/camera/camptured_media_view.dart @@ -0,0 +1,107 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +class CapturedMediaView extends StatelessWidget { + final String path; + final String? capturedMediaHintText; + final Function(String path, String? caption) onSubmitMediaFromCamera; + + CapturedMediaView( + {Key? key, + required this.path, + required this.onSubmitMediaFromCamera, + this.capturedMediaHintText}) + : super(key: key); + String caption = ''; + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + // actions: [ + // IconButton( + // icon: Icon( + // Icons.crop_rotate, + // size: 27, + // ), + // onPressed: () {}), + // IconButton( + // icon: Icon( + // Icons.emoji_emotions_outlined, + // size: 27, + // ), + // onPressed: () {}), + // IconButton( + // icon: Icon( + // Icons.title, + // size: 27, + // ), + // onPressed: () {}), + // IconButton( + // icon: Icon( + // Icons.edit, + // size: 27, + // ), + // onPressed: () {}), + // ], + ), + body: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: Stack( + children: [ + Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height - 150, + child: Image.file( + File(path), + fit: BoxFit.cover, + ), + ), + Positioned( + bottom: 0, + child: Container( + color: Colors.black38, + width: MediaQuery.of(context).size.width, + padding: EdgeInsets.symmetric(vertical: 5, horizontal: 8), + child: TextFormField( + style: TextStyle( + color: Colors.white, + fontSize: 17, + ), + maxLines: 6, + minLines: 1, + onChanged: (value) { + caption = value; + }, + decoration: InputDecoration( + border: InputBorder.none, + hintText: capturedMediaHintText ?? 'Add Caption....', + hintStyle: TextStyle( + color: Colors.white, + fontSize: 17, + ), + suffixIcon: CircleAvatar( + radius: 27, + backgroundColor: Colors.tealAccent[700], + child: IconButton( + onPressed: () { + onSubmitMediaFromCamera(path, caption); + }, + icon: Icon( + Icons.check, + color: Colors.white, + size: 27, + )), + )), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/new_chat_input_field/widgets/send_button.dart b/lib/components/new_chat_input_field/widgets/send_button.dart new file mode 100644 index 0000000..9290ca6 --- /dev/null +++ b/lib/components/new_chat_input_field/widgets/send_button.dart @@ -0,0 +1,64 @@ +import 'package:chat_package/utils/constants.dart'; +import 'package:flutter/material.dart'; + +class SendButton extends StatelessWidget { + final Color? sendButtonColor; + final bool disableRecording; + final IconData? sendButtonTextIcon; + final IconData? sendButtonRecordIcon; + final Color? sendButtonIconColor; + + final bool isText; + final Function() onPressed; + final Function() onLongPress; + final Function(double) onDragChange; + const SendButton({ + Key? key, + required this.isText, + required this.onPressed, + required this.onLongPress, + required this.onDragChange, + this.sendButtonColor = kPrimaryColor, + this.disableRecording = false, + this.sendButtonRecordIcon, + this.sendButtonTextIcon, + this.sendButtonIconColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed, + onLongPressMoveUpdate: (details) { + if (!isText && + details.localPosition.dx < 0 && + details.localPosition.dx >= -20 && + !disableRecording) { + onDragChange(details.localPosition.dx); + } + }, + onLongPressEnd: (details) { + if (!disableRecording) { + onDragChange(0); + } + }, + onLongPress: () { + if (!isText && !disableRecording) { + onLongPress(); + } + }, + child: CircleAvatar( + radius: 25, + backgroundColor: sendButtonColor, + child: Icon( + isText + ? (sendButtonTextIcon ?? Icons.send) + : (!disableRecording + ? (sendButtonRecordIcon ?? Icons.mic) + : (sendButtonTextIcon ?? Icons.send)), + color: sendButtonIconColor ?? Colors.white, + ), + ), + ); + } +} diff --git a/lib/components/new_chat_input_field/widgets/text_field.dart b/lib/components/new_chat_input_field/widgets/text_field.dart new file mode 100644 index 0000000..b93aa41 --- /dev/null +++ b/lib/components/new_chat_input_field/widgets/text_field.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; + +class CustomTextField extends StatelessWidget { + final String? textFieldHintText; + final TextStyle? textFieldHintTextStyle; + final bool? disableCamera; + final bool? disableAttachment; + + final IconData? cameraIcon; + final IconData? attachmentIcon; + + final Function() onCameraIconPressed; + + final TextEditingController controller; + final FocusNode focusNode; + final Function(String)? onChanged; + final Function()? onAttachmentClicked; + final double width; + const CustomTextField({ + super.key, + required this.controller, + required this.focusNode, + required this.onChanged, + required this.onAttachmentClicked, + required this.width, + this.textFieldHintText, + required this.textFieldHintTextStyle, + required this.disableCamera, + required this.disableAttachment, + required this.cameraIcon, + required this.attachmentIcon, + required this.onCameraIconPressed, + }); + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: Duration(milliseconds: 100), + width: width, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + ), + child: TextFormField( + controller: controller, + focusNode: focusNode, + textAlignVertical: TextAlignVertical.center, + keyboardType: TextInputType.multiline, + maxLines: 5, + minLines: 1, + onChanged: onChanged, + decoration: InputDecoration( + border: InputBorder.none, + hintText: textFieldHintText ?? 'Type a message', + hintStyle: textFieldHintTextStyle ?? TextStyle(color: Colors.grey), + prefixIcon: Icon( + Icons.keyboard, + ), + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + disableAttachment ?? false + ? Container() + : IconButton( + icon: Icon(attachmentIcon ?? Icons.attach_file), + onPressed: onAttachmentClicked, + ), + disableCamera ?? false + ? Container() + : IconButton( + icon: Icon(cameraIcon ?? Icons.camera_alt), + onPressed: onCameraIconPressed, + ), + ], + ), + contentPadding: EdgeInsets.all(5), + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index ac07d37..c7136c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -99,6 +99,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "8.4.2" + camera: + dependency: "direct main" + description: + name: camera + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.1" + camera_android: + dependency: transitive + description: + name: camera_android + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.2" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.10" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.2" + camera_web: + dependency: transitive + description: + name: camera_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1" characters: dependency: transitive description: @@ -202,48 +237,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_keyboard_visibility: - dependency: "direct main" - description: - name: flutter_keyboard_visibility - url: "https://pub.dartlang.org" - source: hosted - version: "5.4.0" - flutter_keyboard_visibility_linux: - dependency: transitive - description: - name: flutter_keyboard_visibility_linux - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_macos: - dependency: transitive - description: - name: flutter_keyboard_visibility_macos - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_platform_interface: - dependency: transitive - description: - name: flutter_keyboard_visibility_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_web: - dependency: transitive - description: - name: flutter_keyboard_visibility_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_windows: - dependency: transitive - description: - name: flutter_keyboard_visibility_windows - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" flutter_lints: dependency: "direct dev" description: @@ -604,6 +597,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.1" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" record: dependency: "direct main" description: @@ -800,4 +800,4 @@ packages: version: "3.1.1" sdks: dart: ">=2.18.1 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index d91ddce..aeabefe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - flutter_keyboard_visibility: ^5.4.0 + # flutter_keyboard_visibility: ^5.4.0 permission_handler: ^10.2.0 image_picker: ^0.8.6 just_audio: ^0.9.30 @@ -20,6 +20,7 @@ dependencies: stop_watch_timer: ^2.0.0 provider: ^6.0.4 freezed_annotation: ^2.2.0 + camera: ^0.10.1 dev_dependencies: flutter_test: