Add initial camera functionality and improve attachment visuals
This commit is contained in:
parent
3c4a4fb80b
commit
52d6f40d82
4 changed files with 308 additions and 46 deletions
83
lib/widgets/messages/message_camera_view.dart
Normal file
83
lib/widgets/messages/message_camera_view.dart
Normal 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(),);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<MessagesList> 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<MessagesList> 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<MessagesList> 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),
|
||||
),
|
||||
|
|
66
pubspec.lock
66
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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue