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/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),
|
||||||
),
|
),
|
||||||
|
|
66
pubspec.lock
66
pubspec.lock
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue