Split attachment list into separate file
This commit is contained in:
parent
52d6f40d82
commit
0e15b3c387
2 changed files with 160 additions and 103 deletions
140
lib/widgets/messages/message_attachment_list.dart
Normal file
140
lib/widgets/messages/message_attachment_list.dart
Normal file
|
@ -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<File>? initialFiles;
|
||||||
|
final Function(List<File> files) onChange;
|
||||||
|
final bool disabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MessageAttachmentList> createState() => _MessageAttachmentListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessageAttachmentListState extends State<MessageAttachmentList> {
|
||||||
|
final List<File> _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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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/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_attachment_list.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/messages/message_camera_view.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';
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'message_bubble.dart';
|
import 'message_bubble.dart';
|
||||||
|
@ -36,7 +36,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
bool _hasText = false;
|
bool _hasText = false;
|
||||||
bool _isSending = false;
|
bool _isSending = false;
|
||||||
bool _attachmentPickerOpen = false;
|
bool _attachmentPickerOpen = false;
|
||||||
double _sendProgress = 0;
|
double? _sendProgress;
|
||||||
|
|
||||||
bool _showBottomBarShadow = false;
|
bool _showBottomBarShadow = false;
|
||||||
bool _showSessionListScrollChevron = false;
|
bool _showSessionListScrollChevron = false;
|
||||||
|
@ -319,108 +319,21 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(false, []) => null,
|
(false, []) => null,
|
||||||
(_, _) =>
|
(_, _) => MessageAttachmentList(
|
||||||
Row(
|
disabled: _isSending,
|
||||||
mainAxisSize: MainAxisSize.max,
|
initialFiles: _loadedFiles,
|
||||||
children: [
|
onChange: (List<File> loadedFiles) => setState(() {
|
||||||
Expanded(
|
_loadedFiles.clear();
|
||||||
child: ShaderMask(
|
_loadedFiles.addAll(loadedFiles);
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_isSending && _loadedFiles.isNotEmpty)
|
if (_isSending && _sendProgress != null)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: LinearProgressIndicator(value: _sendProgress),
|
child: LinearProgressIndicator(value: _sendProgress),
|
||||||
|
@ -548,14 +461,17 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
onPressed: _isSending ? null : () async {
|
onPressed: _isSending ? null : () async {
|
||||||
final sMsgnr = ScaffoldMessenger.of(context);
|
final sMsgnr = ScaffoldMessenger.of(context);
|
||||||
|
final toSend = List.from(_loadedFiles);
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSending = true;
|
_isSending = true;
|
||||||
_sendProgress = 0;
|
_sendProgress = 0;
|
||||||
|
_attachmentPickerOpen = false;
|
||||||
|
_loadedFiles.clear();
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
for (int i = 0; i < _loadedFiles.length; i++) {
|
for (int i = 0; i < toSend.length; i++) {
|
||||||
final totalProgress = i/_loadedFiles.length;
|
final totalProgress = i/toSend.length;
|
||||||
final file = _loadedFiles[i];
|
final file = toSend[i];
|
||||||
await sendImageMessage(apiClient, mClient, file, ClientHolder
|
await sendImageMessage(apiClient, mClient, file, ClientHolder
|
||||||
.of(context)
|
.of(context)
|
||||||
.settingsClient
|
.settingsClient
|
||||||
|
@ -564,13 +480,13 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
.valueOrDefault,
|
.valueOrDefault,
|
||||||
(progress) =>
|
(progress) =>
|
||||||
setState(() {
|
setState(() {
|
||||||
_sendProgress = totalProgress + progress * 1/_loadedFiles.length;
|
_sendProgress = totalProgress + progress * 1/toSend.length;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_sendProgress = 1;
|
_sendProgress = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_hasText) {
|
if (_hasText) {
|
||||||
|
@ -585,6 +501,7 @@ class _MessagesListState extends State<MessagesList> with SingleTickerProviderSt
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSending = false;
|
_isSending = false;
|
||||||
|
_sendProgress = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
iconSize: 28,
|
iconSize: 28,
|
||||||
|
|
Loading…
Reference in a new issue