From 52d6f40d82615d0de29c07b4ae5dc7bb58300679 Mon Sep 17 00:00:00 2001 From: Nutcake Date: Thu, 18 May 2023 11:53:32 +0200 Subject: [PATCH] Add initial camera functionality and improve attachment visuals --- lib/widgets/messages/message_camera_view.dart | 83 +++++++ lib/widgets/messages/messages_list.dart | 203 ++++++++++++++---- pubspec.lock | 66 +++++- pubspec.yaml | 2 + 4 files changed, 308 insertions(+), 46 deletions(-) create mode 100644 lib/widgets/messages/message_camera_view.dart diff --git a/lib/widgets/messages/message_camera_view.dart b/lib/widgets/messages/message_camera_view.dart new file mode 100644 index 0000000..2576644 --- /dev/null +++ b/lib/widgets/messages/message_camera_view.dart @@ -0,0 +1,83 @@ +import 'dart:io'; + +import 'package:camera/camera.dart'; +import 'package:contacts_plus_plus/widgets/default_error_widget.dart'; +import 'package:flutter/material.dart'; + +class MessageCameraView extends StatefulWidget { + const MessageCameraView({super.key}); + + @override + State createState() => _MessageCameraViewState(); + +} + +class _MessageCameraViewState extends State { + final List _cameras = []; + late final CameraController _cameraController; + Future? _initializeControllerFuture; + + @override + void initState() { + super.initState(); + availableCameras().then((List cameras) { + _cameras.clear(); + _cameras.addAll(cameras); + _cameraController = CameraController(cameras.first, ResolutionPreset.high); + setState(() { + _initializeControllerFuture = _cameraController.initialize(); + }); + }); + } + + @override + void dispose() { + _cameraController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Take a picture"), + ), + body: FutureBuilder( + future: _initializeControllerFuture, + builder: (context, snapshot) { + // Can't use hasData since the future returns void. + if (snapshot.connectionState == ConnectionState.done) { + return Column( + children: [ + Expanded(child: CameraPreview(_cameraController)), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton(onPressed: () async { + final sMsgr = ScaffoldMessenger.of(context); + final nav = Navigator.of(context); + try { + await _initializeControllerFuture; + final image = await _cameraController.takePicture(); + nav.pop(File(image.path)); + } catch (e) { + sMsgr.showSnackBar(SnackBar(content: Text("Failed to capture image: $e"))); + } + }, icon: const Icon(Icons.circle_outlined)) + ], + ) + ], + ); + } else if (snapshot.hasError) { + return DefaultErrorWidget( + message: snapshot.error.toString(), + ); + } else { + return const Center(child: CircularProgressIndicator(),); + } + }, + ), + ); + } + +} diff --git a/lib/widgets/messages/messages_list.dart b/lib/widgets/messages/messages_list.dart index 3ae1dc2..2d2789d 100644 --- a/lib/widgets/messages/messages_list.dart +++ b/lib/widgets/messages/messages_list.dart @@ -9,6 +9,7 @@ import 'package:contacts_plus_plus/models/friend.dart'; import 'package:contacts_plus_plus/models/message.dart'; import 'package:contacts_plus_plus/widgets/default_error_widget.dart'; import 'package:contacts_plus_plus/widgets/friends/friend_online_status_indicator.dart'; +import 'package:contacts_plus_plus/widgets/messages/message_camera_view.dart'; import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -286,47 +287,133 @@ class _MessagesListState extends State with SingleTickerProviderSt switchOutCurve: Curves.easeOut, transitionBuilder: (Widget child, animation) => SizeTransition(sizeFactor: animation, child: child,), child: switch ((_attachmentPickerOpen, _loadedFiles)) { - (true, []) => Row( - key: const ValueKey("attachment-picker"), - children: [ - TextButton.icon( - onPressed: _isSending ? null : () async { - final result = await FilePicker.platform.pickFiles(type: FileType.image); - if (result != null && result.files.single.path != null) { - setState(() { - _loadedFiles.add(File(result.files.single.path!)); - }); - } - }, - icon: const Icon(Icons.image), - label: const Text("Gallery"), - ), - TextButton.icon(onPressed: _isSending ? null : (){}, icon: const Icon(Icons.camera), label: const Text("Camera"),), - ], - ), - (false, []) => null, - (_, _) => Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: _loadedFiles.map((e) => TextButton.icon(onPressed: _isSending ? null : (){}, label: Text(basename(e.path)), icon: const Icon(Icons.attach_file))).toList() + (true, []) => + Row( + key: const ValueKey("attachment-picker"), + children: [ + TextButton.icon( + onPressed: _isSending ? null : () async { + final result = await FilePicker.platform.pickFiles(type: FileType.image); + if (result != null && result.files.single.path != null) { + setState(() { + _loadedFiles.add(File(result.files.single.path!)); + }); + } + }, + icon: const Icon(Icons.image), + label: const Text("Gallery"), ), - ), + TextButton.icon( + onPressed: _isSending ? null : () async { + final picture = await Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const MessageCameraView())); + if (picture != null) { + setState(() { + _loadedFiles.add(picture); + }); + } + }, + icon: const Icon(Icons.camera_alt), + label: const Text("Camera"), + ), + ], + ), + (false, []) => null, + (_, _) => + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: ShaderMask( + shaderCallback: (Rect bounds) { + return LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [Colors.transparent, Colors.transparent, Colors.transparent, Theme.of(context).colorScheme.background], + stops: const [0.0, 0.1, 0.9, 1.0], // 10% purple, 80% transparent, 10% purple + ).createShader(bounds); + }, + blendMode: BlendMode.dstOut, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: _loadedFiles.map((file) => + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: TextButton.icon( + onPressed: _isSending ? null : () { + showDialog(context: context, builder: (context) => + AlertDialog( + title: const Text("Remove attachment"), + content: Text( + "This will remove attachment '${basename( + file.path)}', are you sure?"), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("No"), + ), + TextButton( + onPressed: () { + setState(() { + _loadedFiles.remove(file); + }); + Navigator.of(context).pop(); + }, + child: const Text("Yes"), + ) + ], + )); + }, + style: TextButton.styleFrom( + foregroundColor: Theme + .of(context) + .colorScheme + .onBackground, + side: BorderSide( + color: Theme + .of(context) + .colorScheme + .primary, + width: 1 + ), + ), + label: Text(basename(file.path)), + icon: const Icon(Icons.attach_file), + ), + ), + ).toList() + ), + ), + ), + ), + IconButton( + onPressed: _isSending ? null : () async { + final result = await FilePicker.platform.pickFiles(type: FileType.image); + if (result != null && result.files.single.path != null) { + setState(() { + _loadedFiles.add(File(result.files.single.path!)); + }); + } + }, + icon: const Icon(Icons.add_photo_alternate), + ), + IconButton( + onPressed: _isSending ? null : () async { + final picture = await Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const MessageCameraView())); + if (picture != null) { + setState(() { + _loadedFiles.add(picture); + }); + } + }, + icon: const Icon(Icons.add_a_photo), + ), + ], ), - IconButton(onPressed: _isSending ? null : () async { - final result = await FilePicker.platform.pickFiles(type: FileType.image); - if (result != null && result.files.single.path != null) { - setState(() { - _loadedFiles.add(File(result.files.single.path!)); - }); - } - }, icon: const Icon(Icons.image)), - IconButton(onPressed: _isSending ? null : () {}, icon: const Icon(Icons.camera)), - ], - ) }, ), ), @@ -383,11 +470,35 @@ class _MessagesListState extends State with SingleTickerProviderSt ) : IconButton( key: const ValueKey("remove-attachment-icon"), - onPressed: _isSending ? null : () { - setState(() { - _loadedFiles.clear(); - _attachmentPickerOpen = false; - }); + onPressed: _isSending ? null : () async { + if (_loadedFiles.isNotEmpty) { + await showDialog(context: context, builder: (context) => AlertDialog( + title: const Text("Remove all attachments"), + content: const Text("This will remove all attachments, are you sure?"), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("No"), + ), + TextButton( + onPressed: () { + setState(() { + _loadedFiles.clear(); + _attachmentPickerOpen = false; + }); + Navigator.of(context).pop(); + }, + child: const Text("Yes"), + ) + ], + )); + } else { + setState(() { + _attachmentPickerOpen = false; + }); + } }, icon: const Icon(Icons.close), ), @@ -481,7 +592,9 @@ class _MessagesListState extends State with SingleTickerProviderSt ) : IconButton( key: const ValueKey("mic-button"), splashRadius: 24, - onPressed: _isSending ? null : () async {}, + onPressed: _isSending ? null : () async { + // TODO: Implement voice message recording + }, iconSize: 28, icon: const Icon(Icons.mic_outlined), ), diff --git a/pubspec.lock b/pubspec.lock index 251bc5c..72ba257 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + camera: + dependency: "direct main" + description: + name: camera + sha256: "309b823e61f15ff6b5b2e4c0ff2e1512ea661cad5355f71fc581e510ae5b26bb" + url: "https://pub.dev" + source: hosted + version: "0.10.5" + camera_android: + dependency: transitive + description: + name: camera_android + sha256: "61bbae4af0204b9bbfd82182e313d405abf5a01bdb057ff6675f2269a5cab4fd" + url: "https://pub.dev" + source: hosted + version: "0.10.8+1" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "7ac8b950672716722af235eed7a7c37896853669800b7da706bb0a9fd41d3737" + url: "https://pub.dev" + source: hosted + version: "0.9.13+1" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "525017018d116c5db8c4c43ec2d9b1663216b369c9f75149158280168a7ce472" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: d77965f32479ee6d8f48205dcf10f845d7210595c6c00faa51eab265d1cae993 + url: "https://pub.dev" + source: hosted + version: "0.3.1+3" characters: dependency: transitive description: @@ -89,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + url: "https://pub.dev" + source: hosted + version: "0.3.3+4" crypto: dependency: transitive description: @@ -457,7 +505,7 @@ packages: source: hosted version: "1.8.3" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" @@ -560,6 +608,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.5" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" record: dependency: "direct main" description: @@ -677,6 +733,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" string_scanner: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8d0bc76..9a75ed4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,8 @@ dependencies: hive_flutter: ^1.1.0 file_picker: ^5.3.0 record: ^4.4.4 + camera: ^0.10.5 + path_provider: ^2.0.15 dev_dependencies: flutter_test: