Add initial camera functionality and improve attachment visuals

This commit is contained in:
Nutcake 2023-05-18 11:53:32 +02:00
parent 3c4a4fb80b
commit 52d6f40d82
4 changed files with 308 additions and 46 deletions

View file

@ -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<StatefulWidget> createState() => _MessageCameraViewState();
}
class _MessageCameraViewState extends State<MessageCameraView> {
final List<CameraDescription> _cameras = [];
late final CameraController _cameraController;
Future? _initializeControllerFuture;
@override
void initState() {
super.initState();
availableCameras().then((List<CameraDescription> 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(),);
}
},
),
);
}
}

View file

@ -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/models/message.dart';
import 'package:contacts_plus_plus/widgets/default_error_widget.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/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:contacts_plus_plus/widgets/messages/messages_session_header.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -286,7 +287,8 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
switchOutCurve: Curves.easeOut, switchOutCurve: Curves.easeOut,
transitionBuilder: (Widget child, animation) => SizeTransition(sizeFactor: animation, child: child,), transitionBuilder: (Widget child, animation) => SizeTransition(sizeFactor: animation, child: child,),
child: switch ((_attachmentPickerOpen, _loadedFiles)) { child: switch ((_attachmentPickerOpen, _loadedFiles)) {
(true, []) => Row( (true, []) =>
Row(
key: const ValueKey("attachment-picker"), key: const ValueKey("attachment-picker"),
children: [ children: [
TextButton.icon( TextButton.icon(
@ -301,32 +303,117 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
icon: const Icon(Icons.image), icon: const Icon(Icons.image),
label: const Text("Gallery"), label: const Text("Gallery"),
), ),
TextButton.icon(onPressed: _isSending ? null : (){}, icon: const Icon(Icons.camera), label: const Text("Camera"),), 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, (false, []) => null,
(_, _) => Row( (_, _) =>
Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: [ children: [
Expanded( 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( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
children: _loadedFiles.map((e) => TextButton.icon(onPressed: _isSending ? null : (){}, label: Text(basename(e.path)), icon: const Icon(Icons.attach_file))).toList() 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 { ),
IconButton(
onPressed: _isSending ? null : () async {
final result = await FilePicker.platform.pickFiles(type: FileType.image); final result = await FilePicker.platform.pickFiles(type: FileType.image);
if (result != null && result.files.single.path != null) { if (result != null && result.files.single.path != null) {
setState(() { setState(() {
_loadedFiles.add(File(result.files.single.path!)); _loadedFiles.add(File(result.files.single.path!));
}); });
} }
}, icon: const Icon(Icons.image)), },
IconButton(onPressed: _isSending ? null : () {}, icon: const Icon(Icons.camera)), 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),
),
], ],
) ),
}, },
), ),
), ),
@ -383,11 +470,35 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
) : ) :
IconButton( IconButton(
key: const ValueKey("remove-attachment-icon"), key: const ValueKey("remove-attachment-icon"),
onPressed: _isSending ? null : () { 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(() { setState(() {
_loadedFiles.clear(); _loadedFiles.clear();
_attachmentPickerOpen = false; _attachmentPickerOpen = false;
}); });
Navigator.of(context).pop();
},
child: const Text("Yes"),
)
],
));
} else {
setState(() {
_attachmentPickerOpen = false;
});
}
}, },
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
), ),
@ -481,7 +592,9 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
) : IconButton( ) : IconButton(
key: const ValueKey("mic-button"), key: const ValueKey("mic-button"),
splashRadius: 24, splashRadius: 24,
onPressed: _isSending ? null : () async {}, onPressed: _isSending ? null : () async {
// TODO: Implement voice message recording
},
iconSize: 28, iconSize: 28,
icon: const Icon(Icons.mic_outlined), icon: const Icon(Icons.mic_outlined),
), ),

View file

@ -57,6 +57,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" 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: characters:
dependency: transitive dependency: transitive
description: description:
@ -89,6 +129,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" 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: crypto:
dependency: transitive dependency: transitive
description: description:
@ -457,7 +505,7 @@ packages:
source: hosted source: hosted
version: "1.8.3" version: "1.8.3"
path_provider: path_provider:
dependency: transitive dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
@ -560,6 +608,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.5" version: "6.0.5"
quiver:
dependency: transitive
description:
name: quiver
sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47
url: "https://pub.dev"
source: hosted
version: "3.2.1"
record: record:
dependency: "direct main" dependency: "direct main"
description: description:
@ -677,6 +733,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" 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: string_scanner:
dependency: transitive dependency: transitive
description: description:

View file

@ -58,6 +58,8 @@ dependencies:
hive_flutter: ^1.1.0 hive_flutter: ^1.1.0
file_picker: ^5.3.0 file_picker: ^5.3.0
record: ^4.4.4 record: ^4.4.4
camera: ^0.10.5
path_provider: ^2.0.15
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: