diff --git a/lib/widgets/messages/message_attachment_list.dart b/lib/widgets/messages/message_attachment_list.dart new file mode 100644 index 0000000..297733e --- /dev/null +++ b/lib/widgets/messages/message_attachment_list.dart @@ -0,0 +1,140 @@ +import 'dart:io'; + +import 'package:contacts_plus_plus/widgets/messages/message_camera_view.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:path/path.dart'; + +class MessageAttachmentList extends StatefulWidget { + const MessageAttachmentList({required this.onChange, required this.disabled, this.initialFiles, super.key}); + + final List? initialFiles; + final Function(List files) onChange; + final bool disabled; + + @override + State createState() => _MessageAttachmentListState(); +} + +class _MessageAttachmentListState extends State { + final List _loadedFiles = []; + final ScrollController _scrollController = ScrollController(); + bool _showShadow = true; + + @override + void initState() { + super.initState(); + _loadedFiles.clear(); + _loadedFiles.addAll(widget.initialFiles ?? []); + _scrollController.addListener(() { + if (_scrollController.position.maxScrollExtent > 0 && !_showShadow) { + setState(() { + _showShadow = true; + }); + } + if (_scrollController.position.atEdge && _scrollController.position.pixels > 0 + && _showShadow) { + setState(() { + _showShadow = false; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return 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: [0.0, 0.0, _showShadow ? 0.96 : 1.0, 1.0], // 10% purple, 80% transparent, 10% purple + ).createShader(bounds); + }, + blendMode: BlendMode.dstOut, + child: SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + child: Row( + children: _loadedFiles.map((file) => + Padding( + padding: const EdgeInsets.only(left: 4.0, right: 4.0, top: 4.0), + child: TextButton.icon( + onPressed: widget.disabled ? 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: () async { + Navigator.of(context).pop(); + _loadedFiles.remove(file); + await widget.onChange(_loadedFiles); + }, + 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: widget.disabled ? null : () async { + final result = await FilePicker.platform.pickFiles(type: FileType.image); + if (result != null && result.files.single.path != null) { + _loadedFiles.add(File(result.files.single.path!)); + await widget.onChange(_loadedFiles); + } + }, + icon: const Icon(Icons.add_photo_alternate), + ), + IconButton( + onPressed: widget.disabled ? null : () async { + final picture = await Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const MessageCameraView())); + if (picture != null) { + _loadedFiles.add(picture); + await widget.onChange(_loadedFiles); + } + }, + icon: const Icon(Icons.add_a_photo), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/messages/messages_list.dart b/lib/widgets/messages/messages_list.dart index 2d2789d..88e1cc8 100644 --- a/lib/widgets/messages/messages_list.dart +++ b/lib/widgets/messages/messages_list.dart @@ -9,11 +9,11 @@ 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_attachment_list.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'; -import 'package:path/path.dart'; import 'package:provider/provider.dart'; import 'message_bubble.dart'; @@ -36,7 +36,7 @@ class _MessagesListState extends State with SingleTickerProviderSt bool _hasText = false; bool _isSending = false; bool _attachmentPickerOpen = false; - double _sendProgress = 0; + double? _sendProgress; bool _showBottomBarShadow = false; bool _showSessionListScrollChevron = false; @@ -319,108 +319,21 @@ class _MessagesListState extends State with SingleTickerProviderSt ], ), (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), - ), - ], - ), + (_, _) => MessageAttachmentList( + disabled: _isSending, + initialFiles: _loadedFiles, + onChange: (List loadedFiles) => setState(() { + _loadedFiles.clear(); + _loadedFiles.addAll(loadedFiles); + }), + ) }, ), ), ], ), ), - if (_isSending && _loadedFiles.isNotEmpty) + if (_isSending && _sendProgress != null) Align( alignment: Alignment.bottomCenter, child: LinearProgressIndicator(value: _sendProgress), @@ -548,14 +461,17 @@ class _MessagesListState extends State with SingleTickerProviderSt splashRadius: 24, onPressed: _isSending ? null : () async { final sMsgnr = ScaffoldMessenger.of(context); + final toSend = List.from(_loadedFiles); setState(() { _isSending = true; _sendProgress = 0; + _attachmentPickerOpen = false; + _loadedFiles.clear(); }); try { - for (int i = 0; i < _loadedFiles.length; i++) { - final totalProgress = i/_loadedFiles.length; - final file = _loadedFiles[i]; + for (int i = 0; i < toSend.length; i++) { + final totalProgress = i/toSend.length; + final file = toSend[i]; await sendImageMessage(apiClient, mClient, file, ClientHolder .of(context) .settingsClient @@ -564,13 +480,13 @@ class _MessagesListState extends State with SingleTickerProviderSt .valueOrDefault, (progress) => setState(() { - _sendProgress = totalProgress + progress * 1/_loadedFiles.length; + _sendProgress = totalProgress + progress * 1/toSend.length; }), ); } setState(() { - _sendProgress = 1; + _sendProgress = null; }); if (_hasText) { @@ -585,6 +501,7 @@ class _MessagesListState extends State with SingleTickerProviderSt } setState(() { _isSending = false; + _sendProgress = null; }); }, iconSize: 28,